MainViewController.m 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  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. _gitHubOAuthProvider = [FIROAuthProvider providerWithProviderID:@"github.com"];
  84. [[NSNotificationCenter defaultCenter] addObserver:self
  85. selector:@selector(authStateChangedForAuth:)
  86. name:FIRAuthStateDidChangeNotification
  87. object:nil];
  88. self.useStatusBarSpinner = YES;
  89. }
  90. return self;
  91. }
  92. - (void)viewDidLoad {
  93. [super viewDidLoad];
  94. // Give us a circle for the image view:
  95. _userInfoTableViewCell.userInfoProfileURLImageView.layer.cornerRadius =
  96. _userInfoTableViewCell.userInfoProfileURLImageView.frame.size.width / 2.0f;
  97. _userInfoTableViewCell.userInfoProfileURLImageView.layer.masksToBounds = YES;
  98. _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.layer.cornerRadius =
  99. _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.frame.size.width / 2.0f;
  100. _userInMemoryInfoTableViewCell.userInfoProfileURLImageView.layer.masksToBounds = YES;
  101. }
  102. - (void)viewWillAppear:(BOOL)animated {
  103. [super viewWillAppear:animated];
  104. [self updateTable];
  105. [self updateUserInfo];
  106. }
  107. #pragma mark - Public
  108. - (BOOL)handleIncomingLinkWithURL:(NSURL *)URL {
  109. // Parse the query portion of the incoming URL.
  110. NSDictionary<NSString *, NSString *> *queryItems =
  111. parseURL([NSURLComponents componentsWithString:URL.absoluteString].query);
  112. // Check that all necessary query items are available.
  113. NSString *actionCode = queryItems[@"oobCode"];
  114. NSString *mode = queryItems[@"mode"];
  115. if (!actionCode || !mode) {
  116. return NO;
  117. }
  118. // Handle Password Reset action.
  119. if ([mode isEqualToString:kPasswordResetAction]) {
  120. [self showTextInputPromptWithMessage:@"New Password:"
  121. completionBlock:^(BOOL userPressedOK, NSString *_Nullable newPassword) {
  122. if (!userPressedOK || !newPassword.length) {
  123. [UIPasteboard generalPasteboard].string = actionCode;
  124. return;
  125. }
  126. [self showSpinner:^() {
  127. [[AppManager auth] confirmPasswordResetWithCode:actionCode
  128. newPassword:newPassword
  129. completion:^(NSError *_Nullable error) {
  130. [self hideSpinner:^{
  131. if (error) {
  132. [self logFailure:@"Password reset in app failed" error:error];
  133. [self showMessagePrompt:error.localizedDescription];
  134. return;
  135. }
  136. [self logSuccess:@"Password reset in app succeeded."];
  137. [self showMessagePrompt:@"Password reset in app succeeded."];
  138. }];
  139. }];
  140. }];
  141. }];
  142. return YES;
  143. }
  144. if ([mode isEqualToString:kVerifyEmailAction]) {
  145. [self showMessagePromptWithTitle:@"Tap OK to verify email"
  146. message:actionCode
  147. showCancelButton:YES
  148. completion:^(BOOL userPressedOK, NSString *_Nullable userInput) {
  149. if (!userPressedOK) {
  150. return;
  151. }
  152. [self showSpinner:^() {
  153. [[AppManager auth] applyActionCode:actionCode completion:^(NSError *_Nullable error) {
  154. [self hideSpinner:^{
  155. if (error) {
  156. [self logFailure:@"Verify email in app failed" error:error];
  157. [self showMessagePrompt:error.localizedDescription];
  158. return;
  159. }
  160. [self logSuccess:@"Verify email in app succeeded."];
  161. [self showMessagePrompt:@"Verify email in app succeeded."];
  162. }];
  163. }];
  164. }];
  165. }];
  166. return YES;
  167. }
  168. return NO;
  169. }
  170. static NSDictionary<NSString *, NSString *> *parseURL(NSString *urlString) {
  171. NSString *linkURL = [NSURLComponents componentsWithString:urlString].query;
  172. NSArray<NSString *> *URLComponents = [linkURL componentsSeparatedByString:@"&"];
  173. NSMutableDictionary<NSString *, NSString *> *queryItems =
  174. [[NSMutableDictionary alloc] initWithCapacity:URLComponents.count];
  175. for (NSString *component in URLComponents) {
  176. NSRange equalRange = [component rangeOfString:@"="];
  177. if (equalRange.location != NSNotFound) {
  178. NSString *queryItemKey =
  179. [[component substringToIndex:equalRange.location] stringByRemovingPercentEncoding];
  180. NSString *queryItemValue =
  181. [[component substringFromIndex:equalRange.location + 1] stringByRemovingPercentEncoding];
  182. if (queryItemKey && queryItemValue) {
  183. queryItems[queryItemKey] = queryItemValue;
  184. }
  185. }
  186. }
  187. return queryItems;
  188. }
  189. - (void)updateTable {
  190. __weak typeof(self) weakSelf = self;
  191. _tableViewManager.contents =
  192. [StaticContentTableViewContent contentWithSections:@[
  193. // User Defaults
  194. [StaticContentTableViewSection sectionWithTitle:kSectionTitleUserDetails cells:@[
  195. [StaticContentTableViewCell cellWithCustomCell:_userInfoTableViewCell action:^{
  196. [weakSelf presentUserInfo];
  197. }],
  198. [StaticContentTableViewCell cellWithCustomCell:_userToUseCell],
  199. [StaticContentTableViewCell cellWithCustomCell:_userInMemoryInfoTableViewCell action:^{
  200. [weakSelf presentUserInMemoryInfo];
  201. }],
  202. ]],
  203. // Settings
  204. [StaticContentTableViewSection sectionWithTitle:kSectionTitleSettings cells:@[
  205. [StaticContentTableViewCell cellWithTitle:kSectionTitleSettings
  206. action:^{ [weakSelf presentSettings]; }],
  207. [StaticContentTableViewCell cellWithTitle:kNewOrExistingUserToggleTitle
  208. value:_isNewUserToggleOn ? @"Enabled" : @"Disabled"
  209. action:^{
  210. _isNewUserToggleOn = !_isNewUserToggleOn;
  211. [self updateTable]; }],
  212. [StaticContentTableViewCell cellWithTitle:kSwitchToInMemoryUserTitle
  213. action:^{ [weakSelf updateToSavedUser]; }],
  214. ]],
  215. // Auth
  216. [weakSelf authSection],
  217. // Email Auth
  218. [weakSelf emailAuthSection],
  219. // Phone Auth
  220. [weakSelf phoneAuthSection],
  221. // Google Auth
  222. [weakSelf googleAuthSection],
  223. // Facebook Auth
  224. [weakSelf facebookAuthSection],
  225. // OAuth
  226. [weakSelf oAuthSection],
  227. // Custom Auth
  228. [weakSelf customAuthSection],
  229. // Game Center Auth
  230. [weakSelf gameCenterAuthSection],
  231. // User
  232. [weakSelf userSection],
  233. // App
  234. [weakSelf appSection],
  235. // OOB
  236. [weakSelf oobSection],
  237. // Auto Tests
  238. [weakSelf autoTestsSection],
  239. ]];
  240. }
  241. #pragma mark - Internal
  242. - (FIRUser *)user {
  243. return _useUserInMemory ? _userInMemory : [AppManager auth].currentUser;
  244. }
  245. - (void)signInWithProvider:(nonnull id<AuthProvider>)provider callback:(void(^)(void))callback {
  246. if (!provider) {
  247. [self logFailedTest:@"A valid auth provider was not provided to the signInWithProvider."];
  248. return;
  249. }
  250. [provider getAuthCredentialWithPresentingViewController:self
  251. callback:^(FIRAuthCredential *credential,
  252. NSError *error) {
  253. if (!credential) {
  254. [self logFailedTest:@"The test needs a valid credential to continue."];
  255. return;
  256. }
  257. [[AppManager auth] signInWithCredential:credential
  258. completion:^(FIRAuthDataResult *_Nullable result,
  259. NSError *_Nullable error) {
  260. if (error) {
  261. [self logFailure:@"sign-in with provider failed" error:error];
  262. [self logFailedTest:@"Sign-in should succeed"];
  263. return;
  264. } else {
  265. [self logSuccess:@"sign-in with provider succeeded."];
  266. callback();
  267. }
  268. }];
  269. }];
  270. }
  271. - (FIRActionCodeSettings *)actionCodeSettings {
  272. FIRActionCodeSettings *actionCodeSettings = [[FIRActionCodeSettings alloc] init];
  273. actionCodeSettings.URL = self.actionCodeContinueURL;
  274. actionCodeSettings.handleCodeInApp = self.actionCodeRequestType == ActionCodeRequestTypeInApp;
  275. return actionCodeSettings;
  276. }
  277. - (void)reauthenticate:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
  278. FIRUser *user = [self user];
  279. if (!user) {
  280. NSString *provider = @"Firebase";
  281. if ([authProvider isKindOfClass:[GoogleAuthProvider class]]) {
  282. provider = @"Google";
  283. } else if ([authProvider isKindOfClass:[FacebookAuthProvider class]]) {
  284. provider = @"Facebook";
  285. }
  286. NSString *title = @"Missing User";
  287. NSString *message =
  288. [NSString stringWithFormat:@"There is no signed-in %@ user.", provider];
  289. [self showMessagePromptWithTitle:title message:message showCancelButton:NO completion:nil];
  290. return;
  291. }
  292. [authProvider getAuthCredentialWithPresentingViewController:self
  293. callback:^(FIRAuthCredential *credential,
  294. NSError *error) {
  295. if (credential) {
  296. FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
  297. NSError *_Nullable error) {
  298. if (error) {
  299. [self logFailure:@"reauthenticate operation failed" error:error];
  300. } else {
  301. [self logSuccess:@"reauthenticate operation succeeded."];
  302. }
  303. if (authResult.additionalUserInfo) {
  304. [self logSuccess:[self stringWithAdditionalUserInfo:authResult.additionalUserInfo]];
  305. }
  306. [self showTypicalUIForUserUpdateResultsWithTitle:@"Reauthenticate" error:error];
  307. };
  308. [user reauthenticateWithCredential:credential completion:completion];
  309. }
  310. }];
  311. }
  312. - (void)signinWithProvider:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
  313. FIRAuth *auth = [AppManager auth];
  314. if (!auth) {
  315. return;
  316. }
  317. [authProvider getAuthCredentialWithPresentingViewController:self
  318. callback:^(FIRAuthCredential *credential,
  319. NSError *error) {
  320. if (credential) {
  321. FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
  322. NSError *_Nullable error) {
  323. if (error) {
  324. [self logFailure:@"sign-in with provider failed" error:error];
  325. } else {
  326. [self logSuccess:@"sign-in with provider succeeded."];
  327. }
  328. if (authResult.additionalUserInfo) {
  329. [self logSuccess:[self stringWithAdditionalUserInfo:authResult.additionalUserInfo]];
  330. if (_isNewUserToggleOn) {
  331. NSString *newUserString = authResult.additionalUserInfo.isNewUser ?
  332. @"New user" : @"Existing user";
  333. [self showMessagePromptWithTitle:@"New or Existing"
  334. message:newUserString
  335. showCancelButton:NO
  336. completion:nil];
  337. }
  338. }
  339. [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In" error:error];
  340. };
  341. [auth signInWithCredential:credential completion:completion];
  342. }
  343. }];
  344. }
  345. - (void)linkWithAuthProvider:(id<AuthProvider>)authProvider retrieveData:(BOOL)retrieveData {
  346. FIRUser *user = [self user];
  347. if (!user) {
  348. return;
  349. }
  350. [authProvider getAuthCredentialWithPresentingViewController:self
  351. callback:^(FIRAuthCredential *credential,
  352. NSError *error) {
  353. if (credential) {
  354. FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
  355. NSError *_Nullable error) {
  356. if (error) {
  357. [self logFailure:@"link auth provider failed" error:error];
  358. } else {
  359. [self logSuccess:@"link auth provider succeeded."];
  360. }
  361. if (authResult.additionalUserInfo) {
  362. [self logSuccess:[self stringWithAdditionalUserInfo:authResult.additionalUserInfo]];
  363. }
  364. if (retrieveData) {
  365. [self showUIForAuthDataResultWithResult:authResult error:error];
  366. } else {
  367. [self showTypicalUIForUserUpdateResultsWithTitle:@"Link Account" error:error];
  368. }
  369. };
  370. [user linkWithCredential:credential completion:completion];
  371. }
  372. }];
  373. }
  374. - (void)unlinkFromProvider:(NSString *)provider
  375. completion:(nullable TestAutomationCallback)completion {
  376. [[self user] unlinkFromProvider:provider
  377. completion:^(FIRUser *_Nullable user,
  378. NSError *_Nullable error) {
  379. if (error) {
  380. [self logFailure:@"unlink auth provider failed" error:error];
  381. if (completion) {
  382. completion(error);
  383. }
  384. return;
  385. }
  386. [self logSuccess:@"unlink auth provider succeeded."];
  387. if (completion) {
  388. completion(nil);
  389. }
  390. [self showTypicalUIForUserUpdateResultsWithTitle:@"Unlink from Provider" error:error];
  391. }];
  392. }
  393. - (void)updateToSavedUser {
  394. if(![AppManager auth].currentUser) {
  395. NSLog(@"You must be signed in to perform this action");
  396. return;
  397. }
  398. if (!_userInMemory) {
  399. [self showMessagePrompt:[NSString stringWithFormat:@"You need an in-memory user to perform this"
  400. "action, use the M+ button to save a user to memory.", nil]];
  401. return;
  402. }
  403. [[AppManager auth] updateCurrentUser:_userInMemory completion:^(NSError *_Nullable error) {
  404. if (error) {
  405. [self showMessagePrompt:
  406. [NSString stringWithFormat:@"An error Occurred: %@", error.localizedDescription]];
  407. return;
  408. }
  409. }];
  410. }
  411. #pragma mark - Private
  412. - (void)presentSettings {
  413. SettingsViewController *settingsViewController = [[SettingsViewController alloc]
  414. initWithNibName:NSStringFromClass([SettingsViewController class])
  415. bundle:nil];
  416. [self showViewController:settingsViewController sender:self];
  417. }
  418. - (void)presentUserInfo {
  419. UserInfoViewController *userInfoViewController =
  420. [[UserInfoViewController alloc] initWithUser:[AppManager auth].currentUser];
  421. [self showViewController:userInfoViewController sender:self];
  422. }
  423. - (void)presentUserInMemoryInfo {
  424. UserInfoViewController *userInfoViewController =
  425. [[UserInfoViewController alloc] initWithUser:_userInMemory];
  426. [self showViewController:userInfoViewController sender:self];
  427. }
  428. - (NSString *)stringWithAdditionalUserInfo:(nullable FIRAdditionalUserInfo *)additionalUserInfo {
  429. if (!additionalUserInfo) {
  430. return @"(no additional user info)";
  431. }
  432. NSString *newUserString = additionalUserInfo.isNewUser ? @"new user" : @"existing user";
  433. return [NSString stringWithFormat:@"%@: %@", newUserString, additionalUserInfo.profile];
  434. }
  435. - (void)showTypicalUIForUserUpdateResultsWithTitle:(NSString *)resultsTitle
  436. error:(NSError * _Nullable)error {
  437. if (error) {
  438. NSString *message = [NSString stringWithFormat:@"%@ (%ld)\n%@",
  439. error.domain,
  440. (long)error.code,
  441. error.localizedDescription];
  442. if (error.code == FIRAuthErrorCodeAccountExistsWithDifferentCredential) {
  443. NSString *errorEmail = error.userInfo[FIRAuthErrorUserInfoEmailKey];
  444. resultsTitle = [NSString stringWithFormat:@"Existing email : %@", errorEmail];
  445. }
  446. [self showMessagePromptWithTitle:resultsTitle
  447. message:message
  448. showCancelButton:NO
  449. completion:nil];
  450. return;
  451. }
  452. [self updateUserInfo];
  453. }
  454. - (void)showUIForAuthDataResultWithResult:(FIRAuthDataResult *)result
  455. error:(NSError * _Nullable)error {
  456. NSString *errorMessage = [NSString stringWithFormat:@"%@ (%ld)\n%@",
  457. error.domain ?: @"",
  458. (long)error.code,
  459. error.localizedDescription ?: @""];
  460. [self showMessagePromptWithTitle:@"Error"
  461. message:errorMessage
  462. showCancelButton:NO
  463. completion:^(BOOL userPressedOK,
  464. NSString *_Nullable userInput) {
  465. [self showMessagePromptWithTitle:@"Profile Info"
  466. message:[self stringWithAdditionalUserInfo:result.additionalUserInfo]
  467. showCancelButton:NO
  468. completion:nil];
  469. [self updateUserInfo];
  470. }];
  471. }
  472. - (void)updateUserInfo {
  473. [_userInfoTableViewCell updateContentsWithUser:[AppManager auth].currentUser];
  474. [_userInMemoryInfoTableViewCell updateContentsWithUser:_userInMemory];
  475. }
  476. - (void)authStateChangedForAuth:(NSNotification *)notification {
  477. [self updateUserInfo];
  478. if (notification) {
  479. [self log:[NSString stringWithFormat:
  480. @"received FIRAuthStateDidChange notification on user '%@'.",
  481. ((FIRAuth *)notification.object).currentUser.uid]];
  482. }
  483. }
  484. - (void)log:(NSString *)string {
  485. dispatch_async(dispatch_get_main_queue(), ^{
  486. NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  487. dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
  488. NSString *date = [dateFormatter stringFromDate:[NSDate date]];
  489. if (!_consoleString) {
  490. _consoleString = [NSMutableString string];
  491. }
  492. [_consoleString appendString:[NSString stringWithFormat:@"%@ %@\n", date, string]];
  493. _consoleTextView.text = _consoleString;
  494. CGRect targetRect = CGRectMake(0, _consoleTextView.contentSize.height - 1, 1, 1);
  495. [_consoleTextView scrollRectToVisible:targetRect animated:YES];
  496. });
  497. }
  498. - (void)logSuccess:(NSString *)string {
  499. [self log:[NSString stringWithFormat:@"SUCCESS: %@", string]];
  500. }
  501. - (void)logFailure:(NSString *)string error:(NSError * _Nullable) error {
  502. NSString *message =
  503. [NSString stringWithFormat:@"FAILURE: %@ Error Description: %@.", string, error.description];
  504. [self log:message];
  505. }
  506. - (void)logFailedTest:( NSString *_Nonnull )reason {
  507. [self log:[NSString stringWithFormat:@"FAILIURE: TEST FAILED - %@", reason]];
  508. }
  509. #pragma mark - IBAction
  510. - (IBAction)userToUseDidChange:(UISegmentedControl *)sender {
  511. _useUserInMemory = (sender.selectedSegmentIndex == 1);
  512. }
  513. - (IBAction)memoryPlus {
  514. _userInMemory = [AppManager auth].currentUser;
  515. [self updateUserInfo];
  516. }
  517. - (IBAction)memoryClear {
  518. _userInMemory = nil;
  519. [self updateUserInfo];
  520. }
  521. - (IBAction)clearConsole:(id)sender {
  522. [_consoleString appendString:@"\n\n"];
  523. _consoleTextView.text = @"";
  524. }
  525. - (IBAction)copyConsole:(id)sender {
  526. UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
  527. pasteboard.string = _consoleString ?: @"";
  528. }
  529. @end
  530. NS_ASSUME_NONNULL_END