From fcecc7acba6d7820de69c5faecf3545483c13c8b Mon Sep 17 00:00:00 2001 From: hayaksi1 <193020925+hayaksi1@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:33:10 +0300 Subject: [PATCH] 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. --- .../OutgoingVerificationStateMachine.kt | 17 +++++++++--- .../OutgoingVerificationPresenterTest.kt | 26 ++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateMachine.kt index 465fc20066..2fd7948c38 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateMachine.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateMachine.kt @@ -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( 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 { @@ -44,11 +51,15 @@ class OutgoingVerificationStateMachine( } } inState { - 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 { _, state -> state.override { State.VerificationRequestAccepted.andLogStateChange() } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt index c6cbfca281..bf3f690a07 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt @@ -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 {} + 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()