mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
merge AltStore 1.6.3, add dynamic anisette lists, merge SideJITServer integration
* 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>
This commit is contained in:
@@ -14,7 +14,8 @@ import AltStoreCore
|
||||
import AltSign
|
||||
import minimuxer
|
||||
|
||||
enum AuthenticationError: LocalizedError
|
||||
typealias AuthenticationError = AuthenticationErrorCode.Error
|
||||
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
|
||||
{
|
||||
case noTeam
|
||||
case noCertificate
|
||||
@@ -23,11 +24,11 @@ enum AuthenticationError: LocalizedError
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
|
||||
var errorDescription: String? {
|
||||
var errorFailureReason: String {
|
||||
switch self {
|
||||
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
||||
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 .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: "")
|
||||
}
|
||||
@@ -213,8 +214,8 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
|
||||
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 }
|
||||
|
||||
else { throw AuthenticationError(.noTeam) }
|
||||
|
||||
// Account
|
||||
account.isActiveAccount = true
|
||||
|
||||
@@ -432,7 +433,7 @@ private extension AuthenticationOperation
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(error ?? OperationError.unknown))
|
||||
completionHandler(.failure(error ?? OperationError.unknown()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,7 +450,7 @@ private extension AuthenticationOperation
|
||||
if let team = teams.first {
|
||||
return completionHandler(.success(team))
|
||||
} else {
|
||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||
return completionHandler(.failure(AuthenticationError(.noTeam)))
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
@@ -460,7 +461,7 @@ private extension AuthenticationOperation
|
||||
|
||||
if !self.present(selectTeamViewController)
|
||||
{
|
||||
return completionHandler(.failure(AuthenticationError.noTeam))
|
||||
return completionHandler(.failure(AuthenticationError(.noTeam)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -489,20 +490,20 @@ private extension AuthenticationOperation
|
||||
{
|
||||
func requestCertificate()
|
||||
{
|
||||
let machineName = "AltStore - " + UIDevice.current.name
|
||||
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 }
|
||||
|
||||
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
|
||||
throw AuthenticationError(.missingCertificate)
|
||||
}
|
||||
|
||||
certificate.privateKey = privateKey
|
||||
@@ -523,7 +524,7 @@ private extension AuthenticationOperation
|
||||
|
||||
func replaceCertificate(from certificates: [ALTCertificate])
|
||||
{
|
||||
guard let certificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true }) ?? certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
|
||||
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
|
||||
|
||||
@@ -13,11 +13,12 @@ import AltStoreCore
|
||||
import EmotionalDamage
|
||||
import minimuxer
|
||||
|
||||
enum RefreshError: LocalizedError
|
||||
typealias RefreshError = RefreshErrorCode.Error
|
||||
enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable
|
||||
{
|
||||
case noInstalledApps
|
||||
|
||||
var errorDescription: String? {
|
||||
var errorFailureReason: String {
|
||||
switch self
|
||||
{
|
||||
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
|
||||
@@ -94,7 +95,7 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
|
||||
super.main()
|
||||
|
||||
guard !self.installedApps.isEmpty else {
|
||||
self.finish(.failure(RefreshError.noInstalledApps))
|
||||
self.finish(.failure(RefreshError(.noInstalledApps)))
|
||||
return
|
||||
}
|
||||
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
||||
@@ -202,7 +203,7 @@ private extension BackgroundRefreshAppsOperation
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
var shouldPresentAlert = false
|
||||
var shouldPresentAlert = true
|
||||
|
||||
do
|
||||
{
|
||||
@@ -218,20 +219,18 @@ private extension BackgroundRefreshAppsOperation
|
||||
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
||||
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
||||
}
|
||||
catch RefreshError.noInstalledApps
|
||||
catch ~OperationError.Code.noWiFi, ~RefreshErrorCode.noInstalledApps
|
||||
{
|
||||
shouldPresentAlert = false
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to refresh apps in background.", error)
|
||||
|
||||
|
||||
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
||||
content.body = error.localizedDescription
|
||||
|
||||
shouldPresentAlert = false
|
||||
}
|
||||
|
||||
|
||||
if shouldPresentAlert
|
||||
{
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
|
||||
|
||||
@@ -55,12 +55,15 @@ class BackupAppOperation: ResultOperation<Void>
|
||||
let appName = installedApp.name
|
||||
self.appName = appName
|
||||
|
||||
let altstoreOpenURL = URL(string: "sidestore://")!
|
||||
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else {
|
||||
throw OperationError.appNotFound(name: appName)
|
||||
}
|
||||
let altstoreOpenURL = altstoreApp.openAppURL
|
||||
|
||||
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
|
||||
returnURLComponents?.host = "appBackupResponse"
|
||||
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
|
||||
|
||||
|
||||
var openURLComponents = URLComponents()
|
||||
openURLComponents.scheme = installedApp.openAppURL.scheme
|
||||
openURLComponents.host = self.action.rawValue
|
||||
|
||||
@@ -12,66 +12,108 @@ import Roxas
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
private extension DownloadAppOperation
|
||||
{
|
||||
struct DependencyError: ALTLocalizedError
|
||||
{
|
||||
let dependency: Dependency
|
||||
let error: Error
|
||||
|
||||
var failure: String? {
|
||||
return String(format: NSLocalizedString("Could not download “%@”.", comment: ""), self.dependency.preferredFilename)
|
||||
}
|
||||
|
||||
var underlyingError: Error? {
|
||||
return self.error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(DownloadAppOperation)
|
||||
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
let app: AppProtocol
|
||||
let context: AppOperationContext
|
||||
|
||||
|
||||
private let appName: String
|
||||
private let bundleIdentifier: String
|
||||
private var sourceURL: URL?
|
||||
private let destinationURL: URL
|
||||
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
|
||||
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
|
||||
{
|
||||
self.app = app
|
||||
self.context = context
|
||||
|
||||
|
||||
self.appName = app.name
|
||||
self.bundleIdentifier = app.bundleIdentifier
|
||||
self.sourceURL = app.url
|
||||
self.destinationURL = destinationURL
|
||||
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
// App = 3, Dependencies = 1
|
||||
self.progress.totalUnitCount = 4
|
||||
}
|
||||
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
print("Downloading App:", self.bundleIdentifier)
|
||||
|
||||
guard let sourceURL = self.sourceURL else { return self.finish(.failure(OperationError.appNotFound)) }
|
||||
|
||||
self.downloadApp(from: sourceURL) { result in
|
||||
|
||||
self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
|
||||
|
||||
guard let storeApp = self.app as? StoreApp else { return self.download(self.app) }
|
||||
storeApp.managedObjectContext?.perform {
|
||||
do {
|
||||
let latestVersion = try self.verify(storeApp)
|
||||
self.download(latestVersion)
|
||||
} catch let error as VerificationError where error.code == .iOSVersionNotSupported {
|
||||
guard let presentingViewController = self.context.presentingViewController,
|
||||
let latestSupportedVersion = storeApp.latestSupportedVersion,
|
||||
case let version = latestSupportedVersion.version,
|
||||
version != storeApp.installedApp?.version else {
|
||||
return self.finish(.failure(error))
|
||||
}
|
||||
let title = NSLocalizedString("Unsupported iOS Version", comment: "")
|
||||
let message = error.localizedDescription + "\n\n" + NSLocalizedString("Would you like to download the last version compatible with this device instead?", comment: "")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("Download %@ %@", comment: ""), self.appName, version), style: .default) { _ in
|
||||
self.download(latestSupportedVersion)
|
||||
})
|
||||
presentingViewController.present(alertController, animated: true)
|
||||
}
|
||||
} catch {
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<ALTApplication, any Error>) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
||||
} catch {
|
||||
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
|
||||
}
|
||||
super.finish(result)
|
||||
}
|
||||
}
|
||||
|
||||
private extension DownloadAppOperation {
|
||||
func verify(_ storeApp: StoreApp) throws -> AppVersion {
|
||||
guard let version = storeApp.latestAvailableVersion else {
|
||||
let failureReason = String(format: NSLocalizedString("The latest version of %@ could not be determined.", comment: ""), self.appName)
|
||||
throw OperationError.unknown(failureReason: failureReason)
|
||||
}
|
||||
if let minOSVersion = version.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) {
|
||||
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: minOSVersion)
|
||||
} else if let maxOSVersion = version.maxOSVersion, ProcessInfo.processInfo.operatingSystemVersion > maxOSVersion {
|
||||
throw VerificationError.iOSVersionNotSupported(app: storeApp, requiredOSVersion: maxOSVersion)
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
func download(@Managed _ app: AppProtocol) {
|
||||
guard let sourceURL = $app.url else { return self.finish(.failure(OperationError.appNotFound(name: self.appName))) }
|
||||
|
||||
self.downloadIPA(from: sourceURL) { result in
|
||||
do
|
||||
{
|
||||
let application = try result.get()
|
||||
@@ -112,24 +154,7 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<ALTApplication, Error>)
|
||||
{
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
|
||||
}
|
||||
|
||||
super.finish(result)
|
||||
}
|
||||
}
|
||||
|
||||
private extension DownloadAppOperation
|
||||
{
|
||||
func downloadApp(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||
func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||
{
|
||||
func finishOperation(_ result: Result<URL, Error>)
|
||||
{
|
||||
@@ -138,8 +163,8 @@ private extension DownloadAppOperation
|
||||
let fileURL = try result.get()
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
|
||||
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
|
||||
|
||||
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let appBundleURL: URL
|
||||
@@ -178,6 +203,9 @@ private extension DownloadAppOperation
|
||||
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
if let response = response as? HTTPURLResponse {
|
||||
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: sourceURL]) }
|
||||
}
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
finishOperation(.success(fileURL))
|
||||
|
||||
@@ -252,7 +280,7 @@ private extension DownloadAppOperation
|
||||
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
|
||||
|
||||
var dependencyURLs = Set<URL>()
|
||||
var dependencyError: DependencyError?
|
||||
var dependencyError: Error?
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
|
||||
@@ -285,7 +313,7 @@ private extension DownloadAppOperation
|
||||
}
|
||||
catch let error as DecodingError
|
||||
{
|
||||
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not download dependencies for %@.", comment: ""), application.name))
|
||||
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not determine dependencies for %@.", comment: ""), application.name))
|
||||
completionHandler(.failure(nsError))
|
||||
}
|
||||
catch
|
||||
@@ -294,7 +322,7 @@ private extension DownloadAppOperation
|
||||
}
|
||||
}
|
||||
|
||||
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, DependencyError>) -> Void)
|
||||
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
|
||||
do
|
||||
@@ -315,9 +343,10 @@ private extension DownloadAppOperation
|
||||
|
||||
completionHandler(.success(destinationURL))
|
||||
}
|
||||
catch
|
||||
catch let error as NSError
|
||||
{
|
||||
completionHandler(.failure(DependencyError(dependency: dependency, error: error)))
|
||||
let localizedFailure = String(format: NSLocalizedString("The dependency '%@' could not be downloaded.", comment: ""), dependency.preferredFilename)
|
||||
completionHandler(.failure(error.withLocalizedFailure(localizedFailure)))
|
||||
}
|
||||
}
|
||||
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
||||
|
||||
@@ -61,12 +61,10 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case .invalidURL:
|
||||
self.finish(.failure(OperationError.unabletoconnectSideJIT))
|
||||
case .errorConnecting:
|
||||
self.finish(.failure(OperationError.unabletoconnectSideJIT))
|
||||
case .invalidURL, .errorConnecting:
|
||||
self.finish(.failure(OperationError.unableToConnectSideJIT))
|
||||
case .deviceNotFound:
|
||||
self.finish(.failure(OperationError.unabletoconSideJITDevice))
|
||||
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
|
||||
case .other(let message):
|
||||
if let startRange = message.range(of: "<p>"),
|
||||
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
|
||||
|
||||
@@ -45,7 +45,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
return
|
||||
}
|
||||
|
||||
self.url = AnisetteManager.currentURL
|
||||
self.url = URL(string: UserDefaults.standard.menuAnisetteURL)
|
||||
print("Anisette URL: \(self.url!.absoluteString)")
|
||||
|
||||
if let identifier = Keychain.shared.identifier,
|
||||
@@ -408,6 +408,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
|
||||
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
|
||||
fetchClientInfo {
|
||||
print("Fetching anisette V3")
|
||||
let url = UserDefaults.standard.menuAnisetteURL
|
||||
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! JSONSerialization.data(withJSONObject: [
|
||||
|
||||
@@ -45,8 +45,8 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
|
||||
let session = self.context.session
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) }
|
||||
|
||||
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
|
||||
|
||||
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
|
||||
|
||||
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
|
||||
@@ -260,7 +260,7 @@ extension FetchProvisioningProfilesOperation
|
||||
{
|
||||
if let expirationDate = sortedExpirationDates.first
|
||||
{
|
||||
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
||||
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,7 +286,7 @@ extension FetchProvisioningProfilesOperation
|
||||
{
|
||||
if let expirationDate = sortedExpirationDates.first
|
||||
{
|
||||
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
||||
throw OperationError.maximumAppIDLimitReached(appName: application.name, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -12,7 +12,10 @@ import Roxas
|
||||
class ResultOperation<ResultType>: Operation
|
||||
{
|
||||
var resultHandler: ((Result<ResultType, Error>) -> Void)?
|
||||
|
||||
|
||||
// Should only be set by subclasses
|
||||
var localizedFailure: String?
|
||||
|
||||
@available(*, unavailable)
|
||||
override func finish()
|
||||
{
|
||||
@@ -22,16 +25,20 @@ class ResultOperation<ResultType>: Operation
|
||||
func finish(_ result: Result<ResultType, Error>)
|
||||
{
|
||||
guard !self.isFinished else { return }
|
||||
|
||||
|
||||
var result = result
|
||||
|
||||
if self.isCancelled
|
||||
{
|
||||
self.resultHandler?(.failure(OperationError.cancelled))
|
||||
result = .failure(OperationError.cancelled)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.resultHandler?(result)
|
||||
else if case .failure(let nsError as NSError) = result, let localizedFailure, nsError.localizedFailure == nil {
|
||||
// Error doesn't have its own localizedFailure, so we give it the Operation's (if it exists)
|
||||
let error = nsError.withLocalizedFailure(localizedFailure)
|
||||
result = .failure(error)
|
||||
}
|
||||
|
||||
self.resultHandler?(result)
|
||||
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,81 +8,186 @@
|
||||
|
||||
import Foundation
|
||||
import AltSign
|
||||
import AltStoreCore
|
||||
import minimuxer
|
||||
|
||||
enum OperationError: LocalizedError
|
||||
extension OperationError
|
||||
{
|
||||
static let domain = OperationError.unknown._domain
|
||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||
typealias Error = OperationError
|
||||
|
||||
// General
|
||||
case unknown = 1000
|
||||
case unknownResult
|
||||
case cancelled
|
||||
case timedOut
|
||||
case unableToConnectSideJIT
|
||||
case unableToRespondSideJITDevice
|
||||
case wrongSideJITIP
|
||||
case SideJITIssue // (error: String)
|
||||
case refreshsidejit
|
||||
case notAuthenticated
|
||||
case appNotFound
|
||||
case unknownUDID
|
||||
case invalidApp
|
||||
case invalidParameters
|
||||
case maximumAppIDLimitReached//((application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
|
||||
case noSources
|
||||
case openAppFailed//(name: String)
|
||||
case missingAppGroup
|
||||
|
||||
// Connection
|
||||
case noWiFi = 1200
|
||||
case tooNewError
|
||||
case anisetteV1Error//(message: String)
|
||||
case provisioningError//(result: String, message: String?)
|
||||
case anisetteV3Error//(message: String)
|
||||
|
||||
case cacheClearError//(errors: [String])
|
||||
}
|
||||
|
||||
static let unknownResult: OperationError = .init(code: .unknownResult)
|
||||
static let cancelled: OperationError = .init(code: .cancelled)
|
||||
static let timedOut: OperationError = .init(code: .timedOut)
|
||||
static let unableToConnectSideJIT: OperationError = .init(code: .unableToConnectSideJIT)
|
||||
static let unableToRespondSideJITDevice: OperationError = .init(code: .unableToRespondSideJITDevice)
|
||||
static let wrongSideJITIP: OperationError = .init(code: .wrongSideJITIP)
|
||||
static let notAuthenticated: OperationError = .init(code: .notAuthenticated)
|
||||
static let unknownUDID: OperationError = .init(code: .unknownUDID)
|
||||
static let invalidApp: OperationError = .init(code: .invalidApp)
|
||||
static let invalidParameters: OperationError = .init(code: .invalidParameters)
|
||||
static let noSources: OperationError = .init(code: .noSources)
|
||||
static let missingAppGroup: OperationError = .init(code: .missingAppGroup)
|
||||
|
||||
static let noWiFi: OperationError = .init(code: .noWiFi)
|
||||
static let tooNewError: OperationError = .init(code: .tooNewError)
|
||||
static let provisioningError: OperationError = .init(code: .provisioningError)
|
||||
static let anisetteV1Error: OperationError = .init(code: .anisetteV1Error)
|
||||
static let anisetteV3Error: OperationError = .init(code: .anisetteV3Error)
|
||||
|
||||
static let cacheClearError: OperationError = .init(code: .cacheClearError)
|
||||
|
||||
static func unknown(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||
OperationError(code: .unknown, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
||||
}
|
||||
|
||||
static func appNotFound(name: String?) -> OperationError {
|
||||
OperationError(code: .appNotFound, appName: name)
|
||||
}
|
||||
|
||||
static func openAppFailed(name: String?) -> OperationError {
|
||||
OperationError(code: .openAppFailed, appName: name)
|
||||
}
|
||||
|
||||
case unknown
|
||||
case unabletoconnectSideJIT
|
||||
case unabletoconSideJITDevice
|
||||
case wrongIP
|
||||
case SideJITIssue(error: String)
|
||||
case refreshsidejit
|
||||
case unknownResult
|
||||
case cancelled
|
||||
case timedOut
|
||||
static func SideJITIssue(error: String?) -> OperationError {
|
||||
var o = OperationError(code: .SideJITIssue)
|
||||
o.errorFailure = error
|
||||
return o
|
||||
}
|
||||
|
||||
case notAuthenticated
|
||||
case appNotFound
|
||||
|
||||
case unknownUDID
|
||||
|
||||
case invalidApp
|
||||
case invalidParameters
|
||||
|
||||
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
|
||||
|
||||
case noSources
|
||||
|
||||
case openAppFailed(name: String)
|
||||
case missingAppGroup
|
||||
|
||||
case noWiFi
|
||||
case tooNewError
|
||||
case anisetteV1Error(message: String)
|
||||
case provisioningError(result: String, message: String?)
|
||||
case anisetteV3Error(message: String)
|
||||
|
||||
case cacheClearError(errors: [String])
|
||||
|
||||
var failureReason: String? {
|
||||
switch self {
|
||||
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
|
||||
static func maximumAppIDLimitReached(appName: String, requiredAppIDs: Int, availableAppIDs: Int, expirationDate: Date) -> OperationError {
|
||||
OperationError(code: .maximumAppIDLimitReached, appName: appName, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, expirationDate: expirationDate)
|
||||
}
|
||||
|
||||
static func provisioningError(result: String, message: String?) -> OperationError {
|
||||
var o = OperationError(code: .provisioningError, failureReason: result)
|
||||
o.errorTitle = message
|
||||
return o
|
||||
}
|
||||
|
||||
static func cacheClearError(errors: [String]) -> OperationError {
|
||||
OperationError(code: .cacheClearError, failureReason: errors.joined(separator: "\n"))
|
||||
}
|
||||
|
||||
static func anisetteV1Error(message: String) -> OperationError {
|
||||
OperationError(code: .anisetteV1Error, failureReason: message)
|
||||
}
|
||||
|
||||
static func anisetteV3Error(message: String) -> OperationError {
|
||||
OperationError(code: .anisetteV3Error, failureReason: message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct OperationError: ALTLocalizedError {
|
||||
|
||||
let code: Code
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
var appName: String?
|
||||
|
||||
var requiredAppIDs: Int?
|
||||
var availableAppIDs: Int?
|
||||
var expirationDate: Date?
|
||||
|
||||
var sourceFile: String?
|
||||
var sourceLine: UInt?
|
||||
|
||||
private var _failureReason: String?
|
||||
|
||||
private init(code: Code, failureReason: String? = nil,
|
||||
appName: String? = nil, requiredAppIDs: Int? = nil, availableAppIDs: Int? = nil,
|
||||
expirationDate: Date? = nil, sourceFile: String? = nil, sourceLine: UInt? = nil){
|
||||
self.code = code
|
||||
self._failureReason = failureReason
|
||||
|
||||
self.appName = appName
|
||||
self.requiredAppIDs = requiredAppIDs
|
||||
self.availableAppIDs = availableAppIDs
|
||||
self.expirationDate = expirationDate
|
||||
self.sourceFile = sourceFile
|
||||
self.sourceLine = sourceLine
|
||||
}
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code {
|
||||
case .unknown:
|
||||
var failureReason = self._failureReason ?? NSLocalizedString("An unknown error occurred.", comment: "")
|
||||
guard let sourceFile, let sourceLine else { return failureReason }
|
||||
failureReason += " (\(sourceFile) line \(sourceLine)"
|
||||
return failureReason
|
||||
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
|
||||
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
|
||||
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
|
||||
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
|
||||
case .unknownUDID: return NSLocalizedString("SideStore could not determine this device's UDID.", comment: "")
|
||||
case .invalidApp: return NSLocalizedString("The app is in an invalid format.", comment: "")
|
||||
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
|
||||
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs within a 7 day period.", comment: "")
|
||||
case .noSources: return NSLocalizedString("There are no SideStore sources.", comment: "")
|
||||
case .openAppFailed(let name): return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), name)
|
||||
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be found.", comment: "")
|
||||
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
|
||||
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi!\nSideStore will never be able to install or refresh applications without WiFi.", comment: "")
|
||||
case .unabletoconnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "")
|
||||
case .unabletoconSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "")
|
||||
case .wrongIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
|
||||
case .missingAppGroup: return NSLocalizedString("SideStore's shared app group could not be accessed.", comment: "")
|
||||
case .appNotFound:
|
||||
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
||||
return String(format: NSLocalizedString("%@ could not be found.", comment: ""), appName)
|
||||
case .openAppFailed:
|
||||
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
|
||||
return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName)
|
||||
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", comment: "")
|
||||
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "")
|
||||
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "")
|
||||
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "")
|
||||
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
|
||||
case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "")
|
||||
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it without the use of SideJITServer at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "")
|
||||
case .anisetteV1Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: ""), message)
|
||||
case .provisioningError(let result, let message): return String(format: NSLocalizedString("An error occurred when provisioning: %@%@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), result, message != nil ? (" (" + message! + ")") : "")
|
||||
case .anisetteV3Error(let message): return String(format: NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: ""), message)
|
||||
case .cacheClearError(let errors): return String(format: NSLocalizedString("An error occurred while clearing cache: %@", comment: ""), errors.joined(separator: "\n"))
|
||||
case .SideJITIssue(let errors): return NSLocalizedString("SideJITServer Error: \(errors)", comment: "")
|
||||
case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
|
||||
case .provisioningError: return NSLocalizedString("An error occurred when provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
|
||||
case .anisetteV3Error: return NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
|
||||
case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
|
||||
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
switch self
|
||||
switch self.code
|
||||
{
|
||||
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
|
||||
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "")
|
||||
case .maximumAppIDLimitReached:
|
||||
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
||||
let message: String
|
||||
|
||||
guard let appName, let requiredAppIDs, let availableAppIDs, let expirationDate else { return baseMessage }
|
||||
var message: String
|
||||
|
||||
if requiredAppIDs > 1
|
||||
{
|
||||
let availableText: String
|
||||
@@ -94,23 +199,23 @@ enum OperationError: LocalizedError
|
||||
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
|
||||
}
|
||||
|
||||
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
|
||||
message = prefixMessage + " " + baseMessage
|
||||
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), appName, NSNumber(value: requiredAppIDs), availableText)
|
||||
message = prefixMessage + " " + baseMessage + "\n\n"
|
||||
}
|
||||
else
|
||||
{
|
||||
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
|
||||
|
||||
let dateComponentsFormatter = DateComponentsFormatter()
|
||||
dateComponentsFormatter.maximumUnitCount = 1
|
||||
dateComponentsFormatter.unitsStyle = .full
|
||||
|
||||
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
|
||||
|
||||
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
|
||||
message = baseMessage + " " + remainingTimeMessage
|
||||
message = baseMessage + " "
|
||||
}
|
||||
|
||||
|
||||
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: expirationDate)
|
||||
let dateFormatter = DateComponentsFormatter()
|
||||
dateFormatter.maximumUnitCount = 1
|
||||
dateFormatter.unitsStyle = .full
|
||||
|
||||
let remainingTime = dateFormatter.string(from: dateComponents)!
|
||||
|
||||
message += String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
|
||||
|
||||
return message
|
||||
|
||||
default: return nil
|
||||
|
||||
@@ -25,22 +25,38 @@ protocol PatchAppContext
|
||||
var error: Error? { get }
|
||||
}
|
||||
|
||||
enum PatchAppError: LocalizedError
|
||||
extension PatchAppError
|
||||
{
|
||||
case unsupportedOperatingSystemVersion(OperatingSystemVersion)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .unsupportedOperatingSystemVersion(let osVersion):
|
||||
var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)"
|
||||
if osVersion.patchVersion != 0
|
||||
{
|
||||
osVersionString += ".\(osVersion.patchVersion)"
|
||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||
typealias Error = PatchAppError
|
||||
|
||||
case unsupportedOperatingSystemVersion
|
||||
}
|
||||
|
||||
static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError {
|
||||
PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion)
|
||||
}
|
||||
}
|
||||
|
||||
struct PatchAppError: ALTLocalizedError {
|
||||
let code: Code
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
var osVersion: OperatingSystemVersion?
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code {
|
||||
case .unsupportedOperatingSystemVersion:
|
||||
let osVersionString: String
|
||||
|
||||
if let osVersion = self.osVersion?.stringValue {
|
||||
osVersionString = NSLocalizedString("iOS", comment: "") + " " + osVersion
|
||||
} else {
|
||||
osVersionString = NSLocalizedString("your device's iOS version", comment: "")
|
||||
}
|
||||
|
||||
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
|
||||
return errorDescription
|
||||
return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@ private extension PatchViewController
|
||||
|
||||
do
|
||||
{
|
||||
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown }
|
||||
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown() }
|
||||
_ = try result.get()
|
||||
|
||||
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
|
||||
|
||||
@@ -38,8 +38,8 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
|
||||
if let error = self.context.error { return self.finish(.failure(error)) }
|
||||
|
||||
guard let profiles = self.context.provisioningProfiles else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) }
|
||||
|
||||
guard let app = self.context.app else { return self.finish(.failure(OperationError(.appNotFound(name: nil)))) }
|
||||
|
||||
for p in profiles {
|
||||
do {
|
||||
let bytes = p.value.data.toRustByteSlice()
|
||||
@@ -49,14 +49,13 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
|
||||
}
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
print("Sending refresh app request...")
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
|
||||
self.managedObjectContext.perform {
|
||||
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
|
||||
self.finish(.failure(OperationError.appNotFound))
|
||||
self.finish(.failure(OperationError(.appNotFound(name: app.name))))
|
||||
return
|
||||
}
|
||||
installedApp.update(provisioningProfile: p.value)
|
||||
|
||||
@@ -57,7 +57,7 @@ final class SendAppOperation: ResultOperation<()>
|
||||
}
|
||||
} else {
|
||||
print("IPA doesn't exist????")
|
||||
self.finish(.failure(OperationError.appNotFound))
|
||||
self.finish(.failure(OperationError(.appNotFound(name: resignedApp.name))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,48 +8,87 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
enum VerificationError: ALTLocalizedError
|
||||
extension VerificationError
|
||||
{
|
||||
case privateEntitlements(ALTApplication, entitlements: [String: Any])
|
||||
case mismatchedBundleIdentifiers(ALTApplication, sourceBundleID: String)
|
||||
case iOSVersionNotSupported(ALTApplication)
|
||||
enum Code: Int, ALTErrorCode, CaseIterable {
|
||||
typealias Error = VerificationError
|
||||
|
||||
case privateEntitlements
|
||||
case mismatchedBundleIdentifiers
|
||||
case iOSVersionNotSupported
|
||||
}
|
||||
|
||||
static func privateEntitlements(_ entitlements: [String: Any], app: ALTApplication) -> VerificationError {
|
||||
VerificationError(code: .privateEntitlements, app: app, entitlements: entitlements)
|
||||
}
|
||||
|
||||
static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError {
|
||||
VerificationError(code: .mismatchedBundleIdentifiers, app: app, sourceBundleID: sourceBundleID)
|
||||
}
|
||||
|
||||
static func iOSVersionNotSupported(app: AppProtocol, osVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion, requiredOSVersion: OperatingSystemVersion?) -> VerificationError {
|
||||
VerificationError(code: .iOSVersionNotSupported, app: app)
|
||||
}
|
||||
}
|
||||
|
||||
struct VerificationError: ALTLocalizedError {
|
||||
let code: Code
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
@Managed var app: AppProtocol?
|
||||
var entitlements: [String: Any]?
|
||||
var sourceBundleID: String?
|
||||
var deviceOSVersion: OperatingSystemVersion?
|
||||
var requiredOSVersion: OperatingSystemVersion?
|
||||
|
||||
var app: ALTApplication {
|
||||
switch self
|
||||
{
|
||||
case .privateEntitlements(let app, _): return app
|
||||
case .mismatchedBundleIdentifiers(let app, _): return app
|
||||
case .iOSVersionNotSupported(let app): return app
|
||||
var errorDescription: String? {
|
||||
switch self.code {
|
||||
case .iOSVersionNotSupported:
|
||||
guard let deviceOSVersion else { return nil }
|
||||
|
||||
var failureReason = self.errorFailureReason
|
||||
if self.app == nil {
|
||||
let firstLetter = failureReason.prefix(1).lowercased()
|
||||
failureReason = firstLetter + failureReason.dropFirst()
|
||||
}
|
||||
|
||||
return String(formatted: "This device is running iOS %@, but %@", deviceOSVersion.stringValue, failureReason)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var failure: String? {
|
||||
return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name)
|
||||
}
|
||||
|
||||
var failureReason: String? {
|
||||
switch self
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code
|
||||
{
|
||||
case .privateEntitlements(let app, _):
|
||||
return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name)
|
||||
|
||||
case .mismatchedBundleIdentifiers(let app, let sourceBundleID):
|
||||
return String(format: NSLocalizedString("The bundle ID “%@” does not match the one specified by the source (“%@”).", comment: ""), app.bundleIdentifier, sourceBundleID)
|
||||
|
||||
case .iOSVersionNotSupported(let app):
|
||||
let name = app.name
|
||||
|
||||
var version = "iOS \(app.minimumiOSVersion.majorVersion).\(app.minimumiOSVersion.minorVersion)"
|
||||
if app.minimumiOSVersion.patchVersion > 0
|
||||
{
|
||||
version += ".\(app.minimumiOSVersion.patchVersion)"
|
||||
case .privateEntitlements:
|
||||
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||
return String(formatted: "“%@” requires private permissions.", appName)
|
||||
|
||||
case .mismatchedBundleIdentifiers:
|
||||
if let appBundleID = self.$app.bundleIdentifier, let bundleID = self.sourceBundleID {
|
||||
return String(formatted: "The bundle ID '%@' does not match the one specified by the source ('%@').", appBundleID, bundleID)
|
||||
} else {
|
||||
return NSLocalizedString("The bundle ID does not match the one specified by the source.", comment: "")
|
||||
}
|
||||
|
||||
case .iOSVersionNotSupported:
|
||||
let appName = self.$app.name ?? NSLocalizedString("The app", comment: "")
|
||||
let deviceOSVersion = self.deviceOSVersion ?? ProcessInfo.processInfo.operatingSystemVersion
|
||||
|
||||
guard let requiredOSVersion else {
|
||||
return String(formatted: "%@ does not support iOS %@.", appName, deviceOSVersion.stringValue)
|
||||
}
|
||||
if deviceOSVersion > requiredOSVersion {
|
||||
return String(formatted: "%@ requires iOS %@ or earlier", appName, requiredOSVersion.stringValue)
|
||||
} else {
|
||||
return String(formatted: "%@ requires iOS %@ or later", appName, requiredOSVersion.stringValue)
|
||||
}
|
||||
|
||||
let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version)
|
||||
return localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,12 +119,14 @@ final class VerifyAppOperation: ResultOperation<Void>
|
||||
|
||||
guard let app = self.context.app else { throw OperationError.invalidParameters }
|
||||
|
||||
guard app.bundleIdentifier == self.context.bundleIdentifier else {
|
||||
throw VerificationError.mismatchedBundleIdentifiers(app, sourceBundleID: self.context.bundleIdentifier)
|
||||
if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) {
|
||||
guard app.bundleIdentifier == self.context.bundleIdentifier else {
|
||||
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
|
||||
}
|
||||
}
|
||||
|
||||
guard ProcessInfo.processInfo.isOperatingSystemAtLeast(app.minimumiOSVersion) else {
|
||||
throw VerificationError.iOSVersionNotSupported(app)
|
||||
throw VerificationError.iOSVersionNotSupported(app: app, requiredOSVersion: app.minimumiOSVersion)
|
||||
}
|
||||
|
||||
if #available(iOS 13.5, *)
|
||||
@@ -116,7 +157,7 @@ final class VerifyAppOperation: ResultOperation<Void>
|
||||
let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any]
|
||||
|
||||
app.hasPrivateEntitlements = true
|
||||
let error = VerificationError.privateEntitlements(app, entitlements: entitlements)
|
||||
let error = VerificationError.privateEntitlements(entitlements, app: app)
|
||||
self.process(error) { (result) in
|
||||
self.finish(result.mapError { $0 as Error })
|
||||
}
|
||||
@@ -145,9 +186,10 @@ private extension VerifyAppOperation
|
||||
guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
switch error
|
||||
switch error.code
|
||||
{
|
||||
case .privateEntitlements(_, let entitlements):
|
||||
case .privateEntitlements:
|
||||
guard let entitlements = error.entitlements else { return completion(.failure(error)) }
|
||||
let permissions = entitlements.keys.sorted().joined(separator: "\n")
|
||||
let message = String(format: NSLocalizedString("""
|
||||
You must allow access to these private permissions before continuing:
|
||||
@@ -166,8 +208,7 @@ private extension VerifyAppOperation
|
||||
}))
|
||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||
|
||||
case .mismatchedBundleIdentifiers: return completion(.failure(error))
|
||||
case .iOSVersionNotSupported: return completion(.failure(error))
|
||||
case .mismatchedBundleIdentifiers, .iOSVersionNotSupported: return completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user