blockchain: register name at account creation

- Add signup with Blockchain checkbox on new Ring Account view
- Add indicator next to Username field to check availability

Tuleap: #1158
Change-Id: I52ba5fa32068c39f9059a6f4307c2ead89d4526a
diff --git a/src/RingWizardNewAccountVC.mm b/src/RingWizardNewAccountVC.mm
index 254e544..9d0929b 100644
--- a/src/RingWizardNewAccountVC.mm
+++ b/src/RingWizardNewAccountVC.mm
@@ -53,23 +53,34 @@
     __unsafe_unretained IBOutlet NSView* creationView;
 
     __unsafe_unretained IBOutlet NSButton* photoView;
-    __unsafe_unretained IBOutlet NSTextField* nicknameField;
+    __unsafe_unretained IBOutlet NSTextField* displayNameField;
+    __unsafe_unretained IBOutlet NSTextField* registeredNameField;
     __unsafe_unretained IBOutlet NSSecureTextField* passwordField;
     __unsafe_unretained IBOutlet NSSecureTextField* passwordRepeatField;
-    __unsafe_unretained IBOutlet NSTextField* passwordLabel;
-    __unsafe_unretained IBOutlet NSTextField* passwordRepeatLabel;
     __unsafe_unretained IBOutlet NSImageView* passwordCheck;
     __unsafe_unretained IBOutlet NSImageView* passwordRepeatCheck;
-    __unsafe_unretained IBOutlet NSButton* createButton;
-    __unsafe_unretained IBOutlet NSButton* cancelButton;
 
     __unsafe_unretained IBOutlet NSProgressIndicator* progressBar;
+
+    __unsafe_unretained IBOutlet NSButton* cbSignupRing;
+    __unsafe_unretained IBOutlet NSImageView* ivLookupResult;
+    __unsafe_unretained IBOutlet NSProgressIndicator* indicatorLookupResult;
+
+    __unsafe_unretained IBOutlet NSPopover* helpBlockchainContainer;
+    __unsafe_unretained IBOutlet NSPopover* helpPasswordContainer;
+
     Account* accountToCreate;
     NSTimer* errorTimer;
     QMetaObject::Connection stateChanged;
+    QMetaObject::Connection registrationEnded;
+    QMetaObject::Connection registeredNameFound;
+
+    BOOL lookupQueued;
+    NSString* usernameWaitingForLookupResult;
 }
 
-NSInteger const NICKNAME_TAG                    = 1;
+NSInteger const DISPLAY_NAME_TAG                = 1;
+NSInteger const BLOCKCHAIN_NAME_TAG             = 2;
 
 //ERROR CODE for textfields validations
 NSInteger const ERROR_PASSWORD_TOO_SHORT        = -1;
@@ -85,12 +96,23 @@
     return NO;
 }
 
+- (IBAction)showBlockchainHelp:(id)sender
+{
+    [helpBlockchainContainer showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
+}
+
+- (IBAction)showPasswordHelp:(id)sender
+{
+    [helpPasswordContainer showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
+}
+
 - (void)show
 {
     AppDelegate* appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
-    [nicknameField setTag:NICKNAME_TAG];
-    [nicknameField setStringValue:NSFullUserName()];
-    [self controlTextDidChange:[NSNotification notificationWithName:@"PlaceHolder" object:nicknameField]];
+    [displayNameField setTag:DISPLAY_NAME_TAG];
+    [registeredNameField setTag:BLOCKCHAIN_NAME_TAG];
+    [displayNameField setStringValue: NSFullUserName()];
+    [self controlTextDidChange:[NSNotification notificationWithName:@"PlaceHolder" object:displayNameField]];
 
     NSData* imgData = [[[ABAddressBook sharedAddressBook] me] imageData];
     if (imgData != nil) {
@@ -129,8 +151,8 @@
 }
 
 - (void)pictureTakerDidEnd:(IKPictureTaker *) picker
-                 returnCode:(NSInteger) code
-                contextInfo:(void*) contextInfo
+                returnCode:(NSInteger) code
+               contextInfo:(void*) contextInfo
 {
     if (auto outputImage = [picker outputImage]) {
         [photoView setImage:outputImage];
@@ -139,6 +161,7 @@
 }
 
 #pragma mark - Input validation
+
 - (BOOL)isPasswordValid
 {
     return self.password.length >= 6;
@@ -155,7 +178,7 @@
         return [self produceError:error
                          withCode:ERROR_REPEAT_MISMATCH
                        andMessage:NSLocalizedString(@"Passwords don't match",
-                                                     @"Indication for user")];
+                                                    @"Indication for user")];
     }
     return YES;
 }
@@ -166,7 +189,7 @@
         return [self produceError:error
                          withCode:ERROR_PASSWORD_TOO_SHORT
                        andMessage:NSLocalizedString(@"Password is too short",
-                                                     @"Indication for user")];
+                                                    @"Indication for user")];
     }
     return YES;
 }
