Initial implementation of the reset identity feature
This commit is contained in:
parent
2b7f78255f
commit
e16057a366
23 changed files with 1003 additions and 68 deletions
|
|
@ -58,6 +58,9 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object EnterRecoveryKey : NavTarget
|
data object EnterRecoveryKey : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object ResetIdentity : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
|
|
@ -85,6 +88,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
plugins<Callback>().forEach { it.onDone() }
|
plugins<Callback>().forEach { it.onDone() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResetKey() {
|
||||||
|
backstack.push(NavTarget.ResetIdentity)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
@ -94,6 +101,16 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
.callback(secureBackupEntryPointCallback)
|
.callback(secureBackupEntryPointCallback)
|
||||||
.build()
|
.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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ interface SecureBackupEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object CreateNewRecoveryKey : InitialTarget
|
data object CreateNewRecoveryKey : InitialTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object ResetIdentity : InitialTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Params(val initialElement: InitialTarget) : NodeInputs
|
data class Params(val initialElement: InitialTarget) : NodeInputs
|
||||||
|
|
|
||||||
|
|
@ -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.disable.SecureBackupDisableNode
|
||||||
import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode
|
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.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.root.SecureBackupRootNode
|
||||||
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode
|
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
|
@ -48,10 +49,11 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
) : BaseFlowNode<SecureBackupFlowNode.NavTarget>(
|
) : BaseFlowNode<SecureBackupFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
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.Root -> NavTarget.Root
|
||||||
SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey
|
SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey
|
||||||
SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey -> NavTarget.CreateNewRecoveryKey
|
SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey -> NavTarget.CreateNewRecoveryKey
|
||||||
|
is SecureBackupEntryPoint.InitialTarget.ResetIdentity -> NavTarget.ResetIdentity
|
||||||
},
|
},
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
|
|
@ -79,6 +81,9 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object CreateNewRecoveryKey : NavTarget
|
data object CreateNewRecoveryKey : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object ResetIdentity : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callbacks = plugins<SecureBackupEntryPoint.Callback>()
|
private val callbacks = plugins<SecureBackupEntryPoint.Callback>()
|
||||||
|
|
@ -146,6 +151,14 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||||
NavTarget.CreateNewRecoveryKey -> {
|
NavTarget.CreateNewRecoveryKey -> {
|
||||||
createNode<CreateNewRecoveryKeyNode>(buildContext)
|
createNode<CreateNewRecoveryKeyNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
is NavTarget.ResetIdentity -> {
|
||||||
|
val callback = object : ResetIdentityFlowNode.Callback {
|
||||||
|
override fun onDone() {
|
||||||
|
callbacks.forEach { it.onDone() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<ResetIdentityFlowNode>(buildContext, listOf(callback))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.SessionScope
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
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.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
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
|
||||||
|
|
||||||
|
fun whenResetIsDone(block: () -> Unit) {
|
||||||
|
sessionCoroutineScope.launch {
|
||||||
|
sessionVerificationService.sessionVerifiedStatus.filterIsInstance<SessionVerifiedStatus.Verified>().first()
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentSessionId(): SessionId {
|
||||||
|
return matrixClient.sessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* 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.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
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.ResetKeyPasswordNode
|
||||||
|
import io.element.android.features.securebackup.impl.reset.root.ResetKeyRootNode
|
||||||
|
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.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.matrix.api.encryption.IdentityResetHandle
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class ResetIdentityFlowNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val resetIdentityFlowManager: ResetIdentityFlowManager,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
) : 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
|
||||||
|
|
||||||
|
override fun onBuilt() {
|
||||||
|
super.onBuilt()
|
||||||
|
|
||||||
|
resetIdentityFlowManager.whenResetIsDone {
|
||||||
|
plugins<Callback>().forEach { it.onDone() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
is NavTarget.Root -> {
|
||||||
|
val callback = object : ResetKeyRootNode.Callback {
|
||||||
|
override fun onContinue() {
|
||||||
|
coroutineScope.startReset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<ResetKeyRootNode>(buildContext, listOf(callback))
|
||||||
|
}
|
||||||
|
is NavTarget.ResetPassword -> {
|
||||||
|
val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found")
|
||||||
|
createNode<ResetKeyPasswordNode>(
|
||||||
|
buildContext,
|
||||||
|
listOf(ResetKeyPasswordNode.Inputs(resetIdentityFlowManager.currentSessionId(), handle))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.startReset() = launch {
|
||||||
|
val handle = resetIdentityFlowManager.getResetHandle()
|
||||||
|
.filterIsInstance<AsyncData.Success<IdentityResetHandle>>()
|
||||||
|
.first()
|
||||||
|
.data
|
||||||
|
|
||||||
|
when (handle) {
|
||||||
|
is IdentityOidcResetHandle -> {
|
||||||
|
activity.openUrlInChromeCustomTab(null, false, handle.url)
|
||||||
|
handle.resetOidc()
|
||||||
|
}
|
||||||
|
is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
(LocalContext.current as? Activity)?.let { activity = it }
|
||||||
|
|
||||||
|
BackstackView(modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 ResetKeyPasswordEvent {
|
||||||
|
data class Reset(val password: String) : ResetKeyPasswordEvent
|
||||||
|
data object DismissError : ResetKeyPasswordEvent
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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 com.bumble.appyx.core.plugin.plugins
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class ResetKeyPasswordNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
data class Inputs(val userId: UserId, val handle: IdentityPasswordResetHandle) : NodeInputs
|
||||||
|
|
||||||
|
private val presenter by lazy {
|
||||||
|
val inputs = inputs<Inputs>()
|
||||||
|
ResetKeyPasswordPresenter(inputs.userId, inputs.handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = presenter.present()
|
||||||
|
ResetKeyPasswordView(
|
||||||
|
state = state,
|
||||||
|
onBack = ::navigateUp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ResetKeyPasswordPresenter(
|
||||||
|
private val userId: UserId,
|
||||||
|
private val identityPasswordResetHandle: IdentityPasswordResetHandle,
|
||||||
|
) : Presenter<ResetKeyPasswordState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): ResetKeyPasswordState {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val resetAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
|
fun handleEvent(event: ResetKeyPasswordEvent) {
|
||||||
|
when (event) {
|
||||||
|
is ResetKeyPasswordEvent.Reset -> coroutineScope.reset(userId, event.password, resetAction)
|
||||||
|
ResetKeyPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResetKeyPasswordState(
|
||||||
|
resetAction = resetAction.value,
|
||||||
|
eventSink = ::handleEvent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.reset(userId: UserId, password: String, action: MutableState<AsyncAction<Unit>>) = launch {
|
||||||
|
suspend {
|
||||||
|
identityPasswordResetHandle.resetPassword(userId, password).getOrThrow()
|
||||||
|
}.runCatchingUpdatingState(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 ResetKeyPasswordState(
|
||||||
|
val resetAction: AsyncAction<Unit>,
|
||||||
|
val eventSink: (ResetKeyPasswordEvent) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* 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.MutableState
|
||||||
|
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 io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
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.dialogs.ErrorDialog
|
||||||
|
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 ResetKeyPasswordView(
|
||||||
|
state: ResetKeyPasswordState,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val passwordState = textFieldState(stateValue = "")
|
||||||
|
FlowStepPage(
|
||||||
|
modifier = modifier,
|
||||||
|
iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()),
|
||||||
|
title = stringResource(CommonStrings.screen_reset_encryption_password_title),
|
||||||
|
subTitle = stringResource(CommonStrings.screen_reset_encryption_password_subtitle),
|
||||||
|
onBackClick = onBack,
|
||||||
|
content = { Content(textFieldState = passwordState) },
|
||||||
|
buttons = {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(CommonStrings.action_reset_identity),
|
||||||
|
onClick = { state.eventSink(ResetKeyPasswordEvent.Reset(passwordState.value)) },
|
||||||
|
destructive = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.resetAction.isLoading() || state.resetAction.isSuccess()) {
|
||||||
|
ProgressDialog()
|
||||||
|
} else if (state.resetAction.isFailure()) {
|
||||||
|
ErrorDialog(
|
||||||
|
content = stringResource(CommonStrings.error_unknown),
|
||||||
|
onDismiss = { state.eventSink(ResetKeyPasswordEvent.DismissError) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Content(textFieldState: MutableState<String>) {
|
||||||
|
var showPassword by remember { mutableStateOf(false) }
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onTabOrEnterKeyFocusNext(LocalFocusManager.current),
|
||||||
|
value = textFieldState.value,
|
||||||
|
onValueChange = { text -> textFieldState.value = text },
|
||||||
|
label = { Text(stringResource(CommonStrings.common_password)) },
|
||||||
|
placeholder = { Text(stringResource(CommonStrings.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun ResetKeyPasswordViewPreview() {
|
||||||
|
ElementPreview {
|
||||||
|
ResetKeyPasswordView(
|
||||||
|
state = ResetKeyPasswordState(
|
||||||
|
resetAction = AsyncAction.Uninitialized,
|
||||||
|
eventSink = {}
|
||||||
|
),
|
||||||
|
onBack = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 ResetKeyRootEvent {
|
||||||
|
data object Continue : ResetKeyRootEvent
|
||||||
|
data object DismissDialog : ResetKeyRootEvent
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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 ResetKeyRootNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onContinue()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val presenter = ResetKeyRootPresenter()
|
||||||
|
private val callback: Callback = plugins.filterIsInstance<Callback>().first()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = presenter.present()
|
||||||
|
ResetKeyRootView(
|
||||||
|
state = state,
|
||||||
|
onContinue = callback::onContinue,
|
||||||
|
onBack = ::navigateUp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 ResetKeyRootPresenter : Presenter<ResetKeyRootState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): ResetKeyRootState {
|
||||||
|
var displayConfirmDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun handleEvent(event: ResetKeyRootEvent) {
|
||||||
|
displayConfirmDialog = when (event) {
|
||||||
|
ResetKeyRootEvent.Continue -> true
|
||||||
|
ResetKeyRootEvent.DismissDialog -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResetKeyRootState(
|
||||||
|
displayConfirmationDialog = displayConfirmDialog,
|
||||||
|
eventSink = ::handleEvent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 ResetKeyRootState(
|
||||||
|
val displayConfirmationDialog: Boolean,
|
||||||
|
val eventSink: (ResetKeyRootEvent) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -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 ResetKeyRootStateProvider : PreviewParameterProvider<ResetKeyRootState> {
|
||||||
|
override val values: Sequence<ResetKeyRootState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
ResetKeyRootState(
|
||||||
|
displayConfirmationDialog = false,
|
||||||
|
eventSink = {}
|
||||||
|
),
|
||||||
|
ResetKeyRootState(
|
||||||
|
displayConfirmationDialog = true,
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* 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.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 ResetKeyRootView(
|
||||||
|
state: ResetKeyRootState,
|
||||||
|
onContinue: () -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
FlowStepPage(
|
||||||
|
iconStyle = BigIcon.Style.AlertSolid,
|
||||||
|
title = stringResource(io.element.android.libraries.ui.strings.R.string.screen_encryption_reset_title),
|
||||||
|
subTitle = stringResource(io.element.android.libraries.ui.strings.R.string.screen_encryption_reset_subtitle),
|
||||||
|
isScrollable = true,
|
||||||
|
content = { Content() },
|
||||||
|
buttons = {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(id = CommonStrings.action_continue),
|
||||||
|
onClick = { state.eventSink(ResetKeyRootEvent.Continue) },
|
||||||
|
destructive = true,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onBackClick = onBack,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.displayConfirmationDialog) {
|
||||||
|
ConfirmationDialog(
|
||||||
|
title = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_title),
|
||||||
|
content = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_subtitle),
|
||||||
|
submitText = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_action),
|
||||||
|
onSubmitClick = {
|
||||||
|
state.eventSink(ResetKeyRootEvent.DismissDialog)
|
||||||
|
onContinue()
|
||||||
|
},
|
||||||
|
destructiveSubmit = true,
|
||||||
|
onDismiss = { state.eventSink(ResetKeyRootEvent.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(CommonStrings.screen_encryption_reset_bullet_1),
|
||||||
|
iconComposable = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
imageVector = CompoundIcons.Check(),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.colors.iconSuccessPrimary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
InfoListItem(
|
||||||
|
message = stringResource(CommonStrings.screen_encryption_reset_bullet_2),
|
||||||
|
iconComposable = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
imageVector = CompoundIcons.Close(),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
InfoListItem(
|
||||||
|
message = stringResource(CommonStrings.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(CommonStrings.screen_encryption_reset_footer),
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textActionPrimary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun ResetKeyRootViewPreview(@PreviewParameter(ResetKeyRootStateProvider::class) state: ResetKeyRootState) {
|
||||||
|
ElementPreview {
|
||||||
|
ResetKeyRootView(
|
||||||
|
state = state,
|
||||||
|
onContinue = {},
|
||||||
|
onBack = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ interface VerifySessionEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onEnterRecoveryKey()
|
fun onEnterRecoveryKey()
|
||||||
|
fun onResetKey()
|
||||||
fun onDone()
|
fun onDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ class VerifySelfSessionNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onEnterRecoveryKey = callback::onEnterRecoveryKey,
|
onEnterRecoveryKey = callback::onEnterRecoveryKey,
|
||||||
|
onResetKey = callback::onResetKey,
|
||||||
onFinish = callback::onDone,
|
onFinish = callback::onDone,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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.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.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
|
@ -66,6 +68,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver
|
||||||
fun VerifySelfSessionView(
|
fun VerifySelfSessionView(
|
||||||
state: VerifySelfSessionState,
|
state: VerifySelfSessionState,
|
||||||
onEnterRecoveryKey: () -> Unit,
|
onEnterRecoveryKey: () -> Unit,
|
||||||
|
onResetKey: () -> Unit,
|
||||||
onFinish: () -> Unit,
|
onFinish: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -115,6 +118,7 @@ fun VerifySelfSessionView(
|
||||||
goBack = ::resetFlow,
|
goBack = ::resetFlow,
|
||||||
onEnterRecoveryKey = onEnterRecoveryKey,
|
onEnterRecoveryKey = onEnterRecoveryKey,
|
||||||
onFinish = onFinish,
|
onFinish = onFinish,
|
||||||
|
onResetKey = onResetKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -226,6 +230,7 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie
|
||||||
private fun BottomMenu(
|
private fun BottomMenu(
|
||||||
screenState: VerifySelfSessionState,
|
screenState: VerifySelfSessionState,
|
||||||
onEnterRecoveryKey: () -> Unit,
|
onEnterRecoveryKey: () -> Unit,
|
||||||
|
onResetKey: () -> Unit,
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
onFinish: () -> Unit,
|
onFinish: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -236,42 +241,69 @@ private fun BottomMenu(
|
||||||
|
|
||||||
when (verificationViewState) {
|
when (verificationViewState) {
|
||||||
is FlowStep.Initial -> {
|
is FlowStep.Initial -> {
|
||||||
if (verificationViewState.isLastDevice) {
|
BottomMenu {
|
||||||
BottomMenu(
|
if (verificationViewState.isLastDevice) {
|
||||||
positiveButtonTitle = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
Button(
|
||||||
onPositiveButtonClick = onEnterRecoveryKey,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
||||||
} else {
|
onClick = onEnterRecoveryKey,
|
||||||
BottomMenu(
|
)
|
||||||
positiveButtonTitle = stringResource(R.string.screen_identity_use_another_device),
|
} else {
|
||||||
onPositiveButtonClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
|
Button(
|
||||||
negativeButtonTitle = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onNegativeButtonClick = onEnterRecoveryKey,
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
|
||||||
|
onClick = onResetKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is FlowStep.Canceled -> {
|
is FlowStep.Canceled -> {
|
||||||
BottomMenu(
|
BottomMenu {
|
||||||
positiveButtonTitle = stringResource(R.string.screen_session_verification_positive_button_canceled),
|
Button(
|
||||||
onPositiveButtonClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
negativeButtonTitle = stringResource(CommonStrings.action_cancel),
|
text = stringResource(R.string.screen_session_verification_positive_button_canceled),
|
||||||
onNegativeButtonClick = goBack,
|
onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
|
||||||
)
|
)
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
onClick = goBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is FlowStep.Ready -> {
|
is FlowStep.Ready -> {
|
||||||
BottomMenu(
|
BottomMenu {
|
||||||
positiveButtonTitle = stringResource(CommonStrings.action_start),
|
Button(
|
||||||
onPositiveButtonClick = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
negativeButtonTitle = stringResource(CommonStrings.action_cancel),
|
text = stringResource(CommonStrings.action_start),
|
||||||
onNegativeButtonClick = goBack,
|
onClick = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) },
|
||||||
)
|
)
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
onClick = goBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is FlowStep.AwaitingOtherDeviceResponse -> {
|
is FlowStep.AwaitingOtherDeviceResponse -> {
|
||||||
BottomMenu(
|
BottomMenu {
|
||||||
positiveButtonTitle = stringResource(R.string.screen_identity_waiting_on_other_device),
|
Button(
|
||||||
onPositiveButtonClick = {},
|
modifier = Modifier.fillMaxWidth(),
|
||||||
isLoading = true,
|
text = stringResource(R.string.screen_identity_waiting_on_other_device),
|
||||||
)
|
onClick = {},
|
||||||
|
showProgress = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is FlowStep.Verifying -> {
|
is FlowStep.Verifying -> {
|
||||||
val positiveButtonTitle = if (isVerifying) {
|
val positiveButtonTitle = if (isVerifying) {
|
||||||
|
|
@ -279,23 +311,32 @@ private fun BottomMenu(
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.screen_session_verification_they_match)
|
stringResource(R.string.screen_session_verification_they_match)
|
||||||
}
|
}
|
||||||
BottomMenu(
|
BottomMenu {
|
||||||
positiveButtonTitle = positiveButtonTitle,
|
Button(
|
||||||
onPositiveButtonClick = {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
if (!isVerifying) {
|
text = positiveButtonTitle,
|
||||||
eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
showProgress = isVerifying,
|
||||||
}
|
onClick = {
|
||||||
},
|
if (!isVerifying) {
|
||||||
negativeButtonTitle = stringResource(R.string.screen_session_verification_they_dont_match),
|
eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
||||||
onNegativeButtonClick = { eventSink(VerifySelfSessionViewEvents.DeclineVerification) },
|
}
|
||||||
isLoading = isVerifying,
|
},
|
||||||
)
|
)
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.screen_session_verification_they_dont_match),
|
||||||
|
onClick = { eventSink(VerifySelfSessionViewEvents.DeclineVerification) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is FlowStep.Completed -> {
|
is FlowStep.Completed -> {
|
||||||
BottomMenu(
|
BottomMenu {
|
||||||
positiveButtonTitle = stringResource(CommonStrings.action_continue),
|
Button(
|
||||||
onPositiveButtonClick = onFinish,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
text = stringResource(CommonStrings.action_continue),
|
||||||
|
onClick = onFinish,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is FlowStep.Skipped -> return
|
is FlowStep.Skipped -> return
|
||||||
}
|
}
|
||||||
|
|
@ -303,35 +344,13 @@ private fun BottomMenu(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BottomMenu(
|
private fun BottomMenu(
|
||||||
positiveButtonTitle: String?,
|
|
||||||
onPositiveButtonClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
negativeButtonTitle: String? = null,
|
buttons: @Composable ColumnScope.() -> Unit,
|
||||||
negativeButtonEnabled: Boolean = negativeButtonTitle != null,
|
|
||||||
onNegativeButtonClick: () -> Unit = {},
|
|
||||||
isLoading: Boolean = false,
|
|
||||||
) {
|
) {
|
||||||
ButtonColumnMolecule(
|
ButtonColumnMolecule(
|
||||||
modifier = modifier.padding(bottom = 16.dp)
|
modifier = modifier.padding(bottom = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (positiveButtonTitle != null) {
|
buttons()
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,6 +360,7 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta
|
||||||
VerifySelfSessionView(
|
VerifySelfSessionView(
|
||||||
state = state,
|
state = state,
|
||||||
onEnterRecoveryKey = {},
|
onEnterRecoveryKey = {},
|
||||||
|
onResetKey = {},
|
||||||
onFinish = {},
|
onFinish = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.encryption
|
package io.element.android.libraries.matrix.api.encryption
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
|
@ -62,4 +63,17 @@ interface EncryptionService {
|
||||||
* called the fingerprint of the device.
|
* called the fingerprint of the device.
|
||||||
*/
|
*/
|
||||||
suspend fun deviceEd25519(): String?
|
suspend fun deviceEd25519(): String?
|
||||||
|
|
||||||
|
suspend fun startIdentityReset(): Result<IdentityResetHandle?>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IdentityResetHandle
|
||||||
|
|
||||||
|
interface IdentityPasswordResetHandle : IdentityResetHandle {
|
||||||
|
suspend fun resetPassword(userId: UserId, password: String): Result<Unit>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IdentityOidcResetHandle : IdentityResetHandle {
|
||||||
|
val url: String
|
||||||
|
suspend fun resetOidc(): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ 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.BackupUploadState
|
||||||
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
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.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.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
||||||
|
|
@ -198,4 +199,8 @@ internal class RustEncryptionService(
|
||||||
override suspend fun deviceEd25519(): String? {
|
override suspend fun deviceEd25519(): String? {
|
||||||
return service.ed25519Key()
|
return service.ed25519Key()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun startIdentityReset(): Result<IdentityResetHandle?> {
|
||||||
|
return runCatching { service.resetIdentity()?.let(RustIdentityResetHandleFactory::create)?.getOrNull() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.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(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(identityResetHandle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RustPasswordIdentityResetHandle(
|
||||||
|
private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle,
|
||||||
|
) : IdentityPasswordResetHandle {
|
||||||
|
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> {
|
||||||
|
return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue