MainViewController.m 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "MainViewController.h"
  17. #import "MainViewController+App.h"
  18. #import "MainViewController+Auth.h"
  19. #import "MainViewController+AutoTests.h"
  20. #import "MainViewController+Custom.h"
  21. #import "MainViewController+Email.h"
  22. #import "MainViewController+Facebook.h"
  23. #import "MainViewController+GameCenter.h"
  24. #import "MainViewController+Google.h"
  25. #import "MainViewController+Internal.h"
  26. #import "MainViewController+OAuth.h"
  27. #import "MainViewController+OOB.h"
  28. #import "MainViewController+Phone.h"
  29. #import "MainViewController+User.h"
  30. #import <objc/runtime.h>
  31. #import <FirebaseCore/FIRApp.h>
  32. #import <FirebaseCore/FIRAppInternal.h>
  33. #import "AppManager.h"
  34. #import "AuthCredentials.h"
  35. #import "FIRAdditionalUserInfo.h"
  36. #import "FIROAuthProvider.h"
  37. #import "FIRPhoneAuthCredential.h"
  38. #import "FIRPhoneAuthProvider.h"
  39. #import "FIRAuthTokenResult.h"
  40. #import "FirebaseAuth.h"
  41. #import "FacebookAuthProvider.h"
  42. #import "GoogleAuthProvider.h"
  43. #import "SettingsViewController.h"
  44. #import "StaticContentTableViewManager.h"
  45. #import "UIViewController+Alerts.h"
  46. #import "UserInfoViewController.h"
  47. #import "UserTableViewCell.h"
  48. #import "FIRAuth_Internal.h"
  49. NS_ASSUME_NONNULL_BEGIN
  50. static NSString *const kSectionTitleSettings = @"Settings";
  51. static NSString *const kSectionTitleUserDetails = @"User Defaults";
  52. static NSString *const kSwitchToInMemoryUserTitle = @"Switch to in memory user";
  53. static NSString *const kNewOrExistingUserToggleTitle = @"New or Existing User Toggle";
  54. typedef void (^FIRTokenCallback)(NSString *_Nullable token, NSError *_Nullable error);
  55. @implementation MainViewController {
  56. NSMutableString *_consoleString;
  57. /** @var _userInMemory
  58. @brief Acts like the "memory" function of a calculator. An operation allows sample app users
  59. to assign this value based on @c FIRAuth.currentUser or clear this value.
  60. */
  61. FIRUser *_userInMemory;
  62. /** @var _useUserInMemory
  63. @brief Instructs the application to use _userInMemory instead of @c FIRAuth.currentUser for
  64. testing operations. This allows us to test if things still work with a user who is not
  65. the @c FIRAuth.currentUser, and also allows us to test those things while
  66. @c FIRAuth.currentUser remains nil (after a sign-out) and also when @c FIRAuth.currentUser
  67. is non-nil (do to a subsequent sign-in.)
  68. */
  69. BOOL _useUserInMemory;
  70. }
  71. - (id)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil {
  72. self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  73. if (self) {
  74. _actionCodeRequestType = ActionCodeRequestTypeInApp;
  75. _actionCodeContinueURL = [NSURL URLWithString:KCONTINUE_URL];
  76. _authStateDidChangeListeners = [NSMutableArray array];
  77. _IDTokenDidChangeListeners = [NSMutableArray array];
  78. _googleOAuthProvider = [FIROAuthProvider providerWithProviderID:FIRGoogleAuthProviderID];
  79. _microsoftOAuthProvider = [FIROAuthProvider providerWithProviderID:@"microsoft.com"];
  80. _twitterOAuthProvider = [FIROAuthProvider providerWithProviderID:@"twitter.com"];
  81. _linkedinOAuthProvider = [FIROAuthProvider providerWithProviderID:@"linkedin.com"];
  82. _yahooOAuthProvider = [FIROAuthProvider providerWithProviderID:@"yahoo.com"];
  83. [[NSNotificationCenter defaultCenter] addObserver:self
  84. selector:@selector(authStateChangedForAuth:)
  85. name:FIRAuthStateDidChangeNotification
  86. object:nil];
  87. self.useStatusBarSpinner = YES;
  88. }
  89. return self;
  90. }
  91. - (void)viewDidLoad {
  92. [super viewDidLoad];
  93. // Give us a circle for the image view:
  94. _userInfoTableViewCell.userInfoProfileURLImageView.layer.cornerRadius =
  95. _userInfoTableViewCell.userInfoProfileURLImageView.frame.size.width / 2.0f;
  96. _userInfoTableViewCell.userInfoProfileURLImageView.layer.masksToBounds = YES;
  97. _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.layer.cornerRadius =
  98. _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.frame.size.width / 2.0f;
  99. _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.layer.masksToBounds = YES;
  100. }
  101. - (void)viewWillAppear:(BOOL)animated {
  102. [super viewWillAppear:animated];
  103. [self updateTable];
  104. [self updateUserInfo];
  105. }
  106. #pragma mark - Public
  107. - (BOOL)handleIncomingLinkWithURL:(NSURL *)URL {
  108. // Parse the query portion of the incoming URL.
  109. NSDictionary<NSString *, NSString *> *queryItems =
  110. parseURL([NSURLComponents componentsWithString:URL.absoluteString].query);
  111. // Check that all necessary query items are available.
  112. NSString *actionCode = queryItems[@"oobCode"];
  113. NSString *mode = queryItems[@"mode"];
  114. if (!actionCode || !mode) {
  115. return NO;
  116. }
  117. // Handle Password Reset action.
  118. if ([mode isEqualToString:kPasswordResetAction]) {
  119. [self showTextInputPromptWithMessage:@"New Password:"
  120. completionBlock:^(BOOL userPressedOK, NSString *_Nullable newPassword) {
  121. if (!userPressedOK || !newPassword.length) {
  122. [UIPasteboard generalPasteboard].string = actionCode;
  123. return;
  124. }
  125. [self showSpinner:^() {
  126. [[AppManager auth] confirmPasswordResetWithCode:actionCode
  127. newPassword:newPassword
  128. completion:^(NSError *_Nullable error) {
  129. [self hideSpinner:^{
  130. if (error) {
  131. [self logFailure:@"Password reset in app failed" error:error];
  132. [self showMessagePrompt:error.localizedDescription];
  133. return;
  134. }
  135. [self logSuccess:@"Password reset in app succeeded."];
  136. [self showMessagePrompt:@"Password reset in app succeeded."];
  137. }];
  138. }];
  139. }];
  140. }];
  141. return YES;
  142. }
  143. if ([mode isEqualToString:kVerifyEmailAction]) {
  144. [self showMessagePromptWithTitle:@"Tap OK to verify email"
  145. message:actionCode
  146. showCancelButton:YES
  147. completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
  148. if (!userPressedOK) {
  149. return;
  150. }
  151. [self showSpinner:^() {
  152. [[AppManager auth] applyActionCode:actionCode completion:^(NSError *_Nullable error) {
  153. [self hideSpinner:^{
  154. if (error) {
  155. [self logFailure:@"Verify email in app failed" error:error];
  156. [self showMessagePrompt:error.localizedDescription];
  157. return;
  158. }
  159. [self logSuccess:@"Verify email in app succeeded."];
  160. [self showMessagePrompt:@"Verify email in app succeeded."];
  161. }];
  162. }];
  163. }];
  164. }];
  165. return YES;
  166. }
  167. return NO;
  168. }
  169. static NSDictionary<NSString *, NSString *> *parseURL(NSString *urlString) {
  170. NSString *linkURL = [NSURLComponents componentsWithString:urlString].query;
  171. NSArray<NSString *> *URLComponents = [linkURL componentsSeparatedByString:@"&"];
  172. NSMutableDictionary<NSString *, NSString *> *queryItems =
  173. [[NSMutableDictionary alloc] initWithCapacity:URLComponents.count];
  174. for (NSString *component in URLComponents) {
  175. NSRange equalRange = [component rangeOfString:@"="];
  176. if (equalRange.location != NSNotFound) {
  177. NSString *queryItemKey =
  178. [[component substringToIndex:equalRange.location] stringByRemovingPercentEncoding];
  179. NSString *queryItemValue =
  180. [[component substringFromIndex:equalRange.location + 1] stringByRemovingPercentEncoding];
  181. if (queryItemKey && queryItemValue) {
  182. queryItems[queryItemKey] = queryItemValue;
  183. }
  184. }
  185. }
  186. return queryItems;
  187. }
  188. - (void)updateTable {
  189. __weak typeof(self) weakSelf = self;
  190. _tableViewManager.contents =
  191. [StaticContentTableViewContent contentWithSections:@[
  192. // User Defaults
  193. [StaticContentTableViewSection sectionWithTitle:kSectionTitleUserDetails cells:@[
  194. [StaticContentTableViewCell cellWithCustomCell:_userInfoTableViewCell action:^{
  195. [weakSelf presentUserInfo];
  196. }],
  197. [StaticContentTableViewCell cellWithCustomCell:_userToUseCell],
  198. [StaticContentTableViewCell cellWithCustomCell:_userInMemoryInfoTableViewCell action:^{
  199. [weakSelf presentUserInMemoryInfo];
  200. }],
  201. ]],
  202. // Settings
  203. [StaticContentTableViewSection sectionWithTitle:kSectionTitleSettings cells:@[
  204. [StaticContentTableViewCell cellWithTitle:kSectionTitleSettings
  205. action:^{ [weakSelf presentSettings]; }],
  206. [StaticContentTableViewCell cellWithTitle:kNewOrExistingUserToggleTitle
  207. value:_isNewUserToggleOn ? @"Enabled" : @"Disabled"
  208. action:^{
  209. _isNewUserToggleOn = !_isNewUserToggleOn;
  210. [self updateTable]; }],
  211. [StaticContentTableViewCell cellWithTitle:kSwitchToInMemoryUserTitle
  212. action:^{ [weakSelf updateToSavedUser]; }],
  213. ]],
  214. // Auth
  215. [weakSelf authSection],
  216. // Email Auth
  217. [weakSelf emailAuthSection],
  218. // Phone Auth
  219. [weakSelf phoneAuthSection],
  220. // Google Auth
  221. [weakSelf googleAuthSection],
  222. // Facebook Auth
  223. [weakSelf facebookAuthSection],
  224. // OAuth
  225. [weakSelf oAuthSection],
  226. // Custom Auth
  227. [weakSelf customAuthSection],
  228. // Game Center Auth
  229. [weakSelf gameCenterAuthSection],
  230. // User
  231. [weakSelf userSection],
  232. // App
  233. [weakSelf appSection],
  234. // OOB
  235. [weakSelf oobSection],
  236. // Auto Tests
  237. [weakSelf autoTestsSection],
  238. ]];
  239. }
  240. #pragma mark - Internal
  241. - (FIRUser *)user {
  242. return _useUserInMemory ? _userInMemory : [AppManager auth].currentUser;
  243. }
  244. - (void)signInWithProvider:(nonnull id<AuthProvider>)provider callback:(void(^)(void))callback {
  245. if (!provider) {
  246. [self logFailedTest:@"A valid auth provider was not provided to the signInWithProvider."];
  247. return;
  248. }
  249. [provider getAuthCredentialWithPresentingViewController:self
  250. callback:^(FIRAuthCredential *credential,
  251. NSError *error) {
  252. if (!credential) {
  253. [self logFailedTest:@"The test needs a valid credential to continue."];
  254. return;
  255. }
  256. [[AppManager auth] signInWithCredential:credential
  257. completion:^(FIRAuthDataResult *_Nullable result,
  258. NSError *_Nullable error) {
  259. if (error) {
  260. [self logFailure:@"sign-in with provider failed" error:error];
  261. [self logFailedTest:@"Sign-in should succeed"];
  262. return;
  263. } else {
  264. [self logSuccess:@"sign-in with provider succeeded."];
  265. callback();
  266. }
  267. }];
  268. }];
  269. }
  270. - (FIRActionCodeSettings *)actionCodeSettings {
  271. FIRActionCodeSettings *actionCodeSettings = [[FIRActionCodeSettings alloc] init];
  272. actionCodeSettings.URL = self.actionCodeContinueURL;
  273. actionCodeSettings.handleCodeInApp = self.actionCodeRequestType == ActionCodeRequestTypeInApp;
  274. return actionCodeSettings;
  275. }
  276. - (void)reauthenticate:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
  277. FIRUser *user = [self user];
  278. if (!user) {
  279. NSString *provider = @"Firebase";
  280. if ([authProvider isKindOfClass:[GoogleAuthProvider class]]) {
  281. provider = @"Google";
  282. } else if ([authProvider isKindOfClass:[FacebookAuthProvider class]]) {
  283. provider = @"Facebook";
  284. }
  285. NSString *title = @"Missing User";
  286. NSString *message =
  287. [NSString stringWithFormat:@"There is no signed-in %@ user.", provider];
  288. [self showMessagePromptWithTitle:title message:message showCancelButton:NO completion:nil];
  289. return;
  290. }
  291. [authProvider getAuthCredentialWithPresentingViewController:self
  292. callback:^(FIRAuthCredential *credential,
  293. NSError *error) {
  294. if (credential) {
  295. FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
  296. NSError *_Nullable error) {
  297. if (error) {
  298. [self logFailure:@"reauthenticate operation failed" error:error];
  299. } else {
  300. [self logSuccess:@"reauthenticate operation succeeded."];
  301. }
  302. if (authResult.additionalUserInfo) {
  303. [self logSuccess:[self stringWithAdditionalUserInfo:authResult.additionalUserInfo]];
  304. }
  305. [self showTypicalUIForUserUpdateResultsWithTitle:@"Reauthenticate" error:error];
  306. };
  307. [user reauthenticateWithCredential:credential completion:completion];
  308. }
  309. }];
  310. }
  311. - (void)signinWithProvider:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
  312. FIRAuth *auth = [AppManager auth];
  313. if (!auth) {
  314. return;
  315. }
  316. [authProvider getAuthCredentialWithPresentingViewController:self
  317. callback:^(FIRAuthCredential *credential,
  318. NSError *error) {
  319. if (credential) {
  320. FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
  321. NSError *_Nullable error) {
  322. if (error) {
  323. [self logFailure:@"sign-in with provider failed" error:error];
  324. } else {
  325. [self logSuccess:@"sign-in with provider succeeded."];
  326. }
  327. if (authResult.additionalUserInfo) {
  328. [self logSuccess:[self stringWithAdditionalUserInfo:authResult.additionalUserInfo]];
  329. if (_isNewUserToggleOn) {
  330. NSString *newUserString = authResult.additionalUserInfo.isNewUser ?
  331. @"New user" : @"Existing user";
  332. [self showMessagePromptWithTitle:@"New or Existing"
  333. message:newUserString
  334. showCancelButton:NO
  335. completion:nil];
  336. }
  337. }
  338. [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In" error:error];
  339. };
  340. [auth signInWithCredential:credential completion:completion];
  341. }
  342. }];
  343. }
  344. - (void)linkWithAuthProvider:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
  345. FIRUser *user = [self user];
  346. if (!user) {
  347. return;
  348. }
  349. [authProvider getAuthCredentialWithPresentingViewController:self
  350. callback:^(FIRAuthCredential *credential,
  351. NSError *error) {
  352. if (credential) {
  353. FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
  354. NSError *_Nullable error) {
  355. if (error) {
  356. [self logFailure:@"link auth provider failed" error:error];
  357. } else {
  358. [self logSuccess:@"link auth provider succeeded."];
  359. }
  360. if (authResult.additionalUserInfo) {
  361. [self logSuccess:[self stringWithAdditionalUserInfo:authResult.additionalUserInfo]];
  362. }
  363. if (retrieveData) {
  364. [self showUIForAuthDataResultWithResult:authResult error:error];
  365. } else {
  366. [self showTypicalUIForUserUpdateResultsWithTitle:@"Link Account" error:error];
  367. }
  368. };
  369. [user linkWithCredential:credential completion:completion];
  370. }
  371. }];
  372. }
  373. - (void)unlinkFromProvider:(NSString *)provider
  374. completion:(nullable TestAutomationCallback)completion {
  375. [[self user] unlinkFromProvider:provider
  376. completion:^(FIRUser *_Nullable user,
  377. NSError *_Nullable error) {
  378. if (error) {
  379. [self logFailure:@"unlink auth provider failed" error:error];
  380. if (completion) {
  381. completion(error);
  382. }
  383. return;
  384. }
  385. [self logSuccess:@"unlink auth provider succeeded."];
  386. if (completion) {
  387. completion(nil);
  388. }
  389. [self showTypicalUIForUserUpdateResultsWithTitle:@"Unlink from Provider" error:error];
  390. }];
  391. }
  392. - (void)updateToSavedUser {
  393. if(![AppManager auth].currentUser) {
  394. NSLog(@"You must be signed in to perform this action");
  395. return;
  396. }
  397. if (!_userInMemory) {
  398. [self showMessagePrompt:[NSString stringWithFormat:@"You need an in-memory user to perform this"
  399. "action, use the M+ button to save a user to memory.", nil]];
  400. return;
  401. }
  402. [[AppManager auth] updateCurrentUser:_userInMemory completion:^(NSError *_Nullable error) {
  403. if (error) {
  404. [self showMessagePrompt:
  405. [NSString stringWithFormat:@"An error Occurred: %@", error.localizedDescription]];
  406. return;
  407. }
  408. }];
  409. }
  410. #pragma mark - Private
  411. - (void)presentSettings {
  412. SettingsViewController *settingsViewController = [[SettingsViewController alloc]
  413. initWithNibName:NSStringFromClass([SettingsViewController class])
  414. bundle:nil];
  415. [self showViewController:settingsViewController sender:self];
  416. }
  417. - (void)presentUserInfo {
  418. UserInfoViewController *userInfoViewController =
  419. [[UserInfoViewController alloc] initWithUser:[AppManager auth].currentUser];
  420. [self showViewController:userInfoViewController sender:self];
  421. }
  422. - (void)presentUserInMemoryInfo {
  423. UserInfoViewController *userInfoViewController =
  424. [[UserInfoViewController alloc] initWithUser:_userInMemory];
  425. [self showViewController:userInfoViewController sender:self];
  426. }
  427. - (NSString *)stringWithAdditionalUserInfo:(nullable FIRAdditionalUserInfo *)additionalUserInfo {
  428. if (!additionalUserInfo) {
  429. return @"(no additional user info)";
  430. }
  431. NSString *newUserString = additionalUserInfo.isNewUser ? @"new user" : @"existing user";
  432. return [NSString stringWithFormat:@"%@: %@", newUserString, additionalUserInfo.profile];
  433. }
  434. - (void)showTypicalUIForUserUpdateResultsWithTitle:(NSString *)resultsTitle
  435. error:(NSError * _Nullable)error {
  436. if (error) {
  437. NSString *message = [NSString stringWithFormat:@"%@ (%ld)\n%@",
  438. error.domain,
  439. (long)error.code,
  440. error.localizedDescription];
  441. if (error.code == FIRAuthErrorCodeAccountExistsWithDifferentCredential) {
  442. NSString *errorEmail = error.userInfo[FIRAuthErrorUserInfoEmailKey];
  443. resultsTitle = [NSString stringWithFormat:@"Existing email : %@", errorEmail];
  444. }
  445. [self showMessagePromptWithTitle:resultsTitle
  446. message:message
  447. showCancelButton:NO
  448. completion:nil];
  449. return;
  450. }
  451. [self updateUserInfo];
  452. }
  453. - (void)showUIForAuthDataResultWithResult:(FIRAuthDataResult *)result
  454. error:(NSError * _Nullable)error {
  455. NSString *errorMessage = [NSString stringWithFormat:@"%@ (%ld)\n%@",
  456. error.domain ?: @"",
  457. (long)error.code,
  458. error.localizedDescription ?: @""];
  459. [self showMessagePromptWithTitle:@"Error"
  460. message:errorMessage
  461. showCancelButton:NO
  462. completion:^(BOOL userPressedOK,
  463. NSString *_Nullable userInput) {
  464. [self showMessagePromptWithTitle:@"Profile Info"
  465. message:[self stringWithAdditionalUserInfo:result.additionalUserInfo]
  466. showCancelButton:NO
  467. completion:nil];
  468. [self updateUserInfo];
  469. }];
  470. }
  471. - (void)updateUserInfo {
  472. [_userInfoTableViewCell updateContentsWithUser:[AppManager auth].currentUser];
  473. [_userInMemoryInfoTableViewCell updateContentsWithUser:_userInMemory];
  474. }
  475. - (void)authStateChangedForAuth:(NSNotification *)notification {
  476. [self updateUserInfo];
  477. if (notification) {
  478. [self log:[NSString stringWithFormat:
  479. @"received FIRAuthStateDidChange notification on user '%@'.",
  480. ((FIRAuth *)notification.object).currentUser.uid]];
  481. }
  482. }
  483. - (void)log:(NSString *)string {
  484. dispatch_async(dispatch_get_main_queue(), ^{
  485. NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  486. dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
  487. NSString *date = [dateFormatter stringFromDate:[NSDate date]];
  488. if (!_consoleString) {
  489. _consoleString = [NSMutableString string];
  490. }
  491. [_consoleString appendString:[NSString stringWithFormat:@"%@ %@\n", date, string]];
  492. _consoleTextView.text = _consoleString;
  493. CGRect targetRect = CGRectMake(0, _consoleTextView.contentSize.height - 1, 1, 1);
  494. [_consoleTextView scrollRectToVisible:targetRect animated:YES];
  495. });
  496. }
  497. - (void)logSuccess:(NSString *)string {
  498. [self log:[NSString stringWithFormat:@"SUCCESS: %@", string]];
  499. }
  500. - (void)logFailure:(NSString *)string error:(NSError * _Nullable) error {
  501. NSString *message =
  502. [NSString stringWithFormat:@"FAILURE: %@ Error Description: %@.", string, error.description];
  503. [self log:message];
  504. }
  505. - (void)logFailedTest:( NSString *_Nonnull )reason {
  506. [self log:[NSString stringWithFormat:@"FAILIURE: TEST FAILED - %@", reason]];
  507. }
  508. #pragma mark - IBAction
  509. - (IBAction)userToUseDidChange:(UISegmentedControl *)sender {
  510. _useUserInMemory = (sender.selectedSegmentIndex == 1);
  511. }
  512. - (IBAction)memoryPlus {
  513. _userInMemory = [AppManager auth].currentUser;
  514. [self updateUserInfo];
  515. }
  516. - (IBAction)memoryClear {
  517. _userInMemory = nil;
  518. [self updateUserInfo];
  519. }
  520. - (IBAction)clearConsole:(id)sender {
  521. [_consoleString appendString:@"\n\n"];
  522. _consoleTextView.text = @"";
  523. }
  524. - (IBAction)copyConsole:(id)sender {
  525. UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
  526. pasteboard.string = _consoleString ?: @"";
  527. }
  528. @end
  529. NS_ASSUME_NONNULL_END