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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+