@@ -198,17 +221,17 @@
     [self display:loadingView];
     [progressBar startAnimation:nil];
 
-
-    if ([self.alias isEqualToString:@""]) {
-        self.alias = NSLocalizedString(@"Unknown", @"Name used when user leave field empty");
+    NSString* displayName = displayNameField.stringValue;
+    if ([displayName isEqualToString:@""]) {
+        displayName = NSLocalizedString(@"Unknown", @"Name used when user leave field empty");
     }
-    accountToCreate = AccountModel::instance().add(QString::fromNSString(self.alias), Account::Protocol::RING);
 
-    accountToCreate->setAlias([self.alias UTF8String]);
-    accountToCreate->setDisplayName([self.alias UTF8String]);
+    accountToCreate = AccountModel::instance().add(QString::fromNSString(displayName), Account::Protocol::RING);
+    accountToCreate->setAlias([displayName UTF8String]);
+    accountToCreate->setDisplayName([displayName UTF8String]);
 
     if (auto profile = ProfileModel::instance().selectedProfile()) {
-        profile->person()->setFormattedName([self.alias UTF8String]);
+        profile->person()->setFormattedName([displayName UTF8String]);
         QPixmap p;
         auto smallImg = [NSImage imageResize:[photoView image] newSize:{100,100}];
         if (p.loadFromData(QByteArray::fromNSData([smallImg TIFFRepresentation]))) {
@@ -259,28 +282,62 @@
                                                 selector:@selector(didCreateFailed) userInfo:nil
                                                  repeats:NO];
     stateChanged = QObject::connect(&AccountModel::instance(),
-                     &AccountModel::accountStateChanged,
-                     [=](Account *account, const Account::RegistrationState state) {
-                                                             switch(state){
-                                                                 case Account::RegistrationState::READY:
-                                                                 case Account::RegistrationState::TRYING:
-                                                                 case Account::RegistrationState::UNREGISTERED:{
-                                                                     accountToCreate<< Account::EditAction::RELOAD;
-                                                                     QObject::disconnect(stateChanged);
-                                                                     [self.delegate didCreateAccountWithSuccess:YES];
-                                                                     break;
-                                                                 }
-                                                                 case Account::RegistrationState::ERROR:
-                                                                     QObject::disconnect(stateChanged);
-                                                                     [self.delegate didCreateAccountWithSuccess:NO];
-                                                                     break;
-                                                                 case Account::RegistrationState::INITIALIZING:
-                                                                 case Account::RegistrationState::COUNT__:{
-                                                                     //DO Nothing
-                                                                     break;
-                                                                 }
-                                                             }
-                                                         });
+                                    &AccountModel::accountStateChanged,
+                                    [=](Account *account, const Account::RegistrationState state) {
+                                        switch(state){
+                                            case Account::RegistrationState::READY:
+                                            case Account::RegistrationState::TRYING:
+                                            case Account::RegistrationState::UNREGISTERED:{
+                                                accountToCreate << Account::EditAction::RELOAD;
+                                                QObject::disconnect(stateChanged);
+                                                //try to register username
+                                                if (self.signUpBlockchainState == NSOnState){
+                                                    [self startNameRegistration:account];
+                                                } else {
+                                                    [self.delegate didCreateAccountWithSuccess:YES];
+                                                }
+                                                break;
+                                            }
+                                            case Account::RegistrationState::ERROR:
+                                                QObject::disconnect(stateChanged);
+                                                [self.delegate didCreateAccountWithSuccess:NO];
+                                                break;
+                                            case Account::RegistrationState::INITIALIZING:
+                                            case Account::RegistrationState::COUNT__:{
+                                                //Do Nothing
+                                                break;
+                                            }
+                                        }
+                                    });
+}
+
+- (void) startNameRegistration:(Account*) account
+{
+    registrationEnded = QObject::connect(account,
+                                         &Account::nameRegistrationEnded,
+                                         [=] (NameDirectory::RegisterNameStatus status,  const QString& name) {
+                                             QObject::disconnect(registrationEnded);
+                                             switch(status) {
+                                                 case NameDirectory::RegisterNameStatus::WRONG_PASSWORD:
+                                                 case NameDirectory::RegisterNameStatus::ALREADY_TAKEN:
+                                                 case NameDirectory::RegisterNameStatus::NETWORK_ERROR: {
+                                                     [self couldNotRegisterUsername];
+                                                     break;
+                                                 }
+                                                 case NameDirectory::RegisterNameStatus::SUCCESS: {
+                                                     break;
+                                                 }
+                                             }
+
+                                             [self.delegate didCreateAccountWithSuccess:YES];
+                                         });
+    self.isUserNameAvailable = account->registerName(QString::fromNSString(self.password),
+                                                     QString::fromNSString(self.registeredName));
+    if (!self.isUserNameAvailable){
+        NSLog(@"Could not initialize registerName operation");
+        QObject::disconnect(registrationEnded);
+        [self.delegate didCreateAccountWithSuccess:YES];
+    }
 }
 
 - (void)didCreateFailed
