Path: blob/main/Natives/AccountListViewController.m
589 views
#import <AuthenticationServices/AuthenticationServices.h>12#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"910@interface AccountListViewController()<ASWebAuthenticationPresentationContextProviding>1112@property(nonatomic, strong) NSMutableArray *accountList;13@property(nonatomic) ASWebAuthenticationSession *authVC;1415@end1617@implementation AccountListViewController1819- (void)viewDidLoad {20[super viewDidLoad];2122if (self.accountList == nil) {23self.accountList = [NSMutableArray array];24} else {25[self.accountList removeAllObjects];26}2728// List accounts29NSString *listPath = [NSString stringWithFormat:@"%s/accounts", getenv("POJAV_HOME")];30NSFileManager *fm = [NSFileManager defaultManager];31NSArray *files = [fm contentsOfDirectoryAtPath:listPath error:nil];32for(NSString *file in files) {33NSString *path = [listPath stringByAppendingPathComponent:file];34BOOL isDir = NO;35[fm fileExistsAtPath:path isDirectory:(&isDir)];36if(!isDir && [file hasSuffix:@".json"]) {37[self.accountList addObject:parseJSONFromFile(path)];38}39}4041[self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleSingleLine];42}4344- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section45{46return self.accountList.count + 1;47}4849- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath50{51UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];5253if (cell == nil) {54cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];55}5657if (indexPath.row == self.accountList.count) {58cell.imageView.image = [UIImage imageNamed:@"IconAdd"];59cell.textLabel.text = localize(@"login.option.add", nil);60return cell;61}6263NSDictionary *selected = self.accountList[indexPath.row];64// By default, display the saved username65cell.textLabel.text = selected[@"username"];66if ([selected[@"username"] hasPrefix:@"Demo."]) {67// Remove the prefix "Demo."68cell.textLabel.text = [selected[@"username"] substringFromIndex:5];69cell.detailTextLabel.text = localize(@"login.option.demo", nil);70} else if (selected[@"xboxGamertag"] == nil) {71cell.detailTextLabel.text = localize(@"login.option.local", nil);72} else {73// Display the Xbox gamertag for online accounts74cell.detailTextLabel.text = selected[@"xboxGamertag"];75}7677cell.imageView.contentMode = UIViewContentModeCenter;78[cell.imageView setImageWithURL:[NSURL URLWithString:[selected[@"profilePicURL"] stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"]] placeholderImage:[UIImage imageNamed:@"DefaultAccount"]];7980return cell;81}8283- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {84[tableView deselectRowAtIndexPath:indexPath animated:NO];85UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];8687if (indexPath.row == self.accountList.count) {88[self actionAddAccount:cell];89return;90}9192self.modalInPresentation = YES;93self.tableView.userInteractionEnabled = NO;94[self addActivityIndicatorTo:cell];9596id callback = ^(id status, BOOL success) {97[self callbackMicrosoftAuth:status success:success forCell:cell];98};99[[BaseAuthenticator loadSavedName:self.accountList[indexPath.row][@"username"]] refreshTokenWithCallback:callback];100}101102- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {103if (editingStyle == UITableViewCellEditingStyleDelete) {104// TODO: invalidate token105106NSString *str = self.accountList[indexPath.row][@"username"];107NSFileManager *fm = [NSFileManager defaultManager];108NSString *path = [NSString stringWithFormat:@"%s/accounts/%@.json", getenv("POJAV_HOME"), str];109if (self.whenDelete != nil) {110self.whenDelete(str);111}112NSString *xuid = self.accountList[indexPath.row][@"xuid"];113if (xuid) {114[MicrosoftAuthenticator clearTokenDataOfProfile:xuid];115}116[fm removeItemAtPath:path error:nil];117[self.accountList removeObjectAtIndex:indexPath.row];118[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];119}120}121122- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath123{124if (indexPath.row == self.accountList.count) {125return UITableViewCellEditingStyleNone;126} else {127return UITableViewCellEditingStyleDelete;128}129}130131- (NSDictionary *)parseQueryItems:(NSString *)url {132NSMutableDictionary *result = [NSMutableDictionary new];133NSArray<NSURLQueryItem *> *queryItems = [NSURLComponents componentsWithString:url].queryItems;134for (NSURLQueryItem *item in queryItems) {135result[item.name] = item.value;136}137return result;138}139140- (void)actionAddAccount:(UITableViewCell *)sender {141UIAlertController *picker = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];142UIAlertAction *actionMicrosoft = [UIAlertAction actionWithTitle:localize(@"login.option.microsoft", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {143[self actionLoginMicrosoft:sender];144}];145[picker addAction:actionMicrosoft];146UIAlertAction *actionLocal = [UIAlertAction actionWithTitle:localize(@"login.option.local", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {147[self actionLoginLocal:sender];148}];149[picker addAction:actionLocal];150UIAlertAction *cancel = [UIAlertAction actionWithTitle:localize(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil];151[picker addAction:cancel];152153picker.popoverPresentationController.sourceView = sender;154picker.popoverPresentationController.sourceRect = sender.bounds;155156[self presentViewController:picker animated:YES completion:nil];157}158159- (void)actionLoginLocal:(UIView *)sender {160if (getPrefBool(@"warnings.local_warn")) {161setPrefBool(@"warnings.local_warn", NO);162UIAlertController *alert = [UIAlertController alertControllerWithTitle:localize(@"login.warn.title.localmode", nil) message:localize(@"login.warn.message.localmode", nil) preferredStyle:UIAlertControllerStyleActionSheet];163alert.popoverPresentationController.sourceView = sender;164alert.popoverPresentationController.sourceRect = sender.bounds;165UIAlertAction *ok = [UIAlertAction actionWithTitle:localize(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {[self actionLoginLocal:sender];}];166[alert addAction:ok];167[self presentViewController:alert animated:YES completion:nil];168return;169}170UIAlertController *controller = [UIAlertController alertControllerWithTitle:localize(@"Sign in", nil) message:localize(@"login.option.local", nil) preferredStyle:UIAlertControllerStyleAlert];171[controller addTextFieldWithConfigurationHandler:^(UITextField *textField) {172textField.placeholder = localize(@"login.alert.field.username", nil);173textField.clearButtonMode = UITextFieldViewModeWhileEditing;174textField.borderStyle = UITextBorderStyleRoundedRect;175}];176[controller addAction:[UIAlertAction actionWithTitle:localize(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {177NSArray *textFields = controller.textFields;178UITextField *usernameField = textFields[0];179if (usernameField.text.length < 3 || usernameField.text.length > 16) {180controller.message = localize(@"login.error.username.outOfRange", nil);181[self presentViewController:controller animated:YES completion:nil];182} else {183id callback = ^(id status, BOOL success) {184self.whenItemSelected();185[self dismissViewControllerAnimated:YES completion:nil];186};187[[[LocalAuthenticator alloc] initWithInput:usernameField.text] loginWithCallback:callback];188}189}]];190[controller addAction:[UIAlertAction actionWithTitle:localize(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil]];191[self presentViewController:controller animated:YES completion:nil];192}193194- (void)actionLoginMicrosoft:(UITableViewCell *)sender {195NSURL *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"];196197self.authVC =198[[ASWebAuthenticationSession alloc] initWithURL:url199callbackURLScheme:@"ms-xal-00000000402b5328"200completionHandler:^(NSURL * _Nullable callbackURL, NSError * _Nullable error)201{202if (callbackURL == nil) {203if (error.code != ASWebAuthenticationSessionErrorCodeCanceledLogin) {204showDialog(localize(@"Error", nil), error.localizedDescription);205}206return;207}208// NSLog(@"URL returned = %@", [callbackURL absoluteString]);209210NSDictionary *queryItems = [self parseQueryItems:callbackURL.absoluteString];211if (queryItems[@"code"]) {212self.modalInPresentation = YES;213self.tableView.userInteractionEnabled = NO;214[self addActivityIndicatorTo:sender];215id callback = ^(id status, BOOL success) {216if ([status isKindOfClass:NSString.class] && [status isEqualToString:@"DEMO"] && success) {217showDialog(localize(@"login.warn.title.demomode", nil), localize(@"login.warn.message.demomode", nil));218}219[self callbackMicrosoftAuth:status success:success forCell:sender];220};221[[[MicrosoftAuthenticator alloc] initWithInput:queryItems[@"code"]] loginWithCallback:callback];222} else {223if ([queryItems[@"error"] hasPrefix:@"access_denied"]) {224// Ignore access denial responses225return;226}227showDialog(localize(@"Error", nil), queryItems[@"error_description"]);228}229}];230231self.authVC.prefersEphemeralWebBrowserSession = YES;232self.authVC.presentationContextProvider = self;233234if ([self.authVC start] == NO) {235showDialog(localize(@"Error", nil), @"Unable to open Safari");236}237}238239- (void)addActivityIndicatorTo:(UITableViewCell *)cell {240UIActivityIndicatorViewStyle indicatorStyle = UIActivityIndicatorViewStyleMedium;241UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:indicatorStyle];242cell.accessoryView = indicator;243[indicator sizeToFit];244[indicator startAnimating];245}246247- (void)removeActivityIndicatorFrom:(UITableViewCell *)cell {248UIActivityIndicatorView *indicator = (id)cell.accessoryView;249[indicator stopAnimating];250cell.accessoryView = nil;251}252253- (void)callbackMicrosoftAuth:(id)status success:(BOOL)success forCell:(UITableViewCell *)cell {254if (status != nil) {255if (success) {256cell.detailTextLabel.text = status;257} else {258self.modalInPresentation = NO;259self.tableView.userInteractionEnabled = YES;260[self removeActivityIndicatorFrom:cell];261cell.detailTextLabel.text = [status localizedDescription];262NSData *errorData = ((NSError *)status).userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];263NSString *errorStr = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];264NSLog(@"[MSA] Error: %@", errorStr);265showDialog(localize(@"Error", nil), errorStr);266}267} else if (success) {268self.whenItemSelected();269[self removeActivityIndicatorFrom:cell];270[self dismissViewControllerAnimated:YES completion:nil];271}272}273274#pragma mark - UIPopoverPresentationControllerDelegate275- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection {276return UIModalPresentationNone;277}278279#pragma mark - ASWebAuthenticationPresentationContextProviding280- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {281return UIApplication.sharedApplication.windows.firstObject;282}283284@end285286287