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 1/4] 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() From 5bfb614bdb370410d3aaa825eab2c4f3e3895cf1 Mon Sep 17 00:00:00 2001 From: hayaksi1 <193020925+hayaksi1@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:37:11 +0300 Subject: [PATCH 2/4] Restore the UnusedImports suppression on the outgoing verification test ktlint reports the TestScope import as unused because the type only appears in a context parameter, which the rule does not look at. The file carried a file-level suppression for exactly that before this branch dropped it. --- .../impl/outgoing/OutgoingVerificationPresenterTest.kt | 2 ++ 1 file changed, 2 insertions(+) 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 bf3f690a07..f872f495b3 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,6 +6,8 @@ * 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 From 8a32e3d4bfbaea20868822ed08b2c1d3aba12026 Mon Sep 17 00:00:00 2001 From: hayaksi1 <193020925+hayaksi1@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:40:49 +0300 Subject: [PATCH 3/4] Order the outgoing verification state machine imports ktlint's import-ordering rule expects lexicographic order within the first group, so TimeoutCancellationException has to precede delay. --- .../impl/outgoing/OutgoingVerificationStateMachine.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 130dca3eb6..05851437cc 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 @@ -22,8 +22,8 @@ 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.TimeoutCancellationException +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.timeout From af7e7655997b3b6db8826101206337736e0898e9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 25 Aug 2026 13:32:11 +0200 Subject: [PATCH 4/4] Fixing compilation issue after merging develop --- .../impl/outgoing/OutgoingVerificationPresenterTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a65437ba50..7dfacc40f1 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 @@ -152,7 +152,7 @@ class OutgoingVerificationPresenterTest { ) val presenter = createOutgoingVerificationPresenter(service) presenter.test { - awaitItem().eventSink(OutgoingVerificationViewEvents.RequestVerification) + awaitItem().eventSink(OutgoingVerificationViewEvent.RequestVerification) advanceTimeBy(1.minutes) assertThat(awaitItem().step).isEqualTo(Step.AwaitingOtherDeviceResponse) advanceTimeBy(9.minutes)