Commit 0de52659 authored by Silbino Goncalves Matado's avatar Silbino Goncalves Matado Committed by Silbino Goncalves Matado

Lookup and register name: add services and adapters

Add NameService and NameRegistrationAdapter to :

- Verify if the username is valid and available to create a new user
- Register a new username into the blockchain.

Add RegistrationState observation from the daemon to verify if the
account is properly created.

Change-Id: I5a66dde2576391b5ec2dc242fb544dc4fe680d9e
parent fd9f3ab1
This diff is collapsed.
...@@ -31,7 +31,7 @@ enum AccountModelError: Error { ...@@ -31,7 +31,7 @@ enum AccountModelError: Error {
/** /**
A class representing an account. A class representing an account.
*/ */
class AccountModel { class AccountModel : Equatable {
// MARK: Public members // MARK: Public members
let id: String let id: String
...@@ -49,25 +49,18 @@ class AccountModel { ...@@ -49,25 +49,18 @@ class AccountModel {
} }
init(withAccountId accountId: String, init(withAccountId accountId: String,
details: Dictionary<String, String>, details: AccountConfigModel,
volatileDetails: Dictionary<String, String>, volatileDetails: AccountConfigModel,
credentials: Array<Dictionary<String, String>>, credentials: [AccountCredentialsModel],
devices: Dictionary<String, String>) throws { devices: Dictionary<String, String>) throws {
self.id = accountId self.id = accountId
self.details = AccountConfigModel(withDetails: details) self.details = details
self.volatileDetails = AccountConfigModel(withDetails: details) self.volatileDetails = volatileDetails
for credential in credentials {
do {
let cred = try AccountCredentialsModel(withRawaData: credential)
credentialDetails.append(cred)
} catch CredentialsError.NotEnoughData {
print("Not enough data to build a credential object.")
throw CredentialsError.NotEnoughData
} catch {
print("Unexpected error.")
throw AccountModelError.UnexpectedError
}
}
self.devices = devices self.devices = devices
} }
public static func ==(lhs: AccountModel, rhs: AccountModel) -> Bool {
return lhs.id == rhs.id
}
} }
...@@ -54,83 +54,50 @@ class CreateRingAccountViewModel { ...@@ -54,83 +54,50 @@ class CreateRingAccountViewModel {
var password = Variable<String>("") var password = Variable<String>("")
var repeatPassword = Variable<String>("") var repeatPassword = Variable<String>("")
var usernameValid :Observable<Bool> { var passwordValid :Observable<Bool>!
return username.asObservable().map({ username in var passwordsEqual :Observable<Bool>!
return !username.isEmpty var canCreateAccount :Observable<Bool>!
}) var registerUsername = Variable<Bool>(true)
}
var passwordValid :Observable<Bool> {
return Observable<Bool>.combineLatest(self.username.asObservable(),
self.password.asObservable(),
self.repeatPassword.asObservable())
{ (username, password, repeatPassword) in
return password.characters.count >= 6
}
}
var passwordsEqual :Observable<Bool> {
return Observable<Bool>.combineLatest(self.password.asObservable(),
self.repeatPassword.asObservable())
{ password, repeatPassword in
return password == repeatPassword
}
}
var canCreateAccount :Observable<Bool> { var hasNewPassword :Observable<Bool>!
return Observable<Bool>.combineLatest(self.registerUsername.asObservable(), var hidePasswordError :Observable<Bool>!
self.usernameValid, var hideRepeatPasswordError :Observable<Bool>!
self.passwordValid,
self.passwordsEqual)
{ registerUsername, usernameValid, passwordValid, passwordsEquals in
if registerUsername {
return usernameValid && passwordValid && passwordsEquals
} else {
return passwordValid && passwordsEquals
}
}
}
var registerUsername = Variable<Bool>(true) /**
The nameService instance injected in initializer.
*/
fileprivate var nameService: NameService
//Observes if the field is not empty //MARK: - Rx Variables and Observers
var hasNewPassword :Observable<Bool> {
return self.password.asObservable().map({ password in
return password.characters.count == 0
})
}
//Observes if the password is valid and is not empty to show the error message /**
var hidePasswordError :Observable<Bool> { Message presented to the user in function of the status of the current username lookup request
return Observable<Bool>.combineLatest(self.passwordValid, hasNewPassword) */
{ isPasswordValid, hasNewPassword in var usernameValidationMessage :Observable<String>!
return isPasswordValid || hasNewPassword
}
}
//Observes if the password is valid and is not empty to show the error message //MARK: -
var hideRepeatPasswordError :Observable<Bool> {
return Observable<Bool>.combineLatest(self.passwordValid, self.passwordsEqual) { isPasswordValid,
isPasswordsEquals in
return !isPasswordValid || isPasswordsEquals
}
}
/** /**
Default constructor Default constructor
*/ */
init(withAccountService accountService: AccountsService) { init(withAccountService accountService: AccountsService, nameService: NameService) {
self.account = nil self.account = nil
self.accountService = accountService self.accountService = accountService
self.nameService = nameService
self.initObservables()
self.initObservers()
} }
/** /**
Constructor with AccountModel. Constructor with AccountModel.
*/ */
init(withAccountService accountService: AccountsService, init(withAccountService accountService: AccountsService,
accountModel account: AccountModel?) { accountModel account: AccountModel?, nameService: NameService) {
self.account = account self.account = account
self.accountService = accountService self.accountService = accountService
self.nameService = nameService
self.initObservables()
self.initObservers()
} }
/** /**
...@@ -163,9 +130,25 @@ class CreateRingAccountViewModel { ...@@ -163,9 +130,25 @@ class CreateRingAccountViewModel {
if event.eventType == ServiceEventType.AccountAdded { if event.eventType == ServiceEventType.AccountAdded {
print("Account added.") print("Account added.")
} }
if event.eventType == ServiceEventType.AccountsChanged { if event.eventType == ServiceEventType.AccountsChanged {
onSuccessCallback?() 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 }, onError: { error in
onErrorCallback?(error) onErrorCallback?(error)
}) })
...@@ -173,8 +156,9 @@ class CreateRingAccountViewModel { ...@@ -173,8 +156,9 @@ class CreateRingAccountViewModel {
//~ Launch the action. //~ Launch the action.
do { do {
try self?.accountService.addRingAccount(withUsername: nil, //Add account
password: "coucou") try self?.accountService.addRingAccount(withUsername: self?.username.value,
password: (self?.password.value)!)
} }
catch { catch {
onErrorCallback?(error) onErrorCallback?(error)
...@@ -186,4 +170,76 @@ class CreateRingAccountViewModel { ...@@ -186,4 +170,76 @@ class CreateRingAccountViewModel {
.addDisposableTo(disposeBag) .addDisposableTo(disposeBag)
} }
/**
Init obsevables needed to validate the user inputs for account creation
*/
func initObservables() {
self.passwordValid = password.asObservable().map { password in
return password.characters.count >= 6
}.shareReplay(1).observeOn(MainScheduler.instance)
self.passwordsEqual = Observable<Bool>.combineLatest(self.password.asObservable(),
self.repeatPassword.asObservable()) { password,repeatPassword in
return password == repeatPassword
}.shareReplay(1).observeOn(MainScheduler.instance)
self.canCreateAccount = Observable<Bool>.combineLatest(self.registerUsername.asObservable(),
self.nameService.usernameValidationStatus,
self.passwordValid,
self.passwordsEqual)
{ registerUsername, usernameValidationStatus, passwordValid, passwordsEquals in
if registerUsername {
return usernameValidationStatus == .valid && passwordValid && passwordsEquals
} else {
return passwordValid && passwordsEquals
}
}.shareReplay(1).observeOn(MainScheduler.instance)
self.usernameValidationMessage = self.nameService.usernameValidationStatus
.asObservable().map ({ status in
switch status {
case .lookingUp:
return NSLocalizedString("LookingForUsernameAvailability",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
case .invalid:
return NSLocalizedString("InvalidUsername",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
case .alreadyTaken:
return NSLocalizedString("UsernameAlreadyTaken",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
default:
return ""
}
}).shareReplay(1).observeOn(MainScheduler.instance)
hasNewPassword = self.password.asObservable().map({ password in
return password.characters.count > 0
})
hidePasswordError = Observable<Bool>.combineLatest(self.passwordValid, hasNewPassword) { isPasswordValid, hasNewPassword in
return isPasswordValid || !hasNewPassword
}
let hasRepeatPassword = self.repeatPassword.asObservable().map({ repeatPassword in
return repeatPassword.characters.count > 0
})
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
*/
func initObservers() {
self.username.asObservable().subscribe(onNext: { [unowned self] username in
self.nameService.lookupName(withAccount: "", nameserver: "", name: username)
}).addDisposableTo(disposeBag)
}
} }
...@@ -28,6 +28,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -28,6 +28,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
static let daemonService = DaemonService(dRingAdaptor: DRingAdapter()) static let daemonService = DaemonService(dRingAdaptor: DRingAdapter())
static let accountService = AccountsService(withAccountAdapter: AccountAdapter()) static let accountService = AccountsService(withAccountAdapter: AccountAdapter())
static let nameService = NameService(withNameRegistrationAdapter: NameRegistrationAdapter())
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
SystemAdapter().registerConfigurationHandler() SystemAdapter().registerConfigurationHandler()
...@@ -123,8 +124,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -123,8 +124,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - Ring Daemon // MARK: - Ring Daemon
fileprivate func startDaemon() { fileprivate func startDaemon() {
do { do {
try AppDelegate.daemonService.startDaemon() try AppDelegate.daemonService.startDaemon()
AppDelegate.accountService.loadAccounts()
} catch StartDaemonError.InitializationFailure { } catch StartDaemonError.InitializationFailure {
print("Daemon failed to initialize.") print("Daemon failed to initialize.")
} catch StartDaemonError.StartFailure { } catch StartDaemonError.StartFailure {
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="15G1212" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="qdG-Sd-QaE"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="15G31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="qdG-Sd-QaE">
<device id="retina4_0" orientation="portrait"> <device id="retina4_0" orientation="portrait">
<adaptation id="fullscreen"/> <adaptation id="fullscreen"/>
</device> </device>
......
...@@ -36,3 +36,5 @@ ...@@ -36,3 +36,5 @@
"PasswordCharactersNumberError" = "6 characters minimum"; "PasswordCharactersNumberError" = "6 characters minimum";
"PasswordNotMatchingError" = "Passwords do not match"; "PasswordNotMatchingError" = "Passwords do not match";
"LookingForUsernameAvailability" = "Looking for username availability..."; "LookingForUsernameAvailability" = "Looking for username availability...";
"InvalidUsername" = "Invalid username";
"UsernameAlreadyTaken" = "Username already taken";
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
#import "Utils.h" #import "Utils.h"
#import "dring/configurationmanager_interface.h" #import "dring/configurationmanager_interface.h"
#import "RegistrationResponse.h"
@implementation AccountAdapter @implementation AccountAdapter
...@@ -51,6 +52,18 @@ static id <AccountAdapterDelegate> _delegate; ...@@ -51,6 +52,18 @@ static id <AccountAdapterDelegate> _delegate;
[AccountAdapter.delegate accountsChanged]; [AccountAdapter.delegate accountsChanged];
} }
})); }));
confHandlers.insert(exportable_callback<ConfigurationSignal::RegistrationStateChanged>([&](const std::string& account_id, const std::string& state, int detailsCode, const std::string& detailsStr) {
if (AccountAdapter.delegate) {
RegistrationResponse* response = [RegistrationResponse new];
response.accountId = [NSString stringWithUTF8String:account_id.c_str()];
response.state = [NSString stringWithUTF8String:state.c_str()];
response.detailsCode = (RegistrationResponseDetailsCode)detailsCode;
response.details = [NSString stringWithUTF8String:detailsStr.c_str()];
[AccountAdapter.delegate registrationStateChangedWith:response];
}
}));
registerConfHandlers(confHandlers); registerConfHandlers(confHandlers);
} }
#pragma mark - #pragma mark -
......
...@@ -25,3 +25,7 @@ ...@@ -25,3 +25,7 @@
#import "AccountAdapter.h" #import "AccountAdapter.h"
#import "SystemAdapter.h" #import "SystemAdapter.h"
#import "DRingAdapter.h" #import "DRingAdapter.h"
#import "NameRegistrationAdapter.h"
#import "LookupNameResponse.h"
#import "RegistrationResponse.h"
#import "NameRegistrationResponse.h"
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#import <Foundation/Foundation.h>
#import "NameRegistrationAdapter.h"
//Represents the status of the lookup response from to the daemon
typedef NS_ENUM(NSInteger, LookupNameState) {
LookupNameStateFound = 0,
LookupNameStateInvalidName,
LookupNameStateNotFound,
LookupNameStateError
};
@interface LookupNameResponse : NSObject
@property (nonatomic, retain) NSString* accountId;
@property (nonatomic) LookupNameState state;
@property (nonatomic, retain) NSString* address;
@property (nonatomic, retain) NSString* name;
@end
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#import "LookupNameResponse.h"
@implementation LookupNameResponse
@end
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#import <Foundation/Foundation.h>
@protocol NameRegistrationAdapterDelegate;
@interface NameRegistrationAdapter : NSObject
@property (class, nonatomic, weak) id <NameRegistrationAdapterDelegate> delegate;
- (void)lookupNameWithAccount:(NSString*)account nameserver:(NSString*)nameserver
name:(NSString*)name;
- (void)registerNameWithAccount:(NSString*)account password:(NSString*)password
name:(NSString*)name;
@end
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#import "Ring-Swift.h"
#import "NameRegistrationAdapter.h"
#import "Utils.h"
#import "dring/configurationmanager_interface.h"
#import "LookupNameResponse.h"
#import "NameRegistrationResponse.h"
@implementation NameRegistrationAdapter
using namespace DRing;
/// Static delegate that will receive the propagated daemon events
static id <NameRegistrationAdapterDelegate> _delegate;
- (id)init {
if (self = [super init]) {
[self registerConfigurationHandler];
}
return self;
}
#pragma mark -
#pragma mark Callbacks registration
- (void)registerConfigurationHandler {
std::map<std::string, std::shared_ptr<CallbackWrapperBase>> confHandlers;
confHandlers.insert(exportable_callback<ConfigurationSignal::RegisteredNameFound>([&](const std::string&account_id,
int state,
const std::string address,
const std::string& name) {
if (NameRegistrationAdapter.delegate) {
LookupNameResponse* response = [LookupNameResponse new];
response.accountId = [NSString stringWithUTF8String:account_id.c_str()];
response.state = (LookupNameState)state;
response.address = [NSString stringWithUTF8String:address.c_str()];
response.name = [NSString stringWithUTF8String:name.c_str()];
[NameRegistrationAdapter.delegate registeredNameFoundWith:response];
}
}));
confHandlers.insert(exportable_callback<ConfigurationSignal::NameRegistrationEnded>([&](const std::string&account_id,
int state,
const std::string& name) {
if (NameRegistrationAdapter.delegate) {
NameRegistrationResponse* response = [NameRegistrationResponse new];
response.accountId = [NSString stringWithUTF8String:account_id.c_str()];
response.state = (NameRegistrationState)state;
response.name = [NSString stringWithUTF8String:name.c_str()];
[NameRegistrationAdapter.delegate nameRegistrationEndedWith:response];
}
}));
registerConfHandlers(confHandlers);
}
#pragma mark -
- (void)lookupNameWithAccount:(NSString*)account nameserver:(NSString*)nameserver name:(NSString*)name {
lookupName(std::string([account UTF8String]),std::string([nameserver UTF8String]),std::string([name UTF8String]));
}
- (void)registerNameWithAccount:(NSString*)account password:(NSString*)password name:(NSString*)name {
registerName(std::string([account UTF8String]), std::string([password UTF8String]), std::string([name UTF8String]));
}
#pragma mark NameRegistrationAdapterDelegate
+ (id <NameRegistrationAdapterDelegate>)delegate {
return _delegate;
}
+ (void) setDelegate:(id<NameRegistrationAdapterDelegate>)delegate {
_delegate = delegate;
}
#pragma mark -
@end
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
@objc protocol NameRegistrationAdapterDelegate {
func registeredNameFound(with response: LookupNameResponse)
func nameRegistrationEnded(with response: NameRegistrationResponse)
}
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#import <Foundation/Foundation.h>
//Represents the status of the registration response from to the daemon
typedef NS_ENUM(NSInteger, NameRegistrationState) {
NameRegistrationStateSuccess = 0,
NameRegistrationStateInvalidName,
NameRegistrationStateAlreadyTaken,
NameRegistrationStateError
};
@interface NameRegistrationResponse : NSObject
@property (nonatomic, retain) NSString* accountId;
@property (nonatomic) NameRegistrationState state;
@property (nonatomic, retain) NSString* address;
@property (nonatomic, retain) NSString* name;
@end
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#import "NameRegistrationResponse.h"
@implementation NameRegistrationResponse
@end
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import UIKit
import RxSwift
/**
Represents the status of a username validation request when the user is typing his username
*/