mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 11:43:24 +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:
@@ -384,6 +384,7 @@
|
|||||||
D561B2ED28EF5A4F006752E4 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
D570841A2924680D00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
||||||
D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */; };
|
D571ADD02A02FC7200B24B63 /* ALTAppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */; };
|
||||||
@@ -1003,6 +1004,7 @@
|
|||||||
D557A4842AE88227007D0DCF /* PledgeTier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgeTier.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D571ADCF2A02FC7200B24B63 /* ALTAppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTAppPermission.swift; sourceTree = "<group>"; };
|
||||||
@@ -1848,8 +1850,9 @@
|
|||||||
BFBBE2E2229320A2002097FA /* My Apps */,
|
BFBBE2E2229320A2002097FA /* My Apps */,
|
||||||
BFDB69FB22A9A7A6007EA6D6 /* Settings */,
|
BFDB69FB22A9A7A6007EA6D6 /* Settings */,
|
||||||
BFD2478A2284C49000981D42 /* Managing Apps */,
|
BFD2478A2284C49000981D42 /* Managing Apps */,
|
||||||
BF56D2AD23DF9E170006506D /* App IDs */,
|
|
||||||
BFC84A4B2421A13000853474 /* Sources */,
|
BFC84A4B2421A13000853474 /* Sources */,
|
||||||
|
BF56D2AD23DF9E170006506D /* App IDs */,
|
||||||
|
D5BDD9712B1FC8FA001F84DE /* Permissions */,
|
||||||
BFC51D7922972F1F00388324 /* Server */,
|
BFC51D7922972F1F00388324 /* Server */,
|
||||||
BF0DCA642433BDE200E3A595 /* Analytics */,
|
BF0DCA642433BDE200E3A595 /* Analytics */,
|
||||||
BFF00D2E2501BD4B00746320 /* Intents */,
|
BFF00D2E2501BD4B00746320 /* Intents */,
|
||||||
@@ -2262,6 +2265,14 @@
|
|||||||
path = Types;
|
path = Types;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D5BDD9712B1FC8FA001F84DE /* Permissions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D569A5032AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift */,
|
||||||
|
);
|
||||||
|
path = Permissions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D5DB145728F9DC0300A8F606 /* Errors */ = {
|
D5DB145728F9DC0300A8F606 /* Errors */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -3282,6 +3293,7 @@
|
|||||||
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
|
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
|
||||||
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
|
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
|
||||||
D52EF2BE2A0594550096C377 /* AppDetailCollectionViewController.swift in Sources */,
|
D52EF2BE2A0594550096C377 /* AppDetailCollectionViewController.swift in Sources */,
|
||||||
|
D569A5042AF9BC5F00A4CB8B /* ReviewPermissionsViewController.swift in Sources */,
|
||||||
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */,
|
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */,
|
||||||
D5A645212AF591980047D980 /* UTType+AltStore.swift in Sources */,
|
D5A645212AF591980047D980 /* UTType+AltStore.swift in Sources */,
|
||||||
D5F982212AB910180045751F /* AppScreenshotsViewController.swift in Sources */,
|
D5F982212AB910180045751F /* AppScreenshotsViewController.swift in Sources */,
|
||||||
|
|||||||
@@ -80,51 +80,11 @@ class VerifyAppOperation: ResultOperation<Void>
|
|||||||
Task<Void, Never> {
|
Task<Void, Never> {
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
do
|
guard let ipaURL = self.context.ipaURL else { throw OperationError.appNotFound(name: app.name) }
|
||||||
{
|
|
||||||
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.verifyHash(of: app, at: ipaURL, matches: appVersion)
|
try await self.verifyPermissions(of: app, match: 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.finish(.success(()))
|
self.finish(.success(()))
|
||||||
}
|
}
|
||||||
@@ -175,26 +135,45 @@ private extension VerifyAppOperation
|
|||||||
guard let storeApp = await $appVersion.app else { throw OperationError.invalidParameters }
|
guard let storeApp = await $appVersion.app else { throw OperationError.invalidParameters }
|
||||||
|
|
||||||
// Verify source permissions match first.
|
// 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.
|
guard #available(iOS 15, *) else {
|
||||||
// switch self.permissionsMode
|
// Only review downloaded app permissions on iOS 15 and above.
|
||||||
// {
|
return
|
||||||
// case .none, .all: break
|
}
|
||||||
// case .added:
|
|
||||||
// let installedAppURL = InstalledApp.fileURL(for: app)
|
switch self.permissionsMode
|
||||||
// guard let previousApp = ALTApplication(fileURL: installedAppURL) else { throw OperationError.appNotFound(name: app.name) }
|
{
|
||||||
//
|
case .none: break
|
||||||
// var previousEntitlements = Set(previousApp.entitlements.keys)
|
case .all:
|
||||||
// for appExtension in previousApp.appExtensions
|
guard let presentingViewController = self.context.presentingViewController else { break } // Don't fail just because we can't show permissions.
|
||||||
// {
|
|
||||||
// previousEntitlements.formUnion(appExtension.entitlements.keys)
|
let allEntitlements = allPermissions.compactMap { $0 as? ALTEntitlement }
|
||||||
// }
|
if !allEntitlements.isEmpty
|
||||||
//
|
{
|
||||||
// // Make sure all entitlements already exist in previousApp.
|
try await self.review(allEntitlements, for: app, mode: .all, presentingViewController: presentingViewController)
|
||||||
// let addedEntitlements = Array(allPermissions.lazy.compactMap { $0 as? ALTEntitlement }.filter { !previousEntitlements.contains($0) })
|
}
|
||||||
// guard addedEntitlements.isEmpty else { throw VerificationError.addedPermissions(addedEntitlements, appVersion: appVersion) }
|
|
||||||
// }
|
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
|
@discardableResult
|
||||||
@@ -263,11 +242,70 @@ private extension VerifyAppOperation
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
guard missingPermissions.isEmpty else {
|
do
|
||||||
// There is at least one undeclared permission, so throw error.
|
{
|
||||||
throw VerificationError.undeclaredPermissions(missingPermissions, app: app)
|
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
|
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"
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "103",
|
||||||
|
"green" : "82",
|
||||||
|
"red" : "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -5,9 +5,27 @@
|
|||||||
"color-space" : "display-p3",
|
"color-space" : "display-p3",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "176",
|
"blue" : "159",
|
||||||
"green" : "200",
|
"green" : "180",
|
||||||
"red" : "123"
|
"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"
|
"idiom" : "universal"
|
||||||
|
|||||||
Reference in New Issue
Block a user