@@ -293,6 +350,119 @@
     [self.delegate didCreateAccountWithSuccess:NO];
 }
 
+#pragma mark - UserNameRegistration delegate methods
+
+- (IBAction)toggleSignupRing:(id)sender
+{
+    if (self.withBlockchain) {
+        [self lookupUserName];
+    }
+}
+
+- (void)couldNotRegisterUsername
+{
+    // Do nothing
+}
+
+- (BOOL)withBlockchain
+{
+    return self.signUpBlockchainState == NSOnState;
+}
+
+- (BOOL)userNameAvailableORNotBlockchain
+{
+    return !self.withBlockchain || (self.registeredName.length > 0 && self.isUserNameAvailable);
+}
+
+- (void)showLookUpAvailable:(BOOL)available andText:(NSString *)message
+{
+    [ivLookupResult setImage:[NSImage imageNamed:(available?@"ic_action_accept":@"ic_action_cancel")]] ;
+    [ivLookupResult setHidden:NO];
+    [ivLookupResult setToolTip:message];
+}
+
+- (void)onUsernameAvailabilityChangedWithNewAvailability:(BOOL)newAvailability
+{
+    self.isUserNameAvailable = newAvailability;
+}
+
+- (void)hideLookupSpinner
+{
+    [indicatorLookupResult setHidden:YES];
+}
+
+- (void)showLookupSpinner
+{
+    [ivLookupResult setHidden:YES];
+    [indicatorLookupResult setHidden:NO];
+    [indicatorLookupResult startAnimation:nil];
+}
+
+- (BOOL)lookupUserName
+{
+    [self showLookupSpinner];
+    QObject::disconnect(registeredNameFound);
+    registeredNameFound = QObject::connect(
+                                           &NameDirectory::instance(),
+                                           &NameDirectory::registeredNameFound,
+                                           [=] ( const Account* account, NameDirectory::LookupStatus status,  const QString& address, const QString& name) {
+                                               NSLog(@"Name lookup ended");
+                                               lookupQueued = NO;
+                                               //If this is the username we are waiting for, we can disconnect.
+                                               if (name.compare(QString::fromNSString(usernameWaitingForLookupResult)) == 0) {
+                                                   QObject::disconnect(registeredNameFound);
+                                               } else {
+                                                   //Keep waiting...
+                                                   return;
+                                               }
+
+                                               //We may now stop the spinner
+                                               [self hideLookupSpinner];
+
+                                               BOOL isAvailable = NO;
+                                               NSString* message;
+                                               switch(status)
+                                               {
+                                                   case NameDirectory::LookupStatus::SUCCESS:
+                                                   {
+                                                       message = NSLocalizedString(@"The entered username is not available",
+                                                                                   @"Text shown to user when his username is already registered");
+                                                       isAvailable = NO;
+                                                       break;
+                                                   }
+                                                   case NameDirectory::LookupStatus::NOT_FOUND:
+                                                   {
+                                                       message = NSLocalizedString(@"The entered username is available",
+                                                                                   @"Text shown to user when his username is available to be registered");
+                                                       isAvailable = YES;
+                                                       break;
+                                                   }
+                                                   case NameDirectory::LookupStatus::INVALID_NAME:
+                                                   {
+                                                       message = NSLocalizedString(@"The entered username is invalid. It must have at leat 3 characters and contains only lowercase alphanumeric characters.",
+                                                                                   @"Text shown to user when his username is invalid to be registered");
+                                                       isAvailable = NO;
+                                                       break;
+                                                   }
+                                                   case NameDirectory::LookupStatus::ERROR:
+                                                   default:
+                                                   {
+                                                       message = NSLocalizedString(@"Failed to perform lookup",
+                                                                                   @"Text shown to user when an error occur at registration");
+                                                       isAvailable = NO;
+                                                       break;
+                                                   }
+                                               }
+                                               [self showLookUpAvailable:isAvailable andText: message];
+                                               [self onUsernameAvailabilityChangedWithNewAvailability:isAvailable];
+
+                                           });
+
+    //Start the lookup in a second so that the UI dosen't seem to freeze
+    BOOL result = NameDirectory::instance().lookupName(nullptr, QString(), QString::fromNSString(usernameWaitingForLookupResult));
+
+}
+
 #pragma mark - NSOpenSavePanelDelegate delegate methods
 
 - (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError **)outError
