From dddb9c5ddb13c6749463d8827cab03bd7df29ad6 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Wed, 29 Nov 2023 18:24:33 -0600 Subject: [PATCH] Limits installed Patreon apps that no longer have active pledge Patreon apps with inactive pledges still support these actions: * Backed up * Deactivated * Export backup --- AltStore/My Apps/MyAppsViewController.swift | 220 ++++++++++++-------- AltStoreCore/Model/InstalledApp.swift | 61 +++--- 2 files changed, 168 insertions(+), 113 deletions(-) diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 3993c73a..114f4dcc 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -117,6 +117,9 @@ class MyAppsViewController: UICollectionViewController, PeekPopPreviewing { super.viewIsAppearing(animated) + // Ensure the button for each app reflects correct Patreon status. + self.collectionView.reloadData() + self.update() self.fetchAppIDs() @@ -367,9 +370,22 @@ private extension MyAppsViewController cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Refresh %@", comment: ""), installedApp.name) - formatter.includesTimeRemainingPhrase = true + // formatter.includesTimeRemainingPhrase = true - cell.bannerView.accessibilityLabel? += ". " + (formatter.string(from: currentDate, to: installedApp.expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " " + // cell.bannerView.accessibilityLabel? += ". " + (formatter.string(from: currentDate, to: installedApp.expirationDate) ?? NSLocalizedString("Unknown", comment: "")) + " " + + if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged + { + cell.bannerView.button.isEnabled = false + cell.bannerView.button.alpha = 0.5 + } + else + { + cell.bannerView.button.isEnabled = true + cell.bannerView.button.alpha = 1.0 + } + + cell.bannerView.accessibilityLabel? += ". " + String(format: NSLocalizedString("Expires in %@", comment: ""), numberOfDaysText) // Make sure refresh button is correct size. cell.layoutIfNeeded() @@ -450,6 +466,17 @@ private extension MyAppsViewController cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.activateApp(_:)), for: .primaryActionTriggered) cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Activate %@", comment: ""), installedApp.name) + if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged + { + cell.bannerView.button.isEnabled = false + cell.bannerView.button.alpha = 0.5 + } + else + { + cell.bannerView.button.isEnabled = true + cell.bannerView.button.alpha = 1.0 + } + // Make sure refresh button is correct size. cell.layoutIfNeeded() @@ -1689,7 +1716,7 @@ extension MyAppsViewController extension MyAppsViewController { - private func actions(for installedApp: InstalledApp) -> [UIMenuElement] + private func contextMenu(for installedApp: InstalledApp) -> UIMenu { var actions = [UIMenuElement]() @@ -1747,103 +1774,128 @@ extension MyAppsViewController let changeIconMenu = UIMenu(title: NSLocalizedString("Change Icon", comment: ""), image: UIImage(systemName: "photo"), children: changeIconActions) - guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else { - #if BETA - return [refreshAction, changeIconMenu] - #else - return [refreshAction] - #endif - } - - if installedApp.isActive + if installedApp.bundleIdentifier == StoreApp.altstoreAppID { - actions.append(openMenu) - actions.append(refreshAction) + #if BETA + actions = [refreshAction, changeIconMenu] + #else + actions = [refreshAction] + #endif } else { - actions.append(activateAction) - } - - if installedApp.isActive - { - actions.append(jitAction) - } - - #if BETA - actions.append(changeIconMenu) - #endif - - if installedApp.isActive - { - actions.append(backupAction) - } - else if let _ = UTTypeCopyDeclaration(installedApp.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?, !UserDefaults.standard.isLegacyDeactivationSupported - { - // Allow backing up inactive apps if they are still installed, - // but on an iOS version that no longer supports legacy deactivation. - // This handles edge case where you can't install more apps until you - // delete some, but can't activate inactive apps again to back them up first. - actions.append(backupAction) - } - - if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) - { - var backupExists = false - var outError: NSError? = nil - - self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in - #if DEBUG - backupExists = true - #else - backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path) - #endif + if installedApp.isActive + { + actions.append(openMenu) + actions.append(refreshAction) + } + else + { + actions.append(activateAction) } - if backupExists + if installedApp.isActive { - actions.append(exportBackupAction) + actions.append(jitAction) + } + + #if BETA + actions.append(changeIconMenu) + #endif + + if installedApp.isActive + { + actions.append(backupAction) + } + else if let _ = UTTypeCopyDeclaration(installedApp.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?, !UserDefaults.standard.isLegacyDeactivationSupported + { + // Allow backing up inactive apps if they are still installed, + // but on an iOS version that no longer supports legacy deactivation. + // This handles edge case where you can't install more apps until you + // delete some, but can't activate inactive apps again to back them up first. + actions.append(backupAction) + } + + if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) + { + var backupExists = false + var outError: NSError? = nil - if installedApp.isActive + self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in + #if DEBUG + backupExists = true + #else + backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path) + #endif + } + + if backupExists { - actions.append(restoreBackupAction) + actions.append(exportBackupAction) + + if installedApp.isActive + { + actions.append(restoreBackupAction) + } + } + else if let error = outError + { + print("Unable to check if backup exists:", error) } } - else if let error = outError + + if installedApp.isActive { - print("Unable to check if backup exists:", error) + actions.append(deactivateAction) } + + #if DEBUG + + if installedApp.bundleIdentifier != StoreApp.altstoreAppID + { + actions.append(removeAction) + } + + #else + + if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier) + { + // Legacy sideloaded app, so can't detect if it's deleted. + actions.append(removeAction) + } + else if !UserDefaults.standard.isLegacyDeactivationSupported && !installedApp.isActive + { + // Inactive apps are actually deleted, so we need another way + // for user to remove them from AltStore. + actions.append(removeAction) + } + + #endif } - if installedApp.isActive + var title: String? + + if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged { - actions.append(deactivateAction) + let error = OperationError.pledgeInactive(appName: installedApp.name) + title = error.localizedDescription + + // Limit options for apps requiring pledges that we are no longer pledged to. + actions = actions.filter { + $0 == openMenu || + $0 == deactivateAction || + $0 == removeAction || + $0 == backupAction || + $0 == exportBackupAction || + ($0 == refreshAction && storeApp.bundleIdentifier == StoreApp.altstoreAppID) // Always show refresh option for AltStore so the menu will be shown. + } + + // Disable refresh action for AltStore. + refreshAction.attributes = .disabled } - #if DEBUG - - if installedApp.bundleIdentifier != StoreApp.altstoreAppID - { - actions.append(removeAction) - } - - #else - - if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier) - { - // Legacy sideloaded app, so can't detect if it's deleted. - actions.append(removeAction) - } - else if !UserDefaults.standard.isLegacyDeactivationSupported && !installedApp.isActive - { - // Inactive apps are actually deleted, so we need another way - // for user to remove them from AltStore. - actions.append(removeAction) - } - - #endif - - return actions + let menu = UIMenu(title: title ?? "", children: actions) + return menu } override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? @@ -1856,9 +1908,7 @@ extension MyAppsViewController let installedApp = self.dataSource.item(at: indexPath) return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in - let actions = self.actions(for: installedApp) - - let menu = UIMenu(title: "", children: actions) + let menu = self.contextMenu(for: installedApp) return menu } } diff --git a/AltStoreCore/Model/InstalledApp.swift b/AltStoreCore/Model/InstalledApp.swift index 4207c960..46c89798 100644 --- a/AltStoreCore/Model/InstalledApp.swift +++ b/AltStoreCore/Model/InstalledApp.swift @@ -252,18 +252,12 @@ public extension InstalledApp class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp] { - let predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID) - print("Fetch Apps for Refreshing All 'AltStore' predicate: \(String(describing: predicate))") - -// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated -// { -// // No additional predicate -// } -// else -// { -// predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, -// NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))]) -// } + let predicate = NSPredicate(format: "(%K == YES AND %K != %@) AND (%K == nil OR %K == NO OR %K == YES)", + #keyPath(InstalledApp.isActive), + #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID, + #keyPath(InstalledApp.storeApp), + #keyPath(InstalledApp.storeApp.isPledgeRequired), + #keyPath(InstalledApp.storeApp.isPledged)) var installedApps = InstalledApp.all(satisfying: predicate, sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)], @@ -272,7 +266,17 @@ public extension InstalledApp if let altStoreApp = InstalledApp.fetchAltStore(in: context) { // Refresh AltStore last since it causes app to quit. - installedApps.append(altStoreApp) + + if let storeApp = altStoreApp.storeApp, !storeApp.isPledgeRequired || storeApp.isPledged + { + // Only add AltStore if it's the public version OR if it's the beta and we're pledged to it. + installedApps.append(altStoreApp) + } + else + { + // No associated storeApp, so add it just to be safe. + installedApps.append(altStoreApp) + } } return installedApps @@ -283,21 +287,14 @@ public extension InstalledApp // Date 6 hours before now. let date = Date().addingTimeInterval(-1 * 6 * 60 * 60) - let predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)", + let predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@) AND (%K == nil OR %K == NO OR %K == YES)", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.refreshedDate), date as NSDate, - #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID) - print("Active Apps For Background Refresh 'AltStore' predicate: \(String(describing: predicate))") - -// if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated -// { -// // No additional predicate -// } -// else -// { -// predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, -// NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))]) -// } + #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID, + #keyPath(InstalledApp.storeApp), + #keyPath(InstalledApp.storeApp.isPledgeRequired), + #keyPath(InstalledApp.storeApp.isPledged) + ) var installedApps = InstalledApp.all(satisfying: predicate, sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)], @@ -305,8 +302,16 @@ public extension InstalledApp if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date { - // Refresh AltStore last since it may cause app to quit. - installedApps.append(altStoreApp) + if let storeApp = altStoreApp.storeApp, !storeApp.isPledgeRequired || storeApp.isPledged + { + // Only add AltStore if it's the public version OR if it's the beta and we're pledged to it. + installedApps.append(altStoreApp) + } + else + { + // No associated storeApp, so add it just to be safe. + installedApps.append(altStoreApp) + } } return installedApps