From 1cd27761adfbf8805cfb33ddcb5368787d94d914 Mon Sep 17 00:00:00 2001 From: hayaksi1 <193020925+hayaksi1@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:44:08 +0300 Subject: [PATCH] Say so when this session has no secrets to verify another one with MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A session whose recovery state is INCOMPLETE has not got the secrets it would need to sign another device, so accepting an incoming verification request from it can only fail. The screen offered "Start verification" anyway, and the user learnt otherwise at the end of the flow. Show the design's alert step instead — "Verification with this device is currently unavailable" — with only the Ignore action, and leave the request unacknowledged in every other respect. Part of #5344. --- .../incoming/IncomingVerificationPresenter.kt | 12 ++++- .../incoming/IncomingVerificationState.kt | 2 + .../IncomingVerificationStatePreviewParam.kt | 1 + .../impl/incoming/IncomingVerificationView.kt | 14 +++++- .../impl/src/main/res/values/temporary.xml | 11 +++++ .../IncomingVerificationPresenterTest.kt | 46 +++++++++++++++++++ 6 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 features/verifysession/impl/src/main/res/values/temporary.xml 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,