mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Asks user to review permissions when installing/updating apps
When installing, all entitlements will be shown. When updating, only _added_ entitlements will be shown.
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Regex+Permissions.swift"; sourceTree = "<group>"; };
|
||||
D56915082AD5F3E800A2B747 /* AltTests+Sources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AltTests+Sources.swift"; sourceTree = "<group>"; };
|
||||
D569A5032AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPermissionsViewController.swift; sourceTree = "<group>"; };
|
||||
D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = "<group>"; };
|
||||
D571ADCD2A02FA7400B24B63 /* SourceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceError.swift; sourceTree = "<group>"; };
|
||||
D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
};
|
||||
D5BDD9712B1FC8FA001F84DE /* Permissions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D569A5032AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift */,
|
||||
);
|
||||
path = Permissions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
||||
@@ -162,51 +162,11 @@ final class VerifyAppOperation: ResultOperation<Void>
|
||||
Task<Void, Never> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
362
AltStore/Permissions/ReviewPermissionsViewController.swift
Normal file
362
AltStore/Permissions/ReviewPermissionsViewController.swift
Normal file
@@ -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, Error>) -> 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<UICollectionViewListCell>!
|
||||
|
||||
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<UICollectionViewListCell>(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<NSString>
|
||||
{
|
||||
let approveDataSource = RSTDynamicCollectionViewDataSource<NSString>()
|
||||
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<NSString>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewDataSource<NSString>(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<NSString>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewDataSource<NSString>(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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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" : {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user