mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Say so when this session has no secrets to verify another one with
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.
This commit is contained in:
+10
-2
@@ -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()
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
+13
-1
@@ -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>
|
||||
+46
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user