From e16057a366fd4fd14359edb3226b0b70811a4047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 7 Aug 2024 12:59:57 +0200 Subject: [PATCH 01/15] Initial implementation of the reset identity feature --- .../FtueSessionVerificationFlowNode.kt | 17 ++ .../api/SecureBackupEntryPoint.kt | 3 + .../securebackup/impl/SecureBackupFlowNode.kt | 15 +- .../impl/reset/ResetIdentityFlowManager.kt | 79 +++++++++ .../impl/reset/ResetIdentityFlowNode.kt | 127 +++++++++++++++ .../reset/password/ResetKeyPasswordEvent.kt | 22 +++ .../reset/password/ResetKeyPasswordNode.kt | 55 +++++++ .../password/ResetKeyPasswordPresenter.kt | 60 +++++++ .../reset/password/ResetKeyPasswordState.kt | 24 +++ .../reset/password/ResetKeyPasswordView.kt | 120 ++++++++++++++ .../impl/reset/root/ResetKeyRootEvent.kt | 22 +++ .../impl/reset/root/ResetKeyRootNode.kt | 50 ++++++ .../impl/reset/root/ResetKeyRootPresenter.kt | 43 +++++ .../impl/reset/root/ResetKeyRootState.kt | 22 +++ .../reset/root/ResetKeyRootStateProvider.kt | 33 ++++ .../impl/reset/root/ResetKeyRootView.kt | 149 +++++++++++++++++ .../api/VerifySessionEntryPoint.kt | 1 + .../impl/VerifySelfSessionNode.kt | 1 + .../impl/VerifySelfSessionView.kt | 154 ++++++++++-------- .../libraries/matrix/api/MatrixClient.kt | 1 + .../api/encryption/EncryptionService.kt | 14 ++ .../impl/encryption/RustEncryptionService.kt | 5 + .../encryption/RustIdentityResetHandle.kt | 54 ++++++ 23 files changed, 1003 insertions(+), 68 deletions(-) create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordEvent.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordNode.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordPresenter.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordState.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordView.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootEvent.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootNode.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootPresenter.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootState.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootStateProvider.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootView.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt 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) } + } +} From d5c2e5e43690c01938d72faee82a2ae65b2c97af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 8 Aug 2024 16:24:13 +0200 Subject: [PATCH 02/15] Extract OIDC to its own module --- appnav/build.gradle.kts | 2 + .../io/element/android/appnav/RootFlowNode.kt | 4 +- .../android/appnav/intent/IntentResolver.kt | 4 +- .../appnav/intent/IntentResolverTest.kt | 6 +- features/login/impl/build.gradle.kts | 2 + .../features/login/impl/LoginFlowNode.kt | 18 ++--- .../ConfirmAccountProviderPresenter.kt | 10 +-- .../ConfirmAccountProviderPresenterTest.kt | 6 +- .../impl/VerifySelfSessionViewTest.kt | 2 + .../test/encryption/FakeEncryptionService.kt | 9 ++- .../encryption/FakeIdentityResetHandle.kt | 38 ++++++++++ libraries/oidc/api/build.gradle.kts | 27 +++++++ .../android/libraries/oidc/api}/OidcAction.kt | 2 +- .../libraries/oidc/api}/OidcActionFlow.kt | 6 +- .../libraries/oidc/api/OidcEntryPoint.kt | 27 +++++++ .../libraries/oidc/api}/OidcIntentResolver.kt | 2 +- libraries/oidc/impl/build.gradle.kts | 71 +++++++++++++++++++ .../impl}/CustomTabAvailabilityChecker.kt | 2 +- .../oidc/impl/DefaultOidcEntryPoint.kt | 49 +++++++++++++ .../oidc/impl}/DefaultOidcIntentResolver.kt | 6 +- .../libraries/oidc/impl}/OidcUrlParser.kt | 4 +- .../oidc/impl}/customtab/CustomTabHandler.kt | 2 +- .../impl}/customtab/DefaultOidcActionFlow.kt | 10 +-- .../oidc/impl}/webview/OidcEvents.kt | 4 +- .../libraries/oidc/impl}/webview/OidcNode.kt | 2 +- .../oidc/impl}/webview/OidcPresenter.kt | 4 +- .../libraries/oidc/impl}/webview/OidcState.kt | 2 +- .../oidc/impl}/webview/OidcStateProvider.kt | 2 +- .../libraries/oidc/impl}/webview/OidcView.kt | 4 +- .../oidc/impl}/webview/OidcWebViewClient.kt | 2 +- .../impl}/webview/WebViewEventListener.kt | 2 +- .../libraries/oidc/impl}/OidcUrlParserTest.kt | 4 +- .../oidc/impl}/webview/OidcPresenterTest.kt | 4 +- .../kotlin/extension/DependencyHandleScope.kt | 1 + 34 files changed, 283 insertions(+), 57 deletions(-) create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt create mode 100644 libraries/oidc/api/build.gradle.kts rename {features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc => libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api}/OidcAction.kt (93%) rename {features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc => libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api}/OidcActionFlow.kt (79%) create mode 100644 libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt rename {features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc => libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api}/OidcIntentResolver.kt (93%) create mode 100644 libraries/oidc/impl/build.gradle.kts rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/CustomTabAvailabilityChecker.kt (95%) create mode 100644 libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/DefaultOidcIntentResolver.kt (85%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/OidcUrlParser.kt (93%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/customtab/CustomTabHandler.kt (97%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/customtab/DefaultOidcActionFlow.kt (82%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/webview/OidcEvents.kt (86%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/webview/OidcNode.kt (96%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/webview/OidcPresenter.kt (96%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/webview/OidcState.kt (93%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/webview/OidcStateProvider.kt (95%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/webview/OidcView.kt (96%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/webview/OidcWebViewClient.kt (95%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl}/webview/WebViewEventListener.kt (93%) rename {features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl}/OidcUrlParserTest.kt (94%) rename {features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc => libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl}/webview/OidcPresenterTest.kt (97%) diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 53680fd44e..887f48e203 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.deeplink) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.oidc.api) implementation(projects.libraries.preferences.api) implementation(projects.libraries.push.api) implementation(projects.libraries.pushproviders.api) @@ -66,6 +67,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.oidc.impl) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.features.networkmonitor.test) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index d200acb84e..b651f4d77c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -42,8 +42,6 @@ import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.features.viewfolder.api.ViewFolderEntryPoint @@ -59,6 +57,8 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.sessionstorage.api.LoggedInState +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcActionFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt index d41fbbfebb..73ad6f6008 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt @@ -17,12 +17,12 @@ package io.element.android.appnav.intent import android.content.Intent -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.api.oidc.OidcIntentResolver import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcIntentResolver import timber.log.Timber import javax.inject.Inject diff --git a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt index aa0fe9cbf1..8134c091f6 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt @@ -21,9 +21,6 @@ import android.content.Intent import android.net.Uri import androidx.core.net.toUri import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.impl.oidc.DefaultOidcIntentResolver -import io.element.android.features.login.impl.oidc.OidcUrlParser import io.element.android.libraries.deeplink.DeepLinkCreator import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkParser @@ -33,6 +30,9 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.impl.DefaultOidcIntentResolver +import io.element.android.libraries.oidc.impl.OidcUrlParser import io.element.android.tests.testutils.lambda.lambdaError import org.junit.Assert.assertThrows import org.junit.Test diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 6fcdc2c1de..3fbe55f1f9 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.permissions.api) implementation(projects.libraries.qrcode) + implementation(projects.libraries.oidc.api) implementation(libs.androidx.browser) implementation(platform(libs.network.retrofit.bom)) implementation(libs.network.retrofit) @@ -65,6 +66,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.oidc.impl) testImplementation(projects.libraries.permissions.test) testImplementation(projects.tests.testutils) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index 0af102a403..13aac03de4 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -36,12 +36,7 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.login.api.LoginFlowType -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker -import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler -import io.element.android.features.login.impl.oidc.webview.OidcNode import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode @@ -56,6 +51,9 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcActionFlow +import io.element.android.libraries.oidc.api.OidcEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -64,11 +62,10 @@ import kotlinx.parcelize.Parcelize class LoginFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val customTabAvailabilityChecker: CustomTabAvailabilityChecker, - private val customTabHandler: CustomTabHandler, private val accountProviderDataSource: AccountProviderDataSource, private val defaultLoginUserStory: DefaultLoginUserStory, private val oidcActionFlow: OidcActionFlow, + private val oidcEntryPoint: OidcEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -146,11 +143,11 @@ class LoginFlowNode @AssistedInject constructor( ) val callback = object : ConfirmAccountProviderNode.Callback { override fun onOidcDetails(oidcDetails: OidcDetails) { - if (customTabAvailabilityChecker.supportCustomTab()) { + if (oidcEntryPoint.canUseCustomTab()) { // In this case open a Chrome Custom tab activity?.let { customChromeTabStarted = true - customTabHandler.open(it, darkTheme, oidcDetails.url) + oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url) } } else { // Fallback to WebView mode @@ -201,8 +198,7 @@ class LoginFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(callback)) } is NavTarget.OidcView -> { - val input = OidcNode.Inputs(navTarget.oidcDetails) - createNode(buildContext, plugins = listOf(input)) + oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url) } is NavTarget.WaitList -> { val inputs = WaitListNode.Inputs( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index 27aec75739..fdf7994d7e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -27,15 +27,15 @@ import androidx.compose.runtime.rememberCoroutineScope import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcActionFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -43,7 +43,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( @Assisted private val params: Params, private val accountProviderDataSource: AccountProviderDataSource, private val authenticationService: MatrixAuthenticationService, - private val defaultOidcActionFlow: DefaultOidcActionFlow, + private val oidcActionFlow: OidcActionFlow, private val defaultLoginUserStory: DefaultLoginUserStory, ) : Presenter { data class Params( @@ -65,7 +65,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( } LaunchedEffect(Unit) { - defaultOidcActionFlow.collect { oidcAction -> + oidcActionFlow.collect { oidcAction -> if (oidcAction != null) { onOidcAction(oidcAction, loginFlowAction) } @@ -133,6 +133,6 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( } } } - defaultOidcActionFlow.reset() + oidcActionFlow.reset() } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index 5a02797bdb..3875a4b245 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -20,10 +20,8 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow import io.element.android.features.login.impl.util.defaultAccountProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService @@ -31,6 +29,8 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.impl.customtab.DefaultOidcActionFlow import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.waitForPredicate import kotlinx.coroutines.test.runTest @@ -274,7 +274,7 @@ class ConfirmAccountProviderPresenterTest { params = params, accountProviderDataSource = accountProviderDataSource, authenticationService = matrixAuthenticationService, - defaultOidcActionFlow = defaultOidcActionFlow, + oidcActionFlow = defaultOidcActionFlow, defaultLoginUserStory = defaultLoginUserStory, ) } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt index f19f3e1f29..7e5bf928e2 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt @@ -217,12 +217,14 @@ class VerifySelfSessionViewTest { state: VerifySelfSessionState, onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(), + onResetKey: () -> Unit = EnsureNeverCalled(), ) { setContent { VerifySelfSessionView( state = state, onEnterRecoveryKey = onEnterRecoveryKey, onFinish = onFinished, + onResetKey = onResetKey, ) } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index b864c69b0b..fa301e402e 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -20,13 +20,16 @@ import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf -class FakeEncryptionService : EncryptionService { +class FakeEncryptionService( + var startIdentityResetLambda: () -> Result = { error("Not implemented") }, +) : EncryptionService { private var disableRecoveryFailure: Exception? = null override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(RecoveryState.UNKNOWN) @@ -118,6 +121,10 @@ class FakeEncryptionService : EncryptionService { enableRecoveryProgressStateFlow.emit(state) } + override suspend fun startIdentityReset(): Result { + return startIdentityResetLambda() + } + companion object { const val FAKE_RECOVERY_KEY = "fake" } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt new file mode 100644 index 0000000000..bf0aa312be --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.test.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 + +class FakeIdentityOidcResetHandle( + override val url: String = "", + var resetOidcLambda: () -> Result = { error("Not implemented") } +) : IdentityOidcResetHandle { + override suspend fun resetOidc(): Result { + return resetOidcLambda() + } +} + +class FakeIdentityPasswordResetHandle( + var resetPasswordLambda: (UserId, String) -> Result = { _, _ -> error("Not implemented") } +) : IdentityPasswordResetHandle { + override suspend fun resetPassword(userId: UserId, password: String): Result { + return resetPasswordLambda(userId, password) + } +} diff --git a/libraries/oidc/api/build.gradle.kts b/libraries/oidc/api/build.gradle.kts new file mode 100644 index 0000000000..874c3ddbd4 --- /dev/null +++ b/libraries/oidc/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + id("io.element.android-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.libraries.oidc.api" +} + +dependencies { + implementation(projects.libraries.architecture) +} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt similarity index 93% rename from features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt index 6d87872879..cc17159960 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.api.oidc +package io.element.android.libraries.oidc.api sealed interface OidcAction { data object GoBack : OidcAction diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt similarity index 79% rename from features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt index 004e7c8a51..1fd67cfaf5 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt @@ -14,8 +14,12 @@ * limitations under the License. */ -package io.element.android.features.login.api.oidc +package io.element.android.libraries.oidc.api + +import kotlinx.coroutines.flow.FlowCollector interface OidcActionFlow { fun post(oidcAction: OidcAction) + suspend fun collect(collector: FlowCollector) + fun reset() } diff --git a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt new file mode 100644 index 0000000000..c00bf263af --- /dev/null +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.oidc.api + +import android.app.Activity +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node + +interface OidcEntryPoint { + fun canUseCustomTab(): Boolean + fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String) + fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node +} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt similarity index 93% rename from features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt index a6ecf26fca..77597fd432 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.api.oidc +package io.element.android.libraries.oidc.api import android.content.Intent diff --git a/libraries/oidc/impl/build.gradle.kts b/libraries/oidc/impl/build.gradle.kts new file mode 100644 index 0000000000..be4181e169 --- /dev/null +++ b/libraries/oidc/impl/build.gradle.kts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + id("kotlin-parcelize") + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "io.element.android.libraries.oidc.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + implementation(projects.appconfig) + anvil(projects.anvilcodegen) + implementation(projects.libraries.core) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.network) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.testtags) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.permissions.api) + implementation(projects.libraries.qrcode) + implementation(libs.androidx.browser) + implementation(platform(libs.network.retrofit.bom)) + implementation(libs.network.retrofit) + implementation(libs.serialization.json) + api(projects.libraries.oidc.api) + + testImplementation(libs.test.junit) + testImplementation(libs.androidx.compose.ui.test.junit) + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.permissions.test) + testImplementation(projects.tests.testutils) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt index 424e9f13bc..6258e67b0e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.libraries.oidc.impl import android.content.Context import androidx.browser.customtabs.CustomTabsClient diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt new file mode 100644 index 0000000000..1a8ce960c5 --- /dev/null +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.oidc.impl + +import android.app.Activity +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.oidc.impl.webview.OidcNode +import io.element.android.libraries.oidc.api.OidcEntryPoint +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultOidcEntryPoint @Inject constructor( + private val customTabAvailabilityChecker: CustomTabAvailabilityChecker, +) : OidcEntryPoint { + override fun canUseCustomTab(): Boolean { + return customTabAvailabilityChecker.supportCustomTab() + } + + override fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String) { + assert(canUseCustomTab()) { "Custom tab is not supported in this device." } + activity.openUrlInChromeCustomTab(null, darkTheme, url) + } + + override fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node { + assert(!canUseCustomTab()) { "Custom tab should be used instead of the fallback node." } + val inputs = OidcNode.Inputs(OidcDetails(url)) + return parentNode.createNode(buildContext, listOf(inputs)) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt similarity index 85% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt index 8b6844e0f3..9777cee22f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.libraries.oidc.impl import android.content.Intent import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.api.oidc.OidcIntentResolver import io.element.android.libraries.di.AppScope +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcIntentResolver import javax.inject.Inject @ContributesBinding(AppScope::class) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt index 8c8c895acd..ae502d0a8c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.libraries.oidc.impl -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.matrix.api.auth.OidcConfig +import io.element.android.libraries.oidc.api.OidcAction import javax.inject.Inject /** diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt similarity index 97% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt index b83c0eb1ac..21c673145e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.customtab +package io.element.android.libraries.oidc.impl.customtab import android.app.Activity import android.content.ComponentName diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt similarity index 82% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt index 17dfa8418f..0c0737b43e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.customtab +package io.element.android.libraries.oidc.impl.customtab import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcActionFlow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @@ -34,11 +34,11 @@ class DefaultOidcActionFlow @Inject constructor() : OidcActionFlow { mutableStateFlow.value = oidcAction } - suspend fun collect(collector: FlowCollector) { + override suspend fun collect(collector: FlowCollector) { mutableStateFlow.collect(collector) } - fun reset() { + override fun reset() { mutableStateFlow.value = null } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt similarity index 86% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt index 9d2f945e25..602d50628a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview -import io.element.android.features.login.api.oidc.OidcAction +import io.element.android.libraries.oidc.api.OidcAction sealed interface OidcEvents { data object Cancel : OidcEvents diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt index 5cd7cf0c3b..d31bdcd032 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt index 3f10ba256a..853a9b584c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -25,7 +25,7 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.login.api.oidc.OidcAction +import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt index dc4f26d51e..8b1b01b03d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.auth.OidcDetails diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt index 5d44657462..7127c9160e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt index c07078cdff..e8ddcd03c3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import android.webkit.WebView import androidx.activity.compose.BackHandler @@ -28,11 +28,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.viewinterop.AndroidView -import io.element.android.features.login.impl.oidc.OidcUrlParser import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.oidc.impl.OidcUrlParser @Composable fun OidcView( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt index a9cd576e6d..d20eabf00e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import android.webkit.WebResourceRequest import android.webkit.WebView diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt index 446754aced..8f587966bf 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview fun interface WebViewEventListener { /** diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt similarity index 94% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt rename to libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt index 6f63673b23..895e416faa 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParserTest.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.libraries.oidc.impl import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.matrix.api.auth.OidcConfig +import io.element.android.libraries.oidc.api.OidcAction import org.junit.Assert import org.junit.Test diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt similarity index 97% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt rename to libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt index 38d0506dd8..994334d2e9 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt @@ -16,17 +16,17 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package io.element.android.features.login.impl.oidc.webview +package io.element.android.libraries.oidc.impl.webview import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.oidc.api.OidcAction import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 9fd82af4ae..bd83d527ca 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -116,6 +116,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:mediaviewer:impl")) implementation(project(":libraries:troubleshoot:impl")) implementation(project(":libraries:fullscreenintent:impl")) + implementation(project(":libraries:oidc:impl")) } fun DependencyHandlerScope.allServicesImpl() { From 2283e0e361e6747412afcd0407d205dd6a4b5242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 8 Aug 2024 16:37:11 +0200 Subject: [PATCH 03/15] Use extracted OIDC flow to display the reset identity screen --- features/securebackup/impl/build.gradle.kts | 1 + .../impl/reset/ResetIdentityFlowNode.kt | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/features/securebackup/impl/build.gradle.kts b/features/securebackup/impl/build.gradle.kts index 41f3ba8942..a2a2e04a5d 100644 --- a/features/securebackup/impl/build.gradle.kts +++ b/features/securebackup/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.libraries.oidc.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) api(libs.statemachine) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt index a7c0011625..11dfb71c22 100644 --- 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 @@ -41,6 +41,7 @@ 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 io.element.android.libraries.oidc.api.OidcEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first @@ -53,6 +54,7 @@ class ResetIdentityFlowNode @AssistedInject constructor( @Assisted plugins: List, private val resetIdentityFlowManager: ResetIdentityFlowManager, private val coroutineScope: CoroutineScope, + private val oidcEntryPoint: OidcEntryPoint, ) : BaseFlowNode( backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap), buildContext = buildContext, @@ -69,8 +71,8 @@ class ResetIdentityFlowNode @AssistedInject constructor( @Parcelize data object ResetPassword : NavTarget -// @Parcelize -// data class ResetOidc(val url: String) : NavTarget + @Parcelize + data class ResetOidc(val url: String) : NavTarget } private lateinit var activity: Activity @@ -100,6 +102,9 @@ class ResetIdentityFlowNode @AssistedInject constructor( listOf(ResetKeyPasswordNode.Inputs(resetIdentityFlowManager.currentSessionId(), handle)) ) } + is NavTarget.ResetOidc -> { + oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.url) + } } } @@ -111,7 +116,11 @@ class ResetIdentityFlowNode @AssistedInject constructor( when (handle) { is IdentityOidcResetHandle -> { - activity.openUrlInChromeCustomTab(null, false, handle.url) + if (oidcEntryPoint.canUseCustomTab()) { + activity.openUrlInChromeCustomTab(null, false, handle.url) + } else { + backstack.push(NavTarget.ResetOidc(handle.url)) + } handle.resetOidc() } is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) From 252b3e11a0a1301d12836525323df3ff6291fdb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 9 Aug 2024 09:34:04 +0200 Subject: [PATCH 04/15] Improve existing APIs --- .../impl/reset/ResetIdentityFlowManager.kt | 10 ++--- .../impl/reset/ResetIdentityFlowNode.kt | 38 ++++++++++++++++++- .../androidutils/browser/ChromeCustomTab.kt | 7 ++++ .../api/encryption/EncryptionService.kt | 38 ++++++++++++++++++- .../encryption/RustIdentityResetHandle.kt | 13 +++++++ .../encryption/FakeIdentityResetHandle.kt | 14 ++++++- .../libraries/oidc/impl/webview/OidcView.kt | 34 +++++++++++++++-- 7 files changed, 140 insertions(+), 14 deletions(-) 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 index 76674cd87d..fa4f693cd3 100644 --- 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 @@ -17,8 +17,6 @@ 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 @@ -28,11 +26,8 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu 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 @@ -76,4 +71,9 @@ class ResetIdentityFlowManager @Inject constructor( resetHandleFlow } } + + suspend fun cancel() { + currentHandleFlow.value.dataOrNull()?.cancel() + resetHandleFlow.value = AsyncData.Uninitialized + } } 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 index 11dfb71c22..e144a52432 100644 --- 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 @@ -19,8 +19,12 @@ package io.element.android.features.securebackup.impl.reset import android.app.Activity import android.os.Parcelable import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -37,12 +41,14 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.oidc.api.OidcEntryPoint import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -76,6 +82,7 @@ class ResetIdentityFlowNode @AssistedInject constructor( } private lateinit var activity: Activity + private var resetJob: Job? = null override fun onBuilt() { super.onBuilt() @@ -83,6 +90,19 @@ class ResetIdentityFlowNode @AssistedInject constructor( resetIdentityFlowManager.whenResetIsDone { plugins().forEach { it.onDone() } } + + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + // If the custom tab was opened, we need to cancel the reset job + // when we come back to the node if it the reset wasn't successful + cancelResetJob() + } + + override fun onDestroy(owner: LifecycleOwner) { + // Make sure we cancel the reset job when the node is destroyed, just in case + cancelResetJob() + } + }) } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -121,15 +141,29 @@ class ResetIdentityFlowNode @AssistedInject constructor( } else { backstack.push(NavTarget.ResetOidc(handle.url)) } - handle.resetOidc() + resetJob = launch { handle.resetOidc() } } is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) } } + private fun cancelResetJob() { + resetJob?.cancel() + resetJob = null + coroutineScope.launch { resetIdentityFlowManager.cancel() } + } + @Composable override fun View(modifier: Modifier) { - (LocalContext.current as? Activity)?.let { activity = it } + // Workaround to get the current activity + if (!this::activity.isInitialized) { + activity = LocalContext.current as Activity + } + + val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState() + if (startResetState.isLoading()) { + ProgressDialog() + } BackstackView(modifier) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt index ec0d9662c7..425133b797 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt @@ -47,12 +47,19 @@ fun Activity.openUrlInChromeCustomTab( true -> CustomTabsIntent.COLOR_SCHEME_DARK } ) + .setShareIdentityEnabled(false) // Note: setting close button icon does not work // .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp)) // .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) // .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) .apply { session?.let { setSession(it) } } .build() + .apply { + // Disable download button + intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true) + // Disable bookmark button + intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true) + } .launchUrl(this, Uri.parse(url)) } catch (activityNotFoundException: ActivityNotFoundException) { // TODO context.toast(R.string.error_no_external_application_found) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index fdaebc9c97..420940aefe 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 @@ -64,16 +64,52 @@ interface EncryptionService { */ suspend fun deviceEd25519(): String? + /** + * Starts the identity reset process. This will return a handle that can be used to reset the identity. + */ suspend fun startIdentityReset(): Result } -interface IdentityResetHandle +/** + * A handle to reset the user's identity. + */ +interface IdentityResetHandle { + /** + * Cancel the reset process and drops the existing handle in the SDK. + */ + suspend fun cancel() +} +/** + * A handle to reset the user's identity with a password login type. + */ interface IdentityPasswordResetHandle : IdentityResetHandle { + /** + * Reset the password of the user. + * + * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is + * called, or the identity is reset. + * + * @param userId the user id of the user to reset the password for. + * @param password the current password, which will be validated before the process takes place. + */ suspend fun resetPassword(userId: UserId, password: String): Result } +/** + * A handle to reset the user's identity with an OIDC login type. + */ interface IdentityOidcResetHandle : IdentityResetHandle { + /** + * The URL to open in a webview/custom tab to reset the identity. + */ val url: String + + /** + * Reset the identity using the OIDC flow. + * + * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is + * called, or the identity is reset. + */ suspend fun resetOidc(): Result } 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 index 69c7ecdd31..0d51d7c4c2 100644 --- 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 @@ -42,6 +42,10 @@ class RustPasswordIdentityResetHandle( override suspend fun resetPassword(userId: UserId, password: String): Result { return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) } } + + override suspend fun cancel() { + identityResetHandle.cancelAndDestroy() + } } class RustOidcIdentityResetHandle( @@ -51,4 +55,13 @@ class RustOidcIdentityResetHandle( override suspend fun resetOidc(): Result { return runCatching { identityResetHandle.reset(null) } } + + override suspend fun cancel() { + identityResetHandle.cancelAndDestroy() + } +} + +private suspend fun org.matrix.rustcomponents.sdk.IdentityResetHandle.cancelAndDestroy() { + cancel() + destroy() } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt index bf0aa312be..6b3eabd102 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt @@ -22,17 +22,27 @@ import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetH class FakeIdentityOidcResetHandle( override val url: String = "", - var resetOidcLambda: () -> Result = { error("Not implemented") } + var resetOidcLambda: () -> Result = { error("Not implemented") }, + var cancelLambda: () -> Unit = { error("Not implemented") }, ) : IdentityOidcResetHandle { override suspend fun resetOidc(): Result { return resetOidcLambda() } + + override suspend fun cancel() { + cancelLambda() + } } class FakeIdentityPasswordResetHandle( - var resetPasswordLambda: (UserId, String) -> Result = { _, _ -> error("Not implemented") } + var resetPasswordLambda: (UserId, String) -> Result = { _, _ -> error("Not implemented") }, + var cancelLambda: () -> Unit = { error("Not implemented") }, ) : IdentityPasswordResetHandle { override suspend fun resetPassword(userId: UserId, password: String): Result { return resetPasswordLambda(userId, password) } + + override suspend fun cancel() { + cancelLambda() + } } diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt index e8ddcd03c3..56a04cb0ea 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt @@ -16,10 +16,11 @@ package io.element.android.libraries.oidc.impl.webview +import android.annotation.SuppressLint import android.webkit.WebView import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,10 +31,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.viewinterop.AndroidView import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.oidc.impl.OidcUrlParser +@OptIn(ExperimentalMaterial3Api::class) @Composable fun OidcView( state: OidcState, @@ -55,7 +60,7 @@ fun OidcView( OidcWebViewClient(::shouldOverrideUrl) } - BackHandler { + fun onBack() { if (webView?.canGoBack().orFalse()) { webView?.goBack() } else { @@ -64,11 +69,32 @@ fun OidcView( } } - Box(modifier = modifier.statusBarsPadding()) { + BackHandler { onBack() } + + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + BackButton(onClick = ::onBack) + }, + ) + } + ) { contentPadding -> AndroidView( + modifier = Modifier.padding(contentPadding), factory = { context -> WebView(context).apply { webViewClient = oidcWebViewClient + settings.apply { + @SuppressLint("SetJavaScriptEnabled") + javaScriptEnabled = true + allowContentAccess = true + allowFileAccess = true + databaseEnabled = true + domStorageEnabled = true + } loadUrl(state.oidcDetails.url) }.also { webView = it From 903d24ea2fd8eed843e79af1be899bd2dee915f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 12 Aug 2024 12:22:36 +0200 Subject: [PATCH 05/15] Improve APIs, add tests --- .../impl/reset/ResetIdentityFlowManager.kt | 5 - .../impl/reset/ResetIdentityFlowNode.kt | 12 +- ...Event.kt => ResetIdentityPasswordEvent.kt} | 6 +- ...rdNode.kt => ResetIdentityPasswordNode.kt} | 12 +- ...r.kt => ResetIdentityPasswordPresenter.kt} | 22 ++-- ...State.kt => ResetIdentityPasswordState.kt} | 4 +- ...rdView.kt => ResetIdentityPasswordView.kt} | 14 +-- ...RootEvent.kt => ResetIdentityRootEvent.kt} | 6 +- ...eyRootNode.kt => ResetIdentityRootNode.kt} | 6 +- ...enter.kt => ResetIdentityRootPresenter.kt} | 12 +- ...RootState.kt => ResetIdentityRootState.kt} | 4 +- ...r.kt => ResetIdentityRootStateProvider.kt} | 8 +- ...eyRootView.kt => ResetIdentityRootView.kt} | 14 +-- .../reset/ResetIdentityFlowManagerTests.kt | 119 ++++++++++++++++++ .../ResetIdentityPasswordPresenterTest.kt | 96 ++++++++++++++ .../password/ResetIdentityPasswordViewTest.kt | 97 ++++++++++++++ .../root/ResetIdentityRootPresenterTests.kt | 65 ++++++++++ .../reset/root/ResetIdentityRootViewTests.kt | 107 ++++++++++++++++ .../api/encryption/EncryptionService.kt | 4 +- .../impl/encryption/RustEncryptionService.kt | 8 +- .../encryption/RustIdentityResetHandle.kt | 10 +- .../encryption/FakeIdentityResetHandle.kt | 7 +- 22 files changed, 562 insertions(+), 76 deletions(-) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/{ResetKeyPasswordEvent.kt => ResetIdentityPasswordEvent.kt} (79%) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/{ResetKeyPasswordNode.kt => ResetIdentityPasswordNode.kt} (81%) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/{ResetKeyPasswordPresenter.kt => ResetIdentityPasswordPresenter.kt} (68%) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/{ResetKeyPasswordState.kt => ResetIdentityPasswordState.kt} (89%) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/{ResetKeyPasswordView.kt => ResetIdentityPasswordView.kt} (92%) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/{ResetKeyRootEvent.kt => ResetIdentityRootEvent.kt} (82%) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/{ResetKeyRootNode.kt => ResetIdentityRootNode.kt} (91%) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/{ResetKeyRootPresenter.kt => ResetIdentityRootPresenter.kt} (78%) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/{ResetKeyRootState.kt => ResetIdentityRootState.kt} (89%) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/{ResetKeyRootStateProvider.kt => ResetIdentityRootStateProvider.kt} (81%) rename features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/{ResetKeyRootView.kt => ResetIdentityRootView.kt} (92%) create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTests.kt create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTests.kt create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTests.kt 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 index fa4f693cd3..3425ca90be 100644 --- 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 @@ -19,7 +19,6 @@ package io.element.android.features.securebackup.impl.reset import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.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 @@ -46,10 +45,6 @@ class ResetIdentityFlowManager @Inject constructor( } } - fun currentSessionId(): SessionId { - return matrixClient.sessionId - } - fun getResetHandle(): StateFlow> { return if (resetHandleFlow.value.isLoading() || resetHandleFlow.value.isSuccess()) { 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 index e144a52432..0f0278cb66 100644 --- 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 @@ -34,8 +34,8 @@ 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.features.securebackup.impl.reset.password.ResetIdentityPasswordNode +import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.BackstackView @@ -108,18 +108,18 @@ class ResetIdentityFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.Root -> { - val callback = object : ResetKeyRootNode.Callback { + val callback = object : ResetIdentityRootNode.Callback { override fun onContinue() { coroutineScope.startReset() } } - createNode(buildContext, listOf(callback)) + createNode(buildContext, listOf(callback)) } is NavTarget.ResetPassword -> { val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found") - createNode( + createNode( buildContext, - listOf(ResetKeyPasswordNode.Inputs(resetIdentityFlowManager.currentSessionId(), handle)) + listOf(ResetIdentityPasswordNode.Inputs(handle)) ) } is NavTarget.ResetOidc -> { 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/ResetIdentityPasswordEvent.kt similarity index 79% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordEvent.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordEvent.kt index 5fce7cdf85..b76dff920f 100644 --- 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/ResetIdentityPasswordEvent.kt @@ -16,7 +16,7 @@ package io.element.android.features.securebackup.impl.reset.password -sealed interface ResetKeyPasswordEvent { - data class Reset(val password: String) : ResetKeyPasswordEvent - data object DismissError : ResetKeyPasswordEvent +sealed interface ResetIdentityPasswordEvent { + data class Reset(val password: String) : ResetIdentityPasswordEvent + data object DismissError : ResetIdentityPasswordEvent } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt similarity index 81% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordNode.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt index c430bbec28..7397361b03 100644 --- 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/ResetIdentityPasswordNode.kt @@ -21,33 +21,33 @@ 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.core.coroutine.CoroutineDispatchers 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( +class ResetIdentityPasswordNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val coroutineDispatchers: CoroutineDispatchers, ) : Node(buildContext, plugins = plugins) { - data class Inputs(val userId: UserId, val handle: IdentityPasswordResetHandle) : NodeInputs + data class Inputs(val handle: IdentityPasswordResetHandle) : NodeInputs private val presenter by lazy { val inputs = inputs() - ResetKeyPasswordPresenter(inputs.userId, inputs.handle) + ResetIdentityPasswordPresenter(inputs.handle, dispatchers = coroutineDispatchers) } @Composable override fun View(modifier: Modifier) { val state = presenter.present() - ResetKeyPasswordView( + ResetIdentityPasswordView( 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/ResetIdentityPasswordPresenter.kt similarity index 68% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordPresenter.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenter.kt index 19fa002108..baa8ab2844 100644 --- 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/ResetIdentityPasswordPresenter.kt @@ -24,37 +24,37 @@ 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.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class ResetKeyPasswordPresenter( - private val userId: UserId, +class ResetIdentityPasswordPresenter( private val identityPasswordResetHandle: IdentityPasswordResetHandle, -) : Presenter { + private val dispatchers: CoroutineDispatchers, +) : Presenter { @Composable - override fun present(): ResetKeyPasswordState { + override fun present(): ResetIdentityPasswordState { val coroutineScope = rememberCoroutineScope() val resetAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } - fun handleEvent(event: ResetKeyPasswordEvent) { + fun handleEvent(event: ResetIdentityPasswordEvent) { when (event) { - is ResetKeyPasswordEvent.Reset -> coroutineScope.reset(userId, event.password, resetAction) - ResetKeyPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized + is ResetIdentityPasswordEvent.Reset -> coroutineScope.reset(event.password, resetAction) + ResetIdentityPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized } } - return ResetKeyPasswordState( + return ResetIdentityPasswordState( resetAction = resetAction.value, eventSink = ::handleEvent ) } - private fun CoroutineScope.reset(userId: UserId, password: String, action: MutableState>) = launch { + private fun CoroutineScope.reset(password: String, action: MutableState>) = launch(dispatchers.io) { suspend { - identityPasswordResetHandle.resetPassword(userId, password).getOrThrow() + identityPasswordResetHandle.resetPassword(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/ResetIdentityPasswordState.kt similarity index 89% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordState.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordState.kt index 3de58ec032..47662b623e 100644 --- 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/ResetIdentityPasswordState.kt @@ -18,7 +18,7 @@ package io.element.android.features.securebackup.impl.reset.password import io.element.android.libraries.architecture.AsyncAction -data class ResetKeyPasswordState( +data class ResetIdentityPasswordState( val resetAction: AsyncAction, - val eventSink: (ResetKeyPasswordEvent) -> Unit, + val eventSink: (ResetIdentityPasswordEvent) -> 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/ResetIdentityPasswordView.kt similarity index 92% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordView.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt index d9339064f2..d64e451a9f 100644 --- 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/ResetIdentityPasswordView.kt @@ -46,8 +46,8 @@ import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKe import io.element.android.libraries.ui.strings.CommonStrings @Composable -fun ResetKeyPasswordView( - state: ResetKeyPasswordState, +fun ResetIdentityPasswordView( + state: ResetIdentityPasswordState, onBack: () -> Unit, modifier: Modifier = Modifier, ) { @@ -63,7 +63,7 @@ fun ResetKeyPasswordView( Button( modifier = Modifier.fillMaxWidth(), text = stringResource(CommonStrings.action_reset_identity), - onClick = { state.eventSink(ResetKeyPasswordEvent.Reset(passwordState.value)) }, + onClick = { state.eventSink(ResetIdentityPasswordEvent.Reset(passwordState.value)) }, destructive = true, ) } @@ -74,7 +74,7 @@ fun ResetKeyPasswordView( } else if (state.resetAction.isFailure()) { ErrorDialog( content = stringResource(CommonStrings.error_unknown), - onDismiss = { state.eventSink(ResetKeyPasswordEvent.DismissError) } + onDismiss = { state.eventSink(ResetIdentityPasswordEvent.DismissError) } ) } } @@ -107,10 +107,10 @@ private fun Content(textFieldState: MutableState) { @PreviewsDayNight @Composable -internal fun ResetKeyPasswordViewPreview() { +internal fun ResetIdentityPasswordViewPreview() { ElementPreview { - ResetKeyPasswordView( - state = ResetKeyPasswordState( + ResetIdentityPasswordView( + state = ResetIdentityPasswordState( resetAction = AsyncAction.Uninitialized, eventSink = {} ), 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/ResetIdentityRootEvent.kt similarity index 82% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootEvent.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootEvent.kt index 268228ac8b..a1ec4cbe82 100644 --- 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/ResetIdentityRootEvent.kt @@ -16,7 +16,7 @@ package io.element.android.features.securebackup.impl.reset.root -sealed interface ResetKeyRootEvent { - data object Continue : ResetKeyRootEvent - data object DismissDialog : ResetKeyRootEvent +sealed interface ResetIdentityRootEvent { + data object Continue : ResetIdentityRootEvent + data object DismissDialog : ResetIdentityRootEvent } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt similarity index 91% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootNode.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt index b7171aaa7b..9fc5061ae2 100644 --- 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/ResetIdentityRootNode.kt @@ -27,7 +27,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class ResetKeyRootNode @AssistedInject constructor( +class ResetIdentityRootNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, ) : Node(buildContext, plugins = plugins) { @@ -35,13 +35,13 @@ class ResetKeyRootNode @AssistedInject constructor( fun onContinue() } - private val presenter = ResetKeyRootPresenter() + private val presenter = ResetIdentityRootPresenter() private val callback: Callback = plugins.filterIsInstance().first() @Composable override fun View(modifier: Modifier) { val state = presenter.present() - ResetKeyRootView( + ResetIdentityRootView( 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/ResetIdentityRootPresenter.kt similarity index 78% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootPresenter.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenter.kt index d2e3e5dc70..11c96e9ad8 100644 --- 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/ResetIdentityRootPresenter.kt @@ -23,19 +23,19 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Presenter -class ResetKeyRootPresenter : Presenter { +class ResetIdentityRootPresenter : Presenter { @Composable - override fun present(): ResetKeyRootState { + override fun present(): ResetIdentityRootState { var displayConfirmDialog by remember { mutableStateOf(false) } - fun handleEvent(event: ResetKeyRootEvent) { + fun handleEvent(event: ResetIdentityRootEvent) { displayConfirmDialog = when (event) { - ResetKeyRootEvent.Continue -> true - ResetKeyRootEvent.DismissDialog -> false + ResetIdentityRootEvent.Continue -> true + ResetIdentityRootEvent.DismissDialog -> false } } - return ResetKeyRootState( + return ResetIdentityRootState( 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/ResetIdentityRootState.kt similarity index 89% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootState.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootState.kt index faaee64040..de1054c97f 100644 --- 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/ResetIdentityRootState.kt @@ -16,7 +16,7 @@ package io.element.android.features.securebackup.impl.reset.root -data class ResetKeyRootState( +data class ResetIdentityRootState( val displayConfirmationDialog: Boolean, - val eventSink: (ResetKeyRootEvent) -> Unit, + val eventSink: (ResetIdentityRootEvent) -> 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/ResetIdentityRootStateProvider.kt similarity index 81% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootStateProvider.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt index 15299fa5ba..8d780343fe 100644 --- 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/ResetIdentityRootStateProvider.kt @@ -18,14 +18,14 @@ package io.element.android.features.securebackup.impl.reset.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider -class ResetKeyRootStateProvider : PreviewParameterProvider { - override val values: Sequence +class ResetIdentityRootStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - ResetKeyRootState( + ResetIdentityRootState( displayConfirmationDialog = false, eventSink = {} ), - ResetKeyRootState( + ResetIdentityRootState( 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/ResetIdentityRootView.kt similarity index 92% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootView.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt index cbeaa1a14e..8020aee5c6 100644 --- 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/ResetIdentityRootView.kt @@ -43,8 +43,8 @@ import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.persistentListOf @Composable -fun ResetKeyRootView( - state: ResetKeyRootState, +fun ResetIdentityRootView( + state: ResetIdentityRootState, onContinue: () -> Unit, onBack: () -> Unit, ) { @@ -58,7 +58,7 @@ fun ResetKeyRootView( Button( modifier = Modifier.fillMaxWidth(), text = stringResource(id = CommonStrings.action_continue), - onClick = { state.eventSink(ResetKeyRootEvent.Continue) }, + onClick = { state.eventSink(ResetIdentityRootEvent.Continue) }, destructive = true, ) }, @@ -71,11 +71,11 @@ fun ResetKeyRootView( content = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_subtitle), submitText = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_action), onSubmitClick = { - state.eventSink(ResetKeyRootEvent.DismissDialog) + state.eventSink(ResetIdentityRootEvent.DismissDialog) onContinue() }, destructiveSubmit = true, - onDismiss = { state.eventSink(ResetKeyRootEvent.DismissDialog) } + onDismiss = { state.eventSink(ResetIdentityRootEvent.DismissDialog) } ) } } @@ -138,9 +138,9 @@ private fun Content() { @PreviewsDayNight @Composable -internal fun ResetKeyRootViewPreview(@PreviewParameter(ResetKeyRootStateProvider::class) state: ResetKeyRootState) { +internal fun ResetIdentityRootViewPreview(@PreviewParameter(ResetIdentityRootStateProvider::class) state: ResetIdentityRootState) { ElementPreview { - ResetKeyRootView( + ResetIdentityRootView( state = state, onContinue = {}, onBack = {}, diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTests.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTests.kt new file mode 100644 index 0000000000..6669d0cc90 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTests.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ResetIdentityFlowManagerTests { + @Test + fun `getResetHandle - emits a reset handle`() = runTest { + val startResetLambda = lambdaRecorder> { Result.success(FakeIdentityPasswordResetHandle()) } + val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda) + val flowManager = createFlowManager(encryptionService = encryptionService) + + flowManager.getResetHandle().test { + assertThat(awaitItem().isLoading()).isTrue() + assertThat(awaitItem().isSuccess()).isTrue() + startResetLambda.assertions().isCalledOnce() + } + } + + @Test + fun `getResetHandle - om successful handle retrieval returns that same handle`() = runTest { + val startResetLambda = lambdaRecorder> { Result.success(FakeIdentityPasswordResetHandle()) } + val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda) + val flowManager = createFlowManager(encryptionService = encryptionService) + + var result: AsyncData.Success? = null + flowManager.getResetHandle().test { + assertThat(awaitItem().isLoading()).isTrue() + result = awaitItem() as? AsyncData.Success + assertThat(result).isNotNull() + } + + flowManager.getResetHandle().test { + assertThat(awaitItem()).isSameInstanceAs(result) + } + } + + @Test + fun `getResetHandle - will fail if it receives a null reset handle`() = runTest { + val startResetLambda = lambdaRecorder> { Result.success(null) } + val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda) + val flowManager = createFlowManager(encryptionService = encryptionService) + + flowManager.getResetHandle().test { + assertThat(awaitItem().isLoading()).isTrue() + assertThat(awaitItem().isFailure()).isTrue() + startResetLambda.assertions().isCalledOnce() + } + } + + @Test + fun `getResetHandle - fails gracefully when receiving an exception from the encryption service`() = runTest { + val startResetLambda = lambdaRecorder> { Result.failure(IllegalStateException("Failure")) } + val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda) + val flowManager = createFlowManager(encryptionService = encryptionService) + + flowManager.getResetHandle().test { + assertThat(awaitItem().isLoading()).isTrue() + assertThat(awaitItem().isFailure()).isTrue() + startResetLambda.assertions().isCalledOnce() + } + } + + @Test + fun `cancel - resets the state and calls cancel on the reset handle`() = runTest { + val cancelLambda = lambdaRecorder { } + val resetHandle = FakeIdentityPasswordResetHandle(cancelLambda = cancelLambda) + val startResetLambda = lambdaRecorder> { Result.success(resetHandle) } + val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda) + val flowManager = createFlowManager(encryptionService = encryptionService) + + flowManager.getResetHandle().test { + assertThat(awaitItem().isLoading()).isTrue() + assertThat(awaitItem().isSuccess()).isTrue() + + flowManager.cancel() + cancelLambda.assertions().isCalledOnce() + assertThat(awaitItem().isUninitialized()).isTrue() + } + } + + private fun TestScope.createFlowManager( + encryptionService: FakeEncryptionService = FakeEncryptionService(), + client: FakeMatrixClient = FakeMatrixClient(encryptionService = encryptionService), + sessionCoroutineScope: CoroutineScope = this, + sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), + ) = ResetIdentityFlowManager( + matrixClient = client, + sessionCoroutineScope = sessionCoroutineScope, + sessionVerificationService = sessionVerificationService, + ) +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt new file mode 100644 index 0000000000..059983df3d --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ResetIdentityPasswordPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.resetAction.isUninitialized()).isTrue() + } + } + + @Test + fun `present - Reset event succeeds`() = runTest { + val resetLambda = lambdaRecorder> { _ -> Result.success(Unit) } + val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda) + val presenter = createPresenter(identityResetHandle = resetHandle) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ResetIdentityPasswordEvent.Reset("password")) + assertThat(awaitItem().resetAction.isLoading()).isTrue() + assertThat(awaitItem().resetAction.isSuccess()).isTrue() + } + } + + @Test + fun `present - Reset event can fail gracefully`() = runTest { + val resetLambda = lambdaRecorder> { _ -> Result.failure(IllegalStateException("Failed")) } + val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda) + val presenter = createPresenter(identityResetHandle = resetHandle) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ResetIdentityPasswordEvent.Reset("password")) + assertThat(awaitItem().resetAction.isLoading()).isTrue() + assertThat(awaitItem().resetAction.isFailure()).isTrue() + } + } + + @Test + fun `present - DismissError event resets the state`() = runTest { + val resetLambda = lambdaRecorder> { _ -> Result.failure(IllegalStateException("Failed")) } + val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda) + val presenter = createPresenter(identityResetHandle = resetHandle) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ResetIdentityPasswordEvent.Reset("password")) + assertThat(awaitItem().resetAction.isLoading()).isTrue() + assertThat(awaitItem().resetAction.isFailure()).isTrue() + + initialState.eventSink(ResetIdentityPasswordEvent.DismissError) + assertThat(awaitItem().resetAction.isUninitialized()).isTrue() + } + } + + private fun TestScope.createPresenter( + identityResetHandle: FakeIdentityPasswordResetHandle = FakeIdentityPasswordResetHandle(), + ) = ResetIdentityPasswordPresenter( + identityPasswordResetHandle = identityResetHandle, + dispatchers = testCoroutineDispatchers(), + ) +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt new file mode 100644 index 0000000000..17029452db --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.pressBackKey +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ResetIdentityPasswordViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `pressing the back HW button invokes the expected callback`() { + ensureCalledOnce { + rule.setResetPasswordView( + ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), + onBack = it, + ) + rule.pressBackKey() + } + } + + @Test + fun `clicking on the back navigation button invokes the expected callback`() { + ensureCalledOnce { + rule.setResetPasswordView( + ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), + onBack = it, + ) + rule.pressBack() + } + } + + @Test + fun `clicking 'Reset identity' confirms the reset`() { + val eventsRecorder = EventsRecorder() + rule.setResetPasswordView( + ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = eventsRecorder), + ) + rule.onNodeWithText("Password").performTextInput("A password") + + rule.clickOn(CommonStrings.action_reset_identity) + + eventsRecorder.assertSingle(ResetIdentityPasswordEvent.Reset("A password")) + } + + @Test + fun `clicking OK dismisses the error dialog`() { + val eventsRecorder = EventsRecorder() + rule.setResetPasswordView( + ResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("A failure")), eventSink = eventsRecorder), + ) + rule.clickOn(CommonStrings.action_ok) + + eventsRecorder.assertSingle(ResetIdentityPasswordEvent.DismissError) + } +} + +private fun AndroidComposeTestRule.setResetPasswordView( + state: ResetIdentityPasswordState, + onBack: () -> Unit = EnsureNeverCalled(), +) { + setContent { + ResetIdentityPasswordView(state = state, onBack = onBack) + } +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTests.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTests.kt new file mode 100644 index 0000000000..5b669a0dc7 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTests.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ResetIdentityRootPresenterTests { + @Test + fun `present - initial state`() = runTest { + val presenter = ResetIdentityRootPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.displayConfirmationDialog).isFalse() + } + } + + @Test + fun `present - Continue event displays the confirmation dialog`() = runTest { + val presenter = ResetIdentityRootPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ResetIdentityRootEvent.Continue) + + assertThat(awaitItem().displayConfirmationDialog).isTrue() + } + } + + @Test + fun `present - DismissDialog event hides the confirmation dialog`() = runTest { + val presenter = ResetIdentityRootPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ResetIdentityRootEvent.Continue) + assertThat(awaitItem().displayConfirmationDialog).isTrue() + + initialState.eventSink(ResetIdentityRootEvent.DismissDialog) + assertThat(awaitItem().displayConfirmationDialog).isFalse() + } + } +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTests.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTests.kt new file mode 100644 index 0000000000..9367b3c9e0 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTests.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.root + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.pressBackKey +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class ResetIdentityRootViewTests { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `pressing the back HW button invokes the expected callback`() { + ensureCalledOnce { + rule.setResetRootView( + ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), + onBack = it, + ) + rule.pressBackKey() + } + } + + @Test + fun `clicking on the back navigation button invokes the expected callback`() { + ensureCalledOnce { + rule.setResetRootView( + ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), + onBack = it, + ) + rule.pressBack() + } + } + + @Test + @Config(qualifiers = "h720dp") + fun `clicking Continue displays the confirmation dialog`() { + val eventsRecorder = EventsRecorder() + rule.setResetRootView( + ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder), + ) + + rule.clickOn(CommonStrings.action_continue) + + eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue) + } + + @Test + fun `clicking 'Yes, reset now' confirms the reset`() { + ensureCalledOnce { + rule.setResetRootView( + ResetIdentityRootState(displayConfirmationDialog = true, eventSink = {}), + onContinue = it, + ) + rule.clickOn(CommonStrings.screen_reset_encryption_confirmation_alert_action) + } + } + + @Test + fun `clicking Cancel dismisses the dialog`() { + val eventsRecorder = EventsRecorder() + rule.setResetRootView( + ResetIdentityRootState(displayConfirmationDialog = true, eventSink = eventsRecorder), + ) + + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(ResetIdentityRootEvent.DismissDialog) + } +} + +private fun AndroidComposeTestRule.setResetRootView( + state: ResetIdentityRootState, + onBack: () -> Unit = EnsureNeverCalled(), + onContinue: () -> Unit = EnsureNeverCalled(), +) { + setContent { + ResetIdentityRootView(state = state, onContinue = onContinue, onBack = onBack) + } +} 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 420940aefe..5afb81648b 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,7 +16,6 @@ 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 @@ -90,10 +89,9 @@ interface IdentityPasswordResetHandle : IdentityResetHandle { * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is * called, or the identity is reset. * - * @param userId the user id of the user to reset the password for. * @param password the current password, which will be validated before the process takes place. */ - suspend fun resetPassword(userId: UserId, password: String): Result + suspend fun resetPassword(password: String): 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 728fbede55..ae681b2771 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.encryption import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress @@ -55,6 +56,7 @@ internal class RustEncryptionService( private val dispatchers: CoroutineDispatchers, ) : EncryptionService { private val service: Encryption = client.encryption() + private val sessionId = SessionId(client.session().userId) private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper() private val backupUploadStateMapper = BackupUploadStateMapper() @@ -201,6 +203,10 @@ internal class RustEncryptionService( } override suspend fun startIdentityReset(): Result { - return runCatching { service.resetIdentity()?.let(RustIdentityResetHandleFactory::create)?.getOrNull() } + return runCatching { + service.resetIdentity()?.let { handle -> + RustIdentityResetHandleFactory.create(sessionId, handle) + }?.getOrNull() + } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt index 0d51d7c4c2..c4c20eb7d6 100644 --- 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 @@ -25,21 +25,25 @@ import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails import org.matrix.rustcomponents.sdk.CrossSigningResetAuthType object RustIdentityResetHandleFactory { - fun create(identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle): Result { + fun create( + userId: UserId, + 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) + CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle) } } } } class RustPasswordIdentityResetHandle( + private val userId: UserId, private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle, ) : IdentityPasswordResetHandle { - override suspend fun resetPassword(userId: UserId, password: String): Result { + override suspend fun resetPassword(password: String): Result { return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt index 6b3eabd102..69087163d2 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.matrix.test.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 @@ -35,11 +34,11 @@ class FakeIdentityOidcResetHandle( } class FakeIdentityPasswordResetHandle( - var resetPasswordLambda: (UserId, String) -> Result = { _, _ -> error("Not implemented") }, + var resetPasswordLambda: (String) -> Result = { _ -> error("Not implemented") }, var cancelLambda: () -> Unit = { error("Not implemented") }, ) : IdentityPasswordResetHandle { - override suspend fun resetPassword(userId: UserId, password: String): Result { - return resetPasswordLambda(userId, password) + override suspend fun resetPassword(password: String): Result { + return resetPasswordLambda(password) } override suspend fun cancel() { From 0ddadd8c5a20073b4f919af30b6ff1657bddd8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 12 Aug 2024 12:41:04 +0200 Subject: [PATCH 06/15] Fix lint issues --- .../io/element/android/appnav/RootFlowNode.kt | 2 +- .../impl/reset/ResetIdentityFlowNode.kt | 2 +- .../reset/password/ResetIdentityPasswordNode.kt | 1 - .../reset/password/ResetIdentityPasswordView.kt | 14 +++++++++----- .../impl/reset/root/ResetIdentityRootNode.kt | 1 + .../impl/reset/root/ResetIdentityRootView.kt | 2 ++ ...rTests.kt => ResetIdentityRootPresenterTest.kt} | 2 +- .../android/libraries/matrix/api/MatrixClient.kt | 1 - .../libraries/oidc/impl/DefaultOidcEntryPoint.kt | 2 +- .../libraries/oidc/impl/webview/OidcPresenter.kt | 2 +- 10 files changed, 17 insertions(+), 12 deletions(-) rename features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/{ResetIdentityRootPresenterTests.kt => ResetIdentityRootPresenterTest.kt} (98%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index b651f4d77c..87e08b6bb9 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -56,9 +56,9 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData -import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcActionFlow +import io.element.android.libraries.sessionstorage.api.LoggedInState import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach 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 index 0f0278cb66..be538a8665 100644 --- 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 @@ -66,7 +66,7 @@ class ResetIdentityFlowNode @AssistedInject constructor( buildContext = buildContext, plugins = plugins, ) { - interface Callback: Plugin { + interface Callback : Plugin { fun onDone() } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt index 7397361b03..0f158e04c2 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt @@ -36,7 +36,6 @@ class ResetIdentityPasswordNode @AssistedInject constructor( @Assisted plugins: List, private val coroutineDispatchers: CoroutineDispatchers, ) : Node(buildContext, plugins = plugins) { - data class Inputs(val handle: IdentityPasswordResetHandle) : NodeInputs private val presenter by lazy { diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt index d64e451a9f..a43ff8092f 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt @@ -18,7 +18,6 @@ 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 @@ -58,7 +57,12 @@ fun ResetIdentityPasswordView( title = stringResource(CommonStrings.screen_reset_encryption_password_title), subTitle = stringResource(CommonStrings.screen_reset_encryption_password_subtitle), onBackClick = onBack, - content = { Content(textFieldState = passwordState) }, + content = { + Content( + text = passwordState.value, + onTextChange = { passwordState.value = it } + ) + }, buttons = { Button( modifier = Modifier.fillMaxWidth(), @@ -80,14 +84,14 @@ fun ResetIdentityPasswordView( } @Composable -private fun Content(textFieldState: MutableState) { +private fun Content(text: String, onTextChange: (String) -> Unit) { var showPassword by remember { mutableStateOf(false) } OutlinedTextField( modifier = Modifier .fillMaxWidth() .onTabOrEnterKeyFocusNext(LocalFocusManager.current), - value = textFieldState.value, - onValueChange = { text -> textFieldState.value = text }, + value = text, + onValueChange = onTextChange, label = { Text(stringResource(CommonStrings.common_password)) }, placeholder = { Text(stringResource(CommonStrings.screen_reset_encryption_password_placeholder)) }, singleLine = true, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt index 9fc5061ae2..3dd9876fc0 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt @@ -42,6 +42,7 @@ class ResetIdentityRootNode @AssistedInject constructor( override fun View(modifier: Modifier) { val state = presenter.present() ResetIdentityRootView( + modifier = modifier, state = state, onContinue = callback::onContinue, onBack = ::navigateUp, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt index 8020aee5c6..4e0457836f 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt @@ -47,8 +47,10 @@ fun ResetIdentityRootView( state: ResetIdentityRootState, onContinue: () -> Unit, onBack: () -> Unit, + modifier: Modifier = Modifier, ) { FlowStepPage( + modifier = modifier, 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), diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTests.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTest.kt similarity index 98% rename from features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTests.kt rename to features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTest.kt index 5b669a0dc7..feb00bf4de 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTests.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTest.kt @@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test -class ResetIdentityRootPresenterTests { +class ResetIdentityRootPresenterTest { @Test fun `present - initial state`() = runTest { val presenter = ResetIdentityRootPresenter() 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 0591e794ed..3209d49e03 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,7 +41,6 @@ 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/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt index 1a8ce960c5..185c27bf19 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt @@ -24,8 +24,8 @@ import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTa import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails -import io.element.android.libraries.oidc.impl.webview.OidcNode import io.element.android.libraries.oidc.api.OidcEntryPoint +import io.element.android.libraries.oidc.impl.webview.OidcNode import javax.inject.Inject @ContributesBinding(AppScope::class) diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt index 853a9b584c..5e886644f6 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt @@ -25,11 +25,11 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.oidc.api.OidcAction import kotlinx.coroutines.launch class OidcPresenter @AssistedInject constructor( From 04cb1b7d2e99cba73073955ed77dc3f325fac0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 12 Aug 2024 12:54:51 +0200 Subject: [PATCH 07/15] Fix `OidcView` preview --- .../libraries/oidc/impl/webview/OidcView.kt | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt index 56a04cb0ea..0ba9d4ea52 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.viewinterop.AndroidView import io.element.android.libraries.core.bool.orFalse @@ -45,6 +46,7 @@ fun OidcView( onNavigateBack: () -> Unit, modifier: Modifier = Modifier, ) { + val isPreview = LocalInspectionMode.current val oidcUrlParser = remember { OidcUrlParser() } var webView by remember { mutableStateOf(null) } fun shouldOverrideUrl(url: String): Boolean { @@ -86,16 +88,18 @@ fun OidcView( modifier = Modifier.padding(contentPadding), factory = { context -> WebView(context).apply { - webViewClient = oidcWebViewClient - settings.apply { - @SuppressLint("SetJavaScriptEnabled") - javaScriptEnabled = true - allowContentAccess = true - allowFileAccess = true - databaseEnabled = true - domStorageEnabled = true + if (!isPreview) { + webViewClient = oidcWebViewClient + settings.apply { + @SuppressLint("SetJavaScriptEnabled") + javaScriptEnabled = true + allowContentAccess = true + allowFileAccess = true + databaseEnabled = true + domStorageEnabled = true + } + loadUrl(state.oidcDetails.url) } - loadUrl(state.oidcDetails.url) }.also { webView = it } From b022652b0d1944c7f1ea34c9a05243d026f60450 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 12 Aug 2024 11:06:00 +0000 Subject: [PATCH 08/15] Update screenshots --- .../features.login.impl.oidc.webview_OidcView_Day_0_en.png | 3 --- .../features.login.impl.oidc.webview_OidcView_Day_1_en.png | 3 --- .../features.login.impl.oidc.webview_OidcView_Night_0_en.png | 3 --- .../features.login.impl.oidc.webview_OidcView_Night_1_en.png | 3 --- ...impl.reset.password_ResetIdentityPasswordView_Day_0_en.png | 3 +++ ...pl.reset.password_ResetIdentityPasswordView_Night_0_en.png | 3 +++ ...ebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png | 3 +++ ...ebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png | 3 +++ ...ackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png | 3 +++ ...ackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png | 3 +++ ...ures.verifysession.impl_VerifySelfSessionView_Day_0_en.png | 4 ++-- ...ures.verifysession.impl_VerifySelfSessionView_Day_1_en.png | 4 ++-- ...ures.verifysession.impl_VerifySelfSessionView_Day_7_en.png | 4 ++-- ...ures.verifysession.impl_VerifySelfSessionView_Day_8_en.png | 4 ++-- ...ures.verifysession.impl_VerifySelfSessionView_Day_9_en.png | 4 ++-- ...es.verifysession.impl_VerifySelfSessionView_Night_0_en.png | 4 ++-- ...es.verifysession.impl_VerifySelfSessionView_Night_1_en.png | 4 ++-- ...es.verifysession.impl_VerifySelfSessionView_Night_7_en.png | 4 ++-- ...es.verifysession.impl_VerifySelfSessionView_Night_8_en.png | 4 ++-- ...es.verifysession.impl_VerifySelfSessionView_Night_9_en.png | 4 ++-- .../images/libraries.oidc.impl.webview_OidcView_Day_0_en.png | 3 +++ .../images/libraries.oidc.impl.webview_OidcView_Day_1_en.png | 3 +++ .../libraries.oidc.impl.webview_OidcView_Night_0_en.png | 3 +++ .../libraries.oidc.impl.webview_OidcView_Night_1_en.png | 3 +++ 24 files changed, 50 insertions(+), 32 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_1_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_0_en.png deleted file mode 100644 index 1022157a43..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38b4c852a4bb957a10e24a72b70d7edd89a921983a9426592dc1fc53c1d54d99 -size 5450 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_1_en.png deleted file mode 100644 index 39071712f4..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9cdc07cc74bf427e7ae0244b97543f65aecb6b2f8fee7958d18906d333b868b8 -size 8848 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_0_en.png deleted file mode 100644 index 15ff876cf4..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0c39aabac135aa3954ac881a43cc2bd79a2e357ec76f9aa46351b2985c17d73 -size 5437 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_1_en.png deleted file mode 100644 index f99c8e006d..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.oidc.webview_OidcView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c773553943c9728f1e80b6ca6491c9c8c50f9394f3bd1cb430abb24029a3f926 -size 7791 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png new file mode 100644 index 0000000000..e82254087f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:688c50e8f414a6bdb3dd0e718f8de426badcea744509069a3023dfc2d2e59816 +size 29040 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png new file mode 100644 index 0000000000..9b905f24c2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:061cebcf9a9646fb6c962272692fa123d60b9e17cd6b29b581c17ef125880a9e +size 28166 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png new file mode 100644 index 0000000000..499713a15d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9b7a1e2d20171a78da4ce4f590372e1d84f9624c4d56a011e74f91105c09f36 +size 79989 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png new file mode 100644 index 0000000000..6bc0eec9c0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43f2eb2cb1f1b986b58d2614c892f2f7f15f1b696a656165ba1f8644f7759476 +size 62679 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png new file mode 100644 index 0000000000..8685aa5e42 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3c38d62929774fe6e6e003f656efbeec1f675b5dbb9994a917c4978f64d61a9 +size 78280 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png new file mode 100644 index 0000000000..e79557dae6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08e5a9d4e1bc45e9411b74c7277f34fa0ba8d46fdd1556b65b9c50cc78f1468e +size 60595 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png index b5f11147ba..cf48a305b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45548cd4afaac73a72ab4738c099506eb9dead138e8e0cf5668a75c4948b81f6 -size 26027 +oid sha256:ee9c90a91ef8703c4878ea9314121ad8343dd3392c051e7e3e3079198a32ef99 +size 30555 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png index a989b3b474..623f4d1906 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88f3de0736a207057ce0e71fea632e5197874d93045e87443a957ef0da64b8bd -size 22760 +oid sha256:0ee7ae6c83491b5cd52582a8b66e916b4b9b038427f7b7e9e02320bb0f210615 +size 22795 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png index 4a5ee01fb8..a2d07ed6a4 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:146a2a2439de11035332340ce27e622ff20b661d600fc2977d1f82deb81d0f21 -size 24828 +oid sha256:ce48c81c355317abd6b9de4600e86492c3acde2807ca3572e064b61abc06239c +size 29426 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png index a4cde41390..69eb60569f 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:281e08f29d84da5c1e7c01d701bf5d8f415b6d80fb8a1298742d6e629314a7d8 -size 21186 +oid sha256:4d9810f3b326a05492c870ef22dbceec19112dba8beaa000066504cc96d1387a +size 23913 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png index 4479a39460..0bd44d7bc8 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd68268fbc334f92f66413c8f63d9013468f6fa95d5b6264fdfe52e50637399e -size 26627 +oid sha256:892825ecd6a28010a152463e3a6e04ab50504e62d71b24944b144cb897fe3af2 +size 26626 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png index 6ec093bbc8..31ba47d4a4 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf610f5f758e65ae67836a2b40924e966a8add94fd00605c89ef75ddbd3bf22c -size 25324 +oid sha256:453cab91aa056f3536c899354a2fb5de21519e7dce3d5675b1cbca702000e2c1 +size 29602 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png index fddd555a33..8169f8b008 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2ed85817f1d95d1259e07aa566ede6d40360a688e29c0c463378770dd091540 -size 22003 +oid sha256:9653f24a7ba32ad19feb7c79a8fa5fc0f54e027f28b70c7e841fed77a682b129 +size 22051 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png index 130cf67cf4..39ce0ef07f 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:354c1daceebd169b35a6db0173c954a3425c534156fdc662edfba4d710f51a49 -size 24155 +oid sha256:c21120a08c871c7f64952c20be2c90b28590901f8e52893702775b6b2f176892 +size 28524 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png index 4c87611ada..7d1bfb8eab 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d6ce5c3b44127d5d661ac450ea2cf48617e81e4ae40dfb6d9a5cf89f3591800 -size 20598 +oid sha256:1e46ffc3c3679eb5490d01c34d02314bae5611a7b10fb3c758331a28ae78f065 +size 23267 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png index 89268c76ed..6b5f9e58f8 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee39cc4f450e906a2e11ddb13ce4913ca442fa9bca8c95a7a50a9e51fbe2b3f6 -size 25981 +oid sha256:396cbf209b0b899a0f874556d4e47a8204aa9ff1cff69e464b91e09f389488c1 +size 25946 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png new file mode 100644 index 0000000000..785f56af7f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:154a56d848cc735efc25274052ce92b1a20bc8f80f75e91d0d4cfc7d488cd246 +size 5874 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png new file mode 100644 index 0000000000..ae8924e66e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0915d4781155524bece1db737050ceb78ef76128da39b339fd67dd4a2a4dd79d +size 9229 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png new file mode 100644 index 0000000000..c41c1c7921 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2dd3462935091cfe022f0e6fc71b082eed7dab4769def13398a81a90f871b61a +size 5807 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png new file mode 100644 index 0000000000..98b81898ef --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7cd23334c15b9039cbf0924a82703035b4498e67489418e5244cf2df576af9c +size 8111 From 4f1adb16090e035b043ea2d46c7c0741c531c09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 12 Aug 2024 13:59:33 +0200 Subject: [PATCH 09/15] Fix vertical position for the primary button in `VerifySelfSessionView` --- .../features/verifysession/impl/VerifySelfSessionView.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 f1c169b193..878030d9ec 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 @@ -54,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.ButtonSize 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 @@ -260,6 +261,7 @@ private fun BottomMenu( onClick = onEnterRecoveryKey, ) } + // This option should always be displayed TextButton( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.screen_identity_confirmation_cannot_confirm), @@ -303,6 +305,8 @@ private fun BottomMenu( onClick = {}, showProgress = true, ) + // Placeholder so the 1st button keeps its vertical position + Spacer(modifier = Modifier.height(40.dp)) } } is FlowStep.Verifying -> { @@ -336,6 +340,8 @@ private fun BottomMenu( text = stringResource(CommonStrings.action_continue), onClick = onFinish, ) + // Placeholder so the 1st button keeps its vertical position + Spacer(modifier = Modifier.height(48.dp)) } } is FlowStep.Skipped -> return From b3e811bc7ebd31f25fab0b0d1a012c1925965d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 12 Aug 2024 13:59:42 +0200 Subject: [PATCH 10/15] Fix Konsist failures --- ...ntityFlowManagerTests.kt => ResetIdentityFlowManagerTest.kt} | 2 +- ...setIdentityRootViewTests.kt => ResetIdentityRootViewTest.kt} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/{ResetIdentityFlowManagerTests.kt => ResetIdentityFlowManagerTest.kt} (99%) rename features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/{ResetIdentityRootViewTests.kt => ResetIdentityRootViewTest.kt} (99%) diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTests.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt similarity index 99% rename from features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTests.kt rename to features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt index 6669d0cc90..3a1e1898e3 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTests.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test -class ResetIdentityFlowManagerTests { +class ResetIdentityFlowManagerTest { @Test fun `getResetHandle - emits a reset handle`() = runTest { val startResetLambda = lambdaRecorder> { Result.success(FakeIdentityPasswordResetHandle()) } diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTests.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt similarity index 99% rename from features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTests.kt rename to features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt index 9367b3c9e0..8d14618fe8 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTests.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt @@ -34,7 +34,7 @@ import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) -class ResetIdentityRootViewTests { +class ResetIdentityRootViewTest { @get:Rule val rule = createAndroidComposeRule() From 76a091629fe1024b57fd39244d2de3a35fafc75b Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 12 Aug 2024 12:12:43 +0000 Subject: [PATCH 11/15] Update screenshots --- ...ures.verifysession.impl_VerifySelfSessionView_Day_1_en.png | 4 ++-- ...ures.verifysession.impl_VerifySelfSessionView_Day_9_en.png | 4 ++-- ...es.verifysession.impl_VerifySelfSessionView_Night_1_en.png | 4 ++-- ...es.verifysession.impl_VerifySelfSessionView_Night_9_en.png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png index 623f4d1906..bfef3aada5 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ee7ae6c83491b5cd52582a8b66e916b4b9b038427f7b7e9e02320bb0f210615 -size 22795 +oid sha256:a4b44b23029dd1ff76a082a29c0b39584a1e20c92795eb4815d632608abd2858 +size 22702 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png index 0bd44d7bc8..4479a39460 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:892825ecd6a28010a152463e3a6e04ab50504e62d71b24944b144cb897fe3af2 -size 26626 +oid sha256:cd68268fbc334f92f66413c8f63d9013468f6fa95d5b6264fdfe52e50637399e +size 26627 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png index 8169f8b008..89dcb3654d 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9653f24a7ba32ad19feb7c79a8fa5fc0f54e027f28b70c7e841fed77a682b129 -size 22051 +oid sha256:f468a4a9140c4dc23ab3d394049f59b86f0ca2f332bee50abf8eaa0acce166b5 +size 22030 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png index 6b5f9e58f8..89268c76ed 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:396cbf209b0b899a0f874556d4e47a8204aa9ff1cff69e464b91e09f389488c1 -size 25946 +oid sha256:ee39cc4f450e906a2e11ddb13ce4913ca442fa9bca8c95a7a50a9e51fbe2b3f6 +size 25981 From 429853b95746bb0c7226e6798551a7882364d7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 12 Aug 2024 15:32:47 +0200 Subject: [PATCH 12/15] Remove unused import --- .../android/features/verifysession/impl/VerifySelfSessionView.kt | 1 - 1 file changed, 1 deletion(-) 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 878030d9ec..446114752e 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 @@ -54,7 +54,6 @@ 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.ButtonSize 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 From 08ed272991f9717baadfeccc556e4daa3ad18a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 13 Aug 2024 08:54:56 +0200 Subject: [PATCH 13/15] Add an extra test --- .../reset/ResetIdentityFlowManagerTest.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt index 3a1e1898e3..eb33fe5c36 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTest.kt @@ -20,13 +20,16 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test @@ -106,6 +109,32 @@ class ResetIdentityFlowManagerTest { } } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `whenResetIsDone - will trigger the lambda when verification status is verified`() = runTest { + val verificationService = FakeSessionVerificationService() + val flowManager = createFlowManager(sessionVerificationService = verificationService) + var isDone = false + + flowManager.whenResetIsDone { + isDone = true + } + + assertThat(isDone).isFalse() + + verificationService.emitVerifiedStatus(SessionVerifiedStatus.Unknown) + advanceUntilIdle() + assertThat(isDone).isFalse() + + verificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified) + advanceUntilIdle() + assertThat(isDone).isFalse() + + verificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified) + advanceUntilIdle() + assertThat(isDone).isTrue() + } + private fun TestScope.createFlowManager( encryptionService: FakeEncryptionService = FakeEncryptionService(), client: FakeMatrixClient = FakeMatrixClient(encryptionService = encryptionService), From 9c09f96c0b216e9ac3cfd2314541101050a6270f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 14 Aug 2024 16:53:50 +0200 Subject: [PATCH 14/15] Fix most review comments --- .../impl/reset/ResetIdentityFlowManager.kt | 7 ++- .../impl/reset/ResetIdentityFlowNode.kt | 48 +++++++++++-------- .../password/ResetIdentityPasswordNode.kt | 10 ++-- .../ResetIdentityPasswordStateProvider.kt | 38 +++++++++++++++ .../password/ResetIdentityPasswordView.kt | 40 +++++++++------- .../impl/reset/root/ResetIdentityRootView.kt | 19 ++++---- .../impl/src/main/res/values/localazy.xml | 13 +++++ .../password/ResetIdentityPasswordViewTest.kt | 4 +- .../reset/root/ResetIdentityRootViewTest.kt | 3 +- .../test/encryption/FakeEncryptionService.kt | 3 +- libraries/oidc/api/build.gradle.kts | 2 +- libraries/oidc/impl/build.gradle.kts | 10 +--- .../src/main/res/values/localazy.xml | 12 ----- tools/localazy/config.json | 5 +- 14 files changed, 136 insertions(+), 78 deletions(-) create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt 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 index 3425ca90be..16850890a7 100644 --- 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 @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterIsInstance @@ -37,9 +38,10 @@ class ResetIdentityFlowManager @Inject constructor( ) { private val resetHandleFlow: MutableStateFlow> = MutableStateFlow(AsyncData.Uninitialized) val currentHandleFlow: StateFlow> = resetHandleFlow + private var whenResetIsDoneWaitingJob: Job? = null fun whenResetIsDone(block: () -> Unit) { - sessionCoroutineScope.launch { + whenResetIsDoneWaitingJob = sessionCoroutineScope.launch { sessionVerificationService.sessionVerifiedStatus.filterIsInstance().first() block() } @@ -70,5 +72,8 @@ class ResetIdentityFlowManager @Inject constructor( suspend fun cancel() { currentHandleFlow.value.dataOrNull()?.cancel() resetHandleFlow.value = AsyncData.Uninitialized + + whenResetIsDoneWaitingJob?.cancel() + whenResetIsDoneWaitingJob = null } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt index be538a8665..50dcc2dd12 100644 --- 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 @@ -23,6 +23,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.bumble.appyx.core.modality.BuildContext @@ -45,14 +46,13 @@ import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle -import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.oidc.api.OidcEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize +import timber.log.Timber @ContributesNode(SessionScope::class) class ResetIdentityFlowNode @AssistedInject constructor( @@ -94,7 +94,7 @@ class ResetIdentityFlowNode @AssistedInject constructor( lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { // If the custom tab was opened, we need to cancel the reset job - // when we come back to the node if it the reset wasn't successful + // when we come back to the node if the reset wasn't successful cancelResetJob() } @@ -129,22 +129,29 @@ class ResetIdentityFlowNode @AssistedInject constructor( } private fun CoroutineScope.startReset() = launch { - val handle = resetIdentityFlowManager.getResetHandle() - .filterIsInstance>() - .first() - .data - - when (handle) { - is IdentityOidcResetHandle -> { - if (oidcEntryPoint.canUseCustomTab()) { - activity.openUrlInChromeCustomTab(null, false, handle.url) - } else { - backstack.push(NavTarget.ResetOidc(handle.url)) + resetIdentityFlowManager.getResetHandle() + .collectLatest { state -> + when (state) { + is AsyncData.Failure -> { + cancelResetJob() + Timber.e(state.error, "Could not load the reset identity handle.") + } + is AsyncData.Success -> { + when (val handle = state.data) { + is IdentityOidcResetHandle -> { + if (oidcEntryPoint.canUseCustomTab()) { + activity.openUrlInChromeCustomTab(null, false, handle.url) + } else { + backstack.push(NavTarget.ResetOidc(handle.url)) + } + resetJob = launch { handle.resetOidc() } + } + is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) + } + } + else -> Unit } - resetJob = launch { handle.resetOidc() } } - is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) - } } private fun cancelResetJob() { @@ -162,7 +169,10 @@ class ResetIdentityFlowNode @AssistedInject constructor( val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState() if (startResetState.isLoading()) { - ProgressDialog() + ProgressDialog( + properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true), + onDismissRequest = { cancelResetJob() } + ) } BackstackView(modifier) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt index 0f158e04c2..75d487d00e 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt @@ -34,14 +34,14 @@ import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetH class ResetIdentityPasswordNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val coroutineDispatchers: CoroutineDispatchers, + coroutineDispatchers: CoroutineDispatchers, ) : Node(buildContext, plugins = plugins) { data class Inputs(val handle: IdentityPasswordResetHandle) : NodeInputs - private val presenter by lazy { - val inputs = inputs() - ResetIdentityPasswordPresenter(inputs.handle, dispatchers = coroutineDispatchers) - } + private val presenter = ResetIdentityPasswordPresenter( + identityPasswordResetHandle = inputs().handle, + dispatchers = coroutineDispatchers + ) @Composable override fun View(modifier: Modifier) { diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt new file mode 100644 index 0000000000..1a1e768426 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.reset.password + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction + +class ResetIdentityPasswordStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aResetIdentityPasswordState(), + aResetIdentityPasswordState(resetAction = AsyncAction.Loading), + aResetIdentityPasswordState(resetAction = AsyncAction.Success(Unit)), + aResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("Failed"))), + ) +} + +private fun aResetIdentityPasswordState( + resetAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (ResetIdentityPasswordEvent) -> Unit = {}, +) = ResetIdentityPasswordState( + resetAction = resetAction, + eventSink = eventSink, +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt index a43ff8092f..074752ddea 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt @@ -27,12 +27,12 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.libraries.architecture.AsyncAction +import io.element.android.features.securebackup.impl.R import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.ProgressDialog -import io.element.android.libraries.designsystem.components.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 @@ -54,13 +54,19 @@ fun ResetIdentityPasswordView( 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), + title = stringResource(R.string.screen_reset_encryption_password_title), + subTitle = stringResource(R.string.screen_reset_encryption_password_subtitle), onBackClick = onBack, content = { Content( text = passwordState.value, - onTextChange = { passwordState.value = it } + onTextChange = { newText -> + if (state.resetAction.isFailure()) { + state.eventSink(ResetIdentityPasswordEvent.DismissError) + } + passwordState.value = newText + }, + hasError = state.resetAction.isFailure(), ) }, buttons = { @@ -69,22 +75,19 @@ fun ResetIdentityPasswordView( text = stringResource(CommonStrings.action_reset_identity), onClick = { state.eventSink(ResetIdentityPasswordEvent.Reset(passwordState.value)) }, destructive = true, + enabled = passwordState.value.isNotEmpty(), ) } ) + // On success we need to wait until the screen is automatically dismissed, so we keep the progress dialog if (state.resetAction.isLoading() || state.resetAction.isSuccess()) { ProgressDialog() - } else if (state.resetAction.isFailure()) { - ErrorDialog( - content = stringResource(CommonStrings.error_unknown), - onDismiss = { state.eventSink(ResetIdentityPasswordEvent.DismissError) } - ) } } @Composable -private fun Content(text: String, onTextChange: (String) -> Unit) { +private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Boolean) { var showPassword by remember { mutableStateOf(false) } OutlinedTextField( modifier = Modifier @@ -93,7 +96,7 @@ private fun Content(text: String, onTextChange: (String) -> Unit) { value = text, onValueChange = onTextChange, label = { Text(stringResource(CommonStrings.common_password)) }, - placeholder = { Text(stringResource(CommonStrings.screen_reset_encryption_password_placeholder)) }, + placeholder = { Text(stringResource(R.string.screen_reset_encryption_password_placeholder)) }, singleLine = true, visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { @@ -105,19 +108,22 @@ private fun Content(text: String, onTextChange: (String) -> Unit) { IconButton(onClick = { showPassword = !showPassword }) { Icon(imageVector = image, description) } + }, + isError = hasError, + supportingText = if (hasError) { + { Text(stringResource(R.string.screen_reset_encryption_password_error)) } + } else { + null } ) } @PreviewsDayNight @Composable -internal fun ResetIdentityPasswordViewPreview() { +internal fun ResetIdentityPasswordViewPreview(@PreviewParameter(ResetIdentityPasswordStateProvider::class) state: ResetIdentityPasswordState) { ElementPreview { ResetIdentityPasswordView( - state = ResetIdentityPasswordState( - resetAction = AsyncAction.Uninitialized, - eventSink = {} - ), + state = state, onBack = {} ) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt index 4e0457836f..9983f06b44 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.securebackup.impl.R import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage @@ -52,8 +53,8 @@ fun ResetIdentityRootView( FlowStepPage( modifier = modifier, 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), + title = stringResource(R.string.screen_encryption_reset_title), + subTitle = stringResource(R.string.screen_encryption_reset_subtitle), isScrollable = true, content = { Content() }, buttons = { @@ -69,9 +70,9 @@ fun ResetIdentityRootView( 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), + title = stringResource(R.string.screen_reset_encryption_confirmation_alert_title), + content = stringResource(R.string.screen_reset_encryption_confirmation_alert_subtitle), + submitText = stringResource(R.string.screen_reset_encryption_confirmation_alert_action), onSubmitClick = { state.eventSink(ResetIdentityRootEvent.DismissDialog) onContinue() @@ -92,7 +93,7 @@ private fun Content() { modifier = Modifier.fillMaxWidth(), items = persistentListOf( InfoListItem( - message = stringResource(CommonStrings.screen_encryption_reset_bullet_1), + message = stringResource(R.string.screen_encryption_reset_bullet_1), iconComposable = { Icon( modifier = Modifier.size(20.dp), @@ -103,7 +104,7 @@ private fun Content() { }, ), InfoListItem( - message = stringResource(CommonStrings.screen_encryption_reset_bullet_2), + message = stringResource(R.string.screen_encryption_reset_bullet_2), iconComposable = { Icon( modifier = Modifier.size(20.dp), @@ -114,7 +115,7 @@ private fun Content() { }, ), InfoListItem( - message = stringResource(CommonStrings.screen_encryption_reset_bullet_3), + message = stringResource(R.string.screen_encryption_reset_bullet_3), iconComposable = { Icon( modifier = Modifier.size(20.dp), @@ -130,7 +131,7 @@ private fun Content() { Text( modifier = Modifier.fillMaxWidth(), - text = stringResource(CommonStrings.screen_encryption_reset_footer), + text = stringResource(R.string.screen_encryption_reset_footer), style = ElementTheme.typography.fontBodyMdMedium, color = ElementTheme.colors.textActionPrimary, textAlign = TextAlign.Center, diff --git a/features/securebackup/impl/src/main/res/values/localazy.xml b/features/securebackup/impl/src/main/res/values/localazy.xml index e1159031a2..85e801fce1 100644 --- a/features/securebackup/impl/src/main/res/values/localazy.xml +++ b/features/securebackup/impl/src/main/res/values/localazy.xml @@ -16,6 +16,12 @@ "Follow the instructions to create a new recovery key" "Save your new recovery key in a password manager or encrypted note" "Reset the encryption for your account using another device" + "Your account details, contacts, preferences, and chat list will be kept" + "You will lose your existing message history" + "You will need to verify all your existing devices and contacts again" + "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key." + "If you’re not signed in to any other devices and you’ve lost your recovery key, then you’ll need to reset your identity to continue using the app. " + "Reset your identity in case you can’t confirm another way" "Turn off" "You will lose your encrypted messages if you are signed out of all devices." "Are you sure you want to turn off backup?" @@ -51,4 +57,11 @@ "Make sure you can store your recovery key somewhere safe" "Recovery setup successful" "Set up recovery" + "Yes, reset now" + "This process is irreversible." + "Are you sure you want to reset your encryption?" + "An unknown error happened. Please check your account password is correct and try again." + "Enter…" + "Confirm that you want to reset your encryption." + "Enter your account password to continue" diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt index 17029452db..2449146399 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt @@ -76,12 +76,12 @@ class ResetIdentityPasswordViewTest { } @Test - fun `clicking OK dismisses the error dialog`() { + fun `modifying the password dismisses the error state`() { val eventsRecorder = EventsRecorder() rule.setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("A failure")), eventSink = eventsRecorder), ) - rule.clickOn(CommonStrings.action_ok) + rule.onNodeWithText("Password").performTextInput("A password") eventsRecorder.assertSingle(ResetIdentityPasswordEvent.DismissError) } diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt index 8d14618fe8..da7f11ba42 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt @@ -20,6 +20,7 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.securebackup.impl.R import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder @@ -80,7 +81,7 @@ class ResetIdentityRootViewTest { ResetIdentityRootState(displayConfirmationDialog = true, eventSink = {}), onContinue = it, ) - rule.clickOn(CommonStrings.screen_reset_encryption_confirmation_alert_action) + rule.clickOn(R.string.screen_reset_encryption_confirmation_alert_action) } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index fa301e402e..82ab5e251f 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -22,13 +22,14 @@ import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf class FakeEncryptionService( - var startIdentityResetLambda: () -> Result = { error("Not implemented") }, + var startIdentityResetLambda: () -> Result = { lambdaError() }, ) : EncryptionService { private var disableRecoveryFailure: Exception? = null override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) diff --git a/libraries/oidc/api/build.gradle.kts b/libraries/oidc/api/build.gradle.kts index 874c3ddbd4..4e01547d2c 100644 --- a/libraries/oidc/api/build.gradle.kts +++ b/libraries/oidc/api/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. diff --git a/libraries/oidc/impl/build.gradle.kts b/libraries/oidc/impl/build.gradle.kts index be4181e169..db1cdf8d0a 100644 --- a/libraries/oidc/impl/build.gradle.kts +++ b/libraries/oidc/impl/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -43,13 +43,8 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) - implementation(projects.libraries.matrix.api) - implementation(projects.libraries.network) implementation(projects.libraries.designsystem) - implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) - implementation(projects.libraries.permissions.api) - implementation(projects.libraries.qrcode) implementation(libs.androidx.browser) implementation(platform(libs.network.retrofit.bom)) implementation(libs.network.retrofit) @@ -57,15 +52,12 @@ dependencies { api(projects.libraries.oidc.api) testImplementation(libs.test.junit) - testImplementation(libs.androidx.compose.ui.test.junit) testImplementation(libs.androidx.test.ext.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) - testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.permissions.test) testImplementation(projects.tests.testutils) - testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index ef997755dd..730afa291e 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -268,12 +268,6 @@ Reason: %1$s." "Hey, talk to me on %1$s: %2$s" "%1$s Android" "Rageshake to report bug" - "Your account details, contacts, preferences, and chat list will be kept" - "You will lose your existing message history" - "You will need to verify all your existing devices and contacts again" - "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key." - "If you’re not signed in to any other devices and you’ve lost your recovery key, then you’ll need to reset your identity to continue using the app. " - "Reset your identity in case you can’t confirm another way" "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." @@ -284,12 +278,6 @@ Reason: %1$s." "%1$d Pinned messages" "Pinned messages" - "Yes, reset now" - "This process is irreversible." - "Are you sure you want to reset your encryption?" - "Enter…" - "Confirm that you want to reset your encryption." - "Enter your account password to continue" "Pinned messages" "Failed processing media to upload, please try again." "Could not retrieve user details" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 47ef2c1c51..82fb4f2277 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -210,7 +210,10 @@ "screen_chat_backup_.*", "screen_key_backup_disable_.*", "screen_recovery_key_.*", - "screen_create_new_recovery_key_.*" + "screen_create_new_recovery_key_.*", + "screen_encryption_reset.*", + "screen_reset_encryption.*", + "screen\\.reset_encryption.*" ] }, { From 73b0cd97573188f5c8a91c8664bed8ddfa8edf30 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 14 Aug 2024 15:31:44 +0000 Subject: [PATCH 15/15] Update screenshots --- ...impl.reset.password_ResetIdentityPasswordView_Day_0_en.png | 4 ++-- ...impl.reset.password_ResetIdentityPasswordView_Day_1_en.png | 3 +++ ...impl.reset.password_ResetIdentityPasswordView_Day_2_en.png | 3 +++ ...impl.reset.password_ResetIdentityPasswordView_Day_3_en.png | 3 +++ ...pl.reset.password_ResetIdentityPasswordView_Night_0_en.png | 4 ++-- ...pl.reset.password_ResetIdentityPasswordView_Night_1_en.png | 3 +++ ...pl.reset.password_ResetIdentityPasswordView_Night_2_en.png | 3 +++ ...pl.reset.password_ResetIdentityPasswordView_Night_3_en.png | 3 +++ 8 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png index e82254087f..a49af920d9 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:688c50e8f414a6bdb3dd0e718f8de426badcea744509069a3023dfc2d2e59816 -size 29040 +oid sha256:e09769c04ba21aba09eb0de5865f659ad092ba2e46a8b3933f95b1170b09d303 +size 28540 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png new file mode 100644 index 0000000000..4c6ae5f149 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3702ebe296d7a14e20b3d683fb79f6b2455064a8986ce48c870b5cfd68dc5933 +size 27407 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png new file mode 100644 index 0000000000..4c6ae5f149 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3702ebe296d7a14e20b3d683fb79f6b2455064a8986ce48c870b5cfd68dc5933 +size 27407 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png new file mode 100644 index 0000000000..1d5e048087 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b683ed9986e3fd139ee735e51a369322ba3652b2b8a578c05a61f26ee27898e0 +size 39996 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png index 9b905f24c2..96ec36ca5e 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:061cebcf9a9646fb6c962272692fa123d60b9e17cd6b29b581c17ef125880a9e -size 28166 +oid sha256:14d4ed02bb2f949c4cbba84933bbbfb0c550ad261695a371ef12e74a2dbd8812 +size 27600 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png new file mode 100644 index 0000000000..c4e2dae54b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53f3dc9352250ba7495b6af312f3f370463291d29b76c40d4b3c340b77aa5712 +size 25455 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png new file mode 100644 index 0000000000..c4e2dae54b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53f3dc9352250ba7495b6af312f3f370463291d29b76c40d4b3c340b77aa5712 +size 25455 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png new file mode 100644 index 0000000000..0fa0e6a519 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b380335d847fa37e5319541f46f341c5fe8016f3af48765cfe7537c59dd1f58a +size 38379