diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 1161737c..f6a2844c 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -382,6 +382,7 @@ D561B2ED28EF5A4F006752E4 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; D56915072AD5E91B00A2B747 /* Regex+Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */; }; D56915092AD5F3E800A2B747 /* AltTests+Sources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56915082AD5F3E800A2B747 /* AltTests+Sources.swift */; }; + D569A5042AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D569A5032AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift */; }; D5708417292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; }; D570841A2924680D00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; }; D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */; }; @@ -1058,6 +1059,7 @@ D557A4842AE88227007D0DCF /* PledgeTier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgeTier.swift; sourceTree = ""; }; D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Regex+Permissions.swift"; sourceTree = ""; }; D56915082AD5F3E800A2B747 /* AltTests+Sources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AltTests+Sources.swift"; sourceTree = ""; }; + D569A5032AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPermissionsViewController.swift; sourceTree = ""; }; D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = ""; }; D571ADCD2A02FA7400B24B63 /* SourceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceError.swift; sourceTree = ""; }; D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = ""; }; @@ -1955,8 +1957,10 @@ BFBBE2E2229320A2002097FA /* My Apps */, BFDB69FB22A9A7A6007EA6D6 /* Settings */, BFD2478A2284C49000981D42 /* Managing Apps */, - BF56D2AD23DF9E170006506D /* App IDs */, BFC84A4B2421A13000853474 /* Sources */, + BF56D2AD23DF9E170006506D /* App IDs */, + D5BDD9712B1FC8FA001F84DE /* Permissions */, + BFC51D7922972F1F00388324 /* Server */, BF0DCA642433BDE200E3A595 /* Analytics */, BFF00D2E2501BD4B00746320 /* Intents */, BFDB6A0922AAEDA1007EA6D6 /* Operations */, @@ -2332,6 +2336,14 @@ path = Types; sourceTree = ""; }; + D5BDD9712B1FC8FA001F84DE /* Permissions */ = { + isa = PBXGroup; + children = ( + D569A5032AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift */, + ); + path = Permissions; + sourceTree = ""; + }; D5DB145728F9DC0300A8F606 /* Errors */ = { isa = PBXGroup; children = ( @@ -3336,6 +3348,7 @@ BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */, BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */, D52EF2BE2A0594550096C377 /* AppDetailCollectionViewController.swift in Sources */, + D569A5042AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift in Sources */, BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */, D5A645212AF591980047D980 /* UTType+AltStore.swift in Sources */, D5F982212AB910180045751F /* AppScreenshotsViewController.swift in Sources */, diff --git a/AltStore/Operations/VerifyAppOperation.swift b/AltStore/Operations/VerifyAppOperation.swift index 1acae721..ec8f40c9 100644 --- a/AltStore/Operations/VerifyAppOperation.swift +++ b/AltStore/Operations/VerifyAppOperation.swift @@ -162,51 +162,11 @@ final class VerifyAppOperation: ResultOperation Task { do { - 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 recommendedSources = UserDefaults.shared.recommendedSources, let sourceID = await self.context.$appVersion.sourceID - { - let isRecommended = recommendedSources.contains { $0.identifier == sourceID } - guard !isRecommended else { - // Don't enforce permission checking for Recommended 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) - } - } + 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) self.finish(.success(())) } @@ -257,26 +217,45 @@ private extension VerifyAppOperation guard let storeApp = await $appVersion.app else { throw OperationError.invalidParameters } // Verify source permissions match first. - _ = try await self.verifyPermissions(of: app, match: storeApp) + let allPermissions = try await self.verifyPermissions(of: app, match: storeApp) - // TODO: Uncomment to verify added permissions. - // switch self.permissionsMode - // { - // case .none, .all: break - // case .added: - // let installedAppURL = InstalledApp.fileURL(for: app) - // guard let previousApp = ALTApplication(fileURL: installedAppURL) else { throw OperationError.appNotFound(name: app.name) } - // - // var previousEntitlements = Set(previousApp.entitlements.keys) - // for appExtension in previousApp.appExtensions - // { - // previousEntitlements.formUnion(appExtension.entitlements.keys) - // } - // - // // Make sure all entitlements already exist in previousApp. - // let addedEntitlements = Array(allPermissions.lazy.compactMap { $0 as? ALTEntitlement }.filter { !previousEntitlements.contains($0) }) - // guard addedEntitlements.isEmpty else { throw VerificationError.addedPermissions(addedEntitlements, appVersion: appVersion) } - // } + guard #available(iOS 15, *) else { + // Only review downloaded app permissions on iOS 15 and above. + return + } + + switch self.permissionsMode + { + case .none: break + case .all: + guard let presentingViewController = self.context.presentingViewController else { break } // Don't fail just because we can't show permissions. + + let allEntitlements = allPermissions.compactMap { $0 as? ALTEntitlement } + if !allEntitlements.isEmpty + { + try await self.review(allEntitlements, for: app, mode: .all, presentingViewController: presentingViewController) + } + + case .added: + let installedAppURL = InstalledApp.fileURL(for: app) + guard let previousApp = ALTApplication(fileURL: installedAppURL) else { throw OperationError.appNotFound(name: app.name) } + + var previousEntitlements = Set(previousApp.entitlements.keys) + for appExtension in previousApp.appExtensions + { + previousEntitlements.formUnion(appExtension.entitlements.keys) + } + + // Make sure all entitlements already exist in previousApp. + let addedEntitlements = Array(allPermissions.lazy.compactMap { $0 as? ALTEntitlement }.filter { !previousEntitlements.contains($0) }) + if !addedEntitlements.isEmpty + { + // _DO_ throw error if there isn't a presentingViewController. + guard let presentingViewController = self.context.presentingViewController else { throw VerificationError.addedPermissions(addedEntitlements, appVersion: appVersion) } + + try await self.review(addedEntitlements, for: app, mode: .added, presentingViewController: presentingViewController) + } + } } @discardableResult @@ -345,11 +324,70 @@ private extension VerifyAppOperation return true } - guard missingPermissions.isEmpty else { - // There is at least one undeclared permission, so throw error. - throw VerificationError.undeclaredPermissions(missingPermissions, app: app) + do + { + guard missingPermissions.isEmpty else { + // There is at least one undeclared permission, so throw error. + throw VerificationError.undeclaredPermissions(missingPermissions, app: app) + } + } + catch let error as VerificationError where error.code == .undeclaredPermissions + { + #if !BETA + throw error + #endif + + if let recommendedSources = UserDefaults.shared.recommendedSources, let (sourceID, sourceURL) = await $storeApp.perform({ $0.source.map { ($0.identifier, $0.sourceURL) } }) + { + let normalizedSourceURL = try? sourceURL.normalized() + + let isRecommended = recommendedSources.contains { $0.identifier == sourceID || (try? $0.sourceURL?.normalized()) == normalizedSourceURL } + guard !isRecommended else { + // Don't enforce permission checking for Recommended Sources while 2.0 is in beta. + return localPermissions + } + } + + // 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) + } } return localPermissions } + + @MainActor @available(iOS 15, *) + func review(_ permissions: [ALTEntitlement], for app: AppProtocol, mode: PermissionReviewMode, presentingViewController: UIViewController) async throws + { + let reviewPermissionsViewController = ReviewPermissionsViewController(app: app, permissions: permissions, mode: mode) + let navigationController = UINavigationController(rootViewController: reviewPermissionsViewController) + + defer { + navigationController.dismiss(animated: true) + } + + try await withCheckedThrowingContinuation { continuation in + reviewPermissionsViewController.completionHandler = { result in + continuation.resume(with: result) + } + + presentingViewController.present(navigationController, animated: true) + } + } } diff --git a/AltStore/Permissions/ReviewPermissionsViewController.swift b/AltStore/Permissions/ReviewPermissionsViewController.swift new file mode 100644 index 00000000..b4e725c4 --- /dev/null +++ b/AltStore/Permissions/ReviewPermissionsViewController.swift @@ -0,0 +1,362 @@ +// +// ReviewPermissionsViewController.swift +// AltStore +// +// Created by Riley Testut on 11/6/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import UIKit +import SwiftUI + +import AltSign +import AltStoreCore +import Roxas + +@available(iOS 15, *) +extension ReviewPermissionsViewController +{ + private enum Section: Int + { + case known + case unknown + case approve + } +} + +@available(iOS 15, *) +class ReviewPermissionsViewController: UICollectionViewController +{ + let app: AppProtocol + let permissions: [ALTEntitlement] + + let permissionsMode: VerifyAppOperation.PermissionReviewMode + + var completionHandler: ((Result) -> Void)? + + private let knownPermissions: [any ALTAppPermission] + private let unknownPermissions: [any ALTAppPermission] + + private lazy var dataSource = self.makeDataSource() + private lazy var knownPermissionsDataSource = self.makeKnownPermissionsDataSource() + private lazy var unknownPermissionsDataSource = self.makeUnknownPermissionsDataSource() + + private var headerRegistration: UICollectionView.SupplementaryRegistration! + + init(app: AppProtocol, permissions: [ALTEntitlement], mode: VerifyAppOperation.PermissionReviewMode) + { + self.app = app + self.permissions = permissions + self.permissionsMode = mode + + let sortedPermissions = permissions.sorted { + $0.localizedDisplayName.localizedStandardCompare($1.localizedDisplayName) == .orderedAscending + } + + let knownPermissions = sortedPermissions.filter { $0.isKnown } + let unknownPermissions = sortedPermissions.filter { !$0.isKnown } + + self.knownPermissions = knownPermissions + self.unknownPermissions = unknownPermissions + + super.init(collectionViewLayout: UICollectionViewFlowLayout()) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() + { + super.viewDidLoad() + + let buttonAppearance = UIBarButtonItemAppearance(style: .plain) + buttonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.white] + + let appearance = UINavigationBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = UIColor(resource: .gradientTop) + appearance.titleTextAttributes = [.foregroundColor: UIColor.white] + appearance.buttonAppearance = buttonAppearance + self.navigationItem.standardAppearance = appearance + + self.title = NSLocalizedString("Review Permissions", comment: "") + + let collectionViewLayout = self.makeLayout() + self.collectionView.collectionViewLayout = collectionViewLayout + + if #available(iOS 16, *) + { + self.collectionView.backgroundView = UIHostingConfiguration { + LinearGradient(colors: [Color(UIColor(resource: .gradientTop)), Color(.gradientBottom)], startPoint: .top, endPoint: .bottom) + } + .margins(.all, 0) + .makeContentView() + } + else + { + self.collectionView.backgroundColor = UIColor(resource: .gradientBottom) + } + + self.dataSource.proxy = self + self.collectionView.dataSource = self.dataSource + + self.collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier) + self.collectionView.register(UICollectionViewListCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: UICollectionView.elementKindSectionHeader) + + let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(ReviewPermissionsViewController.cancel)) + self.navigationItem.leftBarButtonItem = cancelButton + + self.navigationController?.isModalInPresentation = true + + self.prepareCollectionView() + } +} + +@available(iOS 15, *) +extension ReviewPermissionsViewController +{ + func makeLayout() -> UICollectionViewCompositionalLayout + { + let layout = UICollectionViewCompositionalLayout { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in + guard let self, let section = Section(rawValue: sectionIndex) else { return nil } + + var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + configuration.showsSeparators = true + configuration.separatorConfiguration.color = UIColor(resource: .gradientBottom).withAlphaComponent(0.7) + configuration.separatorConfiguration.bottomSeparatorInsets.leading = 20 + configuration.backgroundColor = .clear + + switch section + { + case .known: configuration.headerMode = .supplementary // Always show header even if no known permissions + case .unknown: configuration.headerMode = self.unknownPermissionsDataSource.items.isEmpty ? .none : .supplementary + case .approve: configuration.headerMode = .none + } + + let layoutSection = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) + layoutSection.contentInsets.top = 15 + + switch section + { + case .known: + if self.knownPermissions.isEmpty + { + layoutSection.contentInsets.top = 0 + layoutSection.contentInsets.bottom = 20 + } + else if self.unknownPermissions.isEmpty + { + layoutSection.contentInsets.bottom = 20 + } + else + { + layoutSection.contentInsets.bottom = 44 + } + + case .unknown: + if self.unknownPermissions.isEmpty + { + layoutSection.contentInsets.top = 0 + layoutSection.contentInsets.bottom = 0 + } + else + { + layoutSection.contentInsets.bottom = 20 + } + + case .approve: layoutSection.contentInsets.bottom = 44 + } + + return layoutSection + } + + return layout + } + + func prepareCollectionView() + { + self.headerRegistration = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, elementKind, indexPath) in + var configuration = UIListContentConfiguration.prominentInsetGroupedHeader() + configuration.textProperties.color = .white + configuration.secondaryTextProperties.color = .white.withAlphaComponent(0.8) + configuration.textToSecondaryTextVerticalPadding = 8 + + switch Section(rawValue: indexPath.section)! + { + case .known: + configuration.text = nil + + switch self.permissionsMode + { + case .all: configuration.secondaryText = String(localized: "“\(self.app.name)” will be automatically given these permissions once installed.") + case .added: configuration.secondaryText = String(localized: "This version of “\(self.app.name)” requires additional permissions.") + case .none: break + } + + case .unknown: + configuration.text = NSLocalizedString("Additional Permissions", comment: "") + configuration.secondaryText = String(format: NSLocalizedString("These are permissions required by “%@” that AltStore does not recognize. Make sure you understand them before continuing.", comment: ""), self.app.name) + + case .approve: break + } + + headerView.contentConfiguration = configuration + headerView.backgroundConfiguration = .clear() + } + } + + func makeDataSource() -> RSTCompositeCollectionViewDataSource + { + let approveDataSource = RSTDynamicCollectionViewDataSource() + approveDataSource.numberOfSectionsHandler = { 1 } + approveDataSource.numberOfItemsHandler = { _ in 1 } + approveDataSource.cellConfigurationHandler = { cell, _, indexPath in + let cell = cell as! UICollectionViewListCell + + var config = cell.defaultContentConfiguration() + config.text = NSLocalizedString("Approve", comment: "") + config.textProperties.color = .white + config.textProperties.font = .preferredFont(forTextStyle: .headline) + config.textProperties.alignment = .center + config.directionalLayoutMargins.top = 15 + config.directionalLayoutMargins.bottom = 15 + cell.contentConfiguration = config + + cell.configurationUpdateHandler = { cell, state in + var content = config.updated(for: state) + + // Change text color when highlighted + if state.isHighlighted + { + content.textProperties.color = .white.withAlphaComponent(0.5) + } + + cell.contentConfiguration = content + } + + var backgroundConfig = UIBackgroundConfiguration.listGroupedCell() + backgroundConfig.backgroundColor = UIColor(resource: .darkButtonBackground) + backgroundConfig.visualEffect = nil + cell.backgroundConfiguration = backgroundConfig + } + + let dataSource = RSTCompositeCollectionViewDataSource(dataSources: [self.knownPermissionsDataSource, + self.unknownPermissionsDataSource, + approveDataSource]) + return dataSource + } + + func makeKnownPermissionsDataSource() -> RSTArrayCollectionViewDataSource + { + let dataSource = RSTArrayCollectionViewDataSource(items: self.knownPermissions.map { $0.rawValue as NSString }) + dataSource.cellConfigurationHandler = { [weak self] cell, permission, indexPath in + let cell = cell as! UICollectionViewListCell + let permission = ALTEntitlement(rawValue: permission as String) + self?.configure(cell, permission: permission) + } + + return dataSource + } + + func makeUnknownPermissionsDataSource() -> RSTArrayCollectionViewDataSource + { + let dataSource = RSTArrayCollectionViewDataSource(items: self.unknownPermissions.map { $0.rawValue as NSString }) + dataSource.cellConfigurationHandler = { [weak self] cell, permission, indexPath in + let cell = cell as! UICollectionViewListCell + let permission = ALTEntitlement(rawValue: permission as String) + self?.configure(cell, permission: permission) + } + + return dataSource + } + + func configure(_ cell: UICollectionViewListCell, permission: ALTEntitlement) + { + var config = cell.defaultContentConfiguration() + config.text = permission.localizedDisplayName + config.textProperties.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).bolded(), size: 0.0) + config.textProperties.color = .label + config.textToSecondaryTextVerticalPadding = 5.0 + config.directionalLayoutMargins.top = 20 + config.directionalLayoutMargins.bottom = 20 + + config.secondaryTextProperties.font = UIFont.preferredFont(forTextStyle: .subheadline) + config.secondaryTextProperties.color = .white.withAlphaComponent(0.8) + + config.imageProperties.tintColor = .white + config.imageToTextPadding = 20 + config.directionalLayoutMargins.leading = 20 + + if permission.isKnown + { + let symbolConfig = UIImage.SymbolConfiguration(scale: .large) + config.image = UIImage(systemName: permission.effectiveSymbolName, withConfiguration: symbolConfig) + config.secondaryText = permission.localizedDescription + } + else + { + config.image = nil + config.secondaryText = permission.rawValue + } + + cell.contentConfiguration = config + + var backgroundConfiguration = UIBackgroundConfiguration.clear() + backgroundConfiguration.backgroundColor = .white.withAlphaComponent(0.25) + backgroundConfiguration.visualEffect = UIVibrancyEffect(blurEffect: .init(style: .systemMaterial), style: .fill) + cell.backgroundConfiguration = backgroundConfiguration + + // Ensure text is legible on gradient background. + cell.overrideUserInterfaceStyle = .dark + } +} + +@available(iOS 15, *) +private extension ReviewPermissionsViewController +{ + @objc + func cancel() + { + self.completionHandler?(.failure(CancellationError())) + self.completionHandler = nil + } +} + +@available(iOS 15, *) +extension ReviewPermissionsViewController +{ + override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView + { + let headerView = self.collectionView.dequeueConfiguredReusableSupplementary(using: self.headerRegistration, for: indexPath) + return headerView + } + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) + { + guard let section = Section(rawValue: indexPath.section), section == .approve else { return } + + self.completionHandler?(.success(())) + self.completionHandler = nil + } +} + +@available(iOS 17, *) +#Preview(traits: .portrait) { + DatabaseManager.shared.startForPreview() + + let app = AnyApp(name: "Delta", bundleIdentifier: "com.rileytestut.Delta", url: nil, storeApp: nil) + let permissions: [ALTEntitlement] = [ + .getTaskAllow, + .appGroups, + .interAppAudio, + .keychainAccessGroups, + .init("com.apple.developer.extended-virtual-addressing"), + .init("com.apple.developer.increased-memory-limit") + ] + + let reviewPermissionsViewController = ReviewPermissionsViewController(app: app, permissions: permissions, mode: .all) + + let navigationController = UINavigationController(rootViewController: reviewPermissionsViewController) + return navigationController +} diff --git a/AltStore/Resources/Assets.xcassets/Colors/DarkButtonBackground.colorset/Contents.json b/AltStore/Resources/Assets.xcassets/Colors/DarkButtonBackground.colorset/Contents.json new file mode 100644 index 00000000..5fe3dcbc --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Colors/DarkButtonBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "74", + "green" : "55", + "red" : "0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.518", + "green" : "0.502", + "red" : "0.004" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltStore/Resources/Assets.xcassets/Colors/GradientBottom.colorset/Contents.json b/AltStore/Resources/Assets.xcassets/Colors/GradientBottom.colorset/Contents.json index c51e8e09..7245ab6c 100644 --- a/AltStore/Resources/Assets.xcassets/Colors/GradientBottom.colorset/Contents.json +++ b/AltStore/Resources/Assets.xcassets/Colors/GradientBottom.colorset/Contents.json @@ -11,6 +11,24 @@ } }, "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "103", + "green" : "82", + "red" : "2" + } + }, + "idiom" : "universal" } ], "info" : { diff --git a/AltStore/Resources/Assets.xcassets/Colors/GradientTop.colorset/Contents.json b/AltStore/Resources/Assets.xcassets/Colors/GradientTop.colorset/Contents.json index a95d693f..34596b2b 100644 --- a/AltStore/Resources/Assets.xcassets/Colors/GradientTop.colorset/Contents.json +++ b/AltStore/Resources/Assets.xcassets/Colors/GradientTop.colorset/Contents.json @@ -5,9 +5,27 @@ "color-space" : "display-p3", "components" : { "alpha" : "1.000", - "blue" : "176", - "green" : "200", - "red" : "123" + "blue" : "159", + "green" : "180", + "red" : "111" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "132", + "green" : "128", + "red" : "1" } }, "idiom" : "universal"