You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/Signal/src/ViewControllers/Registration/CodeVerificationViewControl...

511 lines
21 KiB
Matlab

//
// 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];
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).
10 years ago
}
- (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];
7 years ago
[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];
}
});
});
}
7 years ago
- (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