@@ -300,15 +470,41 @@
     return YES;
 }
 
--(void)controlTextDidChange:(NSNotification *)notif
+- (void)controlTextDidChange:(NSNotification *)notif
 {
     NSTextField* textField = [notif object];
-    // else it is NICKNAME_TAG field
-    NSString* alias = textField.stringValue;
-    if ([alias isEqualToString:@""]) {
-        alias = NSLocalizedString(@"Unknown", @"Name used when user leave field empty");
+    if (textField.tag != BLOCKCHAIN_NAME_TAG) {
+        return;
     }
-    self.alias = alias;
+    NSString* alias = textField.stringValue;
+
+    [self showLookupSpinner];
+    [self onUsernameAvailabilityChangedWithNewAvailability:NO];
+    [NSObject cancelPreviousPerformRequestsWithTarget:self];
+    [self performSelector:@selector(lookUp:) withObject:alias afterDelay:0.5];
+}
+
+- (void) lookUp:(NSString*) name
+{
+    if (self.withBlockchain && !lookupQueued) {
+        usernameWaitingForLookupResult = name;
+        lookupQueued = YES;
+        [self lookupUserName];
+    }
+}
+
+
++ (NSSet *)keyPathsForValuesAffectingUserNameAvailableORNotBlockchain
+{
+    return [NSSet setWithObjects:   NSStringFromSelector(@selector(signUpBlockchainState)),
+            NSStringFromSelector(@selector(isUserNameAvailable)),
+            nil];
+}
+
++ (NSSet *)keyPathsForValuesAffectingWithBlockchain
+{
+    return [NSSet setWithObjects:   NSStringFromSelector(@selector(signUpBlockchainState)),
+            nil];
 }
 
 + (NSSet *)keyPathsForValuesAffectingIsPasswordValid
@@ -320,5 +516,4 @@
 {
     return [NSSet setWithObjects:@"password", @"repeatPassword", nil];
 }
-
 @end