diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 2bb9ed9a..f45e1776 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -144,6 +144,8 @@ BFD52C2022A1A9EC000B7ED1 /* node.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1D22A1A9EC000B7ED1 /* node.c */; }; BFD52C2122A1A9EC000B7ED1 /* node_list.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1E22A1A9EC000B7ED1 /* node_list.c */; }; BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; }; + BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */; }; + BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */; }; BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; }; BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */; }; BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */; }; @@ -361,6 +363,8 @@ BFD52C1D22A1A9EC000B7ED1 /* node.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node.c; path = Dependencies/libplist/libcnary/node.c; sourceTree = SOURCE_ROOT; }; BFD52C1E22A1A9EC000B7ED1 /* node_list.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node_list.c; path = Dependencies/libplist/libcnary/node_list.c; sourceTree = SOURCE_ROOT; }; BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; }; + BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = ""; }; + BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = ""; }; BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = ""; }; BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = ""; }; BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = ""; }; @@ -675,9 +679,11 @@ BFD2478A2284C49000981D42 /* Apps */, BFBBE2E2229320A2002097FA /* My Apps */, BFB1169E22933DDC00BB457C /* Updates */, + BFDB69FB22A9A7A6007EA6D6 /* Account */, BFC51D7922972F1F00388324 /* Server */, BFD247982284D7FC00981D42 /* Model */, BFD2478D2284C4C700981D42 /* Components */, + BFDB6A0622A9B114007EA6D6 /* Protocols */, BFD2479D2284FBBD00981D42 /* Extensions */, BFD247962284D7C100981D42 /* Resources */, BFD247972284D7D800981D42 /* Supporting Files */, @@ -771,6 +777,22 @@ path = Connections; sourceTree = ""; }; + BFDB69FB22A9A7A6007EA6D6 /* Account */ = { + isa = PBXGroup; + children = ( + BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */, + ); + path = Account; + sourceTree = ""; + }; + BFDB6A0622A9B114007EA6D6 /* Protocols */ = { + isa = PBXGroup; + children = ( + BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */, + ); + path = Protocols; + sourceTree = ""; + }; BFE6325822A83BA800F30809 /* Authentication */ = { isa = PBXGroup; children = ( @@ -1128,6 +1150,7 @@ files = ( BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */, + BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */, BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */, BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */, BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */, @@ -1149,6 +1172,7 @@ BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, BFD52BD622A08A85000B7ED1 /* Server.swift in Sources */, BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */, + BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */, BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */, BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */, ); diff --git a/AltStore/Account/AccountViewController.swift b/AltStore/Account/AccountViewController.swift new file mode 100644 index 00000000..3ca63e52 --- /dev/null +++ b/AltStore/Account/AccountViewController.swift @@ -0,0 +1,148 @@ +// +// AccountViewController.swift +// AltStore +// +// Created by Riley Testut on 6/6/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +import Roxas + +class AccountViewController: UITableViewController +{ + private var team: Team? + + private lazy var placeholderView = self.makePlaceholderView() + + @IBOutlet var accountNameLabel: UILabel! + @IBOutlet var accountEmailLabel: UILabel! + + @IBOutlet var teamNameLabel: UILabel! + @IBOutlet var teamTypeLabel: UILabel! + + override func viewDidLoad() + { + super.viewDidLoad() + + self.update() + } + + override func viewWillAppear(_ animated: Bool) + { + super.viewWillAppear(animated) + + self.update() + } +} + +private extension AccountViewController +{ + func makePlaceholderView() -> RSTPlaceholderView + { + let placeholderView = RSTPlaceholderView() + placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + placeholderView.textLabel.text = NSLocalizedString("Not Signed In", comment: "") + placeholderView.detailTextLabel.text = NSLocalizedString("Please sign in with your Apple ID to download and refresh apps.", comment: "") + + let signInButton = UIButton(type: .system) + signInButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + signInButton.setTitle(NSLocalizedString("Sign In", comment: ""), for: .normal) + signInButton.addTarget(self, action: #selector(AccountViewController.signIn(_:)), for: .primaryActionTriggered) + placeholderView.stackView.addArrangedSubview(signInButton) + + return placeholderView + } + + func update() + { + if let team = DatabaseManager.shared.activeTeam() + { + self.tableView.separatorStyle = .singleLine + self.tableView.isScrollEnabled = true + self.tableView.backgroundView = nil + + self.navigationItem.rightBarButtonItem?.isEnabled = true + + self.accountNameLabel.text = team.account.localizedName + self.accountEmailLabel.text = team.account.appleID + + self.teamNameLabel.text = team.name + self.teamTypeLabel.text = team.type.localizedDescription + + self.team = team + } + else + { + self.tableView.separatorStyle = .none + self.tableView.isScrollEnabled = false + self.tableView.backgroundView = self.placeholderView + + self.navigationItem.rightBarButtonItem?.isEnabled = false + + self.team = nil + } + + if self.isViewLoaded + { + self.tableView.reloadData() + } + } +} + +private extension AccountViewController +{ + @objc func signIn(_ sender: UIButton) + { + sender.isIndicatingActivity = true + + AppManager.shared.authenticate(presentingViewController: self) { (result) in + DispatchQueue.main.async { + sender.isIndicatingActivity = false + self.update() + } + } + } + + @IBAction func signOut(_ sender: UIBarButtonItem) + { + func signOut() + { + DatabaseManager.shared.signOut { (error) in + DispatchQueue.main.async { + if let error = error + { + let toastView = RSTToastView(text: error.localizedDescription, detailText: nil) + toastView.tintColor = .red + toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + } + else + { + let toastView = RSTToastView(text: NSLocalizedString("Successfully Signed Out!", comment: ""), detailText: nil) + toastView.tintColor = .altPurple + toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + } + + self.update() + } + } + } + + let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to sign out?", comment: ""), message: NSLocalizedString("You will no longer be able to install or refresh apps once you sign out.", comment: ""), preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Sign Out", comment: ""), style: .destructive) { _ in signOut() }) + alertController.addAction(.cancel) + self.present(alertController, animated: true, completion: nil) + } +} + +extension AccountViewController +{ + override func numberOfSections(in tableView: UITableView) -> Int + { + let count = (self.team == nil) ? 0 : super.numberOfSections(in: tableView) + return count + } +} + + diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index e32b0f4b..a4a2fcc9 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -36,11 +36,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if UserDefaults.standard.firstLaunch == nil { - Keychain.shared.appleIDEmailAddress = nil - Keychain.shared.appleIDPassword = nil - Keychain.shared.signingCertificatePrivateKey = nil - Keychain.shared.signingCertificateIdentifier = nil - + Keychain.shared.reset() UserDefaults.standard.firstLaunch = Date() } diff --git a/AltStore/Authentication/AuthenticationOperation.swift b/AltStore/Authentication/AuthenticationOperation.swift index 58b68036..12b5ac43 100644 --- a/AltStore/Authentication/AuthenticationOperation.swift +++ b/AltStore/Authentication/AuthenticationOperation.swift @@ -77,21 +77,29 @@ class AuthenticationOperation: RSTOperation // Account let account = Account(altAccount, context: context) + account.isActiveAccount = true let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier) let otherAccounts = try context.fetch(otherAccountsFetchRequest) - otherAccounts.forEach(context.delete(_:)) + for account in otherAccounts + { + account.isActiveAccount = false + } // Team let team = Team(altTeam, account: account, context: context) + team.isActiveTeam = true let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier) let otherTeams = try context.fetch(otherTeamsFetchRequest) - otherTeams.forEach(context.delete(_:)) + for team in otherTeams + { + team.isActiveTeam = false + } // Save try context.save() @@ -264,27 +272,12 @@ private extension AuthenticationOperation case .success(let teams): DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in - do + if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier }) { - let fetchRequest = Team.fetchRequest() as NSFetchRequest - fetchRequest.fetchLimit = 1 - fetchRequest.returnsObjectsAsFaults = false - - let fetchedTeams = try context.fetch(fetchRequest) - - if let fetchedTeam = fetchedTeams.first, let altTeam = teams.first(where: { $0.identifier == fetchedTeam.identifier }) - { - completionHandler(.success(altTeam)) - } - else - { - selectTeam(from: teams) - } + completionHandler(.success(altTeam)) } - catch + else { - print("Error fetching Teams.", error) - selectTeam(from: teams) } } diff --git a/AltStore/Authentication/SelectTeamViewController.swift b/AltStore/Authentication/SelectTeamViewController.swift index 6d52ad4b..861c3510 100644 --- a/AltStore/Authentication/SelectTeamViewController.swift +++ b/AltStore/Authentication/SelectTeamViewController.swift @@ -57,16 +57,7 @@ private extension SelectTeamViewController dataSource.proxy = self dataSource.cellConfigurationHandler = { [weak self] (cell, team, indexPath) in cell.textLabel?.text = team.name - - switch team.type - { - case .unknown: cell.detailTextLabel?.text = NSLocalizedString("Unknown", comment: "") - case .free: cell.detailTextLabel?.text = NSLocalizedString("Free Developer Account", comment: "") - case .individual: cell.detailTextLabel?.text = NSLocalizedString("Individual", comment: "") - case .organization: cell.detailTextLabel?.text = NSLocalizedString("Organization", comment: "") - @unknown default: cell.detailTextLabel?.text = nil - } - + cell.detailTextLabel?.text = team.type.localizedDescription cell.accessoryType = (self?.selectedTeam == team) ? .checkmark : .none } diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index 813d1271..59de1fd3 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -21,6 +21,7 @@ + @@ -468,11 +469,145 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/AltStore/Components/Keychain.swift b/AltStore/Components/Keychain.swift index ee700327..6d295274 100644 --- a/AltStore/Components/Keychain.swift +++ b/AltStore/Components/Keychain.swift @@ -20,6 +20,14 @@ class Keychain private init() { } + + func reset() + { + self.appleIDEmailAddress = nil + self.appleIDPassword = nil + self.signingCertificatePrivateKey = nil + self.signingCertificateIdentifier = nil + } } extension Keychain diff --git a/AltStore/Model/Account.swift b/AltStore/Model/Account.swift index 5945f2dd..5a6a732f 100644 --- a/AltStore/Model/Account.swift +++ b/AltStore/Model/Account.swift @@ -12,7 +12,7 @@ import CoreData import AltSign @objc(Account) -class Account: NSManagedObject +class Account: NSManagedObject, Fetchable { var localizedName: String { var components = PersonNameComponents() @@ -30,6 +30,8 @@ class Account: NSManagedObject @NSManaged var firstName: String @NSManaged var lastName: String + @NSManaged var isActiveAccount: Bool + /* Relationships */ @NSManaged var teams: Set diff --git a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents index 50511af0..5c281e2d 100644 --- a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents +++ b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents @@ -4,6 +4,7 @@ + @@ -39,6 +40,7 @@ + @@ -49,9 +51,9 @@ + - - + \ No newline at end of file diff --git a/AltStore/Model/DatabaseManager.swift b/AltStore/Model/DatabaseManager.swift index 36a55ac2..7374b25a 100644 --- a/AltStore/Model/DatabaseManager.swift +++ b/AltStore/Model/DatabaseManager.swift @@ -38,6 +38,35 @@ public extension DatabaseManager completionHandler(error) } } + + func signOut(completionHandler: @escaping (Error?) -> Void) + { + self.persistentContainer.performBackgroundTask { (context) in + if let account = self.activeAccount(in: context) + { + account.isActiveAccount = false + } + + if let team = self.activeTeam(in: context) + { + team.isActiveTeam = false + } + + do + { + try context.save() + + Keychain.shared.reset() + + completionHandler(nil) + } + catch + { + print("Failed to save when signing out.", error) + completionHandler(error) + } + } + } } public extension DatabaseManager @@ -46,3 +75,22 @@ public extension DatabaseManager return self.persistentContainer.viewContext } } + +extension DatabaseManager +{ + func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account? + { + let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount)) + + let activeAccount = Account.first(satisfying: predicate, in: context) + return activeAccount + } + + func activeTeam(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Team? + { + let predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam)) + + let activeTeam = Team.first(satisfying: predicate, in: context) + return activeTeam + } +} diff --git a/AltStore/Model/Team.swift b/AltStore/Model/Team.swift index 4b808e30..cdb3ded3 100644 --- a/AltStore/Model/Team.swift +++ b/AltStore/Model/Team.swift @@ -11,14 +11,30 @@ import CoreData import AltSign +extension ALTTeamType +{ + var localizedDescription: String { + switch self + { + case .free: return NSLocalizedString("Free Developer Account", comment: "") + case .individual: return NSLocalizedString("Individual", comment: "") + case .organization: return NSLocalizedString("Organization", comment: "") + case .unknown: fallthrough + @unknown default: return NSLocalizedString("Unknown", comment: "") + } + } +} + @objc(Team) -class Team: NSManagedObject +class Team: NSManagedObject, Fetchable { /* Properties */ @NSManaged var name: String @NSManaged var identifier: String @NSManaged var type: ALTTeamType + @NSManaged var isActiveTeam: Bool + /* Relationships */ @NSManaged private(set) var account: Account! diff --git a/AltStore/Protocols/Fetchable.swift b/AltStore/Protocols/Fetchable.swift new file mode 100644 index 00000000..b1bba1d5 --- /dev/null +++ b/AltStore/Protocols/Fetchable.swift @@ -0,0 +1,61 @@ +// +// NSManagedObject+Conveniences.swift +// AltStore +// +// Created by Riley Testut on 6/6/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import CoreData + +protocol Fetchable: NSManagedObject +{ +} + +extension Fetchable +{ + static func first(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext) -> Self? + { + let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, returnFirstResult: true) + return managedObjects.first + } + + static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext) -> [Self] + { + let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, returnFirstResult: false) + return managedObjects + } + + private static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext, returnFirstResult: Bool) -> [Self] + { + let registeredObjects = context.registeredObjects.lazy.compactMap({ $0 as? Self }).filter({ predicate?.evaluate(with: $0) != false }) + + if let managedObject = registeredObjects.first, returnFirstResult + { + return [managedObject] + } + + let fetchRequest = self.fetchRequest() as! NSFetchRequest + fetchRequest.predicate = predicate + fetchRequest.sortDescriptors = sortDescriptors + + do + { + let managedObjects = try context.fetch(fetchRequest) + + if let managedObject = managedObjects.first, returnFirstResult + { + return [managedObject] + } + else + { + return managedObjects + } + } + catch + { + print("Failed to fetch managed objects.", error) + return [] + } + } +}