diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt index 4cde3f927f..d619058199 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt @@ -13,6 +13,7 @@ package io.element.android.features.verifysession.impl.incoming import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect 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 @@ -26,6 +27,8 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.di.annotations.SessionCoroutineScope +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.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationFlowState @@ -45,6 +48,7 @@ class IncomingVerificationPresenter( @Assisted private val navigator: IncomingVerificationNavigator, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val sessionVerificationService: SessionVerificationService, + private val encryptionService: EncryptionService, private val stateMachine: IncomingVerificationStateMachine, private val dateFormatter: DateFormatter, ) : Presenter { @@ -92,12 +96,15 @@ class IncomingVerificationPresenter( mode = DateFormatterMode.TimeOrDate, ) } - val step by remember { + val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() + val isMissingSecrets = recoveryState == RecoveryState.INCOMPLETE + val step by remember(isMissingSecrets) { derivedStateOf { - stateAndDispatch.state.value.toVerificationStep( + val currentStep = stateAndDispatch.state.value.toVerificationStep( sessionVerificationRequestDetails = verificationRequest.details, formattedSignInTime = formattedSignInTime, ) + if (isMissingSecrets && currentStep is Step.Initial) Step.Unavailable else currentStep } } @@ -131,6 +138,7 @@ class IncomingVerificationPresenter( } else { stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge) } + Step.Unavailable, Step.Canceled, Step.Completed, Step.Failure -> navigator.onFinish() diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt index e3663800b2..a772cfe861 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt @@ -32,6 +32,8 @@ data class IncomingVerificationState( val isWaiting: Boolean, ) : Step + data object Unavailable : Step + data object Canceled : Step data object Completed : Step data object Failure : Step diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt index 68672be025..1214ba29ef 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt @@ -43,6 +43,7 @@ open class IncomingVerificationStatePreviewParam : PreviewParameterProvider BigIcon.Style.AlertSolid + Step.Canceled, + Step.Unavailable -> BigIcon.Style.AlertSolid is Step.Initial -> when (request) { is VerificationRequest.Incoming.OtherSession -> BigIcon.Style.Default(CompoundIcons.Devices()) is VerificationRequest.Incoming.User -> BigIcon.Style.Default(CompoundIcons.UserProfileSolid()) @@ -106,6 +107,7 @@ private fun IncomingVerificationHeader(step: Step, request: VerificationRequest. } val titleTextId = when (step) { Step.Canceled -> CommonStrings.common_verification_failed + Step.Unavailable -> R.string.screen_session_verification_unavailable_title is Step.Initial -> R.string.screen_session_verification_request_title is Step.Verifying -> when (step.data) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title @@ -116,6 +118,7 @@ private fun IncomingVerificationHeader(step: Step, request: VerificationRequest. } val subtitleTextId = when (step) { Step.Canceled -> R.string.screen_session_verification_request_failure_subtitle + Step.Unavailable -> R.string.screen_session_verification_unavailable_subtitle is Step.Initial -> when (request) { is VerificationRequest.Incoming.OtherSession -> R.string.screen_session_verification_request_subtitle is VerificationRequest.Incoming.User -> R.string.screen_session_verification_user_responder_subtitle @@ -246,6 +249,15 @@ private fun IncomingVerificationBottomMenu( ) } } + Step.Unavailable -> { + VerificationBottomMenu { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_ignore), + onClick = { eventSink(IncomingVerificationViewEvent.IgnoreVerification) }, + ) + } + } Step.Canceled, is Step.Completed, is Step.Failure -> { diff --git a/features/verifysession/impl/src/main/res/values/temporary.xml b/features/verifysession/impl/src/main/res/values/temporary.xml new file mode 100644 index 0000000000..b0db8e7fc6 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values/temporary.xml @@ -0,0 +1,11 @@ + + + + Verification with this device is currently unavailable + Please use another device or recovery key. + diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt index 45d5c623c4..1574224ed9 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt @@ -13,6 +13,8 @@ import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificat import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.core.FlowId +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.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -21,6 +23,7 @@ import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.test.A_USER_ID +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 io.element.android.tests.testutils.lambda.lambdaError @@ -294,6 +297,47 @@ class IncomingVerificationPresenterTest { navigatorLambda.assertions().isCalledOnce() } } + + @Test + fun `present - a session missing its secrets cannot be used to verify and offers no verification action`() = runTest { + val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> } + val resetLambda = lambdaRecorder { } + val acceptVerificationRequestLambda = lambdaRecorder { lambdaError() } + val encryptionService = FakeEncryptionService().apply { + recoveryStateStateFlow.value = RecoveryState.INCOMPLETE + } + createPresenter( + service = FakeSessionVerificationService( + acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda, + acceptVerificationRequestLambda = acceptVerificationRequestLambda, + resetLambda = resetLambda, + ), + encryptionService = encryptionService, + ).test { + advanceUntilIdle() + assertThat(awaitItem().step).isEqualTo(IncomingVerificationState.Step.Unavailable) + acceptVerificationRequestLambda.assertions().isNeverCalled() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - a session that holds its secrets is offered the verification action`() = runTest { + val encryptionService = FakeEncryptionService().apply { + recoveryStateStateFlow.value = RecoveryState.ENABLED + } + createPresenter( + service = FakeSessionVerificationService( + acknowledgeVerificationRequestLambda = { }, + resetLambda = { }, + ), + encryptionService = encryptionService, + ).test { + advanceUntilIdle() + assertThat(awaitItem().step).isInstanceOf(IncomingVerificationState.Step.Initial::class.java) + cancelAndIgnoreRemainingEvents() + } + } } private val anIncomingSessionVerificationRequest = VerificationRequest.Incoming.OtherSession( @@ -314,11 +358,13 @@ internal fun TestScope.createPresenter( verificationRequest: VerificationRequest.Incoming = anIncomingSessionVerificationRequest, navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() }, service: SessionVerificationService = FakeSessionVerificationService(), + encryptionService: EncryptionService = FakeEncryptionService(), dateFormatter: DateFormatter = FakeDateFormatter(), ) = IncomingVerificationPresenter( verificationRequest = verificationRequest, navigator = navigator, sessionVerificationService = service, + encryptionService = encryptionService, stateMachine = IncomingVerificationStateMachine(service), dateFormatter = dateFormatter, sessionCoroutineScope = backgroundScope,