blob: 3db504537030ac863f22a17b8a1b9517c1a83c62 [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"
Andreas Traczyk92c101e2018-07-03 14:43:53 -040045#import "utils.h"
Loïc Siretfcb4ca62016-09-21 17:12:09 -040046
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;
Kateryna Kostiuk87ae2bf2018-05-04 13:46:17 -040062 __unsafe_unretained IBOutlet NSImageView* addProfilePhotoImage;
Loïc Siret3652cfb2016-10-27 10:12:07 -040063
64 __unsafe_unretained IBOutlet NSProgressIndicator* progressBar;
Alexandre Lision882289b2016-10-31 16:10:39 -040065
Alexandre Lision882289b2016-10-31 16:10:39 -040066 __unsafe_unretained IBOutlet NSImageView* ivLookupResult;
67 __unsafe_unretained IBOutlet NSProgressIndicator* indicatorLookupResult;
68
69 __unsafe_unretained IBOutlet NSPopover* helpBlockchainContainer;
70 __unsafe_unretained IBOutlet NSPopover* helpPasswordContainer;
71
Loïc Siretfcb4ca62016-09-21 17:12:09 -040072 Account* accountToCreate;
73 NSTimer* errorTimer;
74 QMetaObject::Connection stateChanged;
Alexandre Lision882289b2016-10-31 16:10:39 -040075 QMetaObject::Connection registrationEnded;
76 QMetaObject::Connection registeredNameFound;
77
78 BOOL lookupQueued;
79 NSString* usernameWaitingForLookupResult;
Loïc Siretfcb4ca62016-09-21 17:12:09 -040080}
81
Alexandre Lision882289b2016-10-31 16:10:39 -040082NSInteger const DISPLAY_NAME_TAG = 1;
83NSInteger const BLOCKCHAIN_NAME_TAG = 2;
Loïc Siretfcb4ca62016-09-21 17:12:09 -040084
Loïc Siret3652cfb2016-10-27 10:12:07 -040085//ERROR CODE for textfields validations
86NSInteger const ERROR_PASSWORD_TOO_SHORT = -1;
87NSInteger const ERROR_REPEAT_MISMATCH = -2;
88
89
90- (BOOL)produceError:(NSError**)error withCode:(NSInteger)code andMessage:(NSString*)message
91{
92 if (error != NULL){
93 NSDictionary *errorDetail = @{NSLocalizedDescriptionKey: message};
94 *error = [NSError errorWithDomain:@"Input" code:code userInfo:errorDetail];
95 }
96 return NO;
97}
Loïc Siretfcb4ca62016-09-21 17:12:09 -040098
Alexandre Lision882289b2016-10-31 16:10:39 -040099- (IBAction)showBlockchainHelp:(id)sender
100{
101 [helpBlockchainContainer showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
102}
103
104- (IBAction)showPasswordHelp:(id)sender
105{
106 [helpPasswordContainer showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
107}
108
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400109- (void)show
110{
111 AppDelegate* appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
Alexandre Lision882289b2016-10-31 16:10:39 -0400112 [displayNameField setTag:DISPLAY_NAME_TAG];
113 [registeredNameField setTag:BLOCKCHAIN_NAME_TAG];
114 [displayNameField setStringValue: NSFullUserName()];
115 [self controlTextDidChange:[NSNotification notificationWithName:@"PlaceHolder" object:displayNameField]];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400116 [photoView setWantsLayer: YES];
117 photoView.layer.cornerRadius = photoView.frame.size.width / 2;
118 photoView.layer.masksToBounds = YES;
Alexandre Lisionc2ad6392016-11-08 11:20:34 -0500119 self.signUpBlockchainState = YES;
120 [self toggleSignupRing:nil];
Kateryna Kostiuk87ae2bf2018-05-04 13:46:17 -0400121 [addProfilePhotoImage setWantsLayer: YES];
122 [photoView setBordered:YES];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400123
124 [self display:creationView];
125}
126
127- (void)removeSubviews
128{
129 while ([self.view.subviews count] > 0){
130 [[self.view.subviews firstObject] removeFromSuperview];
131 }
132}
133
134- (void)display:(NSView *)view
135{
136 [self.delegate showView:view];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400137}
138
139- (IBAction)editPhoto:(id)sender
140{
Loïc Siret3652cfb2016-10-27 10:12:07 -0400141 auto pictureTaker = [IKPictureTaker pictureTaker];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400142
143 [pictureTaker beginPictureTakerSheetForWindow:[self.delegate window]
144 withDelegate:self
145 didEndSelector:@selector(pictureTakerDidEnd:returnCode:contextInfo:)
146 contextInfo:nil];
147
148}
149
150- (void)pictureTakerDidEnd:(IKPictureTaker *) picker
Alexandre Lision882289b2016-10-31 16:10:39 -0400151 returnCode:(NSInteger) code
152 contextInfo:(void*) contextInfo
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400153{
154 if (auto outputImage = [picker outputImage]) {
Kateryna Kostiuk87ae2bf2018-05-04 13:46:17 -0400155 [photoView setBordered:NO];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400156 [photoView setImage:outputImage];
Kateryna Kostiuk87ae2bf2018-05-04 13:46:17 -0400157 [addProfilePhotoImage setHidden:YES];
158 } else if(!photoView.image) {
159 [photoView setBordered:YES];
160 [addProfilePhotoImage setHidden:NO];
Alexandre Lisionc2ad6392016-11-08 11:20:34 -0500161 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400162}
163
Loïc Siret3652cfb2016-10-27 10:12:07 -0400164#pragma mark - Input validation
Alexandre Lision882289b2016-10-31 16:10:39 -0400165
Loïc Siret3652cfb2016-10-27 10:12:07 -0400166- (BOOL)isPasswordValid
167{
168 return self.password.length >= 6;
169}
170
171- (BOOL)isRepeatPasswordValid
172{
Anthony Léonard24110e82017-09-15 16:29:11 -0400173 return [self.password isEqualToString:self.repeatPassword] || ([self.password length] == 0 && [self.repeatPassword length] == 0);
Loïc Siret3652cfb2016-10-27 10:12:07 -0400174}
175
176- (BOOL)validateRepeatPassword:(NSError **)error
177{
178 if (!self.isRepeatPasswordValid){
179 return [self produceError:error
180 withCode:ERROR_REPEAT_MISMATCH
181 andMessage:NSLocalizedString(@"Passwords don't match",
Alexandre Lision882289b2016-10-31 16:10:39 -0400182 @"Indication for user")];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400183 }
184 return YES;
185}
186
187- (BOOL)validatePassword:(NSError **)error
188{
189 if (!self.isRepeatPasswordValid){
190 return [self produceError:error
191 withCode:ERROR_PASSWORD_TOO_SHORT
192 andMessage:NSLocalizedString(@"Password is too short",
Alexandre Lision882289b2016-10-31 16:10:39 -0400193 @"Indication for user")];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400194 }
195 return YES;
196}
197
198- (BOOL)validateUserInputPassword:(NSError **)error
199{
200 return [self validatePassword:error] && [self validateRepeatPassword:error];
201}
202
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400203- (IBAction)createRingAccount:(id)sender
204{
Loïc Siret3652cfb2016-10-27 10:12:07 -0400205 NSError *error = nil;
206 if (![self validateUserInputPassword:&error]){
207 NSAlert* alert = [NSAlert alertWithMessageText:[error localizedDescription]
208 defaultButton:NSLocalizedString(@"Revise Input",
209 @"Button title")
210 alternateButton:nil
211 otherButton:nil
212 informativeTextWithFormat:@"%@",error];
213
214 [alert beginSheetModalForWindow:passwordField.window
215 modalDelegate:nil
216 didEndSelector:NULL
217 contextInfo:NULL];
218
219 return;
220 }
221
222 [self display:loadingView];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400223 [progressBar startAnimation:nil];
Loïc Siret3652cfb2016-10-27 10:12:07 -0400224
Alexandre Lision882289b2016-10-31 16:10:39 -0400225 NSString* displayName = displayNameField.stringValue;
226 if ([displayName isEqualToString:@""]) {
227 displayName = NSLocalizedString(@"Unknown", @"Name used when user leave field empty");
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400228 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400229
Alexandre Lision882289b2016-10-31 16:10:39 -0400230 accountToCreate = AccountModel::instance().add(QString::fromNSString(displayName), Account::Protocol::RING);
231 accountToCreate->setAlias([displayName UTF8String]);
232 accountToCreate->setDisplayName([displayName UTF8String]);
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400233
234 if (auto profile = ProfileModel::instance().selectedProfile()) {
Alexandre Lision882289b2016-10-31 16:10:39 -0400235 profile->person()->setFormattedName([displayName UTF8String]);
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400236 QPixmap p;
237 auto smallImg = [NSImage imageResize:[photoView image] newSize:{100,100}];
238 if (p.loadFromData(QByteArray::fromNSData([smallImg TIFFRepresentation]))) {
239 profile->person()->setPhoto(QVariant(p));
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400240 }
241 profile->save();
Kateryna Kostiuk87ae2bf2018-05-04 13:46:17 -0400242
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400243 }
244
Loïc Siret3652cfb2016-10-27 10:12:07 -0400245 QModelIndex qIdx = AccountModel::instance().protocolModel()->selectionModel()->currentIndex();
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400246
247 [self setCallback];
248
249 [self performSelector:@selector(saveAccount) withObject:nil afterDelay:1];
250 [self registerDefaultPreferences];
251}
252
253/**
254 * Set default values for preferences
255 */
256- (void)registerDefaultPreferences
257{
Andreas Traczyk92c101e2018-07-03 14:43:53 -0400258 if (!appSandboxed()) {
259 // enable AutoStartup
260 LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
261 if (loginItemsRef == nil) return;
262 CFURLRef appUrl = (__bridge CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
263 LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, appUrl, NULL, NULL);
264 if (itemRef) CFRelease(itemRef);
265 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400266
267 // enable Notifications
268 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:Preferences::Notifications];
269}
270
271- (void)saveAccount
272{
273 accountToCreate->setArchivePassword(QString::fromNSString(passwordField.stringValue));
274 accountToCreate->setUpnpEnabled(YES); // Always active upnp
275 accountToCreate << Account::EditAction::SAVE;
276}
277
278- (void)setCallback
279{
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400280 stateChanged = QObject::connect(&AccountModel::instance(),
Alexandre Lision882289b2016-10-31 16:10:39 -0400281 &AccountModel::accountStateChanged,
282 [=](Account *account, const Account::RegistrationState state) {
283 switch(state){
284 case Account::RegistrationState::READY:
285 case Account::RegistrationState::TRYING:
286 case Account::RegistrationState::UNREGISTERED:{
287 accountToCreate << Account::EditAction::RELOAD;
288 QObject::disconnect(stateChanged);
289 //try to register username
290 if (self.signUpBlockchainState == NSOnState){
291 [self startNameRegistration:account];
292 } else {
293 [self.delegate didCreateAccountWithSuccess:YES];
294 }
295 break;
296 }
297 case Account::RegistrationState::ERROR:
298 QObject::disconnect(stateChanged);
299 [self.delegate didCreateAccountWithSuccess:NO];
300 break;
301 case Account::RegistrationState::INITIALIZING:
302 case Account::RegistrationState::COUNT__:{
303 //Do Nothing
304 break;
305 }
306 }
307 });
308}
309
310- (void) startNameRegistration:(Account*) account
311{
Alexandre Lisione857c0c2016-11-03 10:13:00 -0400312 // Dismiss this screen if after 30 seconds the name is still not registered
313 errorTimer = [NSTimer scheduledTimerWithTimeInterval:30
314 target:self
315 selector:@selector(nameRegistrationTimeout) userInfo:nil
316 repeats:NO];
Alexandre Lision882289b2016-10-31 16:10:39 -0400317 registrationEnded = QObject::connect(account,
318 &Account::nameRegistrationEnded,
319 [=] (NameDirectory::RegisterNameStatus status, const QString& name) {
320 QObject::disconnect(registrationEnded);
321 switch(status) {
322 case NameDirectory::RegisterNameStatus::WRONG_PASSWORD:
323 case NameDirectory::RegisterNameStatus::ALREADY_TAKEN:
324 case NameDirectory::RegisterNameStatus::NETWORK_ERROR: {
325 [self couldNotRegisterUsername];
326 break;
327 }
328 case NameDirectory::RegisterNameStatus::SUCCESS: {
329 break;
330 }
331 }
332
333 [self.delegate didCreateAccountWithSuccess:YES];
334 });
335 self.isUserNameAvailable = account->registerName(QString::fromNSString(self.password),
336 QString::fromNSString(self.registeredName));
337 if (!self.isUserNameAvailable){
338 NSLog(@"Could not initialize registerName operation");
339 QObject::disconnect(registrationEnded);
340 [self.delegate didCreateAccountWithSuccess:YES];
341 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400342}
343
Alexandre Lisione857c0c2016-11-03 10:13:00 -0400344- (void)nameRegistrationTimeout
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400345{
Alexandre Lisione857c0c2016-11-03 10:13:00 -0400346 // This callback is used when registration takes more than 30 seconds
347 // It skips the wizard and brings the main window
348 [self.delegate didCreateAccountWithSuccess:YES];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400349}
350
351- (IBAction)cancel:(id)sender
352{
353 [self.delegate didCreateAccountWithSuccess:NO];
354}
355
Alexandre Lision882289b2016-10-31 16:10:39 -0400356#pragma mark - UserNameRegistration delegate methods
357
358- (IBAction)toggleSignupRing:(id)sender
359{
360 if (self.withBlockchain) {
361 [self lookupUserName];
362 }
363}
364
365- (void)couldNotRegisterUsername
366{
367 // Do nothing
368}
369
370- (BOOL)withBlockchain
371{
372 return self.signUpBlockchainState == NSOnState;
373}
374
375- (BOOL)userNameAvailableORNotBlockchain
376{
377 return !self.withBlockchain || (self.registeredName.length > 0 && self.isUserNameAvailable);
378}
379
380- (void)showLookUpAvailable:(BOOL)available andText:(NSString *)message
381{
382 [ivLookupResult setImage:[NSImage imageNamed:(available?@"ic_action_accept":@"ic_action_cancel")]] ;
383 [ivLookupResult setHidden:NO];
384 [ivLookupResult setToolTip:message];
385}
386
387- (void)onUsernameAvailabilityChangedWithNewAvailability:(BOOL)newAvailability
388{
389 self.isUserNameAvailable = newAvailability;
390}
391
392- (void)hideLookupSpinner
393{
394 [indicatorLookupResult setHidden:YES];
395}
396
397- (void)showLookupSpinner
398{
399 [ivLookupResult setHidden:YES];
400 [indicatorLookupResult setHidden:NO];
401 [indicatorLookupResult startAnimation:nil];
402}
403
404- (BOOL)lookupUserName
405{
406 [self showLookupSpinner];
407 QObject::disconnect(registeredNameFound);
408 registeredNameFound = QObject::connect(
409 &NameDirectory::instance(),
410 &NameDirectory::registeredNameFound,
411 [=] ( const Account* account, NameDirectory::LookupStatus status, const QString& address, const QString& name) {
412 NSLog(@"Name lookup ended");
413 lookupQueued = NO;
414 //If this is the username we are waiting for, we can disconnect.
415 if (name.compare(QString::fromNSString(usernameWaitingForLookupResult)) == 0) {
416 QObject::disconnect(registeredNameFound);
417 } else {
418 //Keep waiting...
419 return;
420 }
421
422 //We may now stop the spinner
423 [self hideLookupSpinner];
424
425 BOOL isAvailable = NO;
426 NSString* message;
427 switch(status)
428 {
429 case NameDirectory::LookupStatus::SUCCESS:
430 {
431 message = NSLocalizedString(@"The entered username is not available",
432 @"Text shown to user when his username is already registered");
433 isAvailable = NO;
434 break;
435 }
436 case NameDirectory::LookupStatus::NOT_FOUND:
437 {
438 message = NSLocalizedString(@"The entered username is available",
439 @"Text shown to user when his username is available to be registered");
440 isAvailable = YES;
441 break;
442 }
443 case NameDirectory::LookupStatus::INVALID_NAME:
444 {
Alexandre Lision5dc5d312016-11-10 10:41:37 -0500445 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 -0400446 @"Text shown to user when his username is invalid to be registered");
447 isAvailable = NO;
448 break;
449 }
450 case NameDirectory::LookupStatus::ERROR:
451 default:
452 {
453 message = NSLocalizedString(@"Failed to perform lookup",
454 @"Text shown to user when an error occur at registration");
455 isAvailable = NO;
456 break;
457 }
458 }
459 [self showLookUpAvailable:isAvailable andText: message];
460 [self onUsernameAvailabilityChangedWithNewAvailability:isAvailable];
461
462 });
463
464 //Start the lookup in a second so that the UI dosen't seem to freeze
465 BOOL result = NameDirectory::instance().lookupName(nullptr, QString(), QString::fromNSString(usernameWaitingForLookupResult));
466
467}
468
Alexandre Lision5dc5d312016-11-10 10:41:37 -0500469#pragma mark - NSTextFieldDelegate delegate methods
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400470
Alexandre Lision882289b2016-10-31 16:10:39 -0400471- (void)controlTextDidChange:(NSNotification *)notif
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400472{
473 NSTextField* textField = [notif object];
Alexandre Lision882289b2016-10-31 16:10:39 -0400474 if (textField.tag != BLOCKCHAIN_NAME_TAG) {
475 return;
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400476 }
Alexandre Lision882289b2016-10-31 16:10:39 -0400477 NSString* alias = textField.stringValue;
478
479 [self showLookupSpinner];
480 [self onUsernameAvailabilityChangedWithNewAvailability:NO];
481 [NSObject cancelPreviousPerformRequestsWithTarget:self];
482 [self performSelector:@selector(lookUp:) withObject:alias afterDelay:0.5];
483}
484
485- (void) lookUp:(NSString*) name
486{
487 if (self.withBlockchain && !lookupQueued) {
488 usernameWaitingForLookupResult = name;
489 lookupQueued = YES;
490 [self lookupUserName];
491 }
492}
493
494
495+ (NSSet *)keyPathsForValuesAffectingUserNameAvailableORNotBlockchain
496{
497 return [NSSet setWithObjects: NSStringFromSelector(@selector(signUpBlockchainState)),
498 NSStringFromSelector(@selector(isUserNameAvailable)),
499 nil];
500}
501
502+ (NSSet *)keyPathsForValuesAffectingWithBlockchain
503{
504 return [NSSet setWithObjects: NSStringFromSelector(@selector(signUpBlockchainState)),
505 nil];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400506}
507
Loïc Siret3652cfb2016-10-27 10:12:07 -0400508+ (NSSet *)keyPathsForValuesAffectingIsPasswordValid
509{
510 return [NSSet setWithObjects:@"password", nil];
511}
512
513+ (NSSet *)keyPathsForValuesAffectingIsRepeatPasswordValid
514{
515 return [NSSet setWithObjects:@"password", @"repeatPassword", nil];
516}
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400517@end