import Shared import Combine import Defaults import XXModels import Keychain import XXClient import CloudFiles import CheckVersion import AppResources import BackupFeature import ReportingFeature import CloudFilesDropbox import XXMessengerClient import UpdateErrors import FetchBannedList import ProcessBannedList import AppCore import Foundation import PermissionsFeature import ComposableArchitecture import XXDatabase import XXLegacyDatabaseMigrator import class XXClient.Cancellable import PulseLogHandler final class LaunchViewModel { enum Destination { case chats, onboarding } struct UpdateModel { let content: String let urlString: String let positiveActionTitle: String let negativeActionTitle: String? let actionStyle: CapsuleButtonStyle } struct ViewState { var shouldShowTerms = false var shouldOfferUpdate: UpdateModel? var shouldPushEndDestination: Destination? } @Dependency(\.app.log) var log @Dependency(\.app.bgQueue) var bgQueue @Dependency(\.permissions) var permissions @Dependency(\.app.messenger) var messenger @Dependency(\.app.dbManager) var dbManager @Dependency(\.updateErrors) var updateErrors @Dependency(\.app.hudManager) var hudManager @Dependency(\.checkVersion) var checkVersion @Dependency(\.dummyTraffic) var dummyTraffic @Dependency(\.app.toastManager) var toastManager @Dependency(\.fetchBannedList) var fetchBannedList @Dependency(\.reportingStatus) var reportingStatus @Dependency(\.app.networkMonitor) var networkMonitor @Dependency(\.processBannedList) var processBannedList @Dependency(\.app.authHandler) var authHandler @Dependency(\.app.groupRequest) var groupRequest @Dependency(\.app.backupHandler) var backupHandler @Dependency(\.app.messageListener) var messageListener @Dependency(\.app.receiveFileHandler) var receiveFileHandler @Dependency(\.app.groupMessageHandler) var groupMessageHandler var authHandlerCancellable: Cancellable? var groupRequestCancellable: Cancellable? var backupHandlerCancellable: Cancellable? var networkHandlerCancellable: Cancellable? var receiveFileHandlerCancellable: Cancellable? var groupMessageHandlerCancellable: Cancellable? var messageListenerHandlerCancellable: Cancellable? @KeyObject(.username, defaultValue: nil) var username: String? @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool @KeyObject(.acceptedTerms, defaultValue: false) var didAcceptTerms: Bool @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn: Bool var statePublisher: AnyPublisher<ViewState, Never> { stateSubject.eraseToAnyPublisher() } let dropboxManager = CloudFilesManager.dropbox( appKey: "ppx0de5f16p9aq2", path: "/backup/backup.xxm" ) let sftpManager = CloudFilesManager.sftp( host: "", username: "", password: "", fileName: "" ) let stateSubject = CurrentValueSubject <ViewState, Never>(.init()) func startLaunch() { if !didAcceptTerms { stateSubject.value.shouldShowTerms = true } hudManager.show() checkVersion { switch $0 { case .success(let result): switch result { case .updated: self.didVerifyVersion() case .outdated(let appUrl): self.hudManager.hide() self.stateSubject.value.shouldOfferUpdate = .init( content: Localized.Launch.Version.Recommended.title, urlString: appUrl, positiveActionTitle: Localized.Launch.Version.Recommended.positive, negativeActionTitle: Localized.Launch.Version.Recommended.negative, actionStyle: .simplestColoredRed ) case .wayTooOld(let appUrl, let minimumVersionMessage): self.hudManager.hide() self.stateSubject.value.shouldOfferUpdate = .init( content: minimumVersionMessage, urlString: appUrl, positiveActionTitle: Localized.Launch.Version.Required.positive, negativeActionTitle: nil, actionStyle: .brandColored ) } case .failure(let error): self.hudManager.show(.init( title: Localized.Launch.Version.failed, content: error.localizedDescription )) } } } func didRefuseUpdating() { hudManager.show() didVerifyVersion() } private func didVerifyVersion() { updateBannedList { self.updateErrors { switch $0 { case .success: do { if !self.dbManager.hasDB() { try self.dbManager.makeDB() } try self.setupMessenger() } catch { let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) self.hudManager.show(.init(content: xxError)) } case .failure(let error): self.hudManager.show(.init(error: error)) } } } } } extension LaunchViewModel { func setupMessenger() throws { _ = try messenger.setLogLevel(.trace) messenger.startLogging() authHandlerCancellable = authHandler { [weak self] in self?.log(.error($0 as NSError)) } backupHandlerCancellable = backupHandler { [weak self] in self?.log(.error($0 as NSError)) } receiveFileHandlerCancellable = receiveFileHandler { [weak self] in self?.log(.error($0 as NSError)) } messageListenerHandlerCancellable = messageListener { [weak self] in self?.log(.error($0 as NSError)) } if messenger.isLoaded() == false { if messenger.isCreated() == false { try messenger.create() } try messenger.load() } try messenger.start() if messenger.isConnected() == false { try messenger.connect() try messenger.listenForMessages() } let dummyTrafficManager = try NewDummyTrafficManager.live( cMixId: messenger.e2e()!.getId() ) dummyTraffic.set(dummyTrafficManager) try dummyTrafficManager.setStatus(dummyTrafficOn) let endDestination: Destination if messenger.isLoggedIn() == false { if try messenger.isRegistered() { try messenger.logIn() endDestination = .chats } else { try? sftpManager.unlink() try? dropboxManager.unlink() endDestination = .onboarding } } else { endDestination = .chats } defer { hudManager.hide() if endDestination == .chats { if isBiometricsOn, permissions.biometrics.status() { permissions.biometrics.request { [weak self] granted in guard let self else { return } if granted { self.stateSubject.value.shouldPushEndDestination = .chats } else { // TODO: A fallback state for failing biometrics } } } else { stateSubject.value.shouldPushEndDestination = .chats } } else { stateSubject.value.shouldPushEndDestination = .onboarding } } if !messenger.isBackupRunning() { try? messenger.resumeBackup() } groupRequestCancellable = groupRequest { [weak self] in self?.log(.error($0 as NSError)) } groupMessageHandlerCancellable = groupMessageHandler { [weak self] in self?.log(.error($0 as NSError)) } try messenger.startGroupChat() try messenger.trackServices { [weak self] in self?.log(.error($0 as NSError)) } try messenger.startFileTransfer() networkMonitor.start() networkHandlerCancellable = messenger.cMix.get()!.addHealthCallback( HealthCallback { self.networkMonitor.update($0) } ) try failPendingProcessesFromLastSession() } } extension LaunchViewModel { func failPendingProcessesFromLastSession() throws { try dbManager.getDB().bulkUpdateMessages( .init(status: [.sending]), .init(status: .sendingFailed) ) } func updateBannedList(completion: @escaping () -> Void) { fetchBannedList { result in switch result { case .failure(_): DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.updateBannedList(completion: completion) } case .success(let data): self.processBannedList(data, completion: completion) } } } func processBannedList(_ data: Data, completion: @escaping () -> Void) { processBannedList( data: data, forEach: { result in switch result { case .success(let userId): let query = Contact.Query(id: [userId]) if var contact = try! dbManager.getDB().fetchContacts(query).first { if contact.isBanned == false { contact.isBanned = true try! dbManager.getDB().saveContact(contact) enqueueBanWarning(contact: contact) } } else { try! dbManager.getDB().saveContact(.init(id: userId, isBanned: true)) } case .failure(_): break } }, completion: { result in switch result { case .failure(_): DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.updateBannedList(completion: completion) } case .success(_): completion() } } ) } func enqueueBanWarning(contact: XXModels.Contact) { let name = (contact.nickname ?? contact.username) ?? "One of your contacts" toastManager.enqueue(.init( title: "\(name) has been banned for offensive content.", leftImage: Asset.requestSentToaster.image )) } func getContactWith(userId: Data) -> XXModels.Contact? { try? dbManager.getDB().fetchContacts(.init( id: [userId], isBlocked: reportingStatus.isEnabled() ? false : nil, isBanned: reportingStatus.isEnabled() ? false : nil )).first } func getGroupInfoWith(groupId: Data) -> GroupInfo? { try? dbManager.getDB().fetchGroupInfos(.init(groupId: groupId)).first } }