Files
letro-ios/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenViewModel.swift
Stefan Ceriu 8b8b2bde0b Move verification request acceptance confirmation to method call response instead of delegate callback
This because the Rust side verification state machine doesn't wait for the request
to be sent and acknowledged before changing its inner state and with the automatic
starting of SAS in https://github.com/element-hq/element-x-ios/pull/5116 that creates
race conditions between `m.key.verification.ready` and `m.key.verification.start`.
2026-05-06 11:11:38 +03:00

234 lines
8.7 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
typealias SessionVerificationViewModelType = StateStoreViewModel<SessionVerificationScreenViewState, SessionVerificationScreenViewAction>
class SessionVerificationScreenViewModel: SessionVerificationViewModelType, SessionVerificationScreenViewModelProtocol {
private let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol
private let flow: SessionVerificationScreenFlow
private var stateMachine: SessionVerificationScreenStateMachine
private var actionsSubject: PassthroughSubject<SessionVerificationScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<SessionVerificationScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol,
flow: SessionVerificationScreenFlow,
appSettings: AppSettings,
mediaProvider: MediaProviderProtocol,
verificationState: SessionVerificationScreenStateMachine.State = .initial) {
self.sessionVerificationControllerProxy = sessionVerificationControllerProxy
self.flow = flow
stateMachine = SessionVerificationScreenStateMachine(state: verificationState)
super.init(initialViewState: .init(flow: flow,
learnMoreURL: appSettings.encryptionURL,
verificationState: verificationState),
mediaProvider: mediaProvider)
setupStateMachine()
sessionVerificationControllerProxy.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
guard let self else { return }
switch callback {
case .receivedVerificationRequest:
break // Incoming verification requests are handled on the higher levels
case .acceptedVerificationRequest:
self.stateMachine.processEvent(.didAcceptVerificationRequest)
case .startedSasVerification:
self.stateMachine.processEvent(.didStartSasVerification)
case .receivedVerificationData(let emojis):
guard self.stateMachine.state == .sasVerificationStarted else {
MXLog.warning("Callbacks: Ignoring receivedVerificationData due to invalid state.")
return
}
self.stateMachine.processEvent(.didReceiveChallenge(emojis: emojis))
case .finished:
self.stateMachine.processEvent(.didAcceptChallenge)
case .cancelled:
self.stateMachine.processEvent(.didCancel)
case .failed:
self.stateMachine.processEvent(.didFail)
}
}
.store(in: &cancellables)
switch flow {
case .deviceResponder(let details), .userResponder(let details):
Task {
await self.sessionVerificationControllerProxy.acknowledgeVerificationRequest(details: details)
}
default:
break
}
}
override func process(viewAction: SessionVerificationScreenViewAction) {
switch viewAction {
case .acceptVerificationRequest:
stateMachine.processEvent(.acceptVerificationRequest)
case .ignoreVerificationRequest:
actionsSubject.send(.finished)
case .requestVerification:
stateMachine.processEvent(.requestVerification)
case .restart:
stateMachine.processEvent(.restart)
case .accept:
stateMachine.processEvent(.acceptChallenge)
case .decline:
stateMachine.processEvent(.declineChallenge)
case .cancel:
stateMachine.processEvent(.cancel)
actionsSubject.send(.finished)
case .done:
actionsSubject.send(.finished)
}
}
func stop() {
switch stateMachine.state {
case .initial, .verified, .cancelled: // non-cancellable states
return
default:
stateMachine.processEvent(.cancel)
}
}
// MARK: - Private
private func setupStateMachine() {
stateMachine.addTransitionHandler { [weak self] context in
guard let self else { return }
state.verificationState = context.toState
switch (context.fromState, context.event, context.toState) {
case (.initial, .acceptVerificationRequest, .acceptingVerificationRequest):
acceptVerificationRequest()
case (.initial, .requestVerification, .requestingVerification):
Task {
switch await self.requestVerification() {
case .success:
// Need to wait for the callback from the remote
break
case .failure:
self.stateMachine.processEvent(.didFail)
}
}
case (.acceptingVerificationRequest, .didAcceptVerificationRequest, .verificationRequestAccepted):
startSasVerification()
case (.showingChallenge, .acceptChallenge, .acceptingChallenge):
acceptChallenge()
case (.showingChallenge, .declineChallenge, .decliningChallenge):
declineChallenge()
case (_, .cancel, .cancelling):
cancelVerification()
case (_, _, .verified):
actionsSubject.send(.finished)
case (.initial, _, .cancelled):
switch flow {
case .deviceResponder, .userResponder:
actionsSubject.send(.finished)
default:
break
}
default:
break
}
}
stateMachine.addErrorHandler { context in
MXLog.error("Failed transition with context: \(context)")
}
}
private func acceptVerificationRequest() {
Task {
guard flow.isResponder else {
fatalError("Incorrect API usage.")
}
switch await sessionVerificationControllerProxy.acceptVerificationRequest() {
case .success:
// Need to wait for the callback from the remote
break
case .failure:
stateMachine.processEvent(.didFail)
}
}
}
private func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
switch flow {
case .deviceInitiator:
return await sessionVerificationControllerProxy.requestDeviceVerification()
case .userInitiator(let userID):
return await sessionVerificationControllerProxy.requestUserVerification(userID)
default:
fatalError("Incorrect API usage.")
}
}
private func cancelVerification() {
Task {
switch await sessionVerificationControllerProxy.cancelVerification() {
case .success:
stateMachine.processEvent(.didCancel)
case .failure:
stateMachine.processEvent(.didFail)
}
}
}
private func startSasVerification() {
Task {
switch await sessionVerificationControllerProxy.startSasVerification() {
case .success:
// Need to wait for the callback from the remote
break
case .failure:
stateMachine.processEvent(.didFail)
}
}
}
private func acceptChallenge() {
Task {
switch await sessionVerificationControllerProxy.approveVerification() {
case .success:
// Need to wait for the callback from the remote
break
case .failure:
stateMachine.processEvent(.didFail)
}
}
}
private func declineChallenge() {
Task {
switch await sessionVerificationControllerProxy.declineVerification() {
case .success:
stateMachine.processEvent(.didCancel)
case .failure:
stateMachine.processEvent(.didFail)
}
}
}
}