mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 19:53:25 +01:00
Revises appPermissions JSON format
• Split into `entitlements` and `privacy` sections • `entitlements` is an array of entitlement keys • `privacy` is a dictionary mapping UsageDescription keys to their descriptions
This commit is contained in:
@@ -374,6 +374,7 @@
|
|||||||
D55467C52A8D72C300F4CE90 /* ActiveAppsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */; };
|
D55467C52A8D72C300F4CE90 /* ActiveAppsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */; };
|
||||||
D561B2EB28EF5A4F006752E4 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; };
|
D561B2EB28EF5A4F006752E4 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; };
|
||||||
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, ); }; };
|
||||||
|
D56915062AD5D75B00A2B747 /* Regex+Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56915052AD5D75B00A2B747 /* Regex+Permissions.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 */; };
|
||||||
@@ -1035,6 +1036,7 @@
|
|||||||
D552B1D72A042A740066216F /* AppPermissionsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermissionsCard.swift; sourceTree = "<group>"; };
|
D552B1D72A042A740066216F /* AppPermissionsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermissionsCard.swift; sourceTree = "<group>"; };
|
||||||
D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = "<group>"; };
|
D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = "<group>"; };
|
||||||
D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsWidget.swift; sourceTree = "<group>"; };
|
D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsWidget.swift; sourceTree = "<group>"; };
|
||||||
|
D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Regex+Permissions.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>"; };
|
||||||
@@ -2013,6 +2015,7 @@
|
|||||||
D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */,
|
D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */,
|
||||||
D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */,
|
D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */,
|
||||||
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */,
|
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */,
|
||||||
|
D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3177,6 +3180,8 @@
|
|||||||
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
|
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
|
||||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
||||||
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
|
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
|
||||||
|
D56915062AD5D75B00A2B747 /* Regex+Permissions.swift in Sources */,
|
||||||
|
D5935AED29C39DE300C157EF /* SourceDetailsComponents.swift in Sources */,
|
||||||
D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */,
|
D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */,
|
||||||
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
|
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
|
||||||
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
||||||
|
|||||||
31
AltStore/Extensions/Regex+Permissions.swift
Normal file
31
AltStore/Extensions/Regex+Permissions.swift
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// Regex+Permissions.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 10/10/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import RegexBuilder
|
||||||
|
|
||||||
|
@available(iOS 16, *)
|
||||||
|
extension Regex where Output == (Substring, Substring)
|
||||||
|
{
|
||||||
|
static var privacyPermission: some RegexComponent<(Substring, Substring)> {
|
||||||
|
Regex {
|
||||||
|
Optionally {
|
||||||
|
"NS"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture permission "name"
|
||||||
|
Capture {
|
||||||
|
OneOrMore(.anyGraphemeCluster)
|
||||||
|
}
|
||||||
|
|
||||||
|
"UsageDescription"
|
||||||
|
|
||||||
|
// Optional suffix
|
||||||
|
Optionally(OneOrMore(.anyGraphemeCluster))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -294,41 +294,23 @@ private extension VerifyAppOperation
|
|||||||
|
|
||||||
|
|
||||||
// Privacy
|
// Privacy
|
||||||
let allPrivacyPermissions: Set<ALTAppPrivacyPermission>
|
let allPrivacyPermissions = ([app] + app.appExtensions).flatMap { (app) in
|
||||||
if #available(iOS 16, *)
|
let permissions = app.bundle.infoDictionary?.keys.compactMap { key -> ALTAppPrivacyPermission? in
|
||||||
{
|
if #available(iOS 16, *)
|
||||||
let regex = Regex {
|
{
|
||||||
"NS"
|
guard key.wholeMatch(of: Regex.privacyPermission) != nil else { return nil }
|
||||||
|
}
|
||||||
// Capture permission "name"
|
else
|
||||||
Capture {
|
{
|
||||||
OneOrMore(.anyGraphemeCluster)
|
guard key.contains("UsageDescription") else { return nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
"UsageDescription"
|
let permission = ALTAppPrivacyPermission(rawValue: key)
|
||||||
|
return permission
|
||||||
|
} ?? []
|
||||||
|
|
||||||
// Optional suffix
|
return permissions
|
||||||
Optionally(OneOrMore(.anyGraphemeCluster))
|
|
||||||
}
|
|
||||||
|
|
||||||
let privacyPermissions = ([app] + app.appExtensions).flatMap { (app) in
|
|
||||||
let permissions = app.bundle.infoDictionary?.keys.compactMap { key -> ALTAppPrivacyPermission? in
|
|
||||||
guard let match = key.wholeMatch(of: regex) else { return nil }
|
|
||||||
|
|
||||||
let permission = ALTAppPrivacyPermission(rawValue: String(match.1))
|
|
||||||
return permission
|
|
||||||
} ?? []
|
|
||||||
|
|
||||||
return permissions
|
|
||||||
}
|
|
||||||
|
|
||||||
allPrivacyPermissions = Set(privacyPermissions)
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
allPrivacyPermissions = []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Verify permissions.
|
// Verify permissions.
|
||||||
let sourcePermissions: Set<AnyHashable> = Set(await $storeApp.perform { $0.permissions.map { AnyHashable($0.permission) } })
|
let sourcePermissions: Set<AnyHashable> = Set(await $storeApp.perform { $0.permissions.map { AnyHashable($0.permission) } })
|
||||||
@@ -336,8 +318,37 @@ private extension VerifyAppOperation
|
|||||||
|
|
||||||
// To pass: EVERY permission in localPermissions must also appear in sourcePermissions.
|
// To pass: EVERY permission in localPermissions must also appear in sourcePermissions.
|
||||||
// If there is a single missing permission, throw error.
|
// If there is a single missing permission, throw error.
|
||||||
let missingPermissions: [any ALTAppPermission] = localPermissions.filter { !sourcePermissions.contains(AnyHashable($0)) }
|
let missingPermissions: [any ALTAppPermission] = localPermissions.filter { permission in
|
||||||
guard missingPermissions.isEmpty else { throw VerificationError.undeclaredPermissions(missingPermissions, app: app) }
|
if sourcePermissions.contains(AnyHashable(permission))
|
||||||
|
{
|
||||||
|
// `permission` exists in source, so return false.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
else if permission.type == .privacy
|
||||||
|
{
|
||||||
|
guard #available(iOS 16, *) else {
|
||||||
|
// Assume all privacy permissions _are_ included in source on pre-iOS 16 devices.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special-handling for legacy privacy permissions.
|
||||||
|
if let match = permission.rawValue.firstMatch(of: Regex.privacyPermission),
|
||||||
|
case let legacyPermission = ALTAppPrivacyPermission(rawValue: String(match.1)),
|
||||||
|
sourcePermissions.contains(AnyHashable(legacyPermission))
|
||||||
|
{
|
||||||
|
// The legacy name of this permission exists in the source, so return false.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source doesn't contain permission or its legacy name, so assume it is missing.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard missingPermissions.isEmpty else {
|
||||||
|
// There is at least one undeclared permission, so throw error.
|
||||||
|
throw VerificationError.undeclaredPermissions(missingPermissions, app: app)
|
||||||
|
}
|
||||||
|
|
||||||
return localPermissions
|
return localPermissions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public extension ALTAppPermissionType
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(AppPermission) @dynamicMemberLookup
|
@objc(AppPermission) @dynamicMemberLookup
|
||||||
public class AppPermission: NSManagedObject, Decodable, Fetchable
|
public class AppPermission: NSManagedObject, Fetchable
|
||||||
{
|
{
|
||||||
/* Properties */
|
/* Properties */
|
||||||
@NSManaged public var type: ALTAppPermissionType
|
@NSManaged public var type: ALTAppPermissionType
|
||||||
@@ -108,37 +108,13 @@ public class AppPermission: NSManagedObject, Decodable, Fetchable
|
|||||||
super.init(entity: entity, insertInto: context)
|
super.init(entity: entity, insertInto: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey
|
convenience init(permission: String, usageDescription: String?, type: ALTAppPermissionType, context: NSManagedObjectContext)
|
||||||
{
|
{
|
||||||
case name
|
self.init(entity: AppPermission.entity(), insertInto: context)
|
||||||
case usageDescription
|
|
||||||
}
|
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws
|
self._permission = permission
|
||||||
{
|
self.usageDescription = usageDescription
|
||||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
self.type = type
|
||||||
|
|
||||||
super.init(entity: AppPermission.entity(), insertInto: context)
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
self._permission = try container.decode(String.self, forKey: .name)
|
|
||||||
self.usageDescription = try container.decodeIfPresent(String.self, forKey: .usageDescription)
|
|
||||||
|
|
||||||
// Will be updated from StoreApp.
|
|
||||||
self.type = .unknown
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
if let context = self.managedObjectContext
|
|
||||||
{
|
|
||||||
context.delete(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,3 +136,113 @@ public extension AppPermission
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct AnyDecodable: Decodable
|
||||||
|
{
|
||||||
|
init(from decoder: Decoder) throws
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal struct AppPermissions: Decodable
|
||||||
|
{
|
||||||
|
var entitlements: [AppPermission] = []
|
||||||
|
var privacy: [AppPermission] = []
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey, Decodable
|
||||||
|
{
|
||||||
|
case entitlements
|
||||||
|
case privacy
|
||||||
|
|
||||||
|
// Legacy
|
||||||
|
case name
|
||||||
|
case usageDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws
|
||||||
|
{
|
||||||
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.entitlements = try self.parseEntitlements(from: container, into: context)
|
||||||
|
self.privacy = try self.parsePrivacyPermissions(from: container, into: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func parseEntitlements(from container: KeyedDecodingContainer<CodingKeys>, into context: NSManagedObjectContext) throws -> [AppPermission]
|
||||||
|
{
|
||||||
|
guard container.contains(.entitlements) else { return [] }
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Legacy
|
||||||
|
// Must parse as [String: String], NOT [CodingKeys: String], to avoid incorrect DecodingError.typeMismatch error.
|
||||||
|
let rawEntitlements = try container.decode([[String: String]].self, forKey: .entitlements)
|
||||||
|
|
||||||
|
let entitlements = try rawEntitlements.compactMap { (dictionary) -> AppPermission? in
|
||||||
|
guard let name = dictionary[CodingKeys.name.rawValue] else {
|
||||||
|
let context = DecodingError.Context(codingPath: container.codingPath, debugDescription: "Legacy entitlements must have `name` key.")
|
||||||
|
throw DecodingError.keyNotFound(CodingKeys.name, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
let entitlement = AppPermission(permission: name, usageDescription: nil, type: .entitlement, context: context)
|
||||||
|
return entitlement
|
||||||
|
}
|
||||||
|
|
||||||
|
return entitlements
|
||||||
|
}
|
||||||
|
catch DecodingError.typeMismatch
|
||||||
|
{
|
||||||
|
// Detailed
|
||||||
|
// AnyDecodable ensures we're forward-compatible with any values we may later require for entitlement permissions.
|
||||||
|
let rawEntitlements = try container.decode([String: AnyDecodable?].self, forKey: .entitlements)
|
||||||
|
|
||||||
|
let entitlements = rawEntitlements.map { AppPermission(permission: $0.key, usageDescription: nil, type: .entitlement, context: context) }
|
||||||
|
return entitlements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch DecodingError.typeMismatch
|
||||||
|
{
|
||||||
|
// Default
|
||||||
|
let rawEntitlements = try container.decode([String].self, forKey: .entitlements)
|
||||||
|
|
||||||
|
let entitlements = rawEntitlements.map { AppPermission(permission: $0, usageDescription: nil, type: .entitlement, context: context) }
|
||||||
|
return entitlements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func parsePrivacyPermissions(from container: KeyedDecodingContainer<CodingKeys>, into context: NSManagedObjectContext) throws -> [AppPermission]
|
||||||
|
{
|
||||||
|
guard container.contains(.privacy) else { return [] }
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Legacy
|
||||||
|
// Must parse as [String: String], NOT [CodingKeys: String], to avoid incorrect DecodingError.typeMismatch error.
|
||||||
|
let rawPermissions = try container.decode([[String: String]].self, forKey: .privacy)
|
||||||
|
|
||||||
|
let permissions = try rawPermissions.compactMap { (dictionary) -> AppPermission? in
|
||||||
|
guard let name = dictionary[CodingKeys.name.rawValue] else {
|
||||||
|
let context = DecodingError.Context(codingPath: container.codingPath, debugDescription: "Legacy privacy permissions must have `name` key.")
|
||||||
|
throw DecodingError.keyNotFound(CodingKeys.name, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
let usageDescription = dictionary[CodingKeys.usageDescription.rawValue]
|
||||||
|
|
||||||
|
let permission = AppPermission(permission: name, usageDescription: usageDescription, type: .privacy, context: context)
|
||||||
|
return permission
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
|
catch DecodingError.typeMismatch
|
||||||
|
{
|
||||||
|
// Default
|
||||||
|
let rawPermissions = try container.decode([String: String?].self, forKey: .privacy)
|
||||||
|
|
||||||
|
let permissions = rawPermissions.map { AppPermission(permission: $0, usageDescription: $1, type: .privacy, context: context) }
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ public extension StoreApp
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
static let dolphinAppID = "me.oatmealdome.dolphinios-njb"
|
static let dolphinAppID = "me.oatmealdome.dolphinios-njb"
|
||||||
|
|
||||||
private struct AppPermissions: Decodable
|
|
||||||
{
|
|
||||||
var entitlements: [AppPermission]?
|
|
||||||
var privacy: [AppPermission]?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
@@ -299,10 +293,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
|
|
||||||
if let appPermissions = try container.decodeIfPresent(AppPermissions.self, forKey: .permissions)
|
if let appPermissions = try container.decodeIfPresent(AppPermissions.self, forKey: .permissions)
|
||||||
{
|
{
|
||||||
appPermissions.entitlements?.forEach { $0.type = .entitlement }
|
let allPermissions = appPermissions.entitlements + appPermissions.privacy
|
||||||
appPermissions.privacy?.forEach { $0.type = .privacy }
|
|
||||||
|
|
||||||
let allPermissions = (appPermissions.entitlements ?? []) + (appPermissions.privacy ?? [])
|
|
||||||
for permission in allPermissions
|
for permission in allPermissions
|
||||||
{
|
{
|
||||||
permission.appBundleID = self.bundleIdentifier
|
permission.appBundleID = self.bundleIdentifier
|
||||||
|
|||||||
Reference in New Issue
Block a user