From 390a77011521eb0dac785428597fa59742b0ba53 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 10 Feb 2020 16:30:54 -0800 Subject: [PATCH] Improves error message when registering app + app extension after App ID limit is reached --- .../ALTApplication+AppExtensions.swift | 29 +++++ AltStore/Model/Team.swift | 5 + AltStore/Operations/OperationError.swift | 40 ++++--- AltStore/Operations/ResignAppOperation.swift | 109 ++++++++++-------- 4 files changed, 123 insertions(+), 60 deletions(-) create mode 100644 AltStore/Extensions/ALTApplication+AppExtensions.swift diff --git a/AltStore/Extensions/ALTApplication+AppExtensions.swift b/AltStore/Extensions/ALTApplication+AppExtensions.swift new file mode 100644 index 00000000..3ed8f0d2 --- /dev/null +++ b/AltStore/Extensions/ALTApplication+AppExtensions.swift @@ -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 { + guard let bundle = Bundle(url: self.fileURL) else { return [] } + + var appExtensions: Set = [] + + 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 + } +} diff --git a/AltStore/Model/Team.swift b/AltStore/Model/Team.swift index d8e1ad32..9dcc69ce 100644 --- a/AltStore/Model/Team.swift +++ b/AltStore/Model/Team.swift @@ -25,6 +25,11 @@ extension ALTTeamType } } +extension Team +{ + static let maximumFreeAppIDs = 10 +} + @objc(Team) class Team: NSManagedObject, Fetchable { diff --git a/AltStore/Operations/OperationError.swift b/AltStore/Operations/OperationError.swift index 20ed243a..5637006d 100644 --- a/AltStore/Operations/OperationError.swift +++ b/AltStore/Operations/OperationError.swift @@ -24,7 +24,7 @@ enum OperationError: LocalizedError case invalidParameters case iOSVersionNotSupported(ALTApplication) - case maximumAppIDLimitReached(Date) + case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date) case noSources @@ -58,26 +58,38 @@ enum OperationError: LocalizedError var recoverySuggestion: String? { switch self { - case .maximumAppIDLimitReached(let date): - let remainingTime: String + case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date): + let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "") + let message: String - let numberOfDays = date.numberOfCalendarDays(since: Date()) - switch numberOfDays { - case 0: - let components = Calendar.current.dateComponents([.hour], from: Date(), to: date) - let numberOfHours = components.hour! + if requiredAppIDs > 1 + { + let availableText: String - switch numberOfHours + switch availableAppIDs { - case 1: remainingTime = NSLocalizedString("1 hour", comment: "") - default: remainingTime = String(format: NSLocalizedString("%@ hours", comment: ""), NSNumber(value: numberOfHours)) + case 0: availableText = NSLocalizedString("none are available", comment: "") + 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: "") - default: remainingTime = String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays)) + let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText) + 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 default: return nil diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift index 9977406e..1d3e72a8 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -102,53 +102,43 @@ private extension ResignAppOperation { 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)) } - - let dispatchGroup = DispatchGroup() - - var profiles = [String: ALTProvisioningProfile]() - var error: Error? - - dispatchGroup.enter() + guard let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) } self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in switch result { - case .failure(let e): error = e + case .failure(let error): completionHandler(.failure(error)) case .success(let profile): - profiles[app.bundleIdentifier] = profile - } - 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 } + var profiles = [app.bundleIdentifier: profile] + var error: Error? - dispatchGroup.enter() + let dispatchGroup = DispatchGroup() - self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in - switch result - { - case .failure(let e): error = e - case .success(let profile): - profiles[appExtension.bundleIdentifier] = profile + for appExtension in app.appExtensions + { + dispatchGroup.enter() + + self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in + 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) } + let preferredName: String + + if let parentApp = parentApp + { + preferredName = "\(parentApp.name) - \(app.name)" + } + else + { + preferredName = app.name + } + // 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 { 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) -> Void) + func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { - let appName = app.name - ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in do { @@ -230,7 +229,27 @@ private extension ResignAppOperation } 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 @@ -240,11 +259,9 @@ private extension ResignAppOperation } catch ALTAppleAPIError.maximumAppIDLimitReached { - let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 }) - if let expirationDate = sortedExpirationDates.first { - throw OperationError.maximumAppIDLimitReached(expirationDate) + throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate) } else {