Commit 478dd483 authored by Silbino Goncalves Matado's avatar Silbino Goncalves Matado Committed by Hadrien De Sousa

CreateRingAccount: Add progress views and succes/error messages

Add progress views and alerts popups for success and error to
indicate the status of account creation to the user.

Refactored CreateAccountViewModel callbacks for Rx Subjects

Progress views are made by PKHUD Carthage package

Change-Id: I8d7c83374cf672b61be89462e4d7ba3dd9aef9e8
Reviewed-by: default avatarHadrien De Sousa <hadrien.desousa@savoirfairelinux.com>
parent 0de52659
github "ReactiveX/RxSwift" ~> 3.0
\ No newline at end of file
github "ReactiveX/RxSwift" == 3.0.1
github "pkluz/PKHUD" ~> 4.0
github "pkluz/PKHUD" "4.2.1"
github "ReactiveX/RxSwift" "3.0.1"
......@@ -115,6 +115,7 @@
564C44601E943C37000F92B1 /* NameRegistrationAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 564C445F1E943C37000F92B1 /* NameRegistrationAdapter.mm */; };
564C44621E943DE6000F92B1 /* NameService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44611E943DE6000F92B1 /* NameService.swift */; };
564C44641E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44631E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift */; };
568F56751EA7E5DE00132D7D /* PKHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 568F56721EA7E38F00132D7D /* PKHUD.framework */; };
56AC64D51E7C7F4000EA1AA9 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64D41E7C7F4000EA1AA9 /* WelcomeViewController.swift */; };
56AC64D91E8012CA00EA1AA9 /* Walkthrough.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56AC64DB1E8012CA00EA1AA9 /* Walkthrough.strings */; };
56AC64DF1E804ECC00EA1AA9 /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64DE1E804ECC00EA1AA9 /* SwitchCell.swift */; };
......@@ -264,6 +265,7 @@
564C445F1E943C37000F92B1 /* NameRegistrationAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = NameRegistrationAdapter.mm; sourceTree = "<group>"; };
564C44611E943DE6000F92B1 /* NameService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NameService.swift; sourceTree = "<group>"; };
564C44631E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NameRegistrationAdapterDelegate.swift; sourceTree = "<group>"; };
568F56721EA7E38F00132D7D /* PKHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PKHUD.framework; path = Carthage/Build/iOS/PKHUD.framework; sourceTree = "<group>"; };
56AC64D41E7C7F4000EA1AA9 /* WelcomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
56AC64DA1E8012CA00EA1AA9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Walkthrough.strings; sourceTree = "<group>"; };
56AC64DE1E804ECC00EA1AA9 /* SwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCell.swift; sourceTree = "<group>"; };
......@@ -281,6 +283,7 @@
buildActionMask = 2147483647;
files = (
56BBC9C51ED8BF3300CDAF8B /* libargon2.a in Frameworks */,
568F56751EA7E5DE00132D7D /* PKHUD.framework in Frameworks */,
02674C851E0C757B0065EDF9 /* RxCocoa.framework in Frameworks */,
02674C861E0C757B0065EDF9 /* RxSwift.framework in Frameworks */,
043866211D218B1100E06CE2 /* AudioToolbox.framework in Frameworks */,
......@@ -393,6 +396,7 @@
isa = PBXGroup;
children = (
56BBC9C41ED8BF3300CDAF8B /* libargon2.a */,
568F56721EA7E38F00132D7D /* PKHUD.framework */,
02674C801E0C757B0065EDF9 /* RxBlocking.framework */,
02674C811E0C757B0065EDF9 /* RxCocoa.framework */,
02674C821E0C757B0065EDF9 /* RxSwift.framework */,
......@@ -822,6 +826,7 @@
inputPaths = (
"$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework",
"$(SRCROOT)/Carthage/Build/iOS/RxCocoa.framework",
"$(SRCROOT)/Carthage/Build/iOS/PKHUD.framework",
);
outputPaths = (
);
......
......@@ -48,6 +48,11 @@ class CreateRingAccountViewModel {
*/
fileprivate let accountService: AccountsService
/**
The nameService instance injected in initializer.
*/
fileprivate var nameService: NameService
//MARK: - Rx Variables and Observers
var username = Variable<String>("")
......@@ -63,12 +68,7 @@ class CreateRingAccountViewModel {
var hidePasswordError :Observable<Bool>!
var hideRepeatPasswordError :Observable<Bool>!
/**
The nameService instance injected in initializer.
*/
fileprivate var nameService: NameService
//MARK: - Rx Variables and Observers
var accountCreationState = PublishSubject<AccountCreationState>()
/**
Message presented to the user in function of the status of the current username lookup request
......@@ -101,79 +101,20 @@ class CreateRingAccountViewModel {
}
/**
Create the observers to the streams passed in parameters.
It will allow this ViewModel to react to other entities' events.
- Parameter observable: An observable stream to subscribe on.
Any observed event on this stream will trigger the action of creating an account.
- Parameter onStartCallback: Closure that will be triggered when the action will begin.
- Parameter onSuccessCallback: Closure that will be triggered when the action will succeed.
- Parameter onErrorCallback: Closure that will be triggered in case of error.
Start the process of account creation
*/
func configureAddAccountObservers(observable: Observable<Void>,
onStartCallback: ((() -> Void)?),
onSuccessCallback: ((() -> Void)?),
onErrorCallback: (((Error?) -> Void)?)) {
_ = observable
.subscribe(
onNext: { [weak self] in
//~ Let the caller know that the action has just begun.
onStartCallback?()
//~ Dispose any previously running stream. There is only one add account action
//~ simultaneously authorized.
self?.addAccountDisposable?.dispose()
//~ Subscribe on the AccountsService responseStream to get results.
self?.addAccountDisposable = self?.accountService
.sharedResponseStream
.subscribe(onNext:{ (event) in
if event.eventType == ServiceEventType.AccountAdded {
print("Account added.")
}
if event.eventType == ServiceEventType.AccountsChanged {
onSuccessCallback?()
}
if event.eventType == ServiceEventType.RegistrationStateChanged {
if event.getEventInput(ServiceEventInput.RegistrationState) == Unregistered {
//Register username
if (self?.registerUsername.value)! {
self?.nameService
.registerName(withAccount: (self?.accountService.currentAccount?.id)!,
password: (self?.password.value)!,
name: (self?.username.value)!)
}
}
}
}, onError: { error in
onErrorCallback?(error)
})
self?.addAccountDisposable?.addDisposableTo((self?.disposeBag)!)
//~ Launch the action.
do {
//Add account
try self?.accountService.addRingAccount(withUsername: self?.username.value,
password: (self?.password.value)!)
}
catch {
onErrorCallback?(error)
}
},
onError: { (error) in
onErrorCallback?(error)
})
.addDisposableTo(disposeBag)
func createAccount() {
//Add account
accountCreationState.onNext(.started)
self.accountService.addRingAccount(withUsername: self.username.value,
password: self.password.value)
}
/**
Init obsevables needed to validate the user inputs for account creation
*/
func initObservables() {
fileprivate func initObservables() {
self.passwordValid = password.asObservable().map { password in
return password.characters.count >= 6
......@@ -231,15 +172,101 @@ class CreateRingAccountViewModel {
hideRepeatPasswordError = Observable<Bool>.combineLatest(self.passwordValid,self.passwordsEqual, hasRepeatPassword) { isPasswordValid, isPasswordsEquals, hasRepeatPassword in
return !isPasswordValid || isPasswordsEquals || !hasRepeatPassword
}
}
/**
Init observers needed to validate the user inputs for account creation
Init observers for account creation
*/
func initObservers() {
fileprivate func initObservers() {
//Loookup name request observer
self.username.asObservable().subscribe(onNext: { [unowned self] username in
self.nameService.lookupName(withAccount: "", nameserver: "", name: username)
}).addDisposableTo(disposeBag)
//Name registration observer
self.accountService
.sharedResponseStream
.filter({ event in
return event.eventType == ServiceEventType.RegistrationStateChanged &&
event.getEventInput(ServiceEventInput.RegistrationState) == Unregistered &&
self.registerUsername.value
})
.subscribe(onNext:{ [unowned self] event in
//Launch the process of name registration
if let currentAccountId = self.accountService.currentAccount?.id {
self.nameService.registerName(withAccount: currentAccountId,
password: self.password.value,
name: self.username.value)
}
})
.addDisposableTo(disposeBag)
//Account creation state observer
self.accountService
.sharedResponseStream
.subscribe(onNext: { [unowned self] event in
if event.getEventInput(ServiceEventInput.RegistrationState) == Unregistered {
self.accountCreationState.onNext(.success)
} else if event.getEventInput(ServiceEventInput.RegistrationState) == ErrorGeneric {
self.accountCreationState.onError(AccountCreationError.generic)
} else if event.getEventInput(ServiceEventInput.RegistrationState) == ErrorNetwork {
self.accountCreationState.onError(AccountCreationError.network)
}
}, onError: { error in
self.accountCreationState.onError(AccountCreationError.unknown)
}).addDisposableTo(disposeBag)
}
}
//MARK: Account Creation state
enum AccountCreationState {
case started
case success
case error(error: AccountCreationError)
}
enum AccountCreationError: Error {
case generic
case network
case unknown
}
extension AccountCreationError: LocalizedError {
var title: String {
switch self {
case .generic:
return NSLocalizedString("AccountCannotBeFoundTitle",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
case .network:
return NSLocalizedString("AccountNoNetworkTitle",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
default:
return NSLocalizedString("AccountDefaultErrorTitle",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
}
}
var message: String {
switch self {
case .generic:
return NSLocalizedString("AcountCannotBeFoundMessage",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
case .network:
return NSLocalizedString("AccountNoNetworkMessage",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
default:
return NSLocalizedString("AccountDefaultErrorMessage",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
}
}
}
......@@ -38,3 +38,18 @@
"LookingForUsernameAvailability" = "Looking for username availability...";
"InvalidUsername" = "Invalid username";
"UsernameAlreadyTaken" = "Username already taken";
//Progress View
"WaitCreateAccountTitle" = "Adding account";
//Alerts
"AccountCannotBeFoundTitle" = "Can't find account";
"AcountCannotBeFoundMessage" = "Account couldn't be found on the Ring network. Make sure it was exported on Ring from an existing device, and that provided credentials are correct.";
"AccountAddedTitle" = "Account Added";
"AccountNoNetworkTitle" = "Can't connect to the network";
"AccountNoNetworkMessage" = "Could not add account because Ring couldn't connect to the distributed network. Check your device connectivity.";
"AccountDefaultErrorTitle" = "Unknown error";
"AccountDefaultErrorMessage" = "The account couldn't be created.";
......@@ -23,5 +23,6 @@ import Foundation
/**
Time interval between TextField events in seconds
*/
let textFieldThrottlingDuration = 0.5
let alertFlashDuration = 1.0
......@@ -157,9 +157,8 @@ class AccountsService: AccountAdapterDelegate {
- Parameter username: the username chosen by the user, if any
- Parameter password: the password chosen by the user
- Throws: AddAccountError
*/
func addRingAccount(withUsername username: String?, password: String) throws {
func addRingAccount(withUsername username: String?, password: String) {
do {
var ringDetails = try self.getRingInitialAccountDetails()
if username != nil {
......@@ -196,7 +195,7 @@ class AccountsService: AccountAdapterDelegate {
self.currentAccount = account
}
catch {
throw error
self.responseStream.onError(error)
}
}
......
......@@ -22,6 +22,7 @@ import UIKit
import RxCocoa
import RxSwift
import PKHUD
fileprivate enum CreateRingAccountCellType {
case registerPublicUsername
......@@ -62,35 +63,44 @@ class CreateRingAccountViewController: UITableViewController {
That allows to build the binding part of the MVVM pattern.
*/
fileprivate func bindViews() {
//~ Create the stream. Won't start until an observer subscribes to it.
let createAccountObservable:Observable<Void> = self.mCreateAccountButton
//Add Account button action
self.mCreateAccountButton
.rx
.tap
.takeUntil(self.rx.deallocated)
mAccountViewModel.configureAddAccountObservers(
observable: createAccountObservable,
onStartCallback: { [weak self] in
self?.setCreateAccountAsLoading()
},
onSuccessCallback: { [weak self] in
print("Account created.")
self?.setCreateAccountAsIdle()
},
onErrorCallback: { [weak self] (error) in
print("Error creating account...")
if error != nil {
print(error!)
.subscribe(onNext: {
self.mAccountViewModel.createAccount()
})
.addDisposableTo(self.mDisposeBag)
//Add Account Registration state
self.mAccountViewModel.accountCreationState.observeOn(MainScheduler.instance).subscribe(
onNext: { [unowned self] state in
switch state {
case .started:
self.setCreateAccountAsLoading()
case .success:
self.setCreateAccountAsIdle()
self.showDeviceAddedAlert()
self.dismiss(animated: true, completion: nil)
default:
return
}
self?.setCreateAccountAsIdle()
})
},
onError: { [unowned self] error in
self.showErrorAlert(error as! AccountCreationError)
self.setCreateAccountAsIdle()
}).addDisposableTo(mDisposeBag)
_ = self.mAccountViewModel.registerUsername.asObservable()
//Show or hide user name field
self.mAccountViewModel.registerUsername.asObservable()
.subscribe(onNext: { [weak self] showUsernameField in
self?.toggleRegisterSwitch(showUsernameField)
}).addDisposableTo(mDisposeBag)
_ = self.mAccountViewModel.canCreateAccount
//Enables create account button
self.mAccountViewModel.canCreateAccount
.bindTo(self.mCreateAccountButton.rx.isEnabled)
.addDisposableTo(mDisposeBag)
}
......@@ -109,14 +119,37 @@ class CreateRingAccountViewController: UITableViewController {
}
fileprivate func setCreateAccountAsLoading() {
print("Creating account...")
self.mCreateAccountButton.setTitle("Loading...", for: .normal)
self.mCreateAccountButton.isUserInteractionEnabled = false
print("Creating account...")
self.mCreateAccountButton.setTitle("Loading...", for: .normal)
self.mCreateAccountButton.isUserInteractionEnabled = false
let title = NSLocalizedString("WaitCreateAccountTitle",
tableName:LocalizedStringTableNames.walkthrough,
comment: "")
HUD.show(.labeledProgress(title: title,subtitle: nil))
}
fileprivate func setCreateAccountAsIdle() {
self.mCreateAccountButton.setTitle("Create a Ring account", for: .normal)
self.mCreateAccountButton.isUserInteractionEnabled = true
self.mCreateAccountButton.setTitle("Create a Ring account", for: .normal)
self.mCreateAccountButton.isUserInteractionEnabled = true
HUD.hide()
}
fileprivate func showDeviceAddedAlert() {
let title = NSLocalizedString("AccountAddedTitle",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
HUD.flash(.labeledSuccess(title: title, subtitle: nil), delay: alertFlashDuration)
}
fileprivate func showErrorAlert(_ error: AccountCreationError) {
let alert = UIAlertController.init(title: error.title,
message: error.message,
preferredStyle: .alert)
alert.addAction(UIAlertAction.init(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment