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