session-ios/Signal/src/view controllers/RegisterViewController.m

432 lines
18 KiB
Objective-C

#import "Environment.h"
#import "HttpManager.h"
#import "LocalizableText.h"
#import "NBAsYouTypeFormatter.h"
#import "PhoneNumber.h"
#import "PhoneNumberDirectoryFilterManager.h"
#import "PhoneNumberUtil.h"
#import "PreferencesUtil.h"
#import "PushManager.h"
#import "RegisterViewController.h"
#import "SignalUtil.h"
#import "SGNKeychainUtil.h"
#import "ThreadManager.h"
#import "Util.h"
#define REGISTER_VIEW_NUMBER 0
#define CHALLENGE_VIEW_NUMBER 1
#define COUNTRY_CODE_CHARACTER_MAX 3
#define SERVER_TIMEOUT_SECONDS 20
#define SMS_VERIFICATION_TIMEOUT_SECONDS 4*60
#define VOICE_VERIFICATION_COOLDOWN_SECONDS 4
#define IPHONE_BLUE [UIColor colorWithRed:22 green:173 blue:214 alpha:1]
@interface RegisterViewController () {
NSMutableString *_enteredPhoneNumber;
NSTimer* countdownTimer;
NSDate *timeoutDate;
}
@end
@implementation RegisterViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self localizeButtonText];
DDLogInfo(@"Opened Registration View");
[self populateDefaultCountryNameAndCode];
_scrollView.contentSize = _containerView.bounds.size;
BOOL isRegisteredAlready = [Environment isRegistered];
_registerCancelButton.hidden = !isRegisteredAlready;
[self initializeKeyboardHandlers];
[self setPlaceholderTextColor:[UIColor lightGrayColor]];
_enteredPhoneNumber = [NSMutableString string];
}
+ (RegisterViewController*)registerViewController {
RegisterViewController *viewController = [RegisterViewController new];
viewController->registered = [FutureSource new];
viewController->life = [CancelTokenSource cancelTokenSource];
[[viewController->life getToken] whenCancelledTryCancel:viewController->registered];
return viewController;
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleDefault;
}
- (void)setPlaceholderTextColor:(UIColor *)color {
NSAttributedString *placeholder = _phoneNumberTextField.attributedPlaceholder;
if (placeholder.length) {
NSDictionary * attributes = [placeholder attributesAtIndex:0
effectiveRange:NULL];
NSMutableDictionary *newAttributes = [[NSMutableDictionary alloc] initWithDictionary:attributes];
newAttributes[NSForegroundColorAttributeName] = color;
NSString *placeholderString = [placeholder string];
_phoneNumberTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:placeholderString
attributes:newAttributes];
}
}
- (void)localizeButtonText {
[_registerCancelButton setTitle:TXT_CANCEL_TITLE forState:UIControlStateNormal];
[_continueToWhisperButton setTitle:CONTINUE_TO_WHISPER_TITLE forState:UIControlStateNormal];
[_registerButton setTitle:REGISTER_BUTTON_TITLE forState:UIControlStateNormal];
[_challengeButton setTitle:CHALLENGE_CODE_BUTTON_TITLE forState:UIControlStateNormal];
}
- (IBAction)registerCancelButtonTapped {
[self dismissView];
}
- (void) dismissView {
[self stopVoiceVerificationCountdownTimer];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)populateDefaultCountryNameAndCode {
NSLocale *locale = [NSLocale currentLocale];
NSString *countryCode = [locale objectForKey:NSLocaleCountryCode];
NSNumber *cc = [[NBPhoneNumberUtil sharedInstance] getCountryCodeForRegion:countryCode];
_countryCodeLabel.text = [NSString stringWithFormat:@"%@%@",COUNTRY_CODE_PREFIX, cc];
_countryNameLabel.text = [PhoneNumberUtil countryNameFromCountryCode:countryCode];
}
- (IBAction)changeNumberTapped {
[self showViewNumber:REGISTER_VIEW_NUMBER];
}
- (IBAction)changeCountryCodeTapped {
CountryCodeViewController *countryCodeController = [CountryCodeViewController new];
countryCodeController.delegate = self;
[self presentViewController:countryCodeController animated:YES completion:nil];
}
-(Future*) asyncRegister:(PhoneNumber*)phoneNumber untilCancelled:(id<CancelToken>)cancelToken {
[SGNKeychainUtil generateServerAuthPassword];
[SGNKeychainUtil setLocalNumberTo:phoneNumber];
CancellableOperationStarter regStarter = ^Future *(id<CancelToken> internalUntilCancelledToken) {
HttpRequest *registerRequest = [HttpRequest httpRequestToStartRegistrationOfPhoneNumber];
return [HttpManager asyncOkResponseFromMasterServer:registerRequest
unlessCancelled:internalUntilCancelledToken
andErrorHandler:[Environment errorNoter]];
};
Future *futurePhoneRegistrationStarted = [AsyncUtil raceCancellableOperation:regStarter
againstTimeout:20.0
untilCancelled:cancelToken];
return [futurePhoneRegistrationStarted then:^(id _) {
[self showViewNumber:CHALLENGE_VIEW_NUMBER];
[self.challengeNumberLabel setText:[phoneNumber description]];
[_registerCancelButton removeFromSuperview];
[self startVoiceVerificationCountdownTimer];
self->futureChallengeAcceptedSource = [FutureSource new];
return futureChallengeAcceptedSource;
}];
}
- (void)registerPhoneNumberTapped {
NSString *phoneNumber = [NSString stringWithFormat:@"%@%@", _countryCodeLabel.text, _phoneNumberTextField.text];
PhoneNumber* localNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:phoneNumber];
if(localNumber==nil){ return; }
[_phoneNumberTextField resignFirstResponder];
Future* futureFinished = [self asyncRegister:localNumber untilCancelled:[life getToken]];
[_registerActivityIndicator startAnimating];
_registerButton.enabled = NO;
[futureFinished catchDo:^(id error) {
NSError *err = ((NSError*)error);
[_registerActivityIndicator stopAnimating];
_registerButton.enabled = YES;
DDLogError(@"Registration failed with information %@", err.description);
UIAlertView *registrationErrorAV = [[UIAlertView alloc]initWithTitle:REGISTER_ERROR_ALERT_VIEW_TITLE message:REGISTER_ERROR_ALERT_VIEW_BODY delegate:nil cancelButtonTitle:REGISTER_ERROR_ALERT_VIEW_DISMISS otherButtonTitles:nil, nil];
[registrationErrorAV show];
}];
}
- (void)dismissTapped {
[self dismissView];
}
- (void)verifyChallengeTapped {
[_challengeTextField resignFirstResponder];
_challengeButton.enabled = NO;
[_challengeActivityIndicator startAnimating];
HttpRequest *verifyRequest = [HttpRequest httpRequestToVerifyAccessToPhoneNumberWithChallenge:_challengeTextField.text];
Future *futureDone = [HttpManager asyncOkResponseFromMasterServer:verifyRequest
unlessCancelled:nil
andErrorHandler:[Environment errorNoter]];
[futureDone catchDo:^(id error) {
if ([error isKindOfClass:[HttpResponse class]]) {
HttpResponse* badResponse = error;
if ([badResponse getStatusCode] == 401) {
UIAlertView *incorrectChallengeCodeAV = [[UIAlertView alloc]initWithTitle:REGISTER_CHALLENGE_ALERT_VIEW_TITLE message:REGISTER_CHALLENGE_ALERT_VIEW_BODY delegate:nil cancelButtonTitle:REGISTER_CHALLENGE_ALERT_DISMISS otherButtonTitles:nil, nil];
[incorrectChallengeCodeAV show];
_challengeButton.enabled = YES;
[_challengeActivityIndicator stopAnimating];
return;
}
}
_challengeButton.enabled = YES;
[_challengeActivityIndicator stopAnimating];
[Environment errorNoter](error, @"While Verifying Challenge.", NO);
}];
[futureDone thenDo:^(id result) {
[futureChallengeAcceptedSource trySetResult:@YES];
}];
[futureChallengeAcceptedSource thenDo:^(id value) {
[[PushManager sharedManager] askForPushRegistrationWithSuccess:^{
[Environment setRegistered:YES];
[registered trySetResult:@YES];
[[[Environment getCurrent] phoneDirectoryManager] forceUpdate];
[self dismissView];
} failure:^{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:REGISTER_ERROR_ALERT_VIEW_TITLE message:REGISTER_ERROR_ALERT_VIEW_BODY delegate:nil cancelButtonTitle:REGISTER_ERROR_ALERT_VIEW_DISMISS otherButtonTitles:nil, nil];
[alertView show];
_challengeButton.enabled = YES;
[_challengeActivityIndicator stopAnimating];
}];
}];
}
- (void)showViewNumber:(NSInteger)viewNumber {
if (viewNumber == REGISTER_VIEW_NUMBER) {
[_registerActivityIndicator stopAnimating];
_registerButton.enabled = YES;
}
[self stopVoiceVerificationCountdownTimer];
[_scrollView setContentOffset:CGPointMake(_scrollView.frame.size.width*viewNumber, 0) animated:YES];
}
- (void)presentInvalidCountryCodeError {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:REGISTER_CC_ERR_ALERT_VIEW_TITLE
message:REGISTER_CC_ERR_ALERT_VIEW_MESSAGE
delegate:nil
cancelButtonTitle:REGISTER_CC_ERR_ALERT_VIEW_DISMISS
otherButtonTitles:nil];
[alertView show];
}
- (void) startVoiceVerificationCountdownTimer{
[self.initiateVoiceVerificationButton.titleLabel setTextAlignment:NSTextAlignmentCenter];
self.initiateVoiceVerificationButton.hidden = NO;
NSTimeInterval smsTimeoutTimeInterval = SMS_VERIFICATION_TIMEOUT_SECONDS;
NSDate *now = [[NSDate alloc] init];
timeoutDate = [[NSDate alloc] initWithTimeInterval:smsTimeoutTimeInterval sinceDate:now];
countdownTimer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(countdowntimerFired)
userInfo:nil repeats:YES];
}
- (void) stopVoiceVerificationCountdownTimer{
[countdownTimer invalidate];
}
- (void) countdowntimerFired {
NSDate *now = [[NSDate alloc] init];
NSCalendar *sysCalendar = [NSCalendar currentCalendar];
unsigned int unitFlags = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents *conversionInfo = [sysCalendar components:unitFlags fromDate:now toDate:timeoutDate options:0];
NSString* timeLeft = [NSString stringWithFormat:@"%ld:%02ld",(long)[conversionInfo minute],(long)[conversionInfo second]];
[self.voiceChallengeTextLabel setText:timeLeft];
if (0 <= [now compare:timeoutDate]) {
[self initiateVoiceVerification];
}
}
- (void) initiateVoiceVerification{
[self stopVoiceVerificationCountdownTimer];
CancellableOperationStarter callStarter = ^Future *(id<CancelToken> internalUntilCancelledToken) {
HttpRequest* voiceVerifyReq = [HttpRequest httpRequestToStartRegistrationOfPhoneNumberWithVoice];
[self.voiceChallengeTextLabel setText:@"Calling" ];
return [HttpManager asyncOkResponseFromMasterServer:voiceVerifyReq
unlessCancelled:internalUntilCancelledToken
andErrorHandler:[Environment errorNoter]];
};
Future *futureVoiceVerificationStarted = [AsyncUtil raceCancellableOperation:callStarter
againstTimeout:SERVER_TIMEOUT_SECONDS
untilCancelled:[life getToken]];
[futureVoiceVerificationStarted catchDo:^(id errorId) {
HttpResponse* error = (HttpResponse*)errorId;
[self.voiceChallengeTextLabel setText:[error getStatusText]];
}];
[futureVoiceVerificationStarted finally:^id(id _id) {
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, VOICE_VERIFICATION_COOLDOWN_SECONDS * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self.voiceChallengeTextLabel setText:@"Re-Call"];
});
return _id;
}];
}
- (IBAction)initiateVoiceVerificationButtonHandler {
[self initiateVoiceVerification];
}
#pragma mark - Keyboard notifications
- (void)initializeKeyboardHandlers{
UITapGestureRecognizer *outsideTabRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboardFromAppropriateSubView)];
[self.view addGestureRecognizer:outsideTabRecognizer];
[self observeKeyboardNotifications];
}
-(void) dismissKeyboardFromAppropriateSubView {
[self.view endEditing:NO];
}
- (void)observeKeyboardNotifications {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)keyboardWillShow:(NSNotification *)notification {
double duration = [[notification userInfo][UIKeyboardAnimationDurationUserInfoKey] doubleValue];
[UIView animateWithDuration:duration animations:^{
CGSize keyboardSize = [[notification userInfo][UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;;
_scrollView.frame = CGRectMake(CGRectGetMinX(_scrollView.frame),
CGRectGetMinY(_scrollView.frame)-keyboardSize.height,
CGRectGetWidth(_scrollView.frame),
CGRectGetHeight(_scrollView.frame));
}];
}
- (void)keyboardWillHide:(NSNotification *)notification {
double duration = [[notification userInfo][UIKeyboardAnimationDurationUserInfoKey] doubleValue];
[UIView animateWithDuration:duration animations:^{
_scrollView.frame = CGRectMake(CGRectGetMinX(_scrollView.frame),
CGRectGetMinY(self.view.frame),
CGRectGetWidth(_scrollView.frame),
CGRectGetHeight(_scrollView.frame));
}];
}
#pragma mark - CountryCodeViewControllerDelegate
- (void)countryCodeViewController:(CountryCodeViewController *)vc
didSelectCountryCode:(NSString *)code
forCountry:(NSString *)country {
_countryCodeLabel.text = code;
_countryNameLabel.text = country;
[self updatePhoneNumberFieldWithString:code cursorposition:_enteredPhoneNumber.length];
[vc dismissViewControllerAnimated:YES completion:nil];
}
- (void)countryCodeViewControllerDidCancel:(CountryCodeViewController *)vc {
[vc dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - UITextFieldDelegate
- (NSUInteger) calculateLocationOffset:(NSUInteger)location {
NSUInteger offset = 0, phonenumberposition = 0;
for (NSUInteger i = 0; i < location; i++) {
if ([_phoneNumberTextField.text characterAtIndex:i] != [_enteredPhoneNumber characterAtIndex:phonenumberposition]) {
offset++;
} else {
phonenumberposition++;
}
}
return offset;
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string {
NSUInteger offset = [self calculateLocationOffset:range.location];
unichar currentPoschar = 0;
range.location -= offset;
BOOL handleBackspace = range.length == 1;
if (handleBackspace) {
if ((range.location + offset) < _phoneNumberTextField.text.length) {
currentPoschar = [_phoneNumberTextField.text characterAtIndex:(range.location + offset)];
}
if ((currentPoschar < '0' || currentPoschar > '9') && currentPoschar != 0) {
range.location--;
}
NSRange backspaceRange = NSMakeRange(range.location, 1);
[_enteredPhoneNumber replaceCharactersInRange:backspaceRange withString:string];
range.location++;
} else {
NSString* sanitizedString = [[string componentsSeparatedByCharactersInSet:[[NSCharacterSet decimalDigitCharacterSet ] invertedSet]] componentsJoinedByString:@""];
NSMutableString* mutablePhoneNumber = [NSMutableString stringWithString:_enteredPhoneNumber];
[mutablePhoneNumber insertString:sanitizedString atIndex:range.location];
_enteredPhoneNumber = mutablePhoneNumber;
if ((range.location + offset + 1) < _phoneNumberTextField.text.length) {
currentPoschar = [_phoneNumberTextField.text characterAtIndex:(range.location + offset + 1)];
}
if ((currentPoschar < '0' || currentPoschar > '9') && currentPoschar != 0) {
range.location++;
}
}
[self updatePhoneNumberFieldWithString:_enteredPhoneNumber cursorposition:range.location+offset];
return NO;
}
-(void) updatePhoneNumberFieldWithString:(NSString*)input
cursorposition:(NSUInteger)cursorpos {
NSString* result = [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:_enteredPhoneNumber
withSpecifiedCountryCodeString:_countryCodeLabel.text];
cursorpos += result.length - _phoneNumberTextField.text.length;
_phoneNumberTextField.text = result;
UITextPosition *position = [_phoneNumberTextField positionFromPosition:[_phoneNumberTextField beginningOfDocument]
offset:(NSInteger)cursorpos];
[_phoneNumberTextField setSelectedTextRange:[_phoneNumberTextField textRangeFromPosition:position
toPosition:position]];
}
@end