diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift new file mode 100644 index 000000000..c00b9b7a4 --- /dev/null +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift @@ -0,0 +1,73 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit + +protocol KeyBackupSetupPassphraseCoordinatorDelegate: class { + func keyBackupSetupPassphraseCoordinator(_ keyBackupSetupPassphraseCoordinator: KeyBackupSetupPassphraseCoordinator, didCompleteWithMegolmBackupCreationInfo megolmBackupCreationInfo: MXMegolmBackupCreationInfo) + func keyBackupSetupPassphraseCoordinatorDidCancel(_ keyBackupSetupPassphraseCoordinator: KeyBackupSetupPassphraseCoordinator) +} + +final class KeyBackupSetupPassphraseCoordinator: KeyBackupSetupPassphraseCoordinatorType { + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private var keyBackupSetupPassphraseViewModel: KeyBackupSetupPassphraseViewModelType + private let keyBackupSetupPassphraseViewController: KeyBackupSetupPassphraseViewController + + // MARK: Public + + var childCoordinators: [Coordinator] = [] + + weak var delegate: KeyBackupSetupPassphraseCoordinatorDelegate? + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + + let keyBackup = MXKeyBackup(matrixSession: session) + let keyBackupSetupPassphraseViewModel = KeyBackupSetupPassphraseViewModel(keyBackup: keyBackup) + let keyBackupSetupPassphraseViewController = KeyBackupSetupPassphraseViewController.instantiate(with: keyBackupSetupPassphraseViewModel) + self.keyBackupSetupPassphraseViewModel = keyBackupSetupPassphraseViewModel + self.keyBackupSetupPassphraseViewController = keyBackupSetupPassphraseViewController + } + + // MARK: - Public methods + + func start() { + self.keyBackupSetupPassphraseViewModel.coordinatorDelegate = self + } + + func toPresentable() -> UIViewController { + return self.keyBackupSetupPassphraseViewController + } +} + +// MARK: - KeyBackupSetupPassphraseViewModelCoordinatorDelegate +extension KeyBackupSetupPassphraseCoordinator: KeyBackupSetupPassphraseViewModelCoordinatorDelegate { + func keyBackupSetupPassphraseViewModelDidCancel(_ viewModel: KeyBackupSetupPassphraseViewModelType) { + self.delegate?.keyBackupSetupPassphraseCoordinatorDidCancel(self) + } + + func keyBackupSetupPassphraseViewModel(_ viewModel: KeyBackupSetupPassphraseViewModelType, didCompleteWithMegolmBackupCreationInfo megolmBackupCreationInfo: MXMegolmBackupCreationInfo) { + self.delegate?.keyBackupSetupPassphraseCoordinator(self, didCompleteWithMegolmBackupCreationInfo: megolmBackupCreationInfo) + } +} diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinatorType.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinatorType.swift new file mode 100644 index 000000000..f163f7af3 --- /dev/null +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinatorType.swift @@ -0,0 +1,21 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// `KeyBackupSetupPassphraseCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow. +protocol KeyBackupSetupPassphraseCoordinatorType: Coordinator, Presentable { +} diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewAction.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewAction.swift new file mode 100644 index 000000000..9f74ae331 --- /dev/null +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewAction.swift @@ -0,0 +1,25 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// KeyBackupSetupPassphraseViewController view actions exposed to view model +enum KeyBackupSetupPassphraseViewAction { + case setupPassphrase + case skip + case skipAlertSkip + case skipAlertContinue +} diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewController.storyboard b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewController.storyboard new file mode 100644 index 000000000..3f1e48e8f --- /dev/null +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewController.storyboard @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewController.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewController.swift new file mode 100644 index 000000000..0186cdb36 --- /dev/null +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewController.swift @@ -0,0 +1,395 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +final class KeyBackupSetupPassphraseViewController: UIViewController { + + // MARK: - Constants + + private enum Constants { + static let animationDuration: TimeInterval = 0.3 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var scrollView: UIScrollView! + @IBOutlet private weak var informationLabel: UILabel! + + @IBOutlet private weak var formBackgroundView: UIView! + + @IBOutlet private weak var passphraseTitleLabel: UILabel! + @IBOutlet private weak var passphraseTextField: UITextField! + + @IBOutlet private weak var passphraseAdditionalInfoView: UIView! + @IBOutlet private weak var passphraseStrengthView: PasswordStrengthView! + @IBOutlet private weak var passphraseAdditionalLabel: UILabel! + + @IBOutlet private weak var formSeparatorView: UIView! + + @IBOutlet private weak var confirmPassphraseTitleLabel: UILabel! + @IBOutlet private weak var confirmPassphraseTextField: UITextField! + + @IBOutlet private weak var confirmPassphraseAdditionalInfoView: UIView! + @IBOutlet private weak var confirmPassphraseAdditionalLabel: UILabel! + + @IBOutlet private weak var setPassphraseButtonBackgroundView: UIView! + @IBOutlet private weak var setPassphraseButton: UIButton! + + // MARK: Private + + private var isFirstViewAppearing: Bool = true + private var isPassphraseTextFieldEditedOnce: Bool = false + private var isConfirmPassphraseTextFieldEditedOnce: Bool = false + private var keyboardAvoider: KeyboardAvoider? + private var viewModel: KeyBackupSetupPassphraseViewModelType! + private var theme: Theme! + private var errorPresenter: MXKErrorPresentation! + private var activityPresenter: ActivityIndicatorPresenter! + private weak var skipAlertController: UIAlertController? + + // MARK: - Setup + + class func instantiate(with viewModel: KeyBackupSetupPassphraseViewModelType) -> KeyBackupSetupPassphraseViewController { + let viewController = StoryboardScene.KeyBackupSetupPassphraseViewController.initialScene.instantiate() + viewController.viewModel = viewModel + viewController.theme = ThemeService.shared().theme + return viewController + } + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + + self.title = VectorL10n.keyBackupSetupTitle + self.vc_removeBackTitle() + + self.setupViews() + self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.scrollView) + self.activityPresenter = ActivityIndicatorPresenter() + self.errorPresenter = MXKErrorAlertPresentation() + + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + + self.viewModel.viewDelegate = self + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.keyboardAvoider?.startAvoiding() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if self.isFirstViewAppearing { + self.isFirstViewAppearing = false + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.view.endEditing(true) + self.keyboardAvoider?.stopAvoiding() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if self.isFirstViewAppearing { + // Workaround to layout passphraseStrengthView corner radius + self.passphraseStrengthView.setNeedsLayout() + } + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return self.theme.statusBarStyle + } + + // MARK: - Private + + private func update(theme: Theme) { + self.theme = theme + + self.view.backgroundColor = theme.headerBackgroundColor + + if let navigationBar = self.navigationController?.navigationBar { + theme.applyStyle(onNavigationBar: navigationBar) + } + + self.informationLabel.textColor = theme.textPrimaryColor + + self.formBackgroundView.backgroundColor = theme.backgroundColor + self.passphraseTitleLabel.textColor = theme.textPrimaryColor + theme.applyStyle(onTextField: self.passphraseTextField) + self.passphraseTextField.attributedPlaceholder = NSAttributedString(string: VectorL10n.keyBackupSetupPassphrasePassphrasePlaceholder, + attributes: [.foregroundColor : theme.placeholderTextColor]) + self.updatePassphraseAdditionalLabel() + + self.formSeparatorView.backgroundColor = theme.separatorColor + + self.confirmPassphraseTitleLabel.textColor = theme.textPrimaryColor + theme.applyStyle(onTextField: self.confirmPassphraseTextField) + self.confirmPassphraseTextField.attributedPlaceholder = NSAttributedString(string: VectorL10n.keyBackupSetupPassphraseConfirmPassphraseTitle, + attributes: [.foregroundColor : theme.placeholderTextColor]) + self.updateConfirmPassphraseAdditionalLabel() + + self.setPassphraseButton.backgroundColor = theme.backgroundColor + theme.applyStyle(onButton: self.setPassphraseButton) + } + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + @objc private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) + } + + private func setupViews() { + let skipBarButtonItem = MXKBarButtonItem(title: VectorL10n.keyBackupSetupSkipAction, style: .plain) { [weak self] in + self?.skipButtonAction() + } + + self.navigationItem.rightBarButtonItem = skipBarButtonItem + + self.scrollView.keyboardDismissMode = .interactive + + self.informationLabel.text = VectorL10n.keyBackupSetupPassphraseInfo + + self.passphraseTitleLabel.text = VectorL10n.keyBackupSetupPassphrasePassphraseTitle + self.passphraseTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) + self.passphraseStrengthView.strength = self.viewModel.passphraseStrength + self.passphraseAdditionalInfoView.isHidden = true + + self.confirmPassphraseTitleLabel.text = VectorL10n.keyBackupSetupPassphraseConfirmPassphraseTitle + self.confirmPassphraseTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) + self.confirmPassphraseAdditionalInfoView.isHidden = true + + self.setPassphraseButton.titleLabel?.numberOfLines = 0 + self.setPassphraseButton.setTitle(VectorL10n.keyBackupSetupPassphraseSetPassphraseAction, for: .normal) + + self.updateSetPassphraseButton() + } + + private func showPassphraseAdditionalInfo(animated: Bool) { + guard self.passphraseAdditionalInfoView.isHidden else { + return + } + + UIView.animate(withDuration: Constants.animationDuration) { + self.passphraseAdditionalInfoView.isHidden = false + } + } + + private func showConfirmPassphraseAdditionalInfo(animated: Bool) { + guard self.confirmPassphraseAdditionalInfoView.isHidden else { + return + } + + UIView.animate(withDuration: Constants.animationDuration) { + self.confirmPassphraseAdditionalInfoView.isHidden = false + } + } + + private func hideConfirmPassphraseAdditionalInfo(animated: Bool) { + guard self.confirmPassphraseAdditionalInfoView.isHidden == false else { + return + } + + UIView.animate(withDuration: Constants.animationDuration) { + self.confirmPassphraseAdditionalInfoView.isHidden = true + } + } + + private func updatePassphraseStrengthView() { + self.passphraseStrengthView.strength = self.viewModel.passphraseStrength + } + + private func updatePassphraseAdditionalLabel() { + + let text: String + let textColor: UIColor + + if self.viewModel.isPassphraseValid { + text = VectorL10n.keyBackupSetupPassphrasePassphraseValid + textColor = self.theme.tintColor + } else { + text = VectorL10n.keyBackupSetupPassphrasePassphraseInvalid + textColor = self.theme.notificationPrimaryColor + } + + self.passphraseAdditionalLabel.text = text + self.passphraseAdditionalLabel.textColor = textColor + } + + private func updateConfirmPassphraseAdditionalLabel() { + + let text: String + let textColor: UIColor + + if self.viewModel.isConfirmPassphraseValid { + text = VectorL10n.keyBackupSetupPassphraseConfirmPassphraseValid + textColor = self.theme.tintColor + } else { + text = VectorL10n.keyBackupSetupPassphraseConfirmPassphraseInvalid + textColor = self.theme.notificationPrimaryColor + } + + self.confirmPassphraseAdditionalLabel.text = text + self.confirmPassphraseAdditionalLabel.textColor = textColor + } + + private func updateSetPassphraseButton() { + self.setPassphraseButton.isEnabled = self.viewModel.isFormValid + } + + private func render(viewState: KeyBackupSetupPassphraseViewState) { + switch viewState { + case .loading: + self.renderLoading() + case .loaded: + self.renderLoaded() + case .error(let error): + self.render(error: error) + } + } + + private func renderLoading() { + self.view.endEditing(true) + self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) + } + + private func renderLoaded() { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + } + + private func render(error: Error) { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + self.hideSkipAlert(animated: false) + self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) + } + + private func showSkipAlert() { + guard self.skipAlertController == nil else { + return + } + + let alertController = UIAlertController(title: VectorL10n.keyBackupSetupSkipAlertTitle, + message: VectorL10n.keyBackupSetupSkipAlertMessage, + preferredStyle:.alert) + + alertController.addAction(UIAlertAction(title: VectorL10n.continue, style: .cancel, handler: { action in + self.viewModel.process(viewAction: .skipAlertContinue) + })) + + alertController.addAction(UIAlertAction(title: VectorL10n.keyBackupSetupSkipAlertSkipAction , style: .default, handler: { action in + self.viewModel.process(viewAction: .skipAlertSkip) + })) + + self.present(alertController, animated: true, completion: nil) + self.skipAlertController = alertController + } + + private func hideSkipAlert(animated: Bool) { + self.skipAlertController?.dismiss(animated: true, completion: nil) + } + + // MARK: - Actions + + @IBAction private func passphraseVisibilityButtonAction(_ sender: Any) { + guard self.isPassphraseTextFieldEditedOnce else { + return + } + self.passphraseTextField.isSecureTextEntry.toggle() + } + + @objc func textFieldDidChange(_ textField: UITextField) { + + if textField == self.passphraseTextField { + self.viewModel.passphrase = textField.text + + self.updatePassphraseAdditionalLabel() + self.updatePassphraseStrengthView() + + // Show passphrase additional info at first character entered + if self.isPassphraseTextFieldEditedOnce == false && textField.text?.isEmpty == false { + self.isPassphraseTextFieldEditedOnce = true + self.showPassphraseAdditionalInfo(animated: true) + } + } else { + self.viewModel.confirmPassphrase = textField.text + } + + // Show confirm passphrase additional info if needed + self.updateConfirmPassphraseAdditionalLabel() + if self.viewModel.confirmPassphrase?.isEmpty == false && self.viewModel.isPassphraseValid { + self.showConfirmPassphraseAdditionalInfo(animated: true) + } else { + self.hideConfirmPassphraseAdditionalInfo(animated: true) + } + + // Enable validate button if form is valid + self.updateSetPassphraseButton() + } + + @IBAction private func setPassphraseButtonAction(_ sender: Any) { + self.viewModel.process(viewAction: .setupPassphrase) + } + + private func skipButtonAction() { + self.viewModel.process(viewAction: .skip) + } +} + +// MARK: - UITextFieldDelegate +extension KeyBackupSetupPassphraseViewController: UITextFieldDelegate { + + func textFieldShouldClear(_ textField: UITextField) -> Bool { + self.textFieldDidChange(textField) + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + + if textField == self.passphraseTextField { + self.confirmPassphraseTextField.becomeFirstResponder() + } else { + textField.resignFirstResponder() + } + + return true + } +} + +// MARK: - KeyBackupSetupPassphraseViewModelViewDelegate +extension KeyBackupSetupPassphraseViewController: KeyBackupSetupPassphraseViewModelViewDelegate { + func keyBackupSetupPassphraseViewModel(_ viewModel: KeyBackupSetupPassphraseViewModelType, didUpdateViewState viewSate: KeyBackupSetupPassphraseViewState) { + self.render(viewState: viewSate) + } + + func keyBackupSetupPassphraseViewModelShowSkipAlert(_ viewModel: KeyBackupSetupPassphraseViewModelType) { + self.showSkipAlert() + } +} diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewModel.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewModel.swift new file mode 100644 index 000000000..17bccb7b0 --- /dev/null +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewModel.swift @@ -0,0 +1,138 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +final class KeyBackupSetupPassphraseViewModel: KeyBackupSetupPassphraseViewModelType { + + // MARK: - Properties + + // MARK: Private + + private(set) var passphraseStrength: PasswordStrength = .tooGuessable + private let passwordStrengthManager: PasswordStrengthManager + private let keyBackup: MXKeyBackup + private let coordinatorDelegateQueue: OperationQueue + + // MARK: Public + + var passphrase: String? { + didSet { + self.updatePassphraseStrength() + } + } + + var confirmPassphrase: String? + + var isPassphraseValid: Bool { + return self.passphraseStrength == .veryUnguessable + } + + var isConfirmPassphraseValid: Bool { + guard self.isPassphraseValid, let confirmPassphrase = self.confirmPassphrase else { + return false + } + return confirmPassphrase == passphrase + } + + var isFormValid: Bool { + return self.isPassphraseValid && self.isConfirmPassphraseValid + } + + weak var viewDelegate: KeyBackupSetupPassphraseViewModelViewDelegate? + weak var coordinatorDelegate: KeyBackupSetupPassphraseViewModelCoordinatorDelegate? + + // MARK: - Setup + + init(keyBackup: MXKeyBackup) { + self.passwordStrengthManager = PasswordStrengthManager() + self.keyBackup = keyBackup + + let coordinatorDelegateQueue = OperationQueue() + coordinatorDelegateQueue.name = "KeyBackupSetupPassphraseViewModel.coordinatorDelegateQueue" + coordinatorDelegateQueue.maxConcurrentOperationCount = 1 + self.coordinatorDelegateQueue = coordinatorDelegateQueue + } + + // MARK: - Public + + func process(viewAction: KeyBackupSetupPassphraseViewAction) { + switch viewAction { + case .setupPassphrase: + self.setupPassphrase() + case .skip: + self.pauseCoordinatorOperations() + self.viewDelegate?.keyBackupSetupPassphraseViewModelShowSkipAlert(self) + case.skipAlertContinue: + self.resumeCoordinatorOperations() + case.skipAlertSkip: + self.cancelCoordinatorOperations() + self.coordinatorDelegate?.keyBackupSetupPassphraseViewModelDidCancel(self) + } + } + + // MARK: - Private + + func setupPassphrase() { + guard let passphrase = self.passphrase else { + return + } + + self.viewDelegate?.keyBackupSetupPassphraseViewModel(self, didUpdateViewState: .loading) + + self.keyBackup.prepareKeyBackupVersion(withPassword: passphrase, success: { [weak self] (megolmBackupCreationInfo) in + guard let sself = self else { + return + } + + sself.viewDelegate?.keyBackupSetupPassphraseViewModel(sself, didUpdateViewState: .loaded) + + sself.coordinatorDelegateQueue.addOperation { + DispatchQueue.main.async { + sself.coordinatorDelegate?.keyBackupSetupPassphraseViewModel(sself, didCompleteWithMegolmBackupCreationInfo: megolmBackupCreationInfo) + } + } + }, failure: { [weak self] error in + guard let sself = self else { + return + } + sself.viewDelegate?.keyBackupSetupPassphraseViewModel(sself, didUpdateViewState: .error(error)) + }) + } + + private func updatePassphraseStrength() { + self.passphraseStrength = self.passwordStrength(for: self.passphrase) + } + + private func passwordStrength(for password: String?) -> PasswordStrength { + guard let password = password else { + return .tooGuessable + } + return self.passwordStrengthManager.passwordStrength(for: password) + } + + private func pauseCoordinatorOperations() { + self.coordinatorDelegateQueue.isSuspended = true + } + + private func resumeCoordinatorOperations() { + self.coordinatorDelegateQueue.isSuspended = false + } + + private func cancelCoordinatorOperations() { + self.coordinatorDelegateQueue.cancelAllOperations() + } +} diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewModelType.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewModelType.swift new file mode 100644 index 000000000..40d94da38 --- /dev/null +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewModelType.swift @@ -0,0 +1,44 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +protocol KeyBackupSetupPassphraseViewModelViewDelegate: class { + func keyBackupSetupPassphraseViewModel(_ viewModel: KeyBackupSetupPassphraseViewModelType, didUpdateViewState viewSate: KeyBackupSetupPassphraseViewState) + func keyBackupSetupPassphraseViewModelShowSkipAlert(_ viewModel: KeyBackupSetupPassphraseViewModelType) +} + +protocol KeyBackupSetupPassphraseViewModelCoordinatorDelegate: class { + func keyBackupSetupPassphraseViewModel(_ viewModel: KeyBackupSetupPassphraseViewModelType, didCompleteWithMegolmBackupCreationInfo megolmBackupCreationInfo: MXMegolmBackupCreationInfo) + func keyBackupSetupPassphraseViewModelDidCancel(_ viewModel: KeyBackupSetupPassphraseViewModelType) +} + +/// Protocol describing the view model used by `KeyBackupSetupPassphraseViewController` +protocol KeyBackupSetupPassphraseViewModelType { + + var passphrase: String? { get set } + var confirmPassphrase: String? { get set } + var passphraseStrength: PasswordStrength { get } + + var isPassphraseValid: Bool { get } + var isConfirmPassphraseValid: Bool { get } + var isFormValid: Bool { get } + + var viewDelegate: KeyBackupSetupPassphraseViewModelViewDelegate? { get set } + var coordinatorDelegate: KeyBackupSetupPassphraseViewModelCoordinatorDelegate? { get set } + + func process(viewAction: KeyBackupSetupPassphraseViewAction) +} diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewState.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewState.swift new file mode 100644 index 000000000..ba0e09e1d --- /dev/null +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseViewState.swift @@ -0,0 +1,24 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// KeyBackupSetupPassphraseViewController view state +enum KeyBackupSetupPassphraseViewState { + case loading + case loaded + case error(Error) +} diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/PasswordStrengthView.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/PasswordStrengthView.swift new file mode 100644 index 000000000..aa498124f --- /dev/null +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/PasswordStrengthView.swift @@ -0,0 +1,131 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import Reusable + +final class PasswordStrengthView: UIView, NibOwnerLoadable { + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var firstStrengthView: UIView! + @IBOutlet private weak var secondStrengthView: UIView! + @IBOutlet private weak var thirdStrengthView: UIView! + @IBOutlet private weak var fourthStrengthView: UIView! + + // MARK: Private + + private var strengthViews: [UIView] = [] + + private let strengthViewDefaultColor = UIColor(rgb: 0x9E9E9E) + + private var strengthViewColors: [Int: UIColor] = [ + 0: UIColor(rgb: 0xF56679), + 1: UIColor(rgb: 0xFFC666), + 2: UIColor(rgb: 0xF8E71C), + 3: UIColor(rgb: 0x7AC9A1) + ] + + // MARK: Public + + var strength: PasswordStrength = .tooGuessable { + didSet { + self.updateStrengthColors() + } + } + + // MARK: - Setup + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.loadNibContent() + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.loadNibContent() + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + self.strengthViews = [self.firstStrengthView, + self.secondStrengthView, + self.thirdStrengthView, + self.fourthStrengthView] + + for strenghView in self.strengthViews { + strenghView.layer.masksToBounds = true + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + for strenghView in self.strengthViews { + strenghView.layer.cornerRadius = strenghView.bounds.height/2 + } + } + + // MARK: - Private + + private func updateStrengthColors() { + let strengthViewIndex: Int + + switch self.strength { + case .tooGuessable, .veryGuessable: + strengthViewIndex = 0 + case .somewhatGuessable: + strengthViewIndex = 1 + case .safelyUnguessable: + strengthViewIndex = 2 + case .veryUnguessable: + strengthViewIndex = 3 + } + + self.color(until: strengthViewIndex) + } + + private func color(until strengthViewIndex: Int) { + var index: Int = 0 + + for strenghView in self.strengthViews { + + let color: UIColor + + if index <= strengthViewIndex { + color = self.color(for: index) + } else { + color = self.strengthViewDefaultColor + } + + strenghView.backgroundColor = color + + index+=1 + } + } + + private func color(for index: Int) -> UIColor { + guard let color = self.strengthViewColors[index] else { + return self.strengthViewDefaultColor + } + return color + } +} diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/PasswordStrengthView.xib b/Riot/Modules/KeyBackup/Setup/Passphrase/PasswordStrengthView.xib new file mode 100644 index 000000000..2ab94ba6c --- /dev/null +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/PasswordStrengthView.xib @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +