mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
* Change error from Swift.Error to NSError
* Adds ResultOperation.localizedFailure
* Finish Riley's monster commit
3b38d725d7
May the Gods have mercy on my soul.
* Fix format strings I broke
* Include "Enable JIT" errors in Error Log
* Fix minimuxer status checking
* [skip ci] Update the no wifi message to include VPN
* Opens Error Log when tapping ToastView
* Fixes Error Log context menu covering cell content
* Fixes Error Log context menu appearing while scrolling
* Fixes incorrect Search FAQ URL
* Fix Error Log showing UIAlertController on iOS 14+
* Fix Error Log not showing UIAlertController on iOS <=13
* Fix wrong color in AuthenticationViewController
* Fix typo
* Fixes logging non-AltServerErrors as AltServerError.underlyingError
* Limits quitting other AltStore/SideStore processes to database migrations
* Skips logging cancelled errors
* Replaces StoreApp.latestVersion with latestSupportedVersion + latestAvailableVersion
We now store the latest supported version as a relationship on StoreApp, rather than the latest available version. This allows us to reference the latest supported version in predicates and sort descriptors.
However, we kept the underlying Core Data property name the same to avoid extra migration.
* Conforms OperatingSystemVersion to Comparable
* Parses AppVersion.minOSVersion/maxOSVersion from source JSON
* Supports non-NSManagedObjects for @Managed properties
This allows us to use @Managed with properties that may or may not be NSManagedObjects at runtime (e.g. protocols). If they are, Managed will keep strong reference to context like before.
* Supports optional @Managed properties
* Conforms AppVersion to AppProtocol
* Verifies min/max OS version before downloading app + asks user to download older app version if necessary
* Improves error message when file does not exist at AppVersion.downloadURL
* Removes unnecessary StoreApp convenience properties
* Removes unnecessary StoreApp convenience properties as well as fix other issues
* Remove Settings bundle, add SwiftUI view instead
Fix refresh all shortcut intent
* Update AuthenticationOperation.swift
Signed-off-by: June Park <rjp2030@outlook.com>
* Fix build issues given by develop
* Add availability check to fix CI build(?)
* If it's gonna be that way...
---------
Signed-off-by: June Park <rjp2030@outlook.com>
Co-authored-by: nythepegasus <nythepegasus84@gmail.com>
Co-authored-by: Riley Testut <riley@rileytestut.com>
Co-authored-by: ny <me@nythepegas.us>
701 lines
30 KiB
Swift
701 lines
30 KiB
Swift
//
|
|
// AuthenticationOperation.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 6/5/19.
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import Roxas
|
|
import Network
|
|
|
|
import AltStoreCore
|
|
import AltSign
|
|
import minimuxer
|
|
|
|
typealias AuthenticationError = AuthenticationErrorCode.Error
|
|
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
|
|
{
|
|
case noTeam
|
|
case noCertificate
|
|
case teamSelectorError
|
|
|
|
case missingPrivateKey
|
|
case missingCertificate
|
|
|
|
var errorFailureReason: String {
|
|
switch self {
|
|
case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams?", comment: "")
|
|
case .noCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
|
|
case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", 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: "")
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc(AuthenticationOperation)
|
|
final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
|
|
{
|
|
let context: AuthenticatedOperationContext
|
|
|
|
private weak var presentingViewController: UIViewController?
|
|
|
|
private lazy var navigationController: UINavigationController = {
|
|
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
|
|
if #available(iOS 13.0, *)
|
|
{
|
|
navigationController.isModalInPresentation = true
|
|
}
|
|
return navigationController
|
|
}()
|
|
|
|
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
|
|
|
|
private var appleIDEmailAddress: String?
|
|
private var appleIDPassword: String?
|
|
private var shouldShowInstructions = false
|
|
|
|
private let operationQueue = OperationQueue()
|
|
|
|
private var submitCodeAction: UIAlertAction?
|
|
|
|
init(context: AuthenticatedOperationContext, presentingViewController: UIViewController?)
|
|
{
|
|
self.context = context
|
|
self.presentingViewController = presentingViewController
|
|
|
|
super.init()
|
|
|
|
self.context.authenticationOperation = self
|
|
self.operationQueue.name = "com.altstore.AuthenticationOperation"
|
|
self.progress.totalUnitCount = 4
|
|
}
|
|
|
|
override func main()
|
|
{
|
|
super.main()
|
|
|
|
if let error = self.context.error
|
|
{
|
|
self.finish(.failure(error))
|
|
return
|
|
}
|
|
|
|
// Sign In
|
|
self.signIn() { (result) in
|
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
|
|
|
switch result
|
|
{
|
|
case .failure(let error): self.finish(.failure(error))
|
|
case .success((let account, let session)):
|
|
self.context.session = session
|
|
self.progress.completedUnitCount += 1
|
|
|
|
// Fetch Team
|
|
self.fetchTeam(for: account, session: session) { (result) in
|
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
|
|
|
switch result
|
|
{
|
|
case .failure(let error): self.finish(.failure(error))
|
|
case .success(let team):
|
|
self.context.team = team
|
|
self.progress.completedUnitCount += 1
|
|
|
|
// Fetch Certificate
|
|
self.fetchCertificate(for: team, session: session) { (result) in
|
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
|
|
|
switch result
|
|
{
|
|
case .failure(let error): self.finish(.failure(error))
|
|
case .success(let certificate):
|
|
self.context.certificate = certificate
|
|
self.progress.completedUnitCount += 1
|
|
|
|
// Register Device
|
|
self.registerCurrentDevice(for: team, session: session) { (result) in
|
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
|
|
|
switch result
|
|
{
|
|
case .failure(let error): self.finish(.failure(error))
|
|
case .success:
|
|
self.progress.completedUnitCount += 1
|
|
|
|
// Save account/team to disk.
|
|
self.save(team) { (result) in
|
|
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
|
|
|
switch result
|
|
{
|
|
case .failure(let error): self.finish(.failure(error))
|
|
case .success:
|
|
// Must cache App IDs _after_ saving account/team to disk.
|
|
self.cacheAppIDs(team: team, session: session) { (result) in
|
|
let result = result.map { _ in (team, certificate, session) }
|
|
self.finish(result)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
{
|
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
|
context.performAndWait {
|
|
do
|
|
{
|
|
let account: Account
|
|
let team: Team
|
|
|
|
if let tempAccount = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context)
|
|
{
|
|
account = tempAccount
|
|
}
|
|
else
|
|
{
|
|
account = Account(altTeam.account, context: context)
|
|
}
|
|
|
|
if let tempTeam = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
|
|
{
|
|
team = tempTeam
|
|
}
|
|
else
|
|
{
|
|
team = Team(altTeam, account: account, context: context)
|
|
}
|
|
|
|
account.update(account: altTeam.account)
|
|
|
|
if let providedEmailAddress = self.appleIDEmailAddress
|
|
{
|
|
// Save the user's provided email address instead of the one associated with their account (which may be outdated).
|
|
account.appleID = providedEmailAddress
|
|
}
|
|
|
|
team.update(team: altTeam)
|
|
|
|
try context.save()
|
|
|
|
completionHandler(.success(()))
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
override func finish(_ result: Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>)
|
|
{
|
|
guard !self.isFinished else { return }
|
|
|
|
print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
|
|
|
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
|
context.perform {
|
|
do
|
|
{
|
|
let (altTeam, altCertificate, session) = try result.get()
|
|
|
|
guard
|
|
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
|
|
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
|
|
else { throw AuthenticationError(.noTeam) }
|
|
|
|
// Account
|
|
account.isActiveAccount = true
|
|
|
|
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
|
|
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
|
|
|
|
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
|
|
for account in otherAccounts
|
|
{
|
|
account.isActiveAccount = false
|
|
}
|
|
|
|
// Team
|
|
team.isActiveTeam = true
|
|
|
|
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
|
|
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
|
|
|
|
let otherTeams = try context.fetch(otherTeamsFetchRequest)
|
|
for team in otherTeams
|
|
{
|
|
team.isActiveTeam = false
|
|
}
|
|
|
|
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
|
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
|
{
|
|
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
|
|
}
|
|
else
|
|
{
|
|
UserDefaults.standard.activeAppsLimit = nil
|
|
}
|
|
|
|
// Save
|
|
try context.save()
|
|
|
|
// Update keychain
|
|
Keychain.shared.appleIDEmailAddress = self.appleIDEmailAddress ?? altTeam.account.appleID // Prefer the user's provided email address over the one associated with their account (which may be outdated).
|
|
Keychain.shared.appleIDPassword = self.appleIDPassword
|
|
|
|
Keychain.shared.signingCertificate = altCertificate.p12Data()
|
|
Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier
|
|
|
|
self.showInstructionsIfNecessary() { (didShowInstructions) in
|
|
|
|
let signer = ALTSigner(team: altTeam, certificate: altCertificate)
|
|
// Refresh screen must go last since a successful refresh will cause the app to quit.
|
|
self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in
|
|
super.finish(result)
|
|
|
|
DispatchQueue.main.async {
|
|
self.navigationController.dismiss(animated: true, completion: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
super.finish(result)
|
|
|
|
DispatchQueue.main.async {
|
|
self.navigationController.dismiss(animated: true, completion: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension AuthenticationOperation
|
|
{
|
|
func present(_ viewController: UIViewController) -> Bool
|
|
{
|
|
guard let presentingViewController = self.presentingViewController else { return false }
|
|
|
|
self.navigationController.view.tintColor = .white
|
|
|
|
if self.navigationController.viewControllers.isEmpty
|
|
{
|
|
guard presentingViewController.presentedViewController == nil else { return false }
|
|
|
|
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, ALTAppleAPISession), Swift.Error>) -> Void)
|
|
{
|
|
func authenticate()
|
|
{
|
|
DispatchQueue.main.async {
|
|
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
|
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
|
|
self.authenticate(appleID: appleID, password: password) { (result) in
|
|
completionHandler(result)
|
|
}
|
|
}
|
|
authenticationViewController.completionHandler = { (result) in
|
|
if let (account, session, password) = result
|
|
{
|
|
// We presented the Auth UI and the user signed in.
|
|
// In this case, we'll assume we should show the instructions again.
|
|
self.shouldShowInstructions = true
|
|
|
|
self.appleIDPassword = password
|
|
completionHandler(.success((account, session)))
|
|
}
|
|
else
|
|
{
|
|
completionHandler(.failure(OperationError.cancelled))
|
|
}
|
|
}
|
|
|
|
if !self.present(authenticationViewController)
|
|
{
|
|
completionHandler(.failure(OperationError.notAuthenticated))
|
|
}
|
|
}
|
|
}
|
|
|
|
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
|
|
{
|
|
self.authenticate(appleID: appleID, password: password) { (result) in
|
|
switch result
|
|
{
|
|
case .success((let account, let session)):
|
|
self.appleIDPassword = password
|
|
completionHandler(.success((account, session)))
|
|
|
|
case .failure(ALTAppleAPIError.incorrectCredentials), .failure(ALTAppleAPIError.appSpecificPasswordRequired):
|
|
authenticate()
|
|
|
|
case .failure(let error):
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
authenticate()
|
|
}
|
|
}
|
|
|
|
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
|
|
{
|
|
self.appleIDEmailAddress = appleID
|
|
|
|
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
|
|
fetchAnisetteDataOperation.resultHandler = { (result) in
|
|
switch result
|
|
{
|
|
case .failure(let error): completionHandler(.failure(error))
|
|
case .success(let anisetteData):
|
|
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
|
|
|
|
if let presentingViewController = self.presentingViewController
|
|
{
|
|
verificationHandler = { (completionHandler) in
|
|
DispatchQueue.main.async {
|
|
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
|
|
alertController.addTextField { (textField) in
|
|
textField.autocorrectionType = .no
|
|
textField.autocapitalizationType = .none
|
|
textField.keyboardType = .numberPad
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
|
|
}
|
|
|
|
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
|
|
let textField = alertController.textFields?.first
|
|
|
|
let code = textField?.text ?? ""
|
|
completionHandler(code)
|
|
}
|
|
submitAction.isEnabled = false
|
|
alertController.addAction(submitAction)
|
|
self.submitCodeAction = submitAction
|
|
|
|
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
|
|
completionHandler(nil)
|
|
})
|
|
|
|
if self.navigationController.presentingViewController != nil
|
|
{
|
|
self.navigationController.present(alertController, animated: true, completion: nil)
|
|
}
|
|
else
|
|
{
|
|
presentingViewController.present(alertController, animated: true, completion: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No view controller to present security code alert, so don't provide verificationHandler.
|
|
verificationHandler = nil
|
|
}
|
|
|
|
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
|
|
verificationHandler: verificationHandler) { (account, session, error) in
|
|
if let account = account, let session = session
|
|
{
|
|
completionHandler(.success((account, session)))
|
|
}
|
|
else
|
|
{
|
|
completionHandler(.failure(error ?? OperationError.unknown()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.operationQueue.addOperation(fetchAnisetteDataOperation)
|
|
}
|
|
|
|
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
|
|
{
|
|
func selectTeam(from teams: [ALTTeam])
|
|
{
|
|
if teams.count <= 1 {
|
|
if let team = teams.first {
|
|
return completionHandler(.success(team))
|
|
} else {
|
|
return completionHandler(.failure(AuthenticationError(.noTeam)))
|
|
}
|
|
} else {
|
|
DispatchQueue.main.async {
|
|
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
|
|
|
|
selectTeamViewController.teams = teams
|
|
selectTeamViewController.completionHandler = completionHandler
|
|
|
|
if !self.present(selectTeamViewController)
|
|
{
|
|
return completionHandler(.failure(AuthenticationError(.noTeam)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
|
|
switch Result(teams, error)
|
|
{
|
|
case .failure(let error): completionHandler(.failure(error))
|
|
case .success(let teams):
|
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
|
if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier })
|
|
{
|
|
completionHandler(.success(altTeam))
|
|
}
|
|
else
|
|
{
|
|
selectTeam(from: teams)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
|
|
{
|
|
func requestCertificate()
|
|
{
|
|
let machineName = "SideStore - " + UIDevice.current.name
|
|
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
|
|
do
|
|
{
|
|
let certificate = try Result(certificate, error).get()
|
|
guard let privateKey = certificate.privateKey else { throw AuthenticationError(.missingPrivateKey) }
|
|
|
|
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
|
do
|
|
{
|
|
let certificates = try Result(certificates, error).get()
|
|
|
|
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
|
|
throw AuthenticationError(.missingCertificate)
|
|
}
|
|
|
|
certificate.privateKey = privateKey
|
|
completionHandler(.success(certificate))
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
func replaceCertificate(from certificates: [ALTCertificate])
|
|
{
|
|
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "SideStore") == true }) ?? certificates.first else { return completionHandler(.failure(OperationError.notAuthenticated)) }
|
|
|
|
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
|
if let error = error, !success
|
|
{
|
|
completionHandler(.failure(error))
|
|
}
|
|
else
|
|
{
|
|
requestCertificate()
|
|
}
|
|
}
|
|
}
|
|
|
|
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
|
do
|
|
{
|
|
let certificates = try Result(certificates, error).get()
|
|
|
|
if
|
|
let data = Keychain.shared.signingCertificate,
|
|
let localCertificate = ALTCertificate(p12Data: data, password: nil),
|
|
let certificate = certificates.first(where: { $0.serialNumber == localCertificate.serialNumber })
|
|
{
|
|
// We have a certificate stored in the keychain and it hasn't been revoked.
|
|
localCertificate.machineIdentifier = certificate.machineIdentifier
|
|
completionHandler(.success(localCertificate))
|
|
}
|
|
else if
|
|
let serialNumber = Keychain.shared.signingCertificateSerialNumber,
|
|
let privateKey = Keychain.shared.signingCertificatePrivateKey,
|
|
let certificate = certificates.first(where: { $0.serialNumber == serialNumber })
|
|
{
|
|
// LEGACY
|
|
// We have the private key for one of the certificates, so add it to certificate and use it.
|
|
certificate.privateKey = privateKey
|
|
completionHandler(.success(certificate))
|
|
}
|
|
else if
|
|
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String,
|
|
let certificate = certificates.first(where: { $0.serialNumber == serialNumber }),
|
|
let machineIdentifier = certificate.machineIdentifier,
|
|
FileManager.default.fileExists(atPath: Bundle.main.certificateURL.path),
|
|
let data = try? Data(contentsOf: Bundle.main.certificateURL),
|
|
let localCertificate = ALTCertificate(p12Data: data, password: machineIdentifier)
|
|
{
|
|
// We have an embedded certificate that hasn't been revoked.
|
|
localCertificate.machineIdentifier = machineIdentifier
|
|
completionHandler(.success(localCertificate))
|
|
}
|
|
else if certificates.isEmpty
|
|
{
|
|
// No certificates, so request a new one.
|
|
requestCertificate()
|
|
}
|
|
else
|
|
{
|
|
// We don't have private keys for any of the certificates,
|
|
// so we need to revoke one and create a new one.
|
|
replaceCertificate(from: certificates)
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
|
{
|
|
guard let udid = fetch_udid()?.toString() else {
|
|
return completionHandler(.failure(OperationError.unknownUDID))
|
|
}
|
|
|
|
ALTAppleAPI.shared.fetchDevices(for: team, types: [.iphone, .ipad], session: session) { (devices, error) in
|
|
do
|
|
{
|
|
let devices = try Result(devices, error).get()
|
|
|
|
if let device = devices.first(where: { $0.identifier == udid })
|
|
{
|
|
completionHandler(.success(device))
|
|
}
|
|
else
|
|
{
|
|
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, type: .iphone, team: team, session: session) { (device, error) in
|
|
completionHandler(Result(device, error))
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
{
|
|
let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
|
|
fetchAppIDsOperation.resultHandler = { (result) in
|
|
do
|
|
{
|
|
let (_, context) = try result.get()
|
|
try context.save()
|
|
|
|
completionHandler(.success(()))
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
|
|
self.operationQueue.addOperation(fetchAppIDsOperation)
|
|
}
|
|
|
|
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
|
|
{
|
|
guard self.shouldShowInstructions else { return completionHandler(false) }
|
|
|
|
DispatchQueue.main.async {
|
|
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
|
|
instructionsViewController.showsBottomButton = true
|
|
instructionsViewController.completionHandler = {
|
|
completionHandler(true)
|
|
}
|
|
|
|
if !self.present(instructionsViewController)
|
|
{
|
|
completionHandler(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
|
|
{
|
|
guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) }
|
|
|
|
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
|
|
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
|
|
|
|
#if DEBUG
|
|
completionHandler(false)
|
|
#else
|
|
DispatchQueue.main.async {
|
|
let context = AuthenticatedOperationContext(context: self.context)
|
|
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
|
|
|
|
let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController
|
|
refreshViewController.context = context
|
|
refreshViewController.completionHandler = { _ in
|
|
completionHandler(true)
|
|
}
|
|
|
|
if !self.present(refreshViewController)
|
|
{
|
|
completionHandler(false)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
extension AuthenticationOperation
|
|
{
|
|
@objc func textFieldTextDidChange(_ notification: Notification)
|
|
{
|
|
guard let textField = notification.object as? UITextField else { return }
|
|
|
|
self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6
|
|
}
|
|
}
|