blob: 27f153f80b479fe81824afcc3ff26185516b82ed [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
Loïc Siretfcb4ca62016-09-21 17:12:09 -040024#import <Quartz/Quartz.h>
25
26//Qt
27#import <QUrl>
28#import <QPixmap>
29
30//LRC
31#import <accountmodel.h>
32#import <protocolmodel.h>
33#import <profilemodel.h>
34#import <QItemSelectionModel>
35#import <account.h>
36#import <certificate.h>
37#import <profilemodel.h>
38#import <profile.h>
39#import <person.h>
40
41#import "AppDelegate.h"
42#import "Constants.h"
43#import "views/NSImage+Extensions.h"
44#import "views/NSColor+RingTheme.h"
45
46@interface RingWizardNewAccountVC ()
47@end
48
49@implementation RingWizardNewAccountVC
50{
Loïc Siret3652cfb2016-10-27 10:12:07 -040051 __unsafe_unretained IBOutlet NSView* loadingView;
52 __unsafe_unretained IBOutlet NSView* creationView;
53
Loïc Siretfcb4ca62016-09-21 17:12:09 -040054 __unsafe_unretained IBOutlet NSButton* photoView;
Alexandre Lision882289b2016-10-31 16:10:39 -040055 __unsafe_unretained IBOutlet NSTextField* displayNameField;
56 __unsafe_unretained IBOutlet NSTextField* registeredNameField;
Loïc Siretfcb4ca62016-09-21 17:12:09 -040057 __unsafe_unretained IBOutlet NSSecureTextField* passwordField;
Loïc Siret3652cfb2016-10-27 10:12:07 -040058 __unsafe_unretained IBOutlet NSSecureTextField* passwordRepeatField;
Loïc Siret3652cfb2016-10-27 10:12:07 -040059 __unsafe_unretained IBOutlet NSImageView* passwordCheck;
60 __unsafe_unretained IBOutlet NSImageView* passwordRepeatCheck;
Kateryna Kostiuk87ae2bf2018-05-04 13:46:17 -040061 __unsafe_unretained IBOutlet NSImageView* addProfilePhotoImage;
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 [photoView setWantsLayer: YES];
116 photoView.layer.cornerRadius = photoView.frame.size.width / 2;
117 photoView.layer.masksToBounds = YES;
Alexandre Lisionc2ad6392016-11-08 11:20:34 -0500118 self.signUpBlockchainState = YES;
119 [self toggleSignupRing:nil];
Kateryna Kostiuk87ae2bf2018-05-04 13:46:17 -0400120 [addProfilePhotoImage setWantsLayer: YES];
121 [photoView setBordered:YES];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400122
123 [self display:creationView];
124}
125
126- (void)removeSubviews
127{
128 while ([self.view.subviews count] > 0){
129 [[self.view.subviews firstObject] removeFromSuperview];
130 }
131}
132
133- (void)display:(NSView *)view
134{
135 [self.delegate showView:view];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400136}
137
138- (IBAction)editPhoto:(id)sender
139{
Loïc Siret3652cfb2016-10-27 10:12:07 -0400140 auto pictureTaker = [IKPictureTaker pictureTaker];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400141
142 [pictureTaker beginPictureTakerSheetForWindow:[self.delegate window]
143 withDelegate:self
144 didEndSelector:@selector(pictureTakerDidEnd:returnCode:contextInfo:)
145 contextInfo:nil];
146
147}
148
149- (void)pictureTakerDidEnd:(IKPictureTaker *) picker
Alexandre Lision882289b2016-10-31 16:10:39 -0400150 returnCode:(NSInteger) code
151 contextInfo:(void*) contextInfo
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400152{
153 if (auto outputImage = [picker outputImage]) {
Kateryna Kostiuk87ae2bf2018-05-04 13:46:17 -0400154 [photoView setBordered:NO];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400155 [photoView setImage:outputImage];
Kateryna Kostiuk87ae2bf2018-05-04 13:46:17 -0400156 [addProfilePhotoImage setHidden:YES];
157 } else if(!photoView.image) {
158 [photoView setBordered:YES];
159 [addProfilePhotoImage setHidden:NO];
Alexandre Lisionc2ad6392016-11-08 11:20:34 -0500160 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400161}
162
Loïc Siret3652cfb2016-10-27 10:12:07 -0400163#pragma mark - Input validation
Alexandre Lision882289b2016-10-31 16:10:39 -0400164
Loïc Siret3652cfb2016-10-27 10:12:07 -0400165- (BOOL)isPasswordValid
166{
167 return self.password.length >= 6;
168}
169
170- (BOOL)isRepeatPasswordValid
171{
Anthony Léonard24110e82017-09-15 16:29:11 -0400172 return [self.password isEqualToString:self.repeatPassword] || ([self.password length] == 0 && [self.repeatPassword length] == 0);
Loïc Siret3652cfb2016-10-27 10:12:07 -0400173}
174
175- (BOOL)validateRepeatPassword:(NSError **)error
176{
177 if (!self.isRepeatPasswordValid){
178 return [self produceError:error
179 withCode:ERROR_REPEAT_MISMATCH
180 andMessage:NSLocalizedString(@"Passwords don't match",
Alexandre Lision882289b2016-10-31 16:10:39 -0400181 @"Indication for user")];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400182 }
183 return YES;
184}
185
186- (BOOL)validatePassword:(NSError **)error
187{
188 if (!self.isRepeatPasswordValid){
189 return [self produceError:error
190 withCode:ERROR_PASSWORD_TOO_SHORT
191 andMessage:NSLocalizedString(@"Password is too short",
Alexandre Lision882289b2016-10-31 16:10:39 -0400192 @"Indication for user")];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400193 }
194 return YES;
195}
196
197- (BOOL)validateUserInputPassword:(NSError **)error
198{
199 return [self validatePassword:error] && [self validateRepeatPassword:error];
200}
201
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400202- (IBAction)createRingAccount:(id)sender
203{
Loïc Siret3652cfb2016-10-27 10:12:07 -0400204 NSError *error = nil;
205 if (![self validateUserInputPassword:&error]){
206 NSAlert* alert = [NSAlert alertWithMessageText:[error localizedDescription]
207 defaultButton:NSLocalizedString(@"Revise Input",
208 @"Button title")
209 alternateButton:nil
210 otherButton:nil
211 informativeTextWithFormat:@"%@",error];
212
213 [alert beginSheetModalForWindow:passwordField.window
214 modalDelegate:nil
215 didEndSelector:NULL
216 contextInfo:NULL];
217
218 return;
219 }
220
221 [self display:loadingView];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400222 [progressBar startAnimation:nil];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400223
Alexandre Lision882289b2016-10-31 16:10:39 -0400224 NSString* displayName = displayNameField.stringValue;
225 if ([displayName isEqualToString:@""]) {
226 displayName = NSLocalizedString(@"Unknown", @"Name used when user leave field empty");
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400227 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400228
Alexandre Lision882289b2016-10-31 16:10:39 -0400229 accountToCreate = AccountModel::instance().add(QString::fromNSString(displayName), Account::Protocol::RING);
230 accountToCreate->setAlias([displayName UTF8String]);
231 accountToCreate->setDisplayName([displayName UTF8String]);
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400232
233 if (auto profile = ProfileModel::instance().selectedProfile()) {
Alexandre Lision882289b2016-10-31 16:10:39 -0400234 profile->person()->setFormattedName([displayName UTF8String]);
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400235 QPixmap p;
236 auto smallImg = [NSImage imageResize:[photoView image] newSize:{100,100}];
237 if (p.loadFromData(QByteArray::fromNSData([smallImg TIFFRepresentation]))) {
238 profile->person()->setPhoto(QVariant(p));
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400239 }
240 profile->save();
Kateryna Kostiuk87ae2bf2018-05-04 13:46:17 -0400241
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400242 }
243
Loïc Siret3652cfb2016-10-27 10:12:07 -0400244 QModelIndex qIdx = AccountModel::instance().protocolModel()->selectionModel()->currentIndex();
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400245
246 [self setCallback];
247
248 [self performSelector:@selector(saveAccount) withObject:nil afterDelay:1];
249 [self registerDefaultPreferences];
250}
251
252/**
253 * Set default values for preferences
254 */
255- (void)registerDefaultPreferences
256{
257 // enable AutoStartup
258 LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
259 if (loginItemsRef == nil) return;
260 CFURLRef appUrl = (__bridge CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
261 LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, appUrl, NULL, NULL);
262 if (itemRef) CFRelease(itemRef);
263
264 // enable Notifications
265 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:Preferences::Notifications];
266}
267
268- (void)saveAccount
269{
270 accountToCreate->setArchivePassword(QString::fromNSString(passwordField.stringValue));
271 accountToCreate->setUpnpEnabled(YES); // Always active upnp
272 accountToCreate << Account::EditAction::SAVE;
273}
274
275- (void)setCallback
276{
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400277 stateChanged = QObject::connect(&AccountModel::instance(),
Alexandre Lision882289b2016-10-31 16:10:39 -0400278 &AccountModel::accountStateChanged,
279 [=](Account *account, const Account::RegistrationState state) {
280 switch(state){
281 case Account::RegistrationState::READY:
282 case Account::RegistrationState::TRYING:
283 case Account::RegistrationState::UNREGISTERED:{
284 accountToCreate << Account::EditAction::RELOAD;
285 QObject::disconnect(stateChanged);
286 //try to register username
287 if (self.signUpBlockchainState == NSOnState){
288 [self startNameRegistration:account];
289 } else {
290 [self.delegate didCreateAccountWithSuccess:YES];
291 }
292 break;
293 }
294 case Account::RegistrationState::ERROR:
295 QObject::disconnect(stateChanged);
296 [self.delegate didCreateAccountWithSuccess:NO];
297 break;
298 case Account::RegistrationState::INITIALIZING:
299 case Account::RegistrationState::COUNT__:{
300 //Do Nothing
301 break;
302 }
303 }
304 });
305}
306
307- (void) startNameRegistration:(Account*) account
308{
Alexandre Lisione857c0c2016-11-03 10:13:00 -0400309 // Dismiss this screen if after 30 seconds the name is still not registered
310 errorTimer = [NSTimer scheduledTimerWithTimeInterval:30
311 target:self
312 selector:@selector(nameRegistrationTimeout) userInfo:nil
313 repeats:NO];
Alexandre Lision882289b2016-10-31 16:10:39 -0400314 registrationEnded = QObject::connect(account,
315 &Account::nameRegistrationEnded,
316 [=] (NameDirectory::RegisterNameStatus status, const QString& name) {
317 QObject::disconnect(registrationEnded);
318 switch(status) {
319 case NameDirectory::RegisterNameStatus::WRONG_PASSWORD:
320 case NameDirectory::RegisterNameStatus::ALREADY_TAKEN:
321 case NameDirectory::RegisterNameStatus::NETWORK_ERROR: {
322 [self couldNotRegisterUsername];
323 break;
324 }
325 case NameDirectory::RegisterNameStatus::SUCCESS: {
326 break;
327 }
328 }
329
330 [self.delegate didCreateAccountWithSuccess:YES];
331 });
332 self.isUserNameAvailable = account->registerName(QString::fromNSString(self.password),
333 QString::fromNSString(self.registeredName));
334 if (!self.isUserNameAvailable){
335 NSLog(@"Could not initialize registerName operation");
336 QObject::disconnect(registrationEnded);
337 [self.delegate didCreateAccountWithSuccess:YES];
338 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400339}
340
Alexandre Lisione857c0c2016-11-03 10:13:00 -0400341- (void)nameRegistrationTimeout
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400342{
Alexandre Lisione857c0c2016-11-03 10:13:00 -0400343 // This callback is used when registration takes more than 30 seconds
344 // It skips the wizard and brings the main window
345 [self.delegate didCreateAccountWithSuccess:YES];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400346}
347
348- (IBAction)cancel:(id)sender
349{
350 [self.delegate didCreateAccountWithSuccess:NO];
351}
352
Alexandre Lision882289b2016-10-31 16:10:39 -0400353#pragma mark - UserNameRegistration delegate methods
354
355- (IBAction)toggleSignupRing:(id)sender
356{
357 if (self.withBlockchain) {
358 [self lookupUserName];
359 }
360}
361
362- (void)couldNotRegisterUsername
363{
364 // Do nothing
365}
366
367- (BOOL)withBlockchain
368{
369 return self.signUpBlockchainState == NSOnState;
370}
371
372- (BOOL)userNameAvailableORNotBlockchain
373{
374 return !self.withBlockchain || (self.registeredName.length > 0 && self.isUserNameAvailable);
375}
376
377- (void)showLookUpAvailable:(BOOL)available andText:(NSString *)message
378{
379 [ivLookupResult setImage:[NSImage imageNamed:(available?@"ic_action_accept":@"ic_action_cancel")]] ;
380 [ivLookupResult setHidden:NO];
381 [ivLookupResult setToolTip:message];
382}
383
384- (void)onUsernameAvailabilityChangedWithNewAvailability:(BOOL)newAvailability
385{
386 self.isUserNameAvailable = newAvailability;
387}
388
389- (void)hideLookupSpinner
390{
391 [indicatorLookupResult setHidden:YES];
392}
393
394- (void)showLookupSpinner
395{
396 [ivLookupResult setHidden:YES];
397 [indicatorLookupResult setHidden:NO];
398 [indicatorLookupResult startAnimation:nil];
399}
400
401- (BOOL)lookupUserName
402{
403 [self showLookupSpinner];
404 QObject::disconnect(registeredNameFound);
405 registeredNameFound = QObject::connect(
406 &NameDirectory::instance(),
407 &NameDirectory::registeredNameFound,
408 [=] ( const Account* account, NameDirectory::LookupStatus status, const QString& address, const QString& name) {
409 NSLog(@"Name lookup ended");
410 lookupQueued = NO;
411 //If this is the username we are waiting for, we can disconnect.
412 if (name.compare(QString::fromNSString(usernameWaitingForLookupResult)) == 0) {
413 QObject::disconnect(registeredNameFound);
414 } else {
415 //Keep waiting...
416 return;
417 }
418
419 //We may now stop the spinner
420 [self hideLookupSpinner];
421
422 BOOL isAvailable = NO;
423 NSString* message;
424 switch(status)
425 {
426 case NameDirectory::LookupStatus::SUCCESS:
427 {
428 message = NSLocalizedString(@"The entered username is not available",
429 @"Text shown to user when his username is already registered");
430 isAvailable = NO;
431 break;
432 }
433 case NameDirectory::LookupStatus::NOT_FOUND:
434 {
435 message = NSLocalizedString(@"The entered username is available",
436 @"Text shown to user when his username is available to be registered");
437 isAvailable = YES;
438 break;
439 }
440 case NameDirectory::LookupStatus::INVALID_NAME:
441 {
Alexandre Lision5dc5d312016-11-10 10:41:37 -0500442 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 -0400443 @"Text shown to user when his username is invalid to be registered");
444 isAvailable = NO;
445 break;
446 }
447 case NameDirectory::LookupStatus::ERROR:
448 default:
449 {
450 message = NSLocalizedString(@"Failed to perform lookup",
451 @"Text shown to user when an error occur at registration");
452 isAvailable = NO;
453 break;
454 }
455 }
456 [self showLookUpAvailable:isAvailable andText: message];
457 [self onUsernameAvailabilityChangedWithNewAvailability:isAvailable];
458
459 });
460
461 //Start the lookup in a second so that the UI dosen't seem to freeze
462 BOOL result = NameDirectory::instance().lookupName(nullptr, QString(), QString::fromNSString(usernameWaitingForLookupResult));
463
464}
465
Alexandre Lision5dc5d312016-11-10 10:41:37 -0500466#pragma mark - NSTextFieldDelegate delegate methods
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400467
Alexandre Lision882289b2016-10-31 16:10:39 -0400468- (void)controlTextDidChange:(NSNotification *)notif
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400469{
470 NSTextField* textField = [notif object];
Alexandre Lision882289b2016-10-31 16:10:39 -0400471 if (textField.tag != BLOCKCHAIN_NAME_TAG) {
472 return;
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400473 }
Alexandre Lision882289b2016-10-31 16:10:39 -0400474 NSString* alias = textField.stringValue;
475
476 [self showLookupSpinner];
477 [self onUsernameAvailabilityChangedWithNewAvailability:NO];
478 [NSObject cancelPreviousPerformRequestsWithTarget:self];
479 [self performSelector:@selector(lookUp:) withObject:alias afterDelay:0.5];
480}
481
482- (void) lookUp:(NSString*) name
483{
484 if (self.withBlockchain && !lookupQueued) {
485 usernameWaitingForLookupResult = name;
486 lookupQueued = YES;
487 [self lookupUserName];
488 }
489}
490
491
492+ (NSSet *)keyPathsForValuesAffectingUserNameAvailableORNotBlockchain
493{
494 return [NSSet setWithObjects: NSStringFromSelector(@selector(signUpBlockchainState)),
495 NSStringFromSelector(@selector(isUserNameAvailable)),
496 nil];
497}
498
499+ (NSSet *)keyPathsForValuesAffectingWithBlockchain
500{
501 return [NSSet setWithObjects: NSStringFromSelector(@selector(signUpBlockchainState)),
502 nil];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400503}
504
Loïc Siret3652cfb2016-10-27 10:12:07 -0400505+ (NSSet *)keyPathsForValuesAffectingIsPasswordValid
506{
507 return [NSSet setWithObjects:@"password", nil];
508}
509
510+ (NSSet *)keyPathsForValuesAffectingIsRepeatPasswordValid
511{
512 return [NSSet setWithObjects:@"password", @"repeatPassword", nil];
513}
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400514@end