From 2179c17de816aeb5f3e10acc10e16db9a28ce05e Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 9 Jun 2023 16:48:58 +0200 Subject: [PATCH] Verification: integrate with new statemachine library --- features/verifysession/impl/build.gradle.kts | 1 + .../impl/VerifySelfSessionPresenter.kt | 87 ++++++---- .../impl/VerifySelfSessionStateMachine.kt | 157 +++++++++--------- .../impl/VerifySelfSessionPresenterTests.kt | 148 +++++++---------- gradle/libs.versions.toml | 1 + .../FakeSessionVerificationService.kt | 7 +- 6 files changed, 193 insertions(+), 208 deletions(-) diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts index 8a248127f2..09ebbd3ca5 100644 --- a/features/verifysession/impl/build.gradle.kts +++ b/features/verifysession/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) implementation(projects.libraries.statemachine) + api(libs.statemachine) api(projects.features.verifysession.api) testImplementation(libs.test.junit) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt index aa1fe59c32..689ff3a154 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt @@ -14,24 +14,31 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.verifysession.impl import androidx.compose.runtime.Composable 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 -import androidx.compose.runtime.rememberCoroutineScope +import com.freeletics.flowredux.compose.rememberStateAndDispatch import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import javax.inject.Inject import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState class VerifySelfSessionPresenter @Inject constructor( private val sessionVerificationService: SessionVerificationService, + private val stateMachine: VerifySelfSessionStateMachine, ) : Presenter { @Composable @@ -40,48 +47,36 @@ class VerifySelfSessionPresenter @Inject constructor( // Force reset, just in case the service was left in a broken state sessionVerificationService.reset() } - - val coroutineScope = rememberCoroutineScope() - val stateMachine = remember { VerifySelfSessionStateMachine(coroutineScope, sessionVerificationService) } - - // Create the new view state from the StateMachine state - val stateMachineCurrentState by stateMachine.state.collectAsState() - val verificationFlowState by remember { - derivedStateOf { stateMachineStateToViewState(stateMachineCurrentState) } + val stateAndDispatch = stateMachine.rememberStateAndDispatch() + val verificationFlowStep by remember { + derivedStateOf { stateAndDispatch.state.value.toVerificationStep() } + } + // Start this after observing state machine + LaunchedEffect(Unit) { + observeVerificationService() } fun handleEvents(event: VerifySelfSessionViewEvents) { when (event) { - VerifySelfSessionViewEvents.RequestVerification -> stateMachine.process(StateMachineEvent.RequestVerification) - VerifySelfSessionViewEvents.StartSasVerification -> stateMachine.process(StateMachineEvent.StartSasVerification) - VerifySelfSessionViewEvents.Restart -> stateMachine.process(StateMachineEvent.Restart) - VerifySelfSessionViewEvents.ConfirmVerification -> stateMachine.process(StateMachineEvent.AcceptChallenge) - VerifySelfSessionViewEvents.DeclineVerification -> stateMachine.process(StateMachineEvent.DeclineChallenge) - VerifySelfSessionViewEvents.CancelAndClose -> { - if (stateMachineCurrentState !in sequenceOf( - StateMachineState.Initial, - StateMachineState.Completed, - StateMachineState.Canceled - ) - ) { - stateMachine.process(StateMachineEvent.Cancel) - } - } + VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification) + VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification) + VerifySelfSessionViewEvents.Restart -> stateAndDispatch.dispatchAction(StateMachineEvent.Restart) + VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge) + VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge) + VerifySelfSessionViewEvents.CancelAndClose -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel) } } - return VerifySelfSessionState( - verificationFlowStep = verificationFlowState, + verificationFlowStep = verificationFlowStep, eventSink = ::handleEvents, ) } - private fun stateMachineStateToViewState(state: StateMachineState): VerifySelfSessionState.VerificationStep = - when (state) { - StateMachineState.Initial -> { + private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.VerificationStep = + when (val machineState = this) { + StateMachineState.Initial, null -> { VerifySelfSessionState.VerificationStep.Initial } - StateMachineState.RequestingVerification, StateMachineState.StartingSasVerification, StateMachineState.SasVerificationStarted, @@ -98,15 +93,41 @@ class VerifySelfSessionPresenter @Inject constructor( } is StateMachineState.Verifying -> { - val async = when (state) { + val async = when (machineState) { is StateMachineState.Verifying.Replying -> Async.Loading() else -> Async.Uninitialized } - VerifySelfSessionState.VerificationStep.Verifying(state.emojis, async) + VerifySelfSessionState.VerificationStep.Verifying(machineState.emojis, async) } StateMachineState.Completed -> { VerifySelfSessionState.VerificationStep.Completed } } + + private fun CoroutineScope.observeVerificationService() { + sessionVerificationService.verificationFlowState.onEach { verificationAttemptState -> + when (verificationAttemptState) { + VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Restart) + VerificationFlowState.AcceptedVerificationRequest -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest) + } + VerificationFlowState.StartedSasVerification -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidStartSasVerification) + } + is VerificationFlowState.ReceivedVerificationData -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidReceiveChallenge(verificationAttemptState.emoji)) + } + VerificationFlowState.Finished -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptChallenge) + } + VerificationFlowState.Canceled -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidCancel) + } + VerificationFlowState.Failed -> { + stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidFail) + } + } + }.launchIn(this) + } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt index 310721d0f2..29818197e0 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt @@ -15,117 +15,114 @@ */ @file:Suppress("WildcardImport") +@file:OptIn(ExperimentalCoroutinesApi::class) package io.element.android.features.verifysession.impl +import com.freeletics.flowredux.dsl.FlowReduxStateMachine import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationEmoji -import io.element.android.libraries.matrix.api.verification.VerificationFlowState -import io.element.android.libraries.statemachine.createStateMachine -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -class VerifySelfSessionStateMachine( - coroutineScope: CoroutineScope, +import kotlinx.coroutines.ExperimentalCoroutinesApi +import javax.inject.Inject +import com.freeletics.flowredux.dsl.State as MachineState + +class VerifySelfSessionStateMachine @Inject constructor( private val sessionVerificationService: SessionVerificationService, +) : FlowReduxStateMachine( + initialState = State.Initial ) { - private val stateMachine = createStateMachine { - addInitialState(State.Initial) { - on(State.RequestingVerification) - on(State.StartingSasVerification) - } - addState { - onEnter { - coroutineScope.launch { + init { + spec { + inState { + on { _: Event.RequestVerification, state: MachineState -> + state.override { State.RequestingVerification } + } + on { _: Event.StartSasVerification, state: MachineState -> + state.override { State.StartingSasVerification } + } + } + inState { + onEnterEffect { sessionVerificationService.requestVerification() } + on { _: Event.DidAcceptVerificationRequest, state: MachineState -> + state.override { State.VerificationRequestAccepted } + } + on { _: Event.DidFail, state: MachineState -> + state.override { State.Initial } + } } - on(State.VerificationRequestAccepted) - on(State.Initial) - } - addState { - onEnter { - coroutineScope.launch { + inState { + onEnterEffect { sessionVerificationService.startVerification() } } - } - addState { - on(State.StartingSasVerification) - } - addState { - on(State.RequestingVerification) - } - addState { - on { event, _ -> State.Verifying.ChallengeReceived(event.emojis) } - } - addState { - on { _, prevState -> State.Verifying.Replying(prevState.emojis, true) } - on { _, prevState -> State.Verifying.Replying(prevState.emojis, false) } - } - addState { - onEnter { state -> - coroutineScope.launch { + inState { + on { _: Event.StartSasVerification, state: MachineState -> + state.override { State.StartingSasVerification } + } + } + inState { + on { _: Event.Restart, state: MachineState -> + state.override { State.RequestingVerification } + } + } + inState { + on { event: Event.DidReceiveChallenge, state: MachineState -> + state.override { State.Verifying.ChallengeReceived(event.emojis) } + } + } + inState { + on { _: Event.AcceptChallenge, state: MachineState -> + state.override { State.Verifying.Replying(state.snapshot.emojis, accept = true) } + } + on { _: Event.DeclineChallenge, state: MachineState -> + state.override { State.Verifying.Replying(state.snapshot.emojis, accept = false) } + } + } + inState { + onEnterEffect { state -> if (state.accept) { sessionVerificationService.approveVerification() } else { sessionVerificationService.declineVerification() } } + on { _: Event.DidAcceptChallenge, state: MachineState -> + state.override { State.Completed } + } } - on(State.Completed) - } - addState { - onEnter { - coroutineScope.launch { + inState { + onEnterEffect { sessionVerificationService.cancelVerification() } } - } - on(State.SasVerificationStarted) - on(State.Canceling) - on(State.Canceled) - on(State.Canceled) - } - - init { - // Observe the verification service state, translate it to state machine input events - sessionVerificationService.verificationFlowState.onEach { verificationAttemptState -> - when (verificationAttemptState) { - VerificationFlowState.Initial -> stateMachine.restart() - VerificationFlowState.AcceptedVerificationRequest -> { - stateMachine.process(Event.DidAcceptVerificationRequest) - } - VerificationFlowState.StartedSasVerification -> { - stateMachine.process(Event.DidStartSasVerification) - } - is VerificationFlowState.ReceivedVerificationData -> { - // For some reason we receive this state twice, we need to discard the 2nd one - if (stateMachine.currentState == State.SasVerificationStarted) { - stateMachine.process(Event.DidReceiveChallenge(verificationAttemptState.emoji)) - } + inState { + on { _: Event.DidStartSasVerification, state: MachineState -> + state.override { State.SasVerificationStarted } } - VerificationFlowState.Finished -> { - stateMachine.process(Event.DidAcceptChallenge) + on { _: Event.Cancel, state: MachineState -> + if (state.snapshot in sequenceOf( + State.Initial, + State.Completed, + State.Canceled + )) { + state.noChange() + } else { + state.override { State.Canceling } + } } - VerificationFlowState.Canceled -> { - stateMachine.process(Event.DidCancel) + on { _: Event.DidCancel, state: MachineState -> + state.override { State.Canceled } } - VerificationFlowState.Failed -> { - stateMachine.process(Event.DidFail) + on { _: Event.DidFail, state: MachineState -> + state.override { State.Canceled } } } - }.launchIn(coroutineScope) + } } - val state: StateFlow = stateMachine.stateFlow - - fun process(event: Event) = stateMachine.process(event) - sealed interface State { /** The initial state, before verification started. */ object Initial : State diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt index 4007f4c8cf..0b58c125de 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt @@ -18,14 +18,13 @@ package io.element.android.features.verifysession.impl import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow -import app.cash.turbine.Event import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as VerificationStep +import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep import io.element.android.libraries.architecture.Async -import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.VerificationEmoji +import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -36,8 +35,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Initial state is received`() = runTest { - val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter() moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -48,30 +46,18 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Handles requestVerification`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - eventSink(VerifySelfSessionViewEvents.RequestVerification) - // Await for other device response: - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) - // Await for the state to be Ready - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Ready) - // Await for other device response (again): - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) - // Finally, ChallengeReceived: - val verifyingState = awaitItem() - assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) + requestVerificationAndAwaitVerifyingState(service) } } @Test fun `present - Handles startSasVerification`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -82,6 +68,7 @@ class VerifySelfSessionPresenterTests { // Await for other device response: assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) // ChallengeReceived: + service.triggerReceiveVerificationData() val verifyingState = awaitItem() assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) } @@ -89,8 +76,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Cancelation on initial state does nothing`() = runTest { - val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter() moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -105,65 +91,43 @@ class VerifySelfSessionPresenterTests { @Test fun `present - A fail in the flow cancels it`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - eventSink(VerifySelfSessionViewEvents.RequestVerification) - - val verifyingState = awaitChallengeReceivedState() - assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) - + val state = requestVerificationAndAwaitVerifyingState(service) service.shouldFail = true - eventSink(VerifySelfSessionViewEvents.ConfirmVerification) - - val remainingEvents = cancelAndConsumeRemainingEvents().mapNotNull { (it as? Event.Item)?.value } - assertThat(remainingEvents.last().verificationFlowStep).isEqualTo(VerificationStep.Canceled) + state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification) + // Cancelling + assertThat(awaitItem().verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) + // Cancelled + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) } } @Test fun `present - Canceling the flow once it's verifying cancels it`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - eventSink(VerifySelfSessionViewEvents.RequestVerification) - - val verifyingState = awaitChallengeReceivedState() - assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) - - eventSink(VerifySelfSessionViewEvents.CancelAndClose) - - val remainingEvents = cancelAndConsumeRemainingEvents().mapNotNull { (it as? Event.Item)?.value } - assertThat(remainingEvents.last().verificationFlowStep).isEqualTo(VerificationStep.Canceled) + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(VerifySelfSessionViewEvents.CancelAndClose) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) } } @Test fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - eventSink(VerifySelfSessionViewEvents.RequestVerification) - - val verifyingState = awaitChallengeReceivedState() - assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) - + requestVerificationAndAwaitVerifyingState(service) service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(emptyList())) - ensureAllEventsConsumed() } } @@ -171,21 +135,14 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Restart after cancelation returns to requesting verification`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - - eventSink(VerifySelfSessionViewEvents.RequestVerification) - assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Uninitialized)) - + val state = requestVerificationAndAwaitVerifyingState(service) service.givenVerificationFlowState(VerificationFlowState.Canceled) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) - - eventSink(VerifySelfSessionViewEvents.Restart) + state.eventSink(VerifySelfSessionViewEvents.Restart) // Went back to requesting verification assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) cancelAndIgnoreRemainingEvents() @@ -200,18 +157,12 @@ class VerifySelfSessionPresenterTests { val service = FakeSessionVerificationService().apply { givenEmojiList(emojis) } - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - - eventSink(VerifySelfSessionViewEvents.RequestVerification) - assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emojis, Async.Uninitialized)) - - eventSink(VerifySelfSessionViewEvents.ConfirmVerification) + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emojis, Async.Loading())) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed) } @@ -220,28 +171,41 @@ class VerifySelfSessionPresenterTests { @Test fun `present - When verification is declined, the flow is canceled`() = runTest { val service = FakeSessionVerificationService() - val presenter = VerifySelfSessionPresenter(service) + val presenter = createPresenter(service) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) - val eventSink = initialState.eventSink - - eventSink(VerifySelfSessionViewEvents.RequestVerification) - - assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Uninitialized)) - eventSink(VerifySelfSessionViewEvents.DeclineVerification) - + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(VerifySelfSessionViewEvents.DeclineVerification) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Loading())) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) } } - private suspend fun ReceiveTurbine.awaitChallengeReceivedState(): VerifySelfSessionState { - // Skip 'waiting for response', 'ready' and 'starting verification' state - skipItems(3) - // Received challenge - return awaitItem() + private suspend fun ReceiveTurbine.requestVerificationAndAwaitVerifyingState( + fakeService: FakeSessionVerificationService + ): VerifySelfSessionState { + var state = awaitItem() + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial) + state.eventSink(VerifySelfSessionViewEvents.RequestVerification) + // Await for other device response: + state = awaitItem() + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) + // Await for the state to be Ready + state = awaitItem() + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Ready) + state.eventSink(VerifySelfSessionViewEvents.StartSasVerification) + // Await for other device response (again): + state = awaitItem() + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) + fakeService.triggerReceiveVerificationData() + // Finally, ChallengeReceived: + state = awaitItem() + assertThat(state.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) + return state + } + + private fun createPresenter(service: FakeSessionVerificationService = FakeSessionVerificationService()): VerifySelfSessionPresenter { + return VerifySelfSessionPresenter(service, VerifySelfSessionStateMachine(service)) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0690511adf..9b626bc206 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -150,6 +150,7 @@ gujun_span = "me.gujun.android:span:1.7" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } +statemachine = "com.freeletics.flowredux:compose:1.1.0" # Analytics posthog = "com.posthog.android:posthog:2.0.3" diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt index 01e20b83a9..2f7887c537 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt @@ -39,8 +39,6 @@ class FakeSessionVerificationService : SessionVerificationService { override suspend fun requestVerification() { _verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest - _verificationFlowState.value = VerificationFlowState.StartedSasVerification - _verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList) } override suspend fun cancelVerification() { @@ -63,9 +61,12 @@ class FakeSessionVerificationService : SessionVerificationService { } } + fun triggerReceiveVerificationData() { + _verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList) + } + override suspend fun startVerification() { _verificationFlowState.value = VerificationFlowState.StartedSasVerification - _verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList) } fun givenVerifiedStatus(status: SessionVerifiedStatus) {