diff --git a/App/client-ios.xcodeproj/project.pbxproj b/App/client-ios.xcodeproj/project.pbxproj index 5a37a1fdc96d3f08bc3a58a1292fcdc22f9f402d..87ff542bdc1cb222888508d0c6737164e264baf4 100644 --- a/App/client-ios.xcodeproj/project.pbxproj +++ b/App/client-ios.xcodeproj/project.pbxproj @@ -448,7 +448,7 @@ CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 288; + CURRENT_PROJECT_VERSION = 289; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -487,7 +487,7 @@ CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 288; + CURRENT_PROJECT_VERSION = 289; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -522,7 +522,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 288; + CURRENT_PROJECT_VERSION = 289; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -553,7 +553,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 288; + CURRENT_PROJECT_VERSION = 289; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index 9b7e38445e805bcea087ffe8c3e22a8c7c078f3d..c4d76fdeef07350ba48d3fb275c7101d0518926b 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -15,6 +15,8 @@ import XXClient import XXMessengerClient import CloudFiles +import CloudFilesDrive +import CloudFilesICloud import CloudFilesDropbox public class AppDelegate: UIResponder, UIApplicationDelegate { diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index b15fe648a8c9d98c6faf73de46d1b5025d68cb0c..5fe201895d13401ece9994aab414f6ff5c0b7230 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -43,233 +43,233 @@ import RequestsFeature import OnboardingFeature import ContactListFeature -import KeychainAccess import XXClient +import KeychainAccess struct DependencyRegistrator { - static private let container = DependencyInjection.Container.shared - - // MARK: MOCK - - static func registerForMock() { - container.register(XXLogger.noop) - container.register(CrashReporter.noop) - container.register(VersionChecker.mock) - container.register(ReportingStatus.mock()) - container.register(SendReport.mock()) - container.register(MockNetworkMonitor() as NetworkMonitoring) - container.register(KeyObjectStore.userDefaults) - container.register(MockPushHandler() as PushHandling) - container.register(MockKeychainHandler() as KeychainHandling) - container.register(MockPermissionHandler() as PermissionHandling) - - registerCommonDependencies() - } - - // MARK: LIVE - - static func registerForLive() { - let cMixManager = CMixManager.live(passwordStorage: .keychain) - container.register(cMixManager) - - container.register(GetIdFromContact.live) - container.register(GetFactsFromContact.live) - - container.register(KeyObjectStore.userDefaults) - container.register(XXLogger.live()) - container.register(CrashReporter.live) - container.register(VersionChecker.live()) - container.register(ReportingStatus.live()) - container.register(SendReport.live) - - container.register(NetworkMonitor() as NetworkMonitoring) - container.register(PushHandler() as PushHandling) - container.register(KeychainHandler() as KeychainHandling) - container.register(PermissionHandler() as PermissionHandling) - - registerCommonDependencies() - } - - // MARK: COMMON - - static private func registerCommonDependencies() { - container.register(Voxophone()) - container.register(BackupService()) - container.register(MakeAppScreenshot.live) - container.register(FetchBannedList.live) - container.register(ProcessBannedList.live) - container.register(MakeReportDrawer.live) - - // MARK: Isolated - - container.register(HUD()) - container.register(ThemeController() as ThemeControlling) - container.register(ToastController()) - container.register(StatusBarController() as StatusBarStyleControlling) - - // MARK: Coordinators - - container.register( - TermsCoordinator.live( - usernameFactory: OnboardingUsernameController.init, - chatListFactory: ChatListController.init - ) - ) - - container.register( - LaunchCoordinator( - termsFactory: TermsConditionsController.init, - searchFactory: SearchContainerController.init, - requestsFactory: RequestsContainerController.init, - chatListFactory: ChatListController.init, - onboardingFactory: OnboardingStartController.init, - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:) - ) as LaunchCoordinating) - - container.register( - BackupCoordinator( - sftpFactory: BackupSFTPController.init(_:), - passphraseFactory: BackupPassphraseController.init(_:_:) - ) as BackupCoordinating) - - container.register( - MenuCoordinator( - scanFactory: ScanContainerController.init, - chatsFactory: ChatListController.init, - profileFactory: ProfileController.init, - settingsFactory: SettingsController.init, - contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init - ) as MenuCoordinating) - - container.register( - SearchCoordinator( - contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init, - contactFactory: ContactController.init(_:), - countriesFactory: CountryListController.init(_:) - ) as SearchCoordinating) - - container.register( - ProfileCoordinator( - emailFactory: ProfileEmailController.init, - phoneFactory: ProfilePhoneController.init, - imagePickerFactory: UIImagePickerController.init, - permissionFactory: RequestPermissionController.init, - sideMenuFactory: MenuController.init(_:_:), - countriesFactory: CountryListController.init(_:), - codeFactory: ProfileCodeController.init(_:_:) - ) as ProfileCoordinating) - - container.register( - SettingsCoordinator( - backupFactory: BackupController.init, - advancedFactory: SettingsAdvancedController.init, - accountDeleteFactory: AccountDeleteController.init, - sideMenuFactory: MenuController.init(_:_:) - ) as SettingsCoordinating) - - container.register( - RestoreCoordinator( - successFactory: RestoreSuccessController.init, - chatListFactory: ChatListController.init, - restoreFactory: RestoreController.init(_:), - sftpFactory: RestoreSFTPController.init(_:), - passphraseFactory: RestorePassphraseController.init(_:_:) - ) as RestoreCoordinating) - - container.register( - ChatCoordinator( - retryFactory: RetrySheetController.init, - webFactory: WebScreen.init(url:), - previewFactory: QLPreviewController.init, - contactFactory: ContactController.init(_:), - imagePickerFactory: UIImagePickerController.init, - permissionFactory: RequestPermissionController.init - ) as ChatCoordinating) - - container.register( - ContactCoordinator( - requestsFactory: RequestsContainerController.init, - singleChatFactory: SingleChatController.init(_:), - imagePickerFactory: UIImagePickerController.init, - nicknameFactory: NicknameController.init(_:_:) - ) as ContactCoordinating) - - container.register( - RequestsCoordinator( - searchFactory: SearchContainerController.init, - contactFactory: ContactController.init(_:), - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:), - sideMenuFactory: MenuController.init(_:_:), - nicknameFactory: NicknameController.init(_:_:) - ) as RequestsCoordinating) - - container.register( - OnboardingCoordinator( - emailFactory: OnboardingEmailController.init, - phoneFactory: OnboardingPhoneController.init, - searchFactory: SearchContainerController.init, - welcomeFactory: OnboardingWelcomeController.init, - chatListFactory: ChatListController.init, - termsFactory: TermsConditionsController.init, - usernameFactory: OnboardingUsernameController.init, - restoreListFactory: RestoreListController.init, - successFactory: OnboardingSuccessController.init(_:), - countriesFactory: CountryListController.init(_:), - phoneConfirmationFactory: OnboardingPhoneConfirmationController.init(_:_:), - emailConfirmationFactory: OnboardingEmailConfirmationController.init(_:_:) - ) as OnboardingCoordinating) - - container.register( - ContactListCoordinator( - scanFactory: ScanContainerController.init, - searchFactory: SearchContainerController.init, - newGroupFactory: CreateGroupController.init, - requestsFactory: RequestsContainerController.init, - contactFactory: ContactController.init(_:), - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:), - sideMenuFactory: MenuController.init(_:_:), - groupDrawerFactory: CreateDrawerController.init(_:_:) - ) as ContactListCoordinating) - - container.register( - ScanCoordinator( - emailFactory: ProfileEmailController.init, - phoneFactory: ProfilePhoneController.init, - contactsFactory: ContactListController.init, - requestsFactory: RequestsContainerController.init, - contactFactory: ContactController.init(_:), - sideMenuFactory: MenuController.init(_:_:) - ) as ScanCoordinating) - - - container.register( - ChatListCoordinator( - scanFactory: ScanContainerController.init, - searchFactory: SearchContainerController.init, - newGroupFactory: CreateGroupController.init, - contactsFactory: ContactListController.init, - contactFactory: ContactController.init(_:), - singleChatFactory: SingleChatController.init(_:), - groupChatFactory: GroupChatController.init(_:), - sideMenuFactory: MenuController.init(_:_:) - ) as ChatListCoordinating) - } + static private let container = DependencyInjection.Container.shared + + // MARK: MOCK + + static func registerForMock() { + container.register(XXLogger.noop) + container.register(CrashReporter.noop) + container.register(VersionChecker.mock) + container.register(ReportingStatus.mock()) + container.register(SendReport.mock()) + container.register(MockNetworkMonitor() as NetworkMonitoring) + container.register(KeyObjectStore.userDefaults) + container.register(MockPushHandler() as PushHandling) + container.register(MockKeychainHandler() as KeychainHandling) + container.register(MockPermissionHandler() as PermissionHandling) + + registerCommonDependencies() + } + + // MARK: LIVE + + static func registerForLive() { + let cMixManager = CMixManager.live(passwordStorage: .keychain) + container.register(cMixManager) + + container.register(GetIdFromContact.live) + container.register(GetFactsFromContact.live) + + container.register(KeyObjectStore.userDefaults) + container.register(XXLogger.live()) + container.register(CrashReporter.live) + container.register(VersionChecker.live()) + container.register(ReportingStatus.live()) + container.register(SendReport.live) + + container.register(NetworkMonitor() as NetworkMonitoring) + container.register(PushHandler() as PushHandling) + container.register(KeychainHandler() as KeychainHandling) + container.register(PermissionHandler() as PermissionHandling) + + registerCommonDependencies() + } + + // MARK: COMMON + + static private func registerCommonDependencies() { + container.register(Voxophone()) + container.register(BackupService()) + container.register(MakeAppScreenshot.live) + container.register(FetchBannedList.live) + container.register(ProcessBannedList.live) + container.register(MakeReportDrawer.live) + + // MARK: Isolated + + container.register(HUD()) + container.register(ThemeController() as ThemeControlling) + container.register(ToastController()) + container.register(StatusBarController() as StatusBarStyleControlling) + + // MARK: Coordinators + + container.register( + TermsCoordinator.live( + usernameFactory: OnboardingUsernameController.init, + chatListFactory: ChatListController.init + ) + ) + + container.register( + LaunchCoordinator( + termsFactory: TermsConditionsController.init, + searchFactory: SearchContainerController.init, + requestsFactory: RequestsContainerController.init, + chatListFactory: ChatListController.init, + onboardingFactory: OnboardingStartController.init, + singleChatFactory: SingleChatController.init(_:), + groupChatFactory: GroupChatController.init(_:) + ) as LaunchCoordinating) + + container.register( + BackupCoordinator( + sftpFactory: BackupSFTPController.init(_:), + passphraseFactory: BackupPassphraseController.init(_:_:) + ) as BackupCoordinating) + + container.register( + MenuCoordinator( + scanFactory: ScanContainerController.init, + chatsFactory: ChatListController.init, + profileFactory: ProfileController.init, + settingsFactory: SettingsController.init, + contactsFactory: ContactListController.init, + requestsFactory: RequestsContainerController.init + ) as MenuCoordinating) + + container.register( + SearchCoordinator( + contactsFactory: ContactListController.init, + requestsFactory: RequestsContainerController.init, + contactFactory: ContactController.init(_:), + countriesFactory: CountryListController.init(_:) + ) as SearchCoordinating) + + container.register( + ProfileCoordinator( + emailFactory: ProfileEmailController.init, + phoneFactory: ProfilePhoneController.init, + imagePickerFactory: UIImagePickerController.init, + permissionFactory: RequestPermissionController.init, + sideMenuFactory: MenuController.init(_:_:), + countriesFactory: CountryListController.init(_:), + codeFactory: ProfileCodeController.init(_:_:) + ) as ProfileCoordinating) + + container.register( + SettingsCoordinator( + backupFactory: BackupController.init, + advancedFactory: SettingsAdvancedController.init, + accountDeleteFactory: AccountDeleteController.init, + sideMenuFactory: MenuController.init(_:_:) + ) as SettingsCoordinating) + + container.register( + RestoreCoordinator( + successFactory: RestoreSuccessController.init, + chatListFactory: ChatListController.init, + restoreFactory: RestoreController.init(_:), + sftpFactory: RestoreSFTPController.init(_:), + passphraseFactory: RestorePassphraseController.init(_:_:) + ) as RestoreCoordinating) + + container.register( + ChatCoordinator( + retryFactory: RetrySheetController.init, + webFactory: WebScreen.init(url:), + previewFactory: QLPreviewController.init, + contactFactory: ContactController.init(_:), + imagePickerFactory: UIImagePickerController.init, + permissionFactory: RequestPermissionController.init + ) as ChatCoordinating) + + container.register( + ContactCoordinator( + requestsFactory: RequestsContainerController.init, + singleChatFactory: SingleChatController.init(_:), + imagePickerFactory: UIImagePickerController.init, + nicknameFactory: NicknameController.init(_:_:) + ) as ContactCoordinating) + + container.register( + RequestsCoordinator( + searchFactory: SearchContainerController.init, + contactFactory: ContactController.init(_:), + singleChatFactory: SingleChatController.init(_:), + groupChatFactory: GroupChatController.init(_:), + sideMenuFactory: MenuController.init(_:_:), + nicknameFactory: NicknameController.init(_:_:) + ) as RequestsCoordinating) + + container.register( + OnboardingCoordinator( + emailFactory: OnboardingEmailController.init, + phoneFactory: OnboardingPhoneController.init, + searchFactory: SearchContainerController.init, + welcomeFactory: OnboardingWelcomeController.init, + chatListFactory: ChatListController.init, + termsFactory: TermsConditionsController.init, + usernameFactory: OnboardingUsernameController.init, + restoreListFactory: RestoreListController.init, + successFactory: OnboardingSuccessController.init(_:), + countriesFactory: CountryListController.init(_:), + phoneConfirmationFactory: OnboardingPhoneConfirmationController.init(_:_:), + emailConfirmationFactory: OnboardingEmailConfirmationController.init(_:_:) + ) as OnboardingCoordinating) + + container.register( + ContactListCoordinator( + scanFactory: ScanContainerController.init, + searchFactory: SearchContainerController.init, + newGroupFactory: CreateGroupController.init, + requestsFactory: RequestsContainerController.init, + contactFactory: ContactController.init(_:), + singleChatFactory: SingleChatController.init(_:), + groupChatFactory: GroupChatController.init(_:), + sideMenuFactory: MenuController.init(_:_:), + groupDrawerFactory: CreateDrawerController.init(_:_:) + ) as ContactListCoordinating) + + container.register( + ScanCoordinator( + emailFactory: ProfileEmailController.init, + phoneFactory: ProfilePhoneController.init, + contactsFactory: ContactListController.init, + requestsFactory: RequestsContainerController.init, + contactFactory: ContactController.init(_:), + sideMenuFactory: MenuController.init(_:_:) + ) as ScanCoordinating) + + + container.register( + ChatListCoordinator( + scanFactory: ScanContainerController.init, + searchFactory: SearchContainerController.init, + newGroupFactory: CreateGroupController.init, + contactsFactory: ContactListController.init, + contactFactory: ContactController.init(_:), + singleChatFactory: SingleChatController.init(_:), + groupChatFactory: GroupChatController.init(_:), + sideMenuFactory: MenuController.init(_:_:) + ) as ChatListCoordinating) + } } extension PasswordStorage { - static let keychain: PasswordStorage = { - let keychain = KeychainAccess.Keychain( - service: "XXM" - ) - return PasswordStorage( - save: { password in keychain[data: "password"] = password}, - load: { try keychain[data: "password"] ?? { throw MissingPasswordError() }() }, - remove: { try keychain.remove("password") } - ) - }() + static let keychain: PasswordStorage = { + let keychain = KeychainAccess.Keychain( + service: "XXM" + ) + return PasswordStorage( + save: { password in keychain[data: "password"] = password}, + load: { try keychain[data: "password"] ?? { throw MissingPasswordError() }() }, + remove: { try keychain.remove("password") } + ) + }() } diff --git a/Sources/BackupFeature/Controllers/BackupConfigController.swift b/Sources/BackupFeature/Controllers/BackupConfigController.swift index 722e5530adcbe0bf9e4e8be9d0f9518f3893fd02..f17bdbc85f9df2b65441645f456730d7ac642469 100644 --- a/Sources/BackupFeature/Controllers/BackupConfigController.swift +++ b/Sources/BackupFeature/Controllers/BackupConfigController.swift @@ -2,6 +2,7 @@ import UIKit import Models import Shared import Combine +import CloudFiles import DrawerFeature import DependencyInjection @@ -131,7 +132,7 @@ final class BackupConfigController: UIViewController { .store(in: &cancellables) } - private func decorate(enabledService: BackupProvider?) { + private func decorate(enabledService: CloudService?) { var button: BackupSwitcherButton? switch enabledService { @@ -188,7 +189,7 @@ final class BackupConfigController: UIViewController { } } - private func decorate(connectedServices: Set<BackupProvider>) { + private func decorate(connectedServices: Set<CloudService>) { if connectedServices.contains(.icloud) { screenView.iCloudButton.showSwitcher(enabled: false) } else { diff --git a/Sources/BackupFeature/Controllers/BackupSFTPController.swift b/Sources/BackupFeature/Controllers/BackupSFTPController.swift new file mode 100644 index 0000000000000000000000000000000000000000..7c5c9c7cf372c5837ded9979478c25a8e568cc45 --- /dev/null +++ b/Sources/BackupFeature/Controllers/BackupSFTPController.swift @@ -0,0 +1,88 @@ +import HUD +import UIKit +import Combine +import DependencyInjection +import ScrollViewController + +public final class BackupSFTPController: UIViewController { + @Dependency private var hud: HUD + + lazy private var screenView = BackupSFTPView() + lazy private var scrollViewController = ScrollViewController() + + private let completion: (String, String, String) -> Void + private let viewModel = BackupSFTPViewModel() + private var cancellables = Set<AnyCancellable>() + + public init(_ completion: @escaping (String, String, String) -> Void) { + self.completion = completion + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationItem.backButtonTitle = "" + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupBindings() + } + + private func setupScrollView() { + scrollViewController.scrollView.backgroundColor = .white + + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + } + + private func setupBindings() { + viewModel.hudPublisher + .receive(on: DispatchQueue.main) + .sink { [hud] in hud.update(with: $0) } + .store(in: &cancellables) + + viewModel.authPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] params in + completion(params.0, params.1, params.2) + navigationController?.popViewController(animated: true) + }.store(in: &cancellables) + + screenView.hostField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterHost($0) } + .store(in: &cancellables) + + screenView.usernameField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterUsername($0) } + .store(in: &cancellables) + + screenView.passwordField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterPassword($0) } + .store(in: &cancellables) + + viewModel.statePublisher + .receive(on: DispatchQueue.main) + .map(\.isButtonEnabled) + .sink { [unowned self] in screenView.loginButton.isEnabled = $0 } + .store(in: &cancellables) + + screenView.loginButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapLogin() } + .store(in: &cancellables) + } +} diff --git a/Sources/BackupFeature/Models/BackupSettings.swift b/Sources/BackupFeature/Models/BackupSettings.swift deleted file mode 100644 index 5442f3015665a829f20287318a35ae47221a27f9..0000000000000000000000000000000000000000 --- a/Sources/BackupFeature/Models/BackupSettings.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation -import CloudFiles - -public struct BackupSettings: Equatable, Codable { - public var wifiOnlyBackup: Bool - public var automaticBackups: Bool - public var enabledService: BackupProvider? - public var connectedServices: Set<BackupProvider> - public var backups: [BackupProvider: Fetch.Metadata] - - public init( - wifiOnlyBackup: Bool = false, - automaticBackups: Bool = false, - enabledService: BackupProvider? = nil, - connectedServices: Set<BackupProvider> = [], - backups: [BackupProvider: Fetch.Metadata] = [:] - ) { - self.wifiOnlyBackup = wifiOnlyBackup - self.automaticBackups = automaticBackups - self.enabledService = enabledService - self.connectedServices = connectedServices - self.backups = backups - } - - public func toData() -> Data { - (try? PropertyListEncoder().encode(self)) ?? Data() - } - - public init(fromData data: Data?) { - if let data = data, let settings = try? PropertyListDecoder().decode(BackupSettings.self, from: data) { - self.init( - wifiOnlyBackup: settings.wifiOnlyBackup, - automaticBackups: settings.automaticBackups, - enabledService: settings.enabledService, - connectedServices: settings.connectedServices, - backups: settings.backups - ) - } else { - self.init( - wifiOnlyBackup: false, - automaticBackups: true, - enabledService: nil, - connectedServices: [], - backups: [:] - ) - } - } -} diff --git a/Sources/BackupFeature/Service/BackupService.swift b/Sources/BackupFeature/Service/BackupService.swift index 1599a423406940914a6423d4f4030bcfad45f82d..c99e9446fc9ec9b54ef6593cc3acb484f4db95bb 100644 --- a/Sources/BackupFeature/Service/BackupService.swift +++ b/Sources/BackupFeature/Service/BackupService.swift @@ -3,24 +3,12 @@ import Models import Combine import XXClient import Defaults -import NetworkMonitor -import XXMessengerClient -import DependencyInjection - import CloudFiles import CloudFilesSFTP -import CloudFilesDrive -import CloudFilesICloud -import CloudFilesDropbox - +import NetworkMonitor import KeychainAccess - -public enum BackupProvider: Equatable, Codable { - case sftp - case drive - case icloud - case dropbox -} +import XXMessengerClient +import DependencyInjection public final class BackupService { @Dependency var messenger: Messenger @@ -31,17 +19,20 @@ public final class BackupService { @KeyObject(.username, defaultValue: nil) var username: String? @KeyObject(.backupSettings, defaultValue: nil) var storedSettings: Data? - public var settingsPublisher: AnyPublisher<BackupSettings, Never> { + public var settingsPublisher: AnyPublisher<CloudSettings, Never> { settings.handleEvents(receiveSubscription: { [weak self] _ in guard let self = self else { return } - self.refreshConnections() - self.refreshBackups() + self.settings.value.connectedServices = CloudFilesManager.all.linkedServices() + CloudFilesManager.all.lastBackups { [weak self] in + guard let self else { return } + self.settings.value.backups = $0 + } }).eraseToAnyPublisher() } private var connType: ConnectionType = .wifi private var cancellables = Set<AnyCancellable>() - private lazy var settings = CurrentValueSubject<BackupSettings, Never>(.init(fromData: storedSettings)) + private lazy var settings = CurrentValueSubject<CloudSettings, Never>(.init(fromData: storedSettings)) public init() { settings @@ -56,173 +47,160 @@ public final class BackupService { .store(in: &cancellables) } - public func setupSFTP(host: String, username: String, password: String) { - managers[.sftp] = .sftp( - host: host, - username: username, - password: password, - fileName: "backup.xxm" - ) - refreshBackups() - refreshConnections() + func didSetWiFiOnly(enabled: Bool) { + settings.value.wifiOnlyBackup = enabled } -} -extension BackupService { - public func stopBackups() { - if messenger.isBackupRunning() == true { - try! messenger.stopBackup() - } + func didSetAutomaticBackup(enabled: Bool) { + settings.value.automaticBackups = enabled + shouldBackupIfSetAutomatic() } - public func initializeBackup(passphrase: String) { - try! messenger.startBackup( - password: passphrase, - params: .init( - username: username!, - email: email, - phone: phone - ) - ) + func toggle(service: CloudService, enabling: Bool) { + settings.value.enabledService = enabling ? service : nil } - public func performBackupIfAutomaticIsEnabled() { - guard settings.value.automaticBackups == true else { return } - performBackup() + func didForceBackup() { + if let lastBackup = try? Data(contentsOf: getBackupURL()) { + performUpload(of: lastBackup) + } } - public func performBackup() { - guard let directoryUrl = try? FileManager.default.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) else { fatalError("Couldn't generate the URL to persist the backup") } - - let fileUrl = directoryUrl - .appendingPathComponent("backup") - .appendingPathExtension("xxm") - - guard let data = try? Data(contentsOf: fileUrl) else { return } - performBackup(data: data) + public func didUpdateFacts() { + storeFacts() } - public func updateBackup(data: Data) { - guard let directoryUrl = try? FileManager.default.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) else { fatalError("Couldn't generate the URL to persist the backup") } - - let fileUrl = directoryUrl - .appendingPathComponent("backup") - .appendingPathExtension("xxm") - + public func updateLocalBackup(_ data: Data) { do { - try data.write(to: fileUrl) + try data.write(to: getBackupURL()) + shouldBackupIfSetAutomatic() } catch { fatalError("Couldn't write backup to fileurl") } + } - let isWifiOnly = settings.value.wifiOnlyBackup - let isAutomaticEnabled = settings.value.automaticBackups - let hasEnabledService = settings.value.enabledService != nil - - if isWifiOnly { - guard connType == .wifi else { return } + private func shouldBackupIfSetAutomatic() { + guard let lastBackup = try? Data(contentsOf: getBackupURL()) else { + print(">>> No stored backup so won't upload anything.") + return + } + guard settings.value.automaticBackups else { + print(">>> Backups are not set to automatic") + return + } + guard settings.value.enabledService != nil else { + print(">>> No service enabled to upload") + return + } + if settings.value.wifiOnlyBackup { + guard connType == .wifi else { + print(">>> WiFi only backups, and connType != Wifi") + return + } } else { - guard connType != .unknown else { return } + guard connType != .unknown else { + print(">>> Connectivity is unknown") + return + } } + performUpload(of: lastBackup) + } - if isAutomaticEnabled && hasEnabledService { - performBackup() + // MARK: - Messenger + + func initializeBackup(passphrase: String) { + do { + try messenger.startBackup( + password: passphrase, + params: .init( + username: username!, + email: email, + phone: phone + ) + ) + } catch { + print(">>> Exception when calling `messenger.startBackup`: \(error.localizedDescription)") } + } - refreshBackups() + func stopBackups() { + if messenger.isBackupRunning() == true { + do { + try messenger.stopBackup() + } catch { + print(">>> Exception when calling `messenger.stopBackup`: \(error.localizedDescription)") + } + } } - public func setBackupOnlyOnWifi(_ enabled: Bool) { - settings.value.wifiOnlyBackup = enabled + func storeFacts() { + var facts: [String: String] = [:] + facts["username"] = username! + facts["email"] = email + facts["phone"] = phone + facts["timestamp"] = "\(Date.asTimestamp)" + guard let backupManager = messenger.backup.get() else { + print(">>> Tried to store facts in JSON but there's no backup manager instance") + return + } + guard let data = try? JSONSerialization.data(withJSONObject: facts) else { + print(">>> Tried to generate data with json dictionary but failed") + return + } + guard let string = String(data: data, encoding: .utf8) else { + print(">>> Tried to extract string from json dict object but failed") + return + } + backupManager.addJSON(string) } - public func setBackupAutomatically(_ enabled: Bool) { - settings.value.automaticBackups = enabled + // MARK: - CloudProviders - guard enabled else { return } - performBackup() - } + func setupSFTP(host: String, username: String, password: String) { + let sftpManager = CloudFilesManager.sftp( + host: host, + username: username, + password: password, + fileName: "backup.xxm" + ) - public func toggle(service: BackupProvider, enabling: Bool) { - settings.value.enabledService = enabling ? service : nil - } + CloudFilesManager.all[.sftp] = sftpManager - public func authorize( - service: BackupProvider, - presenting screen: UIViewController - ) { do { - try managers[service]?.link(screen) { [weak self] in - guard let self else { return } + try sftpManager.fetch { switch $0 { - case .success: - self.refreshConnections() - self.refreshBackups() + case .success(let metadata): + self.settings.value.backups[.sftp] = metadata case .failure(let error): - print(error.localizedDescription) + print(">>> Error fetching sftp: \(error.localizedDescription)") } } } catch { - print(error.localizedDescription) + print(">>> Exception fetching sftp: \(error.localizedDescription)") } } -} -extension BackupService { - private func refreshConnections() { - managers.forEach { provider, manager in - if manager.isLinked() && !settings.value.connectedServices.contains(provider) { - settings.value.connectedServices.insert(provider) - } else if !manager.isLinked() && settings.value.connectedServices.contains(provider) { - settings.value.connectedServices.remove(provider) - } - } - } - - private func refreshBackups() { - managers.forEach { provider, manager in - if manager.isLinked() { - do { - try manager.fetch { [weak self] in - guard let self else { return } - - switch $0 { - case .success(let metadata): - self.settings.value.backups[provider] = metadata - case .failure(let error): - print(error.localizedDescription) - } - } - } catch { - print(error.localizedDescription) + func authorize( + service: CloudService, + presenting screen: UIViewController + ) { + service.authorize(presenting: screen) { + switch $0 { + case .success: + self.settings.value.connectedServices = CloudFilesManager.all.linkedServices() + CloudFilesManager.all.lastBackups { [weak self] in + guard let self else { return } + self.settings.value.backups = $0 } + case .failure(let error): + print(">>> Tried to authorize \(service) but failed: \(error.localizedDescription)") } } } - private func performBackup(data: Data) { + func performUpload(of data: Data) { guard let enabledService = settings.value.enabledService else { - fatalError("Trying to backup but nothing is enabled") - } - - let url = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent(UUID().uuidString) - - do { - try data.write(to: url, options: .atomic) - } catch { - print(">>> Couldn't write to temp: \(error.localizedDescription)") - return + fatalError(">>> Trying to backup but nothing is enabled") } if enabledService == .sftp { @@ -230,10 +208,10 @@ extension BackupService { guard let host = try? keychain.get("host"), let password = try? keychain.get("pwd"), let username = try? keychain.get("username") else { - fatalError("Tried to perform an sftp backup but its not configured") + fatalError(">>> Tried to perform an sftp backup but its not configured") } - managers[.sftp] = .sftp( + CloudFilesManager.all[.sftp] = .sftp( host: host, username: username, password: password, @@ -241,24 +219,29 @@ extension BackupService { ) } - if let manager = managers[enabledService] { - do { - try manager.upload(data) { [weak self] in - guard let self else { return } - - switch $0 { - case .success(let metadata): - self.settings.value.backups[enabledService] = .init( - size: metadata.size, - lastModified: metadata.lastModified - ) - case .failure(let error): - print(error.localizedDescription) - } - } - } catch { - print(error.localizedDescription) + enabledService.backup(data: data) { + switch $0 { + case .success(let metadata): + self.settings.value.backups[enabledService] = .init( + size: metadata.size, + lastModified: metadata.lastModified + ) + case .failure(let error): + print(">>> Failed to perform a backup upload: \(error.localizedDescription)") } } } + + private func getBackupURL() -> URL { + guard let folderURL = try? FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) else { fatalError(">>> Couldn't generate the URL for backup") } + + return folderURL + .appendingPathComponent("backup") + .appendingPathExtension("xxm") + } } diff --git a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift index ba2ce35ef322919fa4584106eca3fdc848ff89e4..6d5daa394016276b431fe1ea1b218d9eade1e23a 100644 --- a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift @@ -21,15 +21,15 @@ struct BackupConfigViewModel { var didTapBackupNow: () -> Void var didChooseWifiOnly: (Bool) -> Void var didChooseAutomatic: (Bool) -> Void - var didToggleService: (UIViewController, BackupProvider, Bool) -> Void - var didTapService: (BackupProvider, UIViewController) -> Void + var didToggleService: (UIViewController, CloudService, Bool) -> Void + var didTapService: (CloudService, UIViewController) -> Void var wifiOnly: () -> AnyPublisher<Bool, Never> var automatic: () -> AnyPublisher<Bool, Never> var lastBackup: () -> AnyPublisher<Fetch.Metadata?, Never> var actionState: () -> AnyPublisher<BackupActionState, Never> - var enabledService: () -> AnyPublisher<BackupProvider?, Never> - var connectedServices: () -> AnyPublisher<Set<BackupProvider>, Never> + var enabledService: () -> AnyPublisher<CloudService?, Never> + var connectedServices: () -> AnyPublisher<Set<CloudService>, Never> } extension BackupConfigViewModel { @@ -44,14 +44,14 @@ extension BackupConfigViewModel { return .init( didTapBackupNow: { - context.service.performBackup() + context.service.didForceBackup() context.hud.update(with: .on) DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { context.hud.update(with: .none) } }, - didChooseWifiOnly: context.service.setBackupOnlyOnWifi(_:), - didChooseAutomatic: context.service.setBackupAutomatically(_:), + didChooseWifiOnly: context.service.didSetWiFiOnly(enabled:), + didChooseAutomatic: context.service.didSetAutomaticBackup(enabled:), didToggleService: { controller, service, enabling in guard enabling == true else { context.service.toggle(service: service, enabling: false) diff --git a/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift b/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..9b113c9f175c712fd4715b749238e73d21ad54b1 --- /dev/null +++ b/Sources/BackupFeature/ViewModels/BackupSFTPViewModel.swift @@ -0,0 +1,102 @@ +import UIKit + +import HUD +import Shout +import Socket +import Combine +import Foundation +import CloudFiles +import CloudFilesSFTP + +struct SFTPViewState { + var host: String = "" + var username: String = "" + var password: String = "" + var isButtonEnabled: Bool = false +} + +final class BackupSFTPViewModel { + var hudPublisher: AnyPublisher<HUDStatus, Never> { + hudSubject.eraseToAnyPublisher() + } + + var statePublisher: AnyPublisher<SFTPViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + var authPublisher: AnyPublisher<(String, String, String), Never> { + authSubject.eraseToAnyPublisher() + } + + private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) + private let stateSubject = CurrentValueSubject<SFTPViewState, Never>(.init()) + private let authSubject = PassthroughSubject<(String, String, String), Never>() + + func didEnterHost(_ string: String) { + stateSubject.value.host = string + validate() + } + + func didEnterUsername(_ string: String) { + stateSubject.value.username = string + validate() + } + + func didEnterPassword(_ string: String) { + stateSubject.value.password = string + validate() + } + + func didTapLogin() { + hudSubject.send(.on) + + let host = stateSubject.value.host + let username = stateSubject.value.username + let password = stateSubject.value.password + + let anyController = UIViewController() + + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + do { + try CloudFilesManager.sftp( + host: host, + username: username, + password: password, + fileName: "backup.xxm" + ).link(anyController) { + switch $0 { + case .success: + self.hudSubject.send(.none) + self.authSubject.send((host, username, password)) + case .failure(let error): + var message = "An error occurred while trying to link SFTP: " + + if case let CloudFilesSFTP.SFTP.SFTPError.link(linkError) = error { + if let sshError = linkError as? SSHError { + message.append(sshError.message) + } else if let socketError = linkError as? Socket.Error, let reason = socketError.errorReason { + message.append(reason) + } else { + message.append(error.localizedDescription) + } + } else { + message.append(error.localizedDescription) + } + + self.hudSubject.send(.error(.init(content: message))) + } + } + } catch { + self.hudSubject.send(.error(.init(with: error))) + } + } + } + + private func validate() { + stateSubject.value.isButtonEnabled = + !stateSubject.value.host.isEmpty && + !stateSubject.value.username.isEmpty && + !stateSubject.value.password.isEmpty + } +} diff --git a/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift b/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift index c504cd0dec0903c6b21add727adedddc83d6b16b..1dc8e61209c4dde61cc375c74de2181abddc5217 100644 --- a/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupSetupViewModel.swift @@ -2,10 +2,11 @@ import UIKit import Models import Shared import Combine +import CloudFiles import DependencyInjection struct BackupSetupViewModel { - var didTapService: (BackupProvider, UIViewController) -> Void + var didTapService: (CloudService, UIViewController) -> Void } extension BackupSetupViewModel { diff --git a/Sources/BackupFeature/Views/RestoreSFTPView.swift b/Sources/BackupFeature/Views/RestoreSFTPView.swift new file mode 100644 index 0000000000000000000000000000000000000000..dfb4d73a12c784ac51c25b27702ac2d592744f01 --- /dev/null +++ b/Sources/BackupFeature/Views/RestoreSFTPView.swift @@ -0,0 +1,83 @@ +import UIKit +import Shared +import InputField + +final class BackupSFTPView: UIView { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let hostField = OutlinedInputField() + let usernameField = OutlinedInputField() + let passwordField = OutlinedInputField() + let loginButton = CapsuleButton() + let stackView = UIStackView() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + titleLabel.textColor = Asset.neutralDark.color + titleLabel.text = Localized.AccountRestore.Sftp.title + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + let attString = NSMutableAttributedString( + string: Localized.AccountRestore.Sftp.subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ]) + + attString.setAttributes( + attributes: [ + .foregroundColor: Asset.neutralDark.color, + .font: Fonts.Mulish.bold.font(size: 12.0) as Any, + .paragraphStyle: paragraph + ], betweenCharacters: "*") + + subtitleLabel.numberOfLines = 0 + subtitleLabel.attributedText = attString + + hostField.setup(title: Localized.AccountRestore.Sftp.host) + usernameField.setup(title: Localized.AccountRestore.Sftp.username) + passwordField.setup(title: Localized.AccountRestore.Sftp.password, sensitive: true) + + loginButton.set(style: .brandColored, title: Localized.AccountRestore.Sftp.login) + + stackView.spacing = 30 + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.addArrangedSubview(hostField) + stackView.addArrangedSubview(usernameField) + stackView.addArrangedSubview(passwordField) + stackView.addArrangedSubview(loginButton) + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(15) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) + } + + subtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) + } + + stackView.snp.makeConstraints { + $0.top.equalTo(subtitleLabel.snp.bottom).offset(28) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index a207de77bfd825167e5f71efeac4600512daa929..66ba1a1e0323cb8145fa9f623f147733b2dd4294 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -448,21 +448,21 @@ extension LaunchViewModel { } private func generateTransferManager(_ messenger: Messenger) throws { - let manager = try InitFileTransfer.live( - e2eId: messenger.e2e()!.getId(), - callback: .init(handle: { [weak self] in - guard let self = self else { return } - - switch $0 { - case .success(let receivedFile): - self.handleIncomingTransfer(receivedFile, messenger: messenger) - case .failure(let error): - print(error.localizedDescription) - } - }) - ) - - DependencyInjection.Container.shared.register(manager) + // let manager = try InitFileTransfer.live( + // e2eId: messenger.e2e()!.getId(), + // callback: .init(handle: { [weak self] in + // guard let self = self else { return } + // + // switch $0 { + // case .success(let receivedFile): + // self.handleIncomingTransfer(receivedFile, messenger: messenger) + // case .failure(let error): + // print(error.localizedDescription) + // } + // }) + // ) + // + // DependencyInjection.Container.shared.register(manager) } private func generateTrafficManager(_ messenger: Messenger) throws { @@ -728,8 +728,9 @@ extension LaunchViewModel { private func makeMessenger() -> Messenger { var environment: MessengerEnvironment = .live() environment.ndfEnvironment = .mainnet - environment.udAddress = "46.101.98.49:18001" - environment.udCert = """ + environment.udEnvironment = .init( + address: "46.101.98.49:18001", + cert: """ -----BEGIN CERTIFICATE----- MIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx @@ -751,10 +752,11 @@ extension LaunchViewModel { tgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5 6m52PyzMNV+2N21IPppKwA== -----END CERTIFICATE----- - """.data(using: .utf8)! - environment.udContact = """ + """.data(using: .utf8)!, + contact: """ <xxc(2)7mbKFLE201WzH4SGxAOpHjjehwztIV+KGifi5L/PYPcDkAZiB9kZo+Dl3Vc7dD2SdZCFMOJVgwqGzfYRDkjc8RGEllBqNxq2sRRX09iQVef0kJQUgJCHNCOcvm6Ki0JJwvjLceyFh36iwK8oLbhLgqEZY86UScdACTyBCzBIab3ob5mBthYc3mheV88yq5PGF2DQ+dEvueUm+QhOSfwzppAJA/rpW9Wq9xzYcQzaqc3ztAGYfm2BBAHS7HVmkCbvZ/K07Xrl4EBPGHJYq12tWAN/C3mcbbBYUOQXyEzbSl/mO7sL3ORr0B4FMuqCi8EdlD6RO52pVhY+Cg6roRH1t5Ng1JxPt8Mv1yyjbifPhZ5fLKwxBz8UiFORfk0/jnhwgm25LRHqtNRRUlYXLvhv0HhqyYTUt17WNtCLATSVbqLrFGdy2EGadn8mP+kQNHp93f27d/uHgBNNe7LpuYCJMdWpoG6bOqmHEftxt0/MIQA8fTtTm3jJzv+7/QjZJDvQIv0SNdp8HFogpuwde+GuS4BcY7v5xz+ArGWcRR63ct2z83MqQEn9ODr1/gAAAgA7szRpDDQIdFUQo9mkWg8xBA==xxc> - """.data(using: .utf8) + """.data(using: .utf8)! + ) return Messenger.live(environment) } @@ -777,7 +779,7 @@ extension LaunchViewModel { private func setupBackupCallback(_ messenger: Messenger) { backupCallbackCancellable = messenger.registerBackupCallback(.init(handle: { [weak self] in print(">>> Backup callback from bindings got called") - self?.backupService.updateBackup(data: $0) + self?.backupService.updateLocalBackup($0) })) } diff --git a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift index b7564509410a86b480489e08206abe80e188893d..40c7abb500cf46d2e7805470fac3a6ca95c33543 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift @@ -86,7 +86,7 @@ final class ProfileCodeViewModel { self.hudRelay.send(.none) self.completionRelay.send(self.confirmation) - self.backupService.performBackup() + self.backupService.didUpdateFacts() } catch { self.hudRelay.send(.error(.init(with: error))) } diff --git a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift index b1bddd42bbbe751e5194848fca749bb76d85106d..d700516ef7f2bf1df7f917cf91c306f518e607fd 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift @@ -108,7 +108,7 @@ final class ProfileViewModel { self.isPhoneSharing = false } - self.backupService.performBackup() + self.backupService.didUpdateFacts() self.hudRelay.send(.none) self.refresh() } catch { diff --git a/Sources/RestoreFeature/Utils/PlistSecrets.swift b/Sources/RestoreFeature/Utils/PlistSecrets.swift deleted file mode 100644 index a96d25f9e7a272043daa2ade32e9dea5626bfb08..0000000000000000000000000000000000000000 --- a/Sources/RestoreFeature/Utils/PlistSecrets.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -struct PlistSecrets { - struct GooglePlist: Decodable { - let apiKey: String - let clientId: String - - enum CodingKeys: String, CodingKey { - case apiKey = "API_KEY" - case clientId = "CLIENT_ID" - } - } - - struct InfoPlist: Decodable { - let dropboxAppKey: String - - enum CodingKeys: String, CodingKey { - case dropboxAppKey = "DROPBOX_APP_KEY" - } - } - - static var googleAPIKey: String { - guard let url = Bundle.main.url(forResource: "GoogleService-Info", withExtension: "plist"), - let data = try? Data(contentsOf: url), - let plist = try? PropertyListDecoder().decode(GooglePlist.self, from: data) else { - fatalError("Can't decode GoogleService-Info.plist") - } - return plist.apiKey - } - - static var googleClientId: String { - guard let url = Bundle.main.url(forResource: "GoogleService-Info", withExtension: "plist"), - let data = try? Data(contentsOf: url), - let plist = try? PropertyListDecoder().decode(GooglePlist.self, from: data) else { - fatalError("Can't decode GoogleService-Info.plist") - } - return plist.clientId - } - - static var dropboxAppKey: String { - guard let url = Bundle.main.url(forResource: "Info", withExtension: "plist"), - let data = try? Data(contentsOf: url), - let plist = try? PropertyListDecoder().decode(InfoPlist.self, from: data) else { - fatalError("Can't decode info.plist") - } - return plist.dropboxAppKey - } -} diff --git a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift index ec247ddcc0e005ca089e42343459fa73288f8eec..2b992439f004284dae958827025882ab0640c9b2 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift @@ -1,24 +1,12 @@ import HUD import UIKit import Combine - import CloudFiles import CloudFilesSFTP -import CloudFilesDrive -import CloudFilesICloud -import CloudFilesDropbox - import DependencyInjection -enum RestorationProvider: String, Equatable, Hashable { - case sftp - case drive - case icloud - case dropbox -} - public struct RestorationDetails { - var provider: RestorationProvider + var provider: CloudService var metadata: Fetch.Metadata? } @@ -35,27 +23,12 @@ final class RestoreListViewModel { detailsSubject.eraseToAnyPublisher() } - private var managers: [RestorationProvider: CloudFilesManager] = [ - .icloud: .iCloud( - fileName: "backup.xxm" - ), - .dropbox: .dropbox( - appKey: PlistSecrets.dropboxAppKey, - path: "/backup/backup.xxm" - ), - .drive: .drive( - apiKey: PlistSecrets.googleAPIKey, - clientId: PlistSecrets.googleClientId, - fileName: "backup.xxm" - ) - ] - private let sftpSubject = PassthroughSubject<Void, Never>() private let hudSubject = PassthroughSubject<HUDStatus, Never>() private let detailsSubject = PassthroughSubject<RestorationDetails, Never>() func setupSFTP(host: String, username: String, password: String) { - managers[.sftp] = .sftp( + CloudFilesManager.all[.sftp] = .sftp( host: host, username: username, password: password, @@ -65,7 +38,7 @@ final class RestoreListViewModel { } func link( - provider: RestorationProvider, + provider: CloudService, from controller: UIViewController, onSuccess: @escaping () -> Void ) { @@ -73,11 +46,8 @@ final class RestoreListViewModel { sftpSubject.send(()) return } - guard let manager = managers[provider] else { - return - } do { - try manager.link(controller) { [weak self] in + try CloudFilesManager.all[provider]!.link(controller) { [weak self] in guard let self else {return } switch $0 { @@ -92,24 +62,19 @@ final class RestoreListViewModel { } } - func fetch(provider: RestorationProvider) { - guard let manager = managers[provider] else { - return - } + func fetch(provider: CloudService) { hudSubject.send(.on) do { - try manager.fetch { [weak self] in + try CloudFilesManager.all[provider]!.fetch { [weak self] in guard let self else { return } switch $0 { case .success(let metadata): - DependencyInjection.Container.shared.register(manager) - + self.hudSubject.send(.none) self.detailsSubject.send(.init( provider: provider, metadata: metadata )) - self.hudSubject.send(.none) case .failure(let error): self.hudSubject.send(.error(.init(with: error))) } diff --git a/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift index a24baf97da13697938f17a59a26aa4dbce2f1a99..6c509cbbf1ee17116a5c5424615485ac10935b16 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreSFTPViewModel.swift @@ -1,6 +1,5 @@ -import UIKit - import HUD +import UIKit import Combine import Foundation import CloudFiles diff --git a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift index 390c756dcfbe161ea056c8fde95f962eda37dadc..98b68d83c36a974dfcea2ba1f447ba29a6df9f83 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift @@ -17,7 +17,7 @@ enum Step { case parsingData case failDownload(Error) case downloading(Float, Float) - case idle(RestorationProvider, CloudFiles.Fetch.Metadata?) + case idle(CloudService, CloudFiles.Fetch.Metadata?) } extension Step: Equatable { @@ -39,7 +39,6 @@ extension Step: Equatable { final class RestoreViewModel { @Dependency var database: Database @Dependency var messenger: Messenger - @Dependency var manager: CloudFilesManager @KeyObject(.phone, defaultValue: nil) var phone: String? @KeyObject(.email, defaultValue: nil) var email: String? @@ -77,7 +76,7 @@ final class RestoreViewModel { stepSubject.send(.downloading(0.0, metadata.size)) do { - try manager.download { [weak self] in + try CloudFilesManager.all[details.provider]!.download { [weak self] in guard let self else { return } switch $0 { diff --git a/Sources/RestoreFeature/Views/RestoreView.swift b/Sources/RestoreFeature/Views/RestoreView.swift index 5f1648ff53f89bec8ad233d748b932a27511a5e9..720128d33fa4bed8bd11e1ffed0873f9452682eb 100644 --- a/Sources/RestoreFeature/Views/RestoreView.swift +++ b/Sources/RestoreFeature/Views/RestoreView.swift @@ -1,5 +1,6 @@ import UIKit import Shared +import CloudFiles final class RestoreView: UIView { let titleLabel = UILabel() @@ -104,7 +105,7 @@ final class RestoreView: UIView { } private func displayDetailsFrom( - _ provider: RestorationProvider, + _ provider: CloudService, size: Float, lastDate: Date ) { @@ -132,7 +133,7 @@ final class RestoreView: UIView { progressView.isHidden = true } - private func missingMetadataFor(_ provider: RestorationProvider) { + private func missingMetadataFor(_ provider: CloudService) { titleLabel.text = Localized.AccountRestore.NotFound.title subtitleLabel.text = Localized.AccountRestore.NotFound.subtitle(provider.name()) @@ -144,7 +145,7 @@ final class RestoreView: UIView { } } -private extension RestorationProvider { +private extension CloudService { func name() -> String { switch self { case .drive: