From b196981c89ca16d365e2c12291e7d6a2ef7ab08a Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 24 Jan 2020 14:14:08 -0800 Subject: [PATCH] Improves 10 App ID limit error handling --- AltStore/App Detail/AppViewController.swift | 4 +- AltStore/Browse/BrowseViewController.swift | 4 +- AltStore/Components/ToastView.swift | 34 +++++++++++++++ AltStore/My Apps/MyAppsViewController.swift | 45 ++++++++++++-------- AltStore/Operations/OperationError.swift | 32 ++++++++++++++ AltStore/Operations/ResignAppOperation.swift | 26 ++++++++++- Dependencies/AltSign | 2 +- 7 files changed, 123 insertions(+), 24 deletions(-) diff --git a/AltStore/App Detail/AppViewController.swift b/AltStore/App Detail/AppViewController.swift index 238dbe9c..c9c04f99 100644 --- a/AltStore/App Detail/AppViewController.swift +++ b/AltStore/App Detail/AppViewController.swift @@ -502,8 +502,8 @@ extension AppViewController catch { DispatchQueue.main.async { - let toastView = ToastView(text: error.localizedDescription, detailText: nil) - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2) + let toastView = ToastView(error: error) + toastView.show(in: self) } } diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index 775dd2d2..b9637497 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -255,8 +255,8 @@ private extension BrowseViewController { case .failure(OperationError.cancelled): break // Ignore case .failure(let error): - let toastView = ToastView(text: error.localizedDescription, detailText: nil) - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2) + let toastView = ToastView(error: error) + toastView.show(in: self) case .success: print("Installed app:", app.bundleIdentifier) } diff --git a/AltStore/Components/ToastView.swift b/AltStore/Components/ToastView.swift index c609a421..7a8bb74d 100644 --- a/AltStore/Components/ToastView.swift +++ b/AltStore/Components/ToastView.swift @@ -10,11 +10,35 @@ import Roxas class ToastView: RSTToastView { + var preferredDuration: TimeInterval + override init(text: String, detailText detailedText: String?) { + if detailedText == nil + { + self.preferredDuration = 2.0 + } + else + { + self.preferredDuration = 8.0 + } + super.init(text: text, detailText: detailedText) self.layoutMargins = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) + self.setNeedsLayout() + } + + convenience init(error: Error) + { + if let error = error as? LocalizedError + { + self.init(text: error.localizedDescription, detailText: error.recoverySuggestion ?? error.failureReason) + } + else + { + self.init(text: error.localizedDescription, detailText: nil) + } } required init(coder aDecoder: NSCoder) { @@ -27,4 +51,14 @@ class ToastView: RSTToastView self.layer.cornerRadius = 16 } + + func show(in viewController: UIViewController) + { + self.show(in: viewController.navigationController?.view ?? viewController.view, duration: self.preferredDuration) + } + + override func show(in view: UIView) + { + self.show(in: view, duration: self.preferredDuration) + } } diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 3206583c..a1adf549 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -383,9 +383,8 @@ private extension MyAppsViewController switch result { case .failure(let error): - let toastView = ToastView(text: error.localizedDescription, detailText: nil) - toastView.setNeedsLayout() - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + let toastView = ToastView(error: error) + toastView.show(in: self) case .success(let results): let failures = results.compactMapValues { (result) -> Error? in @@ -399,22 +398,32 @@ private extension MyAppsViewController guard !failures.isEmpty else { break } - let localizedText: String - let detailText: String? + let toastView: ToastView - if let failure = failures.first, failures.count == 1 + if let failure = failures.first, results.count == 1 { - localizedText = failure.value.localizedDescription - detailText = nil + toastView = ToastView(error: failure.value) } else { - localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count)) - detailText = failures.first?.value.localizedDescription + let localizedText: String + + if failures.count == 1 + { + localizedText = NSLocalizedString("Failed to refresh 1 app.", comment: "") + } + else + { + localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count)) + } + + let detailText = failures.first?.value.localizedDescription + + toastView = ToastView(text: localizedText, detailText: detailText) + toastView.preferredDuration = 2.0 } - let toastView = ToastView(text: localizedText, detailText: detailText) - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + toastView.show(in: self) } self.refreshGroup = nil @@ -574,8 +583,8 @@ private extension MyAppsViewController self.collectionView.reloadItems(at: [indexPath]) case .failure(let error): - let toastView = ToastView(text: error.localizedDescription, detailText: nil) - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2) + let toastView = ToastView(error: error) + toastView.show(in: self) self.collectionView.reloadItems(at: [indexPath]) @@ -637,8 +646,8 @@ private extension MyAppsViewController DispatchQueue.main.async { if let error = result.error { - let toastView = ToastView(text: error.localizedDescription, detailText: nil) - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + let toastView = ToastView(error: error) + toastView.show(in: self) } else { @@ -666,8 +675,8 @@ private extension MyAppsViewController DispatchQueue.main.async { self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false - let toastView = ToastView(text: error.localizedDescription, detailText: nil) - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + let toastView = ToastView(error: error) + toastView.show(in: self) } completion(.failure(error)) diff --git a/AltStore/Operations/OperationError.swift b/AltStore/Operations/OperationError.swift index 1fe70acc..20ed243a 100644 --- a/AltStore/Operations/OperationError.swift +++ b/AltStore/Operations/OperationError.swift @@ -24,6 +24,7 @@ enum OperationError: LocalizedError case invalidParameters case iOSVersionNotSupported(ALTApplication) + case maximumAppIDLimitReached(Date) case noSources @@ -49,6 +50,37 @@ enum OperationError: LocalizedError let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version) return localizedDescription + + case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "") + } + } + + var recoverySuggestion: String? { + switch self + { + case .maximumAppIDLimitReached(let date): + let remainingTime: 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! + + switch numberOfHours + { + case 1: remainingTime = NSLocalizedString("1 hour", comment: "") + default: remainingTime = String(format: NSLocalizedString("%@ hours", comment: ""), NSNumber(value: numberOfHours)) + } + + case 1: remainingTime = NSLocalizedString("1 day", comment: "") + default: remainingTime = String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays)) + } + + 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 e5499396..9977406e 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -231,7 +231,31 @@ private extension ResignAppOperation else { ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in - completionHandler(Result(appID, error)) + do + { + do + { + let appID = try Result(appID, error).get() + completionHandler(.success(appID)) + } + catch ALTAppleAPIError.maximumAppIDLimitReached + { + let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 }) + + if let expirationDate = sortedExpirationDates.first + { + throw OperationError.maximumAppIDLimitReached(expirationDate) + } + else + { + throw ALTAppleAPIError(.maximumAppIDLimitReached) + } + } + } + catch + { + completionHandler(.failure(error)) + } } } } diff --git a/Dependencies/AltSign b/Dependencies/AltSign index 4b43efed..8afbc8a8 160000 --- a/Dependencies/AltSign +++ b/Dependencies/AltSign @@ -1 +1 @@ -Subproject commit 4b43efed0d676fa016b26d136fa990ea8126f821 +Subproject commit 8afbc8a84520f15c65dec289eb8734c303d2402d