// // 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 class SessionVerificationScreenViewModel: SessionVerificationViewModelType, SessionVerificationScreenViewModelProtocol { private let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol private let flow: SessionVerificationScreenFlow private var stateMachine: SessionVerificationScreenStateMachine private var actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { 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 { 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) } } } }