CompletedIDViewController.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2015-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "CompletedIDViewController.h"
  21. #import "UserSettings.h"
  22. #import "ContactStore.h"
  23. #import "MyIdentityStore.h"
  24. #import "ServerAPIConnector.h"
  25. #import "PhoneNumberNormalizer.h"
  26. #import "RectUtil.h"
  27. #import "ProgressLabel.h"
  28. #import "IntroQuestionView.h"
  29. #import "WorkDataFetcher.h"
  30. #import "MDMSetup.h"
  31. #import "Threema-Swift.h"
  32. #import "ConversationUtils.h"
  33. #import "NibUtil.h"
  34. #import "AppDelegate.h"
  35. #define SYNC_TIMEOUT_S 10
  36. @interface CompletedIDViewController () <IntroQuestionDelegate>
  37. @property MyIdentityStore *identityStore;
  38. @property dispatch_semaphore_t syncEmailSemaphore;
  39. @property dispatch_semaphore_t syncPhoneSemaphore;
  40. @property dispatch_semaphore_t syncAdressBookSemaphore;
  41. @property ProgressLabel *emailProgressLabel;
  42. @property ProgressLabel *phoneProgressLabel;
  43. @property ProgressLabel *syncProgressLabel;
  44. @property BOOL isProcessing;
  45. @property BOOL hasErrors;
  46. @property NSString *phoneNumber;
  47. @end
  48. @implementation CompletedIDViewController
  49. - (void)viewDidLoad {
  50. [super viewDidLoad];
  51. _identityStore = [MyIdentityStore sharedMyIdentityStore];
  52. [self setup];
  53. }
  54. - (void)viewWillAppear:(BOOL)animated {
  55. [super viewWillAppear:animated];
  56. [self updateData];
  57. MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:YES];
  58. if ([mdmSetup skipWizard]) {
  59. [self finishAction:nil];
  60. }
  61. }
  62. - (void)updateData {
  63. NSString *nickname = _identityStore.pushFromName;
  64. if (nickname.length == 0) {
  65. nickname = _identityStore.identity;
  66. }
  67. self.nicknameValue.text = nickname;
  68. if (_identityStore.createIDEmail.length > 0) {
  69. self.emailValue.text = _identityStore.createIDEmail;
  70. } else {
  71. self.emailValue.text = @"-";
  72. }
  73. if (_identityStore.linkedMobileNo) {
  74. self.phoneValue.text = _identityStore.createIDPhone;
  75. } else if (_identityStore.createIDPhone.length > 0) {
  76. PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance];
  77. NSString *prettyMobileNo;
  78. _phoneNumber = [normalizer phoneNumberToE164:_identityStore.createIDPhone withDefaultRegion:[PhoneNumberNormalizer userRegion] prettyFormat:&prettyMobileNo];
  79. self.phoneValue.text = prettyMobileNo;
  80. } else {
  81. self.phoneValue.text = @"-";
  82. }
  83. if ([UserSettings sharedUserSettings].syncContacts) {
  84. self.syncContactValue.text = [BundleUtil localizedStringForKey:@"On"];
  85. } else {
  86. self.syncContactValue.text = [BundleUtil localizedStringForKey:@"Off"];
  87. }
  88. if (_identityStore.tempSafePassword != nil && _identityStore.tempSafePassword.length > 0) {
  89. self.enableSafeValue.text = [BundleUtil localizedStringForKey:@"On"];
  90. } else {
  91. self.enableSafeValue.text = [BundleUtil localizedStringForKey:@"Off"];
  92. }
  93. }
  94. - (void)adaptToSmallScreen {
  95. [super adaptToSmallScreen];
  96. CGFloat yOffset = -28.0;
  97. _nickNameView.frame = [RectUtil offsetRect:_nickNameView.frame byX:0.0 byY:yOffset];
  98. yOffset -= 8.0;
  99. _linkedToView.frame = [RectUtil offsetRect:_linkedToView.frame byX:0.0 byY:yOffset];
  100. yOffset -= 8.0;
  101. _syncContactsView.frame = [RectUtil offsetRect:_syncContactsView.frame byX:0.0 byY:yOffset];
  102. yOffset -= 16.0;
  103. _finishButton.frame = [RectUtil offsetRect:_finishButton.frame byX:0.0 byY:yOffset];
  104. }
  105. - (void)setup {
  106. _finishButton.layer.cornerRadius = 3;
  107. _titleLabel.text = [BundleUtil localizedStringForKey:@"id_completed_title"];
  108. _nickNameLabel.text = [BundleUtil localizedStringForKey:@"id_completed_nickname"];
  109. _linkedToLabel.text = [BundleUtil localizedStringForKey:@"id_completed_linked_to"];
  110. _syncContactsLabel.text = [BundleUtil localizedStringForKey:@"id_completed_sync_contacts"];
  111. _enableSafeLabel.text = [BundleUtil localizedStringForKey:@"safe_setup_backup_title"];
  112. [_finishButton setTitle:[BundleUtil localizedStringForKey:@"finish"] forState:UIControlStateNormal];
  113. self.scrollView.contentSize = self.mainContentView.frame.size;
  114. _finishButton.backgroundColor = [Colors mainThemeDark];
  115. [_finishButton setTitleColor:[Colors white] forState:UIControlStateNormal];
  116. if ([AppDelegate hasBottomSafeAreaInsets]) {
  117. CGFloat iPadSpace = SYSTEM_IS_IPAD ? 50 : 0;
  118. _finishButton.frame = CGRectMake(_finishButton.frame.origin.x, _finishButton.frame.origin.y - 20.0 - iPadSpace, _finishButton.frame.size.width, _finishButton.frame.size.height);
  119. }
  120. UIImage *contactImage = [BundleUtil imageNamed:@"Contact"];
  121. _contactImageView.image = [contactImage imageWithTint:[Colors white]];
  122. _phoneImageView.image = [UIImage imageNamed:@"Phone" inColor:[Colors white]];
  123. _mailImageView.image = [UIImage imageNamed:@"Mail" inColor:[Colors white]];
  124. if (![LicenseStore requiresLicenseKey]) {
  125. _emailView.hidden = YES;
  126. }
  127. }
  128. - (void)resetPersistedIDCreateState {
  129. _identityStore.pendingCreateID = NO;
  130. }
  131. - (BOOL)linkEmail {
  132. if (_identityStore.createIDEmail.length < 1) {
  133. return NO;
  134. }
  135. if (_identityStore.linkedEmail.length > 0) {
  136. return NO;
  137. }
  138. _syncEmailSemaphore = dispatch_semaphore_create(0);
  139. NSString *email = _identityStore.createIDEmail;
  140. dispatch_async(dispatch_get_main_queue(), ^{
  141. [self showEmailProgress];
  142. });
  143. ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
  144. [conn linkEmailWithStore:[MyIdentityStore sharedMyIdentityStore] email:email onCompletion:^(BOOL linked) {
  145. dispatch_async(dispatch_get_main_queue(), ^{
  146. [_emailProgressLabel hideActivityIndicator];
  147. if (!linked) {
  148. NSString *message = [BundleUtil localizedStringForKey:@"link_email_sent_title"];
  149. [_emailProgressLabel showSuccessMessage:message];
  150. _emailProgressLabel.userInteractionEnabled = NO;
  151. }
  152. });
  153. [self signalSemaphore:_syncEmailSemaphore];
  154. } onError:^(NSError *error) {
  155. _hasErrors = YES;
  156. dispatch_async(dispatch_get_main_queue(), ^{
  157. [_emailProgressLabel showErrorMessage:error.localizedDescription];
  158. });
  159. [self signalSemaphore:_syncEmailSemaphore];
  160. }];
  161. dispatch_semaphore_wait(_syncEmailSemaphore, dispatch_time(DISPATCH_TIME_NOW, SYNC_TIMEOUT_S * NSEC_PER_SEC));
  162. return YES;
  163. }
  164. - (BOOL)linkPhone {
  165. if (_identityStore.createIDPhone.length < 1) {
  166. return NO;
  167. }
  168. if (_identityStore.linkedMobileNo.length > 0) {
  169. return NO;
  170. }
  171. _syncPhoneSemaphore = dispatch_semaphore_create(0);
  172. if (_phoneNumber == nil) {
  173. // no normalized phone number - ignore (or try _identityStore.createIDPhone??)
  174. return NO;
  175. }
  176. dispatch_async(dispatch_get_main_queue(), ^{
  177. [self showPhoneProgress];
  178. });
  179. ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
  180. [conn linkMobileNoWithStore:[MyIdentityStore sharedMyIdentityStore] mobileNo:_phoneNumber onCompletion:^(BOOL linked) {
  181. dispatch_async(dispatch_get_main_queue(), ^{
  182. [_phoneProgressLabel hideActivityIndicator];
  183. NSString *message = [BundleUtil localizedStringForKey:@"linking_phone_sms_sent"];
  184. [_phoneProgressLabel showSuccessMessage:message];
  185. _phoneProgressLabel.userInteractionEnabled = NO;
  186. });
  187. [self signalSemaphore:_syncPhoneSemaphore];
  188. } onError:^(NSError *error) {
  189. _hasErrors = YES;
  190. dispatch_async(dispatch_get_main_queue(), ^{
  191. [_phoneProgressLabel showErrorMessage:error.localizedDescription];
  192. });
  193. [self signalSemaphore:_syncPhoneSemaphore];
  194. }];
  195. dispatch_semaphore_wait(_syncPhoneSemaphore, dispatch_time(DISPATCH_TIME_NOW, SYNC_TIMEOUT_S * NSEC_PER_SEC));
  196. return YES;
  197. }
  198. - (BOOL)syncAdressBook {
  199. if ([UserSettings sharedUserSettings].syncContacts == NO) {
  200. return NO;
  201. }
  202. _syncAdressBookSemaphore = dispatch_semaphore_create(0);
  203. dispatch_async(dispatch_get_main_queue(), ^{
  204. [self showProgress:_syncContactsView progressValue:_syncContactValue progressText:[BundleUtil localizedStringForKey:@"syncing_contacts"]];
  205. });
  206. [[ContactStore sharedContactStore] synchronizeAddressBookForceFullSync:YES onCompletion:^(BOOL addressBookAccessGranted) {
  207. dispatch_async(dispatch_get_main_queue(), ^{
  208. NSString *message = [BundleUtil localizedStringForKey:@"Done"];
  209. [_syncProgressLabel showSuccessMessage:message];
  210. });
  211. [self signalSemaphore:_syncAdressBookSemaphore];
  212. } onError:^(NSError *error) {
  213. _hasErrors = YES;
  214. dispatch_async(dispatch_get_main_queue(), ^{
  215. [_syncProgressLabel showErrorMessage:error.localizedDescription];
  216. });
  217. [self signalSemaphore:_syncAdressBookSemaphore];
  218. }];
  219. dispatch_semaphore_wait(_syncAdressBookSemaphore, dispatch_time(DISPATCH_TIME_NOW, SYNC_TIMEOUT_S * NSEC_PER_SEC));
  220. return YES;
  221. }
  222. - (BOOL)enableSafe {
  223. SafeConfigManager *safeConfigManager = [[SafeConfigManager alloc] init];
  224. SafeStore *safeStore = [[SafeStore alloc] initWithSafeConfigManagerAsObject:safeConfigManager serverApiConnector:[[ServerAPIConnector alloc] init]];
  225. SafeManager *safeManager = [[SafeManager alloc] initWithSafeConfigManagerAsObject:safeConfigManager safeStore:safeStore safeApiService:[[SafeApiService alloc] init]];
  226. // apply Threema Safe password and server config from MDM
  227. MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:YES];
  228. if ([mdmSetup isSafeBackupPasswordPreset]) {
  229. self.identityStore.tempSafePassword = [mdmSetup safePassword];
  230. }
  231. if ([mdmSetup isSafeBackupServerPreset]) {
  232. [safeConfigManager setCustomServer:[mdmSetup safeServerUrl]];
  233. [safeConfigManager setServer:[safeStore composeSafeServerAuthWithServer:[mdmSetup safeServerUrl] user:[mdmSetup safeServerUsername] password:[mdmSetup safeServerPassword]].absoluteString];
  234. }
  235. if (self.identityStore.tempSafePassword == nil || self.identityStore.tempSafePassword.length < 8) {
  236. [safeManager deactivate];
  237. return NO;
  238. }
  239. dispatch_async(dispatch_get_main_queue(), ^{
  240. [self showProgress:_enableSafeView progressValue:_enableSafeValue progressText:[BundleUtil localizedStringForKey:@"safe_preparing"]];
  241. });
  242. NSError *__autoreleasing _Nullable * _Nullable error = NULL;
  243. dispatch_sync(dispatch_get_main_queue(), ^{
  244. [safeManager activateWithIdentity:self.identityStore.identity password:self.identityStore.tempSafePassword error:error];
  245. });
  246. if (error != nil) {
  247. _hasErrors = YES;
  248. return NO;
  249. } else {
  250. return YES;
  251. }
  252. }
  253. - (void)signalSemaphore:(dispatch_semaphore_t)semaphore {
  254. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
  255. dispatch_semaphore_signal(semaphore);
  256. });
  257. }
  258. - (void)arrangeViewsForCompletion {
  259. CGFloat addedPhoneHeight = _phoneView.frame.size.height;
  260. CGFloat addedEmailHeight = _emailView.frame.size.height;
  261. // double size of email & phone fields
  262. _phoneView.frame = [RectUtil changeSizeOf:_phoneView.frame deltaX:0.0 deltaY:addedPhoneHeight];
  263. _emailView.frame = [RectUtil changeSizeOf:_emailView.frame deltaX:0.0 deltaY:addedEmailHeight];
  264. _linkedToView.frame = [RectUtil changeSizeOf:_linkedToView.frame deltaX:0.0 deltaY:addedPhoneHeight + addedEmailHeight];
  265. [UIView animateWithDuration:0.4 delay:0 options:0 animations:^{
  266. //hide title and increase content (scroll) section
  267. _titleLabel.alpha = 0.0;
  268. _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);
  269. CGFloat yOffset = 0;
  270. _nickNameView.frame = [RectUtil offsetRect:_nickNameView.frame byX:0.0 byY:yOffset];
  271. _linkedToView.frame = [RectUtil offsetRect:_linkedToView.frame byX:0.0 byY:yOffset];
  272. _emailView.frame = [RectUtil offsetRect:_emailView.frame byX:0.0 byY:addedPhoneHeight];
  273. _syncContactsView.frame = [RectUtil offsetRect:_syncContactsView.frame byX:0.0 byY:addedPhoneHeight];
  274. _enableSafeView.frame = [RectUtil offsetRect:_enableSafeView.frame byX:0.0 byY:addedPhoneHeight];
  275. _finishButton.frame = [RectUtil offsetRect:_finishButton.frame byX:0.0 byY:-yOffset];
  276. [self.containerDelegate hideControls:YES];
  277. } completion:nil];
  278. }
  279. - (void)showEmailProgress {
  280. CGFloat yPos = CGRectGetMaxY(_emailValue.frame);
  281. CGRect labelRect = [RectUtil setYPositionOf:_emailValue.frame y:yPos];
  282. _emailProgressLabel = [[ProgressLabel alloc] initWithFrame:labelRect];
  283. _emailProgressLabel.backgroundColor = [UIColor clearColor];
  284. _emailProgressLabel.font = [_emailValue.font fontWithSize:14.0];
  285. _emailProgressLabel.textColor = _emailValue.textColor;
  286. _emailProgressLabel.alpha = 0.0;
  287. _emailProgressLabel.text = [BundleUtil localizedStringForKey:@"linking_email"];
  288. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedEmailProgress)];
  289. [_emailProgressLabel addGestureRecognizer:tapGesture];
  290. [_emailView addSubview:_emailProgressLabel];
  291. [UIView animateWithDuration:0.4 delay:0 options:0 animations:^{
  292. _emailProgressLabel.alpha = 1.0;
  293. [_emailProgressLabel showActivityIndicator];
  294. } completion:nil];
  295. }
  296. - (void)showPhoneProgress {
  297. CGFloat yPos = CGRectGetMaxY(_phoneValue.frame);
  298. CGRect labelRect = [RectUtil setYPositionOf:_phoneValue.frame y:yPos];
  299. _phoneProgressLabel = [[ProgressLabel alloc] initWithFrame:labelRect];
  300. _phoneProgressLabel.backgroundColor = [UIColor clearColor];
  301. _phoneProgressLabel.font = [_phoneValue.font fontWithSize:14.0];
  302. _phoneProgressLabel.textColor = _phoneValue.textColor;
  303. _phoneProgressLabel.alpha = 0.0;
  304. _phoneProgressLabel.text = [BundleUtil localizedStringForKey:@"linking_phone"];
  305. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedPhoneProgress)];
  306. [_phoneProgressLabel addGestureRecognizer:tapGesture];
  307. [_phoneView addSubview:_phoneProgressLabel];
  308. [UIView animateWithDuration:0.4 delay:0 options:0 animations:^{
  309. _phoneProgressLabel.alpha = 1.0;
  310. [_phoneProgressLabel showActivityIndicator];
  311. } completion:nil];
  312. }
  313. - (void) showProgress:(UIView*)container progressValue:(UILabel*)value progressText:(NSString*)text {
  314. CGRect labelRect = value.frame;
  315. _syncProgressLabel = [[ProgressLabel alloc] initWithFrame:labelRect];
  316. _syncProgressLabel.backgroundColor = [UIColor clearColor];
  317. _syncProgressLabel.font = [value.font fontWithSize:14.0];
  318. _syncProgressLabel.textColor = value.textColor;
  319. _syncProgressLabel.alpha = 0.0;
  320. _syncProgressLabel.text = text;
  321. [container addSubview:_syncProgressLabel];
  322. [UIView animateWithDuration:0.4 delay:0 options:0 animations:^{
  323. value.alpha = 0.0;
  324. _syncProgressLabel.alpha = 1.0;
  325. [_syncProgressLabel showActivityIndicator];
  326. } completion:nil];
  327. }
  328. - (BOOL)isInputValid {
  329. if (_isProcessing) {
  330. return NO;
  331. }
  332. return YES;
  333. }
  334. - (IBAction)finishAction:(id)sender {
  335. if (_isProcessing == NO) {
  336. _isProcessing = YES;
  337. [self startCompletionProcess];
  338. } else {
  339. [self finishCompletionProcess];
  340. }
  341. }
  342. - (void)startCompletionProcess {
  343. _finishButton.userInteractionEnabled = NO;
  344. _finishButton.alpha = 0.0;
  345. [self resetPersistedIDCreateState];
  346. [self arrangeViewsForCompletion];
  347. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
  348. NSInteger timout = 2000;
  349. if ([self linkPhone]) {
  350. timout += 1000;
  351. }
  352. if ([self linkEmail]) {
  353. timout += 1000;
  354. }
  355. if ([self syncAdressBook]) {
  356. timout += 500;
  357. }
  358. if ([self enableSafe]) {
  359. timout += 500;
  360. }
  361. // do not show Threema Safe Intro after setup wizard
  362. [[UserSettings sharedUserSettings] setSafeIntroShown:YES];
  363. if (_hasErrors == NO) {
  364. // no errors - continue after timeout
  365. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timout * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
  366. [_delegate completedIDSetup];
  367. });
  368. } else {
  369. // user needs to confirm
  370. dispatch_async(dispatch_get_main_queue(), ^{
  371. [_finishButton setTitle:[BundleUtil localizedStringForKey:@"Done"] forState:UIControlStateNormal];
  372. _finishButton.userInteractionEnabled = YES;
  373. _finishButton.alpha = 1.0;
  374. });
  375. }
  376. _identityStore.createIDPhone = nil;
  377. _identityStore.createIDEmail = nil;
  378. _identityStore.tempSafePassword = nil;
  379. });
  380. }
  381. - (void)finishCompletionProcess {
  382. dispatch_async(dispatch_get_main_queue(), ^{
  383. [_delegate completedIDSetup];
  384. });
  385. }
  386. #pragma mark - gesture recognizers
  387. - (void)tappedPhoneProgress {
  388. [self showErrorText:_phoneProgressLabel.text];
  389. }
  390. - (void)tappedEmailProgress {
  391. [self showErrorText:_emailProgressLabel.text];
  392. }
  393. - (void)showErrorText:(NSString *)errorText {
  394. IntroQuestionView *view = (IntroQuestionView *)[NibUtil loadViewFromNibWithName:@"IntroQuestionView"];
  395. view.showOnlyOkButton = YES;
  396. view.questionLabel.text = errorText;
  397. view.delegate = self;
  398. view.frame = [RectUtil rect:view.frame centerIn:self.view.frame round:YES];
  399. [self.view addSubview:view];
  400. [self showMessageView:view];
  401. }
  402. - (void)selectedOk:(IntroQuestionView *)sender {
  403. [self hideMessageView:sender ignoreControls:YES];
  404. }
  405. @end