Give up on a verification request the other session never accepts

The outgoing verification flow had no client side timeout: if the other
session never answered, the screen waited on "Waiting to accept request"
for as long as the user left it open, with no indication that the
request could no longer succeed.

Bound the wait to the ten minutes the Matrix specification gives a
verification request. Past that the other session can no longer accept
it, so cancel the request and move to the canceled state, which already
offers to start again.
This commit is contained in:
hayaksi1
2026-08-17 11:33:10 +03:00
parent 1a3b89de12
commit fcecc7acba
2 changed files with 37 additions and 6 deletions
@@ -23,9 +23,11 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.timeout
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import com.freeletics.flowredux.dsl.State as MachineState
@@ -36,6 +38,11 @@ class OutgoingVerificationStateMachine(
) : FlowReduxStateMachine<OutgoingVerificationStateMachine.State, OutgoingVerificationStateMachine.Event>(
initialState = State.Initial,
) {
companion object {
/** Lifetime the Matrix specification gives a verification request; past it the other session can no longer accept ours. */
private val VERIFICATION_REQUEST_TIMEOUT = 10.minutes
}
init {
spec {
inState<State.Initial> {
@@ -44,11 +51,15 @@ class OutgoingVerificationStateMachine(
}
}
inState<State.RequestingVerification> {
onEnterEffect { event ->
when (event.verificationRequest) {
onEnter { state ->
val verificationRequest = state.snapshot.verificationRequest
when (verificationRequest) {
is VerificationRequest.Outgoing.CurrentSession -> sessionVerificationService.requestDeviceVerification()
is VerificationRequest.Outgoing.User -> sessionVerificationService.requestUserVerification(event.verificationRequest.userId)
is VerificationRequest.Outgoing.User -> sessionVerificationService.requestUserVerification(verificationRequest.userId)
}
delay(VERIFICATION_REQUEST_TIMEOUT)
sessionVerificationService.cancelVerification()
state.override { State.Canceled.andLogStateChange() }
}
on<Event.DidAcceptVerificationRequest> { _, state ->
state.override { State.VerificationRequestAccepted.andLogStateChange() }
@@ -6,8 +6,6 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:Suppress("UnusedImports")
package io.element.android.features.verifysession.impl.outgoing
import app.cash.turbine.ReceiveTurbine
@@ -30,10 +28,12 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import kotlin.time.Duration.Companion.minutes
@ExperimentalCoroutinesApi
class OutgoingVerificationPresenterTest {
@@ -140,6 +140,26 @@ class OutgoingVerificationPresenterTest {
}
}
@Test
fun `present - A verification request the other session never accepts is canceled`() = runTest {
val cancelVerificationRecorder = lambdaRecorder<Unit> {}
val service = unverifiedSessionService(
requestDeviceVerificationLambda = { },
cancelVerificationLambda = cancelVerificationRecorder,
)
val presenter = createOutgoingVerificationPresenter(service)
presenter.test {
awaitItem().eventSink(OutgoingVerificationViewEvents.RequestVerification)
advanceTimeBy(1.minutes)
assertThat(awaitItem().step).isEqualTo(Step.AwaitingOtherDeviceResponse)
advanceTimeBy(9.minutes)
expectNoEvents()
advanceTimeBy(2.minutes)
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
cancelVerificationRecorder.assertions().isCalledOnce()
}
}
@Test
fun `present - Canceling the flow once it's verifying cancels it`() = runTest {
val service = unverifiedSessionService(
@@ -283,7 +303,7 @@ class OutgoingVerificationPresenterTest {
var state = awaitItem()
assertThat(state.step).isEqualTo(Step.Initial)
state.eventSink(OutgoingVerificationViewEvents.RequestVerification)
testScope.advanceUntilIdle()
testScope.advanceTimeBy(1.minutes)
// Await for other device response:
fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
state = awaitItem()