diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt index a5dc840267..3e4ed218a5 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt @@ -58,6 +58,9 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( @Parcelize data object EnterRecoveryKey : NavTarget + + @Parcelize + data object ResetIdentity : NavTarget } interface Callback : Plugin { @@ -85,6 +88,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( override fun onDone() { plugins().forEach { it.onDone() } } + + override fun onResetKey() { + backstack.push(NavTarget.ResetIdentity) + } }) .build() } @@ -94,6 +101,16 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( .callback(secureBackupEntryPointCallback) .build() } + is NavTarget.ResetIdentity -> { + secureBackupEntryPoint.nodeBuilder(this, buildContext) + .params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity)) + .callback(object : SecureBackupEntryPoint.Callback { + override fun onDone() { + plugins().forEach { it.onDone() } + } + }) + .build() + } } } diff --git a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt index 45e3a75738..416ecff1bc 100644 --- a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt +++ b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt @@ -34,6 +34,9 @@ interface SecureBackupEntryPoint : FeatureEntryPoint { @Parcelize data object CreateNewRecoveryKey : InitialTarget + + @Parcelize + data object ResetIdentity : InitialTarget } data class Params(val initialElement: InitialTarget) : NodeInputs diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt index f54bfaee96..d741eb11a7 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt @@ -34,6 +34,7 @@ import io.element.android.features.securebackup.impl.createkey.CreateNewRecovery import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode +import io.element.android.features.securebackup.impl.reset.ResetIdentityFlowNode import io.element.android.features.securebackup.impl.root.SecureBackupRootNode import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode import io.element.android.libraries.architecture.BackstackView @@ -48,10 +49,11 @@ class SecureBackupFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BaseFlowNode( backstack = BackStack( - initialElement = when (plugins.filterIsInstance(SecureBackupEntryPoint.Params::class.java).first().initialElement) { + initialElement = when (plugins.filterIsInstance().first().initialElement) { SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey -> NavTarget.CreateNewRecoveryKey + is SecureBackupEntryPoint.InitialTarget.ResetIdentity -> NavTarget.ResetIdentity }, savedStateMap = buildContext.savedStateMap, ), @@ -79,6 +81,9 @@ class SecureBackupFlowNode @AssistedInject constructor( @Parcelize data object CreateNewRecoveryKey : NavTarget + + @Parcelize + data object ResetIdentity : NavTarget } private val callbacks = plugins() @@ -146,6 +151,14 @@ class SecureBackupFlowNode @AssistedInject constructor( NavTarget.CreateNewRecoveryKey -> { createNode(buildContext) } + is NavTarget.ResetIdentity -> { + val callback = object : ResetIdentityFlowNode.Callback { + override fun onDone() { + callbacks.forEach { it.onDone() } + } + } + createNode(buildContext, listOf(callback)) + } } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt new file mode 100644 index 0000000000..76674cd87d --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset + +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.di.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> = MutableStateFlow(AsyncData.Uninitialized) + val currentHandleFlow: StateFlow> = resetHandleFlow + + fun whenResetIsDone(block: () -> Unit) { + sessionCoroutineScope.launch { + sessionVerificationService.sessionVerifiedStatus.filterIsInstance().first() + block() + } + } + + fun currentSessionId(): SessionId { + return matrixClient.sessionId + } + + fun getResetHandle(): StateFlow> { + 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 + } + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt new file mode 100644 index 0000000000..a7c0011625 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt @@ -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, + private val resetIdentityFlowManager: ResetIdentityFlowManager, + private val coroutineScope: CoroutineScope, +) : BaseFlowNode( + 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().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(buildContext, listOf(callback)) + } + is NavTarget.ResetPassword -> { + val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found") + createNode( + buildContext, + listOf(ResetKeyPasswordNode.Inputs(resetIdentityFlowManager.currentSessionId(), handle)) + ) + } + } + } + + private fun CoroutineScope.startReset() = launch { + val handle = resetIdentityFlowManager.getResetHandle() + .filterIsInstance>() + .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) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordEvent.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordEvent.kt new file mode 100644 index 0000000000..5fce7cdf85 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordEvent.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +sealed interface ResetKeyPasswordEvent { + data class Reset(val password: String) : ResetKeyPasswordEvent + data object DismissError : ResetKeyPasswordEvent +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordNode.kt new file mode 100644 index 0000000000..c430bbec28 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordNode.kt @@ -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, +) : Node(buildContext, plugins = plugins) { + + data class Inputs(val userId: UserId, val handle: IdentityPasswordResetHandle) : NodeInputs + + private val presenter by lazy { + val inputs = inputs() + ResetKeyPasswordPresenter(inputs.userId, inputs.handle) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ResetKeyPasswordView( + state = state, + onBack = ::navigateUp + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordPresenter.kt new file mode 100644 index 0000000000..19fa002108 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordPresenter.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.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 { + @Composable + override fun present(): ResetKeyPasswordState { + val coroutineScope = rememberCoroutineScope() + + val resetAction = remember { mutableStateOf>(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>) = launch { + suspend { + identityPasswordResetHandle.resetPassword(userId, password).getOrThrow() + }.runCatchingUpdatingState(action) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordState.kt new file mode 100644 index 0000000000..3de58ec032 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import io.element.android.libraries.architecture.AsyncAction + +data class ResetKeyPasswordState( + val resetAction: AsyncAction, + val eventSink: (ResetKeyPasswordEvent) -> Unit, +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordView.kt new file mode 100644 index 0000000000..d9339064f2 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordView.kt @@ -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) { + 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 = {} + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootEvent.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootEvent.kt new file mode 100644 index 0000000000..268228ac8b --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootEvent.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +sealed interface ResetKeyRootEvent { + data object Continue : ResetKeyRootEvent + data object DismissDialog : ResetKeyRootEvent +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootNode.kt new file mode 100644 index 0000000000..b7171aaa7b --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootNode.kt @@ -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, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onContinue() + } + + private val presenter = ResetKeyRootPresenter() + private val callback: Callback = plugins.filterIsInstance().first() + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ResetKeyRootView( + state = state, + onContinue = callback::onContinue, + onBack = ::navigateUp, + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootPresenter.kt new file mode 100644 index 0000000000..d2e3e5dc70 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootPresenter.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter + +class ResetKeyRootPresenter : Presenter { + @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 + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootState.kt new file mode 100644 index 0000000000..faaee64040 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +data class ResetKeyRootState( + val displayConfirmationDialog: Boolean, + val eventSink: (ResetKeyRootEvent) -> Unit, +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootStateProvider.kt new file mode 100644 index 0000000000..15299fa5ba --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootStateProvider.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +class ResetKeyRootStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + ResetKeyRootState( + displayConfirmationDialog = false, + eventSink = {} + ), + ResetKeyRootState( + displayConfirmationDialog = true, + eventSink = {} + ) + ) +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootView.kt new file mode 100644 index 0000000000..cbeaa1a14e --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootView.kt @@ -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 = {}, + ) + } +} diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt index 8d19ca5698..deb5cdf267 100644 --- a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt +++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt @@ -31,6 +31,7 @@ interface VerifySessionEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onEnterRecoveryKey() + fun onResetKey() fun onDone() } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt index 9ce1358683..0ed9524626 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt @@ -43,6 +43,7 @@ class VerifySelfSessionNode @AssistedInject constructor( state = state, modifier = modifier, onEnterRecoveryKey = callback::onEnterRecoveryKey, + onResetKey = callback::onResetKey, onFinish = callback::onDone, ) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt index 6b908e3ebd..f1c169b193 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt @@ -20,6 +20,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -53,6 +54,7 @@ import io.element.android.libraries.designsystem.components.PageTitle import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -66,6 +68,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver fun VerifySelfSessionView( state: VerifySelfSessionState, onEnterRecoveryKey: () -> Unit, + onResetKey: () -> Unit, onFinish: () -> Unit, modifier: Modifier = Modifier, ) { @@ -115,6 +118,7 @@ fun VerifySelfSessionView( goBack = ::resetFlow, onEnterRecoveryKey = onEnterRecoveryKey, onFinish = onFinish, + onResetKey = onResetKey, ) } ) { @@ -226,6 +230,7 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie private fun BottomMenu( screenState: VerifySelfSessionState, onEnterRecoveryKey: () -> Unit, + onResetKey: () -> Unit, goBack: () -> Unit, onFinish: () -> Unit, ) { @@ -236,42 +241,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( } 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( @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 VerifySelfSessionView( state = state, onEnterRecoveryKey = {}, + onResetKey = {}, onFinish = {}, ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 3209d49e03..0591e794ed 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -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 diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index f47487c634..fdaebc9c97 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -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 { * called the fingerprint of the device. */ suspend fun deviceEd25519(): String? + + suspend fun startIdentityReset(): Result +} + +interface IdentityResetHandle + +interface IdentityPasswordResetHandle : IdentityResetHandle { + suspend fun resetPassword(userId: UserId, password: String): Result +} + +interface IdentityOidcResetHandle : IdentityResetHandle { + val url: String + suspend fun resetOidc(): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index 68ab4a611e..728fbede55 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -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( override suspend fun deviceEd25519(): String? { return service.ed25519Key() } + + override suspend fun startIdentityReset(): Result { + return runCatching { service.resetIdentity()?.let(RustIdentityResetHandleFactory::create)?.getOrNull() } + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt new file mode 100644 index 0000000000..69c7ecdd31 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.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 { + 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 { + 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 { + return runCatching { identityResetHandle.reset(null) } + } +}