blob: 9d0929b2fb3356730aea4e17e44379d8d1e59525 [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{
280 errorTimer = [NSTimer scheduledTimerWithTimeInterval:30
281 target:self
282 selector:@selector(didCreateFailed) userInfo:nil
283 repeats:NO];
284 stateChanged = QObject::connect(&AccountModel::instance(),
Alexandre Lision882289b2016-10-31 16:10:39 -0400285 &AccountModel::accountStateChanged,
286 [=](Account *account, const Account::RegistrationState state) {
287 switch(state){
288 case Account::RegistrationState::READY:
289 case Account::RegistrationState::TRYING:
290 case Account::RegistrationState::UNREGISTERED:{
291 accountToCreate << Account::EditAction::RELOAD;
292 QObject::disconnect(stateChanged);
293 //try to register username
294 if (self.signUpBlockchainState == NSOnState){
295 [self startNameRegistration:account];
296 } else {
297 [self.delegate didCreateAccountWithSuccess:YES];
298 }
299 break;
300 }
301 case Account::RegistrationState::ERROR:
302 QObject::disconnect(stateChanged);
303 [self.delegate didCreateAccountWithSuccess:NO];
304 break;
305 case Account::RegistrationState::INITIALIZING:
306 case Account::RegistrationState::COUNT__:{
307 //Do Nothing
308 break;
309 }
310 }
311 });
312}
313
314- (void) startNameRegistration:(Account*) account
315{
316 registrationEnded = QObject::connect(account,
317 &Account::nameRegistrationEnded,
318 [=] (NameDirectory::RegisterNameStatus status, const QString& name) {
319 QObject::disconnect(registrationEnded);
320 switch(status) {
321 case NameDirectory::RegisterNameStatus::WRONG_PASSWORD:
322 case NameDirectory::RegisterNameStatus::ALREADY_TAKEN:
323 case NameDirectory::RegisterNameStatus::NETWORK_ERROR: {
324 [self couldNotRegisterUsername];
325 break;
326 }
327 case NameDirectory::RegisterNameStatus::SUCCESS: {
328 break;
329 }
330 }
331
332 [self.delegate didCreateAccountWithSuccess:YES];
333 });
334 self.isUserNameAvailable = account->registerName(QString::fromNSString(self.password),
335 QString::fromNSString(self.registeredName));
336 if (!self.isUserNameAvailable){
337 NSLog(@"Could not initialize registerName operation");
338 QObject::disconnect(registrationEnded);
339 [self.delegate didCreateAccountWithSuccess:YES];
340 }
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400341}
342
343- (void)didCreateFailed
344{
345 [self.delegate didCreateAccountWithSuccess:NO];
346}
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 {
442 message = NSLocalizedString(@"The entered username is invalid. It must have at leat 3 characters and contains only lowercase alphanumeric characters.",
443 @"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
Loïc Siretfcb4ca62016-09-21 17:12:09 -0400466#pragma mark - NSOpenSavePanelDelegate delegate methods
467
468- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError **)outError
469{
470 return YES;
471}
472
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