AccountListViewController.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. #import <AuthenticationServices/AuthenticationServices.h>
  2. #import "authenticator/BaseAuthenticator.h"
  3. #import "AccountListViewController.h"
  4. #import "AFNetworking.h"
  5. #import "LauncherPreferences.h"
  6. #import "UIImageView+AFNetworking.h"
  7. #import "ios_uikit_bridge.h"
  8. #import "utils.h"
  9. @interface AccountListViewController()<ASWebAuthenticationPresentationContextProviding>
  10. @property(nonatomic, strong) NSMutableArray *accountList;
  11. @property(nonatomic) ASWebAuthenticationSession *authVC;
  12. @end
  13. @implementation AccountListViewController
  14. - (void)viewDidLoad {
  15. [super viewDidLoad];
  16. if (self.accountList == nil) {
  17. self.accountList = [NSMutableArray array];
  18. } else {
  19. [self.accountList removeAllObjects];
  20. }
  21. // List accounts
  22. NSString *listPath = [NSString stringWithFormat:@"%s/accounts", getenv("POJAV_HOME")];
  23. NSFileManager *fm = [NSFileManager defaultManager];
  24. NSArray *files = [fm contentsOfDirectoryAtPath:listPath error:nil];
  25. for(NSString *file in files) {
  26. NSString *path = [listPath stringByAppendingPathComponent:file];
  27. BOOL isDir = NO;
  28. [fm fileExistsAtPath:path isDirectory:(&isDir)];
  29. if(!isDir && [file hasSuffix:@".json"]) {
  30. [self.accountList addObject:parseJSONFromFile(path)];
  31. }
  32. }
  33. [self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleSingleLine];
  34. }
  35. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  36. {
  37. return self.accountList.count + 1;
  38. }
  39. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  40. {
  41. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
  42. if (cell == nil) {
  43. cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
  44. }
  45. if (indexPath.row == self.accountList.count) {
  46. cell.imageView.image = [UIImage imageNamed:@"IconAdd"];
  47. cell.textLabel.text = localize(@"login.option.add", nil);
  48. return cell;
  49. }
  50. NSDictionary *selected = self.accountList[indexPath.row];
  51. // By default, display the saved username
  52. cell.textLabel.text = selected[@"username"];
  53. if ([selected[@"username"] hasPrefix:@"Demo."]) {
  54. // Remove the prefix "Demo."
  55. cell.textLabel.text = [selected[@"username"] substringFromIndex:5];
  56. cell.detailTextLabel.text = localize(@"login.option.demo", nil);
  57. } else if (selected[@"xboxGamertag"] == nil) {
  58. cell.detailTextLabel.text = localize(@"login.option.local", nil);
  59. } else {
  60. // Display the Xbox gamertag for online accounts
  61. cell.detailTextLabel.text = selected[@"xboxGamertag"];
  62. }
  63. cell.imageView.contentMode = UIViewContentModeCenter;
  64. [cell.imageView setImageWithURL:[NSURL URLWithString:[selected[@"profilePicURL"] stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"]] placeholderImage:[UIImage imageNamed:@"DefaultAccount"]];
  65. return cell;
  66. }
  67. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  68. [tableView deselectRowAtIndexPath:indexPath animated:NO];
  69. UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
  70. if (indexPath.row == self.accountList.count) {
  71. [self actionAddAccount:cell];
  72. return;
  73. }
  74. self.modalInPresentation = YES;
  75. self.tableView.userInteractionEnabled = NO;
  76. [self addActivityIndicatorTo:cell];
  77. id callback = ^(id status, BOOL success) {
  78. [self callbackMicrosoftAuth:status success:success forCell:cell];
  79. };
  80. [[BaseAuthenticator loadSavedName:self.accountList[indexPath.row][@"username"]] refreshTokenWithCallback:callback];
  81. }
  82. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
  83. if (editingStyle == UITableViewCellEditingStyleDelete) {
  84. // TODO: invalidate token
  85. NSString *str = self.accountList[indexPath.row][@"username"];
  86. NSFileManager *fm = [NSFileManager defaultManager];
  87. NSString *path = [NSString stringWithFormat:@"%s/accounts/%@.json", getenv("POJAV_HOME"), str];
  88. if (self.whenDelete != nil) {
  89. self.whenDelete(str);
  90. }
  91. NSString *xuid = self.accountList[indexPath.row][@"xuid"];
  92. if (xuid) {
  93. [MicrosoftAuthenticator clearTokenDataOfProfile:xuid];
  94. }
  95. [fm removeItemAtPath:path error:nil];
  96. [self.accountList removeObjectAtIndex:indexPath.row];
  97. [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
  98. }
  99. }
  100. - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
  101. {
  102. if (indexPath.row == self.accountList.count) {
  103. return UITableViewCellEditingStyleNone;
  104. } else {
  105. return UITableViewCellEditingStyleDelete;
  106. }
  107. }
  108. - (NSDictionary *)parseQueryItems:(NSString *)url {
  109. NSMutableDictionary *result = [NSMutableDictionary new];
  110. NSArray<NSURLQueryItem *> *queryItems = [NSURLComponents componentsWithString:url].queryItems;
  111. for (NSURLQueryItem *item in queryItems) {
  112. result[item.name] = item.value;
  113. }
  114. return result;
  115. }
  116. - (void)actionAddAccount:(UITableViewCell *)sender {
  117. UIAlertController *picker = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
  118. UIAlertAction *actionMicrosoft = [UIAlertAction actionWithTitle:localize(@"login.option.microsoft", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
  119. [self actionLoginMicrosoft:sender];
  120. }];
  121. [picker addAction:actionMicrosoft];
  122. UIAlertAction *actionLocal = [UIAlertAction actionWithTitle:localize(@"login.option.local", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
  123. [self actionLoginLocal:sender];
  124. }];
  125. [picker addAction:actionLocal];
  126. UIAlertAction *cancel = [UIAlertAction actionWithTitle:localize(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil];
  127. [picker addAction:cancel];
  128. picker.popoverPresentationController.sourceView = sender;
  129. picker.popoverPresentationController.sourceRect = sender.bounds;
  130. [self presentViewController:picker animated:YES completion:nil];
  131. }
  132. - (void)actionLoginLocal:(UIView *)sender {
  133. if (getPrefBool(@"warnings.local_warn")) {
  134. setPrefBool(@"warnings.local_warn", NO);
  135. UIAlertController *alert = [UIAlertController alertControllerWithTitle:localize(@"login.warn.title.localmode", nil) message:localize(@"login.warn.message.localmode", nil) preferredStyle:UIAlertControllerStyleActionSheet];
  136. alert.popoverPresentationController.sourceView = sender;
  137. alert.popoverPresentationController.sourceRect = sender.bounds;
  138. UIAlertAction *ok = [UIAlertAction actionWithTitle:localize(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {[self actionLoginLocal:sender];}];
  139. [alert addAction:ok];
  140. [self presentViewController:alert animated:YES completion:nil];
  141. return;
  142. }
  143. UIAlertController *controller = [UIAlertController alertControllerWithTitle:localize(@"Sign in", nil) message:localize(@"login.option.local", nil) preferredStyle:UIAlertControllerStyleAlert];
  144. [controller addTextFieldWithConfigurationHandler:^(UITextField *textField) {
  145. textField.placeholder = localize(@"login.alert.field.username", nil);
  146. textField.clearButtonMode = UITextFieldViewModeWhileEditing;
  147. textField.borderStyle = UITextBorderStyleRoundedRect;
  148. }];
  149. [controller addAction:[UIAlertAction actionWithTitle:localize(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
  150. NSArray *textFields = controller.textFields;
  151. UITextField *usernameField = textFields[0];
  152. if (usernameField.text.length < 3 || usernameField.text.length > 16) {
  153. controller.message = localize(@"login.error.username.outOfRange", nil);
  154. [self presentViewController:controller animated:YES completion:nil];
  155. } else {
  156. id callback = ^(id status, BOOL success) {
  157. self.whenItemSelected();
  158. [self dismissViewControllerAnimated:YES completion:nil];
  159. };
  160. [[[LocalAuthenticator alloc] initWithInput:usernameField.text] loginWithCallback:callback];
  161. }
  162. }]];
  163. [controller addAction:[UIAlertAction actionWithTitle:localize(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
  164. [self presentViewController:controller animated:YES completion:nil];
  165. }
  166. - (void)actionLoginMicrosoft:(UITableViewCell *)sender {
  167. NSURL *url = [NSURL URLWithString:@"https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&response_type=code&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL&redirect_url=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf"];
  168. self.authVC =
  169. [[ASWebAuthenticationSession alloc] initWithURL:url
  170. callbackURLScheme:@"ms-xal-00000000402b5328"
  171. completionHandler:^(NSURL * _Nullable callbackURL, NSError * _Nullable error)
  172. {
  173. if (callbackURL == nil) {
  174. if (error.code != ASWebAuthenticationSessionErrorCodeCanceledLogin) {
  175. showDialog(localize(@"Error", nil), error.localizedDescription);
  176. }
  177. return;
  178. }
  179. // NSLog(@"URL returned = %@", [callbackURL absoluteString]);
  180. NSDictionary *queryItems = [self parseQueryItems:callbackURL.absoluteString];
  181. if (queryItems[@"code"]) {
  182. self.modalInPresentation = YES;
  183. self.tableView.userInteractionEnabled = NO;
  184. [self addActivityIndicatorTo:sender];
  185. id callback = ^(id status, BOOL success) {
  186. if ([status isKindOfClass:NSString.class] && [status isEqualToString:@"DEMO"] && success) {
  187. showDialog(localize(@"login.warn.title.demomode", nil), localize(@"login.warn.message.demomode", nil));
  188. }
  189. [self callbackMicrosoftAuth:status success:success forCell:sender];
  190. };
  191. [[[MicrosoftAuthenticator alloc] initWithInput:queryItems[@"code"]] loginWithCallback:callback];
  192. } else {
  193. if ([queryItems[@"error"] hasPrefix:@"access_denied"]) {
  194. // Ignore access denial responses
  195. return;
  196. }
  197. showDialog(localize(@"Error", nil), queryItems[@"error_description"]);
  198. }
  199. }];
  200. self.authVC.prefersEphemeralWebBrowserSession = YES;
  201. self.authVC.presentationContextProvider = self;
  202. if ([self.authVC start] == NO) {
  203. showDialog(localize(@"Error", nil), @"Unable to open Safari");
  204. }
  205. }
  206. - (void)addActivityIndicatorTo:(UITableViewCell *)cell {
  207. UIActivityIndicatorViewStyle indicatorStyle = UIActivityIndicatorViewStyleMedium;
  208. UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:indicatorStyle];
  209. cell.accessoryView = indicator;
  210. [indicator sizeToFit];
  211. [indicator startAnimating];
  212. }
  213. - (void)removeActivityIndicatorFrom:(UITableViewCell *)cell {
  214. UIActivityIndicatorView *indicator = (id)cell.accessoryView;
  215. [indicator stopAnimating];
  216. cell.accessoryView = nil;
  217. }
  218. - (void)callbackMicrosoftAuth:(id)status success:(BOOL)success forCell:(UITableViewCell *)cell {
  219. if (status != nil) {
  220. if (success) {
  221. cell.detailTextLabel.text = status;
  222. } else {
  223. self.modalInPresentation = NO;
  224. self.tableView.userInteractionEnabled = YES;
  225. [self removeActivityIndicatorFrom:cell];
  226. cell.detailTextLabel.text = [status localizedDescription];
  227. NSData *errorData = ((NSError *)status).userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
  228. NSString *errorStr = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
  229. NSLog(@"[MSA] Error: %@", errorStr);
  230. showDialog(localize(@"Error", nil), errorStr);
  231. }
  232. } else if (success) {
  233. self.whenItemSelected();
  234. [self removeActivityIndicatorFrom:cell];
  235. [self dismissViewControllerAnimated:YES completion:nil];
  236. }
  237. }
  238. #pragma mark - UIPopoverPresentationControllerDelegate
  239. - (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection {
  240. return UIModalPresentationNone;
  241. }
  242. #pragma mark - ASWebAuthenticationPresentationContextProviding
  243. - (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
  244. return UIApplication.sharedApplication.windows.firstObject;
  245. }
  246. @end