session-ios/Signal/src/ViewControllers/Registration/CodeVerificationViewControl...

511 lines
21 KiB
Objective-C

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "CodeVerificationViewController.h"
#import "OWS2FARegistrationViewController.h"
#import "ProfileViewController.h"
#import "Signal-Swift.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalMessaging/UIViewController+OWS.h>
#import <SignalServiceKit/OWSError.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSNetworkManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface CodeVerificationViewController () <UITextFieldDelegate>
@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) OWSFlatButton *submitButton;
@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 = SignalApp.sharedApp.accountManager;
return self;
}
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
_accountManager = SignalApp.sharedApp.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 = [self phoneNumberText];
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];
// This view is used in more than one context.
//
// * Usually, it is pushed atop RegistrationViewController in which
// case we want a "back" button.
// * It can also be used to re-register from the app's "de-registration"
// views, in which case RegistrationViewController is not used and we
// do _not_ want a "back" button.
if (self.navigationController.viewControllers.count > 1) {
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 autoPinLeadingToSuperviewMarginWithInset:10.f];
[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];
_phoneNumberLabel.numberOfLines = 2;
_phoneNumberLabel.adjustsFontSizeToFitWidth = YES;
_phoneNumberLabel.textAlignment = NSTextAlignmentCenter;
[self.view addSubview:_phoneNumberLabel];
[_phoneNumberLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5(32)];
[_phoneNumberLabel autoPinEdge:ALEdgeTop
toEdge:ALEdgeBottom
ofView:header
withOffset:ScaleFromIPhone5To7Plus(30, 100)];
const CGFloat kHMargin = 36;
if (UIDevice.currentDevice.isShorterThanIPhone5) {
_challengeTextField = [DismissableTextField new];
} else {
_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.keyboardType = UIKeyboardTypeNumberPad;
_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];
const CGFloat kSubmitButtonHeight = 47.f;
// NOTE: We use ows_signalBrandBlueColor instead of ows_materialBlueColor
// throughout the onboarding flow to be consistent with the headers.
OWSFlatButton *submitButton =
[OWSFlatButton buttonWithTitle:NSLocalizedString(@"VERIFICATION_CHALLENGE_SUBMIT_CODE",
@"button text during registration to submit your SMS verification code.")
font:[OWSFlatButton fontForHeight:kSubmitButtonHeight]
titleColor:[UIColor whiteColor]
backgroundColor:[UIColor ows_signalBrandBlueColor]
target:self
selector:@selector(submitVerificationCode)];
self.submitButton = submitButton;
[self.view addSubview:_submitButton];
[_submitButton autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:underscoreView withOffset:15];
[_submitButton autoPinWidthToSuperviewWithMargin:kHMargin];
[_submitButton autoSetDimension:ALDimensionHeight toSize:kSubmitButtonHeight];
const CGFloat kSpinnerSize = 20;
const CGFloat kSpinnerSpacing = ScaleFromIPhone5To7Plus(5, 15);
_submitCodeSpinner =
[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
[_submitButton addSubview:_submitCodeSpinner];
[_submitCodeSpinner autoSetDimension:ALDimensionWidth toSize:kSpinnerSize];
[_submitCodeSpinner autoSetDimension:ALDimensionHeight toSize:kSpinnerSize];
[_submitCodeSpinner autoVCenterInSuperview];
[_submitCodeSpinner autoPinTrailingToSuperviewMarginWithInset: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:_submitButton 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 autoPinTrailingToSuperviewMarginWithInset: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 autoPinTrailingToSuperviewMarginWithInset:kSpinnerSpacing];
}
- (NSString *)phoneNumberText
{
OWSAssert([TSAccountManager localNumber] != nil);
return [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:[TSAccountManager localNumber]];
}
- (void)updatePhoneNumberLabel
{
_phoneNumberLabel.text =
[NSString stringWithFormat:NSLocalizedString(@"VERIFICATION_PHONE_NUMBER_FORMAT",
@"Label indicating the phone number currently being verified."),
[self phoneNumberText]];
}
- (void)startActivityIndicator
{
[self.submitCodeSpinner startAnimating];
[self enableServerActions:NO];
[self.challengeTextField resignFirstResponder];
}
- (void)stopActivityIndicator
{
[self enableServerActions:YES];
[self.submitCodeSpinner stopAnimating];
}
- (void)submitVerificationCode
{
[self startActivityIndicator];
OWSProdInfo([OWSAnalyticsEvents registrationRegisteringCode]);
__weak CodeVerificationViewController *weakSelf = self;
[self.accountManager registerWithVerificationCode:[self validationCodeFromTextField] pin:nil]
.then(^{
OWSProdInfo([OWSAnalyticsEvents registrationRegisteringSubmittedCode]);
DDLogInfo(@"%@ Successfully registered Signal account.", weakSelf.logTag);
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf stopActivityIndicator];
[weakSelf verificationWasCompleted];
});
})
.catch(^(NSError *error) {
DDLogError(@"%@ error: %@, %@, %zd", weakSelf.logTag, [error class], error.domain, error.code);
OWSProdInfo([OWSAnalyticsEvents registrationRegistrationFailed]);
DDLogError(@"%@ error verifying challenge: %@", weakSelf.logTag, error);
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf stopActivityIndicator];
if ([error.domain isEqualToString:OWSSignalServiceKitErrorDomain]
&& error.code == OWSErrorCodeRegistrationMissing2FAPIN) {
CodeVerificationViewController *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
DDLogInfo(@"%@ Showing 2FA registration view.", strongSelf.logTag);
OWS2FARegistrationViewController *viewController = [OWS2FARegistrationViewController new];
viewController.verificationCode = strongSelf.validationCodeFromTextField;
[strongSelf.navigationController pushViewController:viewController animated:YES];
} else {
[weakSelf presentAlertWithVerificationError:error];
[weakSelf.challengeTextField becomeFirstResponder];
}
});
});
}
- (void)verificationWasCompleted
{
[ProfileViewController presentForRegistration:self.navigationController];
}
- (void)presentAlertWithVerificationError:(NSError *)error
{
UIAlertController *alert;
alert = [UIAlertController
alertControllerWithTitle:NSLocalizedString(@"REGISTRATION_VERIFICATION_FAILED_TITLE", @"Alert view title")
message:error.localizedDescription
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:CommonStrings.dismissButton
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[self.challengeTextField becomeFirstResponder];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
- (NSString *)validationCodeFromTextField
{
return [self.challengeTextField.text stringByReplacingOccurrencesOfString:@"-" withString:@""];
}
#pragma mark - Actions
- (void)sendCodeViaSMSAction:(id)sender
{
OWSProdInfo([OWSAnalyticsEvents registrationRegisteringRequestedNewCodeBySms]);
[self enableServerActions:NO];
[_requestCodeAgainSpinner startAnimating];
__weak CodeVerificationViewController *weakSelf = self;
[TSAccountManager rerequestSMSWithSuccess:^{
DDLogInfo(@"%@ Successfully requested SMS code", weakSelf.logTag);
[weakSelf enableServerActions:YES];
[weakSelf.requestCodeAgainSpinner stopAnimating];
}
failure:^(NSError *error) {
DDLogError(@"%@ Failed to request SMS code with error: %@", weakSelf.logTag, error);
[weakSelf showRegistrationErrorMessage:error];
[weakSelf enableServerActions:YES];
[weakSelf.requestCodeAgainSpinner stopAnimating];
[weakSelf.challengeTextField becomeFirstResponder];
}];
}
- (void)sendCodeViaVoiceAction:(id)sender
{
OWSProdInfo([OWSAnalyticsEvents registrationRegisteringRequestedNewCodeByVoice]);
[self enableServerActions:NO];
[_requestCallSpinner startAnimating];
__weak CodeVerificationViewController *weakSelf = self;
[TSAccountManager rerequestVoiceWithSuccess:^{
DDLogInfo(@"%@ Successfully requested voice code", weakSelf.logTag);
[weakSelf enableServerActions:YES];
[weakSelf.requestCallSpinner stopAnimating];
}
failure:^(NSError *error) {
DDLogError(@"%@ Failed to request voice code with error: %@", weakSelf.logTag, error);
[weakSelf showRegistrationErrorMessage:error];
[weakSelf enableServerActions:YES];
[weakSelf.requestCallSpinner stopAnimating];
[weakSelf.challengeTextField becomeFirstResponder];
}];
}
- (void)showRegistrationErrorMessage:(NSError *)registrationError
{
[OWSAlerts showAlertWithTitle:registrationError.localizedDescription
message:registrationError.localizedRecoverySuggestion];
}
- (void)enableServerActions:(BOOL)enabled
{
[_submitButton setEnabled:enabled];
[_sendCodeViaSMSAgainButton setEnabled:enabled];
[_sendCodeViaVoiceButton setEnabled:enabled];
}
- (void)backButtonPressed:(id)sender
{
OWSProdInfo([OWSAnalyticsEvents registrationVerificationBack]);
[self.navigationController popViewControllerAnimated:YES];
}
#pragma mark - Keyboard notifications
- (void)initializeKeyboardHandlers
{
UITapGestureRecognizer *outsideTabRecognizer =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboardFromAppropriateSubView)];
[self.view addGestureRecognizer:outsideTabRecognizer];
self.view.userInteractionEnabled = YES;
}
- (void)dismissKeyboardFromAppropriateSubView
{
[self.view endEditing:NO];
}
- (BOOL)textField:(UITextField *)textField
shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)insertionText
{
// Verification codes take this form: "123-456".
//
// * We only want to let the user "6 decimal digits + 1 hyphen = 7".
// * The user shouldn't have to enter the hyphen - it should be added automatically.
// * The user should be able to copy and paste freely.
// * Invalid input (including extraneous hyphens) should be simply ignored.
//
// We accomplish this by being permissive and trying to "take as much of the user
// input as possible".
//
// * Always accept deletes.
// * Ignore invalid input.
// * Take partial input if possible.
NSString *oldText = textField.text;
// Construct the new contents of the text field by:
// 1. Determining the "left" substring: the contents of the old text _before_ the deletion range.
// Filtering will remove non-decimal digit characters like hyphen "-".
NSString *left = [oldText substringToIndex:range.location].digitsOnly;
// 2. Determining the "right" substring: the contents of the old text _after_ the deletion range.
NSString *right = [oldText substringFromIndex:range.location + range.length].digitsOnly;
// 3. Determining the "center" substring: the contents of the new insertion text.
NSString *center = insertionText.digitsOnly;
// 3a. Trim the tail of the "center" substring to ensure that we don't end up
// with more than 6 decimal digits.
while (center.length > 0 && left.length + center.length + right.length > 6) {
center = [center substringToIndex:center.length - 1];
}
// 4. Construct the "raw" new text by concatenating left, center and right.
NSString *rawNewText = [[left stringByAppendingString:center] stringByAppendingString:right];
// 5. Construct the "formatted" new text by inserting a hyphen if necessary.
NSString *formattedNewText
= (rawNewText.length <= 3 ? rawNewText
: [[[rawNewText substringToIndex:3] stringByAppendingString:@"-"]
stringByAppendingString:[rawNewText substringFromIndex:3]]);
textField.text = formattedNewText;
// Move the cursor after the newly inserted text.
NSUInteger newInsertionPoint = left.length + center.length;
if (newInsertionPoint > 3) {
// Nudge the cursor to the right to reflect the hyphen
// if necessary.
newInsertionPoint++;
}
UITextPosition *newPosition =
[textField positionFromPosition:textField.beginningOfDocument offset:(NSInteger)newInsertionPoint];
textField.selectedTextRange = [textField textRangeFromPosition:newPosition toPosition:newPosition];
return NO;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[self submitVerificationCode];
[textField resignFirstResponder];
return NO;
}
- (void)setVerificationCodeAndTryToVerify:(NSString *)verificationCode
{
NSString *rawNewText = verificationCode.digitsOnly;
NSString *formattedNewText
= (rawNewText.length <= 3 ? rawNewText
: [[[rawNewText substringToIndex:3] stringByAppendingString:@"-"]
stringByAppendingString:[rawNewText substringFromIndex:3]]);
self.challengeTextField.text = formattedNewText;
// Move the cursor after the newly inserted text.
UITextPosition *newPosition = [self.challengeTextField endOfDocument];
self.challengeTextField.selectedTextRange =
[self.challengeTextField textRangeFromPosition:newPosition toPosition:newPosition];
[self submitVerificationCode];
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleLightContent;
}
@end
NS_ASSUME_NONNULL_END