Merge pull request #7551 from hayaksi1/fix/5344-verification-unavailable

Say so when this session has no secrets to verify another one with
This commit is contained in:
Benoit Marty
2026-09-02 14:56:51 +02:00
committed by GitHub
8 changed files with 89 additions and 3 deletions
@@ -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<IncomingVerificationState> {
@@ -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()
@@ -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
@@ -43,6 +43,7 @@ open class IncomingVerificationStatePreviewParam : PreviewParameterProvider<Inco
anIncomingVerificationState(step = Step.Completed, verificationRequest = anIncomingUserVerificationRequest()),
anIncomingVerificationState(step = Step.Failure),
anIncomingVerificationState(step = Step.Canceled),
anIncomingVerificationState(step = Step.Unavailable),
// Add other state here
)
}
@@ -95,7 +95,8 @@ fun IncomingVerificationView(
@Composable
private fun IncomingVerificationHeader(step: Step, request: VerificationRequest.Incoming) {
val iconStyle = when (step) {
Step.Canceled -> 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 -> {
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2026 Element Creations Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<string name="screen_session_verification_unavailable_title">Verification with this device is currently unavailable</string>
<string name="screen_session_verification_unavailable_subtitle">Please use another device or recovery key.</string>
</resources>
@@ -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<VerificationRequest.Incoming, Unit> { _ -> }
val resetLambda = lambdaRecorder<Boolean, Unit> { }
val acceptVerificationRequestLambda = lambdaRecorder<Unit> { 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,