import Shared import Combine import XXModels import Defaults import XXClient import Foundation import Permissions import ReportingFeature import XXMessengerClient import DependencyInjection enum ScanningStatus: Equatable { case reading case processing case success case failed(ScanningError) } enum ScanningError: Equatable { case requestOpened case unknown(String) case cameraPermission case alreadyFriends(String) } final class SearchRightViewModel { @Dependency var database: Database @Dependency var permissions: PermissionHandling @Dependency var reportingStatus: ReportingStatus var foundPublisher: AnyPublisher<XXModels.Contact, Never> { foundSubject.eraseToAnyPublisher() } var cameraSemaphorePublisher: AnyPublisher<Bool, Never> { cameraSemaphoreSubject.eraseToAnyPublisher() } var statusPublisher: AnyPublisher<ScanningStatus, Never> { statusSubject.eraseToAnyPublisher() } private let foundSubject = PassthroughSubject<XXModels.Contact, Never>() private let cameraSemaphoreSubject = PassthroughSubject<Bool, Never>() private(set) var statusSubject = CurrentValueSubject<ScanningStatus, Never>(.reading) func viewWillAppear() { permissions.requestCamera { [weak self] granted in guard let self = self else { return } if granted { self.statusSubject.value = .reading self.cameraSemaphoreSubject.send(true) } else { self.statusSubject.send(.failed(.cameraPermission)) } } } func viewWillDisappear() { cameraSemaphoreSubject.send(false) } func didScan(data: Data) { /// We need to be accepting new readings in order /// to process what just got scanned. /// guard statusSubject.value == .reading else { return } statusSubject.send(.processing) /// Whatever got scanned, needs to have id and username /// otherwise is just noise or an unknown qr code /// let user = XXClient.Contact.live(data) guard let uid = try? user.getId(), let facts = try? user.getFacts(), let username = facts.first(where: { $0.type == .username })?.value else { let errorTitle = Localized.Scan.Error.invalid statusSubject.send(.failed(.unknown(errorTitle))) return } let email = facts.first { $0.type == .email }?.value let phone = facts.first { $0.type == .phone }?.value /// Make sure we are not processing a contact /// that we already have /// if let alreadyContact = try? database.fetchContacts(.init(id: [uid])).first { if alreadyContact.isBlocked, reportingStatus.isEnabled() { statusSubject.send(.failed(.unknown("You previously blocked this user."))) return } if alreadyContact.isBanned, reportingStatus.isEnabled() { statusSubject.send(.failed(.unknown("This user was banned."))) return } /// Show error accordingly to the auth status /// if alreadyContact.authStatus == .friend { statusSubject.send(.failed(.alreadyFriends(username))) } else if [.requested, .verified].contains(alreadyContact.authStatus) { statusSubject.send(.failed(.requestOpened)) } else { let generalErrorTitle = Localized.Scan.Error.general statusSubject.send(.failed(.unknown(generalErrorTitle))) } return } statusSubject.send(.success) cameraSemaphoreSubject.send(false) foundSubject.send(.init( id: uid, marshaled: data, username: username, email: email, phone: phone, nickname: nil, photo: nil, authStatus: .stranger, isRecent: false, createdAt: Date() )) } }