blob: 9338f1839f2b856a7430c0a973de26c4f5f894d0 [file] [log] [blame]
Loïc Siretfcb4ca62016-09-21 17:12:09 -04001/*
2 * Copyright (C) 2015-2016 Savoir-faire Linux Inc.
3 * Author: Loïc Siret <loic.siret@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#import "RingWizardNewAccountVC.h"
21
22
23//Cocoa
24#import <AddressBook/AddressBook.h>
25#import <Quartz/Quartz.h>
26
27//Qt
28#import <QUrl>
29#import <QPixmap>
30
31//LRC
32#import <accountmodel.h>
33#import <protocolmodel.h>
34#import <profilemodel.h>
35#import <QItemSelectionModel>
36#import <account.h>
37#import <certificate.h>
38#import <profilemodel.h>
39#import <profile.h>
40#import <person.h>
41
42#import "AppDelegate.h"
43#import "Constants.h"
44#import "views/NSImage+Extensions.h"
45#import "views/NSColor+RingTheme.h"
46
47@interface RingWizardNewAccountVC ()
48@end
49
50@implementation RingWizardNewAccountVC
51{
Loïc Siret3652cfb2016-10-27 10:12:07 -040052 __unsafe_unretained IBOutlet NSView* loadingView;
53 __unsafe_unretained IBOutlet NSView* creationView;
54
Loïc Siretfcb4ca62016-09-21 17:12:09 -040055 __unsafe_unretained IBOutlet NSButton* photoView;
Alexandre Lision882289b2016-10-31 16:10:39 -040056 __unsafe_unretained IBOutlet NSTextField* displayNameField;
57 __unsafe_unretained IBOutlet NSTextField* registeredNameField;
Loïc Siretfcb4ca62016-09-21 17:12:09 -040058 __unsafe_unretained IBOutlet NSSecureTextField* passwordField;
Loïc Siret3652cfb2016-10-27 10:12:07 -040059 __unsafe_unretained IBOutlet NSSecureTextField* passwordRepeatField;
Loïc Siret3652cfb2016-10-27 10:12:07 -040060 __unsafe_unretained IBOutlet NSImageView* passwordCheck;
61 __unsafe_unretained IBOutlet NSImageView* passwordRepeatCheck;
Loïc Siret3652cfb2016-10-27 10:12:07 -040062
63 __unsafe_unretained IBOutlet NSProgressIndicator* progressBar;
Alexandre Lision882289b2016-10-31 16:10:39 -040064
Alexandre Lision882289b2016-10-31 16:10:39 -040065 __unsafe_unretained IBOutlet NSImageView* ivLookupResult;
66 __unsafe_unretained IBOutlet NSProgressIndicator* indicatorLookupResult;
67
68 __unsafe_unretained IBOutlet NSPopover* helpBlockchainContainer;
69 __unsafe_unretained IBOutlet NSPopover* helpPasswordContainer;
70
Loïc Siretfcb4ca62016-09-21 17:12:09 -040071 Account* accountToCreate;
72 NSTimer* errorTimer;
73 QMetaObject::Connection stateChanged;
Alexandre Lision882289b2016-10-31 16:10:39 -040074 QMetaObject::Connection registrationEnded;
75 QMetaObject::Connection registeredNameFound;
76
77 BOOL lookupQueued;
78 NSString* usernameWaitingForLookupResult;
Loïc Siretfcb4ca62016-09-21 17:12:09 -040079}
80
Alexandre Lision882289b2016-10-31 16:10:39 -040081NSInteger const DISPLAY_NAME_TAG = 1;
82NSInteger const BLOCKCHAIN_NAME_TAG = 2;
Loïc Siretfcb4ca62016-09-21 17:12:09 -040083
Loïc Siret3652cfb2016-10-27 10:12:07 -040084//ERROR CODE for textfields validations
85NSInteger const ERROR_PASSWORD_TOO_SHORT = -1;
86NSInteger const ERROR_REPEAT_MISMATCH = -2;
87
88
89- (BOOL)produceError:(NSError**)error withCode:(NSInteger)code andMessage:(NSString*)message
90{
91 if (error != NULL){
92 NSDictionary *errorDetail = @{NSLocalizedDescriptionKey: message};
93 *error = [NSError errorWithDomain:@"Input" code:code userInfo:errorDetail];
94 }
95 return NO;
96}
Loïc Siretfcb4ca62016-09-21 17:12:09 -040097
Alexandre Lision882289b2016-10-31 16:10:39 -040098- (IBAction)showBlockchainHelp:(id)sender
99{
100 [helpBlockchainContainer showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
101}
102
103- (IBAction)showPasswordHelp:(id)sender
104{
105 [helpPasswordContainer showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
106}
107
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400108- (void)show
109{
110 AppDelegate* appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
Alexandre Lision882289b2016-10-31 16:10:39 -0400111 [displayNameField setTag:DISPLAY_NAME_TAG];
112 [registeredNameField setTag:BLOCKCHAIN_NAME_TAG];
113 [displayNameField setStringValue: NSFullUserName()];
114 [self controlTextDidChange:[NSNotification notificationWithName:@"PlaceHolder" object:displayNameField]];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400115
116 NSData* imgData = [[[ABAddressBook sharedAddressBook] me] imageData];
117 if (imgData != nil) {
118 [photoView setImage:[[NSImage alloc] initWithData:imgData]];
119 } else
120 [photoView setImage:[NSImage imageNamed:@"default_user_icon"]];
121
122 [photoView setWantsLayer: YES];
123 photoView.layer.cornerRadius = photoView.frame.size.width / 2;
124 photoView.layer.masksToBounds = YES;
Alexandre Lisionc2ad6392016-11-08 11:20:34 -0500125 self.signUpBlockchainState = YES;
126 [self toggleSignupRing:nil];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400127
128 [self display:creationView];
129}
130
131- (void)removeSubviews
132{
133 while ([self.view.subviews count] > 0){
134 [[self.view.subviews firstObject] removeFromSuperview];
135 }
136}
137
138- (void)display:(NSView *)view
139{
140 [self.delegate showView:view];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400141}
142
143- (IBAction)editPhoto:(id)sender
144{
Loïc Siret3652cfb2016-10-27 10:12:07 -0400145 auto pictureTaker = [IKPictureTaker pictureTaker];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400146
147 [pictureTaker beginPictureTakerSheetForWindow:[self.delegate window]
148 withDelegate:self
149 didEndSelector:@selector(pictureTakerDidEnd:returnCode:contextInfo:)
150 contextInfo:nil];
151
152}
153
154- (void)pictureTakerDidEnd:(IKPictureTaker *) picker
Alexandre Lision882289b2016-10-31 16:10:39 -0400155 returnCode:(NSInteger) code
156 contextInfo:(void*) contextInfo
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400157{
158 if (auto outputImage = [picker outputImage]) {
159 [photoView setImage:outputImage];
Alexandre Lisionc2ad6392016-11-08 11:20:34 -0500160 } else {
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400161 [photoView setImage:[NSImage imageNamed:@"default_user_icon"]];
Alexandre Lisionc2ad6392016-11-08 11:20:34 -0500162 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400163}
164
Loïc Siret3652cfb2016-10-27 10:12:07 -0400165#pragma mark - Input validation
Alexandre Lision882289b2016-10-31 16:10:39 -0400166
Loïc Siret3652cfb2016-10-27 10:12:07 -0400167- (BOOL)isPasswordValid
168{
169 return self.password.length >= 6;
170}
171
172- (BOOL)isRepeatPasswordValid
173{
Anthony Léonard24110e82017-09-15 16:29:11 -0400174 return [self.password isEqualToString:self.repeatPassword] || ([self.password length] == 0 && [self.repeatPassword length] == 0);
Loïc Siret3652cfb2016-10-27 10:12:07 -0400175}
176
177- (BOOL)validateRepeatPassword:(NSError **)error
178{
179 if (!self.isRepeatPasswordValid){
180 return [self produceError:error
181 withCode:ERROR_REPEAT_MISMATCH
182 andMessage:NSLocalizedString(@"Passwords don't match",
Alexandre Lision882289b2016-10-31 16:10:39 -0400183 @"Indication for user")];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400184 }
185 return YES;
186}
187
188- (BOOL)validatePassword:(NSError **)error
189{
190 if (!self.isRepeatPasswordValid){
191 return [self produceError:error
192 withCode:ERROR_PASSWORD_TOO_SHORT
193 andMessage:NSLocalizedString(@"Password is too short",
Alexandre Lision882289b2016-10-31 16:10:39 -0400194 @"Indication for user")];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400195 }
196 return YES;
197}
198
199- (BOOL)validateUserInputPassword:(NSError **)error
200{
201 return [self validatePassword:error] && [self validateRepeatPassword:error];
202}
203
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400204- (IBAction)createRingAccount:(id)sender
205{
Loïc Siret3652cfb2016-10-27 10:12:07 -0400206 NSError *error = nil;
207 if (![self validateUserInputPassword:&error]){
208 NSAlert* alert = [NSAlert alertWithMessageText:[error localizedDescription]
209 defaultButton:NSLocalizedString(@"Revise Input",
210 @"Button title")
211 alternateButton:nil
212 otherButton:nil
213 informativeTextWithFormat:@"%@",error];
214
215 [alert beginSheetModalForWindow:passwordField.window
216 modalDelegate:nil
217 didEndSelector:NULL
218 contextInfo:NULL];
219
220 return;
221 }
222
223 [self display:loadingView];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400224 [progressBar startAnimation:nil];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400225
Alexandre Lision882289b2016-10-31 16:10:39 -0400226 NSString* displayName = displayNameField.stringValue;
227 if ([displayName isEqualToString:@""]) {
228 displayName = NSLocalizedString(@"Unknown", @"Name used when user leave field empty");
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400229 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400230
Alexandre Lision882289b2016-10-31 16:10:39 -0400231 accountToCreate = AccountModel::instance().add(QString::fromNSString(displayName), Account::Protocol::RING);
232 accountToCreate->setAlias([displayName UTF8String]);
233 accountToCreate->setDisplayName([displayName UTF8String]);
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400234
235 if (auto profile = ProfileModel::instance().selectedProfile()) {
Alexandre Lision882289b2016-10-31 16:10:39 -0400236 profile->person()->setFormattedName([displayName UTF8String]);
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400237 QPixmap p;
238 auto smallImg = [NSImage imageResize:[photoView image] newSize:{100,100}];
239 if (p.loadFromData(QByteArray::fromNSData([smallImg TIFFRepresentation]))) {
240 profile->person()->setPhoto(QVariant(p));
Alexandre Lision849514f2016-10-25 14:07:51 -0400241 } else {
242 auto defaultAvatar = [NSImage imageResize:[NSImage imageNamed:@"default_user_icon"] newSize:{100,100}];
243 p.loadFromData(QByteArray::fromNSData([defaultAvatar TIFFRepresentation]));
244 profile->person()->setPhoto(QVariant(p));
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400245 }
246 profile->save();
247 }
248
Loïc Siret3652cfb2016-10-27 10:12:07 -0400249 QModelIndex qIdx = AccountModel::instance().protocolModel()->selectionModel()->currentIndex();
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400250
251 [self setCallback];
252
253 [self performSelector:@selector(saveAccount) withObject:nil afterDelay:1];
254 [self registerDefaultPreferences];
255}
256
257/**
258 * Set default values for preferences
259 */
260- (void)registerDefaultPreferences
261{
262 // enable AutoStartup
263 LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
264 if (loginItemsRef == nil) return;
265 CFURLRef appUrl = (__bridge CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
266 LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, appUrl, NULL, NULL);
267 if (itemRef) CFRelease(itemRef);
268
269 // enable Notifications
270 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:Preferences::Notifications];
271}
272
273- (void)saveAccount
274{
275 accountToCreate->setArchivePassword(QString::fromNSString(passwordField.stringValue));
276 accountToCreate->setUpnpEnabled(YES); // Always active upnp
277 accountToCreate << Account::EditAction::SAVE;
278}
279
280- (void)setCallback
281{
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400282 stateChanged = QObject::connect(&AccountModel::instance(),
Alexandre Lision882289b2016-10-31 16:10:39 -0400283 &AccountModel::accountStateChanged,
284 [=](Account *account, const Account::RegistrationState state) {
285 switch(state){
286 case Account::RegistrationState::READY:
287 case Account::RegistrationState::TRYING:
288 case Account::RegistrationState::UNREGISTERED:{
289 accountToCreate << Account::EditAction::RELOAD;
290 QObject::disconnect(stateChanged);
291 //try to register username
292 if (self.signUpBlockchainState == NSOnState){
293 [self startNameRegistration:account];
294 } else {
295 [self.delegate didCreateAccountWithSuccess:YES];
296 }
297 break;
298 }
299 case Account::RegistrationState::ERROR:
300 QObject::disconnect(stateChanged);
301 [self.delegate didCreateAccountWithSuccess:NO];
302 break;
303 case Account::RegistrationState::INITIALIZING:
304 case Account::RegistrationState::COUNT__:{
305 //Do Nothing
306 break;
307 }
308 }
309 });
310}
311
312- (void) startNameRegistration:(Account*) account
313{
Alexandre Lisione857c0c2016-11-03 10:13:00 -0400314 // Dismiss this screen if after 30 seconds the name is still not registered
315 errorTimer = [NSTimer scheduledTimerWithTimeInterval:30
316 target:self
317 selector:@selector(nameRegistrationTimeout) userInfo:nil
318 repeats:NO];
Alexandre Lision882289b2016-10-31 16:10:39 -0400319 registrationEnded = QObject::connect(account,
320 &Account::nameRegistrationEnded,
321 [=] (NameDirectory::RegisterNameStatus status, const QString& name) {
322 QObject::disconnect(registrationEnded);
323 switch(status) {
324 case NameDirectory::RegisterNameStatus::WRONG_PASSWORD:
325 case NameDirectory::RegisterNameStatus::ALREADY_TAKEN:
326 case NameDirectory::RegisterNameStatus::NETWORK_ERROR: {
327 [self couldNotRegisterUsername];
328 break;
329 }
330 case NameDirectory::RegisterNameStatus::SUCCESS: {
331 break;
332 }
333 }
334
335 [self.delegate didCreateAccountWithSuccess:YES];
336 });
337 self.isUserNameAvailable = account->registerName(QString::fromNSString(self.password),
338 QString::fromNSString(self.registeredName));
339 if (!self.isUserNameAvailable){
340 NSLog(@"Could not initialize registerName operation");
341 QObject::disconnect(registrationEnded);
342 [self.delegate didCreateAccountWithSuccess:YES];
343 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400344}
345
Alexandre Lisione857c0c2016-11-03 10:13:00 -0400346- (void)nameRegistrationTimeout
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400347{
Alexandre Lisione857c0c2016-11-03 10:13:00 -0400348 // This callback is used when registration takes more than 30 seconds
349 // It skips the wizard and brings the main window
350 [self.delegate didCreateAccountWithSuccess:YES];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400351}
352
353- (IBAction)cancel:(id)sender
354{
355 [self.delegate didCreateAccountWithSuccess:NO];
356}
357
Alexandre Lision882289b2016-10-31 16:10:39 -0400358#pragma mark - UserNameRegistration delegate methods
359
360- (IBAction)toggleSignupRing:(id)sender
361{
362 if (self.withBlockchain) {
363 [self lookupUserName];
364 }
365}
366
367- (void)couldNotRegisterUsername
368{
369 // Do nothing
370}
371
372- (BOOL)withBlockchain
373{
374 return self.signUpBlockchainState == NSOnState;
375}
376
377- (BOOL)userNameAvailableORNotBlockchain
378{
379 return !self.withBlockchain || (self.registeredName.length > 0 && self.isUserNameAvailable);
380}
381
382- (void)showLookUpAvailable:(BOOL)available andText:(NSString *)message
383{
384 [ivLookupResult setImage:[NSImage imageNamed:(available?@"ic_action_accept":@"ic_action_cancel")]] ;
385 [ivLookupResult setHidden:NO];
386 [ivLookupResult setToolTip:message];
387}
388
389- (void)onUsernameAvailabilityChangedWithNewAvailability:(BOOL)newAvailability
390{
391 self.isUserNameAvailable = newAvailability;
392}
393
394- (void)hideLookupSpinner
395{
396 [indicatorLookupResult setHidden:YES];
397}
398
399- (void)showLookupSpinner
400{
401 [ivLookupResult setHidden:YES];
402 [indicatorLookupResult setHidden:NO];
403 [indicatorLookupResult startAnimation:nil];
404}
405
406- (BOOL)lookupUserName
407{
408 [self showLookupSpinner];
409 QObject::disconnect(registeredNameFound);
410 registeredNameFound = QObject::connect(
411 &NameDirectory::instance(),
412 &NameDirectory::registeredNameFound,
413 [=] ( const Account* account, NameDirectory::LookupStatus status, const QString& address, const QString& name) {
414 NSLog(@"Name lookup ended");
415 lookupQueued = NO;
416 //If this is the username we are waiting for, we can disconnect.
417 if (name.compare(QString::fromNSString(usernameWaitingForLookupResult)) == 0) {
418 QObject::disconnect(registeredNameFound);
419 } else {
420 //Keep waiting...
421 return;
422 }
423
424 //We may now stop the spinner
425 [self hideLookupSpinner];
426
427 BOOL isAvailable = NO;
428 NSString* message;
429 switch(status)
430 {
431 case NameDirectory::LookupStatus::SUCCESS:
432 {
433 message = NSLocalizedString(@"The entered username is not available",
434 @"Text shown to user when his username is already registered");
435 isAvailable = NO;
436 break;
437 }
438 case NameDirectory::LookupStatus::NOT_FOUND:
439 {
440 message = NSLocalizedString(@"The entered username is available",
441 @"Text shown to user when his username is available to be registered");
442 isAvailable = YES;
443 break;
444 }
445 case NameDirectory::LookupStatus::INVALID_NAME:
446 {
Alexandre Lision5dc5d312016-11-10 10:41:37 -0500447 message = NSLocalizedString(@"The entered username is invalid. It must have at least 3 characters and contain only lowercase alphanumeric characters.",
Alexandre Lision882289b2016-10-31 16:10:39 -0400448 @"Text shown to user when his username is invalid to be registered");
449 isAvailable = NO;
450 break;
451 }
452 case NameDirectory::LookupStatus::ERROR:
453 default:
454 {
455 message = NSLocalizedString(@"Failed to perform lookup",
456 @"Text shown to user when an error occur at registration");
457 isAvailable = NO;
458 break;
459 }
460 }
461 [self showLookUpAvailable:isAvailable andText: message];
462 [self onUsernameAvailabilityChangedWithNewAvailability:isAvailable];
463
464 });
465
466 //Start the lookup in a second so that the UI dosen't seem to freeze
467 BOOL result = NameDirectory::instance().lookupName(nullptr, QString(), QString::fromNSString(usernameWaitingForLookupResult));
468
469}
470
Alexandre Lision5dc5d312016-11-10 10:41:37 -0500471#pragma mark - NSTextFieldDelegate delegate methods
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400472
Alexandre Lision882289b2016-10-31 16:10:39 -0400473- (void)controlTextDidChange:(NSNotification *)notif
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400474{
475 NSTextField* textField = [notif object];
Alexandre Lision882289b2016-10-31 16:10:39 -0400476 if (textField.tag != BLOCKCHAIN_NAME_TAG) {
477 return;
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400478 }
Alexandre Lision882289b2016-10-31 16:10:39 -0400479 NSString* alias = textField.stringValue;
480
481 [self showLookupSpinner];
482 [self onUsernameAvailabilityChangedWithNewAvailability:NO];
483 [NSObject cancelPreviousPerformRequestsWithTarget:self];
484 [self performSelector:@selector(lookUp:) withObject:alias afterDelay:0.5];
485}
486
487- (void) lookUp:(NSString*) name
488{
489 if (self.withBlockchain && !lookupQueued) {
490 usernameWaitingForLookupResult = name;
491 lookupQueued = YES;
492 [self lookupUserName];
493 }
494}
495
496
497+ (NSSet *)keyPathsForValuesAffectingUserNameAvailableORNotBlockchain
498{
499 return [NSSet setWithObjects: NSStringFromSelector(@selector(signUpBlockchainState)),
500 NSStringFromSelector(@selector(isUserNameAvailable)),
501 nil];
502}
503
504+ (NSSet *)keyPathsForValuesAffectingWithBlockchain
505{
506 return [NSSet setWithObjects: NSStringFromSelector(@selector(signUpBlockchainState)),
507 nil];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400508}
509
Loïc Siret3652cfb2016-10-27 10:12:07 -0400510+ (NSSet *)keyPathsForValuesAffectingIsPasswordValid
511{
512 return [NSSet setWithObjects:@"password", nil];
513}
514
515+ (NSSet *)keyPathsForValuesAffectingIsRepeatPasswordValid
516{
517 return [NSSet setWithObjects:@"password", @"repeatPassword", nil];
518}
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400519@end