mirror of
https://github.com/SideStore/SideStore.git
synced 2026-05-14 21:35:38 +02:00
[AltServer] Downloads latest supported AltStore version for device OS version
Asks user to install latest compatible version instead if latest AltStore version does not support their device’s OS version.
This commit is contained in:
@@ -255,7 +255,7 @@ private extension AppDelegate
|
|||||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
UNUserNotificationCenter.current().add(request)
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
|
||||||
case .failure(~OperationErrorCode.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
case .failure(OperationError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
||||||
// Ignore
|
// Ignore
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -10,29 +10,105 @@ import Cocoa
|
|||||||
import UserNotifications
|
import UserNotifications
|
||||||
import ObjectiveC
|
import ObjectiveC
|
||||||
|
|
||||||
private let appGroupsSemaphore = DispatchSemaphore(value: 1)
|
#if STAGING
|
||||||
|
let altstoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/apps-staging.json")!
|
||||||
|
#else
|
||||||
|
let altstoreSourceURL = URL(string: "https://apps.altstore.io")!
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if BETA
|
||||||
|
let altstoreBundleID = "com.rileytestut.AltStore.Beta"
|
||||||
|
#else
|
||||||
|
let altstoreBundleID = "com.rileytestut.AltStore"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private let appGroupsSemaphore = DispatchSemaphore(value: 1)
|
||||||
private let developerDiskManager = DeveloperDiskManager()
|
private let developerDiskManager = DeveloperDiskManager()
|
||||||
|
|
||||||
typealias OperationError = OperationErrorCode.Error
|
private let session: URLSession = {
|
||||||
enum OperationErrorCode: Int, ALTErrorEnum
|
let configuration = URLSessionConfiguration.default
|
||||||
|
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
||||||
|
configuration.urlCache = nil
|
||||||
|
|
||||||
|
let session = URLSession(configuration: configuration)
|
||||||
|
return session
|
||||||
|
}()
|
||||||
|
|
||||||
|
extension OperationError
|
||||||
{
|
{
|
||||||
case cancelled
|
enum Code: Int, ALTErrorCode
|
||||||
case noTeam
|
{
|
||||||
case missingPrivateKey
|
typealias Error = OperationError
|
||||||
case missingCertificate
|
|
||||||
|
case cancelled
|
||||||
|
case noTeam
|
||||||
|
case missingPrivateKey
|
||||||
|
case missingCertificate
|
||||||
|
|
||||||
|
// Source JSON
|
||||||
|
case appNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
static let cancelled = OperationError(code: .cancelled)
|
||||||
|
static let noTeam = OperationError(code: .noTeam)
|
||||||
|
static let missingPrivateKey = OperationError(code: .missingPrivateKey)
|
||||||
|
static let missingCertificate = OperationError(code: .missingCertificate)
|
||||||
|
|
||||||
|
static func appNotFound(bundleID: String) -> OperationError { OperationError(code: .appNotFound, bundleID: bundleID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OperationError: ALTLocalizedError
|
||||||
|
{
|
||||||
|
var code: Code
|
||||||
|
var errorTitle: String?
|
||||||
|
var errorFailure: String?
|
||||||
|
|
||||||
|
var bundleID: String?
|
||||||
|
|
||||||
var errorFailureReason: String {
|
var errorFailureReason: String {
|
||||||
switch self
|
switch self.code
|
||||||
{
|
{
|
||||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||||
case .noTeam: return NSLocalizedString("You are not a member of any developer teams.", comment: "")
|
case .noTeam: return NSLocalizedString("You are not a member of any developer teams.", comment: "")
|
||||||
case .missingPrivateKey: return NSLocalizedString("The developer certificate's private key could not be found.", comment: "")
|
case .missingPrivateKey: return NSLocalizedString("The developer certificate's private key could not be found.", comment: "")
|
||||||
case .missingCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
|
case .missingCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
|
||||||
|
case .appNotFound:
|
||||||
|
let appBundleID = self.bundleID.map { "“\($0)”" } ?? "AltStore"
|
||||||
|
return String(format: NSLocalizedString("%@ could not be located in the source JSON.", comment: ""), appBundleID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension ALTDeviceManager
|
||||||
|
{
|
||||||
|
struct Source: Decodable
|
||||||
|
{
|
||||||
|
struct App: Decodable
|
||||||
|
{
|
||||||
|
struct Version: Decodable
|
||||||
|
{
|
||||||
|
var version: String
|
||||||
|
var downloadURL: URL
|
||||||
|
|
||||||
|
var minimumOSVersion: OperatingSystemVersion? {
|
||||||
|
return self.minOSVersion.map { OperatingSystemVersion(string: $0) }
|
||||||
|
}
|
||||||
|
private var minOSVersion: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
var name: String
|
||||||
|
var bundleIdentifier: String
|
||||||
|
|
||||||
|
var versions: [Version]?
|
||||||
|
}
|
||||||
|
|
||||||
|
var name: String
|
||||||
|
var identifier: String
|
||||||
|
|
||||||
|
var apps: [App]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ALTDeviceManager
|
extension ALTDeviceManager
|
||||||
{
|
{
|
||||||
func installApplication(at url: URL, to altDevice: ALTDevice, appleID: String, password: String, completion: @escaping (Result<ALTApplication, Error>) -> Void)
|
func installApplication(at url: URL, to altDevice: ALTDevice, appleID: String, password: String, completion: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||||
@@ -102,7 +178,7 @@ extension ALTDeviceManager
|
|||||||
fallthrough // Continue installing app even if we couldn't install Developer disk image.
|
fallthrough // Continue installing app even if we couldn't install Developer disk image.
|
||||||
|
|
||||||
case .success:
|
case .success:
|
||||||
self.downloadApp(from: url) { (result) in
|
self.downloadApp(from: url, for: altDevice) { (result) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let fileURL = try result.get()
|
let fileURL = try result.get()
|
||||||
@@ -229,26 +305,111 @@ extension ALTDeviceManager
|
|||||||
|
|
||||||
private extension ALTDeviceManager
|
private extension ALTDeviceManager
|
||||||
{
|
{
|
||||||
func downloadApp(from url: URL, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
func downloadApp(from url: URL, for device: ALTDevice, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||||
{
|
{
|
||||||
guard !url.isFileURL else { return completionHandler(.success(url)) }
|
guard !url.isFileURL else { return completionHandler(.success(url)) }
|
||||||
|
|
||||||
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
self.fetchAltStoreDownloadURL(for: device) { result in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): completionHandler(.failure(error))
|
||||||
|
case .success(let url):
|
||||||
|
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if let response = response as? HTTPURLResponse
|
||||||
|
{
|
||||||
|
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: url]) }
|
||||||
|
}
|
||||||
|
|
||||||
|
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||||
|
completionHandler(.success(fileURL))
|
||||||
|
|
||||||
|
do { try FileManager.default.removeItem(at: fileURL) }
|
||||||
|
catch { print("Failed to remove downloaded .ipa.", error) }
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadTask.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAltStoreDownloadURL(for device: ALTDevice, completion: @escaping (Result<URL, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let dataTask = session.dataTask(with: altstoreSourceURL) { (data, response, error) in
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
if let response = response as? HTTPURLResponse
|
||||||
completionHandler(.success(fileURL))
|
{
|
||||||
|
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: altstoreSourceURL]) }
|
||||||
|
}
|
||||||
|
|
||||||
|
let (data, _) = try Result((data, response), error).get()
|
||||||
|
let source = try Foundation.JSONDecoder().decode(Source.self, from: data)
|
||||||
|
|
||||||
|
let osName = device.type.osName ?? "iOS"
|
||||||
|
|
||||||
|
guard let altstore = source.apps.first(where: { $0.bundleIdentifier == altstoreBundleID }) else { throw OperationError.appNotFound(bundleID: altstoreBundleID) }
|
||||||
|
guard let latestVersion = altstore.versions?.first else { throw ALTServerError(.unsupportediOSVersion, userInfo: [ALTAppNameErrorKey: "AltStore",
|
||||||
|
ALTOperatingSystemNameErrorKey: osName,
|
||||||
|
ALTOperatingSystemVersionErrorKey: "12.2"]) }
|
||||||
|
|
||||||
|
let minOSVersionString = latestVersion.minimumOSVersion?.stringValue ?? "12.2"
|
||||||
|
|
||||||
|
guard let latestSupportedVersion = altstore.versions?.first(where: { appVersion in
|
||||||
|
if let minOSVersion = appVersion.minimumOSVersion, device.osVersion < minOSVersion
|
||||||
|
{
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}) else { throw ALTServerError(.unsupportediOSVersion, userInfo: [ALTAppNameErrorKey: "AltStore",
|
||||||
|
ALTOperatingSystemNameErrorKey: osName,
|
||||||
|
ALTOperatingSystemVersionErrorKey: minOSVersionString]) }
|
||||||
|
|
||||||
|
guard latestSupportedVersion.version != latestVersion.version else {
|
||||||
|
// The newest version is also the newest compatible version, so return its downloadURL.
|
||||||
|
return completion(.success(latestVersion.downloadURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
var message = String(format: NSLocalizedString("%@ is running %@ %@, but AltStore requires %@ %@ or later.", comment: ""), device.name, osName, device.osVersion.stringValue, osName, minOSVersionString)
|
||||||
|
message += "\n\n"
|
||||||
|
message += NSLocalizedString("Would you like to download the last version compatible with your device instead?", comment: "")
|
||||||
|
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = String(format: NSLocalizedString("Unsupported %@ Version", comment: ""), osName)
|
||||||
|
alert.informativeText = message
|
||||||
|
|
||||||
|
let buttonTitle = String(format: NSLocalizedString("Download %@ %@", comment: ""), altstore.name, latestSupportedVersion.version)
|
||||||
|
alert.addButton(withTitle: buttonTitle)
|
||||||
|
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||||
|
|
||||||
|
let index = alert.runModal()
|
||||||
|
if index == .alertFirstButtonReturn
|
||||||
|
{
|
||||||
|
completion(.success(latestSupportedVersion.downloadURL))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completion(.failure(OperationError.cancelled))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
do { try FileManager.default.removeItem(at: fileURL) }
|
|
||||||
catch { print("Failed to remove downloaded .ipa.", error) }
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
completionHandler(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadTask.resume()
|
dataTask.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticate(appleID: String, password: String, anisetteData: ALTAnisetteData, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void)
|
func authenticate(appleID: String, password: String, anisetteData: ALTAnisetteData, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
#import <AltStoreCore/AltStoreCore-Swift.h>
|
#import <AltStoreCore/AltStoreCore-Swift.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@import AltSign;
|
||||||
|
|
||||||
NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError";
|
NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError";
|
||||||
NSErrorDomain const AltServerInstallationErrorDomain = @"Apple.InstallationError";
|
NSErrorDomain const AltServerInstallationErrorDomain = @"Apple.InstallationError";
|
||||||
NSErrorDomain const AltServerConnectionErrorDomain = @"AltServer.ConnectionError";
|
NSErrorDomain const AltServerConnectionErrorDomain = @"AltServer.ConnectionError";
|
||||||
@@ -190,7 +192,17 @@ NSErrorUserInfoKey const ALTOperatingSystemVersionErrorKey = @"ALTOperatingSyste
|
|||||||
return NSLocalizedString(@"You cannot activate more than 3 apps with a non-developer Apple ID.", @"");
|
return NSLocalizedString(@"You cannot activate more than 3 apps with a non-developer Apple ID.", @"");
|
||||||
|
|
||||||
case ALTServerErrorUnsupportediOSVersion:
|
case ALTServerErrorUnsupportediOSVersion:
|
||||||
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
|
{
|
||||||
|
NSString *appName = self.userInfo[ALTAppNameErrorKey];
|
||||||
|
NSString *osVersion = [self altserver_osVersion];
|
||||||
|
|
||||||
|
if (appName == nil || osVersion == nil)
|
||||||
|
{
|
||||||
|
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [NSString stringWithFormat:NSLocalizedString(@"%@ requires %@ or later.", @""), appName, osVersion];
|
||||||
|
}
|
||||||
|
|
||||||
case ALTServerErrorUnknownRequest:
|
case ALTServerErrorUnknownRequest:
|
||||||
return NSLocalizedString(@"AltServer does not support this request.", @"");
|
return NSLocalizedString(@"AltServer does not support this request.", @"");
|
||||||
|
|||||||
Reference in New Issue
Block a user