// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "CodeVerificationViewController.h" #import "AppDelegate.h" #import "RPAccountManager.h" #import "Signal-Swift.h" #import "SignalsNavigationController.h" #import "SignalsViewController.h" #import #import #import #import NS_ASSUME_NONNULL_BEGIN NSString *const kCompletedRegistrationSegue = @"CompletedRegistration"; @interface CodeVerificationViewController () @property (nonatomic, readonly) AccountManager *accountManager; // Where the user enters the verification code they wish to document @property (nonatomic) UITextField *challengeTextField; @property (nonatomic) UILabel *phoneNumberLabel; //// User action buttons @property (nonatomic) UIButton *challengeButton; @property (nonatomic) UIButton *sendCodeViaSMSAgainButton; @property (nonatomic) UIButton *sendCodeViaVoiceButton; @property (nonatomic) UIActivityIndicatorView *submitCodeSpinner; @property (nonatomic) UIActivityIndicatorView *requestCodeAgainSpinner; @property (nonatomic) UIActivityIndicatorView *requestCallSpinner; @end #pragma mark - @implementation CodeVerificationViewController - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (!self) { return self; } _accountManager = [Environment getCurrent].accountManager; return self; } - (instancetype)init { self = [super init]; if (!self) { return self; } _accountManager = [Environment getCurrent].accountManager; return self; } #pragma mark - View Lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self createViews]; [self initializeKeyboardHandlers]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self enableServerActions:YES]; [self updatePhoneNumberLabel]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [_challengeTextField becomeFirstResponder]; } #pragma mark - - (void)createViews { self.view.backgroundColor = [UIColor whiteColor]; self.view.opaque = YES; UIColor *signalBlueColor = [UIColor ows_signalBrandBlueColor]; UIView *header = [UIView new]; header.backgroundColor = signalBlueColor; [self.view addSubview:header]; [header autoPinWidthToSuperview]; [header autoPinEdgeToSuperviewEdge:ALEdgeTop]; // The header will grow to accomodate the titleLabel's height. UILabel *titleLabel = [UILabel new]; titleLabel.textColor = [UIColor whiteColor]; titleLabel.text = NSLocalizedString(@"VERIFICATION_HEADER", @"Navigation title in the registration flow - during the sms code verification process."); titleLabel.font = [UIFont ows_mediumFontWithSize:20.f]; [header addSubview:titleLabel]; [titleLabel autoPinToTopLayoutGuideOfViewController:self withInset:0]; [titleLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [titleLabel autoSetDimension:ALDimensionHeight toSize:40]; [titleLabel autoHCenterInSuperview]; UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom]; [backButton setTitle:NSLocalizedString(@"VERIFICATION_BACK_BUTTON", @"button text for back button on verification view") forState:UIControlStateNormal]; [backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; backButton.titleLabel.font = [UIFont ows_mediumFontWithSize:14.f]; [header addSubview:backButton]; [backButton autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:10]; [backButton autoAlignAxis:ALAxisHorizontal toSameAxisOfView:titleLabel]; [backButton addTarget:self action:@selector(backButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; _phoneNumberLabel = [UILabel new]; _phoneNumberLabel.textColor = [UIColor ows_darkGrayColor]; _phoneNumberLabel.font = [UIFont ows_regularFontWithSize:20.f]; [self.view addSubview:_phoneNumberLabel]; [_phoneNumberLabel autoHCenterInSuperview]; [_phoneNumberLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:header withOffset:ScaleFromIPhone5To7Plus(25, 100)]; const CGFloat kHMargin = 36; _challengeTextField = [UITextField new]; _challengeTextField.textColor = [UIColor blackColor]; _challengeTextField.placeholder = NSLocalizedString(@"VERIFICATION_CHALLENGE_DEFAULT_TEXT", @"Text field placeholder for SMS verification code during registration"); _challengeTextField.font = [UIFont ows_lightFontWithSize:21.f]; _challengeTextField.textAlignment = NSTextAlignmentCenter; _challengeTextField.delegate = self; [self.view addSubview:_challengeTextField]; [_challengeTextField autoPinWidthToSuperviewWithMargin:kHMargin]; [_challengeTextField autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_phoneNumberLabel withOffset:25]; UIView *underscoreView = [UIView new]; underscoreView.backgroundColor = [UIColor colorWithWhite:0.5 alpha:1.f]; [self.view addSubview:underscoreView]; [underscoreView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_challengeTextField withOffset:3]; [underscoreView autoPinWidthToSuperviewWithMargin:kHMargin]; [underscoreView autoSetDimension:ALDimensionHeight toSize:1.f]; _challengeButton = [UIButton buttonWithType:UIButtonTypeCustom]; _challengeButton.backgroundColor = signalBlueColor; [_challengeButton setTitle:NSLocalizedString(@"VERIFICATION_CHALLENGE_SUBMIT_CODE", @"button text during registration to submit your SMS verification code") forState:UIControlStateNormal]; [_challengeButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; _challengeButton.titleLabel.font = [UIFont ows_mediumFontWithSize:14.f]; [_challengeButton addTarget:self action:@selector(verifyChallengeAction:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_challengeButton]; [_challengeButton autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:underscoreView withOffset:15]; [_challengeButton autoPinWidthToSuperviewWithMargin:kHMargin]; [_challengeButton autoSetDimension:ALDimensionHeight toSize:47.f]; const CGFloat kSpinnerSize = 20; const CGFloat kSpinnerSpacing = 15; _submitCodeSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; [_challengeButton addSubview:_submitCodeSpinner]; [_submitCodeSpinner autoSetDimension:ALDimensionWidth toSize:kSpinnerSize]; [_submitCodeSpinner autoSetDimension:ALDimensionHeight toSize:kSpinnerSize]; [_submitCodeSpinner autoVCenterInSuperview]; [_submitCodeSpinner autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:kSpinnerSpacing]; _sendCodeViaSMSAgainButton = [UIButton buttonWithType:UIButtonTypeCustom]; _sendCodeViaSMSAgainButton.backgroundColor = [UIColor whiteColor]; [_sendCodeViaSMSAgainButton setTitle:NSLocalizedString(@"VERIFICATION_CHALLENGE_SUBMIT_AGAIN", @"button text during registration to request another SMS code be sent") forState:UIControlStateNormal]; [_sendCodeViaSMSAgainButton setTitleColor:signalBlueColor forState:UIControlStateNormal]; _sendCodeViaSMSAgainButton.titleLabel.font = [UIFont ows_mediumFontWithSize:14.f]; [_sendCodeViaSMSAgainButton addTarget:self action:@selector(sendCodeViaSMSAction:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_sendCodeViaSMSAgainButton]; [_sendCodeViaSMSAgainButton autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_challengeButton withOffset:10]; [_sendCodeViaSMSAgainButton autoPinWidthToSuperviewWithMargin:kHMargin]; [_sendCodeViaSMSAgainButton autoSetDimension:ALDimensionHeight toSize:35]; _requestCodeAgainSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; [_sendCodeViaSMSAgainButton addSubview:_requestCodeAgainSpinner]; [_requestCodeAgainSpinner autoSetDimension:ALDimensionWidth toSize:kSpinnerSize]; [_requestCodeAgainSpinner autoSetDimension:ALDimensionHeight toSize:kSpinnerSize]; [_requestCodeAgainSpinner autoVCenterInSuperview]; [_requestCodeAgainSpinner autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:kSpinnerSpacing]; _sendCodeViaVoiceButton = [UIButton buttonWithType:UIButtonTypeCustom]; _sendCodeViaVoiceButton.backgroundColor = [UIColor whiteColor]; [_sendCodeViaVoiceButton setTitle:NSLocalizedString(@"VERIFICATION_CHALLENGE_SEND_VIA_VOICE", @"button text during registration to request phone number verification be done via phone call") forState:UIControlStateNormal]; [_sendCodeViaVoiceButton setTitleColor:signalBlueColor forState:UIControlStateNormal]; _sendCodeViaVoiceButton.titleLabel.font = [UIFont ows_mediumFontWithSize:14.f]; [_sendCodeViaVoiceButton addTarget:self action:@selector(sendCodeViaVoiceAction:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_sendCodeViaVoiceButton]; [_sendCodeViaVoiceButton autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_sendCodeViaSMSAgainButton]; [_sendCodeViaVoiceButton autoPinWidthToSuperviewWithMargin:kHMargin]; [_sendCodeViaVoiceButton autoSetDimension:ALDimensionHeight toSize:35]; _requestCallSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; [_sendCodeViaVoiceButton addSubview:_requestCallSpinner]; [_requestCallSpinner autoSetDimension:ALDimensionWidth toSize:kSpinnerSize]; [_requestCallSpinner autoSetDimension:ALDimensionHeight toSize:kSpinnerSize]; [_requestCallSpinner autoVCenterInSuperview]; [_requestCallSpinner autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:kSpinnerSpacing]; } - (void)updatePhoneNumberLabel { NSString *phoneNumber = [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:[TSAccountManager localNumber]]; OWSAssert([TSAccountManager localNumber] != nil); _phoneNumberLabel.text = [NSString stringWithFormat:NSLocalizedString(@"VERIFICATION_PHONE_NUMBER_FORMAT", @"Label indicating the phone number currently being verified."), phoneNumber]; } - (void)startActivityIndicator { [self.submitCodeSpinner startAnimating]; [self enableServerActions:NO]; [self.challengeTextField resignFirstResponder]; } - (void)stopActivityIndicator { [self enableServerActions:YES]; [self.submitCodeSpinner stopAnimating]; } - (void)verifyChallengeAction:(nullable id)sender { [self startActivityIndicator]; [self.accountManager registerWithVerificationCode:[self validationCodeFromTextField]] .then(^{ DDLogInfo(@"%@ Successfully registered Signal account.", self.tag); dispatch_async(dispatch_get_main_queue(), ^{ [self stopActivityIndicator]; [self performSegueWithIdentifier:kCompletedRegistrationSegue sender:nil]; }); }) .catch(^(NSError *_Nonnull error) { DDLogError(@"%@ error verifying challenge: %@", self.tag, error); dispatch_async(dispatch_get_main_queue(), ^{ [self stopActivityIndicator]; [self presentAlertWithVerificationError:error]; }); }); } - (void)presentAlertWithVerificationError:(NSError *)error { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"REGISTRATION_VERIFICATION_FAILED_TITLE", @"Alert view title") message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"DISMISS_BUTTON_TEXT", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [_challengeTextField becomeFirstResponder]; }]; [alertController addAction:dismissAction]; [self presentViewController:alertController animated:YES completion:nil]; } - (NSString *)validationCodeFromTextField { return [self.challengeTextField.text stringByReplacingOccurrencesOfString:@"-" withString:@""]; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(nullable id)sender { DDLogInfo(@"%@ preparing for CompletedRegistrationSeque", self.tag); if ([segue.identifier isEqualToString:kCompletedRegistrationSegue]) { if (![segue.destinationViewController isKindOfClass:[SignalsNavigationController class]]) { DDLogError(@"%@ Unexpected destination view controller: %@", self.tag, segue.destinationViewController); return; } SignalsNavigationController *snc = (SignalsNavigationController *)segue.destinationViewController; AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; appDelegate.window.rootViewController = snc; if (![snc.topViewController isKindOfClass:[SignalsViewController class]]) { DDLogError(@"%@ Unexpected top view controller: %@", self.tag, snc.topViewController); return; } DDLogDebug(@"%@ notifying signals view controller of new user.", self.tag); SignalsViewController *signalsViewController = (SignalsViewController *)snc.topViewController; signalsViewController.newlyRegisteredUser = YES; } } #pragma mark - Send codes again - (void)sendCodeViaSMSAction:(id)sender { [self enableServerActions:NO]; [_requestCodeAgainSpinner startAnimating]; [TSAccountManager rerequestSMSWithSuccess:^{ DDLogInfo(@"%@ Successfully requested SMS code", self.tag); [self enableServerActions:YES]; [_requestCodeAgainSpinner stopAnimating]; } failure:^(NSError *error) { DDLogError(@"%@ Failed to request SMS code with error: %@", self.tag, error); [self showRegistrationErrorMessage:error]; [self enableServerActions:YES]; [_requestCodeAgainSpinner stopAnimating]; }]; } - (void)sendCodeViaVoiceAction:(id)sender { [self enableServerActions:NO]; [_requestCallSpinner startAnimating]; [TSAccountManager rerequestVoiceWithSuccess:^{ DDLogInfo(@"%@ Successfully requested voice code", self.tag); [self enableServerActions:YES]; [_requestCallSpinner stopAnimating]; } failure:^(NSError *error) { DDLogError(@"%@ Failed to request voice code with error: %@", self.tag, error); [self showRegistrationErrorMessage:error]; [self enableServerActions:YES]; [_requestCallSpinner stopAnimating]; }]; } - (void)showRegistrationErrorMessage:(NSError *)registrationError { UIAlertView *registrationErrorAV = [[UIAlertView alloc] initWithTitle:registrationError.localizedDescription message:registrationError.localizedRecoverySuggestion delegate:nil cancelButtonTitle:NSLocalizedString(@"OK", @"") otherButtonTitles:nil, nil]; [registrationErrorAV show]; } - (void)enableServerActions:(BOOL)enabled { [_challengeButton setEnabled:enabled]; [_sendCodeViaSMSAgainButton setEnabled:enabled]; [_sendCodeViaVoiceButton setEnabled:enabled]; } - (void)backButtonPressed:(id)sender { [self.navigationController popViewControllerAnimated:YES]; } #pragma mark - Keyboard notifications - (void)initializeKeyboardHandlers { UITapGestureRecognizer *outsideTabRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboardFromAppropriateSubView)]; [self.view addGestureRecognizer:outsideTabRecognizer]; } - (void)dismissKeyboardFromAppropriateSubView { [self.view endEditing:NO]; } - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { if (range.location == 7) { return NO; } if (range.length == 0 && ![[NSCharacterSet decimalDigitCharacterSet] characterIsMember:[string characterAtIndex:0]]) { return NO; } if (range.length == 0 && range.location == 2) { textField.text = [NSString stringWithFormat:@"%@%@-", textField.text, string]; return NO; } if (range.length == 1 && range.location == 3) { range.location--; range.length = 2; textField.text = [textField.text stringByReplacingCharactersInRange:range withString:@""]; return NO; } return YES; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [self verifyChallengeAction:nil]; [textField resignFirstResponder]; return NO; } - (void)setVerificationCodeAndTryToVerify:(NSString *)verificationCode { self.challengeTextField.text = verificationCode; [self verifyChallengeAction:nil]; } #pragma mark - Logging + (NSString *)tag { return [NSString stringWithFormat:@"[%@]", self.class]; } - (NSString *)tag { return self.class.tag; } @end NS_ASSUME_NONNULL_END