import AppCore import Combine import ComposableArchitecture import Foundation import XCTestDynamicOverlay import XXMessengerClient import XXModels public struct RestoreState: Equatable { public enum Field: String, Hashable { case passphrase } public struct File: Equatable { public init(name: String, data: Data) { self.name = name self.data = data } public var name: String public var data: Data } public init( file: File? = nil, fileImportFailure: String? = nil, restoreFailures: [String] = [], focusedField: Field? = nil, isImportingFile: Bool = false, passphrase: String = "", isRestoring: Bool = false ) { self.file = file self.fileImportFailure = fileImportFailure self.restoreFailures = restoreFailures self.focusedField = focusedField self.isImportingFile = isImportingFile self.passphrase = passphrase self.isRestoring = isRestoring } public var file: File? public var fileImportFailure: String? public var restoreFailures: [String] @BindableState public var focusedField: Field? @BindableState public var isImportingFile: Bool @BindableState public var passphrase: String @BindableState public var isRestoring: Bool } public enum RestoreAction: Equatable, BindableAction { case importFileTapped case fileImport(Result<URL, NSError>) case restoreTapped case finished case failed([NSError]) case binding(BindingAction<RestoreState>) } public struct RestoreEnvironment { public init( messenger: Messenger, db: DBManagerGetDB, loadData: URLDataLoader, now: @escaping () -> Date, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue> ) { self.messenger = messenger self.db = db self.loadData = loadData self.now = now self.mainQueue = mainQueue self.bgQueue = bgQueue } public var messenger: Messenger public var db: DBManagerGetDB public var loadData: URLDataLoader public var now: () -> Date public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> } #if DEBUG extension RestoreEnvironment { public static let unimplemented = RestoreEnvironment( messenger: .unimplemented, db: .unimplemented, loadData: .unimplemented, now: XCTUnimplemented("\(Self.self).now"), mainQueue: .unimplemented, bgQueue: .unimplemented ) } #endif public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironment> { state, action, env in switch action { case .importFileTapped: state.isImportingFile = true state.fileImportFailure = nil return .none case .fileImport(.success(let url)): state.isImportingFile = false do { state.file = .init( name: url.lastPathComponent, data: try env.loadData(url) ) state.fileImportFailure = nil } catch { state.file = nil state.fileImportFailure = error.localizedDescription } return .none case .fileImport(.failure(let error)): state.isImportingFile = false state.file = nil state.fileImportFailure = error.localizedDescription return .none case .restoreTapped: guard let backupData = state.file?.data, backupData.count > 0 else { return .none } let backupPassphrase = state.passphrase state.isRestoring = true state.restoreFailures = [] return Effect.result { do { let result = try env.messenger.restoreBackup( backupData: backupData, backupPassphrase: backupPassphrase ) let facts = try env.messenger.ud.tryGet().getFacts() try env.db().saveContact(Contact( id: try env.messenger.e2e.tryGet().getContact().getId(), username: facts.get(.username)?.value, email: facts.get(.email)?.value, phone: facts.get(.phone)?.value, createdAt: env.now() )) try result.restoredContacts.forEach { contactId in if try env.db().fetchContacts(.init(id: [contactId])).isEmpty { try env.db().saveContact(Contact( id: contactId, createdAt: env.now() )) } } return .success(.finished) } catch { var errors = [error as NSError] do { try env.messenger.destroy() } catch { errors.append(error as NSError) } return .success(.failed(errors)) } } .subscribe(on: env.bgQueue) .receive(on: env.mainQueue) .eraseToEffect() case .finished: state.isRestoring = false return .none case .failed(let errors): state.isRestoring = false state.restoreFailures = errors.map(\.localizedDescription) return .none case .binding(_): return .none } } .binding()