blob: 44bed1350639f272d43f89c05ff1431f53518b98 [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
65 __unsafe_unretained IBOutlet NSButton* cbSignupRing;
66 __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
117 NSData* imgData = [[[ABAddressBook sharedAddressBook] me] imageData];
118 if (imgData != nil) {
119 [photoView setImage:[[NSImage alloc] initWithData:imgData]];
120 } else
121 [photoView setImage:[NSImage imageNamed:@"default_user_icon"]];
122
123 [photoView setWantsLayer: YES];
124 photoView.layer.cornerRadius = photoView.frame.size.width / 2;
125 photoView.layer.masksToBounds = YES;
Loïc Siret3652cfb2016-10-27 10:12:07 -0400126
127 [self display:creationView];
128}
129
130- (void)removeSubviews
131{
132 while ([self.view.subviews count] > 0){
133 [[self.view.subviews firstObject] removeFromSuperview];
134 }
135}
136
137- (void)display:(NSView *)view
138{
139 [self.delegate showView:view];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400140}
141
142- (IBAction)editPhoto:(id)sender
143{
Loïc Siret3652cfb2016-10-27 10:12:07 -0400144 auto pictureTaker = [IKPictureTaker pictureTaker];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400145
146 [pictureTaker beginPictureTakerSheetForWindow:[self.delegate window]
147 withDelegate:self
148 didEndSelector:@selector(pictureTakerDidEnd:returnCode:contextInfo:)
149 contextInfo:nil];
150
151}
152
153- (void)pictureTakerDidEnd:(IKPictureTaker *) picker
Alexandre Lision882289b2016-10-31 16:10:39 -0400154 returnCode:(NSInteger) code
155 contextInfo:(void*) contextInfo
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400156{
157 if (auto outputImage = [picker outputImage]) {
158 [photoView setImage:outputImage];
159 } else
160 [photoView setImage:[NSImage imageNamed:@"default_user_icon"]];
161}
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{
172 return [self.password isEqualToString:self.repeatPassword];
173}
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));
Alexandre Lision849514f2016-10-25 14:07:51 -0400239 } else {
240 auto defaultAvatar = [NSImage imageResize:[NSImage imageNamed:@"default_user_icon"] newSize:{100,100}];
241 p.loadFromData(QByteArray::fromNSData([defaultAvatar TIFFRepresentation]));
242 profile->person()->setPhoto(QVariant(p));
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400243 }
244 profile->save();
245 }
246
Loïc Siret3652cfb2016-10-27 10:12:07 -0400247 QModelIndex qIdx = AccountModel::instance().protocolModel()->selectionModel()->currentIndex();
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400248
249 [self setCallback];
250
251 [self performSelector:@selector(saveAccount) withObject:nil afterDelay:1];
252 [self registerDefaultPreferences];
253}
254
255/**
256 * Set default values for preferences
257 */
258- (void)registerDefaultPreferences
259{
260 // enable AutoStartup
261 LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
262 if (loginItemsRef == nil) return;
263 CFURLRef appUrl = (__bridge CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
264 LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, appUrl, NULL, NULL);
265 if (itemRef) CFRelease(itemRef);
266
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 {
445 message = NSLocalizedString(@"The entered username is invalid. It must have at leat 3 characters and contains only lowercase alphanumeric characters.",
446 @"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
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400469#pragma mark - NSOpenSavePanelDelegate delegate methods
470
471- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError **)outError
472{
473 return YES;
474}
475
Alexandre Lision882289b2016-10-31 16:10:39 -0400476- (void)controlTextDidChange:(NSNotification *)notif
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400477{
478 NSTextField* textField = [notif object];
Alexandre Lision882289b2016-10-31 16:10:39 -0400479 if (textField.tag != BLOCKCHAIN_NAME_TAG) {
480 return;
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400481 }
Alexandre Lision882289b2016-10-31 16:10:39 -0400482 NSString* alias = textField.stringValue;
483
484 [self showLookupSpinner];
485 [self onUsernameAvailabilityChangedWithNewAvailability:NO];
486 [NSObject cancelPreviousPerformRequestsWithTarget:self];
487 [self performSelector:@selector(lookUp:) withObject:alias afterDelay:0.5];
488}
489
490- (void) lookUp:(NSString*) name
491{
492 if (self.withBlockchain && !lookupQueued) {
493 usernameWaitingForLookupResult = name;
494 lookupQueued = YES;
495 [self lookupUserName];
496 }
497}
498
499
500+ (NSSet *)keyPathsForValuesAffectingUserNameAvailableORNotBlockchain
501{
502 return [NSSet setWithObjects: NSStringFromSelector(@selector(signUpBlockchainState)),
503 NSStringFromSelector(@selector(isUserNameAvailable)),
504 nil];
505}
506
507+ (NSSet *)keyPathsForValuesAffectingWithBlockchain
508{
509 return [NSSet setWithObjects: NSStringFromSelector(@selector(signUpBlockchainState)),
510 nil];
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400511}
512
Loïc Siret3652cfb2016-10-27 10:12:07 -0400513+ (NSSet *)keyPathsForValuesAffectingIsPasswordValid
514{
515 return [NSSet setWithObjects:@"password", nil];
516}
517
518+ (NSSet *)keyPathsForValuesAffectingIsRepeatPasswordValid
519{
520 return [NSSet setWithObjects:@"password", @"repeatPassword", nil];
521}
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400522@end