MainViewController.m 22 KB

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