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 github "ReactiveX/RxSwift" == 3.0.1
\ No newline at end of file github "pkluz/PKHUD" ~> 4.0
github "pkluz/PKHUD" "4.2.1"
github "ReactiveX/RxSwift" "3.0.1" github "ReactiveX/RxSwift" "3.0.1"
...@@ -115,6 +115,7 @@ ...@@ -115,6 +115,7 @@
564C44601E943C37000F92B1 /* NameRegistrationAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 564C445F1E943C37000F92B1 /* NameRegistrationAdapter.mm */; }; 564C44601E943C37000F92B1 /* NameRegistrationAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 564C445F1E943C37000F92B1 /* NameRegistrationAdapter.mm */; };
564C44621E943DE6000F92B1 /* NameService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44611E943DE6000F92B1 /* NameService.swift */; }; 564C44621E943DE6000F92B1 /* NameService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44611E943DE6000F92B1 /* NameService.swift */; };
564C44641E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44631E943E1E000F92B1 /* NameRegistrationAdapterDelegate.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 */; }; 56AC64D51E7C7F4000EA1AA9 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64D41E7C7F4000EA1AA9 /* WelcomeViewController.swift */; };
56AC64D91E8012CA00EA1AA9 /* Walkthrough.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56AC64DB1E8012CA00EA1AA9 /* Walkthrough.strings */; }; 56AC64D91E8012CA00EA1AA9 /* Walkthrough.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56AC64DB1E8012CA00EA1AA9 /* Walkthrough.strings */; };
56AC64DF1E804ECC00EA1AA9 /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64DE1E804ECC00EA1AA9 /* SwitchCell.swift */; }; 56AC64DF1E804ECC00EA1AA9 /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64DE1E804ECC00EA1AA9 /* SwitchCell.swift */; };
...@@ -264,6 +265,7 @@ ...@@ -264,6 +265,7 @@
564C445F1E943C37000F92B1 /* NameRegistrationAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = NameRegistrationAdapter.mm; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 56AC64DE1E804ECC00EA1AA9 /* SwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCell.swift; sourceTree = "<group>"; };
...@@ -281,6 +283,7 @@ ...@@ -281,6 +283,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
56BBC9C51ED8BF3300CDAF8B /* libargon2.a in Frameworks */, 56BBC9C51ED8BF3300CDAF8B /* libargon2.a in Frameworks */,
568F56751EA7E5DE00132D7D /* PKHUD.framework in Frameworks */,
02674C851E0C757B0065EDF9 /* RxCocoa.framework in Frameworks */, 02674C851E0C757B0065EDF9 /* RxCocoa.framework in Frameworks */,
02674C861E0C757B0065EDF9 /* RxSwift.framework in Frameworks */, 02674C861E0C757B0065EDF9 /* RxSwift.framework in Frameworks */,
043866211D218B1100E06CE2 /* AudioToolbox.framework in Frameworks */, 043866211D218B1100E06CE2 /* AudioToolbox.framework in Frameworks */,
...@@ -393,6 +396,7 @@ ...@@ -393,6 +396,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
56BBC9C41ED8BF3300CDAF8B /* libargon2.a */, 56BBC9C41ED8BF3300CDAF8B /* libargon2.a */,
568F56721EA7E38F00132D7D /* PKHUD.framework */,
02674C801E0C757B0065EDF9 /* RxBlocking.framework */, 02674C801E0C757B0065EDF9 /* RxBlocking.framework */,
02674C811E0C757B0065EDF9 /* RxCocoa.framework */, 02674C811E0C757B0065EDF9 /* RxCocoa.framework */,
02674C821E0C757B0065EDF9 /* RxSwift.framework */, 02674C821E0C757B0065EDF9 /* RxSwift.framework */,
...@@ -822,6 +826,7 @@ ...@@ -822,6 +826,7 @@
inputPaths = ( inputPaths = (
"$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework", "$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework",
"$(SRCROOT)/Carthage/Build/iOS/RxCocoa.framework", "$(SRCROOT)/Carthage/Build/iOS/RxCocoa.framework",
"$(SRCROOT)/Carthage/Build/iOS/PKHUD.framework",
); );
outputPaths = ( outputPaths = (
); );
......
...@@ -48,6 +48,11 @@ class CreateRingAccountViewModel { ...@@ -48,6 +48,11 @@ class CreateRingAccountViewModel {
*/ */
fileprivate let accountService: AccountsService fileprivate let accountService: AccountsService
/**
The nameService instance injected in initializer.
*/
fileprivate var nameService: NameService
//MARK: - Rx Variables and Observers //MARK: - Rx Variables and Observers
var username = Variable<String>("") var username = Variable<String>("")
...@@ -63,12 +68,7 @@ class CreateRingAccountViewModel { ...@@ -63,12 +68,7 @@ class CreateRingAccountViewModel {
var hidePasswordError :Observable<Bool>! var hidePasswordError :Observable<Bool>!
var hideRepeatPasswordError :Observable<Bool>! var hideRepeatPasswordError :Observable<Bool>!
/** var accountCreationState = PublishSubject<AccountCreationState>()
The nameService instance injected in initializer.
*/
fileprivate var nameService: NameService
//MARK: - Rx Variables and Observers
/** /**
Message presented to the user in function of the status of the current username lookup request Message presented to the user in function of the status of the current username lookup request
...@@ -101,79 +101,20 @@ class CreateRingAccountViewModel { ...@@ -101,79 +101,20 @@ class CreateRingAccountViewModel {
} }
/** /**
Create the observers to the streams passed in parameters. Start the process of account creation
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.
*/ */
func configureAddAccountObservers(observable: Observable<Void>, func createAccount() {
onStartCallback: ((() -> Void)?),
onSuccessCallback: ((() -> Void)?), //Add account
onErrorCallback: (((Error?) -> Void)?)) { accountCreationState.onNext(.started)
_ = observable self.accountService.addRingAccount(withUsername: self.username.value,
.subscribe( password: self.password.value)
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)
} }
/** /**
Init obsevables needed to validate the user inputs for account creation Init obsevables needed to validate the user inputs for account creation
*/ */
func initObservables() { fileprivate func initObservables() {
self.passwordValid = password.asObservable().map { password in self.passwordValid = password.asObservable().map { password in
return password.characters.count >= 6 return password.characters.count >= 6
...@@ -231,15 +172,101 @@ class CreateRingAccountViewModel { ...@@ -231,15 +172,101 @@ class CreateRingAccountViewModel {
hideRepeatPasswordError = Observable<Bool>.combineLatest(self.passwordValid,self.passwordsEqual, hasRepeatPassword) { isPasswordValid, isPasswordsEquals, hasRepeatPassword in hideRepeatPasswordError = Observable<Bool>.combineLatest(self.passwordValid,self.passwordsEqual, hasRepeatPassword) { isPasswordValid, isPasswordsEquals, hasRepeatPassword in
return !isPasswordValid || isPasswordsEquals || !hasRepeatPassword 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.username.asObservable().subscribe(onNext: { [unowned self] username in
self.nameService.lookupName(withAccount: "", nameserver: "", name: username) self.nameService.lookupName(withAccount: "", nameserver: "", name: username)
}).addDisposableTo(disposeBag) }).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 @@ ...@@ -38,3 +38,18 @@
"LookingForUsernameAvailability" = "Looking for username availability..."; "LookingForUsernameAvailability" = "Looking for username availability...";
"InvalidUsername" = "Invalid username"; "InvalidUsername" = "Invalid username";
"UsernameAlreadyTaken" = "Username already taken"; "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 ...@@ -23,5 +23,6 @@ import Foundation
/** /**
Time interval between TextField events in seconds Time interval between TextField events in seconds
*/ */
let textFieldThrottlingDuration = 0.5 let textFieldThrottlingDuration = 0.5
let alertFlashDuration = 1.0
...@@ -157,9 +157,8 @@ class AccountsService: AccountAdapterDelegate { ...@@ -157,9 +157,8 @@ class AccountsService: AccountAdapterDelegate {
- Parameter username: the username chosen by the user, if any - Parameter username: the username chosen by the user, if any
- Parameter password: the password chosen by the user - 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 { do {
var ringDetails = try self.getRingInitialAccountDetails() var ringDetails = try self.getRingInitialAccountDetails()
if username != nil { if username != nil {
...@@ -196,7 +195,7 @@ class AccountsService: AccountAdapterDelegate { ...@@ -196,7 +195,7 @@ class AccountsService: AccountAdapterDelegate {
self.currentAccount = account self.currentAccount = account
} }
catch { catch {
throw error self.responseStream.onError(error)
} }
} }
......
...@@ -22,6 +22,7 @@ import UIKit ...@@ -22,6 +22,7 @@ import UIKit
import RxCocoa import RxCocoa
import RxSwift import RxSwift
import PKHUD
fileprivate enum CreateRingAccountCellType { fileprivate enum CreateRingAccountCellType {
case registerPublicUsername case registerPublicUsername
...@@ -62,35 +63,44 @@ class CreateRingAccountViewController: UITableViewController { ...@@ -62,35 +63,44 @@ class CreateRingAccountViewController: UITableViewController {
That allows to build the binding part of the MVVM pattern. That allows to build the binding part of the MVVM pattern.
*/ */
fileprivate func bindViews() { 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 .rx
.tap .tap
.takeUntil(self.rx.deallocated) .takeUntil(self.rx.deallocated)
.subscribe(onNext: {
mAccountViewModel.configureAddAccountObservers( self.mAccountViewModel.createAccount()
observable: createAccountObservable, })
onStartCallback: { [weak self] in .addDisposableTo(self.mDisposeBag)
self?.setCreateAccountAsLoading()
}, //Add Account Registration state
onSuccessCallback: { [weak self] in self.mAccountViewModel.accountCreationState.observeOn(MainScheduler.instance).subscribe(
print("Account created.") onNext: { [unowned self] state in
self?.setCreateAccountAsIdle() switch state {
}, case .started:
onErrorCallback: { [weak self] (error) in self.setCreateAccountAsLoading()
print("Error creating account...") case .success:
if error != nil { self.setCreateAccountAsIdle()
print(error!) 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 .subscribe(onNext: { [weak self] showUsernameField in
self?.toggleRegisterSwitch(showUsernameField) self?.toggleRegisterSwitch(showUsernameField)
}).addDisposableTo(mDisposeBag) }).addDisposableTo(mDisposeBag)
_ = self.mAccountViewModel.canCreateAccount //Enables create account button
self.mAccountViewModel.canCreateAccount
.bindTo(self.mCreateAccountButton.rx.isEnabled) .bindTo(self.mCreateAccountButton.rx.isEnabled)
.addDisposableTo(mDisposeBag) .addDisposableTo(mDisposeBag)
} }
...@@ -109,14 +119,37 @@ class CreateRingAccountViewController: UITableViewController { ...@@ -109,14 +119,37 @@ class CreateRingAccountViewController: UITableViewController {
} }
fileprivate func setCreateAccountAsLoading() { fileprivate func setCreateAccountAsLoading() {
print("Creating account...") print("Creating account...")
self.mCreateAccountButton.setTitle("Loading...", for: .normal) self.mCreateAccountButton.setTitle("Loading...", for: .normal)
self.mCreateAccountButton.isUserInteractionEnabled = false self.mCreateAccountButton.isUserInteractionEnabled = false
let title = NSLocalizedString("WaitCreateAccountTitle",
tableName:LocalizedStringTableNames.walkthrough,
comment: "")
HUD.show(.labeledProgress(title: title,subtitle: nil))
} }
fileprivate func setCreateAccountAsIdle() { fileprivate func setCreateAccountAsIdle() {
self.mCreateAccountButton.setTitle("Create a Ring account", for: .normal) self.mCreateAccountButton.setTitle("Create a Ring account", for: .normal)
self.mCreateAccountButton.isUserInteractionEnabled = true 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