diff --git a/App/client-ios.xcodeproj/project.pbxproj b/App/client-ios.xcodeproj/project.pbxproj index 76f38ed41ee371a895528771b590f85c69276b6c..0876fa9598cb2d915930270dfc19ba8e7e1c5ecb 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 = 48; + CURRENT_PROJECT_VERSION = 58; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -463,7 +463,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.7; + MARKETING_VERSION = 1.0.8; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.mock; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -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 = 48; + CURRENT_PROJECT_VERSION = 58; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; @@ -503,7 +503,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.7; + MARKETING_VERSION = 1.0.8; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -522,7 +522,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 48; + CURRENT_PROJECT_VERSION = 58; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -536,7 +536,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; + MARKETING_VERSION = 1.0.8; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.mock.notifications; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -553,7 +553,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 48; + CURRENT_PROJECT_VERSION = 58; DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -567,7 +567,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.7; + MARKETING_VERSION = 1.0.8; PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.notifications; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Package.swift b/Package.swift index ed7f12f2320f58eff87fb47cfcecc45d369c8d83..cf6ac5fc3323dffb1360e4b5863725f2d5751b31 100644 --- a/Package.swift +++ b/Package.swift @@ -657,6 +657,7 @@ let package = Package( "HUD", "Shared", "Models", + "InputField", "Presentation", "GoogleDriveFeature", "iCloudFeature", diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 59b18eb00646812bbd93ee249335a2b65bd2d6bf..aa07ecd90afcaa69874c49b578a1e43b247032b0 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -105,7 +105,9 @@ struct DependencyRegistrator { // MARK: Coordinators - container.register(BackupCoordinator() as BackupCoordinating) + container.register(BackupCoordinator( + passphraseFactory: BackupPassphraseController.init(_:_:) + ) as BackupCoordinating) container.register( SearchCoordinator( @@ -134,7 +136,8 @@ struct DependencyRegistrator { RestoreCoordinator( successFactory: RestoreSuccessController.init, chatListFactory: ChatListController.init, - restoreFactory: RestoreController.init(_:_:) + restoreFactory: RestoreController.init(_:_:), + passphraseFactory: RestorePassphraseController.init(_:) ) as RestoreCoordinating) container.register( diff --git a/Sources/BackupFeature/Controllers/BackupConfigController.swift b/Sources/BackupFeature/Controllers/BackupConfigController.swift index aa48d9e1db6874afb18d2dcb0d29519497c3e1e8..04b1c56b48c0363d899fd93e316b0e97af5bdd74 100644 --- a/Sources/BackupFeature/Controllers/BackupConfigController.swift +++ b/Sources/BackupFeature/Controllers/BackupConfigController.swift @@ -97,17 +97,17 @@ final class BackupConfigController: UIViewController { screenView.googleDriveButton.switcherView .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(.drive, screenView.googleDriveButton.switcherView.isOn) } + .sink { [unowned self] in viewModel.didToggleService(self, .drive, screenView.googleDriveButton.switcherView.isOn) } .store(in: &cancellables) screenView.dropboxButton.switcherView .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(.dropbox, screenView.dropboxButton.switcherView.isOn) } + .sink { [unowned self] in viewModel.didToggleService(self, .dropbox, screenView.dropboxButton.switcherView.isOn) } .store(in: &cancellables) screenView.iCloudButton.switcherView .publisher(for: .valueChanged) - .sink { [unowned self] in viewModel.didToggleService(.icloud, screenView.iCloudButton.switcherView.isOn) } + .sink { [unowned self] in viewModel.didToggleService(self, .icloud, screenView.iCloudButton.switcherView.isOn) } .store(in: &cancellables) screenView.dropboxButton diff --git a/Sources/BackupFeature/Controllers/BackupController.swift b/Sources/BackupFeature/Controllers/BackupController.swift index 65ef336de786ffb0b62789f21c0059995d643591..a01ff0caa91be5ffdfa55e6fe9fe8ea7ebab41bf 100644 --- a/Sources/BackupFeature/Controllers/BackupController.swift +++ b/Sources/BackupFeature/Controllers/BackupController.swift @@ -14,7 +14,7 @@ public final class BackupController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = Asset.neutralWhite.color - hud.update(with: .on) + hud.update(with: .on(nil)) setupNavigationBar() setupBindings() diff --git a/Sources/BackupFeature/Controllers/BackupPassphraseController.swift b/Sources/BackupFeature/Controllers/BackupPassphraseController.swift new file mode 100644 index 0000000000000000000000000000000000000000..b5bd426b6f9242c42d507f4dd5feedd8abccdf17 --- /dev/null +++ b/Sources/BackupFeature/Controllers/BackupPassphraseController.swift @@ -0,0 +1,100 @@ +import UIKit +import Shared +import Combine +import InputField +import ScrollViewController + +public final class BackupPassphraseController: UIViewController { + lazy private var screenView = BackupPassphraseView() + + private var passphrase = "" { + didSet { + switch Validator.backupPassphrase.validate(passphrase) { + case .success: + screenView.continueButton.isEnabled = true + case .failure: + screenView.continueButton.isEnabled = false + } + } + } + + private let cancelClosure: EmptyClosure + private let stringClosure: StringClosure + private var cancellables = Set<AnyCancellable>() + private let keyboardListener = KeyboardFrameChangeListener(notificationCenter: .default) + + public init( + _ cancelClosure: @escaping EmptyClosure, + _ stringClosure: @escaping StringClosure + ) { + self.stringClosure = stringClosure + self.cancelClosure = cancelClosure + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + let view = UIView() + view.addSubview(screenView) + + screenView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.right.equalToSuperview() + make.bottom.equalToSuperview().offset(0) + } + + self.view = view + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupKeyboard() + setupBindings() + + screenView.continueButton.isEnabled = false + } + + private func setupKeyboard() { + keyboardListener.keyboardFrameWillChange = { [weak self] keyboard in + guard let self = self else { return } + + let inset = self.view.frame.height - self.view.convert(keyboard.frame, from: nil).minY + + self.screenView.snp.updateConstraints { + $0.bottom.equalToSuperview().offset(-inset) + } + + self.view.setNeedsLayout() + + UIView.animate(withDuration: keyboard.animationDuration) { + self.view.layoutIfNeeded() + } + } + } + + private func setupBindings() { + screenView.inputField.returnPublisher + .sink { [unowned self] in screenView.inputField.endEditing(true) } + .store(in: &cancellables) + + screenView.cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in dismiss(animated: true) { self.cancelClosure() }} + .store(in: &cancellables) + + screenView.inputField + .textPublisher + .sink { [unowned self] in passphrase = $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .store(in: &cancellables) + + screenView.continueButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { + self.stringClosure(self.passphrase) + } + }.store(in: &cancellables) + } +} diff --git a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift b/Sources/BackupFeature/Coordinator/BackupCoordinator.swift index 75181b7ce104d68e8f8b159a2b0069a725622cc6..14598be27d56881b5ac2b7d44960b08da4359d38 100644 --- a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift +++ b/Sources/BackupFeature/Coordinator/BackupCoordinator.swift @@ -1,18 +1,52 @@ import UIKit +import Shared import Presentation public protocol BackupCoordinating { - func toPopup(_: UIViewController, from: UIViewController) + func toPopup( + _: UIViewController, + from: UIViewController + ) + + func toPassphrase( + from: UIViewController, + cancelClosure: @escaping EmptyClosure, + passphraseClosure: @escaping StringClosure + ) } public struct BackupCoordinator: BackupCoordinating { var bottomPresenter: Presenting = BottomPresenter() - public init() {} + var passphraseFactory: ( + @escaping EmptyClosure, + @escaping StringClosure + ) -> UIViewController + + public init( + passphraseFactory: @escaping ( + @escaping EmptyClosure, + @escaping StringClosure + ) -> UIViewController + ) { + self.passphraseFactory = passphraseFactory + } } public extension BackupCoordinator { - func toPopup(_ screen: UIViewController, from parent: UIViewController) { + func toPopup( + _ screen: UIViewController, + from parent: UIViewController + ) { + bottomPresenter.present(screen, from: parent) + } + + func toPassphrase( + from parent: UIViewController, + cancelClosure: @escaping EmptyClosure, + passphraseClosure: @escaping StringClosure + ) { + let screen = passphraseFactory(cancelClosure, passphraseClosure) bottomPresenter.present(screen, from: parent) } } diff --git a/Sources/BackupFeature/Service/BackupService.swift b/Sources/BackupFeature/Service/BackupService.swift index 4c62ba50d557ff4271ccf29b2786df73f17206c8..8b82a8f27c40af85acd836182144d05c945008e1 100644 --- a/Sources/BackupFeature/Service/BackupService.swift +++ b/Sources/BackupFeature/Service/BackupService.swift @@ -16,6 +16,8 @@ public final class BackupService { @KeyObject(.backupSettings, defaultValue: Data()) private var storedSettings: Data + public var passphrase: String? + public var settingsPublisher: AnyPublisher<BackupSettings, Never> { settings.handleEvents(receiveSubscription: { [weak self] _ in guard let self = self else { return } diff --git a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift index c11b31a8183899a257d755270d9628c39c92d987..f136ca8c15c83578636ed3ef00643817d73b8de9 100644 --- a/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift +++ b/Sources/BackupFeature/ViewModels/BackupConfigViewModel.swift @@ -16,7 +16,7 @@ struct BackupConfigViewModel { var didTapBackupNow: () -> Void var didChooseWifiOnly: (Bool) -> Void var didChooseAutomatic: (Bool) -> Void - var didToggleService: (CloudService, Bool) -> Void + var didToggleService: (UIViewController, CloudService, Bool) -> Void var didTapService: (CloudService, UIViewController) -> Void var wifiOnly: () -> AnyPublisher<Bool, Never> @@ -32,6 +32,7 @@ extension BackupConfigViewModel { class Context { @Dependency var hud: HUDType @Dependency var service: BackupService + @Dependency var coordinator: BackupCoordinating } let context = Context() @@ -39,14 +40,33 @@ extension BackupConfigViewModel { return .init( didTapBackupNow: { context.service.performBackup() - context.hud.update(with: .on) + context.hud.update(with: .on(nil)) DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { context.hud.update(with: .none) } }, didChooseWifiOnly: context.service.setBackupOnlyOnWifi(_:), didChooseAutomatic: context.service.setBackupAutomatically(_:), - didToggleService: context.service.toggle, + didToggleService: { controller, service, enabling in + guard enabling == true else { + context.service.toggle(service: service, enabling: enabling) + return + } + + context.coordinator.toPassphrase(from: controller, cancelClosure: { + context.service.toggle(service: service, enabling: false) + }, passphraseClosure: { passphrase in + context.service.passphrase = passphrase + context.hud.update(with: .on("Initializing and securing your backup file will take few seconds, please keep the app open.")) + DispatchQueue.global().async { + context.service.toggle(service: service, enabling: enabling) + + DispatchQueue.main.async { + context.hud.update(with: .none) + } + } + }) + }, didTapService: context.service.authorize, wifiOnly: { context.service.settingsPublisher diff --git a/Sources/BackupFeature/Views/BackupPassphraseView.swift b/Sources/BackupFeature/Views/BackupPassphraseView.swift new file mode 100644 index 0000000000000000000000000000000000000000..b23c5dcdb5195a7d0f5ff3ac10757ac143ae683f --- /dev/null +++ b/Sources/BackupFeature/Views/BackupPassphraseView.swift @@ -0,0 +1,67 @@ +import UIKit +import Shared +import InputField + +final class BackupPassphraseView: UIView { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let inputField = InputField() + let stackView = UIStackView() + let continueButton = CapsuleButton() + let cancelButton = CapsuleButton() + + init() { + super.init(frame: .zero) + setup() + } + + required init?(coder: NSCoder) { nil } + + private func setup() { + layer.cornerRadius = 40 + backgroundColor = Asset.neutralWhite.color + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + subtitleLabel.numberOfLines = 0 + titleLabel.textColor = Asset.neutralActive.color + subtitleLabel.textColor = Asset.neutralActive.color + + inputField.setup( + style: .regular, + title: "Passphrase", + placeholder: "* * * * * *", + subtitleColor: Asset.neutralDisabled.color + ) + + titleLabel.text = "Secure your backup" + titleLabel.textAlignment = .left + titleLabel.font = Fonts.Mulish.bold.font(size: 26.0) + + subtitleLabel.text = "Please select a password for your backup. If you lose this password, you will not be able to restore your account. Make sure to keep a record somewhere safe. Your password needs to be at least 8 characters with at least 1 uppercase, 1 lowercase and 1 number characters" + subtitleLabel.textAlignment = .left + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + continueButton.setStyle(.brandColored) + continueButton.setTitle("Set password and continue", for: .normal) + + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle("Cancel", for: .normal) + + stackView.spacing = 20 + stackView.axis = .vertical + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(subtitleLabel) + stackView.addArrangedSubview(inputField) + stackView.addArrangedSubview(continueButton) + stackView.addArrangedSubview(cancelButton) + + addSubview(stackView) + + stackView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(60) + make.left.equalToSuperview().offset(50) + make.right.equalToSuperview().offset(-50) + make.bottom.equalToSuperview().offset(-70) + } + } +} diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index bb926f2bed4317841f961acf0495c6593d0905d8..3ec0712e0b1a481a9f82dadfc7102e1e702e5cc0 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -94,7 +94,7 @@ final class SingleChatViewModel { func didSend(image: UIImage) { guard let imageData = image.orientedUp().jpegData(compressionQuality: 1.0) else { return } - hudRelay.send(.on) + hudRelay.send(.on(nil)) session.send(imageData: imageData, to: contact) { [weak self] in switch $0 { diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift index f2f0ad01c3cac4c502624cc3d6cabf4ad11d078c..2b704f532b05b78f52fa39a05d90a6c80c5e91a6 100644 --- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift +++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift @@ -204,7 +204,7 @@ final class ChatListViewModel: ChatListViewModelType { } do { - hudRelay.send(.on) + hudRelay.send(.on(nil)) try session.leave(group: group) hudRelay.send(.none) } catch { diff --git a/Sources/ContactFeature/ViewModels/ContactViewModel.swift b/Sources/ContactFeature/ViewModels/ContactViewModel.swift index 8d185fcfe2cef238d7a6ee66449ecbe665b258e1..fd456e3f46d14f1f352a364ab35b1c4295e88635 100644 --- a/Sources/ContactFeature/ViewModels/ContactViewModel.swift +++ b/Sources/ContactFeature/ViewModels/ContactViewModel.swift @@ -61,7 +61,7 @@ final class ContactViewModel { } func didTapDelete() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) do { try session.deleteContact(contact) @@ -90,7 +90,7 @@ final class ContactViewModel { } func didTapResend() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } @@ -106,7 +106,7 @@ final class ContactViewModel { } func didTapRequest(with nickname: String) { - hudRelay.send(.on) + hudRelay.send(.on(nil)) contact.nickname = nickname backgroundScheduler.schedule { [weak self] in @@ -123,7 +123,7 @@ final class ContactViewModel { } func didTapAccept(_ nickname: String) { - hudRelay.send(.on) + hudRelay.send(.on(nil)) contact.nickname = nickname backgroundScheduler.schedule { [weak self] in diff --git a/Sources/ContactFeature/Views/NickameView.swift b/Sources/ContactFeature/Views/NickameView.swift index 25e881a5d6b5efead25d1654f7d19f8d07e0bc3e..e6eaaf46bea3db16667d3f8cbdc6652113f626c9 100644 --- a/Sources/ContactFeature/Views/NickameView.swift +++ b/Sources/ContactFeature/Views/NickameView.swift @@ -3,16 +3,12 @@ import Shared import InputField final class NickameView: UIView { - // MARK: UI - let title = UILabel() let icon = UIImageView() let input = InputField() let stack = UIStackView() let save = CapsuleButton() - // MARK: Lifecycle - init() { super.init(frame: .zero) setup() @@ -31,8 +27,6 @@ final class NickameView: UIView { } } - // MARK: Private - private func setup() { layer.cornerRadius = 40 backgroundColor = Asset.neutralWhite.color diff --git a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift index 34b3203fc5922e5e981c9ab4d6235355927aeaf6..403e8361b163faa018d16e2702f349428df38838 100644 --- a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift @@ -69,7 +69,7 @@ final class CreateGroupViewModel { } func create(name: String, welcome: String?, members: [Contact]) { - hudRelay.send(.on) + hudRelay.send(.on(nil)) session.createGroup(name: name, welcome: welcome, members: members) { [weak self] in guard let self = self else { return } diff --git a/Sources/HUD/DotAnimation.swift b/Sources/HUD/DotAnimation.swift index ed889c3f8aae0d5204df78f4ad1cc7fca756d3b3..f7bfae046b167ba1a0974723b10c359189ec5de0 100644 --- a/Sources/HUD/DotAnimation.swift +++ b/Sources/HUD/DotAnimation.swift @@ -1,14 +1,10 @@ import UIKit final class DotAnimation: UIView { - // MARK: UI - let leftDot = UIView() let middleDot = UIView() let rightDot = UIView() - // MARK: Properties - var leftInvert = false var middleInvert = false var rightInvert = false @@ -19,8 +15,6 @@ final class DotAnimation: UIView { var displayLink: CADisplayLink? - // MARK: Lifecycle - init() { super.init(frame: .zero) setup() @@ -28,8 +22,6 @@ final class DotAnimation: UIView { required init?(coder: NSCoder) { nil } - // MARK: Public - func setColor( _ color: UIColor = UIColor( red: 0, @@ -43,8 +35,6 @@ final class DotAnimation: UIView { rightDot.backgroundColor = color } - // MARK: Private - private func setup() { setupCornerRadius() setColor() @@ -86,8 +76,6 @@ final class DotAnimation: UIView { } } - // MARK: Selectors - @objc private func handleAnimations() { let factor: CGFloat = 70 diff --git a/Sources/HUD/ErrorView.swift b/Sources/HUD/ErrorView.swift index 10147b88eb23ebc63415ce77257f8cd36f304017..2692ab2a53274cbb2e3d63303ad830885f4a8116 100644 --- a/Sources/HUD/ErrorView.swift +++ b/Sources/HUD/ErrorView.swift @@ -3,24 +3,17 @@ import Shared import SnapKit final class ErrorView: UIView { - // MARK: UI - let title = UILabel() let content = UILabel() let stack = UIStackView() let button = CapsuleButton() - // MARK: Lifecycle - init(with model: HUDError) { super.init(frame: .zero) setup(with: model) } required init?(coder: NSCoder) { nil } - - - // MARK: Private private func setup(with model: HUDError) { layer.cornerRadius = 6 diff --git a/Sources/HUD/HUD.swift b/Sources/HUD/HUD.swift index 72e7190bc0ef5303de62a7a7480ba27fe12dc2da..9f45b18078d75a2d11654f99a9bd2d1b2746f4ee 100644 --- a/Sources/HUD/HUD.swift +++ b/Sources/HUD/HUD.swift @@ -10,7 +10,7 @@ private enum Constants { } public enum HUDStatus: Equatable { - case on + case on(String?) case none case error(HUDError) @@ -55,14 +55,10 @@ public protocol HUDType { } public final class HUD: HUDType { - // MARK: UI - private(set) var window: UIWindow? private(set) var errorView: ErrorView? + private(set) var titleLabel: UILabel? private(set) var animation: DotAnimation? - - // MARK: Properties - private var cancellables = Set<AnyCancellable>() private var status: HUDStatus = .none { @@ -71,10 +67,16 @@ public final class HUD: HUDType { self.errorView = nil self.animation = nil self.window = nil + self.titleLabel = nil switch status { - case .on: + case .on(let text): animation = DotAnimation() + + if let text = text { + titleLabel = UILabel() + titleLabel!.text = text + } case .error(let error): errorView = ErrorView(with: error) case .none: @@ -86,8 +88,13 @@ public final class HUD: HUDType { if oldValue.isPresented == false && status.isPresented == true { switch status { - case .on: + case .on(let text): animation = DotAnimation() + + if let text = text { + titleLabel = UILabel() + titleLabel!.text = text + } case .error(let error): errorView = ErrorView(with: error) case .none: @@ -103,18 +110,12 @@ public final class HUD: HUDType { } } - // MARK: Lifecycle - public init() {} - // MARK: Public - public func update(with status: HUDStatus) { self.status = status } - // MARK: Private - private func showWindow() { window = Window() window?.backgroundColor = UIColor.black.withAlphaComponent(0.5) @@ -126,6 +127,17 @@ public final class HUD: HUDType { animation.snp.makeConstraints { $0.center.equalToSuperview() } } + if let titleLabel = titleLabel { + window?.addSubview(titleLabel) + titleLabel.textAlignment = .center + titleLabel.numberOfLines = 0 + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(18) + make.center.equalToSuperview().offset(50) + make.right.equalToSuperview().offset(-18) + } + } + if let errorView = errorView { window?.addSubview(errorView) errorView.snp.makeConstraints { make in @@ -154,6 +166,7 @@ public final class HUD: HUDType { self.cancellables.removeAll() self.errorView = nil self.animation = nil + self.titleLabel = nil self.window = nil } } diff --git a/Sources/InputField/Validator.swift b/Sources/InputField/Validator.swift index ef4a5e2f531a9775c8507f038aab61c6bc4802ee..0a9f30779bfa6115b91f65ade241a9c021495442 100644 --- a/Sources/InputField/Validator.swift +++ b/Sources/InputField/Validator.swift @@ -52,6 +52,22 @@ public extension Validator where T == (String, String) { } public extension Validator where T == String { + static var backupPassphrase: Self { + Validator { passphrase -> ValidationResult in + guard passphrase.trimmingCharacters(in: .whitespacesAndNewlines).count >= 8 else { + return .failure("") + } + + let regex = try? NSRegularExpression(pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$") + + guard let regex = regex, regex.firstMatch(in: passphrase, options: [], range: passphrase.fullRange()) != nil else { + return .failure("") + } + + return .success(nil) + } + } + static var username: Self { Validator { username -> ValidationResult in guard username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false else { @@ -62,8 +78,8 @@ public extension Validator where T == String { let last = username.last, (first.isLetter || first.isNumber), (last.isLetter || last.isNumber) else { - return .failure(Constants.usernameStartEndInvalid) - } + return .failure(Constants.usernameStartEndInvalid) + } guard username.trimmingCharacters(in: .whitespacesAndNewlines).count >= 4 else { return .failure(Constants.usernameMinimum) diff --git a/Sources/Integration/Client.swift b/Sources/Integration/Client.swift index 690f6132f7ed344d74dcde0014778fc464f71dce..51bdf9d3365534ad96e669e99cb7230111d3a6dd 100644 --- a/Sources/Integration/Client.swift +++ b/Sources/Integration/Client.swift @@ -70,13 +70,25 @@ public class Client { } } - public func listenBackup() { + public func initializeBackup(passphrase: String) { backupManager = nil - backupManager = bindings.listenBackups { [weak backupSubject] in + backupManager = bindings.initializeBackup(passphrase: passphrase) { [weak backupSubject] in backupSubject?.send($0) } } + public func resumeBackup() { + backupManager = nil + backupManager = bindings.resumeBackup { [weak backupSubject] in + backupSubject?.send($0) + } + } + + // public func isBackupRunning() -> Bool { + // guard let backupManager = backupManager else { return false } + // return backupManager.isBackupRunning() + // } + public func addJson(_ string: String) { guard let backupManager = backupManager else { return } backupManager.addJson(string) diff --git a/Sources/Integration/Interfaces/BindingsInterface.swift b/Sources/Integration/Interfaces/BindingsInterface.swift index e9d913af958664feb10e0746f0687dcf17dd4064..ed5b307775eac621c5dfe9395d827e5e8f9a9766 100644 --- a/Sources/Integration/Interfaces/BindingsInterface.swift +++ b/Sources/Integration/Interfaces/BindingsInterface.swift @@ -28,6 +28,7 @@ public protocol E2ESendReportType { public protocol BackupInterface { func stop() throws func addJson(_: String?) + func isBackupRunning() -> Bool } public protocol RestoreReportType { @@ -137,7 +138,14 @@ public protocol BindingsInterface { func listenMessages(_: @escaping (Message) -> Void) throws - func listenBackups(_: @escaping (Data) -> Void) -> BackupInterface + func initializeBackup( + passphrase: String, + callback: @escaping (Data) -> Void + ) -> BackupInterface + + func resumeBackup( + callback: @escaping (Data) -> Void + ) -> BackupInterface func listenRequests( _ requests: @escaping (Contact) -> Void, diff --git a/Sources/Integration/Listeners.swift b/Sources/Integration/Listeners.swift index cd81bc446f79b0d1e5b0339cae546f9191b58467..3372cb26b7d27d06c51582ccfb6fd514286d316e 100644 --- a/Sources/Integration/Listeners.swift +++ b/Sources/Integration/Listeners.swift @@ -2,7 +2,7 @@ import Models import Shared import Bindings import Foundation - +import os.log import Combine public extension BindingsClient { @@ -22,9 +22,17 @@ public extension BindingsClient { registerPreimageCallback(receptionId, pin: callback) } - func listenBackups(_ callback: @escaping (Data) -> Void) -> BackupInterface { + func initializeBackup(passphrase: String, callback: @escaping (Data) -> Void) -> BackupInterface { + var error: NSError? + os_signpost(.begin, log: logHandler, name: "Encrypting", "Calling BindingsInitializeBackup") + let backup = BindingsInitializeBackup(passphrase, UpdateBackupCallback(callback), self, &error) + os_signpost(.end, log: logHandler, name: "Encrypting", "Finished BindingsInitializeBackup") + return backup! + } + + func resumeBackup(callback: @escaping (Data) -> Void) -> BackupInterface { var error: NSError? - let backup = BindingsInitializeBackup("", UpdateBackupCallback(callback), self, &error) + let backup = BindingsResumeBackup(UpdateBackupCallback(callback), self, &error) return backup! } diff --git a/Sources/Integration/Mocks/BindingsMock.swift b/Sources/Integration/Mocks/BindingsMock.swift index 9d2c8acc3a34b210567bb90cdc9dcbb797d8f14d..3ab8c5e3405500028f1f996200c6133fc8cdcc87 100644 --- a/Sources/Integration/Mocks/BindingsMock.swift +++ b/Sources/Integration/Mocks/BindingsMock.swift @@ -84,7 +84,14 @@ public final class BindingsMock: BindingsInterface { public func listenMessages(_: @escaping (Message) -> Void) throws {} - public func listenBackups(_: @escaping (Data) -> Void) -> BackupInterface { fatalError() } + public func initializeBackup( + passphrase: String, + callback: @escaping (Data) -> Void + ) -> BackupInterface { fatalError() } + + public func resumeBackup( + callback: @escaping (Data) -> Void + ) -> BackupInterface { fatalError() } public func listenNetworkUpdates(_: @escaping (Bool) -> Void) {} diff --git a/Sources/Integration/Session/Session.swift b/Sources/Integration/Session/Session.swift index 5cdb481b0040f90c52210d27e92bf8ffb7e66d4a..b674246c17f7bfed38a3e3173a1583ef9e7f7c8a 100644 --- a/Sources/Integration/Session/Session.swift +++ b/Sources/Integration/Session/Session.swift @@ -9,6 +9,10 @@ import BackupFeature import NetworkMonitor import DependencyInjection +import os.log + +let logHandler = OSLog(subsystem: "xx.network", category: "Performance debugging") + struct BackupParameters: Codable { var email: String? var phone: String? @@ -115,9 +119,17 @@ public final class Session: SessionType { .eraseToAnyPublisher() } - public init(backupFile: Data, ndf: String) throws { + public init( + passphrase: String, + backupFile: Data, + ndf: String + ) throws { let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking - let (client, backupData) = try network.newClientFromBackup(data: backupFile, ndf: ndf) + + os_signpost(.begin, log: logHandler, name: "Decrypting", "Calling newClientFromBackup") + let (client, backupData) = try network.newClientFromBackup(passphrase: passphrase, data: backupFile, ndf: ndf) + os_signpost(.end, log: logHandler, name: "Decrypting", "Finished newClientFromBackup") + self.client = client dbManager = GRDBDatabaseManager() @@ -319,9 +331,17 @@ public final class Session: SessionType { .removeDuplicates() .sink { [unowned self] in if $0 == true { - client.listenBackup() + guard let passphrase = backupService.passphrase else { + client.resumeBackup() + updateFactsOnBackup() + return + } + + client.initializeBackup(passphrase: passphrase) + backupService.passphrase = nil updateFactsOnBackup() } else { + backupService.passphrase = nil client.stopListeningBackup() } } diff --git a/Sources/Integration/XXNetwork.swift b/Sources/Integration/XXNetwork.swift index 4d6ee69040d61cf04d42f39924fc6d1182960276..5761631d28d8b39cb22aafa00f89dbb53f946f2e 100644 --- a/Sources/Integration/XXNetwork.swift +++ b/Sources/Integration/XXNetwork.swift @@ -15,9 +15,23 @@ public protocol XXNetworking { func purgeFiles() func updateErrors() func newClient(ndf: String) throws -> Client - func updateNDF(_: @escaping (Result<String, Error>) -> Void) - func newClientFromBackup(data: Data, ndf: String) throws -> (Client, Data?) - func loadClient(with: Data, fromBackup: Bool, email: String?, phone: String?) throws -> Client + + func updateNDF( + _: @escaping (Result<String, Error>) -> Void + ) + + func loadClient( + with: Data, + fromBackup: Bool, + email: String?, + phone: String? + ) throws -> Client + + func newClientFromBackup( + passphrase: String, + data: Data, + ndf: String + ) throws -> (Client, Data?) } public struct XXNetwork<B: BindingsInterface> { @@ -61,13 +75,26 @@ extension XXNetwork: XXNetworking { FileManager.xxCleanup() } - public func newClientFromBackup(data: Data, ndf: String) throws -> (Client, Data?) { + public func newClientFromBackup( + passphrase: String, + data: Data, + ndf: String + ) throws -> (Client, Data?) { + var error: NSError? let password = B.secret(32)! try keychain.store(password: password) - let backupData = B.fromBackup(ndf, FileManager.xxPath, password, nil, data, &error) + let backupData = B.fromBackup( + ndf, + FileManager.xxPath, + password, + "\(passphrase)".data(using: .utf8), + data, + &error + ) + if let error = error { throw error } var email: String? diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift index b3871d6234517f1d493ab61da1466bc754446fd7..1263fc26efb0f01d50f00a25e8b4b27e3b821b81 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailConfirmationViewModel.swift @@ -58,7 +58,7 @@ final class OnboardingEmailConfirmationViewModel { } func didTapNext() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift index c3cbbb897840964e15f908b9fa83d5d6537f7b23..618a903459267835ea47210d17ab0e5f763ac68e 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingEmailViewModel.swift @@ -38,7 +38,7 @@ final class OnboardingEmailViewModel { } func didTapNext() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift index 9992881ee768ac0010f57b0f506ff62fbc1f48e1..589977fbea9101172f02371f907786e0f9947786 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift @@ -51,7 +51,7 @@ final class OnboardingLaunchViewModel { private var cancellables = Set<AnyCancellable>() func didFinishSplash() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) versioning() .sink { [unowned self] in @@ -89,7 +89,7 @@ final class OnboardingLaunchViewModel { } func versionApproved() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) network.writeLogs() network.updateNDF { [weak self] in diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift index 2bd5a7ae35fbca7bf871479f998abeef9e4ce625..0af84d605d2c56a938e474d27fd79ad324943ee3 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneConfirmationViewModel.swift @@ -58,7 +58,7 @@ final class OnboardingPhoneConfirmationViewModel { } func didTapNext() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift index 0aff02f402420b959d03f166ba4eb456d1098bea..4698673d45cbe3b05f73fb3b1943f598f1c9655e 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingPhoneViewModel.swift @@ -47,7 +47,7 @@ final class OnboardingPhoneViewModel { } func didTapNext() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift index ecb64035e73e259bf147ce4ee7f427723d40b3f3..8766aac8e7629e4bdb9079cd8a3774aa2d1989a2 100644 --- a/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift +++ b/Sources/OnboardingFeature/ViewModels/OnboardingUsernameViewModel.swift @@ -42,7 +42,7 @@ final class OnboardingUsernameViewModel { } func didTapRegister() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift index d9763e989e23e37c9736582a41fcf1b63aef03b2..0cc8f60cfaea6921d99a90ccbba74a0cdfc322c2 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileCodeViewModel.swift @@ -57,7 +57,7 @@ final class ProfileCodeViewModel { } func didTapNext() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift index 6b57bc81fe4040faaccaeefff057a275a90deaac..df2bb28cdaa889462d1db1eb506cb14129567312 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileEmailViewModel.swift @@ -40,7 +40,7 @@ final class ProfileEmailViewModel { } func didTapNext() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift index 1013725b419a118c4e437d3e8959c50748447d72..9bf5da6e11788124e2bba6b04ba05f2f64b7f033 100644 --- a/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfilePhoneViewModel.swift @@ -47,7 +47,7 @@ final class ProfilePhoneViewModel { } func didTapNext() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift index a7066eccfaada6e74960beb7a5b3b8b7e72548cb..8f03379f02adde54606a70e4bd7c76f3e1fea87d 100644 --- a/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift +++ b/Sources/ProfileFeature/ViewModels/ProfileViewModel.swift @@ -83,7 +83,7 @@ final class ProfileViewModel { } func didTapDelete(isEmail: Bool) { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift index 2a415f083602ae9f567290133a49fbe25f0a2ef3..f86a34ad238b2715cb74acd454e67cdee7704ffd 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift @@ -38,7 +38,7 @@ final class RequestsFailedViewModel { // MARK: Public func didTapRetry(_ contact: Contact) { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift index 663649493c752d6797dcb998087d3d3212aaccb9..43616b7dcb62ae636154defcf9125a3661d6a52f 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift @@ -44,7 +44,7 @@ final class RequestsReceivedViewModel { // MARK: Public func didAccept(_ group: Group) { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } @@ -59,7 +59,7 @@ final class RequestsReceivedViewModel { } func didAccept(_ contact: Contact) { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift index 690056d65066ee9fae79064c3e0ad8111acc676c..0ab05705e83d77a177c5f13a0fff821ef0bd6c71 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift @@ -37,7 +37,7 @@ final class RequestsSentViewModel { } func didTapResend(_ contact: Contact) { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/RestoreFeature/Controllers/RestoreController.swift b/Sources/RestoreFeature/Controllers/RestoreController.swift index 1d87c70e5735f920d1b5b01f5c7aadfa17fb04eb..e51c2273383f7e49a08b1be4c7ba3f371501e5e1 100644 --- a/Sources/RestoreFeature/Controllers/RestoreController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreController.swift @@ -55,6 +55,14 @@ public final class RestoreController: UIViewController { .sink { [unowned self] in screenView.updateFor(step: $0) + if $0 == .wrongPass { + coordinator.toPassphrase(from: self) { pass in + self.viewModel.retryWith(passphrase: pass) + } + + return + } + if $0 == .done { coordinator.toSuccess(from: self) } @@ -72,8 +80,11 @@ public final class RestoreController: UIViewController { screenView.restoreButton .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapRestore() } - .store(in: &cancellables) + .sink { [unowned self] in + coordinator.toPassphrase(from: self) { passphrase in + self.viewModel.didTapRestore(passphrase: passphrase) + } + }.store(in: &cancellables) } @objc private func didTapBack() { diff --git a/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift b/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift new file mode 100644 index 0000000000000000000000000000000000000000..4d19e356729508ea5b688bb8435e09c602b5c8a6 --- /dev/null +++ b/Sources/RestoreFeature/Controllers/RestorePassphraseController.swift @@ -0,0 +1,93 @@ +import UIKit +import Shared +import Combine +import InputField +import ScrollViewController + +public final class RestorePassphraseController: UIViewController { + lazy private var screenView = RestorePassphraseView() + + private var passphrase = "" { + didSet { + switch Validator.backupPassphrase.validate(passphrase) { + case .success: + screenView.continueButton.isEnabled = true + case .failure: + screenView.continueButton.isEnabled = false + } + } + } + + private let completion: StringClosure + private var cancellables = Set<AnyCancellable>() + private let keyboardListener = KeyboardFrameChangeListener(notificationCenter: .default) + + public init(_ completion: @escaping StringClosure) { + self.completion = completion + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + let view = UIView() + view.addSubview(screenView) + + screenView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview() + make.right.equalToSuperview() + make.bottom.equalToSuperview().offset(0) + } + + self.view = view + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupKeyboard() + setupBindings() + + screenView.continueButton.isEnabled = false + } + + private func setupKeyboard() { + keyboardListener.keyboardFrameWillChange = { [weak self] keyboard in + guard let self = self else { return } + + let inset = self.view.frame.height - self.view.convert(keyboard.frame, from: nil).minY + + self.screenView.snp.updateConstraints { + $0.bottom.equalToSuperview().offset(-inset) + } + + self.view.setNeedsLayout() + + UIView.animate(withDuration: keyboard.animationDuration) { + self.view.layoutIfNeeded() + } + } + } + + private func setupBindings() { + screenView.inputField.returnPublisher + .sink { [unowned self] in screenView.inputField.endEditing(true) } + .store(in: &cancellables) + + screenView.cancelButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in dismiss(animated: true) } + .store(in: &cancellables) + + screenView.inputField + .textPublisher + .sink { [unowned self] in passphrase = $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .store(in: &cancellables) + + screenView.continueButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true, completion: { self.completion(self.passphrase) }) + }.store(in: &cancellables) + } +} diff --git a/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift b/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift index 2ade73355fa9b2b3de55cac81fa3b11b9b37621b..6876e8ceff5bbc7268bc13c71561f1664575fb66 100644 --- a/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift +++ b/Sources/RestoreFeature/Coordinator/RestoreCoordinator.swift @@ -1,11 +1,13 @@ import UIKit import Models +import Shared import Presentation public protocol RestoreCoordinating { func toChats(from: UIViewController) func toSuccess(from: UIViewController) func toPopup(_: UIViewController, from: UIViewController) + func toPassphrase(from: UIViewController, _: @escaping StringClosure) func toRestore(using: String, with: RestoreSettings, from: UIViewController) } @@ -17,15 +19,18 @@ public struct RestoreCoordinator: RestoreCoordinating { var successFactory: () -> UIViewController var chatListFactory: () -> UIViewController var restoreFactory: (String, RestoreSettings) -> UIViewController + var passphraseFactory: (@escaping StringClosure) -> UIViewController public init( successFactory: @escaping () -> UIViewController, chatListFactory: @escaping () -> UIViewController, - restoreFactory: @escaping (String, RestoreSettings) -> UIViewController + restoreFactory: @escaping (String, RestoreSettings) -> UIViewController, + passphraseFactory: @escaping (@escaping StringClosure) -> UIViewController ) { self.successFactory = successFactory self.restoreFactory = restoreFactory self.chatListFactory = chatListFactory + self.passphraseFactory = passphraseFactory } } @@ -52,4 +57,12 @@ public extension RestoreCoordinator { func toPopup(_ popup: UIViewController, from parent: UIViewController) { bottomPresenter.present(popup, from: parent) } + + func toPassphrase( + from parent: UIViewController, + _ completion: @escaping StringClosure + ) { + let screen = passphraseFactory(completion) + bottomPresenter.present(screen, from: parent) + } } diff --git a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift index 88d507c17b55eabdfd7ea24c8e22d8a3a609468d..3e91448aed07d65e2cdf650033be9d15be08fb4d 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift @@ -38,7 +38,7 @@ final class RestoreListViewModel { drive.authorize(presenting: controller) { authResult in switch authResult { case .success: - self.hudSubject.send(.on) + self.hudSubject.send(.on(nil)) self.drive.downloadMetadata { downloadResult in switch downloadResult { case .success(let metadata): @@ -63,7 +63,7 @@ final class RestoreListViewModel { private func didRequestICloudAuthorization() { if icloud.isAuthorized() { - self.hudSubject.send(.on) + self.hudSubject.send(.on(nil)) icloud.downloadMetadata { result in switch result { @@ -95,7 +95,7 @@ final class RestoreListViewModel { case .success(let bool): guard bool == true else { return } - self.hudSubject.send(.on) + self.hudSubject.send(.on(nil)) dropbox.downloadMetadata { metadataResult in switch metadataResult { case .success(let metadata): diff --git a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift index 24a3cfb6a91b001547f410882d2cfadd619623af..37a5bbe469665e83df59f9a5c3215c4223e426d5 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift @@ -16,6 +16,7 @@ enum RestorationStep { case idle(CloudService, Backup?) case downloading(Float, Float) case failDownload(Error) + case wrongPass case parsingData case done } @@ -23,14 +24,14 @@ enum RestorationStep { extension RestorationStep: Equatable { static func ==(lhs: RestorationStep, rhs: RestorationStep) -> Bool { switch (lhs, rhs) { - case (.done, .done): + case (.done, .done), (.wrongPass, .wrongPass): return true case let (.failDownload(a), .failDownload(b)): return a.localizedDescription == b.localizedDescription case let (.downloading(a, b), .downloading(c, d)): return a == c && b == d case (.idle, _), (.downloading, _), (.parsingData, _), - (.done, _), (.failDownload, _): + (.done, _), (.failDownload, _), (.wrongPass, _): return false } } @@ -45,7 +46,12 @@ final class RestoreViewModel { var step: AnyPublisher<RestorationStep, Never> { stepRelay.eraseToAnyPublisher() } + // TO REFACTOR: + // + private var pendingData: Data? + private let ndf: String + private var passphrase: String! private let settings: RestoreSettings private let stepRelay: CurrentValueSubject<RestorationStep, Never> @@ -55,7 +61,14 @@ final class RestoreViewModel { self.stepRelay = .init(.idle(settings.cloudService, settings.backup)) } - func didTapRestore() { + func retryWith(passphrase: String) { + self.passphrase = passphrase + continueRestoring(data: pendingData!) + } + + func didTapRestore(passphrase: String) { + self.passphrase = passphrase + guard let backup = settings.backup else { fatalError() } stepRelay.send(.downloading(0.0, backup.size)) @@ -121,10 +134,19 @@ final class RestoreViewModel { DispatchQueue.global().async { [weak self] in guard let self = self else { return } - let session = try! Session(backupFile: data, ndf: self.ndf) - DependencyInjection.Container.shared.register(session as SessionType) - - self.stepRelay.send(.done) + do { + let session = try Session( + passphrase: self.passphrase, + backupFile: data, + ndf: self.ndf + ) + + DependencyInjection.Container.shared.register(session as SessionType) + self.stepRelay.send(.done) + } catch { + self.pendingData = data + self.stepRelay.send(.wrongPass) + } } } } diff --git a/Sources/RestoreFeature/Views/RestorePassphraseView.swift b/Sources/RestoreFeature/Views/RestorePassphraseView.swift new file mode 100644 index 0000000000000000000000000000000000000000..d54fbd4a7577fe1f13d1813cf2047f3956eb8832 --- /dev/null +++ b/Sources/RestoreFeature/Views/RestorePassphraseView.swift @@ -0,0 +1,67 @@ +import UIKit +import Shared +import InputField + +final class RestorePassphraseView: UIView { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let inputField = InputField() + let stackView = UIStackView() + let continueButton = CapsuleButton() + let cancelButton = CapsuleButton() + + init() { + super.init(frame: .zero) + setup() + } + + required init?(coder: NSCoder) { nil } + + private func setup() { + layer.cornerRadius = 40 + backgroundColor = Asset.neutralWhite.color + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + subtitleLabel.numberOfLines = 0 + titleLabel.textColor = Asset.neutralActive.color + subtitleLabel.textColor = Asset.neutralActive.color + + inputField.setup( + style: .regular, + title: "Passphrase", + placeholder: "* * * * * *", + subtitleColor: Asset.neutralDisabled.color + ) + + titleLabel.text = "Backup password" + titleLabel.textAlignment = .left + titleLabel.font = Fonts.Mulish.bold.font(size: 26.0) + + subtitleLabel.text = "Please enter your backup password that you used when you did the backup setup" + subtitleLabel.textAlignment = .left + subtitleLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + continueButton.setStyle(.brandColored) + continueButton.setTitle("Continue", for: .normal) + + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle("Cancel", for: .normal) + + stackView.spacing = 20 + stackView.axis = .vertical + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(subtitleLabel) + stackView.addArrangedSubview(inputField) + stackView.addArrangedSubview(continueButton) + stackView.addArrangedSubview(cancelButton) + + addSubview(stackView) + + stackView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(60) + make.left.equalToSuperview().offset(50) + make.right.equalToSuperview().offset(-50) + make.bottom.equalToSuperview().offset(-70) + } + } +} diff --git a/Sources/RestoreFeature/Views/RestoreView.swift b/Sources/RestoreFeature/Views/RestoreView.swift index cb7f0b7dcde49e40faa4872dcb99c88e6e3199f0..52686480e81c655b774bb59703c30f7ef8dc3f99 100644 --- a/Sources/RestoreFeature/Views/RestoreView.swift +++ b/Sources/RestoreFeature/Views/RestoreView.swift @@ -90,6 +90,8 @@ final class RestoreView: UIView { progressView.isHidden = false progressView.update(downloaded: downloaded, total: total) + case .wrongPass: + progressView.descriptiveProgressLabel.text = "Incorrect password" case .failDownload(let error): progressView.descriptiveProgressLabel.text = error.localizedDescription diff --git a/Sources/SearchFeature/ViewModels/SearchViewModel.swift b/Sources/SearchFeature/ViewModels/SearchViewModel.swift index 3e19639851ab8c0ff325e64f12cb7e0ac7692732..99305371659316ed457663191e616e8bb36d61a2 100644 --- a/Sources/SearchFeature/ViewModels/SearchViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchViewModel.swift @@ -61,7 +61,7 @@ final class SearchViewModel { } func didTapSearch() { - hudRelay.send(.on) + hudRelay.send(.on(nil)) backgroundScheduler.schedule { [weak self] in guard let self = self else { return } diff --git a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift index db7e64016a317b8fe74fa496102437c771f500db..0547bbbce7fd175563e525276af29f27c2a2df6c 100644 --- a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift @@ -17,7 +17,7 @@ final class AccountDeleteViewModel { deleting = true DispatchQueue.main.async { [weak self] in - self?.hudRelay.send(.on) + self?.hudRelay.send(.on(nil)) } do { diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift index a90685459ed3895ec092639512af84d11dff949d..9eb30e134418cbaad8df3a6bd2e32562e594277d 100644 --- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift @@ -110,7 +110,7 @@ final class SettingsViewModel { } private func pushNotifications(enable: Bool) { - hudRelay.send(.on) + hudRelay.send(.on(nil)) if enable == true { pushHandler.didRequestAuthorization { [weak self] result in diff --git a/XCFrameworks/Bindings.xcframework/Info.plist b/XCFrameworks/Bindings.xcframework/Info.plist index 5da456bbdabbf3d610daca4ce17734b523413a53..3c96df61083ca794226526858401b4539235d6ba 100644 --- a/XCFrameworks/Bindings.xcframework/Info.plist +++ b/XCFrameworks/Bindings.xcframework/Info.plist @@ -6,30 +6,30 @@ <array> <dict> <key>LibraryIdentifier</key> - <string>ios-arm64</string> + <string>ios-arm64_x86_64-simulator</string> <key>LibraryPath</key> <string>Bindings.framework</string> <key>SupportedArchitectures</key> <array> <string>arm64</string> + <string>x86_64</string> </array> <key>SupportedPlatform</key> <string>ios</string> + <key>SupportedPlatformVariant</key> + <string>simulator</string> </dict> <dict> <key>LibraryIdentifier</key> - <string>ios-arm64_x86_64-simulator</string> + <string>ios-arm64</string> <key>LibraryPath</key> <string>Bindings.framework</string> <key>SupportedArchitectures</key> <array> <string>arm64</string> - <string>x86_64</string> </array> <key>SupportedPlatform</key> <string>ios</string> - <key>SupportedPlatformVariant</key> - <string>simulator</string> </dict> </array> <key>CFBundlePackageType</key> diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings index 132638347cca07890832d2d4cc9508a49422aa95..45357be1cabae91d29b35413baa479fc4f3f09ed 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings index 132638347cca07890832d2d4cc9508a49422aa95..45357be1cabae91d29b35413baa479fc4f3f09ed 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings index 132638347cca07890832d2d4cc9508a49422aa95..45357be1cabae91d29b35413baa479fc4f3f09ed 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/Current/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings index 70d29b99750c2894dd0cde34438a187dd60ea325..731f69e41028398b03fffcccc5f2430ff8fa2191 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings index 70d29b99750c2894dd0cde34438a187dd60ea325..731f69e41028398b03fffcccc5f2430ff8fa2191 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings differ diff --git a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings index 70d29b99750c2894dd0cde34438a187dd60ea325..731f69e41028398b03fffcccc5f2430ff8fa2191 100644 Binary files a/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings and b/XCFrameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/Current/Bindings differ