Improves error message when registering app + app extension after App ID limit is reached

This commit is contained in:
Riley Testut
2020-02-10 16:30:54 -08:00
parent 9a50774f5f
commit 390a770115
4 changed files with 123 additions and 60 deletions

View File

@@ -0,0 +1,29 @@
//
// ALTApplication+AppExtensions.swift
// AltStore
//
// Created by Riley Testut on 2/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import AltSign
extension ALTApplication
{
var appExtensions: Set<ALTApplication> {
guard let bundle = Bundle(url: self.fileURL) else { return [] }
var appExtensions: Set<ALTApplication> = []
if let directory = bundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
{
for case let fileURL as URL in enumerator where fileURL.pathExtension.lowercased() == "appex"
{
guard let appExtension = ALTApplication(fileURL: fileURL) else { continue }
appExtensions.insert(appExtension)
}
}
return appExtensions
}
}

View File

@@ -25,6 +25,11 @@ extension ALTTeamType
} }
} }
extension Team
{
static let maximumFreeAppIDs = 10
}
@objc(Team) @objc(Team)
class Team: NSManagedObject, Fetchable class Team: NSManagedObject, Fetchable
{ {

View File

@@ -24,7 +24,7 @@ enum OperationError: LocalizedError
case invalidParameters case invalidParameters
case iOSVersionNotSupported(ALTApplication) case iOSVersionNotSupported(ALTApplication)
case maximumAppIDLimitReached(Date) case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
case noSources case noSources
@@ -58,26 +58,38 @@ enum OperationError: LocalizedError
var recoverySuggestion: String? { var recoverySuggestion: String? {
switch self switch self
{ {
case .maximumAppIDLimitReached(let date): case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
let remainingTime: String let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
let message: String
let numberOfDays = date.numberOfCalendarDays(since: Date()) if requiredAppIDs > 1
switch numberOfDays { {
case 0: let availableText: String
let components = Calendar.current.dateComponents([.hour], from: Date(), to: date)
let numberOfHours = components.hour!
switch numberOfHours switch availableAppIDs
{ {
case 1: remainingTime = NSLocalizedString("1 hour", comment: "") case 0: availableText = NSLocalizedString("none are available", comment: "")
default: remainingTime = String(format: NSLocalizedString("%@ hours", comment: ""), NSNumber(value: numberOfHours)) case 1: availableText = NSLocalizedString("only 1 is available", comment: "")
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
} }
case 1: remainingTime = NSLocalizedString("1 day", comment: "") let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
default: remainingTime = String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays)) message = prefixMessage + " " + baseMessage
}
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
} }
let message = String(format: NSLocalizedString("Delete sideloaded apps to free up App ID slots. You can register another App ID in %@.", comment: ""), remainingTime)
return message return message
default: return nil default: return nil

View File

@@ -102,53 +102,43 @@ private extension ResignAppOperation
{ {
func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void) func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
{ {
guard let bundle = Bundle(url: fileURL), let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) } guard let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
let dispatchGroup = DispatchGroup()
var profiles = [String: ALTProvisioningProfile]()
var error: Error?
dispatchGroup.enter()
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
switch result switch result
{ {
case .failure(let e): error = e case .failure(let error): completionHandler(.failure(error))
case .success(let profile): case .success(let profile):
profiles[app.bundleIdentifier] = profile var profiles = [app.bundleIdentifier: profile]
} var error: Error?
dispatchGroup.leave()
}
if let directory = bundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
{
for case let fileURL as URL in enumerator where fileURL.pathExtension.lowercased() == "appex"
{
guard let appExtension = ALTApplication(fileURL: fileURL) else { continue }
dispatchGroup.enter() let dispatchGroup = DispatchGroup()
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in for appExtension in app.appExtensions
switch result {
{ dispatchGroup.enter()
case .failure(let e): error = e
case .success(let profile): self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
profiles[appExtension.bundleIdentifier] = profile switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
if let error = error
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(profiles))
} }
dispatchGroup.leave()
} }
}
}
dispatchGroup.notify(queue: .global()) {
if let error = error
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(profiles))
} }
} }
} }
@@ -181,8 +171,19 @@ private extension ResignAppOperation
preferredBundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID) preferredBundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
} }
let preferredName: String
if let parentApp = parentApp
{
preferredName = "\(parentApp.name) - \(app.name)"
}
else
{
preferredName = app.name
}
// Register // Register
self.register(app, bundleIdentifier: preferredBundleID, team: team, session: session) { (result) in self.registerAppID(for: app, name: preferredName, bundleIdentifier: preferredBundleID, team: team, session: session) { (result) in
switch result switch result
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
@@ -215,10 +216,8 @@ private extension ResignAppOperation
} }
} }
func register(_ app: ALTApplication, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void) func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{ {
let appName = app.name
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do do
{ {
@@ -230,7 +229,27 @@ private extension ResignAppOperation
} }
else else
{ {
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in let requiredAppIDs = 1 + application.appExtensions.count
let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count)
let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 })
if team.type == .free
{
if requiredAppIDs > availableAppIDs
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
}
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
do do
{ {
do do
@@ -240,11 +259,9 @@ private extension ResignAppOperation
} }
catch ALTAppleAPIError.maximumAppIDLimitReached catch ALTAppleAPIError.maximumAppIDLimitReached
{ {
let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 })
if let expirationDate = sortedExpirationDates.first if let expirationDate = sortedExpirationDates.first
{ {
throw OperationError.maximumAppIDLimitReached(expirationDate) throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
} }
else else
{ {