mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-21 20:53:26 +01: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
|
||||||
{
|
{
|
||||||
|
enum Code: Int, ALTErrorCode
|
||||||
|
{
|
||||||
|
typealias Error = OperationError
|
||||||
|
|
||||||
case cancelled
|
case cancelled
|
||||||
case noTeam
|
case noTeam
|
||||||
case missingPrivateKey
|
case missingPrivateKey
|
||||||
case missingCertificate
|
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,13 +305,23 @@ 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)) }
|
||||||
|
|
||||||
|
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
|
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
||||||
do
|
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()
|
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||||
completionHandler(.success(fileURL))
|
completionHandler(.success(fileURL))
|
||||||
|
|
||||||
@@ -250,6 +336,81 @@ private extension ALTDeviceManager
|
|||||||
|
|
||||||
downloadTask.resume()
|
downloadTask.resume()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAltStoreDownloadURL(for device: ALTDevice, completion: @escaping (Result<URL, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let dataTask = session.dataTask(with: altstoreSourceURL) { (data, response, error) in
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if let response = response as? HTTPURLResponse
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
{
|
||||||
|
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 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