// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // 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 "CompletedIDViewController.h" #import "UserSettings.h" #import "ContactStore.h" #import "MyIdentityStore.h" #import "ServerAPIConnector.h" #import "PhoneNumberNormalizer.h" #import "RectUtil.h" #import "ProgressLabel.h" #import "IntroQuestionView.h" #import "WorkDataFetcher.h" #import "MDMSetup.h" #import "Threema-Swift.h" #import "ConversationUtils.h" #import "NibUtil.h" #import "AppDelegate.h" #define SYNC_TIMEOUT_S 10 @interface CompletedIDViewController () @property MyIdentityStore *identityStore; @property dispatch_semaphore_t syncEmailSemaphore; @property dispatch_semaphore_t syncPhoneSemaphore; @property dispatch_semaphore_t syncAdressBookSemaphore; @property ProgressLabel *emailProgressLabel; @property ProgressLabel *phoneProgressLabel; @property ProgressLabel *syncProgressLabel; @property BOOL isProcessing; @property BOOL hasErrors; @property NSString *phoneNumber; @end @implementation CompletedIDViewController - (void)viewDidLoad { [super viewDidLoad]; _identityStore = [MyIdentityStore sharedMyIdentityStore]; [self setup]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self updateData]; MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:YES]; if ([mdmSetup skipWizard]) { [self finishAction:nil]; } } - (void)updateData { NSString *nickname = _identityStore.pushFromName; if (nickname.length == 0) { nickname = _identityStore.identity; } self.nicknameValue.text = nickname; if (_identityStore.createIDEmail.length > 0) { self.emailValue.text = _identityStore.createIDEmail; } else { self.emailValue.text = @"-"; } if (_identityStore.linkedMobileNo) { self.phoneValue.text = _identityStore.createIDPhone; } else if (_identityStore.createIDPhone.length > 0) { PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance]; NSString *prettyMobileNo; _phoneNumber = [normalizer phoneNumberToE164:_identityStore.createIDPhone withDefaultRegion:[PhoneNumberNormalizer userRegion] prettyFormat:&prettyMobileNo]; self.phoneValue.text = prettyMobileNo; } else { self.phoneValue.text = @"-"; } if ([UserSettings sharedUserSettings].syncContacts) { self.syncContactValue.text = [BundleUtil localizedStringForKey:@"On"]; } else { self.syncContactValue.text = [BundleUtil localizedStringForKey:@"Off"]; } if (_identityStore.tempSafePassword != nil && _identityStore.tempSafePassword.length > 0) { self.enableSafeValue.text = [BundleUtil localizedStringForKey:@"On"]; } else { self.enableSafeValue.text = [BundleUtil localizedStringForKey:@"Off"]; } } - (void)adaptToSmallScreen { [super adaptToSmallScreen]; CGFloat yOffset = -28.0; _nickNameView.frame = [RectUtil offsetRect:_nickNameView.frame byX:0.0 byY:yOffset]; yOffset -= 8.0; _linkedToView.frame = [RectUtil offsetRect:_linkedToView.frame byX:0.0 byY:yOffset]; yOffset -= 8.0; _syncContactsView.frame = [RectUtil offsetRect:_syncContactsView.frame byX:0.0 byY:yOffset]; yOffset -= 16.0; _finishButton.frame = [RectUtil offsetRect:_finishButton.frame byX:0.0 byY:yOffset]; } - (void)setup { _finishButton.layer.cornerRadius = 3; _titleLabel.text = [BundleUtil localizedStringForKey:@"id_completed_title"]; _nickNameLabel.text = [BundleUtil localizedStringForKey:@"id_completed_nickname"]; _linkedToLabel.text = [BundleUtil localizedStringForKey:@"id_completed_linked_to"]; _syncContactsLabel.text = [BundleUtil localizedStringForKey:@"id_completed_sync_contacts"]; _enableSafeLabel.text = [BundleUtil localizedStringForKey:@"safe_setup_backup_title"]; [_finishButton setTitle:[BundleUtil localizedStringForKey:@"finish"] forState:UIControlStateNormal]; self.scrollView.contentSize = self.mainContentView.frame.size; _finishButton.backgroundColor = [Colors mainThemeDark]; [_finishButton setTitleColor:[Colors white] forState:UIControlStateNormal]; if ([AppDelegate hasBottomSafeAreaInsets]) { CGFloat iPadSpace = SYSTEM_IS_IPAD ? 50 : 0; _finishButton.frame = CGRectMake(_finishButton.frame.origin.x, _finishButton.frame.origin.y - 20.0 - iPadSpace, _finishButton.frame.size.width, _finishButton.frame.size.height); } UIImage *contactImage = [BundleUtil imageNamed:@"Contact"]; _contactImageView.image = [contactImage imageWithTint:[Colors white]]; _phoneImageView.image = [UIImage imageNamed:@"Phone" inColor:[Colors white]]; _mailImageView.image = [UIImage imageNamed:@"Mail" inColor:[Colors white]]; if (![LicenseStore requiresLicenseKey]) { _emailView.hidden = YES; } } - (void)resetPersistedIDCreateState { _identityStore.pendingCreateID = NO; } - (BOOL)linkEmail { if (_identityStore.createIDEmail.length < 1) { return NO; } if (_identityStore.linkedEmail.length > 0) { return NO; } _syncEmailSemaphore = dispatch_semaphore_create(0); NSString *email = _identityStore.createIDEmail; dispatch_async(dispatch_get_main_queue(), ^{ [self showEmailProgress]; }); ServerAPIConnector *conn = [[ServerAPIConnector alloc] init]; [conn linkEmailWithStore:[MyIdentityStore sharedMyIdentityStore] email:email onCompletion:^(BOOL linked) { dispatch_async(dispatch_get_main_queue(), ^{ [_emailProgressLabel hideActivityIndicator]; if (!linked) { NSString *message = [BundleUtil localizedStringForKey:@"link_email_sent_title"]; [_emailProgressLabel showSuccessMessage:message]; _emailProgressLabel.userInteractionEnabled = NO; } }); [self signalSemaphore:_syncEmailSemaphore]; } onError:^(NSError *error) { _hasErrors = YES; dispatch_async(dispatch_get_main_queue(), ^{ [_emailProgressLabel showErrorMessage:error.localizedDescription]; }); [self signalSemaphore:_syncEmailSemaphore]; }]; dispatch_semaphore_wait(_syncEmailSemaphore, dispatch_time(DISPATCH_TIME_NOW, SYNC_TIMEOUT_S * NSEC_PER_SEC)); return YES; } - (BOOL)linkPhone { if (_identityStore.createIDPhone.length < 1) { return NO; } if (_identityStore.linkedMobileNo.length > 0) { return NO; } _syncPhoneSemaphore = dispatch_semaphore_create(0); if (_phoneNumber == nil) { // no normalized phone number - ignore (or try _identityStore.createIDPhone??) return NO; } dispatch_async(dispatch_get_main_queue(), ^{ [self showPhoneProgress]; }); ServerAPIConnector *conn = [[ServerAPIConnector alloc] init]; [conn linkMobileNoWithStore:[MyIdentityStore sharedMyIdentityStore] mobileNo:_phoneNumber onCompletion:^(BOOL linked) { dispatch_async(dispatch_get_main_queue(), ^{ [_phoneProgressLabel hideActivityIndicator]; NSString *message = [BundleUtil localizedStringForKey:@"linking_phone_sms_sent"]; [_phoneProgressLabel showSuccessMessage:message]; _phoneProgressLabel.userInteractionEnabled = NO; }); [self signalSemaphore:_syncPhoneSemaphore]; } onError:^(NSError *error) { _hasErrors = YES; dispatch_async(dispatch_get_main_queue(), ^{ [_phoneProgressLabel showErrorMessage:error.localizedDescription]; }); [self signalSemaphore:_syncPhoneSemaphore]; }]; dispatch_semaphore_wait(_syncPhoneSemaphore, dispatch_time(DISPATCH_TIME_NOW, SYNC_TIMEOUT_S * NSEC_PER_SEC)); return YES; } - (BOOL)syncAdressBook { if ([UserSettings sharedUserSettings].syncContacts == NO) { return NO; } _syncAdressBookSemaphore = dispatch_semaphore_create(0); dispatch_async(dispatch_get_main_queue(), ^{ [self showProgress:_syncContactsView progressValue:_syncContactValue progressText:[BundleUtil localizedStringForKey:@"syncing_contacts"]]; }); [[ContactStore sharedContactStore] synchronizeAddressBookForceFullSync:YES onCompletion:^(BOOL addressBookAccessGranted) { dispatch_async(dispatch_get_main_queue(), ^{ NSString *message = [BundleUtil localizedStringForKey:@"Done"]; [_syncProgressLabel showSuccessMessage:message]; }); [self signalSemaphore:_syncAdressBookSemaphore]; } onError:^(NSError *error) { _hasErrors = YES; dispatch_async(dispatch_get_main_queue(), ^{ [_syncProgressLabel showErrorMessage:error.localizedDescription]; }); [self signalSemaphore:_syncAdressBookSemaphore]; }]; dispatch_semaphore_wait(_syncAdressBookSemaphore, dispatch_time(DISPATCH_TIME_NOW, SYNC_TIMEOUT_S * NSEC_PER_SEC)); return YES; } - (BOOL)enableSafe { SafeConfigManager *safeConfigManager = [[SafeConfigManager alloc] init]; SafeStore *safeStore = [[SafeStore alloc] initWithSafeConfigManagerAsObject:safeConfigManager serverApiConnector:[[ServerAPIConnector alloc] init]]; SafeManager *safeManager = [[SafeManager alloc] initWithSafeConfigManagerAsObject:safeConfigManager safeStore:safeStore safeApiService:[[SafeApiService alloc] init]]; // apply Threema Safe password and server config from MDM MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:YES]; if ([mdmSetup isSafeBackupPasswordPreset]) { self.identityStore.tempSafePassword = [mdmSetup safePassword]; } if ([mdmSetup isSafeBackupServerPreset]) { [safeConfigManager setCustomServer:[mdmSetup safeServerUrl]]; [safeConfigManager setServer:[safeStore composeSafeServerAuthWithServer:[mdmSetup safeServerUrl] user:[mdmSetup safeServerUsername] password:[mdmSetup safeServerPassword]].absoluteString]; } if (self.identityStore.tempSafePassword == nil || self.identityStore.tempSafePassword.length < 8) { [safeManager deactivate]; return NO; } dispatch_async(dispatch_get_main_queue(), ^{ [self showProgress:_enableSafeView progressValue:_enableSafeValue progressText:[BundleUtil localizedStringForKey:@"safe_preparing"]]; }); NSError *__autoreleasing _Nullable * _Nullable error = NULL; dispatch_sync(dispatch_get_main_queue(), ^{ [safeManager activateWithIdentity:self.identityStore.identity password:self.identityStore.tempSafePassword error:error]; }); if (error != nil) { _hasErrors = YES; return NO; } else { return YES; } } - (void)signalSemaphore:(dispatch_semaphore_t)semaphore { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ dispatch_semaphore_signal(semaphore); }); } - (void)arrangeViewsForCompletion { CGFloat addedPhoneHeight = _phoneView.frame.size.height; CGFloat addedEmailHeight = _emailView.frame.size.height; // double size of email & phone fields _phoneView.frame = [RectUtil changeSizeOf:_phoneView.frame deltaX:0.0 deltaY:addedPhoneHeight]; _emailView.frame = [RectUtil changeSizeOf:_emailView.frame deltaX:0.0 deltaY:addedEmailHeight]; _linkedToView.frame = [RectUtil changeSizeOf:_linkedToView.frame deltaX:0.0 deltaY:addedPhoneHeight + addedEmailHeight]; [UIView animateWithDuration:0.4 delay:0 options:0 animations:^{ //hide title and increase content (scroll) section _titleLabel.alpha = 0.0; _scrollView.frame = CGRectMake(_scrollView.frame.origin.x, _scrollView.frame.origin.y - _titleLabel.frame.size.height, _scrollView.frame.size.width, _scrollView.frame.size.height + _titleLabel.frame.size.height); CGFloat yOffset = 0; _nickNameView.frame = [RectUtil offsetRect:_nickNameView.frame byX:0.0 byY:yOffset]; _linkedToView.frame = [RectUtil offsetRect:_linkedToView.frame byX:0.0 byY:yOffset]; _emailView.frame = [RectUtil offsetRect:_emailView.frame byX:0.0 byY:addedPhoneHeight]; _syncContactsView.frame = [RectUtil offsetRect:_syncContactsView.frame byX:0.0 byY:addedPhoneHeight]; _enableSafeView.frame = [RectUtil offsetRect:_enableSafeView.frame byX:0.0 byY:addedPhoneHeight]; _finishButton.frame = [RectUtil offsetRect:_finishButton.frame byX:0.0 byY:-yOffset]; [self.containerDelegate hideControls:YES]; } completion:nil]; } - (void)showEmailProgress { CGFloat yPos = CGRectGetMaxY(_emailValue.frame); CGRect labelRect = [RectUtil setYPositionOf:_emailValue.frame y:yPos]; _emailProgressLabel = [[ProgressLabel alloc] initWithFrame:labelRect]; _emailProgressLabel.backgroundColor = [UIColor clearColor]; _emailProgressLabel.font = [_emailValue.font fontWithSize:14.0]; _emailProgressLabel.textColor = _emailValue.textColor; _emailProgressLabel.alpha = 0.0; _emailProgressLabel.text = [BundleUtil localizedStringForKey:@"linking_email"]; UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedEmailProgress)]; [_emailProgressLabel addGestureRecognizer:tapGesture]; [_emailView addSubview:_emailProgressLabel]; [UIView animateWithDuration:0.4 delay:0 options:0 animations:^{ _emailProgressLabel.alpha = 1.0; [_emailProgressLabel showActivityIndicator]; } completion:nil]; } - (void)showPhoneProgress { CGFloat yPos = CGRectGetMaxY(_phoneValue.frame); CGRect labelRect = [RectUtil setYPositionOf:_phoneValue.frame y:yPos]; _phoneProgressLabel = [[ProgressLabel alloc] initWithFrame:labelRect]; _phoneProgressLabel.backgroundColor = [UIColor clearColor]; _phoneProgressLabel.font = [_phoneValue.font fontWithSize:14.0]; _phoneProgressLabel.textColor = _phoneValue.textColor; _phoneProgressLabel.alpha = 0.0; _phoneProgressLabel.text = [BundleUtil localizedStringForKey:@"linking_phone"]; UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedPhoneProgress)]; [_phoneProgressLabel addGestureRecognizer:tapGesture]; [_phoneView addSubview:_phoneProgressLabel]; [UIView animateWithDuration:0.4 delay:0 options:0 animations:^{ _phoneProgressLabel.alpha = 1.0; [_phoneProgressLabel showActivityIndicator]; } completion:nil]; } - (void) showProgress:(UIView*)container progressValue:(UILabel*)value progressText:(NSString*)text { CGRect labelRect = value.frame; _syncProgressLabel = [[ProgressLabel alloc] initWithFrame:labelRect]; _syncProgressLabel.backgroundColor = [UIColor clearColor]; _syncProgressLabel.font = [value.font fontWithSize:14.0]; _syncProgressLabel.textColor = value.textColor; _syncProgressLabel.alpha = 0.0; _syncProgressLabel.text = text; [container addSubview:_syncProgressLabel]; [UIView animateWithDuration:0.4 delay:0 options:0 animations:^{ value.alpha = 0.0; _syncProgressLabel.alpha = 1.0; [_syncProgressLabel showActivityIndicator]; } completion:nil]; } - (BOOL)isInputValid { if (_isProcessing) { return NO; } return YES; } - (IBAction)finishAction:(id)sender { if (_isProcessing == NO) { _isProcessing = YES; [self startCompletionProcess]; } else { [self finishCompletionProcess]; } } - (void)startCompletionProcess { _finishButton.userInteractionEnabled = NO; _finishButton.alpha = 0.0; [self resetPersistedIDCreateState]; [self arrangeViewsForCompletion]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ NSInteger timout = 2000; if ([self linkPhone]) { timout += 1000; } if ([self linkEmail]) { timout += 1000; } if ([self syncAdressBook]) { timout += 500; } if ([self enableSafe]) { timout += 500; } // do not show Threema Safe Intro after setup wizard [[UserSettings sharedUserSettings] setSafeIntroShown:YES]; if (_hasErrors == NO) { // no errors - continue after timeout dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timout * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ [_delegate completedIDSetup]; }); } else { // user needs to confirm dispatch_async(dispatch_get_main_queue(), ^{ [_finishButton setTitle:[BundleUtil localizedStringForKey:@"Done"] forState:UIControlStateNormal]; _finishButton.userInteractionEnabled = YES; _finishButton.alpha = 1.0; }); } _identityStore.createIDPhone = nil; _identityStore.createIDEmail = nil; _identityStore.tempSafePassword = nil; }); } - (void)finishCompletionProcess { dispatch_async(dispatch_get_main_queue(), ^{ [_delegate completedIDSetup]; }); } #pragma mark - gesture recognizers - (void)tappedPhoneProgress { [self showErrorText:_phoneProgressLabel.text]; } - (void)tappedEmailProgress { [self showErrorText:_emailProgressLabel.text]; } - (void)showErrorText:(NSString *)errorText { IntroQuestionView *view = (IntroQuestionView *)[NibUtil loadViewFromNibWithName:@"IntroQuestionView"]; view.showOnlyOkButton = YES; view.questionLabel.text = errorText; view.delegate = self; view.frame = [RectUtil rect:view.frame centerIn:self.view.frame round:YES]; [self.view addSubview:view]; [self showMessageView:view]; } - (void)selectedOk:(IntroQuestionView *)sender { [self hideMessageView:sender ignoreControls:YES]; } @end