// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2015-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 "LinkIDViewController.h" #import "MyIdentityStore.h" #import "ServerAPIConnector.h" #import "NBMetadataHelper.h" #import "LinkIDCountryPickerRowView.h" #import "RectUtil.h" #import "PhoneNumberNormalizer.h" #import "UIDefines.h" #import "Utils.h" #import "IntroQuestionView.h" #import "LicenseStore.h" #import "MDMSetup.h" #import "UIImage+ColoredImage.h" #import "NibUtil.h" #define COUNTRY_ROW_HEIGHT 44.0 @interface LinkIDViewController () @property NBMetadataHelper *metaDataHelper; @property NSArray *allPhoneMetadata; @property NSDictionary *selectedPhoneMetadata; @property NSArray *countryNames; @property NSMutableDictionary *country2Region; @property NSString *currentCountry; @property MyIdentityStore *identityStore; @property BOOL didShowEmailWarning; @property BOOL didShowPhoneWarning; @property BOOL didAckInputWarning; @property CGFloat phoneViewYOffset; @property CGFloat countryViewYOffset; @property CGFloat emailViewYOffset; @property IntroQuestionView *questionView; @property BOOL restoredLinkedEmail; @property BOOL restoredLinkedPhone; @end @implementation LinkIDViewController - (void)viewDidLoad { [super viewDidLoad]; _identityStore = [MyIdentityStore sharedMyIdentityStore]; [self checkForRestoredLinkData]; [self setup]; [self setupCountrySelection]; _phoneViewYOffset = _phoneView.frame.origin.y; _countryViewYOffset = _countryView.frame.origin.y; _emailViewYOffset = _emailView.frame.origin.y; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; _identityStore.createIDEmail = _emailTextField.text; if (_restoredLinkedPhone) { _identityStore.createIDPhone = _phoneTextField.text; } else { NSString *phoneNumber = [self fullPhoneNumber]; if (phoneNumber) { _identityStore.createIDPhone = phoneNumber; } else { _identityStore.createIDPhone = nil; } } [self hideKeyboard]; } - (void)checkForRestoredLinkData { if (_identityStore.linkedEmail.length > 0) { _restoredLinkedEmail = YES; _emailTextField.userInteractionEnabled = NO; } if (_identityStore.linkedMobileNo.length > 0) { _restoredLinkedPhone = YES; _phoneTextField.userInteractionEnabled = NO; } } - (NSString *)fullPhoneNumber { if (_phoneTextField.text.length > 0) { NSString *regionalPart = [_phoneTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; return [NSString stringWithFormat:@"%@ %@", _countryCodeLabel.text, regionalPart]; } return nil; } - (void)hideKeyboard { [_emailTextField resignFirstResponder]; [_phoneTextField resignFirstResponder]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self updateView]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self registerForKeyboardNotifications]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; [self unregisterForKeyboardNotifications]; } - (NSString *)countryNameForRegion:(NSString *)region { id countryDictionaryInstance = [NSDictionary dictionaryWithObject:region forKey:NSLocaleCountryCode]; NSString *identifier = [NSLocale localeIdentifierFromComponents:countryDictionaryInstance]; NSString *country = [[NSLocale currentLocale] displayNameForKey:NSLocaleIdentifier value:identifier]; return country; } - (void)setupCountrySelection { NSMutableArray *countries = [NSMutableArray array]; NSMutableArray *codes = [NSMutableArray array]; _country2Region = [NSMutableDictionary dictionary]; [NBMetadataHelper.CCode2CNMap enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { NSString *countryName = [self countryNameForRegion:key]; if (countryName) { [countries addObject:countryName]; [codes addObject:obj]; [_country2Region setObject:key forKey:countryName]; } }]; _countryNames = [countries sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; _countryPicker.dataSource = self; _countryPicker.delegate = self; if (_restoredLinkedPhone == NO) { UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedCountry:)]; [_countryView addGestureRecognizer:tapGesture]; _countryView.userInteractionEnabled = YES; _countryView.isAccessibilityElement = YES; [_countryView setAccessibilityHint: [BundleUtil localizedStringForKey:@"tap_to_change"]]; UITapGestureRecognizer *codeTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedCountry:)]; [_countryCodeLabel addGestureRecognizer:codeTapGesture]; _countryCodeLabel.userInteractionEnabled = YES; _countryCodeLabel.isAccessibilityElement = YES; [_countryCodeLabel setAccessibilityHint: [BundleUtil localizedStringForKey:@"tap_to_change"]]; } UITapGestureRecognizer *mainTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedMainView:)]; [self.mainContentView addGestureRecognizer:mainTapGesture]; } - (void)adaptToSmallScreen { [super adaptToSmallScreen]; CGFloat yOffset = -36.0; _descriptionLabel.frame = [RectUtil offsetRect:_descriptionLabel.frame byX:0.0 byY:yOffset]; _emailView.frame = [RectUtil offsetRect:_emailView.frame byX:0.0 byY:yOffset]; _phoneView.frame = [RectUtil offsetRect:_phoneView.frame byX:0.0 byY:yOffset]; _countryView.frame = [RectUtil offsetRect:_countryView.frame byX:0.0 byY:yOffset]; yOffset -= 32.0; _countryPickerView.frame = [RectUtil offsetRect:_countryPickerView.frame byX:0.0 byY:yOffset]; } - (void)setup { _emailBackgroundView.layer.cornerRadius = 3; _phoneBackroundView.layer.cornerRadius = 3; _countryView.layer.cornerRadius = 3; _countryView.layer.borderColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.1].CGColor; _countryView.layer.borderWidth = 0.5; _emailView.layer.cornerRadius = 3; _emailView.layer.borderColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.1].CGColor; _emailView.layer.borderWidth = 0.5; _phoneView.layer.cornerRadius = 3; _phoneView.layer.borderColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.1].CGColor; _phoneView.layer.borderWidth = 0.5; _selectedCountryButton.layer.cornerRadius = 3; [_selectedCountryButton setTitle:[BundleUtil localizedStringForKey:@"ok"] forState:UIControlStateNormal]; NSString *emailPlaceholder = [BundleUtil localizedStringForKey:@"id_enter_email"]; _emailTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:emailPlaceholder attributes:@{NSForegroundColorAttributeName: THREEMA_COLOR_PLACEHOLDER}]; _emailTextField.delegate = self; _emailTextField.accessibilityHint = [BundleUtil localizedStringForKey:@"Email"]; _phoneTextField.delegate = self; _phoneTextField.accessibilityHint = [BundleUtil localizedStringForKey:@"phone number"]; if ([LicenseStore requiresLicenseKey]) { _descriptionLabel.text = [BundleUtil localizedStringForKey:@"id_link_description_work"]; } else { _descriptionLabel.text = [BundleUtil localizedStringForKey:@"id_link_description"]; } self.moreView.mainView = self.mainContentView; if ([LicenseStore requiresLicenseKey]) { _titleLabel.text = [BundleUtil localizedStringForKey:@"id_link_title_work"]; self.moreView.moreMessageText = [BundleUtil localizedStringForKey:@"more_information_link_id_work"]; } else { _titleLabel.text = [BundleUtil localizedStringForKey:@"id_link_title"]; self.moreView.moreMessageText = [BundleUtil localizedStringForKey:@"more_information_link_id"]; } _titleLabel.accessibilityIdentifier = @"id_link_title"; MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:YES]; if ([mdmSetup readonlyProfile]) { _emailTextField.enabled = NO; _phoneTextField.enabled = NO; } _emailTextField.tintColor = [Colors mainThemeDark]; _phoneTextField.tintColor = [Colors mainThemeDark]; _selectedCountryButton.backgroundColor = [Colors mainThemeDark]; [_selectedCountryButton setTitleColor:[Colors white] forState:UIControlStateNormal]; _phoneImageView.image = [UIImage imageNamed:@"Phone" inColor:[UIColor whiteColor]]; _mailImageView.image = [UIImage imageNamed:@"Mail" inColor:[UIColor whiteColor]]; } - (void)updatePhonePlaceholder { PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance]; NSString *region; if (_currentCountry) { region = [_country2Region objectForKey:_currentCountry]; } else { region = [PhoneNumberNormalizer userRegion]; } if (region) { [self updateUIWithRegion:region]; NSString *examplePhone = [normalizer exampleRegionalPhoneNumberForRegion:region]; if (examplePhone != nil) { NSString *placeholder = [NSString stringWithFormat:@"%@ %@", examplePhone, [BundleUtil localizedStringForKey:@"(optional)"]]; _phoneTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName: THREEMA_COLOR_PLACEHOLDER}]; } } } - (void)updateUIWithRegion:(NSString *)region { if (region) { NSString *countryName = [self countryNameForRegion:region]; _countryLabel.text = countryName; NSInteger row = [_countryNames indexOfObject:countryName]; if (row != NSNotFound) { NSString *codeString = [self codeForCountry:countryName]; _countryCodeLabel.text = codeString; _countryCodeLabel.accessibilityLabel = [NSString stringWithFormat:@"%@, %@", [BundleUtil localizedStringForKey:@"country_code"], codeString]; [_countryPicker selectRow:row inComponent:0 animated:NO]; } } } - (void)updateView { /* linked e-mail */ if (_restoredLinkedEmail) { // A linked email address was restored, but we don't know the actual address (linkedEmail will be "***@***"). _emailTextField.text = [BundleUtil localizedStringForKey:@"(linked)"]; } else if (_identityStore.createIDEmail) { self.emailTextField.text = _identityStore.createIDEmail; } /* linked mobile number */ if (_restoredLinkedPhone) { // A linked phone number was restored, but we don't know the actual number (linkedMobileNo will be "***"). _phoneTextField.text = [BundleUtil localizedStringForKey:@"(linked)"]; } else if (_identityStore.createIDPhone) { NSString *phoneNumber = _identityStore.createIDPhone; PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance]; NSString *region = [normalizer regionForPhoneNumber:phoneNumber]; if (region) { [self updateUIWithRegion:region]; NSString *regionalPart = [normalizer regionalPartForPhoneNumber:phoneNumber]; _phoneTextField.text = regionalPart; } } [self updatePhonePlaceholder]; [self hideEmailIfNeeded]; } - (void)showCountrySelector { [self hideKeyboard]; [self showMessageView:_countryPickerView]; } - (void)hideCountrySelector { [self hideMessageView:_countryPickerView]; } - (BOOL)isInputValid { [self hideKeyboard]; if ([self.moreView isShown]) { return NO; } if (_countryPickerView.hidden == NO) { return NO; } if ([self hasInput] == NO && _didAckInputWarning == NO) { [self showAlert]; return NO; } BOOL shouldPage = YES; if ([self validateEmail] == NO) { shouldPage = NO; } if ([self validatePhoneNumber] == NO) { shouldPage = NO; } return shouldPage; } - (BOOL)hasInput { if (_emailTextField.text.length > 0) { return YES; } if (_phoneTextField.text.length > 0) { return YES; } return NO; } - (BOOL)validateEmail { if (_restoredLinkedEmail) { return YES; } if (_emailTextField.text.length == 0) { _emailStateImageView.hidden = YES; return YES; } BOOL isValid = [Utils isValidEmail:_emailTextField.text]; if (isValid) { _emailStateImageView.hidden = YES; return YES; } else { _emailStateImageView.hidden = NO; } if (_didShowEmailWarning) { return YES; } else { _didShowEmailWarning = YES; return NO; } } - (BOOL)validatePhoneNumber { if (_restoredLinkedPhone) { return YES; } NSString *phone = [self fullPhoneNumber]; if (phone == nil) { _phoneStateImageView.hidden = YES; return YES; } PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance]; NSString *prettyMobileNo; NSString *mobileNo = [normalizer phoneNumberToE164:phone withDefaultRegion:[PhoneNumberNormalizer userRegion] prettyFormat:&prettyMobileNo]; if (mobileNo) { _phoneStateImageView.hidden = YES; return YES; } else { _phoneStateImageView.hidden = NO; } if (_didShowPhoneWarning) { return YES; } else { _didShowPhoneWarning = YES; return NO; } } - (NSString *)codeForCountry:(NSString *)countryName { NSString *region = [_country2Region objectForKey:countryName]; NSString *code = [NBMetadataHelper.CCode2CNMap objectForKey:region]; return [NSString stringWithFormat:@"+%@", code]; } - (void)hideAlert { [self hideMessageView:_questionView]; } - (void)showAlert { [self hideKeyboard]; if (_questionView == nil) { _questionView = (IntroQuestionView *)[NibUtil loadViewFromNibWithName:@"IntroQuestionView"]; if ([LicenseStore requiresLicenseKey]) { _questionView.questionLabel.text = [BundleUtil localizedStringForKey:@"id_link_no_input_work"]; } else { _questionView.questionLabel.text = [BundleUtil localizedStringForKey:@"id_link_no_input"]; } _questionView.delegate = self; _questionView.frame = [RectUtil rect:_questionView.frame centerIn:self.view.frame round:YES]; [self.view addSubview:_questionView]; } [self showMessageView:_questionView]; } - (void)hideEmailIfNeeded { _emailView.hidden = ![LicenseStore requiresLicenseKey]; } #pragma mark - IntroQuestionViewDelegate - (void)selectedYes:(IntroQuestionView *)sender { _didAckInputWarning = YES; [self hideAlert]; [self.containerDelegate pageLeft]; } - (void)selectedNo:(IntroQuestionView *)sender { [self hideAlert]; [_phoneTextField becomeFirstResponder]; } #pragma mark - UITapGestureRecognizer - (void)tappedCountry:(UITapGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateEnded) { [self showCountrySelector]; } } - (void)tappedMainView:(UITapGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateEnded) { [self hideKeyboard]; } } - (IBAction)selectedCountryAction:(id)sender { [self hideCountrySelector]; NSInteger row = [_countryPicker selectedRowInComponent:0]; NSString *name = [_countryNames objectAtIndex:row]; _countryLabel.text = name; _currentCountry = name; _countryCodeLabel.text = [self codeForCountry:name]; [self updatePhonePlaceholder]; } #pragma mark - UIPickerViewDataSource, UIPickerViewDelegate - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { return 1; } - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { return [_countryNames count]; } -(UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view { LinkIDCountryPickerRowView *rowView; if (view) { rowView = (LinkIDCountryPickerRowView *)view; } else { CGRect rect = CGRectMake(0.0, 0.0, _countryPicker.frame.size.width, COUNTRY_ROW_HEIGHT); rowView = [[LinkIDCountryPickerRowView alloc] initWithFrame:rect]; } NSString *name = [_countryNames objectAtIndex:row]; rowView.nameLabel.text = name; rowView.codeLabel.text = [self codeForCountry:name]; return rowView; } #pragma mark - UITextFieldDelegate - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { NSString *newText = [textField.text stringByReplacingCharactersInRange:range withString:string]; if (textField == _emailTextField && _didShowEmailWarning) { _emailTextField.text = newText; [self validateEmail]; return NO; } if (textField == _phoneTextField && _didShowPhoneWarning) { _phoneTextField.text = newText; [self validatePhoneNumber]; return NO; } return YES; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { if (textField == _emailTextField) { [_emailTextField resignFirstResponder]; return NO; } return YES; } # pragma mark Keyboard Notifications - (void)registerForKeyboardNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; } - (void)unregisterForKeyboardNotifications { [[NSNotificationCenter defaultCenter] removeObserver: self]; } - (void)keyboardWillShow: (NSNotification *) notification { NSDictionary* info = [notification userInfo]; CGRect keyboardRect = [[info objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGRect keyboardRectConverted = [self.view convertRect:keyboardRect fromView:nil]; UIView *responderView; if (_emailTextField.isFirstResponder) { responderView = _emailView; } else if (_phoneTextField.isFirstResponder){ responderView = _phoneView; } else { return; } _titleLabel.accessibilityLabel = [BundleUtil localizedStringForKey:@"Done"]; CGFloat diff = CGRectGetMinY(keyboardRectConverted) - CGRectGetMaxY(responderView.frame) - 32.0; if (diff < 0.0) { if (_emailTextField.isFirstResponder) { _phoneView.hidden = YES; _countryView.hidden = YES; } else if (_phoneTextField.isFirstResponder){ _emailView.hidden = YES; _countryView.hidden = YES; } NSTimeInterval animationDuration; UIViewAnimationOptions options = [Utils animationOptionsFor:notification animationDuration:&animationDuration]; [UIView animateWithDuration:animationDuration delay:0 options:options animations:^{ responderView.frame = [RectUtil offsetRect:responderView.frame byX:0.0 byY:diff]; } completion:^(BOOL finished) {}]; } } - (void)keyboardWillHide:(NSNotification*)notification { NSTimeInterval animationDuration; UIViewAnimationOptions options = [Utils animationOptionsFor:notification animationDuration:&animationDuration]; _titleLabel.accessibilityLabel = _titleLabel.text; [UIView animateWithDuration:animationDuration delay:0 options:options animations:^{ _phoneView.frame = [RectUtil setYPositionOf:_phoneView.frame y:_phoneViewYOffset]; _countryView.frame = [RectUtil setYPositionOf:_countryView.frame y:_countryViewYOffset]; _emailView.frame = [RectUtil setYPositionOf:_emailView.frame y:_emailViewYOffset]; } completion:^(BOOL finished) { [self hideEmailIfNeeded]; _countryView.hidden = NO; _phoneView.hidden = NO; }]; } @end