diff --git a/AltStore/Operations/VerifyAppOperation.swift b/AltStore/Operations/VerifyAppOperation.swift index 6c2a4879..19801670 100644 --- a/AltStore/Operations/VerifyAppOperation.swift +++ b/AltStore/Operations/VerifyAppOperation.swift @@ -162,11 +162,51 @@ final class VerifyAppOperation: ResultOperation Task { do { - guard let ipaURL = self.context.ipaURL else { throw OperationError.appNotFound(name: app.name) } - - try await self.verifyHash(of: app, at: ipaURL, matches: appVersion) - try await self.verifyDownloadedVersion(of: app, matches: appVersion) - try await self.verifyPermissions(of: app, match: appVersion) + do + { + guard let ipaURL = self.context.ipaURL else { throw OperationError.appNotFound(name: app.name) } + + try await self.verifyHash(of: app, at: ipaURL, matches: appVersion) + try await self.verifyDownloadedVersion(of: app, matches: appVersion) + + // Verify permissions last in case user bypasses error. + try await self.verifyPermissions(of: app, match: appVersion) + } + catch let error as VerificationError where error.code == .undeclaredPermissions + { + #if !BETA + throw error + #endif + + if let trustedSources = UserDefaults.shared.trustedSources, let sourceID = await self.context.$appVersion.sourceID + { + let isTrusted = trustedSources.contains { $0.identifier == sourceID } + guard !isTrusted else { + // Don't enforce permission checking for Trusted Sources while 2.0 is in beta. + return self.finish(.success(())) + } + } + + // While in beta, allow users to temporarily bypass permissions alert + // so source maintainers have time to update their sources. + guard let presentingViewController = self.context.presentingViewController else { throw error } + + let message = NSLocalizedString("While AltStore 2.0 is in beta, you may choose to ignore this warning at your own risk until the source is updated.", comment: "") + + let ignoreAction = await UIAlertAction(title: NSLocalizedString("Install Anyway", comment: ""), style: .destructive) + let viewPermissionsAction = await UIAlertAction(title: NSLocalizedString("View Permisions", comment: ""), style: .default) + + while true + { + let action = try await presentingViewController.presentConfirmationAlert(title: error.errorFailureReason, + message: message, + actions: [ignoreAction, viewPermissionsAction]) + + guard action == viewPermissionsAction else { break } // break loop to continue with installation (unless we're viewing permissions). + + await presentingViewController.presentAlert(title: NSLocalizedString("Undeclared Permissions", comment: ""), message: error.recoverySuggestion) + } + } self.finish(.success(())) } diff --git a/AltStoreCore/Extensions/AltStore+Async.swift b/AltStoreCore/Extensions/AltStore+Async.swift index 4a66c1c6..80374bb0 100644 --- a/AltStoreCore/Extensions/AltStore+Async.swift +++ b/AltStoreCore/Extensions/AltStore+Async.swift @@ -63,7 +63,7 @@ public extension NSManagedObjectContext public extension UIViewController { @MainActor - func presentAlert(title: String, message: String, action: UIAlertAction? = nil) async + func presentAlert(title: String, message: String?, action: UIAlertAction? = nil) async { let action = action ?? .ok @@ -79,19 +79,33 @@ public extension UIViewController @MainActor func presentConfirmationAlert(title: String, message: String, primaryAction: UIAlertAction, cancelAction: UIAlertAction? = nil) async throws + { + _ = try await self.presentConfirmationAlert(title: title, message: message, actions: [primaryAction], cancelAction: cancelAction) + } + + @MainActor + func presentConfirmationAlert(title: String, message: String, actions: [UIAlertAction], cancelAction: UIAlertAction? = nil) async throws -> UIAlertAction { let cancelAction = cancelAction ?? .cancel - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let action = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: cancelAction.title, style: cancelAction.style) { _ in continuation.resume(throwing: CancellationError()) }) - alertController.addAction(UIAlertAction(title: primaryAction.title, style: primaryAction.style) { _ in - continuation.resume() - }) + + for action in actions + { + alertController.addAction(UIAlertAction(title: action.title, style: action.style) { alertAction in + // alertAction is different than the action provided, + // so return original action instead for == comparison. + continuation.resume(returning: action) + }) + } self.present(alertController, animated: true) } + + return action } }