diff --git a/Classes/BITAuthenticationViewController.h b/Classes/BITAuthenticationViewController.h index becb55d0de..8782985283 100644 --- a/Classes/BITAuthenticationViewController.h +++ b/Classes/BITAuthenticationViewController.h @@ -8,8 +8,24 @@ #import @protocol BITAuthenticationViewControllerDelegate; +@class BITAuthenticator; -@interface BITAuthenticationViewController : UIViewController +@interface BITAuthenticationViewController : UITableViewController + +/** + * can be set to YES to also require the users password + * + * defaults to NO + */ +@property (nonatomic, assign) BOOL requirePassword; + +/** + * TODO: instead of passing the whole authenticator, we actually only need + * something to create and enqueue BITHTTPOperations + */ +@property (nonatomic, weak) BITAuthenticator *authenticator; + +@property (nonatomic, weak) id delegate; @end diff --git a/Classes/BITAuthenticationViewController.m b/Classes/BITAuthenticationViewController.m index 1a50c3ec77..ddaf5b9851 100644 --- a/Classes/BITAuthenticationViewController.m +++ b/Classes/BITAuthenticationViewController.m @@ -7,32 +7,280 @@ // #import "BITAuthenticationViewController.h" +#import "BITAuthenticator_Private.h" +#import "HockeySDKPrivate.h" +#import "HockeySDK.h" -@interface BITAuthenticationViewController () +@interface BITAuthenticationViewController () { + UIStatusBarStyle _statusBarStyle; +} + +@property (nonatomic, copy) NSString *email; +@property (nonatomic, copy) NSString *password; @end @implementation BITAuthenticationViewController -- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil -{ - self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; - if (self) { - // Custom initialization +- (id)initWithStyle:(UITableViewStyle)style { + self = [super initWithStyle:style]; + if (self) { + self.title = BITHockeyLocalizedString(@"HockeyAuthenticatorViewControllerTitle"); + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + [self.tableView setScrollEnabled:NO]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + _statusBarStyle = [[UIApplication sharedApplication] statusBarStyle]; + [[UIApplication sharedApplication] setStatusBarStyle:(self.navigationController.navigationBar.barStyle == UIBarStyleDefault) ? UIStatusBarStyleDefault : UIStatusBarStyleBlackOpaque]; + + // Do any additional setup after loading the view. + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel + target:self + action:@selector(dismissAction:)]; + + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave + target:self + action:@selector(saveAction:)]; + + self.navigationItem.rightBarButtonItem.enabled = [self allRequiredFieldsEntered]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + [[UIApplication sharedApplication] setStatusBarStyle:_statusBarStyle]; +} + +#pragma mark - UIViewController Rotation + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation { + return YES; +} + +#pragma mark - Private methods + +//TODO: extract from feedbackviewcontroller and move to common place +- (BOOL)validateEmail { + NSString *emailRegex = + @"(?:[a-z0-9!#$%\\&'*+/=?\\^_`{|}~-]+(?:\\.[a-z0-9!#$%\\&'*+/=?\\^_`{|}" + @"~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\" + @"x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-" + @"z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5" + @"]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-" + @"9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21" + @"-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"; + NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES[c] %@", emailRegex]; + + return [emailTest evaluateWithObject:self.email]; +} + +- (BOOL)allRequiredFieldsEntered { + if (self.requirePassword && [self.password length] == 0) + return NO; + + if ([self.email length] > 0 && ![self validateEmail]) + return NO; + + return YES; +} + +#pragma mark - Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + NSInteger rows = 1; + + if ([self requirePassword]) rows ++; + + return rows; +} + +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { + if (section == 0) { + return BITHockeyLocalizedString(@"HockeyAuthenticationDataDescription"); + } + + return nil; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *CellIdentifier = @"InputCell"; + + UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + if (cell == nil) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; + + cell.accessoryType = UITableViewCellAccessoryNone; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + cell.backgroundColor = [UIColor whiteColor]; + + UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(110, 10, self.view.frame.size.width - 110 - 35, 30)]; + if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) { + textField.autoresizingMask = UIViewAutoresizingFlexibleWidth; } - return self; + textField.adjustsFontSizeToFitWidth = YES; + textField.textColor = [UIColor blackColor]; + textField.backgroundColor = [UIColor lightGrayColor]; + + if (0 == [indexPath row]) { + textField.placeholder = BITHockeyLocalizedString(@"HockeyFeedbackUserDataEmailPlaceholder"); + textField.text = self.email; + + textField.keyboardType = UIKeyboardTypeEmailAddress; + if ([self requirePassword]) + textField.returnKeyType = UIReturnKeyNext; + else + textField.returnKeyType = UIReturnKeyDone; + + [textField addTarget:self action:@selector(userEmailEntered:) forControlEvents:UIControlEventEditingChanged]; + [textField becomeFirstResponder]; + } else { + textField.placeholder = BITHockeyLocalizedString(@"HockeyAuthenticatorViewControllerPasswordPlaceHolder"); + textField.text = self.password; + + textField.keyboardType = UIKeyboardTypeDefault; + textField.returnKeyType = UIReturnKeyDone; + textField.secureTextEntry = YES; + [textField addTarget:self action:@selector(userPasswordEntered:) forControlEvents:UIControlEventEditingChanged]; + } + + textField.backgroundColor = [UIColor whiteColor]; + textField.autocorrectionType = UITextAutocorrectionTypeNo; + textField.autocapitalizationType = UITextAutocapitalizationTypeNone; + textField.textAlignment = kBITTextLabelAlignmentLeft; + textField.delegate = self; + textField.tag = indexPath.row; + + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + [textField setEnabled: YES]; + + [cell addSubview:textField]; + } + + if (0 == [indexPath row]) { + cell.textLabel.text = BITHockeyLocalizedString(@"HockeyFeedbackUserDataEmail"); + } else { + cell.textLabel.text = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerPassword"); + } + + return cell; } -- (void)viewDidLoad -{ - [super viewDidLoad]; - // Do any additional setup after loading the view. + +- (void)userEmailEntered:(id)sender { + self.email = [(UITextField *)sender text]; + + self.navigationItem.rightBarButtonItem.enabled = [self allRequiredFieldsEntered]; } -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. +- (void)userPasswordEntered:(id)sender { + self.password = [(UITextField *)sender text]; + + self.navigationItem.rightBarButtonItem.enabled = [self allRequiredFieldsEntered]; } +#pragma mark - UITextFieldDelegate + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + NSInteger nextTag = textField.tag + 1; + + UIResponder* nextResponder = [self.view viewWithTag:nextTag]; + if (nextResponder) { + [nextResponder becomeFirstResponder]; + } else { + if ([self allRequiredFieldsEntered]) { + if ([textField isFirstResponder]) + [textField resignFirstResponder]; + + [self saveAction:nil]; + } + } + return NO; +} + +#pragma mark - Actions +- (void)dismissAction:(id)sender { + [self.delegate authenticationViewControllerDidCancel:self]; +} + +- (void)saveAction:(id)sender { + [self showLoginUI:YES]; + + NSString *authenticationEndpoint = @"authenticate"; + __weak typeof (self) weakSelf = self; + + [self.authenticator getPath:authenticationEndpoint + completion:^(BITHTTPOperation *operation, id response, NSError *error) { + typeof (self) strongSelf = weakSelf; + if(nil == response) { + //TODO think about alertview messages + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil + message:@"Failed to authenticate" + delegate:nil + cancelButtonTitle:BITHockeyLocalizedString(@"OK") + otherButtonTitles:nil]; + [alert show]; + } else { + NSError *authParseError = nil; + NSString *authToken = [strongSelf.class authenticationTokenFromReponse:response + error:&authParseError]; + if(nil == authToken) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil + message:@"Failed to authenticate" + delegate:nil + cancelButtonTitle:BITHockeyLocalizedString(@"OK") + otherButtonTitles:nil]; + [alert show]; + } else { + [strongSelf.delegate authenticationViewController:strongSelf authenticatedWithToken:authToken]; + } + } + [self showLoginUI:NO]; + }]; +} + +- (void) showLoginUI:(BOOL) enableLoginUI { + self.navigationItem.rightBarButtonItem.enabled = !enableLoginUI; + self.tableView.userInteractionEnabled = !enableLoginUI; +} + ++ (NSString *) authenticationTokenFromReponse:(id) response error:(NSError **) error { + NSParameterAssert(response); + + NSError *jsonParseError = nil; + id jsonObject = [NSJSONSerialization JSONObjectWithData:response + options:0 + error:&jsonParseError]; + if(nil == jsonObject) { + if(error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAPIServerReturnedInvalidRespone + userInfo:(jsonParseError ? @{NSUnderlyingErrorKey : jsonParseError} : nil)]; + } + return NO; + } + if(![jsonObject isKindOfClass:[NSDictionary class]]) { + if(error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAPIServerReturnedInvalidRespone + userInfo:nil]; + } + return NO; + } + + //TODO: add proper validation + return jsonObject[@"authToken"]; +} @end diff --git a/Classes/BITAuthenticator.m b/Classes/BITAuthenticator.m index 75e0a92ebb..732ab65623 100644 --- a/Classes/BITAuthenticator.m +++ b/Classes/BITAuthenticator.m @@ -159,12 +159,16 @@ static NSString* const kBITAuthenticatorLastAuthenticatedVersionKey = @"BITAuthe BITHockeyLog(@"Already authenticating. Ignoring request"); return; } - UIViewController *viewController = nil; + + BITAuthenticationViewController *viewController = [[BITAuthenticationViewController alloc] initWithStyle:UITableViewStyleGrouped]; + viewController.delegate = self; + viewController.authenticator = self; switch (self.authenticationType) { - case BITAuthenticatorAuthTypeEmail: case BITAuthenticatorAuthTypeEmailAndPassword: - viewController = [UIViewController new]; - //TODO + viewController.requirePassword = YES; + break; + case BITAuthenticatorAuthTypeEmail: + viewController.requirePassword = NO; break; } @@ -173,13 +177,16 @@ static NSString* const kBITAuthenticatorLastAuthenticatedVersionKey = @"BITAuthe _authenticationController = viewController; _authenticationCompletionBlock = completion; UIViewController *rootViewController = [self.findVisibleWindow rootViewController]; - [rootViewController presentModalViewController:_authenticationController + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:viewController]; + [rootViewController presentModalViewController:navController animated:YES]; } } #pragma mark - AuthenticationViewControllerDelegate - (void) authenticationViewControllerDidCancel:(UIViewController*) viewController { + [viewController dismissModalViewControllerAnimated:YES]; + _authenticationController = nil; self.authenticationToken = nil; NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain