[AltStore] Revises authentication flow with better UI

This commit is contained in:
Riley Testut
2019-06-05 18:05:21 -07:00
parent 5c4613fd20
commit 0895e4238f
15 changed files with 1323 additions and 258 deletions

View File

@@ -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()
}
}
}

View File

@@ -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