Verification: integrate with new statemachine library

This commit is contained in:
ganfra 2023-06-09 16:48:58 +02:00
parent 569c235f71
commit 39602d8491
6 changed files with 192 additions and 207 deletions

View file

@ -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<VerifySelfSessionState> {
@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)
}
}

View file

@ -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
import kotlinx.coroutines.ExperimentalCoroutinesApi
import javax.inject.Inject
import com.freeletics.flowredux.dsl.State as MachineState
class VerifySelfSessionStateMachine(
coroutineScope: CoroutineScope,
class VerifySelfSessionStateMachine @Inject constructor(
private val sessionVerificationService: SessionVerificationService,
) : FlowReduxStateMachine<VerifySelfSessionStateMachine.State, VerifySelfSessionStateMachine.Event>(
initialState = State.Initial
) {
private val stateMachine = createStateMachine {
addInitialState(State.Initial) {
on<Event.RequestVerification>(State.RequestingVerification)
on<Event.StartSasVerification>(State.StartingSasVerification)
}
addState<State.RequestingVerification> {
onEnter {
coroutineScope.launch {
sessionVerificationService.requestVerification()
init {
spec {
inState<State.Initial> {
on { _: Event.RequestVerification, state: MachineState<State.Initial> ->
state.override { State.RequestingVerification }
}
on { _: Event.StartSasVerification, state: MachineState<State.Initial> ->
state.override { State.StartingSasVerification }
}
}
on<Event.DidAcceptVerificationRequest>(State.VerificationRequestAccepted)
on<Event.DidFail>(State.Initial)
}
addState<State.StartingSasVerification> {
onEnter {
coroutineScope.launch {
inState<State.RequestingVerification> {
onEnterEffect {
sessionVerificationService.requestVerification()
}
on { _: Event.DidAcceptVerificationRequest, state: MachineState<State.RequestingVerification> ->
state.override { State.VerificationRequestAccepted }
}
on { _: Event.DidFail, state: MachineState<State.RequestingVerification> ->
state.override { State.Initial }
}
}
inState<State.StartingSasVerification> {
onEnterEffect {
sessionVerificationService.startVerification()
}
}
}
addState<State.VerificationRequestAccepted> {
on<Event.StartSasVerification>(State.StartingSasVerification)
}
addState<State.Canceled> {
on<Event.Restart>(State.RequestingVerification)
}
addState<State.SasVerificationStarted> {
on<Event.DidReceiveChallenge> { event, _ -> State.Verifying.ChallengeReceived(event.emojis) }
}
addState<State.Verifying.ChallengeReceived> {
on<Event.AcceptChallenge> { _, prevState -> State.Verifying.Replying(prevState.emojis, true) }
on<Event.DeclineChallenge> { _, prevState -> State.Verifying.Replying(prevState.emojis, false) }
}
addState<State.Verifying.Replying> {
onEnter { state ->
coroutineScope.launch {
inState<State.VerificationRequestAccepted> {
on { _: Event.StartSasVerification, state: MachineState<State.VerificationRequestAccepted> ->
state.override { State.StartingSasVerification }
}
}
inState<State.Canceled> {
on { _: Event.Restart, state: MachineState<State.Canceled> ->
state.override { State.RequestingVerification }
}
}
inState<State.SasVerificationStarted> {
on { event: Event.DidReceiveChallenge, state: MachineState<State.SasVerificationStarted> ->
state.override { State.Verifying.ChallengeReceived(event.emojis) }
}
}
inState<State.Verifying.ChallengeReceived> {
on { _: Event.AcceptChallenge, state: MachineState<State.Verifying.ChallengeReceived> ->
state.override { State.Verifying.Replying(state.snapshot.emojis, accept = true) }
}
on { _: Event.DeclineChallenge, state: MachineState<State.Verifying.ChallengeReceived> ->
state.override { State.Verifying.Replying(state.snapshot.emojis, accept = false) }
}
}
inState<State.Verifying.Replying> {
onEnterEffect { state ->
if (state.accept) {
sessionVerificationService.approveVerification()
} else {
sessionVerificationService.declineVerification()
}
}
on { _: Event.DidAcceptChallenge, state: MachineState<State.Verifying.Replying> ->
state.override { State.Completed }
}
}
on<Event.DidAcceptChallenge>(State.Completed)
}
addState<State.Canceling> {
onEnter {
coroutineScope.launch {
inState<State.Canceling> {
onEnterEffect {
sessionVerificationService.cancelVerification()
}
}
}
on<Event.DidStartSasVerification>(State.SasVerificationStarted)
on<Event.Cancel>(State.Canceling)
on<Event.DidCancel>(State.Canceled)
on<Event.DidFail>(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)
inState {
on { _: Event.DidStartSasVerification, state: MachineState<State> ->
state.override { State.SasVerificationStarted }
}
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))
on { _: Event.Cancel, state: MachineState<State> ->
if (state.snapshot in sequenceOf(
State.Initial,
State.Completed,
State.Canceled
)) {
state.noChange()
} else {
state.override { State.Canceling }
}
}
VerificationFlowState.Finished -> {
stateMachine.process(Event.DidAcceptChallenge)
on { _: Event.DidCancel, state: MachineState<State> ->
state.override { State.Canceled }
}
VerificationFlowState.Canceled -> {
stateMachine.process(Event.DidCancel)
}
VerificationFlowState.Failed -> {
stateMachine.process(Event.DidFail)
on { _: Event.DidFail, state: MachineState<State> ->
state.override { State.Canceled }
}
}
}.launchIn(coroutineScope)
}
}
val state: StateFlow<State> = stateMachine.stateFlow
fun process(event: Event) = stateMachine.process(event)
sealed interface State {
/** The initial state, before verification started. */
object Initial : State