mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
[AltStore] Revises authentication flow with better UI
This commit is contained in:
@@ -144,6 +144,13 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; };
|
||||
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
|
||||
BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */; };
|
||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; };
|
||||
BFFC044E22A204F40066B31F /* App.ipa in Resources */ = {isa = PBXBuildFile; fileRef = BFFC044D22A204F30066B31F /* App.ipa */; };
|
||||
DBAC68F8EC03F4A41D62EDE1 /* Pods_AltStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1039C07E517311FC499A0B64 /* Pods_AltStore.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -354,6 +361,13 @@
|
||||
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; };
|
||||
BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = "<group>"; };
|
||||
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = "<group>"; };
|
||||
BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; };
|
||||
BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = "<group>"; };
|
||||
BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
|
||||
BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaceCertificateViewController.swift; sourceTree = "<group>"; };
|
||||
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = "<group>"; };
|
||||
BFFC044D22A204F30066B31F /* App.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = App.ipa; sourceTree = "<group>"; };
|
||||
EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -657,6 +671,7 @@
|
||||
children = (
|
||||
BFD2476D2284B9A500981D42 /* AppDelegate.swift */,
|
||||
BFD247732284B9A500981D42 /* Main.storyboard */,
|
||||
BFE6325822A83BA800F30809 /* Authentication */,
|
||||
BFD2478A2284C49000981D42 /* Apps */,
|
||||
BFBBE2E2229320A2002097FA /* My Apps */,
|
||||
BFB1169E22933DDC00BB457C /* Updates */,
|
||||
@@ -730,8 +745,10 @@
|
||||
children = (
|
||||
BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */,
|
||||
BFB11691229322E400BB457C /* DatabaseManager.swift */,
|
||||
BFE6326722A858F300F30809 /* Account.swift */,
|
||||
BFBBE2DE22931F73002097FA /* App.swift */,
|
||||
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
|
||||
BFE6326522A857C100F30809 /* Team.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@@ -754,6 +771,18 @@
|
||||
path = Connections;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFE6325822A83BA800F30809 /* Authentication */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFE6325922A83BEB00F30809 /* Authentication.storyboard */,
|
||||
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */,
|
||||
BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */,
|
||||
BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */,
|
||||
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
|
||||
);
|
||||
path = Authentication;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -956,6 +985,7 @@
|
||||
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
||||
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */,
|
||||
BFD247752284B9A500981D42 /* Main.storyboard in Resources */,
|
||||
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1096,23 +1126,29 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */,
|
||||
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
|
||||
BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */,
|
||||
BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */,
|
||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
|
||||
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
|
||||
BFBBE2DF22931F73002097FA /* App.swift in Sources */,
|
||||
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
|
||||
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
||||
BFE6326822A858F300F30809 /* Account.swift in Sources */,
|
||||
BFE6326622A857C200F30809 /* Team.swift in Sources */,
|
||||
BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */,
|
||||
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
|
||||
BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */,
|
||||
BFB116A022933DEB00BB457C /* UpdatesViewController.swift in Sources */,
|
||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
||||
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */,
|
||||
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
|
||||
BFD247932284D4B700981D42 /* AppTableViewCell.swift in Sources */,
|
||||
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
|
||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
||||
BFD52BD622A08A85000B7ED1 /* Server.swift in Sources */,
|
||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
||||
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */,
|
||||
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */,
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
Keychain.shared.appleIDEmailAddress = nil
|
||||
Keychain.shared.appleIDPassword = nil
|
||||
Keychain.shared.signingCertificatePrivateKey = nil
|
||||
Keychain.shared.signingCertificateIdentifier = nil
|
||||
|
||||
UserDefaults.standard.firstLaunch = Date()
|
||||
}
|
||||
|
||||
@@ -86,16 +86,19 @@ private extension AppDetailViewController
|
||||
|
||||
self.descriptionLabel.text = self.app.localizedDescription
|
||||
|
||||
if self.app.installedApp == nil
|
||||
if !self.downloadButton.isIndicatingActivity
|
||||
{
|
||||
let text = String(format: NSLocalizedString("Download %@", comment: ""), self.app.name)
|
||||
self.downloadButton.setTitle(text, for: .normal)
|
||||
self.downloadButton.isEnabled = true
|
||||
}
|
||||
else
|
||||
{
|
||||
self.downloadButton.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal)
|
||||
self.downloadButton.isEnabled = false
|
||||
if self.app.installedApp == nil
|
||||
{
|
||||
let text = String(format: NSLocalizedString("Download %@", comment: ""), self.app.name)
|
||||
self.downloadButton.setTitle(text, for: .normal)
|
||||
self.downloadButton.isEnabled = true
|
||||
}
|
||||
else
|
||||
{
|
||||
self.downloadButton.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal)
|
||||
self.downloadButton.isEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +138,10 @@ private extension AppDetailViewController
|
||||
toastView.show(in: self.navigationController!.view, duration: 2)
|
||||
}
|
||||
}
|
||||
catch AppManager.AppError.authentication(AuthenticationOperation.Error.cancelled)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
@@ -145,8 +152,8 @@ private extension AppDetailViewController
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
sender.isIndicatingActivity = false
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +60,11 @@ class AppManager
|
||||
static let shared = AppManager()
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
private let operationQueue = OperationQueue()
|
||||
|
||||
private init()
|
||||
{
|
||||
self.operationQueue.name = "com.rileytestut.AltStore.AppManager"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +99,15 @@ extension AppManager
|
||||
print("Error while fetching installed apps")
|
||||
}
|
||||
}
|
||||
|
||||
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTTeam, ALTCertificate), Error>) -> Void)
|
||||
{
|
||||
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
||||
authenticationOperation.resultHandler = { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
self.operationQueue.addOperation(authenticationOperation)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppManager
|
||||
@@ -131,14 +142,14 @@ extension AppManager
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.authentication(error)))
|
||||
case .success(let team):
|
||||
case .success(let team, let certificate):
|
||||
|
||||
// Fetch signing resources
|
||||
self.fetchSigningResources(for: app, team: team, presentingViewController: presentingViewController) { (result) in
|
||||
// Fetch provisioning profile
|
||||
self.prepareProvisioningProfile(for: app, team: team) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.fetchingSigningResources(error)))
|
||||
case .success(let certificate, let profile):
|
||||
case .success(let profile):
|
||||
|
||||
// Prepare app
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
@@ -233,44 +244,37 @@ extension AppManager
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.authentication(error)))
|
||||
case .success(let team):
|
||||
case .success(let team, let certificate):
|
||||
|
||||
// Fetch Certificate
|
||||
self.fetchCertificate(for: team, presentingViewController: nil) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.fetchingSigningResources(error)))
|
||||
case .success(let certificate):
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
// Sign
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
|
||||
for app in installedApps
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
app.managedObjectContext?.perform {
|
||||
let bundleIdentifier = app.bundleIdentifier
|
||||
print("Refreshing App:", bundleIdentifier)
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
|
||||
for app in installedApps
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
app.managedObjectContext?.perform {
|
||||
let bundleIdentifier = app.bundleIdentifier
|
||||
print("Refreshing App:", bundleIdentifier)
|
||||
|
||||
self.refresh(app, signer: signer, context: context) { (result) in
|
||||
print("Refreshed App: \(bundleIdentifier).", result)
|
||||
results[bundleIdentifier] = result
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
context.perform {
|
||||
finish(.success(results))
|
||||
}
|
||||
self.refresh(app, signer: signer, context: context) { (result) in
|
||||
print("Refreshed App: \(bundleIdentifier).", result)
|
||||
results[bundleIdentifier] = result
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
context.perform {
|
||||
finish(.success(results))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,63 +303,6 @@ private extension AppManager
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
|
||||
{
|
||||
func authenticate(emailAddress: String, password: String)
|
||||
{
|
||||
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in
|
||||
do
|
||||
{
|
||||
let account = try Result(account, error).get()
|
||||
|
||||
Keychain.shared.appleIDEmailAddress = emailAddress
|
||||
Keychain.shared.appleIDPassword = password
|
||||
|
||||
self.fetchTeam(for: account, presentingViewController: presentingViewController, completionHandler: completionHandler)
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let emailAddress = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
|
||||
{
|
||||
authenticate(emailAddress: emailAddress, password: password)
|
||||
}
|
||||
else if let presentingViewController = presentingViewController
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: "Enter Apple ID + Password", message: "", preferredStyle: .alert)
|
||||
alertController.addTextField { (textField) in
|
||||
textField.placeholder = "Apple ID"
|
||||
textField.textContentType = .emailAddress
|
||||
}
|
||||
alertController.addTextField { (textField) in
|
||||
textField.placeholder = "Password"
|
||||
textField.textContentType = .password
|
||||
}
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: "Sign In", style: .default) { [unowned alertController] (action) in
|
||||
guard
|
||||
let emailAddress = alertController.textFields![0].text,
|
||||
let password = alertController.textFields![1].text,
|
||||
!emailAddress.isEmpty, !password.isEmpty
|
||||
else { return completionHandler(.failure(ALTAppleAPIError(.incorrectCredentials))) }
|
||||
|
||||
authenticate(emailAddress: emailAddress, password: password)
|
||||
})
|
||||
|
||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(AppError.notAuthenticated))
|
||||
}
|
||||
}
|
||||
|
||||
func prepareProvisioningProfile(for app: App, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return completionHandler(.failure(AppError.missingUDID)) }
|
||||
@@ -397,32 +344,6 @@ private extension AppManager
|
||||
}
|
||||
}
|
||||
|
||||
func fetchSigningResources(for app: App, team: ALTTeam, presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTCertificate, ALTProvisioningProfile), Error>) -> Void)
|
||||
{
|
||||
self.fetchCertificate(for: team, presentingViewController: presentingViewController) { (result) in
|
||||
do
|
||||
{
|
||||
let certificate = try result.get()
|
||||
|
||||
self.prepareProvisioningProfile(for: app, team: team) { (result) in
|
||||
do
|
||||
{
|
||||
let provisioningProfile = try result.get()
|
||||
completionHandler(.success((certificate, provisioningProfile)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepare(_ installedApp: InstalledApp, provisioningProfile: ALTProvisioningProfile, signer: ALTSigner, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
@@ -484,132 +405,6 @@ private extension AppManager
|
||||
|
||||
private extension AppManager
|
||||
{
|
||||
func fetchTeam(for account: ALTAccount, presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
|
||||
do
|
||||
{
|
||||
let teams = try Result(teams, error).get()
|
||||
guard teams.count > 0 else { throw ALTAppleAPIError(.noTeams) }
|
||||
|
||||
if let team = teams.first, teams.count == 1
|
||||
{
|
||||
completionHandler(.success(team))
|
||||
}
|
||||
else
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: "Select Team", message: "", preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
|
||||
for team in teams
|
||||
{
|
||||
alertController.addAction(UIAlertAction(title: team.name, style: .default) { (action) in
|
||||
completionHandler(.success(team))
|
||||
})
|
||||
}
|
||||
|
||||
presentingViewController?.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchCertificate(for team: ALTTeam, presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
if
|
||||
let identifier = UserDefaults.standard.signingCertificateIdentifier,
|
||||
let privateKey = Keychain.shared.signingCertificatePrivateKey,
|
||||
let certificate = certificates.first(where: { $0.identifier == identifier })
|
||||
{
|
||||
certificate.privateKey = privateKey
|
||||
completionHandler(.success(certificate))
|
||||
}
|
||||
else if certificates.count < 1
|
||||
{
|
||||
let machineName = "AltStore - " + UIDevice.current.name
|
||||
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team) { (certificate, error) in
|
||||
do
|
||||
{
|
||||
let certificate = try Result(certificate, error).get()
|
||||
guard let privateKey = certificate.privateKey else { throw AppError.missingPrivateKey }
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else {
|
||||
throw AppError.missingCertificate
|
||||
}
|
||||
|
||||
certificate.privateKey = privateKey
|
||||
|
||||
UserDefaults.standard.signingCertificateIdentifier = certificate.identifier
|
||||
Keychain.shared.signingCertificatePrivateKey = privateKey
|
||||
|
||||
completionHandler(.success(certificate))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let presentingViewController = presentingViewController
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: "Too Many Certificates", message: "Please select the certificate you would like to revoke.", preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
|
||||
for certificate in certificates
|
||||
{
|
||||
alertController.addAction(UIAlertAction(title: certificate.name, style: .default) { (action) in
|
||||
ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in
|
||||
do
|
||||
{
|
||||
try Result(success, error).get()
|
||||
self.fetchCertificate(for: team, presentingViewController: presentingViewController, completionHandler: completionHandler)
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(AppError.multipleCertificates))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func register(_ device: ALTDevice, team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
|
||||
|
||||
242
AltStore/Authentication/Authentication.storyboard
Normal file
242
AltStore/Authentication/Authentication.storyboard
Normal file
@@ -0,0 +1,242 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Apple ID-->
|
||||
<scene sceneID="3cc-cd-zDK">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="authenticationViewController" id="nRn-xt-2XS" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" estimatedRowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="r38-H3-S3C">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<sections>
|
||||
<tableViewSection id="uDm-cx-LdY">
|
||||
<string key="footerTitle">Your email address and password are used only to sign in with Apple and is never stored.
|
||||
|
||||
If you have two-factor authentication enabled, make sure to use an app-specific password.</string>
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ER5-4r-tld">
|
||||
<rect key="frame" x="0.0" y="35" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ER5-4r-tld" id="BnC-HI-d8z">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="70T-cn-6XF">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apple ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="09n-b4-DRC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="74" height="43.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="74" id="Y87-hZ-IsD"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Email Address" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="V6B-NM-wpL">
|
||||
<rect key="frame" x="90" y="0.0" width="253" height="43.5"/>
|
||||
<nil key="textColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<textInputTraits key="textInputTraits" returnKeyType="next" enablesReturnKeyAutomatically="YES" textContentType="email"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="nRn-xt-2XS" id="5Us-OB-B4F"/>
|
||||
</connections>
|
||||
</textField>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="70T-cn-6XF" firstAttribute="top" secondItem="BnC-HI-d8z" secondAttribute="top" id="Zyt-OB-o6T"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="70T-cn-6XF" secondAttribute="trailing" id="lYn-uy-vRk"/>
|
||||
<constraint firstAttribute="bottom" secondItem="70T-cn-6XF" secondAttribute="bottom" id="urj-EQ-5WK"/>
|
||||
<constraint firstItem="70T-cn-6XF" firstAttribute="leading" secondItem="BnC-HI-d8z" secondAttribute="leadingMargin" id="yqr-Kr-I93"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="E9B-Cb-M5e">
|
||||
<rect key="frame" x="0.0" y="79" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="E9B-Cb-M5e" id="S4n-4w-12m">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="pON-cO-VYR">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Password" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vqv-cC-kya">
|
||||
<rect key="frame" x="0.0" y="0.0" width="74" height="43.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="74" id="Egk-ba-Kh3"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="z98-Sm-yDv">
|
||||
<rect key="frame" x="90" y="0.0" width="253" height="43.5"/>
|
||||
<nil key="textColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<textInputTraits key="textInputTraits" returnKeyType="go" enablesReturnKeyAutomatically="YES" secureTextEntry="YES" textContentType="password"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="nRn-xt-2XS" id="7pH-Sf-Wmb"/>
|
||||
</connections>
|
||||
</textField>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="pON-cO-VYR" secondAttribute="trailing" id="IPH-Og-2ch"/>
|
||||
<constraint firstAttribute="bottom" secondItem="pON-cO-VYR" secondAttribute="bottom" id="j7H-Ds-pJg"/>
|
||||
<constraint firstItem="pON-cO-VYR" firstAttribute="leading" secondItem="S4n-4w-12m" secondAttribute="leadingMargin" id="uAc-4j-0pB"/>
|
||||
<constraint firstItem="pON-cO-VYR" firstAttribute="top" secondItem="S4n-4w-12m" secondAttribute="top" id="xZe-CS-STZ"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="nRn-xt-2XS" id="VWO-oe-ykv"/>
|
||||
<outlet property="delegate" destination="nRn-xt-2XS" id="CL1-Go-uiO"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Apple ID" id="viw-66-ZJ7">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="KXh-qW-MIA">
|
||||
<connections>
|
||||
<action selector="cancel" destination="nRn-xt-2XS" id="l1X-bA-xsz"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" title="Sign In" style="done" id="mkE-Q8-CxO">
|
||||
<connections>
|
||||
<action selector="authenticate" destination="nRn-xt-2XS" id="q60-9N-xVb"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
<connections>
|
||||
<outlet property="emailAddressTextField" destination="V6B-NM-wpL" id="N3F-eI-yhE"/>
|
||||
<outlet property="passwordTextField" destination="z98-Sm-yDv" id="WDu-6c-oBa"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="v2u-D2-stc" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="605.60000000000002" y="19.340329835082461"/>
|
||||
</scene>
|
||||
<!--Select Team-->
|
||||
<scene sceneID="0Hb-4t-vQ3">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="selectTeamViewController" id="R11-Yh-Wb1" customClass="SelectTeamViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="g2d-7w-OVl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="iCV-rW-IhB" detailTextLabel="2hi-el-KvN" style="IBUITableViewCellStyleSubtitle" id="pPa-pY-koy">
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pPa-pY-koy" id="DjO-Wt-6j2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="iCV-rW-IhB">
|
||||
<rect key="frame" x="16" y="5" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2hi-el-KvN">
|
||||
<rect key="frame" x="16" y="25.5" width="33" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="R11-Yh-Wb1" id="zkX-xW-GvZ"/>
|
||||
<outlet property="delegate" destination="R11-Yh-Wb1" id="vP7-NA-Y0n"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Select Team" id="ALr-U3-Ucl">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="HUE-P1-xa1">
|
||||
<connections>
|
||||
<action selector="cancel" destination="R11-Yh-Wb1" id="Ckg-bQ-0nv"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" title="Next" style="done" id="7Ou-hQ-Cr3">
|
||||
<connections>
|
||||
<action selector="chooseTeam:" destination="R11-Yh-Wb1" id="nin-nM-lxU"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="HxT-dJ-1Ry" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1354" y="20"/>
|
||||
</scene>
|
||||
<!--Replace Certificate-->
|
||||
<scene sceneID="fW2-QW-a2Z">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="replaceCertificateViewController" id="LAG-dk-a0f" customClass="ReplaceCertificateViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="enT-LI-CNI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="luH-7x-QoO" style="IBUITableViewCellStyleDefault" id="i0O-XG-rRJ">
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i0O-XG-rRJ" id="GCT-3I-GCy">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="luH-7x-QoO">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="LAG-dk-a0f" id="kOS-KX-Duz"/>
|
||||
<outlet property="delegate" destination="LAG-dk-a0f" id="plW-kJ-BmR"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Replace Certificate" id="BM2-Vg-AJk">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="lPC-Dj-3Ik">
|
||||
<connections>
|
||||
<action selector="cancel" destination="LAG-dk-a0f" id="5C2-Hg-Les"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" title="Next" style="done" id="ndJ-l9-HeM">
|
||||
<connections>
|
||||
<action selector="replaceCertificate:" destination="LAG-dk-a0f" id="vl2-E6-qi4"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="yxU-EG-3sE" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2135" y="19"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<color key="tintColor" name="Purple"/>
|
||||
</document>
|
||||
382
AltStore/Authentication/AuthenticationOperation.swift
Normal file
382
AltStore/Authentication/AuthenticationOperation.swift
Normal file
@@ -0,0 +1,382 @@
|
||||
//
|
||||
// AuthenticationOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/5/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
extension AuthenticationOperation
|
||||
{
|
||||
enum Error: LocalizedError
|
||||
{
|
||||
case cancelled
|
||||
|
||||
case notAuthenticated
|
||||
case noTeam
|
||||
case noCertificate
|
||||
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
||||
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
||||
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
||||
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationOperation: RSTOperation
|
||||
{
|
||||
var resultHandler: ((Result<(ALTTeam, ALTCertificate), Swift.Error>) -> Void)?
|
||||
|
||||
private weak var presentingViewController: UIViewController?
|
||||
|
||||
private lazy var navigationController = UINavigationController()
|
||||
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
|
||||
|
||||
private var appleIDPassword: String?
|
||||
|
||||
override var isAsynchronous: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(presentingViewController: UIViewController?)
|
||||
{
|
||||
self.presentingViewController = presentingViewController
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.Authenticate")
|
||||
|
||||
func finish(_ result: Result<(ALTTeam, ALTCertificate), Swift.Error>)
|
||||
{
|
||||
print("Finished authenticating with result:", result)
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
do
|
||||
{
|
||||
let (altTeam, altCertificate) = try result.get()
|
||||
let altAccount = altTeam.account
|
||||
|
||||
// Account
|
||||
let account = Account(altAccount, context: context)
|
||||
|
||||
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
|
||||
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
|
||||
|
||||
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
|
||||
otherAccounts.forEach(context.delete(_:))
|
||||
|
||||
// Team
|
||||
let team = Team(altTeam, account: account, context: context)
|
||||
|
||||
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
|
||||
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
|
||||
|
||||
let otherTeams = try context.fetch(otherTeamsFetchRequest)
|
||||
otherTeams.forEach(context.delete(_:))
|
||||
|
||||
// Save
|
||||
try context.save()
|
||||
|
||||
// Update keychain
|
||||
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
|
||||
Keychain.shared.appleIDPassword = self.appleIDPassword
|
||||
|
||||
Keychain.shared.signingCertificateIdentifier = altCertificate.identifier
|
||||
Keychain.shared.signingCertificatePrivateKey = altCertificate.privateKey
|
||||
|
||||
self.resultHandler?(.success((altTeam, altCertificate)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.resultHandler?(.failure(error))
|
||||
}
|
||||
|
||||
self.finish()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
RSTEndBackgroundTask(backgroundTaskID)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign In
|
||||
self.signIn { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let account):
|
||||
|
||||
// Fetch Team
|
||||
self.fetchTeam(for: account) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let team):
|
||||
|
||||
// Fetch Certificate
|
||||
self.fetchCertificate(for: team) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let certificate): finish(.success((team, certificate)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AuthenticationOperation
|
||||
{
|
||||
func present(_ viewController: UIViewController) -> Bool
|
||||
{
|
||||
guard let presentingViewController = self.presentingViewController else { return false }
|
||||
|
||||
self.navigationController.view.tintColor = .altPurple
|
||||
|
||||
if self.navigationController.viewControllers.isEmpty
|
||||
{
|
||||
self.navigationController.setViewControllers([viewController], animated: false)
|
||||
presentingViewController.present(self.navigationController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
viewController.navigationItem.leftBarButtonItem = nil
|
||||
self.navigationController.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private extension AuthenticationOperation
|
||||
{
|
||||
func signIn(completionHandler: @escaping (Result<ALTAccount, Swift.Error>) -> Void)
|
||||
{
|
||||
func authenticate()
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
||||
authenticationViewController.authenticationHandler = { (result) in
|
||||
if let (account, password) = result
|
||||
{
|
||||
self.appleIDPassword = password
|
||||
|
||||
completionHandler(.success(account))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(Error.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(authenticationViewController)
|
||||
{
|
||||
completionHandler(.failure(Error.notAuthenticated))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
|
||||
{
|
||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in
|
||||
do
|
||||
{
|
||||
self.appleIDPassword = password
|
||||
|
||||
let account = try Result(account, error).get()
|
||||
completionHandler(.success(account))
|
||||
}
|
||||
catch ALTAppleAPIError.incorrectCredentials
|
||||
{
|
||||
authenticate()
|
||||
}
|
||||
catch ALTAppleAPIError.appSpecificPasswordRequired
|
||||
{
|
||||
authenticate()
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
authenticate()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
|
||||
{
|
||||
func selectTeam(from teams: [ALTTeam])
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
||||
selectTeamViewController.teams = teams
|
||||
selectTeamViewController.selectionHandler = { (team) in
|
||||
if let team = team
|
||||
{
|
||||
completionHandler(.success(team))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(Error.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(selectTeamViewController)
|
||||
{
|
||||
completionHandler(.failure(Error.noTeam))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
|
||||
switch Result(teams, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let teams):
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
do
|
||||
{
|
||||
let fetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
|
||||
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)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error fetching Teams.", error)
|
||||
|
||||
selectTeam(from: teams)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
|
||||
{
|
||||
func requestCertificate()
|
||||
{
|
||||
let machineName = "AltStore - " + UIDevice.current.name
|
||||
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team) { (certificate, error) in
|
||||
do
|
||||
{
|
||||
let certificate = try Result(certificate, error).get()
|
||||
guard let privateKey = certificate.privateKey else { throw Error.missingPrivateKey }
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else {
|
||||
throw Error.missingCertificate
|
||||
}
|
||||
|
||||
certificate.privateKey = privateKey
|
||||
completionHandler(.success(certificate))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceCertificate(from certificates: [ALTCertificate])
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let replaceCertificateViewController = self.storyboard.instantiateViewController(withIdentifier: "replaceCertificateViewController") as! ReplaceCertificateViewController
|
||||
replaceCertificateViewController.team = team
|
||||
replaceCertificateViewController.certificates = certificates
|
||||
replaceCertificateViewController.replacementHandler = { (certificate) in
|
||||
if certificate != nil
|
||||
{
|
||||
requestCertificate()
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(Error.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(replaceCertificateViewController)
|
||||
{
|
||||
completionHandler(.failure(Error.noCertificate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
if
|
||||
let identifier = Keychain.shared.signingCertificateIdentifier,
|
||||
let privateKey = Keychain.shared.signingCertificatePrivateKey,
|
||||
let certificate = certificates.first(where: { $0.identifier == identifier })
|
||||
{
|
||||
certificate.privateKey = privateKey
|
||||
completionHandler(.success(certificate))
|
||||
}
|
||||
else if certificates.isEmpty
|
||||
{
|
||||
requestCertificate()
|
||||
}
|
||||
else
|
||||
{
|
||||
replaceCertificate(from: certificates)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
151
AltStore/Authentication/AuthenticationViewController.swift
Normal file
151
AltStore/Authentication/AuthenticationViewController.swift
Normal file
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// AuthenticationViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/5/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
class AuthenticationViewController: UITableViewController
|
||||
{
|
||||
var authenticationHandler: (((ALTAccount, String)?) -> Void)?
|
||||
|
||||
private var _didLayoutSubviews = false
|
||||
|
||||
@IBOutlet private var emailAddressTextField: UITextField!
|
||||
@IBOutlet private var passwordTextField: UITextField!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
if !_didLayoutSubviews
|
||||
{
|
||||
self.emailAddressTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
_didLayoutSubviews = true
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
|
||||
private extension AuthenticationViewController
|
||||
{
|
||||
func update()
|
||||
{
|
||||
if let _ = self.validate()
|
||||
{
|
||||
self.navigationItem.rightBarButtonItem?.isEnabled = true
|
||||
}
|
||||
else
|
||||
{
|
||||
self.navigationItem.rightBarButtonItem?.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
func validate() -> (String, String)?
|
||||
{
|
||||
guard
|
||||
let emailAddress = self.emailAddressTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !emailAddress.isEmpty,
|
||||
let password = self.passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !password.isEmpty
|
||||
else { return nil }
|
||||
|
||||
return (emailAddress, password)
|
||||
}
|
||||
|
||||
func authenticate(emailAddress: String, password: String, completionHandler: @escaping (Result<(ALTAccount, [ALTTeam]), Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in
|
||||
switch Result(account, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let account):
|
||||
|
||||
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
|
||||
let result = Result(teams, error).map { (account, $0) }
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AuthenticationViewController
|
||||
{
|
||||
@IBAction func authenticate()
|
||||
{
|
||||
guard let (emailAddress, password) = self.validate() else { return }
|
||||
|
||||
self.emailAddressTextField.resignFirstResponder()
|
||||
self.passwordTextField.resignFirstResponder()
|
||||
|
||||
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = true
|
||||
|
||||
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in
|
||||
do
|
||||
{
|
||||
let account = try Result(account, error).get()
|
||||
self.authenticationHandler?((account, password))
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let toastView = RSTToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
|
||||
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func cancel()
|
||||
{
|
||||
self.authenticationHandler?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthenticationViewController: UITextFieldDelegate
|
||||
{
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool
|
||||
{
|
||||
switch textField
|
||||
{
|
||||
case self.emailAddressTextField: self.passwordTextField.becomeFirstResponder()
|
||||
case self.passwordTextField: self.authenticate()
|
||||
default: break
|
||||
}
|
||||
|
||||
self.update()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
156
AltStore/Authentication/ReplaceCertificateViewController.swift
Normal file
156
AltStore/Authentication/ReplaceCertificateViewController.swift
Normal file
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// ReplaceCertificateViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/5/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
extension ReplaceCertificateViewController
|
||||
{
|
||||
private enum Error: LocalizedError
|
||||
{
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
||||
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ReplaceCertificateViewController: UITableViewController
|
||||
{
|
||||
var replacementHandler: ((ALTCertificate?) -> Void)?
|
||||
|
||||
var team: ALTTeam!
|
||||
|
||||
var certificates: [ALTCertificate] {
|
||||
get {
|
||||
return self.dataSource.items
|
||||
}
|
||||
set {
|
||||
self.dataSource.items = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var selectedCertificate: ALTCertificate? {
|
||||
didSet {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private extension ReplaceCertificateViewController
|
||||
{
|
||||
func makeDataSource() -> RSTArrayTableViewDataSource<ALTCertificate>
|
||||
{
|
||||
let dataSource = RSTArrayTableViewDataSource<ALTCertificate>(items: [])
|
||||
dataSource.proxy = self
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, certificate, indexPath) in
|
||||
cell.textLabel?.text = certificate.name
|
||||
cell.accessoryType = (self?.selectedCertificate == certificate) ? .checkmark : .none
|
||||
}
|
||||
|
||||
let placeholderView = RSTPlaceholderView(frame: .zero)
|
||||
placeholderView.textLabel.text = NSLocalizedString("No Certificates", comment: "")
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("There are no certificates associated with this team.", comment: "")
|
||||
dataSource.placeholderView = placeholderView
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
self.navigationItem.rightBarButtonItem?.isEnabled = (self.selectedCertificate != nil)
|
||||
|
||||
if self.isViewLoaded
|
||||
{
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ReplaceCertificateViewController
|
||||
{
|
||||
@IBAction func replaceCertificate(_ sender: UIBarButtonItem)
|
||||
{
|
||||
guard let certificate = self.selectedCertificate else { return }
|
||||
|
||||
func replace()
|
||||
{
|
||||
sender.isIndicatingActivity = true
|
||||
|
||||
ALTAppleAPI.shared.revoke(certificate, for: self.team) { (success, error) in
|
||||
let result = Result(success, error).map { certificate }
|
||||
|
||||
do
|
||||
{
|
||||
let certificate = try result.get()
|
||||
self.replacementHandler?(certificate)
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let toastView = RSTToastView(text: NSLocalizedString("Error Replacing Certificate", comment: ""), detailText: error.localizedDescription)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
|
||||
sender.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let localizedTitle = String(format: NSLocalizedString("Are you sure you want to replace %@?", comment: ""), certificate.name)
|
||||
let localizedMessage = NSLocalizedString("Any AltStore apps currently installed with this certificate will need to be refreshed.", comment: "")
|
||||
let localizedReplaceActionTitle = String(format: NSLocalizedString("Replace %@", comment: ""), certificate.name)
|
||||
|
||||
let alertController = UIAlertController(title: localizedTitle, message: localizedMessage, preferredStyle: .actionSheet)
|
||||
alertController.addAction(UIAlertAction(title: localizedReplaceActionTitle, style: .destructive) { (action) in
|
||||
replace()
|
||||
})
|
||||
alertController.addAction(.cancel)
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction func cancel()
|
||||
{
|
||||
self.replacementHandler?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension ReplaceCertificateViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
|
||||
{
|
||||
return NSLocalizedString("You have reached the maximum number of development certificates. Please select a certificate to replace.", comment: "")
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
let certificate = self.dataSource.item(at: indexPath)
|
||||
self.selectedCertificate = certificate
|
||||
}
|
||||
}
|
||||
150
AltStore/Authentication/SelectTeamViewController.swift
Normal file
150
AltStore/Authentication/SelectTeamViewController.swift
Normal file
@@ -0,0 +1,150 @@
|
||||
//
|
||||
// SelectTeamViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/5/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
class SelectTeamViewController: UITableViewController
|
||||
{
|
||||
var selectionHandler: ((ALTTeam?) -> Void)?
|
||||
|
||||
var teams: [ALTTeam] {
|
||||
get {
|
||||
return self.dataSource.items
|
||||
}
|
||||
set {
|
||||
self.dataSource.items = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var selectedTeam: ALTTeam? {
|
||||
didSet {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
|
||||
private extension SelectTeamViewController
|
||||
{
|
||||
func makeDataSource() -> RSTArrayTableViewDataSource<ALTTeam>
|
||||
{
|
||||
let dataSource = RSTArrayTableViewDataSource<ALTTeam>(items: [])
|
||||
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.accessoryType = (self?.selectedTeam == team) ? .checkmark : .none
|
||||
}
|
||||
|
||||
let placeholderView = RSTPlaceholderView(frame: .zero)
|
||||
placeholderView.textLabel.text = NSLocalizedString("No Teams", comment: "")
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("You are not a member of any development teams.", comment: "")
|
||||
dataSource.placeholderView = placeholderView
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
self.navigationItem.rightBarButtonItem?.isEnabled = (self.selectedTeam != nil)
|
||||
|
||||
if self.isViewLoaded
|
||||
{
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchCertificates(for team: ALTTeam, completionHandler: @escaping (Result<[ALTCertificate], Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificate, error) in
|
||||
let result = Result(certificate, error)
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SelectTeamViewController
|
||||
{
|
||||
@IBAction func chooseTeam(_ sender: UIBarButtonItem)
|
||||
{
|
||||
guard let team = self.selectedTeam else { return }
|
||||
|
||||
func choose()
|
||||
{
|
||||
sender.isIndicatingActivity = true
|
||||
|
||||
self.selectionHandler?(team)
|
||||
}
|
||||
|
||||
if team.type == .organization
|
||||
{
|
||||
let localizedActionTitle = String(format: NSLocalizedString("Use %@?", comment: ""), team.name)
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to use an Organization team?", comment: ""),
|
||||
message: NSLocalizedString("Doing so may affect other members of this team.", comment: ""), preferredStyle: .actionSheet)
|
||||
alertController.addAction(UIAlertAction(title: localizedActionTitle, style: .destructive, handler: { (action) in
|
||||
choose()
|
||||
}))
|
||||
alertController.addAction(.cancel)
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
choose()
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func cancel()
|
||||
{
|
||||
self.selectionHandler?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension SelectTeamViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
|
||||
{
|
||||
return NSLocalizedString("Select the team you would like to use to install apps.", comment: "")
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
let team = self.dataSource.item(at: indexPath)
|
||||
self.selectedTeam = team
|
||||
}
|
||||
}
|
||||
@@ -53,4 +53,14 @@ extension Keychain
|
||||
self.keychain[data: "signingCertificatePrivateKey"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var signingCertificateIdentifier: String? {
|
||||
get {
|
||||
let identifier = try? self.keychain.get("signingCertificateIdentifier")
|
||||
return identifier
|
||||
}
|
||||
set {
|
||||
self.keychain["signingCertificateIdentifier"] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,4 @@ import Foundation
|
||||
extension UserDefaults
|
||||
{
|
||||
@NSManaged var firstLaunch: Date?
|
||||
@NSManaged var signingCertificateIdentifier: String?
|
||||
}
|
||||
|
||||
59
AltStore/Model/Account.swift
Normal file
59
AltStore/Model/Account.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// Account.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/5/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(Account)
|
||||
class Account: NSManagedObject
|
||||
{
|
||||
var localizedName: String {
|
||||
var components = PersonNameComponents()
|
||||
components.givenName = self.firstName
|
||||
components.familyName = self.lastName
|
||||
|
||||
let name = PersonNameComponentsFormatter.localizedString(from: components, style: .default)
|
||||
return name
|
||||
}
|
||||
|
||||
/* Properties */
|
||||
@NSManaged var appleID: String
|
||||
@NSManaged var identifier: String
|
||||
|
||||
@NSManaged var firstName: String
|
||||
@NSManaged var lastName: String
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var teams: Set<Team>
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(_ account: ALTAccount, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: Account.entity(), insertInto: context)
|
||||
|
||||
self.appleID = account.appleID
|
||||
self.identifier = account.identifier
|
||||
|
||||
self.firstName = account.firstName
|
||||
self.lastName = account.lastName
|
||||
}
|
||||
}
|
||||
|
||||
extension Account
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Account>
|
||||
{
|
||||
return NSFetchRequest<Account>(entityName: "Account")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.99" systemVersion="18F203" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String" syncable="YES"/>
|
||||
<attribute name="firstName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="lastName" attributeType="String" syncable="YES"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="App" representedClassName="App" syncable="YES">
|
||||
<attribute name="developerName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
|
||||
@@ -25,8 +37,21 @@
|
||||
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="installedApp" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="-63" positionY="-18" width="128" height="210"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="120"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="105"/>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="120"/>
|
||||
</elements>
|
||||
</model>
|
||||
52
AltStore/Model/Team.swift
Normal file
52
AltStore/Model/Team.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Team.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(Team)
|
||||
class Team: NSManagedObject
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var name: String
|
||||
@NSManaged var identifier: String
|
||||
@NSManaged var type: ALTTeamType
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged private(set) var account: Account!
|
||||
|
||||
var altTeam: ALTTeam?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(_ team: ALTTeam, account: Account, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: Team.entity(), insertInto: context)
|
||||
|
||||
self.altTeam = team
|
||||
|
||||
self.name = team.name
|
||||
self.identifier = team.identifier
|
||||
self.type = team.type
|
||||
|
||||
self.account = account
|
||||
}
|
||||
}
|
||||
|
||||
extension Team
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Team>
|
||||
{
|
||||
return NSFetchRequest<Team>(entityName: "Team")
|
||||
}
|
||||
}
|
||||
2
Dependencies/AltSign
vendored
2
Dependencies/AltSign
vendored
Submodule Dependencies/AltSign updated: 453d54d552...b8d7cf5e93
Reference in New Issue
Block a user