From 0f44b05a38dba41ce3b564efdeb8950fc18c6b3e Mon Sep 17 00:00:00 2001 From: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com> Date: Wed, 29 Nov 2017 16:31:20 -0500 Subject: [PATCH] link new device with new accounts service Change-Id: Idecf7406a7ac59f6738b72d972deae8c333d57c2 --- Ring/Ring/Coordinators/AppCoordinator.swift | 2 +- .../Me/LinkNewDeviceViewController.storyboard | 4 +- .../Me/LinkNewDeviceViewController.swift | 40 ++++---- .../Features/Me/LinkNewDeviceViewModel.swift | 76 ++++----------- Ring/Ring/Features/Me/Me/MeViewModel.swift | 26 +++--- Ring/Ring/Features/Me/MeCoordinator.swift | 2 +- Ring/Ring/Services/NewAccountsService.swift | 93 +++++++++++++++++++ 7 files changed, 150 insertions(+), 93 deletions(-) diff --git a/Ring/Ring/Coordinators/AppCoordinator.swift b/Ring/Ring/Coordinators/AppCoordinator.swift index 81adeaf39..5d4728542 100644 --- a/Ring/Ring/Coordinators/AppCoordinator.swift +++ b/Ring/Ring/Coordinators/AppCoordinator.swift @@ -64,7 +64,6 @@ final class AppCoordinator: Coordinator, StateableResponsive { self.injectionBag = injectionBag self.navigationController.setNavigationBarHidden(true, animated: false) - self.prepareMainInterface() self.stateSubject.subscribe(onNext: { [unowned self] (state) in guard let state = state as? AppState else { return } @@ -74,6 +73,7 @@ final class AppCoordinator: Coordinator, StateableResponsive { case .needToOnboard: self.showWalkthrough() case .allSet: + self.prepareMainInterface() self.showMainInterface() } }).disposed(by: self.disposeBag) diff --git a/Ring/Ring/Features/Me/LinkNewDeviceViewController.storyboard b/Ring/Ring/Features/Me/LinkNewDeviceViewController.storyboard index f1e3cfc82..1948d286b 100644 --- a/Ring/Ring/Features/Me/LinkNewDeviceViewController.storyboard +++ b/Ring/Ring/Features/Me/LinkNewDeviceViewController.storyboard @@ -1,11 +1,11 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="2dj-eG-xeW"> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="2dj-eG-xeW"> <device id="retina4_7" orientation="portrait"> <adaptation id="fullscreen"/> </device> <dependencies> <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13527"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <scenes> diff --git a/Ring/Ring/Features/Me/LinkNewDeviceViewController.swift b/Ring/Ring/Features/Me/LinkNewDeviceViewController.swift index 5778a590b..9a4698484 100644 --- a/Ring/Ring/Features/Me/LinkNewDeviceViewController.swift +++ b/Ring/Ring/Features/Me/LinkNewDeviceViewController.swift @@ -2,6 +2,7 @@ * Copyright (C) 2017 Savoir-faire Linux Inc. * * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> + * Author: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,25 +19,25 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import Foundation import Reusable import RxSwift import PKHUD -class LinkNewDeviceViewController: UIViewController, StoryboardBased, ViewModelBased { +final class LinkNewDeviceViewController: UIViewController, StoryboardBased, ViewModelBased { - @IBOutlet weak var titleLable: UILabel! - @IBOutlet weak var passwordField: UITextField! - @IBOutlet weak var okButton: UIButton! - @IBOutlet weak var cancelButton: UIButton! - @IBOutlet weak var pinLabel: UILabel! - @IBOutlet weak var explanationMessage: UILabel! - @IBOutlet weak var errorMessage: UILabel! - @IBOutlet weak var background: UIImageView! - @IBOutlet weak var containerView: UIView! + @IBOutlet private weak var titleLable: UILabel! + @IBOutlet private weak var passwordField: UITextField! + @IBOutlet private weak var okButton: UIButton! + @IBOutlet private weak var cancelButton: UIButton! + @IBOutlet private weak var pinLabel: UILabel! + @IBOutlet private weak var explanationMessage: UILabel! + @IBOutlet private weak var errorMessage: UILabel! + @IBOutlet private weak var background: UIImageView! + @IBOutlet private weak var containerView: UIView! var viewModel: LinkNewDeviceViewModel! - let disposeBag = DisposeBag() + + private let disposeBag = DisposeBag() override func viewDidLoad() { @@ -46,21 +47,21 @@ class LinkNewDeviceViewController: UIViewController, StoryboardBased, ViewModelB // initial state self.viewModel.isInitialState .bind(to: self.titleLable.rx.isHidden) - .addDisposableTo(self.disposeBag) + .disposed(by: self.disposeBag) self.viewModel.isInitialState.bind(to: self.passwordField.rx.isHidden) - .addDisposableTo(self.disposeBag) + .disposed(by: self.disposeBag) self.viewModel.isInitialState.bind(to: self.cancelButton.rx.isHidden) - .addDisposableTo(self.disposeBag) + .disposed(by: self.disposeBag) // error state self.viewModel.isErrorState.bind(to: self.errorMessage.rx.isVisible) - .addDisposableTo(self.disposeBag) + .disposed(by: self.disposeBag) // success state self.viewModel.isSuccessState .bind(to: self.explanationMessage.rx.isVisible) - .addDisposableTo(self.disposeBag) + .disposed(by: self.disposeBag) self.viewModel.isSuccessState .bind(to: self.pinLabel.rx.isVisible) - .addDisposableTo(self.disposeBag) + .disposed(by: self.disposeBag) self.viewModel.observableState .observeOn(MainScheduler.instance) @@ -77,7 +78,7 @@ class LinkNewDeviceViewController: UIViewController, StoryboardBased, ViewModelB default: break } - }).addDisposableTo(self.disposeBag) + }).disposed(by: self.disposeBag) cancelButton.rx.tap.subscribe(onNext: { [unowned self] in self.dismiss(animated: true, completion: nil) @@ -109,4 +110,3 @@ class LinkNewDeviceViewController: UIViewController, StoryboardBased, ViewModelB self.explanationMessage.text = self.viewModel.explanationMessage } } - diff --git a/Ring/Ring/Features/Me/LinkNewDeviceViewModel.swift b/Ring/Ring/Features/Me/LinkNewDeviceViewModel.swift index e8a5418d9..845c0958d 100644 --- a/Ring/Ring/Features/Me/LinkNewDeviceViewModel.swift +++ b/Ring/Ring/Features/Me/LinkNewDeviceViewModel.swift @@ -2,6 +2,7 @@ * Copyright (C) 2017 Savoir-faire Linux Inc. * * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> + * Author: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,7 +19,6 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import Foundation import RxSwift import RxDataSources @@ -28,23 +28,6 @@ enum ExportAccountResponse: Int { case networkProblem = 2 } -enum PinError { - case passwordError - case networkError - case defaultError - - var description: String { - switch self { - case .passwordError: - return L10n.Linkdevice.passwordError - case .networkError: - return L10n.Linkdevice.networkError - case .defaultError: - return L10n.Linkdevice.defaultError - } - } -} - enum GeneratingPinState { case initial @@ -66,7 +49,6 @@ enum GeneratingPinState { } func isStateOfType(type: String) -> Bool { - return self.rawValue == type } } @@ -108,17 +90,16 @@ class LinkNewDeviceViewModel: ViewModel, Stateable { } }().share() - let accountService: AccountsService + private let accountsService: NewAccountsService - let disposeBag = DisposeBag() + private let disposeBag = DisposeBag() // MARK: L10n let linkDeviceTitleTitle = L10n.Linkdevice.title let explanationMessage = L10n.Linkdevice.explanationMessage required init(with injectionBag: InjectionBag) { - self.accountService = injectionBag.accountService - + self.accountsService = injectionBag.newAccountsService } func linkDevice(with password: String?) { @@ -127,42 +108,21 @@ class LinkNewDeviceViewModel: ViewModel, Stateable { self.generatingState.value = GeneratingPinState.error(error: PinError.passwordError) return } - self.accountService.exportOnRing(withPassword: password).subscribe(onCompleted: { - if let account = self.accountService.currentAccount { - let accountHelper = AccountModelHelper(withAccount: account) - let uri = accountHelper.ringId - self.accountService.sharedResponseStream - .filter({ exportComplitedEvent in - return exportComplitedEvent.eventType == ServiceEventType.exportOnRingEnded - && exportComplitedEvent.getEventInput(.uri) == uri - }) - .subscribe(onNext: { [unowned self] exportComplitedEvent in - if let state: Int = exportComplitedEvent.getEventInput(.state) { - switch state { - case ExportAccountResponse.success.rawValue: - if let pin: String = exportComplitedEvent.getEventInput(.pin) { - self.generatingState.value = GeneratingPinState.success(pin: pin) - } else { - self.generatingState.value = GeneratingPinState.error(error: PinError.defaultError) - } - case ExportAccountResponse.wrongPassword.rawValue: - self.generatingState.value = GeneratingPinState.error(error: PinError.passwordError) - case ExportAccountResponse.networkProblem.rawValue: - self.generatingState.value = GeneratingPinState.error(error: PinError.networkError) - default: - self.generatingState.value = GeneratingPinState.error(error: PinError.defaultError) - } - - } - }) - .disposed(by: self.disposeBag) - } else { - self.generatingState.value = GeneratingPinState.error(error: PinError.defaultError) + + self.accountsService.currentAccount().asObservable() + .flatMap { [unowned self] (account) -> Observable<String> in + return self.accountsService.exportAccountOnRing(account, withPassword: password) } - }) - { error in - self.generatingState.value = GeneratingPinState.error(error: PinError.passwordError) - }.addDisposableTo(self.disposeBag) + .subscribe(onNext: { [weak self] (pin) in + self?.generatingState.value = GeneratingPinState.success(pin: pin) + }, onError: { [weak self] (error) in + if let pinError = error as? PinError { + self?.generatingState.value = GeneratingPinState.error(error: pinError) + } else { + self?.generatingState.value = GeneratingPinState.error(error: PinError.defaultError) + } + }) + .disposed(by: self.disposeBag) } func refresh() { diff --git a/Ring/Ring/Features/Me/Me/MeViewModel.swift b/Ring/Ring/Features/Me/Me/MeViewModel.swift index 92cad327f..6b53b62bc 100644 --- a/Ring/Ring/Features/Me/Me/MeViewModel.swift +++ b/Ring/Ring/Features/Me/Me/MeViewModel.swift @@ -84,7 +84,7 @@ final class MeViewModel: ViewModel, Stateable { private let nameService: NameService private let log = SwiftyBeaver.self - + private let accountUsername = Variable<String>("") lazy var accountUsernameObservable: Observable<String> = { return self.accountUsername.asObservable() @@ -104,6 +104,14 @@ final class MeViewModel: ViewModel, Stateable { self.accountService = injectionBag.newAccountsService self.nameService = injectionBag.nameService + self.refresh() + } + + func linkDevice() { + self.stateSubject.onNext(MeState.linkNewDevice) + } + + func refresh() { self.accountService.currentAccount() .do(onNext: { [weak self] (account) in let accountUsernameKey = ConfigKeyModel(withKey: ConfigKey.accountUsername) @@ -125,9 +133,9 @@ final class MeViewModel: ViewModel, Stateable { } else { self?.accountSettings.value = [addNewDevice] } - }, onError: { [weak self] (error) in - self?.accountRingId.value = "No RingId found" - self?.log.error("No RingId found - \(error.localizedDescription)") + }, onError: { [weak self] (error) in + self?.accountRingId.value = "No RingId found" + self?.log.error("No RingId found - \(error.localizedDescription)") }) .flatMap { (account) -> PrimitiveSequence<SingleTrait, String> in let registeredNameKey = ConfigKeyModel(withKey: ConfigKey.accountRegisteredName) @@ -140,14 +148,10 @@ final class MeViewModel: ViewModel, Stateable { } .subscribe(onSuccess: { [weak self] (username) in self?.accountUsername.value = username - }, onError: { [weak self] (error) in - self?.accountUsername.value = "No username found" - self?.log.error("No username found - \(error.localizedDescription)") + }, onError: { [weak self] (error) in + self?.accountUsername.value = "No username found" + self?.log.error("No username found - \(error.localizedDescription)") }) .disposed(by: self.disposeBag) } - - func linkDevice() { - self.stateSubject.onNext(MeState.linkNewDevice) - } } diff --git a/Ring/Ring/Features/Me/MeCoordinator.swift b/Ring/Ring/Features/Me/MeCoordinator.swift index 4a5d5a33c..e142fada8 100644 --- a/Ring/Ring/Features/Me/MeCoordinator.swift +++ b/Ring/Ring/Features/Me/MeCoordinator.swift @@ -25,7 +25,7 @@ import RxSwift /// /// - meDetail: user want its account detail /// -linkDevice: link new device to account -public enum MeState: State { +enum MeState: State { case meDetail case linkNewDevice } diff --git a/Ring/Ring/Services/NewAccountsService.swift b/Ring/Ring/Services/NewAccountsService.swift index a0f029b60..da524c31d 100644 --- a/Ring/Ring/Services/NewAccountsService.swift +++ b/Ring/Ring/Services/NewAccountsService.swift @@ -32,6 +32,27 @@ enum AccountError: Error { case unknownError } +enum ExportAccountError: Error { + case unknownError +} + +enum PinError: Error { + case passwordError + case networkError + case defaultError + + var description: String { + switch self { + case .passwordError: + return L10n.Linkdevice.passwordError + case .networkError: + return L10n.Linkdevice.networkError + case .defaultError: + return L10n.Linkdevice.defaultError + } + } +} + /// The New Accounts Service, with no model duplication from the daemon. final class NewAccountsService { @@ -45,6 +66,8 @@ final class NewAccountsService { /// Stream for daemon signal, inaccessible from the outside fileprivate let daemonSignals = PublishSubject<ServiceEvent>() + fileprivate let disposeBag = DisposeBag() + // MARK: - Members lazy var daemonSignalsObservable: Observable<ServiceEvent> = { return self.daemonSignals.asObservable() @@ -207,6 +230,44 @@ final class NewAccountsService { }) } + func exportAccountOnRing(_ account: AccountModel, withPassword password: String) -> Observable<String> { + let export = self.exportAccount(account, withPassword: password) + + let filteredDaemonSignals = self.daemonSignals.filter { (serviceEvent) -> Bool in + if serviceEvent.getEventInput(ServiceEventInput.state) == ErrorGeneric { + throw AccountCreationError.linkError + } else if serviceEvent.getEventInput(ServiceEventInput.state) == ErrorNetwork { + throw AccountCreationError.network + } + + return serviceEvent.eventType == ServiceEventType.exportOnRingEnded + }.asObservable() + + return Observable + .combineLatest(export, filteredDaemonSignals) { (_, serviceEvent) -> String in + let accountModelHelper = AccountModelHelper(withAccount: account) + guard let uri = accountModelHelper.ringId, uri == serviceEvent.getEventInput(.uri) else { + throw ExportAccountError.unknownError + } + if let state: Int = serviceEvent.getEventInput(.state) { + switch state { + case ExportAccountResponse.success.rawValue: + guard let pin: String = serviceEvent.getEventInput(.pin) else { + throw PinError.defaultError + } + return pin + case ExportAccountResponse.wrongPassword.rawValue: + throw PinError.passwordError + case ExportAccountResponse.networkProblem.rawValue: + throw PinError.networkError + default: + throw PinError.defaultError + } + } + throw PinError.defaultError + } + } + } // MARK: - Private daemon wrappers @@ -305,6 +366,19 @@ extension NewAccountsService { return accountModel } + fileprivate func exportAccount(_ account: AccountModel, withPassword password: String) -> Observable<Void> { + return Observable.create { [unowned self] observable in + let export = self.accountAdapter.export(onRing: account.id, password: password) + if export { + observable.onNext() + observable.onCompleted() + } else { + observable.onError(LinkNewDeviceError.unknownError) + } + return Disposables.create() + } + } + } // MARK: - AccountAdapterDelegate @@ -332,6 +406,25 @@ extension NewAccountsService: AccountAdapterDelegate { func exportOnRingEnded(for account: String, state: Int, pin: String) { log.debug("Export on Ring ended.") + + self.getAccount(fromAccountId: account) + .subscribe(onSuccess: { [unowned self] (account) in + let accountHelper = AccountModelHelper(withAccount: account) + if let uri = accountHelper.ringId { + var event = ServiceEvent(withEventType: .exportOnRingEnded) + event.addEventInput(.uri, value: uri) + event.addEventInput(.state, value: state) + event.addEventInput(.pin, value: pin) + self.daemonSignals.onNext(event) + } + }, onError: { [unowned self] (error) in + self.log.error("Account not found") + var event = ServiceEvent(withEventType: .exportOnRingEnded) + event.addEventInput(.state, value: state) + event.addEventInput(.pin, value: pin) + self.daemonSignals.onNext(event) + }) + .disposed(by: self.disposeBag) } } -- GitLab