// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2012-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import "RestoreIdentityViewController.h" #import "UIDefines.h" #import "MBProgressHUD.h" #import "MyIdentityStore.h" #import "NSData+Base32.h" #import "ScanBackupController.h" #import "AppDelegate.h" #import "IdentityBackupStore.h" #import "ServerAPIConnector.h" #import "UIImage+ColoredImage.h" #import "RectUtil.h" #import "Utils.h" #import "IntroQuestionView.h" #import "NibUtil.h" @interface RestoreIdentityViewController () @end @implementation RestoreIdentityViewController - (void)viewDidLoad { [super viewDidLoad]; [self setup]; } - (void)setup { UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedScan:)]; [_scanView addGestureRecognizer:tapGesture]; _scanView.userInteractionEnabled = YES; _scanView.isAccessibilityElement = YES; [_scanView setAccessibilityHint: [BundleUtil localizedStringForKey:@"scan_id_backup"]]; _scanLabel.text = [BundleUtil localizedStringForKey:@"scan_id_backup"]; _scanLabel.textColor = [Colors mainThemeDark]; self.view.backgroundColor = [UIColor clearColor]; _backupTextView.delegate = self; _passwordTextField.delegate = self; _textViewBackground.layer.cornerRadius = 3; _passwordView.layer.cornerRadius = 3; _passwordView.layer.borderColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.1].CGColor; _passwordView.layer.borderWidth = 0.5; _passwordFieldBackground.layer.cornerRadius = 3; _scanView.layer.cornerRadius = 3; _scanView.layer.borderWidth = 1; _scanView.layer.borderColor = [Colors mainThemeDark].CGColor; _scanView.isAccessibilityElement = YES; _scanView.accessibilityTraits = UIAccessibilityTraitButton; _doneButton.layer.cornerRadius = 3; _cancelButton.layer.borderWidth = 1; _cancelButton.layer.borderColor = [Colors mainThemeDark].CGColor; _cancelButton.layer.cornerRadius = 3; [_doneButton setTitle:[BundleUtil localizedStringForKey:@"Done"] forState:UIControlStateNormal]; [_cancelButton setTitle:[BundleUtil localizedStringForKey:@"Cancel"] forState:UIControlStateNormal]; _doneButton.backgroundColor = [Colors mainThemeDark]; [_doneButton setTitleColor:[Colors white] forState:UIControlStateNormal]; [_cancelButton setTitleColor:[Colors mainThemeDark] forState:UIControlStateNormal]; NSString *placeholder = [BundleUtil localizedStringForKey:@"Password"]; _passwordTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName: THREEMA_COLOR_PLACEHOLDER}]; _backupLabel.verticalTextAlignment = SSLabelVerticalTextAlignmentTop; _backupLabel.text = [BundleUtil localizedStringForKey:@"id_backup_placeholder"]; _backupLabel.numberOfLines = 0; _titleLabel.text = [BundleUtil localizedStringForKey:@"restore_id_export"]; _titleLabel.accessibilityIdentifier = @"restore_id_export"; _scanImageView.image = [UIImage imageNamed:@"QRScan" inColor:[Colors mainThemeDark]]; _backupTextView.accessibilityIdentifier = @"backupTextView"; _backupTextView.tintColor = [Colors mainThemeDark]; _passwordTextField.tintColor = [Colors mainThemeDark]; _keyImageView.image = [UIImage imageNamed:@"Key" inColor:[UIColor whiteColor]]; UITapGestureRecognizer *mainTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedMainView:)]; [self.mainContentView addGestureRecognizer:mainTapGesture]; UISwipeGestureRecognizer *swipeGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeAction:)]; [self.view addGestureRecognizer:swipeGesture]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self refreshView]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self updateTextViewWithBackupCode]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self hideKeyboard]; } - (BOOL)shouldAutorotate { return YES; } -(UIInterfaceOrientationMask)supportedInterfaceOrientations { if (SYSTEM_IS_IPAD) { return UIInterfaceOrientationMaskAll; } return UIInterfaceOrientationMaskAllButUpsideDown; } - (void)textViewDidBeginEditing:(UITextView *)textView { [self refreshView]; } - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { if ([text isEqualToString:@"\n"]) { [_passwordTextField becomeFirstResponder]; return NO; } NSCharacterSet *allowedCharacters = [NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"]; if (range.length == 0 && text.length > 0 && ![allowedCharacters characterIsMember:[text characterAtIndex:0]]) { return NO; } return YES; } - (void)textViewDidChange:(UITextView *)textView { //format text into (XXXX-XXXX-XXXX...) NSString *hyphen = @"-"; NSString *rawText = [textView.text stringByReplacingOccurrencesOfString:hyphen withString:@""]; if (rawText.length >= 4) { NSString *newText = @""; NSUInteger index = 0; while (rawText.length > index) { NSRange range = NSMakeRange(index, (rawText.length - index) > 4 ? 4 : rawText.length - index); NSString *n = [rawText substringWithRange:range]; newText = [newText stringByAppendingString:n]; if (range.length == 4 && (rawText.length - index) > 4) { newText = [newText stringByAppendingString:hyphen]; } index += 4; } //calculate new cursor position NSRange cursorPos = textView.selectedRange; NSUInteger hyphenCount = [[textView.text componentsSeparatedByString:hyphen] count] - 1; if (cursorPos.location > (hyphenCount * 4)) { NSUInteger newHyphenCount = [[newText componentsSeparatedByString:hyphen] count] - 1; cursorPos.location += newHyphenCount - hyphenCount; } //update modified text and set cursor position textView.text = newText; textView.selectedRange = cursorPos; } _backupLabel.hidden = textView.text.length > 0 ? YES : NO; [self updateDoneEnabledWithPassword:_passwordTextField.text]; } - (void)textFieldDidBeginEditing:(UITextField *)textField { [self refreshView]; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [self doneAction:nil]; return YES; } - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { NSString *newText = [textField.text stringByReplacingCharactersInRange:range withString:string]; [self updateDoneEnabledWithPassword:newText]; return YES; } - (IBAction)cancelAction:(id)sender { if ([_delegate respondsToSelector:@selector(restoreIdentityCancelled)]) { [_delegate restoreIdentityCancelled]; } } - (IBAction)doneAction:(id)sender { [self hideKeyboard]; [MBProgressHUD showHUDAddedTo:self.view animated:YES]; MyIdentityStore *myIdentityStore = [MyIdentityStore sharedMyIdentityStore]; [myIdentityStore restoreFromBackup:_backupTextView.text withPassword:_passwordTextField.text onCompletion:^{ ServerAPIConnector *apiConnector = [[ServerAPIConnector alloc] init]; /* Obtain server group from server */ [apiConnector updateMyIdentityStore:myIdentityStore onCompletion:^{ [myIdentityStore storeInKeychain]; dispatch_async(dispatch_get_main_queue(), ^{ [MBProgressHUD hideHUDForView:self.view animated:YES]; }); if ([_delegate respondsToSelector:@selector(restoreIdentityDone)]) { [_delegate restoreIdentityDone]; } } onError:^(NSError *error) { [self handleError:error]; }]; } onError:^(NSError *error) { [self handleError:error]; }]; } - (void)showScanViewController { ScanBackupController *scanController = [[ScanBackupController alloc] init]; scanController.containingViewController = self; scanController.delegate = self; [scanController startScan]; } - (void)hideKeyboard { [_backupTextView resignFirstResponder]; [_passwordTextField resignFirstResponder]; } - (void)handleError:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ [self hideKeyboard]; [MBProgressHUD hideHUDForView:self.view animated:YES]; IntroQuestionView *view = (IntroQuestionView *)[NibUtil loadViewFromNibWithName:@"IntroQuestionView"]; view.showOnlyOkButton = YES; view.questionLabel.text = error.localizedDescription; view.delegate = self; view.frame = [RectUtil rect:view.frame centerIn:self.view.frame round:YES]; [self.view addSubview:view]; [self showMessageView:view]; }); } - (void)updateTextViewWithBackupCode { if (_backupTextView.text.length == 0) { if ([AppDelegate sharedAppDelegate].urlRestoreData != nil) { /* put the dashes back in */ _backupTextView.text = [[MyIdentityStore sharedMyIdentityStore] addBackupGroupDashes:[AppDelegate sharedAppDelegate].urlRestoreData]; _backupLabel.hidden = YES; [_passwordTextField becomeFirstResponder]; } else if (_backupData) { _backupTextView.text = [[MyIdentityStore sharedMyIdentityStore] addBackupGroupDashes:_backupData]; _backupLabel.hidden = YES; if (_passwordData) { _passwordTextField.text = _passwordData; } else { [_passwordTextField becomeFirstResponder]; } } else { [_backupTextView becomeFirstResponder]; } } [self updateDoneEnabledWithPassword:_passwordTextField.text]; } - (void)updateDoneEnabledWithPassword:(NSString*)password { /* enable done only if we have 50 bytes worth of Base32 data in backup data and a suitable password */ BOOL enabled = YES; if (password.length < kMinimumPasswordLength) { enabled = NO; } if (![[MyIdentityStore sharedMyIdentityStore] isValidBackupFormat:_backupTextView.text]) { enabled = NO; } if (enabled) { _passwordTextField.enablesReturnKeyAutomatically = YES; _doneButton.userInteractionEnabled = YES; _doneButton.alpha = 1.0; } else { _passwordTextField.enablesReturnKeyAutomatically = NO; _doneButton.userInteractionEnabled = NO; _doneButton.alpha = 0.4; } } - (void) refreshView { _scanView.hidden = _backupTextView.isFirstResponder && [ScanBackupController canScan] ? NO : YES; } #pragma mark - IntroQuestionViewDelegate - (void)selectedOk:(IntroQuestionView *)sender { [self hideMessageView:sender ignoreControls:YES]; [_passwordTextField becomeFirstResponder]; } #pragma mark Scan backup controller delegate - (void)didScanBackup:(NSString *)backup { _backupLabel.hidden = YES; _backupTextView.text = backup; } #pragma mark - UITapGestureRecognizer - (void)tappedScan:(UITapGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateEnded) { [self showScanViewController]; } } - (void)tappedMainView:(UITapGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateEnded) { [self hideKeyboard]; } } #pragma mark - UISwipeGestureRecognizer - (void)swipeAction:(UISwipeGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateEnded) { if ([_delegate respondsToSelector:@selector(restoreIdentityCancelled)]) { [_delegate restoreIdentityCancelled]; } } } @end