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:
Riley Testut
2023-12-07 17:15:08 -06:00
committed by Magesh K
parent d625b381d9
commit cf09843538
6 changed files with 558 additions and 71 deletions

View File

@@ -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 */,

View File

@@ -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)
}
}
}

View 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
}

View File

@@ -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
}
}

View File

@@ -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" : {

View File

@@ -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"