Browse Source

Initial implementation of the reset identity feature

pull/3298/head
Jorge Martín 1 month ago
parent
commit
4ab0b1074d
  1. 17
      features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt
  2. 3
      features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt
  3. 15
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt
  4. 79
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt
  5. 127
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt
  6. 22
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordEvent.kt
  7. 55
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordNode.kt
  8. 60
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordPresenter.kt
  9. 24
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordState.kt
  10. 120
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordView.kt
  11. 22
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootEvent.kt
  12. 50
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootNode.kt
  13. 43
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootPresenter.kt
  14. 22
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootState.kt
  15. 33
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootStateProvider.kt
  16. 149
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootView.kt
  17. 1
      features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt
  18. 1
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt
  19. 154
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt
  20. 1
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
  21. 14
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
  22. 5
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
  23. 54
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt

17
features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt

@ -58,6 +58,9 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( @@ -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( @@ -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( @@ -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()
}
}
}

3
features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt

@ -34,6 +34,9 @@ interface SecureBackupEntryPoint : FeatureEntryPoint { @@ -34,6 +34,9 @@ interface SecureBackupEntryPoint : FeatureEntryPoint {
@Parcelize
data object CreateNewRecoveryKey : InitialTarget
@Parcelize
data object ResetIdentity : InitialTarget
}
data class Params(val initialElement: InitialTarget) : NodeInputs

15
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 @@ -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( @@ -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( @@ -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( @@ -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))
}
}
}

79
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt

@ -0,0 +1,79 @@ @@ -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
}
}
}

127
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt

@ -0,0 +1,127 @@ @@ -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)
}
}

22
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordEvent.kt

@ -0,0 +1,22 @@ @@ -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
}

55
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordNode.kt

@ -0,0 +1,55 @@ @@ -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
)
}
}

60
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordPresenter.kt

@ -0,0 +1,60 @@ @@ -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)
}
}

24
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordState.kt

@ -0,0 +1,24 @@ @@ -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,
)

120
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordView.kt

@ -0,0 +1,120 @@ @@ -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 = {}
)
}
}

22
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootEvent.kt

@ -0,0 +1,22 @@ @@ -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
}

50
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootNode.kt

@ -0,0 +1,50 @@ @@ -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,
)
}
}

43
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootPresenter.kt

@ -0,0 +1,43 @@ @@ -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
)
}
}

22
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootState.kt

@ -0,0 +1,22 @@ @@ -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,
)

33
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootStateProvider.kt

@ -0,0 +1,33 @@ @@ -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 = {}
)
)
}

149
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootView.kt

@ -0,0 +1,149 @@ @@ -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 = {},
)
}
}

1
features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt

@ -31,6 +31,7 @@ interface VerifySessionEntryPoint : FeatureEntryPoint { @@ -31,6 +31,7 @@ interface VerifySessionEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onEnterRecoveryKey()
fun onResetKey()
fun onDone()
}
}

1
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt

@ -43,6 +43,7 @@ class VerifySelfSessionNode @AssistedInject constructor( @@ -43,6 +43,7 @@ class VerifySelfSessionNode @AssistedInject constructor(
state = state,
modifier = modifier,
onEnterRecoveryKey = callback::onEnterRecoveryKey,
onResetKey = callback::onResetKey,
onFinish = callback::onDone,
)
}

154
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt

@ -20,6 +20,7 @@ import androidx.activity.compose.BackHandler @@ -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 @@ -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 @@ -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( @@ -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 @@ -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,69 @@ private fun BottomMenu( @@ -236,42 +241,69 @@ 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,
)
}
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,
)
}
}
is FlowStep.Verifying -> {
val positiveButtonTitle = if (isVerifying) {
@ -279,23 +311,32 @@ private fun BottomMenu( @@ -279,23 +311,32 @@ 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,
)
}
}
is FlowStep.Skipped -> return
}
@ -303,35 +344,13 @@ private fun BottomMenu( @@ -303,35 +344,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 +360,7 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta @@ -341,6 +360,7 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta
VerifySelfSessionView(
state = state,
onEnterRecoveryKey = {},
onResetKey = {},
onFinish = {},
)
}

1
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt

@ -41,6 +41,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService @@ -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.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.sessionstorage.api.LoginType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow

14
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
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.StateFlow
@ -62,4 +63,17 @@ interface EncryptionService { @@ -62,4 +63,17 @@ interface EncryptionService {
* called the fingerprint of the device.
*/
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>
}

5
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt

@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.encryption.BackupState @@ -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.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
@ -198,4 +199,8 @@ internal class RustEncryptionService( @@ -198,4 +199,8 @@ internal class RustEncryptionService(
override suspend fun deviceEd25519(): String? {
return service.ed25519Key()
}
override suspend fun startIdentityReset(): Result<IdentityResetHandle?> {
return runCatching { service.resetIdentity()?.let(RustIdentityResetHandleFactory::create)?.getOrNull() }
}
}

54
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt

@ -0,0 +1,54 @@ @@ -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…
Cancel
Save