From 4345f26d0bdf4038281349091743f185e4ddb461 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 Feb 2024 16:43:09 +0100 Subject: [PATCH] Add a way to enter recovery key to verify the session. --- .../android/appnav/LoggedInFlowNode.kt | 26 ++++++++++--- features/securebackup/api/build.gradle.kts | 1 + .../api/SecureBackupEntryPoint.kt | 26 ++++++++++++- .../impl/DefaultSecureBackupEntryPoint.kt | 16 +++++++- .../securebackup/impl/SecureBackupFlowNode.kt | 6 ++- .../api/VerifySessionEntryPoint.kt | 18 ++++++++- .../impl/DefaultVerifySessionEntryPoint.kt | 16 +++++++- .../impl/VerifySelfSessionNode.kt | 10 +++++ .../impl/VerifySelfSessionPresenter.kt | 17 ++++++-- .../impl/VerifySelfSessionState.kt | 2 +- .../impl/VerifySelfSessionStateProvider.kt | 21 ++++++---- .../impl/VerifySelfSessionView.kt | 39 ++++++++++++++----- .../impl/VerifySelfSessionPresenterTests.kt | 35 ++++++++++++++--- .../src/main/res/values/localazy.xml | 1 + 14 files changed, 193 insertions(+), 41 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 3985128b21..9bb9231dc2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -216,7 +216,9 @@ class LoggedInFlowNode @AssistedInject constructor( data object VerifySession : NavTarget @Parcelize - data object SecureBackup : NavTarget + data class SecureBackup( + val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root + ) : NavTarget @Parcelize data object InviteList : NavTarget @@ -298,7 +300,7 @@ class LoggedInFlowNode @AssistedInject constructor( } override fun onSecureBackupClicked() { - backstack.push(NavTarget.SecureBackup) + backstack.push(NavTarget.SecureBackup()) } override fun onOpenRoomNotificationSettings(roomId: RoomId) { @@ -324,10 +326,24 @@ class LoggedInFlowNode @AssistedInject constructor( .build() } NavTarget.VerifySession -> { - verifySessionEntryPoint.createNode(this, buildContext) + val callback = object : VerifySessionEntryPoint.Callback { + override fun onEnterRecoveryKey() { + backstack.replace( + NavTarget.SecureBackup( + initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey + ) + ) + } + } + verifySessionEntryPoint + .nodeBuilder(this, buildContext) + .callback(callback) + .build() } - NavTarget.SecureBackup -> { - secureBackupEntryPoint.createNode(this, buildContext) + is NavTarget.SecureBackup -> { + secureBackupEntryPoint.nodeBuilder(this, buildContext) + .params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement)) + .build() } NavTarget.InviteList -> { val callback = object : InviteListEntryPoint.Callback { diff --git a/features/securebackup/api/build.gradle.kts b/features/securebackup/api/build.gradle.kts index c9117d1d39..015cc600f2 100644 --- a/features/securebackup/api/build.gradle.kts +++ b/features/securebackup/api/build.gradle.kts @@ -16,6 +16,7 @@ plugins { id("io.element.android-library") + id("kotlin-parcelize") } android { 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 8824fdf84b..1fee6418a9 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 @@ -16,6 +16,28 @@ package io.element.android.features.securebackup.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import android.os.Parcelable +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.NodeInputs +import kotlinx.parcelize.Parcelize -interface SecureBackupEntryPoint : SimpleFeatureEntryPoint +interface SecureBackupEntryPoint : FeatureEntryPoint { + sealed interface InitialTarget : Parcelable { + @Parcelize + data object Root : InitialTarget + + @Parcelize + data object EnterRecoveryKey : InitialTarget + } + + data class Params(val initialElement: InitialTarget) : NodeInputs + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun params(params: Params): NodeBuilder + fun build(): Node + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt index a870255a9a..e3d5fde961 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.securebackup.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,18 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultSecureBackupEntryPoint @Inject constructor() : SecureBackupEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SecureBackupEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : SecureBackupEntryPoint.NodeBuilder { + override fun params(params: SecureBackupEntryPoint.Params): SecureBackupEntryPoint.NodeBuilder { + plugins += params + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } 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 62eecd09c2..172c6672e5 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 @@ -27,6 +27,7 @@ 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.api.SecureBackupEntryPoint 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 @@ -44,7 +45,10 @@ class SecureBackupFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Root, + initialElement = when (plugins.filterIsInstance(SecureBackupEntryPoint.Params::class.java).first().initialElement) { + SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root + SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey + }, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, 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 933ca1994f..5eb1bc8daa 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 @@ -16,6 +16,20 @@ package io.element.android.features.verifysession.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint -interface VerifySessionEntryPoint : SimpleFeatureEntryPoint +interface VerifySessionEntryPoint : FeatureEntryPoint { + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onEnterRecoveryKey() + } +} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt index da8c22e756..b514742a87 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.verifysession.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,18 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultVerifySessionEntryPoint @Inject constructor() : VerifySessionEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): VerifySessionEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : VerifySessionEntryPoint.NodeBuilder { + override fun callback(callback: VerifySessionEntryPoint.Callback): VerifySessionEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } 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 7612f9292c..c7369aab0e 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 @@ -21,9 +21,11 @@ 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.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) @@ -32,12 +34,20 @@ class VerifySelfSessionNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: VerifySelfSessionPresenter, ) : Node(buildContext, plugins = plugins) { + + private fun onEnterRecoveryKey() { + plugins().forEach { + it.onEnterRecoveryKey() + } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() VerifySelfSessionView( state = state, modifier = modifier, + onEnterRecoveryKey = { onEnterRecoveryKey() }, goBack = { navigateUp() } ) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt index 4d6889f716..2a7740d8d0 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt @@ -20,12 +20,15 @@ package io.element.android.features.verifysession.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.freeletics.flowredux.compose.rememberStateAndDispatch import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationFlowState import kotlinx.coroutines.CoroutineScope @@ -38,6 +41,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionStateMach class VerifySelfSessionPresenter @Inject constructor( private val sessionVerificationService: SessionVerificationService, + private val encryptionService: EncryptionService, private val stateMachine: VerifySelfSessionStateMachine, ) : Presenter { @Composable @@ -46,9 +50,14 @@ class VerifySelfSessionPresenter @Inject constructor( // Force reset, just in case the service was left in a broken state sessionVerificationService.reset() } + val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val stateAndDispatch = stateMachine.rememberStateAndDispatch() val verificationFlowStep by remember { - derivedStateOf { stateAndDispatch.state.value.toVerificationStep() } + derivedStateOf { + stateAndDispatch.state.value.toVerificationStep( + canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE + ) + } } // Start this after observing state machine LaunchedEffect(Unit) { @@ -71,10 +80,12 @@ class VerifySelfSessionPresenter @Inject constructor( ) } - private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.VerificationStep = + private fun StateMachineState?.toVerificationStep( + canEnterRecoveryKey: Boolean + ): VerifySelfSessionState.VerificationStep = when (val machineState = this) { StateMachineState.Initial, null -> { - VerifySelfSessionState.VerificationStep.Initial + VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = canEnterRecoveryKey) } StateMachineState.RequestingVerification, StateMachineState.StartingSasVerification, diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt index 1273367a71..fa3cb68adf 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt @@ -28,7 +28,7 @@ data class VerifySelfSessionState( ) { @Stable sealed interface VerificationStep { - data object Initial : VerificationStep + data class Initial(val canEnterRecoveryKey: Boolean) : VerificationStep data object Canceled : VerificationStep data object AwaitingOtherDeviceResponse : VerificationStep data object Ready : VerificationStep diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt index 81f25866bd..eeaf5c4e38 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt @@ -25,24 +25,27 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider get() = sequenceOf( aVerifySelfSessionState(), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized) ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading()) ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Canceled ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Ready ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized) ), + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true) + ), // Add other state here ) } @@ -59,8 +62,10 @@ private fun aDecimalsSessionVerificationData( return SessionVerificationData.Decimals(decimals) } -private fun aVerifySelfSessionState() = VerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial, +private fun aVerifySelfSessionState( + verificationFlowStep: VerifySelfSessionState.VerificationStep = VerifySelfSessionState.VerificationStep.Initial(false), +) = VerifySelfSessionState( + verificationFlowStep = verificationFlowStep, eventSink = {}, ) 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 e71df4c6d1..871825539a 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 @@ -62,6 +62,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver fun VerifySelfSessionView( state: VerifySelfSessionState, modifier: Modifier = Modifier, + onEnterRecoveryKey: () -> Unit, goBack: () -> Unit, ) { fun goBackAndCancelIfNeeded() { @@ -85,7 +86,11 @@ fun VerifySelfSessionView( }, footer = { if (buttonsVisible) { - BottomMenu(screenState = state, goBack = ::goBackAndCancelIfNeeded) + BottomMenu( + screenState = state, + goBack = ::goBackAndCancelIfNeeded, + onEnterRecoveryKey = onEnterRecoveryKey + ) } } ) { @@ -96,13 +101,13 @@ fun VerifySelfSessionView( @Composable private fun HeaderContent(verificationFlowStep: FlowStep) { val iconResourceId = when (verificationFlowStep) { - FlowStep.Initial -> R.drawable.ic_verification_devices + is FlowStep.Initial -> R.drawable.ic_verification_devices FlowStep.Canceled -> R.drawable.ic_verification_warning FlowStep.AwaitingOtherDeviceResponse -> R.drawable.ic_verification_waiting FlowStep.Ready, is FlowStep.Verifying, FlowStep.Completed -> R.drawable.ic_verification_emoji } val titleTextId = when (verificationFlowStep) { - FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title + is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title FlowStep.Canceled -> CommonStrings.common_verification_cancelled FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_title FlowStep.Ready, @@ -113,7 +118,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) { } } val subtitleTextId = when (verificationFlowStep) { - FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_subtitle + is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_subtitle FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_subtitle FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle @@ -136,7 +141,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) { private fun Content(flowState: FlowStep) { Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) { when (flowState) { - FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit + is FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit FlowStep.AwaitingOtherDeviceResponse -> ContentWaiting() is FlowStep.Verifying -> ContentVerifying(flowState) } @@ -203,13 +208,17 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie } @Composable -private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) { +private fun BottomMenu( + screenState: VerifySelfSessionState, + onEnterRecoveryKey: () -> Unit, + goBack: () -> Unit, +) { val verificationViewState = screenState.verificationFlowStep val eventSink = screenState.eventSink val isVerifying = (verificationViewState as? FlowStep.Verifying)?.state is AsyncData.Loading val positiveButtonTitle = when (verificationViewState) { - FlowStep.Initial -> R.string.screen_session_verification_positive_button_initial + is FlowStep.Initial -> R.string.screen_session_verification_positive_button_initial FlowStep.Canceled -> R.string.screen_session_verification_positive_button_canceled is FlowStep.Verifying -> { if (isVerifying) { @@ -222,7 +231,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) else -> null } val negativeButtonTitle = when (verificationViewState) { - FlowStep.Initial -> CommonStrings.action_cancel + is FlowStep.Initial -> CommonStrings.action_cancel FlowStep.Canceled -> CommonStrings.action_cancel is FlowStep.Verifying -> R.string.screen_session_verification_they_dont_match else -> null @@ -230,7 +239,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) val negativeButtonEnabled = !isVerifying val positiveButtonEvent = when (verificationViewState) { - FlowStep.Initial -> VerifySelfSessionViewEvents.RequestVerification + is FlowStep.Initial -> VerifySelfSessionViewEvents.RequestVerification FlowStep.Ready -> VerifySelfSessionViewEvents.StartSasVerification is FlowStep.Verifying -> if (!isVerifying) VerifySelfSessionViewEvents.ConfirmVerification else null FlowStep.Canceled -> VerifySelfSessionViewEvents.Restart @@ -263,6 +272,17 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) enabled = negativeButtonEnabled, ) } + if (verificationViewState is FlowStep.Initial && verificationViewState.canEnterRecoveryKey) { + Text( + text = stringResource(id = CommonStrings.common_or), + color = ElementTheme.colors.textSecondary, + ) + TextButton( + text = stringResource(R.string.screen_session_verification_enter_recovery_key), + modifier = Modifier.fillMaxWidth(), + onClick = onEnterRecoveryKey, + ) + } } } @@ -271,6 +291,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview { VerifySelfSessionView( state = state, + onEnterRecoveryKey = {}, goBack = {}, ) } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt index 240a4f0606..ad128b450d 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt @@ -23,9 +23,13 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationData +import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,7 +48,21 @@ class VerifySelfSessionPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) + } + } + + @Test + fun `present - Initial state is received, can use recovery key`() = runTest { + val presenter = createVerifySelfSessionPresenter( + encryptionService = FakeEncryptionService().apply { + emitRecoveryState(RecoveryState.INCOMPLETE) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(true)) } } @@ -67,7 +85,7 @@ class VerifySelfSessionPresenterTests { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) val eventSink = initialState.eventSink eventSink(VerifySelfSessionViewEvents.StartSasVerification) // Await for other device response: @@ -86,7 +104,7 @@ class VerifySelfSessionPresenterTests { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) val eventSink = initialState.eventSink eventSink(VerifySelfSessionViewEvents.CancelAndClose) expectNoEvents() @@ -203,7 +221,7 @@ class VerifySelfSessionPresenterTests { sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), ): VerifySelfSessionState { var state = awaitItem() - assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) state.eventSink(VerifySelfSessionViewEvents.RequestVerification) // Await for other device response: state = awaitItem() @@ -223,8 +241,13 @@ class VerifySelfSessionPresenterTests { } private fun createVerifySelfSessionPresenter( - service: FakeSessionVerificationService = FakeSessionVerificationService() + service: SessionVerificationService = FakeSessionVerificationService(), + encryptionService: EncryptionService = FakeEncryptionService(), ): VerifySelfSessionPresenter { - return VerifySelfSessionPresenter(service, VerifySelfSessionStateMachine(service)) + return VerifySelfSessionPresenter( + sessionVerificationService = service, + encryptionService = encryptionService, + stateMachine = VerifySelfSessionStateMachine(service), + ) } } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 77248a2339..85a2e78a85 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -141,6 +141,7 @@ "Mute" "No results" "Offline" + "or" "Password" "People" "Permalink"