mirror of
https://github.com/SideStore/SideStore.git
synced 2026-04-10 04:35:41 +02:00
[AltStoreCore] Refactors core AltStore logic into AltStoreCore framework
AltStoreCore will contain all shared AltStore code between AltStore and any app extensions. Initially, it includes all AltStore model logic.
This commit is contained in:
@@ -2,14 +2,4 @@
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "ALTAppPermission.h"
|
||||
#import "ALTPatreonBenefitType.h"
|
||||
#import "ALTSourceUserInfoKey.h"
|
||||
|
||||
#import "NSAttributedString+Markdown.h"
|
||||
|
||||
// Shared
|
||||
#import "ALTConstants.h"
|
||||
#import "ALTConnection.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "CFNotificationName+AltStore.h"
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
import AppCenter
|
||||
import AppCenterAnalytics
|
||||
import AppCenterCrashes
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class PermissionPopoverViewController: UIViewController
|
||||
{
|
||||
var permission: AppPermission!
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
class AppIDsViewController: UICollectionViewController
|
||||
|
||||
@@ -10,6 +10,7 @@ import UIKit
|
||||
import UserNotifications
|
||||
import AVFoundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AltSign
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
class RefreshAltStoreViewController: UIViewController
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
class AppBannerView: RSTNibView
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
//
|
||||
// Keychain.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/4/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import KeychainAccess
|
||||
|
||||
import AltSign
|
||||
|
||||
@propertyWrapper
|
||||
struct KeychainItem<Value>
|
||||
{
|
||||
let key: String
|
||||
|
||||
var wrappedValue: Value? {
|
||||
get {
|
||||
switch Value.self
|
||||
{
|
||||
case is Data.Type: return try? Keychain.shared.keychain.getData(self.key) as? Value
|
||||
case is String.Type: return try? Keychain.shared.keychain.getString(self.key) as? Value
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
set {
|
||||
switch Value.self
|
||||
{
|
||||
case is Data.Type: Keychain.shared.keychain[data: self.key] = newValue as? Data
|
||||
case is String.Type: Keychain.shared.keychain[self.key] = newValue as? String
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(key: String)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
}
|
||||
|
||||
class Keychain
|
||||
{
|
||||
static let shared = Keychain()
|
||||
|
||||
fileprivate let keychain = KeychainAccess.Keychain(service: "com.rileytestut.AltStore").accessibility(.afterFirstUnlock).synchronizable(true)
|
||||
|
||||
@KeychainItem(key: "appleIDEmailAddress")
|
||||
var appleIDEmailAddress: String?
|
||||
|
||||
@KeychainItem(key: "appleIDPassword")
|
||||
var appleIDPassword: String?
|
||||
|
||||
@KeychainItem(key: "signingCertificatePrivateKey")
|
||||
var signingCertificatePrivateKey: Data?
|
||||
|
||||
@KeychainItem(key: "signingCertificateSerialNumber")
|
||||
var signingCertificateSerialNumber: String?
|
||||
|
||||
@KeychainItem(key: "signingCertificate")
|
||||
var signingCertificate: Data?
|
||||
|
||||
@KeychainItem(key: "signingCertificatePassword")
|
||||
var signingCertificatePassword: String?
|
||||
|
||||
@KeychainItem(key: "patreonAccessToken")
|
||||
var patreonAccessToken: String?
|
||||
|
||||
@KeychainItem(key: "patreonRefreshToken")
|
||||
var patreonRefreshToken: String?
|
||||
|
||||
@KeychainItem(key: "patreonCreatorAccessToken")
|
||||
var patreonCreatorAccessToken: String?
|
||||
|
||||
private init()
|
||||
{
|
||||
}
|
||||
|
||||
func reset()
|
||||
{
|
||||
self.appleIDEmailAddress = nil
|
||||
self.appleIDPassword = nil
|
||||
self.signingCertificatePrivateKey = nil
|
||||
self.signingCertificateSerialNumber = nil
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension TimeInterval
|
||||
{
|
||||
static let shortToastViewDuration = 4.0
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension FileManager
|
||||
{
|
||||
var altstoreSharedDirectory: URL? {
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
//
|
||||
// JSONDecoder+Properties.swift
|
||||
// Harmony
|
||||
//
|
||||
// Created by Riley Testut on 10/3/18.
|
||||
// Copyright © 2018 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
extension CodingUserInfoKey
|
||||
{
|
||||
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
|
||||
static let sourceURL = CodingUserInfoKey(rawValue: "sourceURL")!
|
||||
}
|
||||
|
||||
public final class JSONDecoder: Foundation.JSONDecoder
|
||||
{
|
||||
@DecoderItem(key: .managedObjectContext)
|
||||
var managedObjectContext: NSManagedObjectContext?
|
||||
|
||||
@DecoderItem(key: .sourceURL)
|
||||
var sourceURL: URL?
|
||||
}
|
||||
|
||||
extension Decoder
|
||||
{
|
||||
var managedObjectContext: NSManagedObjectContext? { self.userInfo[.managedObjectContext] as? NSManagedObjectContext }
|
||||
var sourceURL: URL? { self.userInfo[.sourceURL] as? URL }
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
struct DecoderItem<Value>
|
||||
{
|
||||
let key: CodingUserInfoKey
|
||||
|
||||
var wrappedValue: Value? {
|
||||
get { fatalError("only works on instance properties of classes") }
|
||||
set { fatalError("only works on instance properties of classes") }
|
||||
}
|
||||
|
||||
init(key: CodingUserInfoKey)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
|
||||
public static subscript<OuterSelf: JSONDecoder>(
|
||||
_enclosingInstance decoder: OuterSelf,
|
||||
wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value?>,
|
||||
storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
|
||||
) -> Value? {
|
||||
get {
|
||||
let wrapper = decoder[keyPath: storageKeyPath]
|
||||
|
||||
let value = decoder.userInfo[wrapper.key] as? Value
|
||||
return value
|
||||
}
|
||||
set {
|
||||
let wrapper = decoder[keyPath: storageKeyPath]
|
||||
decoder.userInfo[wrapper.key] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//
|
||||
// UIColor+Hex.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/15/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor
|
||||
{
|
||||
// Borrowed from https://stackoverflow.com/a/26341062
|
||||
var hexString: String {
|
||||
let components = self.cgColor.components
|
||||
let r: CGFloat = components?[0] ?? 0.0
|
||||
let g: CGFloat = components?[1] ?? 0.0
|
||||
let b: CGFloat = components?[2] ?? 0.0
|
||||
|
||||
let hexString = String.init(format: "%02lX%02lX%02lX", lroundf(Float(r * 255)), lroundf(Float(g * 255)), lroundf(Float(b * 255)))
|
||||
return hexString
|
||||
}
|
||||
|
||||
// Borrowed from https://stackoverflow.com/a/33397427
|
||||
convenience init?(hexString: String)
|
||||
{
|
||||
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int = UInt32()
|
||||
Scanner(string: hex).scanHexInt32(&int)
|
||||
let a, r, g, b: UInt32
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//
|
||||
// UserDefaults+AltStore.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/4/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Roxas
|
||||
|
||||
extension UserDefaults
|
||||
{
|
||||
@NSManaged var firstLaunch: Date?
|
||||
|
||||
@NSManaged var preferredServerID: String?
|
||||
|
||||
@NSManaged var isBackgroundRefreshEnabled: Bool
|
||||
@NSManaged var isDebugModeEnabled: Bool
|
||||
@NSManaged var presentedLaunchReminderNotification: Bool
|
||||
|
||||
@NSManaged var legacySideloadedApps: [String]?
|
||||
|
||||
@NSManaged var isLegacyDeactivationSupported: Bool
|
||||
@NSManaged var activeAppLimitIncludesExtensions: Bool
|
||||
|
||||
var activeAppsLimit: Int? {
|
||||
get {
|
||||
return self._activeAppsLimit?.intValue
|
||||
}
|
||||
set {
|
||||
if let value = newValue
|
||||
{
|
||||
self._activeAppsLimit = NSNumber(value: value)
|
||||
}
|
||||
else
|
||||
{
|
||||
self._activeAppsLimit = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@NSManaged @objc(activeAppsLimit) private var _activeAppsLimit: NSNumber?
|
||||
|
||||
func registerDefaults()
|
||||
{
|
||||
let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0)
|
||||
let isLegacyDeactivationSupported = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5)
|
||||
let activeAppLimitIncludesExtensions = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5)
|
||||
|
||||
self.register(defaults: [
|
||||
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
|
||||
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
|
||||
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
import UIKit
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class LaunchViewController: RSTLaunchViewController
|
||||
{
|
||||
private var didFinishLaunching = false
|
||||
|
||||
@@ -11,8 +11,8 @@ import UIKit
|
||||
import UserNotifications
|
||||
import MobileCoreServices
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
import Roxas
|
||||
|
||||
extension AppManager
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
struct FetchSourcesError: LocalizedError, CustomNSError
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
//
|
||||
// Account.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/5/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(Account)
|
||||
class Account: NSManagedObject, Fetchable
|
||||
{
|
||||
var localizedName: String {
|
||||
var components = PersonNameComponents()
|
||||
components.givenName = self.firstName
|
||||
components.familyName = self.lastName
|
||||
|
||||
let name = PersonNameComponentsFormatter.localizedString(from: components, style: .default)
|
||||
return name
|
||||
}
|
||||
|
||||
/* Properties */
|
||||
@NSManaged var appleID: String
|
||||
@NSManaged var identifier: String
|
||||
|
||||
@NSManaged var firstName: String
|
||||
@NSManaged var lastName: String
|
||||
|
||||
@NSManaged var isActiveAccount: Bool
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var teams: Set<Team>
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(_ account: ALTAccount, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: Account.entity(), insertInto: context)
|
||||
|
||||
self.update(account: account)
|
||||
}
|
||||
|
||||
func update(account: ALTAccount)
|
||||
{
|
||||
self.appleID = account.appleID
|
||||
self.identifier = account.identifier
|
||||
|
||||
self.firstName = account.firstName
|
||||
self.lastName = account.lastName
|
||||
}
|
||||
}
|
||||
|
||||
extension Account
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Account>
|
||||
{
|
||||
return NSFetchRequest<Account>(entityName: "Account")
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>AltStore 7.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,137 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14492.1" systemVersion="18G95" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String" syncable="YES"/>
|
||||
<attribute name="firstName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="lastName" attributeType="String" syncable="YES"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="type" attributeType="String" syncable="YES"/>
|
||||
<attribute name="usageDescription" attributeType="String" syncable="YES"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="caption" attributeType="String" syncable="YES"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
|
||||
<attribute name="title" attributeType="String" syncable="YES"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source" syncable="YES"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="sourceURL" attributeType="URI" syncable="YES"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp" syncable="YES"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="developerName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="iconURL" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable" syncable="YES"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
|
||||
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem" syncable="YES"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
|
||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,152 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="193"/>
|
||||
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="133"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,167 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="193"/>
|
||||
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,169 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16117.1" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
|
||||
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,173 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16117.1" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
|
||||
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
|
||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="118"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,174 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19G73" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
|
||||
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
|
||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="133"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,102 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.99" systemVersion="18F203" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String" syncable="YES"/>
|
||||
<attribute name="firstName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="lastName" attributeType="String" syncable="YES"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="type" attributeType="String" syncable="YES"/>
|
||||
<attribute name="usageDescription" attributeType="String" syncable="YES"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="sourceURL" attributeType="URI" syncable="YES"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="developerName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="iconName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="screenshotNames" attributeType="Transformable" syncable="YES"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
|
||||
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="300"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="105"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,52 +0,0 @@
|
||||
//
|
||||
// AppID.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 1/27/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(AppID)
|
||||
class AppID: NSManagedObject, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var name: String
|
||||
@NSManaged var identifier: String
|
||||
@NSManaged var bundleIdentifier: String
|
||||
@NSManaged var features: [ALTFeature: Any]
|
||||
@NSManaged var expirationDate: Date?
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged private(set) var team: Team?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(_ appID: ALTAppID, team: Team, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: AppID.entity(), insertInto: context)
|
||||
|
||||
self.name = appID.name
|
||||
self.identifier = appID.identifier
|
||||
self.bundleIdentifier = appID.bundleIdentifier
|
||||
self.features = appID.features
|
||||
self.expirationDate = appID.expirationDate
|
||||
|
||||
self.team = team
|
||||
}
|
||||
}
|
||||
|
||||
extension AppID
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<AppID>
|
||||
{
|
||||
return NSFetchRequest<AppID>(entityName: "AppID")
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
//
|
||||
// AppPermission.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import UIKit
|
||||
|
||||
extension ALTAppPermissionType
|
||||
{
|
||||
var localizedShortName: String? {
|
||||
switch self
|
||||
{
|
||||
case .photos: return NSLocalizedString("Photos", comment: "")
|
||||
case .backgroundAudio: return NSLocalizedString("Audio (BG)", comment: "")
|
||||
case .backgroundFetch: return NSLocalizedString("Fetch (BG)", comment: "")
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var localizedName: String? {
|
||||
switch self
|
||||
{
|
||||
case .photos: return NSLocalizedString("Photos", comment: "")
|
||||
case .backgroundAudio: return NSLocalizedString("Background Audio", comment: "")
|
||||
case .backgroundFetch: return NSLocalizedString("Background Fetch", comment: "")
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var icon: UIImage? {
|
||||
switch self
|
||||
{
|
||||
case .photos: return UIImage(named: "PhotosPermission")
|
||||
case .backgroundAudio: return UIImage(named: "BackgroundAudioPermission")
|
||||
case .backgroundFetch: return UIImage(named: "BackgroundFetchPermission")
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(AppPermission)
|
||||
class AppPermission: NSManagedObject, Decodable, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var type: ALTAppPermissionType
|
||||
@NSManaged var usageDescription: String
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged private(set) var app: StoreApp!
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case type
|
||||
case usageDescription
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
super.init(entity: AppPermission.entity(), insertInto: context)
|
||||
|
||||
do
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.usageDescription = try container.decode(String.self, forKey: .usageDescription)
|
||||
|
||||
let rawType = try container.decode(String.self, forKey: .type)
|
||||
self.type = ALTAppPermissionType(rawValue: rawType)
|
||||
}
|
||||
catch
|
||||
{
|
||||
if let context = self.managedObjectContext
|
||||
{
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppPermission
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<AppPermission>
|
||||
{
|
||||
return NSFetchRequest<AppPermission>(entityName: "AppPermission")
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
//
|
||||
// DatabaseManager.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
public class DatabaseManager
|
||||
{
|
||||
public static let shared = DatabaseManager()
|
||||
|
||||
public let persistentContainer: RSTPersistentContainer
|
||||
|
||||
public private(set) var isStarted = false
|
||||
|
||||
private var startCompletionHandlers = [(Error?) -> Void]()
|
||||
|
||||
private init()
|
||||
{
|
||||
self.persistentContainer = RSTPersistentContainer(name: "AltStore")
|
||||
self.persistentContainer.preferredMergePolicy = MergePolicy()
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
func start(completionHandler: @escaping (Error?) -> Void)
|
||||
{
|
||||
self.startCompletionHandlers.append(completionHandler)
|
||||
|
||||
guard self.startCompletionHandlers.count == 1 else { return }
|
||||
|
||||
func finish(_ error: Error?)
|
||||
{
|
||||
self.startCompletionHandlers.forEach { $0(error) }
|
||||
self.startCompletionHandlers.removeAll()
|
||||
}
|
||||
|
||||
guard !self.isStarted else { return finish(nil) }
|
||||
|
||||
self.persistentContainer.loadPersistentStores { (description, error) in
|
||||
guard error == nil else { return finish(error!) }
|
||||
|
||||
self.prepareDatabase() { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
finish(error)
|
||||
|
||||
case .success:
|
||||
self.isStarted = true
|
||||
finish(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func signOut(completionHandler: @escaping (Error?) -> Void)
|
||||
{
|
||||
self.persistentContainer.performBackgroundTask { (context) in
|
||||
if let account = self.activeAccount(in: context)
|
||||
{
|
||||
account.isActiveAccount = false
|
||||
}
|
||||
|
||||
if let team = self.activeTeam(in: context)
|
||||
{
|
||||
team.isActiveTeam = false
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
|
||||
Keychain.shared.reset()
|
||||
|
||||
completionHandler(nil)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to save when signing out.", error)
|
||||
completionHandler(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatabaseManager
|
||||
{
|
||||
var viewContext: NSManagedObjectContext {
|
||||
return self.persistentContainer.viewContext
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseManager
|
||||
{
|
||||
func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account?
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount))
|
||||
|
||||
let activeAccount = Account.first(satisfying: predicate, in: context)
|
||||
return activeAccount
|
||||
}
|
||||
|
||||
func activeTeam(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Team?
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
|
||||
|
||||
let activeTeam = Team.first(satisfying: predicate, in: context)
|
||||
return activeTeam
|
||||
}
|
||||
|
||||
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount?
|
||||
{
|
||||
let patronAccount = PatreonAccount.first(in: context)
|
||||
return patronAccount
|
||||
}
|
||||
}
|
||||
|
||||
private extension DatabaseManager
|
||||
{
|
||||
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let context = self.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return }
|
||||
|
||||
let altStoreSource: Source
|
||||
|
||||
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
|
||||
{
|
||||
altStoreSource = source
|
||||
}
|
||||
else
|
||||
{
|
||||
altStoreSource = Source.makeAltStoreSource(in: context)
|
||||
}
|
||||
|
||||
// Make sure to always update source URL to be current.
|
||||
altStoreSource.sourceURL = Source.altStoreSourceURL
|
||||
|
||||
let storeApp: StoreApp
|
||||
|
||||
if let app = StoreApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: context)
|
||||
{
|
||||
storeApp = app
|
||||
}
|
||||
else
|
||||
{
|
||||
storeApp = StoreApp.makeAltStoreApp(in: context)
|
||||
storeApp.version = localApp.version
|
||||
storeApp.source = altStoreSource
|
||||
}
|
||||
|
||||
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
|
||||
let installedApp: InstalledApp
|
||||
|
||||
if let app = storeApp.installedApp
|
||||
{
|
||||
installedApp = app
|
||||
}
|
||||
else
|
||||
{
|
||||
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context)
|
||||
installedApp.storeApp = storeApp
|
||||
}
|
||||
|
||||
/* App Extensions */
|
||||
var installedExtensions = Set<InstalledExtension>()
|
||||
|
||||
for appExtension in localApp.appExtensions
|
||||
{
|
||||
let resignedBundleID = appExtension.bundleIdentifier
|
||||
let originalBundleID = resignedBundleID.replacingOccurrences(of: localApp.bundleIdentifier, with: StoreApp.altstoreAppID)
|
||||
|
||||
let installedExtension: InstalledExtension
|
||||
|
||||
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
|
||||
{
|
||||
installedExtension = appExtension
|
||||
}
|
||||
else
|
||||
{
|
||||
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: context)
|
||||
}
|
||||
|
||||
installedExtension.update(resignedAppExtension: appExtension)
|
||||
|
||||
installedExtensions.insert(installedExtension)
|
||||
}
|
||||
|
||||
installedApp.appExtensions = installedExtensions
|
||||
|
||||
let fileURL = installedApp.fileURL
|
||||
|
||||
#if DEBUG
|
||||
let replaceCachedApp = true
|
||||
#else
|
||||
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version
|
||||
#endif
|
||||
|
||||
if replaceCachedApp
|
||||
{
|
||||
func update(_ bundle: Bundle, bundleID: String) throws
|
||||
{
|
||||
let infoPlistURL = bundle.bundleURL.appendingPathComponent("Info.plist")
|
||||
|
||||
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
|
||||
infoDictionary[kCFBundleIdentifierKey as String] = bundleID
|
||||
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
|
||||
}
|
||||
|
||||
FileManager.default.prepareTemporaryURL() { (temporaryFileURL) in
|
||||
do
|
||||
{
|
||||
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: temporaryFileURL)
|
||||
|
||||
guard let appBundle = Bundle(url: temporaryFileURL) else { throw ALTError(.invalidApp) }
|
||||
try update(appBundle, bundleID: StoreApp.altstoreAppID)
|
||||
|
||||
if let tempApp = ALTApplication(fileURL: temporaryFileURL)
|
||||
{
|
||||
for appExtension in tempApp.appExtensions
|
||||
{
|
||||
guard let extensionBundle = Bundle(url: appExtension.fileURL) else { throw ALTError(.invalidApp) }
|
||||
guard let installedExtension = installedExtensions.first(where: { $0.resignedBundleIdentifier == appExtension.bundleIdentifier }) else { throw ALTError(.invalidApp) }
|
||||
try update(extensionBundle, bundleID: installedExtension.bundleIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(at: temporaryFileURL, to: fileURL, shouldReplace: true)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to copy AltStore app bundle to its proper location.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cachedRefreshedDate = installedApp.refreshedDate
|
||||
let cachedExpirationDate = installedApp.expirationDate
|
||||
|
||||
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
|
||||
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber)
|
||||
|
||||
if installedApp.refreshedDate < cachedRefreshedDate
|
||||
{
|
||||
// Embedded provisioning profile has a creation date older than our refreshed date.
|
||||
// This most likely means we've refreshed the app since then, and profile is now outdated,
|
||||
// so use cached dates instead (i.e. not the dates updated from provisioning profile).
|
||||
|
||||
installedApp.refreshedDate = cachedRefreshedDate
|
||||
installedApp.expirationDate = cachedExpirationDate
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
//
|
||||
// InstalledApp.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
|
||||
let ALTActiveAppsLimit = 3
|
||||
|
||||
protocol InstalledAppProtocol: Fetchable
|
||||
{
|
||||
var name: String { get }
|
||||
var bundleIdentifier: String { get }
|
||||
var resignedBundleIdentifier: String { get }
|
||||
var version: String { get }
|
||||
|
||||
var refreshedDate: Date { get }
|
||||
var expirationDate: Date { get }
|
||||
var installedDate: Date { get }
|
||||
}
|
||||
|
||||
@objc(InstalledApp)
|
||||
class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var name: String
|
||||
@NSManaged var bundleIdentifier: String
|
||||
@NSManaged var resignedBundleIdentifier: String
|
||||
@NSManaged var version: String
|
||||
|
||||
@NSManaged var refreshedDate: Date
|
||||
@NSManaged var expirationDate: Date
|
||||
@NSManaged var installedDate: Date
|
||||
|
||||
@NSManaged var isActive: Bool
|
||||
|
||||
@NSManaged var certificateSerialNumber: String?
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var storeApp: StoreApp?
|
||||
@NSManaged var team: Team?
|
||||
@NSManaged var appExtensions: Set<InstalledExtension>
|
||||
|
||||
var isSideloaded: Bool {
|
||||
return self.storeApp == nil
|
||||
}
|
||||
|
||||
var appIDCount: Int {
|
||||
return 1 + self.appExtensions.count
|
||||
}
|
||||
|
||||
var requiredActiveSlots: Int {
|
||||
let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? self.appIDCount : 1
|
||||
return requiredActiveSlots
|
||||
}
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: InstalledApp.entity(), insertInto: context)
|
||||
|
||||
self.bundleIdentifier = originalBundleIdentifier
|
||||
|
||||
self.refreshedDate = Date()
|
||||
self.installedDate = Date()
|
||||
|
||||
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
|
||||
|
||||
self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber)
|
||||
}
|
||||
|
||||
func update(resignedApp: ALTApplication, certificateSerialNumber: String?)
|
||||
{
|
||||
self.name = resignedApp.name
|
||||
|
||||
self.resignedBundleIdentifier = resignedApp.bundleIdentifier
|
||||
self.version = resignedApp.version
|
||||
|
||||
self.certificateSerialNumber = certificateSerialNumber
|
||||
|
||||
if let provisioningProfile = resignedApp.provisioningProfile
|
||||
{
|
||||
self.update(provisioningProfile: provisioningProfile)
|
||||
}
|
||||
}
|
||||
|
||||
func update(provisioningProfile: ALTProvisioningProfile)
|
||||
{
|
||||
self.refreshedDate = provisioningProfile.creationDate
|
||||
self.expirationDate = provisioningProfile.expirationDate
|
||||
}
|
||||
}
|
||||
|
||||
extension InstalledApp
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledApp>
|
||||
{
|
||||
return NSFetchRequest<InstalledApp>(entityName: "InstalledApp")
|
||||
}
|
||||
|
||||
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
|
||||
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.version))
|
||||
return fetchRequest
|
||||
}
|
||||
|
||||
class func activeAppsFetchRequest() -> NSFetchRequest<InstalledApp>
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive))
|
||||
return fetchRequest
|
||||
}
|
||||
|
||||
class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp?
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
|
||||
let altStore = InstalledApp.first(satisfying: predicate, in: context)
|
||||
return altStore
|
||||
}
|
||||
|
||||
class func fetchActiveApps(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||
{
|
||||
let activeApps = InstalledApp.fetch(InstalledApp.activeAppsFetchRequest(), in: context)
|
||||
return activeApps
|
||||
}
|
||||
|
||||
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||
{
|
||||
var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
|
||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||
{
|
||||
// No additional predicate
|
||||
}
|
||||
else
|
||||
{
|
||||
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
|
||||
NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
|
||||
}
|
||||
|
||||
var installedApps = InstalledApp.all(satisfying: predicate,
|
||||
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
||||
in: context)
|
||||
|
||||
if let altStoreApp = InstalledApp.fetchAltStore(in: context)
|
||||
{
|
||||
// Refresh AltStore last since it causes app to quit.
|
||||
installedApps.append(altStoreApp)
|
||||
}
|
||||
|
||||
return installedApps
|
||||
}
|
||||
|
||||
class func fetchAppsForBackgroundRefresh(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||
{
|
||||
// Date 6 hours before now.
|
||||
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
||||
|
||||
var predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
|
||||
#keyPath(InstalledApp.isActive),
|
||||
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
||||
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
|
||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||
{
|
||||
// No additional predicate
|
||||
}
|
||||
else
|
||||
{
|
||||
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
|
||||
NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
|
||||
}
|
||||
|
||||
var installedApps = InstalledApp.all(satisfying: predicate,
|
||||
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
||||
in: context)
|
||||
|
||||
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date
|
||||
{
|
||||
// Refresh AltStore last since it may cause app to quit.
|
||||
installedApps.append(altStoreApp)
|
||||
}
|
||||
|
||||
return installedApps
|
||||
}
|
||||
}
|
||||
|
||||
extension InstalledApp
|
||||
{
|
||||
var openAppURL: URL {
|
||||
let openAppURL = URL(string: "altstore-" + self.bundleIdentifier + "://")!
|
||||
return openAppURL
|
||||
}
|
||||
|
||||
class func openAppURL(for app: AppProtocol) -> URL
|
||||
{
|
||||
let openAppURL = URL(string: "altstore-" + app.bundleIdentifier + "://")!
|
||||
return openAppURL
|
||||
}
|
||||
}
|
||||
|
||||
extension InstalledApp
|
||||
{
|
||||
class var appsDirectoryURL: URL {
|
||||
let appsDirectoryURL = FileManager.default.applicationSupportDirectory.appendingPathComponent("Apps")
|
||||
|
||||
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
|
||||
catch { print(error) }
|
||||
|
||||
return appsDirectoryURL
|
||||
}
|
||||
|
||||
class func fileURL(for app: AppProtocol) -> URL
|
||||
{
|
||||
let appURL = self.directoryURL(for: app).appendingPathComponent("App.app")
|
||||
return appURL
|
||||
}
|
||||
|
||||
class func refreshedIPAURL(for app: AppProtocol) -> URL
|
||||
{
|
||||
let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
|
||||
return ipaURL
|
||||
}
|
||||
|
||||
class func directoryURL(for app: AppProtocol) -> URL
|
||||
{
|
||||
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.bundleIdentifier)
|
||||
|
||||
do { try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) }
|
||||
catch { print(error) }
|
||||
|
||||
return directoryURL
|
||||
}
|
||||
|
||||
class func installedAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
|
||||
{
|
||||
let installedAppUTI = "io.altstore.Installed." + bundleIdentifier
|
||||
return installedAppUTI
|
||||
}
|
||||
|
||||
class func installedBackupAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
|
||||
{
|
||||
let installedBackupAppUTI = InstalledApp.installedAppUTI(forBundleIdentifier: bundleIdentifier) + ".backup"
|
||||
return installedBackupAppUTI
|
||||
}
|
||||
|
||||
var directoryURL: URL {
|
||||
return InstalledApp.directoryURL(for: self)
|
||||
}
|
||||
|
||||
var fileURL: URL {
|
||||
return InstalledApp.fileURL(for: self)
|
||||
}
|
||||
|
||||
var refreshedIPAURL: URL {
|
||||
return InstalledApp.refreshedIPAURL(for: self)
|
||||
}
|
||||
|
||||
var installedAppUTI: String {
|
||||
return InstalledApp.installedAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
||||
}
|
||||
|
||||
var installedBackupAppUTI: String {
|
||||
return InstalledApp.installedBackupAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
//
|
||||
// InstalledExtension.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 1/7/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(InstalledExtension)
|
||||
class InstalledExtension: NSManagedObject, InstalledAppProtocol
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var name: String
|
||||
@NSManaged var bundleIdentifier: String
|
||||
@NSManaged var resignedBundleIdentifier: String
|
||||
@NSManaged var version: String
|
||||
|
||||
@NSManaged var refreshedDate: Date
|
||||
@NSManaged var expirationDate: Date
|
||||
@NSManaged var installedDate: Date
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var parentApp: InstalledApp?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(resignedAppExtension: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: InstalledExtension.entity(), insertInto: context)
|
||||
|
||||
self.bundleIdentifier = originalBundleIdentifier
|
||||
|
||||
self.refreshedDate = Date()
|
||||
self.installedDate = Date()
|
||||
|
||||
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
|
||||
|
||||
self.update(resignedAppExtension: resignedAppExtension)
|
||||
}
|
||||
|
||||
func update(resignedAppExtension: ALTApplication)
|
||||
{
|
||||
self.name = resignedAppExtension.name
|
||||
|
||||
self.resignedBundleIdentifier = resignedAppExtension.bundleIdentifier
|
||||
self.version = resignedAppExtension.version
|
||||
|
||||
if let provisioningProfile = resignedAppExtension.provisioningProfile
|
||||
{
|
||||
self.update(provisioningProfile: provisioningProfile)
|
||||
}
|
||||
}
|
||||
|
||||
func update(provisioningProfile: ALTProvisioningProfile)
|
||||
{
|
||||
self.refreshedDate = provisioningProfile.creationDate
|
||||
self.expirationDate = provisioningProfile.expirationDate
|
||||
}
|
||||
}
|
||||
|
||||
extension InstalledExtension
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledExtension>
|
||||
{
|
||||
return NSFetchRequest<InstalledExtension>(entityName: "InstalledExtension")
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
//
|
||||
// MergePolicy.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
import Roxas
|
||||
|
||||
open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
{
|
||||
open override func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws
|
||||
{
|
||||
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
|
||||
for conflict in conflicts
|
||||
{
|
||||
switch conflict.conflictingObjects.first
|
||||
{
|
||||
case is StoreApp where conflict.conflictingObjects.count == 2:
|
||||
// Modified cached StoreApp while replacing it with new one, causing context-level conflict.
|
||||
// Most likely, we set up a relationship between the new StoreApp and a NewsItem,
|
||||
// causing cached StoreApp to delete it's NewsItem relationship, resulting in (resolvable) conflict.
|
||||
|
||||
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp
|
||||
{
|
||||
// Delete previous permissions (same as below).
|
||||
for permission in previousApp.permissions
|
||||
{
|
||||
permission.managedObjectContext?.delete(permission)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown context-level conflict.
|
||||
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
|
||||
}
|
||||
}
|
||||
|
||||
try super.resolve(constraintConflicts: conflicts)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for conflict in conflicts
|
||||
{
|
||||
switch conflict.databaseObject
|
||||
{
|
||||
case let databaseObject as StoreApp:
|
||||
// Delete previous permissions
|
||||
for permission in databaseObject.permissions
|
||||
{
|
||||
permission.managedObjectContext?.delete(permission)
|
||||
}
|
||||
|
||||
case let databaseObject as Source:
|
||||
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
|
||||
|
||||
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
|
||||
let newsItemIdentifiers = Set(conflictedObject.newsItems.map { $0.identifier })
|
||||
|
||||
for app in databaseObject.apps
|
||||
{
|
||||
if !bundleIdentifiers.contains(app.bundleIdentifier)
|
||||
{
|
||||
// No longer listed in Source, so remove it from database.
|
||||
app.managedObjectContext?.delete(app)
|
||||
}
|
||||
}
|
||||
|
||||
for newsItem in databaseObject.newsItems
|
||||
{
|
||||
if !newsItemIdentifiers.contains(newsItem.identifier)
|
||||
{
|
||||
// No longer listed in Source, so remove it from database.
|
||||
newsItem.managedObjectContext?.delete(newsItem)
|
||||
}
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
try super.resolve(constraintConflicts: conflicts)
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// InstalledAppPolicy.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 1/24/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import AltSign
|
||||
|
||||
@objc(InstalledAppToInstalledAppMigrationPolicy)
|
||||
class InstalledAppToInstalledAppMigrationPolicy: NSEntityMigrationPolicy
|
||||
{
|
||||
override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
|
||||
{
|
||||
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
|
||||
|
||||
// Entity must be in manager.destinationContext.
|
||||
let entity = NSEntityDescription.entity(forEntityName: "Team", in: manager.destinationContext)
|
||||
|
||||
let fetchRequest = NSFetchRequest<NSManagedObject>()
|
||||
fetchRequest.entity = entity
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
|
||||
|
||||
let teams = try manager.destinationContext.fetch(fetchRequest)
|
||||
|
||||
// Cannot use NSManagedObject subclasses during migration, so fallback to using KVC instead.
|
||||
dInstance.setValue(teams.first, forKey: #keyPath(InstalledApp.team))
|
||||
}
|
||||
|
||||
@objc(defaultIsActiveForBundleID:team:)
|
||||
func defaultIsActive(for bundleID: String, team: NSManagedObject?) -> NSNumber
|
||||
{
|
||||
let isActive: Bool
|
||||
|
||||
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
||||
if !ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
||||
{
|
||||
isActive = true
|
||||
}
|
||||
else if let team = team, let type = team.value(forKey: #keyPath(Team.type)) as? Int16, type != ALTTeamType.free.rawValue
|
||||
{
|
||||
isActive = true
|
||||
}
|
||||
else
|
||||
{
|
||||
// AltStore should always be active, but deactivate all other apps.
|
||||
isActive = (bundleID == StoreApp.altstoreAppID)
|
||||
|
||||
// We can assume there is an active app limit,
|
||||
// but will confirm next time user authenticates.
|
||||
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
|
||||
}
|
||||
|
||||
return NSNumber(value: isActive)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// StoreAppPolicy.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/14/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
@objc(StoreAppToStoreAppMigrationPolicy)
|
||||
class StoreAppToStoreAppMigrationPolicy: NSEntityMigrationPolicy
|
||||
{
|
||||
@objc(migrateIconURL)
|
||||
func migrateIconURL() -> URL
|
||||
{
|
||||
return URL(string: "https://via.placeholder.com/150")!
|
||||
}
|
||||
|
||||
@objc(migrateScreenshotURLs)
|
||||
func migrateScreenshotURLs() -> NSCopying
|
||||
{
|
||||
return [] as NSArray
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
//
|
||||
// NewsItem.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/29/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
|
||||
@objc(NewsItem)
|
||||
class NewsItem: NSManagedObject, Decodable, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var identifier: String
|
||||
@NSManaged var date: Date
|
||||
|
||||
@NSManaged var title: String
|
||||
@NSManaged var caption: String
|
||||
@NSManaged var tintColor: UIColor
|
||||
@NSManaged var sortIndex: Int32
|
||||
@NSManaged var isSilent: Bool
|
||||
|
||||
@NSManaged var imageURL: URL?
|
||||
@NSManaged var externalURL: URL?
|
||||
|
||||
@NSManaged var appID: String?
|
||||
@NSManaged var sourceIdentifier: String?
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var storeApp: StoreApp?
|
||||
@NSManaged var source: Source?
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case identifier
|
||||
case date
|
||||
case title
|
||||
case caption
|
||||
case tintColor
|
||||
case imageURL
|
||||
case externalURL = "url"
|
||||
case appID
|
||||
case notify
|
||||
}
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
super.init(entity: NewsItem.entity(), insertInto: context)
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||
self.date = try container.decode(Date.self, forKey: .date)
|
||||
|
||||
self.title = try container.decode(String.self, forKey: .title)
|
||||
self.caption = try container.decode(String.self, forKey: .caption)
|
||||
|
||||
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
|
||||
{
|
||||
guard let tintColor = UIColor(hexString: tintColorHex) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
|
||||
}
|
||||
|
||||
self.tintColor = tintColor
|
||||
}
|
||||
|
||||
self.imageURL = try container.decodeIfPresent(URL.self, forKey: .imageURL)
|
||||
self.externalURL = try container.decodeIfPresent(URL.self, forKey: .externalURL)
|
||||
|
||||
self.appID = try container.decodeIfPresent(String.self, forKey: .appID)
|
||||
|
||||
let notify = try container.decodeIfPresent(Bool.self, forKey: .notify) ?? false
|
||||
self.isSilent = !notify
|
||||
}
|
||||
}
|
||||
|
||||
extension NewsItem
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<NewsItem>
|
||||
{
|
||||
return NSFetchRequest<NewsItem>(entityName: "NewsItem")
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
//
|
||||
// PatreonAccount.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct AccountResponse: Decodable
|
||||
{
|
||||
struct Data: Decodable
|
||||
{
|
||||
struct Attributes: Decodable
|
||||
{
|
||||
var first_name: String?
|
||||
var full_name: String
|
||||
}
|
||||
|
||||
var id: String
|
||||
var attributes: Attributes
|
||||
}
|
||||
|
||||
var data: Data
|
||||
var included: [PatronResponse]?
|
||||
}
|
||||
}
|
||||
|
||||
@objc(PatreonAccount)
|
||||
class PatreonAccount: NSManagedObject, Fetchable
|
||||
{
|
||||
@NSManaged var identifier: String
|
||||
|
||||
@NSManaged var name: String
|
||||
@NSManaged var firstName: String?
|
||||
|
||||
@NSManaged var isPatron: Bool
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(response: PatreonAPI.AccountResponse, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: PatreonAccount.entity(), insertInto: context)
|
||||
|
||||
self.identifier = response.data.id
|
||||
self.name = response.data.attributes.full_name
|
||||
self.firstName = response.data.attributes.first_name
|
||||
|
||||
if let patronResponse = response.included?.first
|
||||
{
|
||||
let patron = Patron(response: patronResponse)
|
||||
self.isPatron = (patron.status == .active)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.isPatron = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PatreonAccount
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<PatreonAccount>
|
||||
{
|
||||
return NSFetchRequest<PatreonAccount>(entityName: "PatreonAccount")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
//
|
||||
// RefreshAttempt.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
@objc(RefreshAttempt)
|
||||
class RefreshAttempt: NSManagedObject, Fetchable
|
||||
{
|
||||
@NSManaged var identifier: String
|
||||
@NSManaged var date: Date
|
||||
|
||||
@NSManaged var isSuccess: Bool
|
||||
@NSManaged var errorDescription: String?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(identifier: String, result: Result<[String: Result<InstalledApp, Error>], Error>, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: RefreshAttempt.entity(), insertInto: context)
|
||||
|
||||
self.identifier = identifier
|
||||
self.date = Date()
|
||||
|
||||
do
|
||||
{
|
||||
let results = try result.get()
|
||||
|
||||
for (_, result) in results
|
||||
{
|
||||
guard case let .failure(error) = result else { continue }
|
||||
throw error
|
||||
}
|
||||
|
||||
self.isSuccess = true
|
||||
self.errorDescription = nil
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.isSuccess = false
|
||||
self.errorDescription = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RefreshAttempt
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<RefreshAttempt>
|
||||
{
|
||||
return NSFetchRequest<RefreshAttempt>(entityName: "RefreshAttempt")
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// SecureValueTransformer.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/18/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc(ALTSecureValueTransformer)
|
||||
final class SecureValueTransformer: NSSecureUnarchiveFromDataTransformer
|
||||
{
|
||||
static let name = NSValueTransformerName(rawValue: "ALTSecureValueTransformer")
|
||||
|
||||
override static var allowedTopLevelClasses: [AnyClass] {
|
||||
let allowedClasses = super.allowedTopLevelClasses + [NSError.self]
|
||||
return allowedClasses
|
||||
}
|
||||
|
||||
public static func register()
|
||||
{
|
||||
let transformer = SecureValueTransformer()
|
||||
ValueTransformer.setValueTransformer(transformer, forName: name)
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
//
|
||||
// Source.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
extension Source
|
||||
{
|
||||
#if ALPHA
|
||||
static let altStoreIdentifier = "com.rileytestut.AltStore.Alpha"
|
||||
#else
|
||||
static let altStoreIdentifier = "com.rileytestut.AltStore"
|
||||
#endif
|
||||
|
||||
#if STAGING
|
||||
|
||||
#if ALPHA
|
||||
static let altStoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/sources/alpha/apps-alpha-staging.json")!
|
||||
#else
|
||||
static let altStoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/apps-staging.json")!
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#if ALPHA
|
||||
static let altStoreSourceURL = URL(string: "https://alpha.altstore.io/")!
|
||||
#else
|
||||
static let altStoreSourceURL = URL(string: "https://apps.altstore.io/")!
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@objc(Source)
|
||||
class Source: NSManagedObject, Fetchable, Decodable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var name: String
|
||||
@NSManaged var identifier: String
|
||||
@NSManaged var sourceURL: URL
|
||||
|
||||
@NSManaged var error: NSError?
|
||||
|
||||
/* Non-Core Data Properties */
|
||||
var userInfo: [ALTSourceUserInfoKey: String]?
|
||||
|
||||
/* Relationships */
|
||||
@objc(apps) @NSManaged private(set) var _apps: NSOrderedSet
|
||||
@objc(newsItems) @NSManaged private(set) var _newsItems: NSOrderedSet
|
||||
|
||||
@nonobjc var apps: [StoreApp] {
|
||||
get {
|
||||
return self._apps.array as! [StoreApp]
|
||||
}
|
||||
set {
|
||||
self._apps = NSOrderedSet(array: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@nonobjc var newsItems: [NewsItem] {
|
||||
get {
|
||||
return self._newsItems.array as! [NewsItem]
|
||||
}
|
||||
set {
|
||||
self._newsItems = NSOrderedSet(array: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case name
|
||||
case identifier
|
||||
case sourceURL
|
||||
case userInfo
|
||||
case apps
|
||||
case news
|
||||
}
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
guard let sourceURL = decoder.sourceURL else { preconditionFailure("Decoder must have non-nil sourceURL.") }
|
||||
|
||||
super.init(entity: Source.entity(), insertInto: context)
|
||||
|
||||
do
|
||||
{
|
||||
self.sourceURL = sourceURL
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||
|
||||
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
|
||||
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
|
||||
|
||||
let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? []
|
||||
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { (a, b) in return a })
|
||||
|
||||
for (index, app) in apps.enumerated()
|
||||
{
|
||||
app.sourceIdentifier = self.identifier
|
||||
app.sortIndex = Int32(index)
|
||||
}
|
||||
self._apps = NSMutableOrderedSet(array: apps)
|
||||
|
||||
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
|
||||
for (index, item) in newsItems.enumerated()
|
||||
{
|
||||
item.sourceIdentifier = self.identifier
|
||||
item.sortIndex = Int32(index)
|
||||
}
|
||||
|
||||
for newsItem in newsItems
|
||||
{
|
||||
guard let appID = newsItem.appID else { continue }
|
||||
|
||||
if let storeApp = appsByID[appID]
|
||||
{
|
||||
newsItem.storeApp = storeApp
|
||||
}
|
||||
else
|
||||
{
|
||||
newsItem.storeApp = nil
|
||||
}
|
||||
}
|
||||
self._newsItems = NSMutableOrderedSet(array: newsItems)
|
||||
}
|
||||
catch
|
||||
{
|
||||
if let context = self.managedObjectContext
|
||||
{
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Source
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Source>
|
||||
{
|
||||
return NSFetchRequest<Source>(entityName: "Source")
|
||||
}
|
||||
|
||||
class func makeAltStoreSource(in context: NSManagedObjectContext) -> Source
|
||||
{
|
||||
let source = Source(context: context)
|
||||
source.name = "AltStore"
|
||||
source.identifier = Source.altStoreIdentifier
|
||||
source.sourceURL = Source.altStoreSourceURL
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
class func fetchAltStoreSource(in context: NSManagedObjectContext) -> Source?
|
||||
{
|
||||
let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
|
||||
return source
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
//
|
||||
// StoreApp.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import Roxas
|
||||
import AltSign
|
||||
|
||||
extension StoreApp
|
||||
{
|
||||
#if ALPHA
|
||||
static let altstoreAppID = "com.rileytestut.AltStore.Alpha"
|
||||
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore", "com.rileytestut.AltStore.Beta"]
|
||||
#elseif BETA
|
||||
static let altstoreAppID = "com.rileytestut.AltStore.Beta"
|
||||
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore", "com.rileytestut.AltStore.Alpha"]
|
||||
#else
|
||||
static let altstoreAppID = "com.rileytestut.AltStore"
|
||||
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore.Beta", "com.rileytestut.AltStore.Alpha"]
|
||||
#endif
|
||||
|
||||
static let dolphinAppID = "me.oatmealdome.dolphinios-njb"
|
||||
}
|
||||
|
||||
@objc(StoreApp)
|
||||
class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged private(set) var name: String
|
||||
@NSManaged private(set) var bundleIdentifier: String
|
||||
@NSManaged private(set) var subtitle: String?
|
||||
|
||||
@NSManaged private(set) var developerName: String
|
||||
@NSManaged private(set) var localizedDescription: String
|
||||
@NSManaged private(set) var size: Int32
|
||||
|
||||
@NSManaged private(set) var iconURL: URL
|
||||
@NSManaged private(set) var screenshotURLs: [URL]
|
||||
|
||||
@NSManaged var version: String
|
||||
@NSManaged private(set) var versionDate: Date
|
||||
@NSManaged private(set) var versionDescription: String?
|
||||
|
||||
@NSManaged private(set) var downloadURL: URL
|
||||
@NSManaged private(set) var tintColor: UIColor?
|
||||
@NSManaged private(set) var isBeta: Bool
|
||||
|
||||
@NSManaged var sourceIdentifier: String?
|
||||
|
||||
@NSManaged var sortIndex: Int32
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var installedApp: InstalledApp?
|
||||
@NSManaged var newsItems: Set<NewsItem>
|
||||
|
||||
@NSManaged @objc(source) var _source: Source?
|
||||
@NSManaged @objc(permissions) var _permissions: NSOrderedSet
|
||||
|
||||
@nonobjc var source: Source? {
|
||||
set {
|
||||
self._source = newValue
|
||||
self.sourceIdentifier = newValue?.identifier
|
||||
}
|
||||
get {
|
||||
return self._source
|
||||
}
|
||||
}
|
||||
|
||||
@nonobjc var permissions: [AppPermission] {
|
||||
return self._permissions.array as! [AppPermission]
|
||||
}
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case name
|
||||
case bundleIdentifier
|
||||
case developerName
|
||||
case localizedDescription
|
||||
case version
|
||||
case versionDescription
|
||||
case versionDate
|
||||
case iconURL
|
||||
case screenshotURLs
|
||||
case downloadURL
|
||||
case tintColor
|
||||
case subtitle
|
||||
case permissions
|
||||
case size
|
||||
case isBeta = "beta"
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
// Must initialize with context in order for child context saves to work correctly.
|
||||
super.init(entity: StoreApp.entity(), insertInto: context)
|
||||
|
||||
do
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
self.bundleIdentifier = try container.decode(String.self, forKey: .bundleIdentifier)
|
||||
self.developerName = try container.decode(String.self, forKey: .developerName)
|
||||
self.localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
|
||||
|
||||
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
|
||||
|
||||
self.version = try container.decode(String.self, forKey: .version)
|
||||
self.versionDate = try container.decode(Date.self, forKey: .versionDate)
|
||||
self.versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
||||
|
||||
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
|
||||
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
|
||||
|
||||
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
|
||||
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
|
||||
{
|
||||
guard let tintColor = UIColor(hexString: tintColorHex) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
|
||||
}
|
||||
|
||||
self.tintColor = tintColor
|
||||
}
|
||||
|
||||
self.size = try container.decode(Int32.self, forKey: .size)
|
||||
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||
|
||||
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
|
||||
self._permissions = NSOrderedSet(array: permissions)
|
||||
}
|
||||
catch
|
||||
{
|
||||
if let context = self.managedObjectContext
|
||||
{
|
||||
context.delete(self)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreApp
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
|
||||
{
|
||||
return NSFetchRequest<StoreApp>(entityName: "StoreApp")
|
||||
}
|
||||
|
||||
class func makeAltStoreApp(in context: NSManagedObjectContext) -> StoreApp
|
||||
{
|
||||
let app = StoreApp(context: context)
|
||||
app.name = "AltStore"
|
||||
app.bundleIdentifier = StoreApp.altstoreAppID
|
||||
app.developerName = "Riley Testut"
|
||||
app.localizedDescription = "AltStore is an alternative App Store."
|
||||
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
|
||||
app.screenshotURLs = []
|
||||
app.version = "1.0"
|
||||
app.versionDate = Date()
|
||||
app.downloadURL = URL(string: "http://rileytestut.com")!
|
||||
|
||||
#if BETA
|
||||
app.isBeta = true
|
||||
#endif
|
||||
|
||||
return app
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
//
|
||||
// Team.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
extension ALTTeamType
|
||||
{
|
||||
var localizedDescription: String {
|
||||
switch self
|
||||
{
|
||||
case .free: return NSLocalizedString("Free Developer Account", comment: "")
|
||||
case .individual: return NSLocalizedString("Developer", comment: "")
|
||||
case .organization: return NSLocalizedString("Organization", comment: "")
|
||||
case .unknown: fallthrough
|
||||
@unknown default: return NSLocalizedString("Unknown", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Team
|
||||
{
|
||||
static let maximumFreeAppIDs = 10
|
||||
}
|
||||
|
||||
@objc(Team)
|
||||
class Team: NSManagedObject, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var name: String
|
||||
@NSManaged var identifier: String
|
||||
@NSManaged var type: ALTTeamType
|
||||
|
||||
@NSManaged var isActiveTeam: Bool
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged private(set) var account: Account!
|
||||
@NSManaged var installedApps: Set<InstalledApp>
|
||||
@NSManaged private(set) var appIDs: Set<AppID>
|
||||
|
||||
var altTeam: ALTTeam?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(_ team: ALTTeam, account: Account, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: Team.entity(), insertInto: context)
|
||||
|
||||
self.account = account
|
||||
|
||||
self.update(team: team)
|
||||
}
|
||||
|
||||
func update(team: ALTTeam)
|
||||
{
|
||||
self.altTeam = team
|
||||
|
||||
self.name = team.name
|
||||
self.identifier = team.identifier
|
||||
self.type = team.type
|
||||
}
|
||||
}
|
||||
|
||||
extension Team
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Team>
|
||||
{
|
||||
return NSFetchRequest<Team>(entityName: "Team")
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import UIKit
|
||||
import SafariServices
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import Roxas
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
enum AuthenticationError: LocalizedError
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
extension BackupAppOperation
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
@objc(DownloadAppOperation)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@objc(FetchProvisioningProfilesOperation)
|
||||
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
@objc(FetchSourceOperation)
|
||||
@@ -49,7 +50,7 @@ class FetchSourceOperation: ResultOperation<Source>
|
||||
{
|
||||
let (data, _) = try Result((data, response), error).get()
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
let decoder = AltStoreCore.JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
|
||||
let container = try decoder.singleValueContainer()
|
||||
let text = try container.decode(String.self)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import CoreData
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
class OperationContext
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
import Roxas
|
||||
|
||||
@objc(RefreshAppOperation)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
class RefreshGroup: NSObject
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
@objc(RemoveAppOperation)
|
||||
class RemoveAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
@objc(ResignAppOperation)
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
@objc(SendAppOperation)
|
||||
class SendAppOperation: ResultOperation<ServerConnection>
|
||||
{
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// Benefit.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/21/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct BenefitResponse: Decodable
|
||||
{
|
||||
var id: String
|
||||
}
|
||||
}
|
||||
|
||||
struct Benefit: Hashable
|
||||
{
|
||||
var type: ALTPatreonBenefitType
|
||||
|
||||
init(response: PatreonAPI.BenefitResponse)
|
||||
{
|
||||
self.type = ALTPatreonBenefitType(response.id)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// Campaign.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/21/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct CampaignResponse: Decodable
|
||||
{
|
||||
var id: String
|
||||
}
|
||||
}
|
||||
|
||||
struct Campaign
|
||||
{
|
||||
var identifier: String
|
||||
|
||||
init(response: PatreonAPI.CampaignResponse)
|
||||
{
|
||||
self.identifier = response.id
|
||||
}
|
||||
}
|
||||
@@ -1,419 +0,0 @@
|
||||
//
|
||||
// PatreonAPI.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/20/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AuthenticationServices
|
||||
import CoreData
|
||||
|
||||
private let clientID = "ZMx0EGUWe4TVWYXNZZwK_fbIK5jHFVWoUf1Qb-sqNXmT-YzAGwDPxxq7ak3_W5Q2"
|
||||
private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt"
|
||||
|
||||
private let campaignID = "2863968"
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
enum Error: LocalizedError
|
||||
{
|
||||
case unknown
|
||||
case notAuthenticated
|
||||
case invalidAccessToken
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .unknown: return NSLocalizedString("An unknown error occurred.", comment: "")
|
||||
case .notAuthenticated: return NSLocalizedString("No connected Patreon account.", comment: "")
|
||||
case .invalidAccessToken: return NSLocalizedString("Invalid access token.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AuthorizationType
|
||||
{
|
||||
case none
|
||||
case user
|
||||
case creator
|
||||
}
|
||||
|
||||
enum AnyResponse: Decodable
|
||||
{
|
||||
case tier(TierResponse)
|
||||
case benefit(BenefitResponse)
|
||||
|
||||
enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case type
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
switch type
|
||||
{
|
||||
case "tier":
|
||||
let tier = try TierResponse(from: decoder)
|
||||
self = .tier(tier)
|
||||
|
||||
case "benefit":
|
||||
let benefit = try BenefitResponse(from: decoder)
|
||||
self = .benefit(benefit)
|
||||
|
||||
default: throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unrecognized Patreon response type.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PatreonAPI: NSObject
|
||||
{
|
||||
static let shared = PatreonAPI()
|
||||
|
||||
var isAuthenticated: Bool {
|
||||
return Keychain.shared.patreonAccessToken != nil
|
||||
}
|
||||
|
||||
private var authenticationSession: ASWebAuthenticationSession?
|
||||
|
||||
private let session = URLSession(configuration: .ephemeral)
|
||||
private let baseURL = URL(string: "https://www.patreon.com/")!
|
||||
|
||||
private override init()
|
||||
{
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
func authenticate(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
|
||||
{
|
||||
var components = URLComponents(string: "/oauth2/authorize")!
|
||||
components.queryItems = [URLQueryItem(name: "response_type", value: "code"),
|
||||
URLQueryItem(name: "client_id", value: clientID),
|
||||
URLQueryItem(name: "redirect_uri", value: "https://rileytestut.com/patreon/altstore")]
|
||||
|
||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
||||
|
||||
self.authenticationSession = ASWebAuthenticationSession(url: requestURL, callbackURLScheme: "altstore") { (callbackURL, error) in
|
||||
do
|
||||
{
|
||||
let callbackURL = try Result(callbackURL, error).get()
|
||||
|
||||
guard
|
||||
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
|
||||
let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }),
|
||||
let code = codeQueryItem.value
|
||||
else { throw Error.unknown }
|
||||
|
||||
self.fetchAccessToken(oauthCode: code) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success((let accessToken, let refreshToken)):
|
||||
Keychain.shared.patreonAccessToken = accessToken
|
||||
Keychain.shared.patreonRefreshToken = refreshToken
|
||||
|
||||
self.fetchAccount(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 13.0, *)
|
||||
{
|
||||
self.authenticationSession?.presentationContextProvider = self
|
||||
}
|
||||
|
||||
self.authenticationSession?.start()
|
||||
}
|
||||
|
||||
func fetchAccount(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
|
||||
{
|
||||
var components = URLComponents(string: "/api/oauth2/v2/identity")!
|
||||
components.queryItems = [URLQueryItem(name: "include", value: "memberships"),
|
||||
URLQueryItem(name: "fields[user]", value: "first_name,full_name"),
|
||||
URLQueryItem(name: "fields[member]", value: "full_name,patron_status")]
|
||||
|
||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
||||
let request = URLRequest(url: requestURL)
|
||||
|
||||
self.send(request, authorizationType: .user) { (result: Result<AccountResponse, Swift.Error>) in
|
||||
switch result
|
||||
{
|
||||
case .failure(Error.notAuthenticated):
|
||||
self.signOut() { (result) in
|
||||
completion(.failure(Error.notAuthenticated))
|
||||
}
|
||||
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success(let response):
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let account = PatreonAccount(response: response, context: context)
|
||||
completion(.success(account))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPatrons(completion: @escaping (Result<[Patron], Swift.Error>) -> Void)
|
||||
{
|
||||
var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(campaignID)/members")!
|
||||
components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"),
|
||||
URLQueryItem(name: "fields[tier]", value: "title"),
|
||||
URLQueryItem(name: "fields[member]", value: "full_name,patron_status"),
|
||||
URLQueryItem(name: "page[size]", value: "1000")]
|
||||
|
||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
||||
|
||||
struct Response: Decodable
|
||||
{
|
||||
var data: [PatronResponse]
|
||||
var included: [AnyResponse]
|
||||
var links: [String: URL]?
|
||||
}
|
||||
|
||||
var allPatrons = [Patron]()
|
||||
|
||||
func fetchPatrons(url: URL)
|
||||
{
|
||||
let request = URLRequest(url: url)
|
||||
|
||||
self.send(request, authorizationType: .creator) { (result: Result<Response, Swift.Error>) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success(let response):
|
||||
let tiers = response.included.compactMap { (response) -> Tier? in
|
||||
switch response
|
||||
{
|
||||
case .tier(let tierResponse): return Tier(response: tierResponse)
|
||||
case .benefit: return nil
|
||||
}
|
||||
}
|
||||
|
||||
let tiersByIdentifier = Dictionary(tiers.map { ($0.identifier, $0) }, uniquingKeysWith: { (a, b) in return a })
|
||||
|
||||
let patrons = response.data.map { (response) -> Patron in
|
||||
let patron = Patron(response: response)
|
||||
|
||||
for tierID in response.relationships?.currently_entitled_tiers.data ?? []
|
||||
{
|
||||
guard let tier = tiersByIdentifier[tierID.id] else { continue }
|
||||
patron.benefits.formUnion(tier.benefits)
|
||||
}
|
||||
|
||||
return patron
|
||||
}.filter { $0.benefits.contains(where: { $0.type == .credits }) }
|
||||
|
||||
allPatrons.append(contentsOf: patrons)
|
||||
|
||||
if let nextURL = response.links?["next"]
|
||||
{
|
||||
fetchPatrons(url: nextURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(.success(allPatrons))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchPatrons(url: requestURL)
|
||||
}
|
||||
|
||||
func signOut(completion: @escaping (Result<Void, Swift.Error>) -> Void)
|
||||
{
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
do
|
||||
{
|
||||
let accounts = PatreonAccount.all(in: context, requestProperties: [\FetchRequest.returnsObjectsAsFaults: true])
|
||||
accounts.forEach(context.delete(_:))
|
||||
|
||||
self.deactivateBetaApps(in: context)
|
||||
|
||||
try context.save()
|
||||
|
||||
Keychain.shared.patreonAccessToken = nil
|
||||
Keychain.shared.patreonRefreshToken = nil
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshPatreonAccount()
|
||||
{
|
||||
guard PatreonAPI.shared.isAuthenticated else { return }
|
||||
|
||||
PatreonAPI.shared.fetchAccount { (result: Result<PatreonAccount, Swift.Error>) in
|
||||
do
|
||||
{
|
||||
let account = try result.get()
|
||||
|
||||
if let context = account.managedObjectContext, !account.isPatron
|
||||
{
|
||||
// Deactivate all beta apps now that we're no longer a patron.
|
||||
self.deactivateBetaApps(in: context)
|
||||
}
|
||||
|
||||
try account.managedObjectContext?.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to fetch Patreon account.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PatreonAPI
|
||||
{
|
||||
func fetchAccessToken(oauthCode: String, completion: @escaping (Result<(String, String), Swift.Error>) -> Void)
|
||||
{
|
||||
let encodedRedirectURI = ("https://rileytestut.com/patreon/altstore" as NSString).addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
|
||||
let encodedOauthCode = (oauthCode as NSString).addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
|
||||
|
||||
let body = "code=\(encodedOauthCode)&grant_type=authorization_code&client_id=\(clientID)&client_secret=\(clientSecret)&redirect_uri=\(encodedRedirectURI)"
|
||||
|
||||
let requestURL = URL(string: "/api/oauth2/token", relativeTo: self.baseURL)!
|
||||
|
||||
var request = URLRequest(url: requestURL)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = body.data(using: .utf8)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
struct Response: Decodable
|
||||
{
|
||||
var access_token: String
|
||||
var refresh_token: String
|
||||
}
|
||||
|
||||
self.send(request, authorizationType: .none) { (result: Result<Response, Swift.Error>) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success(let response): completion(.success((response.access_token, response.refresh_token)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshAccessToken(completion: @escaping (Result<Void, Swift.Error>) -> Void)
|
||||
{
|
||||
guard let refreshToken = Keychain.shared.patreonRefreshToken else { return }
|
||||
|
||||
var components = URLComponents(string: "/api/oauth2/token")!
|
||||
components.queryItems = [URLQueryItem(name: "grant_type", value: "refresh_token"),
|
||||
URLQueryItem(name: "refresh_token", value: refreshToken),
|
||||
URLQueryItem(name: "client_id", value: clientID),
|
||||
URLQueryItem(name: "client_secret", value: clientSecret)]
|
||||
|
||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
||||
|
||||
var request = URLRequest(url: requestURL)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
struct Response: Decodable
|
||||
{
|
||||
var access_token: String
|
||||
var refresh_token: String
|
||||
}
|
||||
|
||||
self.send(request, authorizationType: .none) { (result: Result<Response, Swift.Error>) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success(let response):
|
||||
Keychain.shared.patreonAccessToken = response.access_token
|
||||
Keychain.shared.patreonRefreshToken = response.refresh_token
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send<ResponseType: Decodable>(_ request: URLRequest, authorizationType: AuthorizationType, completion: @escaping (Result<ResponseType, Swift.Error>) -> Void)
|
||||
{
|
||||
var request = request
|
||||
|
||||
switch authorizationType
|
||||
{
|
||||
case .none: break
|
||||
case .creator:
|
||||
guard let creatorAccessToken = Keychain.shared.patreonCreatorAccessToken else { return completion(.failure(Error.invalidAccessToken)) }
|
||||
request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization")
|
||||
|
||||
case .user:
|
||||
guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(Error.notAuthenticated)) }
|
||||
request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
let task = self.session.dataTask(with: request) { (data, response, error) in
|
||||
do
|
||||
{
|
||||
let data = try Result(data, error).get()
|
||||
|
||||
if let response = response as? HTTPURLResponse, response.statusCode == 401
|
||||
{
|
||||
switch authorizationType
|
||||
{
|
||||
case .creator: completion(.failure(Error.invalidAccessToken))
|
||||
case .none: completion(.failure(Error.notAuthenticated))
|
||||
case .user:
|
||||
self.refreshAccessToken() { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success: self.send(request, authorizationType: authorizationType, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let response = try JSONDecoder().decode(ResponseType.self, from: data)
|
||||
completion(.success(response))
|
||||
}
|
||||
catch let error
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func deactivateBetaApps(in context: NSManagedObjectContext)
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K != %@ AND %K != nil AND %K == YES",
|
||||
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID, #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))
|
||||
|
||||
let installedApps = InstalledApp.all(satisfying: predicate, in: context)
|
||||
installedApps.forEach { $0.isActive = false }
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension PatreonAPI: ASWebAuthenticationPresentationContextProviding
|
||||
{
|
||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor
|
||||
{
|
||||
return UIApplication.shared.keyWindow ?? UIWindow()
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
//
|
||||
// Patron.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/21/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct PatronResponse: Decodable
|
||||
{
|
||||
struct Attributes: Decodable
|
||||
{
|
||||
var full_name: String
|
||||
var patron_status: String?
|
||||
}
|
||||
|
||||
struct Relationships: Decodable
|
||||
{
|
||||
struct Tiers: Decodable
|
||||
{
|
||||
struct TierID: Decodable
|
||||
{
|
||||
var id: String
|
||||
var type: String
|
||||
}
|
||||
|
||||
var data: [TierID]
|
||||
}
|
||||
|
||||
var currently_entitled_tiers: Tiers
|
||||
}
|
||||
|
||||
var id: String
|
||||
var attributes: Attributes
|
||||
|
||||
var relationships: Relationships?
|
||||
}
|
||||
}
|
||||
|
||||
extension Patron
|
||||
{
|
||||
enum Status: String, Decodable
|
||||
{
|
||||
case active = "active_patron"
|
||||
case declined = "declined_patron"
|
||||
case former = "former_patron"
|
||||
case unknown = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
class Patron
|
||||
{
|
||||
var name: String
|
||||
var identifier: String
|
||||
|
||||
var status: Status
|
||||
|
||||
var benefits: Set<Benefit> = []
|
||||
|
||||
init(response: PatreonAPI.PatronResponse)
|
||||
{
|
||||
self.name = response.attributes.full_name
|
||||
self.identifier = response.id
|
||||
|
||||
if let status = response.attributes.patron_status
|
||||
{
|
||||
self.status = Status(rawValue: status) ?? .unknown
|
||||
}
|
||||
else
|
||||
{
|
||||
self.status = .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
//
|
||||
// Tier.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/21/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PatreonAPI
|
||||
{
|
||||
struct TierResponse: Decodable
|
||||
{
|
||||
struct Attributes: Decodable
|
||||
{
|
||||
var title: String
|
||||
}
|
||||
|
||||
struct Relationships: Decodable
|
||||
{
|
||||
struct Benefits: Decodable
|
||||
{
|
||||
var data: [BenefitResponse]
|
||||
}
|
||||
|
||||
var benefits: Benefits
|
||||
}
|
||||
|
||||
var id: String
|
||||
var attributes: Attributes
|
||||
|
||||
var relationships: Relationships
|
||||
}
|
||||
}
|
||||
|
||||
struct Tier
|
||||
{
|
||||
var name: String
|
||||
var identifier: String
|
||||
|
||||
var benefits: [Benefit] = []
|
||||
|
||||
init(response: PatreonAPI.TierResponse)
|
||||
{
|
||||
self.name = response.attributes.title
|
||||
self.identifier = response.id
|
||||
self.benefits = response.relationships.benefits.data.map(Benefit.init(response:))
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// AppProtocol.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/26/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltSign
|
||||
|
||||
protocol AppProtocol
|
||||
{
|
||||
var name: String { get }
|
||||
var bundleIdentifier: String { get }
|
||||
var url: URL { get }
|
||||
}
|
||||
|
||||
extension ALTApplication: AppProtocol
|
||||
{
|
||||
var url: URL {
|
||||
return self.fileURL
|
||||
}
|
||||
}
|
||||
|
||||
extension StoreApp: AppProtocol
|
||||
{
|
||||
var url: URL {
|
||||
return self.downloadURL
|
||||
}
|
||||
}
|
||||
|
||||
extension InstalledApp: AppProtocol
|
||||
{
|
||||
var url: URL {
|
||||
return self.fileURL
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
//
|
||||
// NSManagedObject+Conveniences.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/6/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
typealias FetchRequest = NSFetchRequest<NSFetchRequestResult>
|
||||
|
||||
protocol Fetchable: NSManagedObject
|
||||
{
|
||||
}
|
||||
|
||||
extension Fetchable
|
||||
{
|
||||
static func first(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext,
|
||||
requestProperties: [PartialKeyPath<FetchRequest>: Any?] = [:]) -> Self?
|
||||
{
|
||||
let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, requestProperties: requestProperties, returnFirstResult: true)
|
||||
return managedObjects.first
|
||||
}
|
||||
|
||||
static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext,
|
||||
requestProperties: [PartialKeyPath<FetchRequest>: Any?] = [:]) -> [Self]
|
||||
{
|
||||
let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, requestProperties: requestProperties, returnFirstResult: false)
|
||||
return managedObjects
|
||||
}
|
||||
|
||||
static func fetch(_ fetchRequest: NSFetchRequest<Self>, in context: NSManagedObjectContext) -> [Self]
|
||||
{
|
||||
do
|
||||
{
|
||||
let managedObjects = try context.fetch(fetchRequest)
|
||||
return managedObjects
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to fetch managed objects. Fetch Request: \(fetchRequest). Error: \(error).")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext, requestProperties: [PartialKeyPath<FetchRequest>: Any?], returnFirstResult: Bool) -> [Self]
|
||||
{
|
||||
let registeredObjects = context.registeredObjects.lazy.compactMap({ $0 as? Self }).filter({ predicate?.evaluate(with: $0) != false })
|
||||
|
||||
if let managedObject = registeredObjects.first, returnFirstResult
|
||||
{
|
||||
return [managedObject]
|
||||
}
|
||||
|
||||
let fetchRequest = self.fetchRequest() as! NSFetchRequest<Self>
|
||||
fetchRequest.predicate = predicate
|
||||
fetchRequest.sortDescriptors = sortDescriptors
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
|
||||
for (keyPath, value) in requestProperties
|
||||
{
|
||||
// Still no easy way to cast PartialKeyPath back to usable WritableKeyPath :(
|
||||
guard let objcKeyString = keyPath._kvcKeyPathString else { continue }
|
||||
fetchRequest.setValue(value, forKey: objcKeyString)
|
||||
}
|
||||
|
||||
let fetchedObjects = self.fetch(fetchRequest, in: context)
|
||||
|
||||
if let fetchedObject = fetchedObjects.first, returnFirstResult
|
||||
{
|
||||
return [fetchedObject]
|
||||
}
|
||||
else
|
||||
{
|
||||
return fetchedObjects
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class ServerConnection
|
||||
{
|
||||
var server: Server
|
||||
@@ -93,7 +95,7 @@ class ServerConnection
|
||||
{
|
||||
let data = try self.process(data: data, error: error)
|
||||
|
||||
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
|
||||
let response = try AltStoreCore.JSONDecoder().decode(ServerResponse.self, from: data)
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class ServerManager: NSObject
|
||||
{
|
||||
static let shared = ServerManager()
|
||||
|
||||
@@ -10,6 +10,7 @@ import UIKit
|
||||
import SafariServices
|
||||
import AuthenticationServices
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
extension PatreonViewController
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
@objc(RefreshAttemptTableViewCell)
|
||||
|
||||
@@ -10,6 +10,8 @@ import UIKit
|
||||
import SafariServices
|
||||
import MessageUI
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension SettingsViewController
|
||||
{
|
||||
fileprivate enum Section: Int, CaseIterable
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import UIKit
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
class SourcesViewController: UICollectionViewController
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// ALTAppPermission.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NSString *ALTAppPermissionType NS_TYPED_EXTENSIBLE_ENUM;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypePhotos;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundAudio;
|
||||
extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundFetch;
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// ALTAppPermission.m
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTAppPermission.h"
|
||||
|
||||
ALTAppPermissionType const ALTAppPermissionTypePhotos = @"photos";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeBackgroundAudio = @"background-audio";
|
||||
ALTAppPermissionType const ALTAppPermissionTypeBackgroundFetch = @"background-fetch";
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// ALTPatreonBenefitType.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/27/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NSString *ALTPatreonBenefitType NS_TYPED_EXTENSIBLE_ENUM;
|
||||
extern ALTPatreonBenefitType const ALTPatreonBenefitTypeBetaAccess;
|
||||
extern ALTPatreonBenefitType const ALTPatreonBenefitTypeCredits;
|
||||
@@ -1,12 +0,0 @@
|
||||
//
|
||||
// ALTPatreonBenefitType.m
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 8/27/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTPatreonBenefitType.h"
|
||||
|
||||
ALTPatreonBenefitType const ALTPatreonBenefitTypeBetaAccess = @"1186336";
|
||||
ALTPatreonBenefitType const ALTPatreonBenefitTypeCredits = @"1186340";
|
||||
@@ -1,12 +0,0 @@
|
||||
//
|
||||
// ALTSourceUserInfoKey.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 11/4/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NSString *ALTSourceUserInfoKey NS_TYPED_EXTENSIBLE_ENUM;
|
||||
extern ALTSourceUserInfoKey const ALTSourceUserInfoKeyPatreonAccessToken;
|
||||
@@ -1,11 +0,0 @@
|
||||
//
|
||||
// ALTSourceUserInfoKey.m
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 11/4/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTSourceUserInfoKey.h"
|
||||
|
||||
ALTSourceUserInfoKey const ALTSourceUserInfoKeyPatreonAccessToken = @"patreonAccessToken";
|
||||
Reference in New Issue
Block a user