session-ios/Signal/src/ViewControllers/CodeVerificationViewController.m

482 lines
21 KiB
Mathematica
Raw Normal View History

2014-10-29 21:58:58 +01:00
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
2014-10-29 21:58:58 +01:00
//
#import "CodeVerificationViewController.h"
#import "AppDelegate.h"
#import "Signal-Swift.h"
#import "SignalsNavigationController.h"
#import "SignalsViewController.h"
2017-03-23 14:55:39 +01:00
#import "StringUtil.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalServiceKit/OWSError.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSNetworkManager.h>
#import <SignalServiceKit/TSStorageManager+keyingMaterial.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) UIButton *challengeButton;
@property (nonatomic) UIButton *sendCodeViaSMSAgainButton;
@property (nonatomic) UIButton *sendCodeViaVoiceButton;
2014-10-29 21:58:58 +01:00
@property (nonatomic) UIActivityIndicatorView *submitCodeSpinner;
@property (nonatomic) UIActivityIndicatorView *requestCodeAgainSpinner;
@property (nonatomic) UIActivityIndicatorView *requestCallSpinner;
2014-10-29 21:58:58 +01:00
@end
#pragma mark -
2014-10-29 21:58:58 +01:00
@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;
}
2017-02-13 18:11:41 +01:00
#pragma mark - View Lifecycle
- (void)viewDidLoad {
2014-10-29 21:58:58 +01:00
[super viewDidLoad];
[self createViews];
2014-10-29 21:58:58 +01:00
[self initializeKeyboardHandlers];
}
2017-02-13 18:11:41 +01:00
- (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;
2017-02-13 18:11:41 +01:00
UIColor *signalBlueColor = [UIColor ows_signalBrandBlueColor];
UIView *header = [UIView new];
header.backgroundColor = signalBlueColor;
[self.view addSubview:header];
[header autoPinWidthToSuperview];
[header autoPinEdgeToSuperviewEdge:ALEdgeTop];
2017-02-13 18:11:41 +01:00
// 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];
2017-02-13 18:11:41 +01:00
[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];
2017-02-13 18:11:41 +01:00
[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];
_phoneNumberLabel.numberOfLines = 2;
_phoneNumberLabel.adjustsFontSizeToFitWidth = YES;
[self.view addSubview:_phoneNumberLabel];
[_phoneNumberLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5(32)];
[_phoneNumberLabel autoPinEdge:ALEdgeTop
toEdge:ALEdgeBottom
ofView:header
withOffset:ScaleFromIPhone5To7Plus(30, 100)];
2017-02-13 18:11:41 +01:00
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.keyboardType = UIKeyboardTypeNumberPad;
_challengeTextField.delegate = self;
[self.view addSubview:_challengeTextField];
2017-02-13 18:11:41 +01:00
[_challengeTextField autoPinWidthToSuperviewWithMargin:kHMargin];
[_challengeTextField autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_phoneNumberLabel
2017-02-13 18:11:41 +01:00
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
2017-02-13 18:11:41 +01:00
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
2017-02-13 18:11:41 +01:00
withOffset:15];
[_challengeButton autoPinWidthToSuperviewWithMargin:kHMargin];
[_challengeButton autoSetDimension:ALDimensionHeight toSize:47.f];
2017-02-13 18:11:41 +01:00
const CGFloat kSpinnerSize = 20;
const CGFloat kSpinnerSpacing = 15;
_submitCodeSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
[_challengeButton addSubview:_submitCodeSpinner];
2017-02-13 18:11:41 +01:00
[_submitCodeSpinner autoSetDimension:ALDimensionWidth toSize:kSpinnerSize];
[_submitCodeSpinner autoSetDimension:ALDimensionHeight toSize:kSpinnerSize];
[_submitCodeSpinner autoVCenterInSuperview];
2017-02-13 18:11:41 +01:00
[_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
2017-02-13 18:11:41 +01:00
withOffset:10];
[_sendCodeViaSMSAgainButton autoPinWidthToSuperviewWithMargin:kHMargin];
[_sendCodeViaSMSAgainButton autoSetDimension:ALDimensionHeight toSize:35];
_requestCodeAgainSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[_sendCodeViaSMSAgainButton addSubview:_requestCodeAgainSpinner];
2017-02-13 18:11:41 +01:00
[_requestCodeAgainSpinner autoSetDimension:ALDimensionWidth toSize:kSpinnerSize];
[_requestCodeAgainSpinner autoSetDimension:ALDimensionHeight toSize:kSpinnerSize];
[_requestCodeAgainSpinner autoVCenterInSuperview];
2017-02-13 18:11:41 +01:00
[_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];
2017-02-13 18:11:41 +01:00
[_sendCodeViaVoiceButton autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_sendCodeViaSMSAgainButton];
[_sendCodeViaVoiceButton autoPinWidthToSuperviewWithMargin:kHMargin];
[_sendCodeViaVoiceButton autoSetDimension:ALDimensionHeight toSize:35];
_requestCallSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[_sendCodeViaVoiceButton addSubview:_requestCallSpinner];
2017-02-13 18:11:41 +01:00
[_requestCallSpinner autoSetDimension:ALDimensionWidth toSize:kSpinnerSize];
[_requestCallSpinner autoSetDimension:ALDimensionHeight toSize:kSpinnerSize];
[_requestCallSpinner autoVCenterInSuperview];
2017-02-13 18:11:41 +01:00
[_requestCallSpinner autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:kSpinnerSpacing];
}
- (NSString *)phoneNumberText
{
2017-02-13 18:11:41 +01:00
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]];
2014-10-29 21:58:58 +01:00
}
- (void)startActivityIndicator
{
[self.submitCodeSpinner startAnimating];
[self enableServerActions:NO];
[self.challengeTextField resignFirstResponder];
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
}
- (void)stopActivityIndicator
{
[self enableServerActions:YES];
[self.submitCodeSpinner stopAnimating];
2014-10-29 21:58:58 +01:00
}
- (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];
UIStoryboard *storyboard = [UIStoryboard main];
UIViewController *viewController = [storyboard instantiateInitialViewController];
OWSAssert([viewController isKindOfClass:[SignalsNavigationController class]]);
SignalsNavigationController *navigationController = (SignalsNavigationController *)viewController;
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
appDelegate.window.rootViewController = navigationController;
OWSAssert([navigationController.topViewController isKindOfClass:[SignalsViewController class]]);
DDLogDebug(@"%@ notifying signals view controller of new user.", self.tag);
SignalsViewController *signalsViewController
= (SignalsViewController *)navigationController.topViewController;
signalsViewController.newlyRegisteredUser = YES;
});
})
.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;
// In the case of the "rate limiting" error, we want to show the
// "recovery suggestion", not the error's "description."
if ([error.domain isEqualToString:TSNetworkManagerDomain] &&
error.code == 413) {
alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"REGISTRATION_VERIFICATION_FAILED_TITLE",
@"Alert view title")
message:error.localizedRecoverySuggestion
preferredStyle:UIAlertControllerStyleAlert];
} else {
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:@""];
}
#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 {
[OWSAlerts showAlertWithTitle:registrationError.localizedDescription
message:registrationError.localizedRecoverySuggestion];
}
- (void)enableServerActions:(BOOL)enabled {
[_challengeButton setEnabled:enabled];
[_sendCodeViaSMSAgainButton setEnabled:enabled];
[_sendCodeViaVoiceButton setEnabled:enabled];
}
- (void)backButtonPressed:(id)sender {
[self.navigationController popViewControllerAnimated:YES];
}
2014-10-29 21:58:58 +01:00
#pragma mark - Keyboard notifications
- (void)initializeKeyboardHandlers {
UITapGestureRecognizer *outsideTabRecognizer =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboardFromAppropriateSubView)];
2014-10-29 21:58:58 +01:00
[self.view addGestureRecognizer:outsideTabRecognizer];
}
- (void)dismissKeyboardFromAppropriateSubView {
2014-10-29 21:58:58 +01:00
[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 verifyChallengeAction:nil];
[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 verifyChallengeAction:nil];
2015-01-30 22:58:23 +01:00
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
2014-10-29 21:58:58 +01:00
@end
NS_ASSUME_NONNULL_END