import Shared import Models import Combine import XXModels import Foundation import Integration import CombineSchedulers import DependencyInjection enum ScanStatus: Equatable { case reading case processing case success case failed(ScanError) } enum ScanError: Equatable { case requestOpened case unknown(String) case cameraPermission case alreadyFriends(String) } struct ScanViewState: Equatable { var status: ScanStatus = .reading } final class ScanViewModel { @Dependency private var session: SessionType var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() var contactPublisher: AnyPublisher<Contact, Never> { contactRelay.eraseToAnyPublisher() } private let contactRelay = PassthroughSubject<Contact, Never>() var state: AnyPublisher<ScanViewState, Never> { stateRelay.eraseToAnyPublisher() } private let stateRelay = CurrentValueSubject<ScanViewState, Never>(.init()) func resetScanner() { stateRelay.value.status = .reading } func didScanData(_ data: Data) { guard stateRelay.value.status == .reading else { return } stateRelay.value.status = .processing backgroundScheduler.schedule { [weak self] in guard let self = self else { return } do { guard let usernameAndId = try self.verifyScanned(data) else { self.stateRelay.value.status = .failed(.unknown(Localized.Scan.Error.general)) return } if let previouslyAdded = try? self.session.dbManager.fetchContacts(.init(id: [usernameAndId.1])).first { var error = ScanError.unknown(Localized.Scan.Error.general) switch previouslyAdded.authStatus { case .friend: error = .alreadyFriends(usernameAndId.0) case .requested, .verified: error = .requestOpened default: break } self.stateRelay.value.status = .failed(error) return } let contact = Contact( id: usernameAndId.1, marshaled: data, username: usernameAndId.0, email: try? self.session.extract(fact: .email, from: data), phone: try? self.session.extract(fact: .phone, from: data), nickname: nil, photo: nil, authStatus: .stranger, isRecent: false, createdAt: Date() ) self.succeed(with: contact) } catch { self.stateRelay.value.status = .failed(.unknown(Localized.Scan.Error.invalid)) } } } private func verifyScanned(_ data: Data) throws -> (String, Data)? { guard let username = try session.extract(fact: .username, from: data), let id = session.getId(from: data) else { return nil } return (username, id) } private func succeed(with contact: Contact) { stateRelay.value.status = .success contactRelay.send(contact) } }