diff --git a/Riot/Generated/Storyboards.swift b/Riot/Generated/Storyboards.swift index 0511af4f1..5102a83f4 100644 --- a/Riot/Generated/Storyboards.swift +++ b/Riot/Generated/Storyboards.swift @@ -162,6 +162,11 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: SecretsRecoveryWithPassphraseViewController.self) } + internal enum SecretsResetViewController: StoryboardType { + internal static let storyboardName = "SecretsResetViewController" + + internal static let initialScene = InitialSceneType(storyboard: SecretsResetViewController.self) + } internal enum SecretsSetupRecoveryKeyViewController: StoryboardType { internal static let storyboardName = "SecretsSetupRecoveryKeyViewController" diff --git a/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift b/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift new file mode 100644 index 000000000..4f1277219 --- /dev/null +++ b/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift @@ -0,0 +1,71 @@ +// File created from ScreenTemplate +// $ createScreen.sh Secrets/Reset SecretsReset +/* + Copyright 2020 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 + +final class SecretsResetCoordinator: SecretsResetCoordinatorType { + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private var secretsResetViewModel: SecretsResetViewModelType + private let secretsResetViewController: SecretsResetViewController + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: SecretsResetCoordinatorDelegate? + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + + let secretsResetViewModel = SecretsResetViewModel(session: self.session) + let secretsResetViewController = SecretsResetViewController.instantiate(with: secretsResetViewModel) + self.secretsResetViewModel = secretsResetViewModel + self.secretsResetViewController = secretsResetViewController + } + + // MARK: - Public methods + + func start() { + self.secretsResetViewModel.coordinatorDelegate = self + } + + func toPresentable() -> UIViewController { + return self.secretsResetViewController + } +} + +// MARK: - SecretsResetViewModelCoordinatorDelegate +extension SecretsResetCoordinator: SecretsResetViewModelCoordinatorDelegate { + + func secretsResetViewModelDidResetSecrets(_ viewModel: SecretsResetViewModelType) { + self.delegate?.secretsResetCoordinatorDidResetSecrets(self) + } + + func secretsResetViewModelDidCancel(_ viewModel: SecretsResetViewModelType) { + self.delegate?.secretsResetCoordinatorDidCancel(self) + } +} diff --git a/Riot/Modules/Secrets/Reset/SecretsResetCoordinatorType.swift b/Riot/Modules/Secrets/Reset/SecretsResetCoordinatorType.swift new file mode 100644 index 000000000..e26ccdacd --- /dev/null +++ b/Riot/Modules/Secrets/Reset/SecretsResetCoordinatorType.swift @@ -0,0 +1,29 @@ +// File created from ScreenTemplate +// $ createScreen.sh Secrets/Reset SecretsReset +/* + Copyright 2020 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 SecretsResetCoordinatorDelegate: class { + func secretsResetCoordinatorDidResetSecrets(_ coordinator: SecretsResetCoordinatorType) + func secretsResetCoordinatorDidCancel(_ coordinator: SecretsResetCoordinatorType) +} + +/// `SecretsResetCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow. +protocol SecretsResetCoordinatorType: Coordinator, Presentable { + var delegate: SecretsResetCoordinatorDelegate? { get } +} diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift new file mode 100644 index 000000000..aa135b5fe --- /dev/null +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift @@ -0,0 +1,27 @@ +// File created from ScreenTemplate +// $ createScreen.sh Secrets/Reset SecretsReset +/* + Copyright 2020 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 + +/// SecretsResetViewController view actions exposed to view model +enum SecretsResetViewAction { + case loadData + case reset + case authenticationInfoEntered(_ authInfo: [String: Any]) + case cancel +} diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewController.storyboard b/Riot/Modules/Secrets/Reset/SecretsResetViewController.storyboard new file mode 100644 index 000000000..9ff64568a --- /dev/null +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewController.storyboard @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift new file mode 100644 index 000000000..341899aca --- /dev/null +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift @@ -0,0 +1,202 @@ +// File created from ScreenTemplate +// $ createScreen.sh Secrets/Reset SecretsReset +/* + Copyright 2020 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 SecretsResetViewController: UIViewController { + + // MARK: - Constants + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var scrollView: UIScrollView! + + @IBOutlet private weak var warningImage: UIImageView! + + @IBOutlet private weak var informationLabel: UILabel! + + @IBOutlet private weak var warningTitle: UILabel! + @IBOutlet private weak var warningMessage: UILabel! + + @IBOutlet private weak var resetButton: RoundedButton! + + // MARK: Private + + private var viewModel: SecretsResetViewModelType! + private var theme: Theme! + private var errorPresenter: MXKErrorPresentation! + private var activityPresenter: ActivityIndicatorPresenter! + + private var authenticatedSessionFactory: AuthenticatedSessionViewControllerFactory? + + // MARK: - Setup + + class func instantiate(with viewModel: SecretsResetViewModelType) -> SecretsResetViewController { + let viewController = StoryboardScene.SecretsResetViewController.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.vc_removeBackTitle() + + self.setupViews() + self.activityPresenter = ActivityIndicatorPresenter() + self.errorPresenter = MXKErrorAlertPresentation() + + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + + self.viewModel.viewDelegate = self + + self.viewModel.process(viewAction: .loadData) + } + + 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.warningImage.tintColor = theme.warningColor + + self.informationLabel.textColor = theme.textPrimaryColor + + self.warningTitle.textColor = theme.warningColor + self.warningMessage.textColor = theme.textPrimaryColor + + self.resetButton.update(theme: theme) + } + + 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 cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in + self?.cancelButtonAction() + } + + self.navigationItem.rightBarButtonItem = cancelBarButtonItem + + self.title = VectorL10n.secretsResetTitle + + self.scrollView.keyboardDismissMode = .interactive + + self.informationLabel.text = VectorL10n.secretsResetInformation + + self.warningTitle.text = VectorL10n.secretsResetWarningTitle + self.warningMessage.text = VectorL10n.secretsResetWarningMessage + + self.resetButton.setTitle(VectorL10n.secretsResetResetAction, for: .normal) + } + + private func render(viewState: SecretsResetViewState) { + switch viewState { + case .resetting: + self.renderLoading() + case .resetDone: + self.renderLoaded() + case .showAuthentication(authData: let authData): + self.showAuthentication(authData: authData) + case .error(let error): + self.render(error: error) + } + } + + private func renderLoading() { + 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.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) + } + + private func showAuthentication(authData: SecretsResetAuthData) { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + + let authenticatedSessionFactory = authData.authenticatedSessionViewControllerFactory + + authenticatedSessionFactory.viewController(forPath: authData.path, httpMethod: authData.httpMethod, title: nil, message: VectorL10n.secretsResetAuthenticationMessage, onViewController: { [weak self] (viewController) in + guard let self = self else { + return + } + self.present(viewController, animated: true, completion: nil) + }, onAuthenticated: { [weak self] (authInfo) in + guard let self = self else { + return + } + self.viewModel.process(viewAction: .authenticationInfoEntered(authInfo)) + }, onCancelled: { + + }, onFailure: { [weak self] (error) in + guard let self = self else { + return + } + self.render(error: error) + }) + + self.authenticatedSessionFactory = authenticatedSessionFactory + } + + // MARK: - Actions + + private func cancelButtonAction() { + self.viewModel.process(viewAction: .cancel) + } + + @IBAction private func resetAction(_ sender: Any) { + self.viewModel.process(viewAction: .reset) + } +} + + +// MARK: - SecretsResetViewModelViewDelegate +extension SecretsResetViewController: SecretsResetViewModelViewDelegate { + + func secretsResetViewModel(_ viewModel: SecretsResetViewModelType, didUpdateViewState viewSate: SecretsResetViewState) { + self.render(viewState: viewSate) + } +} diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift new file mode 100644 index 000000000..d9191a626 --- /dev/null +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift @@ -0,0 +1,103 @@ +// File created from ScreenTemplate +// $ createScreen.sh Secrets/Reset SecretsReset +/* + Copyright 2020 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 SecretsResetViewModel: SecretsResetViewModelType { + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private let recoveryService: MXRecoveryService + + // MARK: Public + + weak var viewDelegate: SecretsResetViewModelViewDelegate? + weak var coordinatorDelegate: SecretsResetViewModelCoordinatorDelegate? + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + self.recoveryService = session.crypto.recoveryService + } + + // MARK: - Public + + func process(viewAction: SecretsResetViewAction) { + switch viewAction { + case .loadData: + break + case .reset: + self.askAuthentication() + case .authenticationInfoEntered(let authInfo): + self.resetSecrets(with: authInfo) + case .cancel: + self.coordinatorDelegate?.secretsResetViewModelDidCancel(self) + } + } + + // MARK: - Private + + private func update(viewState: SecretsResetViewState) { + self.viewDelegate?.secretsResetViewModel(self, didUpdateViewState: viewState) + } + + private func resetSecrets(with authInfo: [String: Any]) { + guard let crossSigning = self.session.crypto.crossSigning else { + return + } + + self.update(viewState: .resetting) + crossSigning.setup(withAuthParams: authInfo, success: { [weak self] in + guard let self = self else { + return + } + self.recoveryService.deleteRecovery(withDeleteServicesBackups: true, success: { [weak self] in + guard let self = self else { + return + } + self.update(viewState: .resetDone) + self.coordinatorDelegate?.secretsResetViewModelDidResetSecrets(self) + + }, failure: { [weak self] error in + guard let self = self else { + return + } + self.update(viewState: .error(error)) + }) + + }, failure: { [weak self] error in + guard let self = self else { + return + } + self.update(viewState: .error(error)) + }) + } + + // NOTE: Use a Coordinator instead of AuthenticatedSessionViewControllerFactory and delegate the presentation to SecretsResetCoordinator + private func askAuthentication() { + let path = "\(kMXAPIPrefixPathUnstable)/keys/device_signing/upload" + let authenticatedSessionFactory = AuthenticatedSessionViewControllerFactory(session: self.session) + let authData = SecretsResetAuthData(path: path, httpMethod: "POST", authenticatedSessionViewControllerFactory: authenticatedSessionFactory) + + self.update(viewState: .showAuthentication(authData: authData)) + } +} diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewModelType.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewModelType.swift new file mode 100644 index 000000000..36c5ad7b6 --- /dev/null +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewModelType.swift @@ -0,0 +1,37 @@ +// File created from ScreenTemplate +// $ createScreen.sh Secrets/Reset SecretsReset +/* + Copyright 2020 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 SecretsResetViewModelViewDelegate: class { + func secretsResetViewModel(_ viewModel: SecretsResetViewModelType, didUpdateViewState viewSate: SecretsResetViewState) +} + +protocol SecretsResetViewModelCoordinatorDelegate: class { + func secretsResetViewModelDidResetSecrets(_ viewModel: SecretsResetViewModelType) + func secretsResetViewModelDidCancel(_ viewModel: SecretsResetViewModelType) +} + +/// Protocol describing the view model used by `SecretsResetViewController` +protocol SecretsResetViewModelType { + + var viewDelegate: SecretsResetViewModelViewDelegate? { get set } + var coordinatorDelegate: SecretsResetViewModelCoordinatorDelegate? { get set } + + func process(viewAction: SecretsResetViewAction) +} diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift new file mode 100644 index 000000000..d5ce37444 --- /dev/null +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift @@ -0,0 +1,33 @@ +// File created from ScreenTemplate +// $ createScreen.sh Secrets/Reset SecretsReset +/* + Copyright 2020 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 + +struct SecretsResetAuthData { + let path: String + let httpMethod: String + let authenticatedSessionViewControllerFactory: AuthenticatedSessionViewControllerFactory +} + +/// SecretsResetViewController view state +enum SecretsResetViewState { + case showAuthentication(authData: SecretsResetAuthData) + case resetting + case resetDone + case error(Error) +}