Compare commits

...

57 Commits
1.2 ... 1.3

Author SHA1 Message Date
Riley Testut
c3257bfbb8 Updates app version to 1.3 2020-04-09 12:26:41 -07:00
Riley Testut
e0d2bab21e [AltServer] Updates app version to 1.3 2020-04-09 12:25:48 -07:00
Riley Testut
7b7613c331 Adds Alpha source JSON 2020-04-09 12:22:57 -07:00
Riley Testut
274a4aea44 Updates apps.json for AltStore 1.3b3 2020-04-09 12:21:23 -07:00
Riley Testut
98146ca8f3 Uses separate App Center tokens for different build types 2020-04-09 12:18:17 -07:00
Riley Testut
c85da1495d Disables Sources functionality for public versions 2020-04-01 13:27:26 -07:00
Riley Testut
1b89b81de0 Enables sideloading for public versions 2020-04-01 13:26:22 -07:00
Riley Testut
29af9af3f3 [AltServer] Updates Carthage dependencies for Xcode 11.4 2020-04-01 13:17:17 -07:00
Riley Testut
b8e1921b74 Leaves apps activated if there is no active app limit during migration 2020-04-01 13:06:06 -07:00
Riley Testut
664c31aba8 [AltServer] Removes duplicate profiles even if they’re excluded 2020-04-01 12:19:25 -07:00
Riley Testut
40d4899bd1 Clarifies AltStore renews App IDs after they expire 2020-04-01 11:53:25 -07:00
Riley Testut
c1aad80578 Adds support for ALPHA builds 2020-04-01 11:51:00 -07:00
Riley Testut
0f939700e2 Fixes potentially incorrect bundle identifier when resigning AltStore with DEBUG build 2020-03-31 14:33:13 -07:00
Riley Testut
193ca28c98 Adds VS App Center analytics + crash reporting
Currently tracks install, refresh, and update app events.
2020-03-31 14:31:34 -07:00
Riley Testut
cd89741827 Updates RefreshError.noInstalledApps’ localized description 2020-03-30 15:38:00 -07:00
Riley Testut
4e29c7a38c Fixes RefreshAltStoreViewController never finishing 2020-03-30 15:23:20 -07:00
Riley Testut
45737250a7 Fixes potentially incorrect bundle identifier when resigning/refreshing AltStore 2020-03-30 15:18:10 -07:00
Riley Testut
197c3b3338 [AltServer] Fixes installing outdated profile after app installation 2020-03-30 15:06:16 -07:00
Riley Testut
162139d52b Updates older ToastView code to use error initializer 2020-03-30 14:07:18 -07:00
Riley Testut
4d75116c2d Fixes missing OperationError recovery suggestions 2020-03-30 13:56:40 -07:00
Riley Testut
99df5aea3e Adds basic search functionality to Browse tab 2020-03-30 13:46:15 -07:00
Riley Testut
cf46bd0a46 Removes ellipsis from AppIDsViewController cell 2020-03-30 13:40:14 -07:00
Riley Testut
c9bffbe74f Deactivates beta apps when no longer a patron/signed in
Prevents beta apps from taking up active app slots despite not being listed in My Apps
2020-03-30 13:34:13 -07:00
Riley Testut
794d26b016 Dismisses SFVC when sideloading apps from News item 2020-03-30 13:26:44 -07:00
Riley Testut
e80847f2a9 Removes sideloading beta alert 2020-03-30 13:25:14 -07:00
Riley Testut
992226f75a Migrates from Core Data model v5 to v6 2020-03-24 13:43:16 -07:00
Riley Testut
a90c0c05a0 Adds initial support for 3rd party Sources 2020-03-24 13:27:44 -07:00
Riley Testut
590ce5c928 Deletes cached apps after they’ve been uninstalled from device 2020-03-23 12:12:49 -07:00
Riley Testut
9e465f8eaa Emphasizes App IDs can’t be deleted in AppIDsViewController message 2020-03-23 11:33:06 -07:00
Riley Testut
1fb6be5bbe Adds Drag & Drop support for activating/deactivating apps 2020-03-20 16:38:54 -07:00
Riley Testut
4fcd691fae Adds option to remove app extensions before installation
Free developer accounts may only have 3 active apps and app extensions, so this option allows users to limit active slots an app will take
2020-03-20 15:56:10 -07:00
Riley Testut
8af1d3f131 Fixes incorrect action when refreshing/activating apps due to cell reuse 2020-03-20 15:52:11 -07:00
Riley Testut
3b7b6a014b Fixes grayed-out .ipas due to duplicate UTI declarations 2020-03-20 15:51:33 -07:00
Riley Testut
0566c152f6 Restores peek & pop in MyAppsViewController on iOS 12 2020-03-20 15:33:29 -07:00
Riley Testut
63c55b41ec Improves error toast view appearance 2020-03-20 15:31:20 -07:00
Riley Testut
a2acbcd5b5 Removes unused Team variable 2020-03-19 11:58:03 -07:00
Riley Testut
4fd2b448bd Fixes race condition when installing app with app groups + extensions 2020-03-19 11:56:28 -07:00
Riley Testut
0d735431e9 Fixes endless refreshing if error occurs when legacy refreshing 2020-03-19 11:53:53 -07:00
Riley Testut
f332060459 Fixes tuple unpacking warning with Xcode 11.4 2020-03-19 11:50:39 -07:00
Riley Testut
5afc513180 [AltServer] Updates app version to 1.3b2 2020-03-17 12:53:53 -07:00
Riley Testut
0d65fc9974 [AltServer] Fixes installing more than 3 apps on 13.3 and below 2020-03-17 12:24:11 -07:00
Riley Testut
a6746754b8 Fixes hard-to-see activity indicators in dark mode 2020-03-16 13:24:04 -07:00
Riley Testut
7474cf4fd1 Updates app version to 1.3b 2020-03-12 10:10:11 -07:00
Riley Testut
b36c09792d Adds BETA compilation condition by default 2020-03-12 10:09:59 -07:00
Riley Testut
a95457cca0 [AltServer] Updates app version to 1.3b 2020-03-12 10:05:18 -07:00
Riley Testut
800dd79c30 Migrates from Core Data model v4 to v5 2020-03-11 17:29:32 -07:00
Riley Testut
bc02cfc8a9 Adds support for activating and deactivating apps
iOS 13.3.1 limits free developer accounts to 3 apps and app extensions. As a workaround, we now allow up to 3 “active” apps (apps with installed provisioning profiles), as well as additional “inactivate” apps which don’t have any profiles installed, causing them to not count towards the total. Inactive apps cannot be opened until they are activated.
2020-03-11 15:49:26 -07:00
Riley Testut
06fed802b1 [AltServer] Manages active/inactive profiles when installing apps 2020-03-11 13:51:39 -07:00
Riley Testut
5e25593c3d [Both] Improves error messages 2020-03-11 13:51:17 -07:00
Riley Testut
4f00018164 Refreshes apps by installing provisioning profiles when possible
Assuming the certificate used to originally sign an app is still valid, we can refresh an app simply by installing new provisioning profiles. However, if the signing certificate is no longer valid, we fall back to the old method of resigning + reinstalling.
2020-03-06 17:34:18 -08:00
Riley Testut
27bce4e456 [AltServer] Supports Install/Remove provisioning profiles requests
Stuff I shoulda committed
2020-03-06 17:14:29 -08:00
Riley Testut
afdefc23ce Changes adjusted app group identifier format 2020-02-26 13:18:56 -08:00
Riley Testut
1290ffba66 [AltServer] Updates app version to 1.2.1 2020-02-26 13:16:15 -08:00
Riley Testut
7a6d9970e8 [AltServer] Fixes plug-in installation error when plug-ins directory does not exist 2020-02-14 17:02:15 -08:00
Riley Testut
07efd681c1 [AltServer] Refactors Mail plug-in installation to fix notarization errors
AltServer must now download the Mail plug-in at runtime, because notarization will fail if AltServer contains an unsigned binary (and as of Catalina, Mail plug-ins only work if they’re unsigned)
2020-02-13 21:49:46 -08:00
Riley Testut
891da58cfd Updates apps.json with AltStore 1.2 2020-02-12 12:01:03 -08:00
Riley Testut
e6230e0140 [AltServer] Updates app version to 1.2 2020-02-12 12:00:50 -08:00
160 changed files with 10141 additions and 2768 deletions

3
.gitmodules vendored
View File

@@ -13,3 +13,6 @@
[submodule "Dependencies/libplist"]
path = Dependencies/libplist
url = https://github.com/libimobiledevice/libplist.git
[submodule "Dependencies/MarkdownAttributedString"]
path = Dependencies/MarkdownAttributedString
url = https://github.com/chockenberry/MarkdownAttributedString.git

View File

@@ -29,6 +29,11 @@ public extension Bundle
return infoPlistURL
}
var provisioningProfileURL: URL {
let infoPlistURL = self.bundleURL.appendingPathComponent("embedded.mobileprovision")
return infoPlistURL
}
var certificateURL: URL {
let infoPlistURL = self.bundleURL.appendingPathComponent("ALTCertificate.p12")
return infoPlistURL

View File

@@ -0,0 +1,58 @@
//
// CodableServerError.swift
// AltKit
//
// Created by Riley Testut on 3/5/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {}
struct CodableServerError: Codable
{
var error: ALTServerError {
return ALTServerError(self.errorCode, userInfo: self.userInfo ?? [:])
}
private var errorCode: ALTServerError.Code
private var userInfo: [String: String]?
private enum CodingKeys: String, CodingKey
{
case errorCode
case userInfo
}
init(error: ALTServerError)
{
self.errorCode = error.code
let userInfo = error.userInfo.compactMapValues { $0 as? String }
if !userInfo.isEmpty
{
self.userInfo = userInfo
}
}
init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let errorCode = try container.decode(Int.self, forKey: .errorCode)
self.errorCode = ALTServerError.Code(rawValue: errorCode) ?? .unknown
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
self.userInfo = userInfo
}
func encode(to encoder: Encoder) throws
{
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.error.code.rawValue, forKey: .errorCode)
try container.encodeIfPresent(self.userInfo, forKey: .userInfo)
}
}

View File

@@ -11,8 +11,13 @@
extern NSErrorDomain const AltServerErrorDomain;
extern NSErrorDomain const AltServerInstallationErrorDomain;
extern NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey;
extern NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey;
typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError)
{
ALTServerErrorUnderlyingError = -1,
ALTServerErrorUnknown = 0,
ALTServerErrorConnectionFailed = 1,
ALTServerErrorLostConnection = 2,
@@ -32,7 +37,9 @@ typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError)
ALTServerErrorUnknownResponse = 12,
ALTServerErrorInvalidAnisetteData = 13,
ALTServerErrorPluginNotFound = 14
ALTServerErrorPluginNotFound = 14,
ALTServerErrorProfileNotFound = 15
};
NS_ASSUME_NONNULL_BEGIN

View File

@@ -11,24 +11,43 @@
NSErrorDomain const AltServerErrorDomain = @"com.rileytestut.AltServer";
NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServer.Installation";
NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode";
NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdentifier";
@implementation NSError (ALTServerError)
+ (void)load
{
[NSError setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey])
if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
{
return [error alt_localizedDescription];
return [error alt_localizedFailureReason];
}
if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey])
{
return [error alt_localizedRecoverySuggestion];
}
return nil;
}];
}
- (nullable NSString *)alt_localizedDescription
- (nullable NSString *)alt_localizedFailureReason
{
switch ((ALTServerError)self.code)
{
case ALTServerErrorUnderlyingError:
{
NSString *underlyingErrorCode = self.userInfo[ALTUnderlyingErrorCodeErrorKey];
if (underlyingErrorCode == nil)
{
return NSLocalizedString(@"An unknown error occured.", @"");
}
return [NSString stringWithFormat:NSLocalizedString(@"Error code: %@", @""), underlyingErrorCode];
}
case ALTServerErrorUnknown:
return NSLocalizedString(@"An unknown error occured.", @"");
@@ -57,7 +76,7 @@ NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServ
return NSLocalizedString(@"An error occured while installing the app.", @"");
case ALTServerErrorMaximumFreeAppLimitReached:
return NSLocalizedString(@"You have reached the limit of 3 apps per device.", @"");
return NSLocalizedString(@"Cannot activate more than 3 apps and app extensions.", @"");
case ALTServerErrorUnsupportediOSVersion:
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
@@ -72,8 +91,44 @@ NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServ
return NSLocalizedString(@"Invalid anisette data.", @"");
case ALTServerErrorPluginNotFound:
return NSLocalizedString(@"Could not connect to Mail plug-in. Please make sure the plug-in is installed and Mail is running, then try again.", @"");
return NSLocalizedString(@"Could not connect to Mail plug-in.", @"");
case ALTServerErrorProfileNotFound:
return [self profileErrorLocalizedDescriptionWithBaseDescription:NSLocalizedString(@"Could not find profile", "")];
}
}
- (nullable NSString *)alt_localizedRecoverySuggestion
{
switch ((ALTServerError)self.code)
{
case ALTServerErrorConnectionFailed:
case ALTServerErrorDeviceNotFound:
return NSLocalizedString(@"Make sure you have trusted this phone with your computer and WiFi sync is enabled.", @"");
case ALTServerErrorPluginNotFound:
return NSLocalizedString(@"Make sure Mail is running and the plug-in is enabled in Mail's preferences.", @"");
default:
return nil;
}
}
- (NSString *)profileErrorLocalizedDescriptionWithBaseDescription:(NSString *)baseDescription
{
NSString *localizedDescription = nil;
NSString *bundleID = self.userInfo[ALTProvisioningProfileBundleIDErrorKey];
if (bundleID)
{
localizedDescription = [NSString stringWithFormat:@"%@ “%@”", baseDescription, bundleID];
}
else
{
localizedDescription = [NSString stringWithFormat:@"%@.", baseDescription];
}
return localizedDescription;
}
@end

View File

@@ -11,9 +11,6 @@ import AltSign
public let ALTServerServiceType = "_altserver._tcp"
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {}
protocol ServerMessageProtocol: Codable
{
var version: Int { get }
@@ -25,6 +22,8 @@ public enum ServerRequest: Decodable
case anisetteData(AnisetteDataRequest)
case prepareApp(PrepareAppRequest)
case beginInstallation(BeginInstallationRequest)
case installProvisioningProfiles(InstallProvisioningProfilesRequest)
case removeProvisioningProfiles(RemoveProvisioningProfilesRequest)
case unknown(identifier: String, version: Int)
var identifier: String {
@@ -33,6 +32,8 @@ public enum ServerRequest: Decodable
case .anisetteData(let request): return request.identifier
case .prepareApp(let request): return request.identifier
case .beginInstallation(let request): return request.identifier
case .installProvisioningProfiles(let request): return request.identifier
case .removeProvisioningProfiles(let request): return request.identifier
case .unknown(let identifier, _): return identifier
}
}
@@ -43,6 +44,8 @@ public enum ServerRequest: Decodable
case .anisetteData(let request): return request.version
case .prepareApp(let request): return request.version
case .beginInstallation(let request): return request.version
case .installProvisioningProfiles(let request): return request.version
case .removeProvisioningProfiles(let request): return request.version
case .unknown(_, let version): return version
}
}
@@ -73,6 +76,14 @@ public enum ServerRequest: Decodable
case "BeginInstallationRequest":
let request = try BeginInstallationRequest(from: decoder)
self = .beginInstallation(request)
case "InstallProvisioningProfilesRequest":
let request = try InstallProvisioningProfilesRequest(from: decoder)
self = .installProvisioningProfiles(request)
case "RemoveProvisioningProfilesRequest":
let request = try RemoveProvisioningProfilesRequest(from: decoder)
self = .removeProvisioningProfiles(request)
default:
self = .unknown(identifier: identifier, version: version)
@@ -84,6 +95,8 @@ public enum ServerResponse: Decodable
{
case anisetteData(AnisetteDataResponse)
case installationProgress(InstallationProgressResponse)
case installProvisioningProfiles(InstallProvisioningProfilesResponse)
case removeProvisioningProfiles(RemoveProvisioningProfilesResponse)
case error(ErrorResponse)
case unknown(identifier: String, version: Int)
@@ -92,6 +105,8 @@ public enum ServerResponse: Decodable
{
case .anisetteData(let response): return response.identifier
case .installationProgress(let response): return response.identifier
case .installProvisioningProfiles(let response): return response.identifier
case .removeProvisioningProfiles(let response): return response.identifier
case .error(let response): return response.identifier
case .unknown(let identifier, _): return identifier
}
@@ -102,6 +117,8 @@ public enum ServerResponse: Decodable
{
case .anisetteData(let response): return response.version
case .installationProgress(let response): return response.version
case .installProvisioningProfiles(let response): return response.version
case .removeProvisioningProfiles(let response): return response.version
case .error(let response): return response.version
case .unknown(_, let version): return version
}
@@ -129,6 +146,14 @@ public enum ServerResponse: Decodable
case "InstallationProgressResponse":
let response = try InstallationProgressResponse(from: decoder)
self = .installationProgress(response)
case "InstallProvisioningProfilesResponse":
let response = try InstallProvisioningProfilesResponse(from: decoder)
self = .installProvisioningProfiles(response)
case "RemoveProvisioningProfilesResponse":
let response = try RemoveProvisioningProfilesResponse(from: decoder)
self = .removeProvisioningProfiles(response)
case "ErrorResponse":
let response = try ErrorResponse(from: decoder)
@@ -140,6 +165,28 @@ public enum ServerResponse: Decodable
}
}
// _Don't_ provide generic SuccessResponse, as that would prevent us
// from easily changing response format for a request in the future.
public struct ErrorResponse: ServerMessageProtocol
{
public var version = 2
public var identifier = "ErrorResponse"
public var error: ALTServerError {
return self.serverError?.error ?? ALTServerError(self.errorCode)
}
private var serverError: CodableServerError?
// Legacy (v1)
private var errorCode: ALTServerError.Code
public init(error: ALTServerError)
{
self.serverError = CodableServerError(error: error)
self.errorCode = error.code
}
}
public struct AnisetteDataRequest: ServerMessageProtocol
{
public var version = 1
@@ -215,27 +262,15 @@ public struct PrepareAppRequest: ServerMessageProtocol
public struct BeginInstallationRequest: ServerMessageProtocol
{
public var version = 1
public var version = 2
public var identifier = "BeginInstallationRequest"
public init()
{
}
}
public struct ErrorResponse: ServerMessageProtocol
{
public var version = 1
public var identifier = "ErrorResponse"
// If activeProfiles is non-nil, then AltServer should remove all profiles except active ones.
public var activeProfiles: Set<String>?
public var error: ALTServerError {
return ALTServerError(self.errorCode)
}
private var errorCode: ALTServerError.Code
public init(error: ALTServerError)
public init(activeProfiles: Set<String>?)
{
self.errorCode = error.code
self.activeProfiles = activeProfiles
}
}
@@ -251,3 +286,96 @@ public struct InstallationProgressResponse: ServerMessageProtocol
self.progress = progress
}
}
public struct InstallProvisioningProfilesRequest: ServerMessageProtocol
{
public var version = 1
public var identifier = "InstallProvisioningProfilesRequest"
public var udid: String
public var provisioningProfiles: Set<ALTProvisioningProfile>
// If activeProfiles is non-nil, then AltServer should remove all profiles except active ones.
public var activeProfiles: Set<String>?
private enum CodingKeys: String, CodingKey
{
case identifier
case version
case udid
case provisioningProfiles
case activeProfiles
}
public init(udid: String, provisioningProfiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?)
{
self.udid = udid
self.provisioningProfiles = provisioningProfiles
self.activeProfiles = activeProfiles
}
public init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(Int.self, forKey: .version)
self.identifier = try container.decode(String.self, forKey: .identifier)
self.udid = try container.decode(String.self, forKey: .udid)
let rawProvisioningProfiles = try container.decode([Data].self, forKey: .provisioningProfiles)
let provisioningProfiles = try rawProvisioningProfiles.map { (data) -> ALTProvisioningProfile in
guard let profile = ALTProvisioningProfile(data: data) else {
throw DecodingError.dataCorruptedError(forKey: CodingKeys.provisioningProfiles, in: container, debugDescription: "Could not parse provisioning profile from data.")
}
return profile
}
self.provisioningProfiles = Set(provisioningProfiles)
self.activeProfiles = try container.decodeIfPresent(Set<String>.self, forKey: .activeProfiles)
}
public func encode(to encoder: Encoder) throws
{
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.version, forKey: .version)
try container.encode(self.identifier, forKey: .identifier)
try container.encode(self.udid, forKey: .udid)
try container.encode(self.provisioningProfiles.map { $0.data }, forKey: .provisioningProfiles)
try container.encodeIfPresent(self.activeProfiles, forKey: .activeProfiles)
}
}
public struct InstallProvisioningProfilesResponse: ServerMessageProtocol
{
public var version = 1
public var identifier = "InstallProvisioningProfilesResponse"
public init()
{
}
}
public struct RemoveProvisioningProfilesRequest: ServerMessageProtocol
{
public var version = 1
public var identifier = "RemoveProvisioningProfilesRequest"
public var udid: String
public var bundleIdentifiers: Set<String>
public init(udid: String, bundleIdentifiers: Set<String>)
{
self.udid = udid
self.bundleIdentifiers = bundleIdentifiers
}
}
public struct RemoveProvisioningProfilesResponse: ServerMessageProtocol
{
public var version = 1
public var identifier = "RemoveProvisioningProfilesResponse"
public init()
{
}
}

Binary file not shown.

View File

@@ -14,24 +14,27 @@ import AltSign
import LaunchAtLogin
import STPrivilegedTask
private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true)
private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle")
enum PluginError: LocalizedError
{
case installationScriptNotFound
case failedToRun(Int)
case scriptError(String)
case cancelled
case unknown
case taskError(String)
case taskErrorCode(Int)
var errorDescription: String? {
switch self
{
case .installationScriptNotFound: return NSLocalizedString("The installation script could not be found.", comment: "")
case .failedToRun(let errorCode): return String(format: NSLocalizedString("The installation script could not be run. (%@)", comment: ""), NSNumber(value: errorCode))
case .scriptError(let output): return output
case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "")
case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "")
case .taskError(let output): return output
case .taskErrorCode(let errorCode): return String(format: NSLocalizedString("There was an error installing the Mail plug-in. (Error Code: %@)", comment: ""), NSNumber(value: errorCode))
}
}
}
private let pluginURL = URL(fileURLWithPath: "/Library/Mail/Bundles/AltPlugin.mailbundle")
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
@@ -108,7 +111,7 @@ private extension AppDelegate
self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:))
if FileManager.default.fileExists(atPath: pluginURL.path)
if self.isMailPluginInstalled
{
self.installMailPluginMenuItem.title = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
}
@@ -182,47 +185,73 @@ private extension AppDelegate
let device = self.connectedDevices[index]
if !self.isMailPluginInstalled
func install()
{
let result = self.installMailPlugin()
guard result else { return }
ALTDeviceManager.shared.installAltStore(to: device, appleID: username, password: password) { (result) in
switch result
{
case .success:
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("Installation Succeeded", comment: "")
content.body = String(format: NSLocalizedString("AltStore was successfully installed on %@.", comment: ""), device.name)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
break
case .failure(let error as NSError):
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = NSLocalizedString("Installation Failed", comment: "")
if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? Error
{
alert.informativeText = underlyingError.localizedDescription
}
else
{
alert.informativeText = error.localizedDescription
}
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
alert.runModal()
}
}
}
ALTDeviceManager.shared.installAltStore(to: device, appleID: username, password: password) { (result) in
switch result
{
case .success:
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("Installation Succeeded", comment: "")
content.body = String(format: NSLocalizedString("AltStore was successfully installed on %@.", comment: ""), device.name)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
break
case .failure(let error as NSError):
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = NSLocalizedString("Installation Failed", comment: "")
if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? Error
{
alert.informativeText = underlyingError.localizedDescription
if !self.isMailPluginInstalled
{
self.installMailPlugin { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(PluginError.cancelled): break
case .failure(let error):
let alert = NSAlert()
alert.messageText = NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
alert.informativeText = error.localizedDescription
alert.runModal()
case .success:
let alert = NSAlert()
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
alert.runModal()
install()
}
}
else
{
alert.informativeText = error.localizedDescription
}
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
alert.runModal()
}
}
else
{
install()
}
}
@objc func toggleLaunchAtLogin(_ item: NSMenuItem)
@@ -241,71 +270,211 @@ private extension AppDelegate
@objc func handleInstallMailPluginMenuItem(_ item: NSMenuItem)
{
installMailPlugin()
if self.isMailPluginInstalled
{
self.uninstallMailPlugin { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(PluginError.cancelled): break
case .failure(let error):
let alert = NSAlert()
alert.messageText = NSLocalizedString("Failed to Uninstall Mail Plug-in", comment: "")
alert.informativeText = error.localizedDescription
alert.runModal()
case .success:
let alert = NSAlert()
alert.messageText = NSLocalizedString("Mail Plug-in Uninstalled", comment: "")
alert.informativeText = NSLocalizedString("Please restart Mail for changes to take effect. You will not be able to use AltServer until the plug-in is reinstalled.", comment: "")
alert.runModal()
}
}
}
}
else
{
self.installMailPlugin { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(PluginError.cancelled): break
case .failure(let error):
let alert = NSAlert()
alert.messageText = NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
alert.informativeText = error.localizedDescription
alert.runModal()
case .success:
let alert = NSAlert()
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
alert.runModal()
}
}
}
}
}
@discardableResult
func installMailPlugin() -> Bool
func installMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
let previouslyInstalled = self.isMailPluginInstalled
let alert = NSAlert()
alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "")
alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "")
if !previouslyInstalled
{
let alert = NSAlert()
alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "")
alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "")
alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let response = alert.runModal()
guard response == .alertFirstButtonReturn else { return false }
alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let response = alert.runModal()
guard response == .alertFirstButtonReturn else { throw PluginError.cancelled }
self.downloadPlugin { (result) in
do
{
let fileURL = try result.get()
defer { try? FileManager.default.removeItem(at: fileURL) }
// Ensure plug-in directory exists.
let authorization = try self.runAndKeepAuthorization("mkdir", arguments: ["-p", pluginDirectoryURL.path])
// Unzip AltPlugin to plug-ins directory.
try self.runAndKeepAuthorization("unzip", arguments: ["-o", fileURL.path, "-d", pluginDirectoryURL.path], authorization: authorization)
guard self.isMailPluginInstalled else { throw PluginError.unknown }
// Enable Mail plug-in preferences.
try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization)
print("Finished installing Mail plug-in!")
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
guard let scriptURL = Bundle.main.url(forResource: self.isMailPluginInstalled ? "UninstallPlugin" : "InstallPlugin", withExtension: "sh") else { throw PluginError.installationScriptNotFound }
try FileManager.default.setAttributes([.posixPermissions: 0o777], ofItemAtPath: scriptURL.path)
let task = STPrivilegedTask()
task.setLaunchPath(scriptURL.path)
task.setCurrentDirectoryPath(scriptURL.deletingLastPathComponent().path)
let errorCode = task.launch()
guard errorCode == 0 else { throw PluginError.failedToRun(Int(errorCode)) }
task.waitUntilExit()
if
let outputData = task.outputFileHandle()?.readDataToEndOfFile(),
let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
{
throw PluginError.scriptError(outputString)
}
if !previouslyInstalled && self.isMailPluginInstalled
{
let alert = NSAlert()
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
alert.runModal()
}
return true
}
catch
{
let alert = NSAlert()
alert.messageText = self.isMailPluginInstalled ? NSLocalizedString("Failed to Uninstall Mail Plug-in", comment: "") : NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
alert.informativeText = error.localizedDescription
alert.runModal()
return false
completionHandler(.failure(PluginError.cancelled))
}
}
func downloadPlugin(completionHandler: @escaping (Result<URL, Error>) -> Void)
{
let pluginURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altserver/altplugin/1_0.zip")!
let downloadTask = URLSession.shared.downloadTask(with: pluginURL) { (fileURL, response, error) in
if let fileURL = fileURL
{
print("Downloaded plugin to URL:", fileURL)
completionHandler(.success(fileURL))
}
else
{
completionHandler(.failure(error ?? PluginError.unknown))
}
}
downloadTask.resume()
}
func uninstallMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let alert = NSAlert()
alert.messageText = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
alert.informativeText = NSLocalizedString("Are you sure you want to uninstall the AltServer Mail plug-in? You will no longer be able to install or refresh apps with AltStore.", comment: "")
alert.addButton(withTitle: NSLocalizedString("Uninstall Plug-in", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let response = alert.runModal()
guard response == .alertFirstButtonReturn else { return completionHandler(.failure(PluginError.cancelled)) }
DispatchQueue.global().async {
do
{
if FileManager.default.fileExists(atPath: pluginURL.path)
{
// Delete Mail plug-in from privileged directory.
try self.run("rm", arguments: ["-rf", pluginURL.path])
}
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
}
}
private extension AppDelegate
{
func run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws
{
_ = try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: true)
}
@discardableResult
func runAndKeepAuthorization(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef
{
return try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: false)
}
func _run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil, freeAuthorization: Bool) throws -> AuthorizationRef
{
var launchPath = "/usr/bin/" + program
if !FileManager.default.fileExists(atPath: launchPath)
{
launchPath = "/bin/" + program
}
print("Running program:", launchPath)
let task = STPrivilegedTask()
task.launchPath = launchPath
task.arguments = arguments
task.freeAuthorizationWhenDone = freeAuthorization
let errorCode: OSStatus
if let authorization = authorization
{
errorCode = task.launch(withAuthorization: authorization)
}
else
{
errorCode = task.launch()
}
guard errorCode == 0 else { throw PluginError.taskErrorCode(Int(errorCode)) }
task.waitUntilExit()
print("Exit code:", task.terminationStatus)
guard task.terminationStatus == 0 else {
let outputData = task.outputFileHandle.readDataToEndOfFile()
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
{
throw PluginError.taskError(outputString)
}
throw PluginError.taskErrorCode(Int(task.terminationStatus))
}
guard let authorization = task.authorization else { throw PluginError.unknown }
return authorization
}
}
extension AppDelegate: NSMenuDelegate

View File

@@ -21,9 +21,8 @@ extension ALTServerError
case let error as ALTServerError: self = error
case is DecodingError: self = ALTServerError(.invalidRequest)
case is EncodingError: self = ALTServerError(.invalidResponse)
default:
assertionFailure("Caught unknown error type")
self = ALTServerError(.unknown)
case let error as NSError:
self = ALTServerError(.unknown, userInfo: error.userInfo)
}
}
}
@@ -266,7 +265,15 @@ private extension ConnectionManager
case .success(.prepareApp(let request)):
self.handlePrepareAppRequest(request, for: connection)
case .success:
case .success(.beginInstallation): break
case .success(.installProvisioningProfiles(let request)):
self.handleInstallProvisioningProfilesRequest(request, for: connection)
case .success(.removeProvisioningProfiles(let request)):
self.handleRemoveProvisioningProfilesRequest(request, for: connection)
case .success(.unknown):
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent unknown request response with result:", result)
@@ -344,10 +351,10 @@ private extension ConnectionManager
switch result
{
case .failure(let error): finish(.failure(error))
case .success(.beginInstallation):
case .success(.beginInstallation(let installRequest)):
print("Installing to device \(request.udid)...")
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, activeProvisioningProfiles: installRequest.activeProfiles, connection: connection) { (result) in
print("Installed to device with result:", result)
switch result
{
@@ -396,14 +403,14 @@ private extension ConnectionManager
}
}
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, connection: ClientConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, activeProvisioningProfiles: Set<String>?, connection: ClientConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
var isSending = false
var observation: NSKeyValueObservation?
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid) { (success, error) in
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid, activeProvisioningProfiles: activeProvisioningProfiles) { (success, error) in
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
if let error = error.map({ $0 as? ALTServerError ?? ALTServerError(.unknown) })
@@ -435,6 +442,54 @@ private extension ConnectionManager
}
})
}
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: ClientConnection)
{
ALTDeviceManager.shared.installProvisioningProfiles(request.provisioningProfiles, toDeviceWithUDID: request.udid, activeProvisioningProfiles: request.activeProfiles) { (success, error) in
if let error = error, !success
{
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
let errorResponse = ErrorResponse(error: ALTServerError(error))
connection.send(errorResponse, shouldDisconnect: true) { (result) in
print("Sent install profiles error response with result:", result)
}
}
else
{
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
let response = InstallProvisioningProfilesResponse()
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent install profiles response to \(connection) with result:", result)
}
}
}
}
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: ClientConnection)
{
ALTDeviceManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers, fromDeviceWithUDID: request.udid) { (success, error) in
if let error = error, !success
{
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
let errorResponse = ErrorResponse(error: ALTServerError(error))
connection.send(errorResponse, shouldDisconnect: true) { (result) in
print("Sent remove profiles error response with result:", result)
}
}
else
{
print("Removed profiles:", request.bundleIdentifiers)
let response = RemoveProvisioningProfilesResponse()
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent remove profiles error response to \(connection) with result:", result)
}
}
}
}
}
private extension ConnectionManager

View File

@@ -536,7 +536,8 @@ To prevent this from happening, feel free to try again with another Apple ID to
{
try Result(success, error).get()
ALTDeviceManager.shared.installApp(at: application.fileURL, toDeviceWithUDID: device.identifier) { (success, error) in
let activeProfiles: Set<String>? = (team.type == .free) ? [profile.bundleIdentifier] : nil
ALTDeviceManager.shared.installApp(at: application.fileURL, toDeviceWithUDID: device.identifier, activeProvisioningProfiles: activeProfiles) { (success, error) in
completionHandler(Result(success, error))
}
}

View File

@@ -27,7 +27,10 @@ extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification
- (void)start;
/* App Installation */
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
- (void)installProvisioningProfiles:(NSSet<ALTProvisioningProfile *> *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
- (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet<NSString *> *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
/* Connections */
- (void)startWiredConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTWiredConnection *_Nullable connection, NSError *_Nullable error))completionHandler;

View File

@@ -71,7 +71,7 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
#pragma mark - App Installation -
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
{
NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:4];
@@ -88,49 +88,51 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
__block misagent_client_t mis = NULL;
__block lockdownd_service_descriptor_t service = NULL;
NSURL *removedProfilesDirectoryURL = [[[NSFileManager defaultManager] temporaryDirectory] URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
NSMutableDictionary<NSString *, ALTProvisioningProfile *> *preferredProfiles = [NSMutableDictionary dictionary];
NSMutableDictionary<NSString *, ALTProvisioningProfile *> *cachedProfiles = [NSMutableDictionary dictionary];
NSMutableSet<ALTProvisioningProfile *> *installedProfiles = [NSMutableSet set];
void (^finish)(NSError *error) = ^(NSError *error) {
void (^finish)(NSError *error) = ^(NSError *e) {
__block NSError *error = e;
if ([[NSFileManager defaultManager] fileExistsAtPath:removedProfilesDirectoryURL.path isDirectory:nil])
if (activeProvisioningProfiles != nil)
{
// Reinstall all provisioning profiles we removed before installation.
// Remove installed provisioning profiles if they're not active.
NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:removedProfilesDirectoryURL.path error:nil];
for (NSString *filename in contents)
for (ALTProvisioningProfile *installedProfile in installedProfiles)
{
NSURL *fileURL = [removedProfilesDirectoryURL URLByAppendingPathComponent:filename];
ALTProvisioningProfile *provisioningProfile = [[ALTProvisioningProfile alloc] initWithURL:fileURL];
if (provisioningProfile == nil)
if (![activeProvisioningProfiles containsObject:installedProfile.bundleIdentifier])
{
continue;
NSError *removeError = nil;
if (![self removeProvisioningProfile:installedProfile misagent:mis error:&removeError])
{
if (error == nil)
{
error = removeError;
}
}
}
ALTProvisioningProfile *preferredProfile = preferredProfiles[provisioningProfile.bundleIdentifier];
if (![preferredProfile isEqual:provisioningProfile])
}
}
[cachedProfiles enumerateKeysAndObjectsUsingBlock:^(NSString *bundleID, ALTProvisioningProfile *profile, BOOL * _Nonnull stop) {
for (ALTProvisioningProfile *installedProfile in installedProfiles)
{
if ([installedProfile.bundleIdentifier isEqualToString:profile.bundleIdentifier])
{
continue;
// Don't reinstall cached profile because it was installed with the app.
return;
}
plist_t pdata = plist_new_data((const char *)provisioningProfile.data.bytes, provisioningProfile.data.length);
if (misagent_install(mis, pdata) == MISAGENT_E_SUCCESS)
{
NSLog(@"Reinstalled profile: %@", provisioningProfile.UUID);
}
else
{
int code = misagent_get_status_code(mis);
NSLog(@"Failed to reinstall provisioning profile %@. (%@)", provisioningProfile.UUID, @(code));
}
plist_free(pdata);
}
[[NSFileManager defaultManager] removeItemAtURL:removedProfilesDirectoryURL error:nil];
}
NSError *installError = nil;
if (![self installProvisioningProfile:profile misagent:mis error:&installError])
{
if (error == nil)
{
error = installError;
}
}
}];
instproxy_client_free(ipc);
afc_client_free(afc);
@@ -183,6 +185,20 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
return finish([NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: fileURL}]);
}
ALTApplication *application = [[ALTApplication alloc] initWithFileURL:appBundleURL];
if (application.provisioningProfile)
{
[installedProfiles addObject:application.provisioningProfile];
}
for (ALTApplication *appExtension in application.appExtensions)
{
if (appExtension.provisioningProfile)
{
[installedProfiles addObject:appExtension.provisioningProfile];
}
}
/* Find Device */
if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS)
{
@@ -286,104 +302,39 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
service = NULL;
}
/* Provisioning Profiles */
NSURL *provisioningProfileURL = [appBundleURL URLByAppendingPathComponent:@"embedded.mobileprovision"];
ALTProvisioningProfile *installationProvisioningProfile = [[ALTProvisioningProfile alloc] initWithURL:provisioningProfileURL];
if (installationProvisioningProfile != nil)
BOOL shouldManageProfiles = (activeProvisioningProfiles != nil || [application.provisioningProfile isFreeProvisioningProfile]);
if (shouldManageProfiles)
{
// Free developer account was used to sign this app, so we need to remove all
// provisioning profiles in order to remain under sideloaded app limit.
NSError *error = nil;
if (![[NSFileManager defaultManager] createDirectoryAtURL:removedProfilesDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error])
NSDictionary<NSString *, ALTProvisioningProfile *> *removedProfiles = [self removeAllFreeProfilesExcludingBundleIdentifiers:nil misagent:mis error:&error];
if (removedProfiles == nil)
{
return finish(error);
}
plist_t rawProfiles = NULL;
if (misagent_copy_all(mis, &rawProfiles) != MISAGENT_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
// For some reason, libplist now fails to parse `rawProfiles` correctly.
// Specifically, it no longer recognizes the nodes in the plist array as "data" nodes.
// However, if we encode it as XML then decode it again, it'll work ¯\_(ツ)_/¯
char *plistXML = nullptr;
uint32_t plistLength = 0;
plist_to_xml(rawProfiles, &plistXML, &plistLength);
plist_t profiles = NULL;
plist_from_xml(plistXML, plistLength, &profiles);
free(plistXML);
uint32_t profileCount = plist_array_get_size(profiles);
for (int i = 0; i < profileCount; i++)
{
plist_t profile = plist_array_get_item(profiles, i);
if (plist_get_node_type(profile) != PLIST_DATA)
[removedProfiles enumerateKeysAndObjectsUsingBlock:^(NSString *bundleID, ALTProvisioningProfile *profile, BOOL * _Nonnull stop) {
if (activeProvisioningProfiles != nil)
{
continue;
}
char *bytes = NULL;
uint64_t length = 0;
plist_get_data_val(profile, &bytes, &length);
if (bytes == NULL)
{
continue;
}
NSData *data = [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:YES];
ALTProvisioningProfile *provisioningProfile = [[ALTProvisioningProfile alloc] initWithData:data];
if (![provisioningProfile isFreeProvisioningProfile])
{
NSLog(@"Ignoring: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier);
continue;
}
ALTProvisioningProfile *preferredProfile = preferredProfiles[provisioningProfile.bundleIdentifier];
if (preferredProfile != nil)
{
if ([provisioningProfile.expirationDate compare:preferredProfile.expirationDate] == NSOrderedDescending)
if ([activeProvisioningProfiles containsObject:bundleID])
{
preferredProfiles[provisioningProfile.bundleIdentifier] = provisioningProfile;
// Only cache active profiles to reinstall afterwards.
cachedProfiles[bundleID] = profile;
}
}
else
{
preferredProfiles[provisioningProfile.bundleIdentifier] = provisioningProfile;
// Cache all profiles to reinstall afterwards if we didn't provide activeProvisioningProfiles.
cachedProfiles[bundleID] = profile;
}
NSString *filename = [NSString stringWithFormat:@"%@.mobileprovision", [[NSUUID UUID] UUIDString]];
NSURL *fileURL = [removedProfilesDirectoryURL URLByAppendingPathComponent:filename];
NSError *copyError = nil;
if (![provisioningProfile.data writeToURL:fileURL options:NSDataWritingAtomic error:&copyError])
{
NSLog(@"Failed to copy profile to temporary URL. %@", copyError);
continue;
}
if (misagent_remove(mis, provisioningProfile.UUID.UUIDString.lowercaseString.UTF8String) == MISAGENT_E_SUCCESS)
{
NSLog(@"Removed provisioning profile: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier);
}
else
{
int code = misagent_get_status_code(mis);
NSLog(@"Failed to remove provisioning profile %@ (Team: %@). Error Code: %@", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier, @(code));
}
}
plist_free(rawProfiles);
plist_free(profiles);
lockdownd_client_free(client);
client = NULL;
}];
}
lockdownd_client_free(client);
client = NULL;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSProgress *installationProgress = [NSProgress progressWithTotalUnitCount:100 parent:progress pendingUnitCount:1];
@@ -547,6 +498,385 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
return success;
}
#pragma mark - Provisioning Profiles -
- (void)installProvisioningProfiles:(NSSet<ALTProvisioningProfile *> *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *error))completionHandler
{
dispatch_async(self.installationQueue, ^{
__block idevice_t device = NULL;
__block lockdownd_client_t client = NULL;
__block afc_client_t afc = NULL;
__block misagent_client_t mis = NULL;
__block lockdownd_service_descriptor_t service = NULL;
void (^finish)(NSError *_Nullable) = ^(NSError *error) {
lockdownd_service_descriptor_free(service);
misagent_client_free(mis);
afc_client_free(afc);
lockdownd_client_free(client);
idevice_free(device);
completionHandler(error == nil, error);
};
/* Find Device */
if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
}
/* Connect to Device */
if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
/* Connect to Misagent */
if (lockdownd_start_service(client, "com.apple.misagent", &service) != LOCKDOWN_E_SUCCESS || service == NULL)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
if (misagent_client_new(device, service, &mis) != MISAGENT_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
NSError *error = nil;
if (activeProvisioningProfiles != nil)
{
// Remove all non-active free provisioning profiles.
NSMutableSet *excludedBundleIdentifiers = [activeProvisioningProfiles mutableCopy];
for (ALTProvisioningProfile *provisioningProfile in provisioningProfiles)
{
// Ensure we DO remove old versions of profiles we're about to install, even if they are active.
[excludedBundleIdentifiers removeObject:provisioningProfile.bundleIdentifier];
}
if (![self removeAllFreeProfilesExcludingBundleIdentifiers:excludedBundleIdentifiers misagent:mis error:&error])
{
return finish(error);
}
}
else
{
// Remove only older versions of provisioning profiles we're about to install.
NSMutableSet *bundleIdentifiers = [NSMutableSet set];
for (ALTProvisioningProfile *provisioningProfile in provisioningProfiles)
{
[bundleIdentifiers addObject:provisioningProfile.bundleIdentifier];
}
if (![self removeProvisioningProfilesForBundleIdentifiers:bundleIdentifiers misagent:mis error:&error])
{
return finish(error);
}
}
for (ALTProvisioningProfile *provisioningProfile in provisioningProfiles)
{
if (![self installProvisioningProfile:provisioningProfile misagent:mis error:&error])
{
return finish(error);
}
}
finish(nil);
});
}
- (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet<NSString *> *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *error))completionHandler
{
dispatch_async(self.installationQueue, ^{
__block idevice_t device = NULL;
__block lockdownd_client_t client = NULL;
__block afc_client_t afc = NULL;
__block misagent_client_t mis = NULL;
__block lockdownd_service_descriptor_t service = NULL;
void (^finish)(NSError *_Nullable) = ^(NSError *error) {
lockdownd_service_descriptor_free(service);
misagent_client_free(mis);
afc_client_free(afc);
lockdownd_client_free(client);
idevice_free(device);
completionHandler(error == nil, error);
};
/* Find Device */
if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
}
/* Connect to Device */
if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
/* Connect to Misagent */
if (lockdownd_start_service(client, "com.apple.misagent", &service) != LOCKDOWN_E_SUCCESS || service == NULL)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
if (misagent_client_new(device, service, &mis) != MISAGENT_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
NSError *error = nil;
if (![self removeProvisioningProfilesForBundleIdentifiers:bundleIdentifiers misagent:mis error:&error])
{
return finish(error);
}
finish(nil);
});
}
- (NSDictionary<NSString *, ALTProvisioningProfile *> *)removeProvisioningProfilesForBundleIdentifiers:(NSSet<NSString *> *)bundleIdentifiers misagent:(misagent_client_t)mis error:(NSError **)error
{
return [self removeAllProfilesForBundleIdentifiers:bundleIdentifiers excludingBundleIdentifiers:nil limitedToFreeProfiles:NO misagent:mis error:error];
}
- (NSDictionary<NSString *, ALTProvisioningProfile *> *)removeAllFreeProfilesExcludingBundleIdentifiers:(nullable NSSet<NSString *> *)bundleIdentifiers misagent:(misagent_client_t)mis error:(NSError **)error
{
return [self removeAllProfilesForBundleIdentifiers:nil excludingBundleIdentifiers:bundleIdentifiers limitedToFreeProfiles:YES misagent:mis error:error];
}
- (NSDictionary<NSString *, ALTProvisioningProfile *> *)removeAllProfilesForBundleIdentifiers:(nullable NSSet<NSString *> *)includedBundleIdentifiers
excludingBundleIdentifiers:(nullable NSSet<NSString *> *)excludedBundleIdentifiers
limitedToFreeProfiles:(BOOL)limitedToFreeProfiles
misagent:(misagent_client_t)mis
error:(NSError **)error
{
NSMutableDictionary<NSString *, ALTProvisioningProfile *> *ignoredProfiles = [NSMutableDictionary dictionary];
NSMutableDictionary<NSString *, ALTProvisioningProfile *> *removedProfiles = [NSMutableDictionary dictionary];
NSArray<ALTProvisioningProfile *> *provisioningProfiles = [self copyProvisioningProfilesWithClient:mis error:error];
if (provisioningProfiles == nil)
{
return nil;
}
for (ALTProvisioningProfile *provisioningProfile in provisioningProfiles)
{
if (limitedToFreeProfiles && ![provisioningProfile isFreeProvisioningProfile])
{
continue;
}
if (includedBundleIdentifiers != nil && ![includedBundleIdentifiers containsObject:provisioningProfile.bundleIdentifier])
{
continue;
}
if (excludedBundleIdentifiers != nil && [excludedBundleIdentifiers containsObject:provisioningProfile.bundleIdentifier])
{
// This provisioning profile has an excluded bundle identifier.
// Ignore it, unless we've already ignored one with the same bundle identifier,
// in which case remove whichever profile is the oldest.
ALTProvisioningProfile *previousProfile = ignoredProfiles[provisioningProfile.bundleIdentifier];
if (previousProfile != nil)
{
// We've already ignored a profile with this bundle identifier,
// so make sure we only ignore the newest one and remove the oldest one.
BOOL isNewerThanPreviousProfile = ([provisioningProfile.expirationDate compare:previousProfile.expirationDate] == NSOrderedDescending);
ALTProvisioningProfile *oldestProfile = isNewerThanPreviousProfile ? previousProfile : provisioningProfile;
ALTProvisioningProfile *newestProfile = isNewerThanPreviousProfile ? provisioningProfile : previousProfile;
ignoredProfiles[provisioningProfile.bundleIdentifier] = newestProfile;
// Don't cache this profile or else it will be reinstalled, so just remove it without caching.
if (![self removeProvisioningProfile:oldestProfile misagent:mis error:error])
{
return nil;
}
}
else
{
ignoredProfiles[provisioningProfile.bundleIdentifier] = provisioningProfile;
}
continue;
}
ALTProvisioningProfile *preferredProfile = removedProfiles[provisioningProfile.bundleIdentifier];
if (preferredProfile != nil)
{
if ([provisioningProfile.expirationDate compare:preferredProfile.expirationDate] == NSOrderedDescending)
{
removedProfiles[provisioningProfile.bundleIdentifier] = provisioningProfile;
}
}
else
{
removedProfiles[provisioningProfile.bundleIdentifier] = provisioningProfile;
}
if (![self removeProvisioningProfile:provisioningProfile misagent:mis error:error])
{
return nil;
}
}
return removedProfiles;
}
- (BOOL)installProvisioningProfile:(ALTProvisioningProfile *)provisioningProfile misagent:(misagent_client_t)mis error:(NSError **)error
{
plist_t pdata = plist_new_data((const char *)provisioningProfile.data.bytes, provisioningProfile.data.length);
misagent_error_t result = misagent_install(mis, pdata);
plist_free(pdata);
if (result == MISAGENT_E_SUCCESS)
{
NSLog(@"Installed profile: %@ (%@)", provisioningProfile.bundleIdentifier, provisioningProfile.UUID);
return YES;
}
else
{
int statusCode = misagent_get_status_code(mis);
NSLog(@"Failed to install provisioning profile %@ (%@). Error Code: %@", provisioningProfile.bundleIdentifier, provisioningProfile.UUID, @(statusCode));
if (error)
{
switch (statusCode)
{
case -402620383:
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorMaximumFreeAppLimitReached userInfo:nil];
break;
default:
NSString *localizedFailure = [NSString stringWithFormat:NSLocalizedString(@"Could not install profile “%@”", @""), provisioningProfile.bundleIdentifier];
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnderlyingError userInfo:@{
NSLocalizedFailureErrorKey: localizedFailure,
ALTUnderlyingErrorCodeErrorKey: [@(statusCode) description],
ALTProvisioningProfileBundleIDErrorKey: provisioningProfile.bundleIdentifier
}];
}
}
return NO;
}
}
- (BOOL)removeProvisioningProfile:(ALTProvisioningProfile *)provisioningProfile misagent:(misagent_client_t)mis error:(NSError **)error
{
misagent_error_t result = misagent_remove(mis, provisioningProfile.UUID.UUIDString.lowercaseString.UTF8String);
if (result == MISAGENT_E_SUCCESS)
{
NSLog(@"Removed provisioning profile: %@ (%@)", provisioningProfile.bundleIdentifier, provisioningProfile.UUID);
return YES;
}
else
{
int statusCode = misagent_get_status_code(mis);
NSLog(@"Failed to remove provisioning profile %@ (%@). Error Code: %@", provisioningProfile.bundleIdentifier, provisioningProfile.UUID, @(statusCode));
if (error)
{
switch (statusCode)
{
case -402620405:
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorProfileNotFound userInfo:nil];
break;
default:
{
NSString *localizedFailure = [NSString stringWithFormat:NSLocalizedString(@"Could not remove profile “%@”", @""), provisioningProfile.bundleIdentifier];
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnderlyingError userInfo:@{
NSLocalizedFailureErrorKey: localizedFailure,
ALTUnderlyingErrorCodeErrorKey: [@(statusCode) description],
ALTProvisioningProfileBundleIDErrorKey: provisioningProfile.bundleIdentifier
}];
}
}
}
return NO;
}
}
- (nullable NSArray<ALTProvisioningProfile *> *)copyProvisioningProfilesWithClient:(misagent_client_t)mis error:(NSError **)error
{
plist_t rawProfiles = NULL;
misagent_error_t result = misagent_copy_all(mis, &rawProfiles);
if (result != MISAGENT_E_SUCCESS)
{
int statusCode = misagent_get_status_code(mis);
if (error)
{
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnderlyingError userInfo:@{
NSLocalizedFailureErrorKey: NSLocalizedString(@"Could not copy provisioning profiles.", @""),
ALTUnderlyingErrorCodeErrorKey: [@(statusCode) description]
}];
}
return nil;
}
/* Copy all provisioning profiles */
// For some reason, libplist now fails to parse `rawProfiles` correctly.
// Specifically, it no longer recognizes the nodes in the plist array as "data" nodes.
// However, if we encode it as XML then decode it again, it'll work ¯\_(ツ)_/¯
char *plistXML = nullptr;
uint32_t plistLength = 0;
plist_to_xml(rawProfiles, &plistXML, &plistLength);
plist_t profiles = NULL;
plist_from_xml(plistXML, plistLength, &profiles);
free(plistXML);
NSMutableArray<ALTProvisioningProfile *> *provisioningProfiles = [NSMutableArray array];
uint32_t profileCount = plist_array_get_size(profiles);
for (int i = 0; i < profileCount; i++)
{
plist_t profile = plist_array_get_item(profiles, i);
if (plist_get_node_type(profile) != PLIST_DATA)
{
continue;
}
char *bytes = NULL;
uint64_t length = 0;
plist_get_data_val(profile, &bytes, &length);
if (bytes == NULL)
{
continue;
}
NSData *data = [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:YES];
ALTProvisioningProfile *provisioningProfile = [[ALTProvisioningProfile alloc] initWithData:data];
if (provisioningProfile == nil)
{
continue;
}
[provisioningProfiles addObject:provisioningProfile];
}
plist_free(rawProfiles);
plist_free(profiles);
return provisioningProfiles;
}
#pragma mark - Connections -
- (void)startWiredConnectionToDevice:(ALTDevice *)altDevice completionHandler:(void (^)(ALTWiredConnection * _Nullable, NSError * _Nullable))completionHandler

View File

@@ -1,13 +0,0 @@
#!/bin/sh
# InstallAltPlugin.sh
# AltStore
#
# Created by Riley Testut on 11/16/19.
# Copyright © 2019 Riley Testut. All rights reserved.
rm -f AltPlugin.mailbundle
unzip AltPlugin.mailbundle.zip 1>/dev/null
mkdir -p /Library/Mail/Bundles
cp -r AltPlugin.mailbundle /Library/Mail/Bundles
defaults write "/Library/Preferences/com.apple.mail" EnableBundles 1

View File

@@ -1,9 +0,0 @@
#!/bin/sh
# UninstallPlugin.sh
# AltStore
#
# Created by Riley Testut on 11/16/19.
# Copyright © 2019 Riley Testut. All rights reserved.
rm -rf /Library/Mail/Bundles/AltPlugin.mailbundle

View File

@@ -19,6 +19,7 @@
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858222DE795100DE9F1E /* MyAppsViewController.swift */; };
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */; };
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */; };
BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0DCA652433BDF500E3A595 /* AnalyticsManager.swift */; };
BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF0F5FC623F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel */; };
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */; };
BF100C54232D7DAE006A8926 /* StoreAppPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */; };
@@ -34,6 +35,8 @@
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */; };
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; };
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; };
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */; };
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */; };
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; };
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648C22E79AC800E9056B /* ALTAppPermission.m */; };
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */; };
@@ -115,12 +118,17 @@
BF45884B2298D55000BD7491 /* thread.h in Headers */ = {isa = PBXBuildFile; fileRef = BF4588492298D55000BD7491 /* thread.h */; };
BF4588882298DD3F00BD7491 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4588872298DD3F00BD7491 /* libxml2.tbd */; };
BF4C7F2523801F0800B2556E /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9B63C5229DD44D002F0A62 /* AltSign.framework */; };
BF4C7F27238086EB00B2556E /* InstallPlugin.sh in Resources */ = {isa = PBXBuildFile; fileRef = BF4C7F26238086EB00B2556E /* InstallPlugin.sh */; };
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */; };
BF56D2AA23DF88310006506D /* AppID.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2A923DF88310006506D /* AppID.swift */; };
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */; };
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */; };
BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; };
BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */; };
BF6C336224197D700034FD24 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; };
BF6C33652419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */; };
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */; };
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */; };
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */; };
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */; };
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */; };
BF718BD123C91BD300A89F2D /* ALTWiredConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD023C91BD300A89F2D /* ALTWiredConnection.m */; };
@@ -128,16 +136,15 @@
BF718BD823C93DB700A89F2D /* AltKit.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD723C93DB700A89F2D /* AltKit.m */; };
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */; };
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; };
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* AppOperationContext.swift */; };
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* OperationContexts.swift */; };
BF770E5622BC3C03002A40FE /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5522BC3C02002A40FE /* Server.swift */; };
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.swift */; };
BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */; };
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */; };
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; };
BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */; };
BF7C627423DBB78C00515A2D /* InstalledAppPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */; };
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C122E659F700049BA1 /* AppContentViewController.swift */; };
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C322E662D300049BA1 /* AppViewController.swift */; };
BF914C262383703800E713BA /* AltPlugin.mailbundle.zip in Resources */ = {isa = PBXBuildFile; fileRef = BF914C252383703800E713BA /* AltPlugin.mailbundle.zip */; };
BF9A03C623C7DD0D000D08DB /* ClientConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9A03C523C7DD0D000D08DB /* ClientConnection.swift */; };
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */; };
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */; };
@@ -145,12 +152,12 @@
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4A22DD137F008935CF /* NavigationBar.swift */; };
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* PillButton.swift */; };
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */; };
BF9F8D1A242AA6BC0024E48B /* AltStore5ToAltStore6.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF9F8D19242AA6BC0024E48B /* AltStore5ToAltStore6.xcmappingmodel */; };
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172823C56042001B5953 /* ServerConnection.swift */; };
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; };
BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172C23C5823E001B5953 /* InstalledExtension.swift */; };
BFA8172F23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */; };
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; };
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; };
BFB1169B2293274D00BB457C /* JSONDecoder+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+Properties.swift */; };
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; };
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; };
BFB49AAA23834CF900D542D9 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */; };
@@ -162,6 +169,10 @@
BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* StoreApp.swift */; };
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; };
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */; };
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */; };
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */; };
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */; };
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC84A4C2421A19100853474 /* SourcesViewController.swift */; };
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; };
BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; };
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; };
@@ -169,6 +180,7 @@
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478B2284C4C300981D42 /* AppIconImageView.swift */; };
BFD2478F2284C8F900981D42 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478E2284C8F900981D42 /* Button.swift */; };
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */; };
BFD44606241188C400EAB90A /* CodableServerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD44605241188C300EAB90A /* CodableServerError.swift */; };
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BD322A0800A000B7ED1 /* ServerManager.swift */; };
BFD52C0122A1A9CB000B7ED1 /* ptrarray.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */; };
BFD52C0222A1A9CB000B7ED1 /* base64.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE622A1A9CA000B7ED1 /* base64.c */; };
@@ -208,7 +220,6 @@
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F3230DDB0A007955AB /* Campaign.swift */; };
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F5230DDB12007955AB /* Tier.swift */; };
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */; };
BFD80D572380C0F700B9C227 /* UninstallPlugin.sh in Resources */ = {isa = PBXBuildFile; fileRef = BFD80D562380C0F700B9C227 /* UninstallPlugin.sh */; };
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */; };
BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */; };
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */; };
@@ -229,8 +240,6 @@
BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; };
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; };
BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */; };
BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE944023F22AA100CDA07D /* AppIDComponents.swift */; };
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; };
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; };
BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */; };
@@ -316,6 +325,7 @@
BF08858222DE795100DE9F1E /* MyAppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewController.swift; sourceTree = "<group>"; };
BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCollectionViewCell.swift; sourceTree = "<group>"; };
BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = "<group>"; };
BF0DCA652433BDF500E3A595 /* AnalyticsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsManager.swift; sourceTree = "<group>"; };
BF0F5FC623F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore3ToAltStore4.xcmappingmodel; sourceTree = "<group>"; };
BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 2.xcdatamodel"; sourceTree = "<group>"; };
BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStoreToAltStore2.xcmappingmodel; sourceTree = "<group>"; };
@@ -334,6 +344,8 @@
BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = "<group>"; };
BF29012E2318F6B100D88A45 /* AppBannerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AppBannerView.xib; sourceTree = "<group>"; };
BF2901302318F7A800D88A45 /* AppBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBannerView.swift; sourceTree = "<group>"; };
BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchProvisioningProfilesOperation.swift; sourceTree = "<group>"; };
BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAppOperation.swift; sourceTree = "<group>"; };
BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; };
BF3D648B22E79AC800E9056B /* ALTAppPermission.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = "<group>"; };
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = "<group>"; };
@@ -422,7 +434,6 @@
BF4588872298DD3F00BD7491 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
BF4588962298DE6E00BD7491 /* libzip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libzip.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF4713A422976CFC00784A2F /* openssl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = openssl.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF4C7F26238086EB00B2556E /* InstallPlugin.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = InstallPlugin.sh; sourceTree = "<group>"; };
BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = "<group>"; };
BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = "<group>"; };
BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 4.xcdatamodel"; sourceTree = "<group>"; };
@@ -434,6 +445,14 @@
BF5C5FC7237DF5AE00EDD0C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BF5C5FCD237DF69100EDD0C6 /* ALTPluginService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPluginService.h; sourceTree = "<group>"; };
BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPluginService.m; sourceTree = "<group>"; };
BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+DirectorySize.swift"; sourceTree = "<group>"; };
BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+LocalizedFailure.swift"; sourceTree = "<group>"; };
BF6C33632419ADEB0034FD24 /* AltStore 5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 5.xcdatamodel"; sourceTree = "<group>"; };
BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore4ToAltStore5.xcmappingmodel; sourceTree = "<group>"; };
BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSAttributedString+Markdown.m"; path = "Dependencies/MarkdownAttributedString/NSAttributedString+Markdown.m"; sourceTree = SOURCE_ROOT; };
BF6C8FAB242935ED00125131 /* NSAttributedString+Markdown.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSAttributedString+Markdown.h"; path = "Dependencies/MarkdownAttributedString/NSAttributedString+Markdown.h"; sourceTree = SOURCE_ROOT; };
BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerCollectionViewCell.swift; sourceTree = "<group>"; };
BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCollectionReusableView.swift; sourceTree = "<group>"; };
BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAltStoreViewController.swift; sourceTree = "<group>"; };
BF718BC723C919CC00A89F2D /* CFNotificationName+AltStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CFNotificationName+AltStore.h"; sourceTree = "<group>"; };
BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CFNotificationName+AltStore.m"; sourceTree = "<group>"; };
@@ -446,9 +465,9 @@
BF718BD723C93DB700A89F2D /* AltKit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AltKit.m; sourceTree = "<group>"; };
BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardingNavigationController.swift; sourceTree = "<group>"; };
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = "<group>"; };
BF770E5322BC044E002A40FE /* AppOperationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOperationContext.swift; sourceTree = "<group>"; };
BF770E5322BC044E002A40FE /* OperationContexts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationContexts.swift; sourceTree = "<group>"; };
BF770E5522BC3C02002A40FE /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
BF770E5722BC3D0F002A40FE /* OperationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationGroup.swift; sourceTree = "<group>"; };
BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshGroup.swift; sourceTree = "<group>"; };
BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = "<group>"; };
BF770E6822BD57DD002A40FE /* Silence.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Silence.m4a; sourceTree = "<group>"; };
BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 3.xcdatamodel"; sourceTree = "<group>"; };
@@ -456,7 +475,6 @@
BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppPolicy.swift; sourceTree = "<group>"; };
BF8F69C122E659F700049BA1 /* AppContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContentViewController.swift; sourceTree = "<group>"; };
BF8F69C322E662D300049BA1 /* AppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = "<group>"; };
BF914C252383703800E713BA /* AltPlugin.mailbundle.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = AltPlugin.mailbundle.zip; sourceTree = "<group>"; };
BF9A03C523C7DD0D000D08DB /* ClientConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConnection.swift; sourceTree = "<group>"; };
BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewController.swift; sourceTree = "<group>"; };
BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseCollectionViewCell.swift; sourceTree = "<group>"; };
@@ -465,12 +483,13 @@
BF9ABA4C22DD16DE008935CF /* PillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButton.swift; sourceTree = "<group>"; };
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Hex.swift"; sourceTree = "<group>"; };
BF9B63C5229DD44D002F0A62 /* AltSign.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF9F8D18242AA6680024E48B /* AltStore 6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 6.xcdatamodel"; sourceTree = "<group>"; };
BF9F8D19242AA6BC0024E48B /* AltStore5ToAltStore6.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore5ToAltStore6.xcmappingmodel; sourceTree = "<group>"; };
BFA8172823C56042001B5953 /* ServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnection.swift; sourceTree = "<group>"; };
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnisetteDataOperation.swift; sourceTree = "<group>"; };
BFA8172C23C5823E001B5953 /* InstalledExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledExtension.swift; sourceTree = "<group>"; };
BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrepareDeveloperAccountOperation.swift; sourceTree = "<group>"; };
BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; };
BFB1169A2293274D00BB457C /* JSONDecoder+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+Properties.swift"; sourceTree = "<group>"; };
BFB1169C22932DB100BB457C /* apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apps.json; sourceTree = "<group>"; };
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindServerOperation.swift; sourceTree = "<group>"; };
BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UpdateCollectionViewCell.xib; sourceTree = "<group>"; };
@@ -485,6 +504,10 @@
BFBBE2DE22931F73002097FA /* StoreApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp.swift; sourceTree = "<group>"; };
BFBBE2E022931F81002097FA /* InstalledApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledApp.swift; sourceTree = "<group>"; };
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAppOperation.swift; sourceTree = "<group>"; };
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeactivateAppOperation.swift; sourceTree = "<group>"; };
BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppsCollectionHeaderView.swift; sourceTree = "<group>"; };
BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstalledAppsCollectionHeaderView.xib; sourceTree = "<group>"; };
BFC84A4C2421A19100853474 /* SourcesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesViewController.swift; sourceTree = "<group>"; };
BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; };
BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
BFD247742284B9A500981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
@@ -495,6 +518,7 @@
BFD2478B2284C4C300981D42 /* AppIconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconImageView.swift; sourceTree = "<group>"; };
BFD2478E2284C8F900981D42 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = "<group>"; };
BFD44605241188C300EAB90A /* CodableServerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableServerError.swift; sourceTree = "<group>"; };
BFD52BD222A06EFB000B7ED1 /* AltKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltKit.h; sourceTree = "<group>"; };
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = "<group>"; };
BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = ptrarray.c; path = Dependencies/libplist/src/ptrarray.c; sourceTree = SOURCE_ROOT; };
@@ -535,7 +559,6 @@
BFD5D6F3230DDB0A007955AB /* Campaign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Campaign.swift; sourceTree = "<group>"; };
BFD5D6F5230DDB12007955AB /* Tier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tier.swift; sourceTree = "<group>"; };
BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsComponents.swift; sourceTree = "<group>"; };
BFD80D562380C0F700B9C227 /* UninstallPlugin.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = UninstallPlugin.sh; sourceTree = "<group>"; };
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+RelativeDate.swift"; sourceTree = "<group>"; };
BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BrowseCollectionViewCell.xib; sourceTree = "<group>"; };
BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = "<group>"; };
@@ -556,8 +579,6 @@
BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = "<group>"; };
BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = "<group>"; };
BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTApplication+AppExtensions.swift"; sourceTree = "<group>"; };
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDComponents.swift; sourceTree = "<group>"; };
BFF0B68D23219520007A79E1 /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = "<group>"; };
BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonComponents.swift; sourceTree = "<group>"; };
BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AboutPatreonHeaderView.xib; sourceTree = "<group>"; };
@@ -637,6 +658,14 @@
path = Extensions;
sourceTree = "<group>";
};
BF0DCA642433BDE200E3A595 /* Analytics */ = {
isa = PBXGroup;
children = (
BF0DCA652433BDF500E3A595 /* AnalyticsManager.swift */,
);
path = Analytics;
sourceTree = "<group>";
};
BF100C4E232D7C95006A8926 /* Migrations */ = {
isa = PBXGroup;
children = (
@@ -652,6 +681,8 @@
BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */,
BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */,
BF0F5FC623F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel */,
BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */,
BF9F8D19242AA6BC0024E48B /* AltStore5ToAltStore6.xcmappingmodel */,
);
path = "Mapping Models";
sourceTree = "<group>";
@@ -673,6 +704,7 @@
BFBAC8852295C90300587369 /* Result+Conveniences.swift */,
BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */,
BF1E3128229F474900370A3C /* ServerProtocol.swift */,
BFD44605241188C300EAB90A /* CodableServerError.swift */,
BF1E314822A060F400370A3C /* NSError+ALTServerError.h */,
BF1E314922A060F400370A3C /* NSError+ALTServerError.m */,
BF718BC723C919CC00A89F2D /* CFNotificationName+AltStore.h */,
@@ -864,7 +896,6 @@
isa = PBXGroup;
children = (
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */,
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */,
);
path = "App IDs";
sourceTree = "<group>";
@@ -881,13 +912,27 @@
path = AltPlugin;
sourceTree = "<group>";
};
BF6C8FA8242935CA00125131 /* Dependencies */ = {
isa = PBXGroup;
children = (
BF6C8FA9242935DB00125131 /* MarkdownAttributedString */,
);
name = Dependencies;
sourceTree = "<group>";
};
BF6C8FA9242935DB00125131 /* MarkdownAttributedString */ = {
isa = PBXGroup;
children = (
BF6C8FAB242935ED00125131 /* NSAttributedString+Markdown.h */,
BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */,
);
name = MarkdownAttributedString;
sourceTree = "<group>";
};
BF703194229F36F6006E110F /* Resources */ = {
isa = PBXGroup;
children = (
BF458693229872EA00BD7491 /* Assets.xcassets */,
BF914C252383703800E713BA /* AltPlugin.mailbundle.zip */,
BF4C7F26238086EB00B2556E /* InstallPlugin.sh */,
BFD80D562380C0F700B9C227 /* UninstallPlugin.sh */,
);
name = Resources;
sourceTree = "<group>";
@@ -940,6 +985,8 @@
BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */,
BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */,
BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */,
BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */,
BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */,
);
path = "My Apps";
sourceTree = "<group>";
@@ -954,6 +1001,14 @@
path = Server;
sourceTree = "<group>";
};
BFC84A4B2421A13000853474 /* Sources */ = {
isa = PBXGroup;
children = (
BFC84A4C2421A19100853474 /* SourcesViewController.swift */,
);
path = Sources;
sourceTree = "<group>";
};
BFD247612284B9A500981D42 = {
isa = PBXGroup;
children = (
@@ -997,7 +1052,9 @@
BFD5D6E6230CC94B007955AB /* Patreon */,
BFD2478A2284C49000981D42 /* Managing Apps */,
BF56D2AD23DF9E170006506D /* App IDs */,
BFC84A4B2421A13000853474 /* Sources */,
BFC51D7922972F1F00388324 /* Server */,
BF0DCA642433BDE200E3A595 /* Analytics */,
BFD247982284D7FC00981D42 /* Model */,
BFDB6A0922AAEDA1007EA6D6 /* Operations */,
BFD2478D2284C4C700981D42 /* Components */,
@@ -1005,6 +1062,7 @@
BFDB6A0622A9B114007EA6D6 /* Protocols */,
BFD2479D2284FBBD00981D42 /* Extensions */,
BFD247962284D7C100981D42 /* Resources */,
BF6C8FA8242935CA00125131 /* Dependencies */,
BFD247972284D7D800981D42 /* Supporting Files */,
);
path = AltStore;
@@ -1049,6 +1107,8 @@
BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */,
BF2901302318F7A800D88A45 /* AppBannerView.swift */,
BF29012E2318F6B100D88A45 /* AppBannerView.xib */,
BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */,
BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -1099,12 +1159,13 @@
isa = PBXGroup;
children = (
BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */,
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */,
BFB1169A2293274D00BB457C /* JSONDecoder+Properties.swift */,
BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */,
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */,
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */,
BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */,
BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */,
BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */,
BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -1167,18 +1228,20 @@
children = (
BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */,
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */,
BF770E5722BC3D0F002A40FE /* OperationGroup.swift */,
BF770E5322BC044E002A40FE /* AppOperationContext.swift */,
BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */,
BF770E5322BC044E002A40FE /* OperationContexts.swift */,
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */,
BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */,
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */,
BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */,
);
path = Operations;
sourceTree = "<group>";
@@ -1415,11 +1478,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BF914C262383703800E713BA /* AltPlugin.mailbundle.zip in Resources */,
BFD80D572380C0F700B9C227 /* UninstallPlugin.sh in Resources */,
BF458694229872EA00BD7491 /* Assets.xcassets in Resources */,
BF458697229872EA00BD7491 /* Main.storyboard in Resources */,
BF4C7F27238086EB00B2556E /* InstallPlugin.sh in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1434,6 +1494,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */,
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
@@ -1578,6 +1639,7 @@
BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */,
BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */,
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */,
BFD44606241188C400EAB90A /* CodableServerError.swift in Sources */,
BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1680,7 +1742,6 @@
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */,
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */,
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */,
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */,
@@ -1689,23 +1750,28 @@
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */,
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */,
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */,
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */,
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */,
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */,
BF6C33652419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel in Sources */,
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */,
BF9F8D1A242AA6BC0024E48B /* AltStore5ToAltStore6.xcmappingmodel in Sources */,
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */,
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */,
BFE6326822A858F300F30809 /* Account.swift in Sources */,
BF6C336224197D700034FD24 /* NSError+LocalizedFailure.swift in Sources */,
BFE6326622A857C200F30809 /* Team.swift in Sources */,
BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
@@ -1713,6 +1779,8 @@
BF100C54232D7DAE006A8926 /* StoreAppPolicy.swift in Sources */,
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */,
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */,
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
@@ -1720,7 +1788,7 @@
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */,
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
BFB1169B2293274D00BB457C /* JSONDecoder+Properties.swift in Sources */,
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */,
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */,
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */,
@@ -1729,15 +1797,15 @@
BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */,
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
BF7C627423DBB78C00515A2D /* InstalledAppPolicy.swift in Sources */,
BFA8172F23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift in Sources */,
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */,
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */,
BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */,
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */,
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */,
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */,
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
@@ -1746,6 +1814,7 @@
BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */,
@@ -1754,11 +1823,14 @@
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */,
BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */,
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */,
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */,
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */,
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
BF770E5622BC3C03002A40FE /* Server.swift in Sources */,
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */,
@@ -1864,7 +1936,7 @@
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 6XVY5G3U44;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
@@ -1897,7 +1969,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14.4;
MARKETING_VERSION = 1.2b2;
MARKETING_VERSION = 1.3;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
@@ -1917,7 +1989,7 @@
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 6XVY5G3U44;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
@@ -1950,7 +2022,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14.4;
MARKETING_VERSION = 1.2b2;
MARKETING_VERSION = 1.3;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
@@ -2124,7 +2196,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -2224,10 +2296,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2;
MARKETING_VERSION = 1.3;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG BETA";
SWIFT_OBJC_BRIDGING_HEADER = "AltStore/AltStore-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -2251,10 +2324,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2;
MARKETING_VERSION = 1.3;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = BETA;
SWIFT_OBJC_BRIDGING_HEADER = "AltStore/AltStore-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
@@ -2324,12 +2398,14 @@
BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
BF9F8D18242AA6680024E48B /* AltStore 6.xcdatamodel */,
BF6C33632419ADEB0034FD24 /* AltStore 5.xcdatamodel */,
BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */,
BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */,
BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */,
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */,
);
currentVersion = BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */;
currentVersion = BF9F8D18242AA6680024E48B /* AltStore 6.xcdatamodel */;
path = AltStore.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

View File

@@ -7,3 +7,5 @@
#import "ALTAppPermission.h"
#import "ALTPatreonBenefitType.h"
#import "ALTSourceUserInfoKey.h"
#import "NSAttributedString+Markdown.h"

View File

@@ -0,0 +1,105 @@
//
// AnalyticsManager.swift
// AltStore
//
// Created by Riley Testut on 3/31/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AppCenter
import AppCenterAnalytics
import AppCenterCrashes
#if DEBUG
private let appCenterAppSecret = "bb08e9bb-c126-408d-bf3f-324c8473fd40"
#elseif RELEASE
private let appCenterAppSecret = "b6718932-294a-432b-81f2-be1e17ff85c5"
#else
private let appCenterAppSecret = "e873f6ca-75eb-4685-818f-801e0e375d60"
#endif
extension AnalyticsManager
{
enum EventProperty: String
{
case name
case bundleIdentifier
case developerName
case version
case size
case tintColor
case sourceIdentifier
case sourceURL
}
enum Event
{
case installedApp(InstalledApp)
case updatedApp(InstalledApp)
case refreshedApp(InstalledApp)
var name: String {
switch self
{
case .installedApp: return "installed_app"
case .updatedApp: return "updated_app"
case .refreshedApp: return "refreshed_app"
}
}
var properties: [EventProperty: String] {
let properties: [EventProperty: String?]
switch self
{
case .installedApp(let app), .updatedApp(let app), .refreshedApp(let app):
let appBundleURL = InstalledApp.fileURL(for: app)
let appBundleSize = FileManager.default.directorySize(at: appBundleURL)
properties = [
.name: app.name,
.bundleIdentifier: app.bundleIdentifier,
.developerName: app.storeApp?.developerName,
.version: app.version,
.size: appBundleSize?.description,
.tintColor: app.storeApp?.tintColor?.hexString,
.sourceIdentifier: app.storeApp?.sourceIdentifier,
.sourceURL: app.storeApp?.source?.sourceURL.absoluteString
]
}
return properties.compactMapValues { $0 }
}
}
}
class AnalyticsManager
{
static let shared = AnalyticsManager()
private init()
{
}
}
extension AnalyticsManager
{
func start()
{
MSAppCenter.start(appCenterAppSecret, withServices:[
MSAnalytics.self,
MSCrashes.self
])
}
func trackEvent(_ event: Event)
{
let properties = event.properties.reduce(into: [:]) { (properties, item) in
properties[item.key.rawValue] = item.value
}
MSAnalytics.trackEvent(event.name, withProperties: properties)
}
}

View File

@@ -71,7 +71,7 @@ private extension AppIDsViewController
dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in
let tintColor = UIColor.altPrimary
let cell = cell as! AppIDCollectionViewCell
let cell = cell as! BannerCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = tintColor
@@ -80,6 +80,8 @@ private extension AppIDsViewController
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.betaBadgeView.isHidden = true
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
if let expirationDate = appID.expirationDate
{
cell.bannerView.button.isHidden = false
@@ -181,31 +183,34 @@ extension AppIDsViewController: UICollectionViewDelegateFlowLayout
switch kind
{
case UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! AppIDsCollectionReusableView
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! TextCollectionReusableView
headerView.layoutMargins.left = self.view.layoutMargins.left
headerView.layoutMargins.right = self.view.layoutMargins.right
if let activeTeam = DatabaseManager.shared.activeTeam(), activeTeam.type == .free
{
headerView.textLabel.text = """
let text = NSLocalizedString("""
Each app and app extension installed with AltStore must register an App ID with Apple. Apple limits free developer accounts to 10 App IDs at a time.
App IDs expire after one week, but AltStore will automatically renew them for all installed apps. Once an App ID expires, it no longer counts toward your total.
"""
**App IDs can't be deleted**, but they do expire after one week. AltStore will automatically renew App IDs for all active apps once they've expired.
""", comment: "")
let attributedText = NSAttributedString(markdownRepresentation: text, attributes: [.font: headerView.textLabel.font as Any])
headerView.textLabel.attributedText = attributedText
}
else
{
headerView.textLabel.text = """
headerView.textLabel.text = NSLocalizedString("""
Each app and app extension installed with AltStore must register an App ID with Apple.
App IDs for paid developer accounts never expire, and there is no limit to how many you can create.
"""
""", comment: "")
}
return headerView
case UICollectionView.elementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! AppIDsCollectionReusableView
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! TextCollectionReusableView
let count = self.dataSource.itemCount
if count == 1

View File

@@ -21,7 +21,7 @@ private enum RefreshError: LocalizedError
var errorDescription: String? {
switch self
{
case .noInstalledApps: return NSLocalizedString("No installed apps to refresh.", comment: "")
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
}
}
}
@@ -64,9 +64,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private var runningApplications: Set<String>?
private var backgroundRefreshContext: NSManagedObjectContext? // Keep context alive until finished refreshing.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
AnalyticsManager.shared.start()
self.setTintColor()
ServerManager.shared.startDiscovering()
@@ -209,6 +212,8 @@ extension AppDelegate
}
taskCompletionHandler()
self.backgroundRefreshContext = nil
}
if let error = taskResult.error
@@ -247,20 +252,20 @@ private extension AppDelegate
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
{
var fetchSourceResult: Result<Source, Error>?
var fetchSourcesResult: Result<Set<Source>, Error>?
var serversResult: Result<Void, Error>?
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
AppManager.shared.fetchSource() { (result) in
fetchSourceResult = result
AppManager.shared.fetchSources() { (result) in
fetchSourcesResult = result
do
{
let source = try result.get()
let sources = try result.get()
guard let context = source.managedObjectContext else { return }
guard let context = sources.first?.managedObjectContext else { return }
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
previousUpdatesFetchRequest.includesPendingChanges = false
@@ -328,7 +333,7 @@ private extension AppDelegate
{
print("Error fetching apps:", error)
fetchSourceResult = .failure(error)
fetchSourcesResult = .failure(error)
}
dispatchGroup.leave()
@@ -339,7 +344,6 @@ private extension AppDelegate
dispatchGroup.enter()
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
guard !installedApps.isEmpty else {
serversResult = .success(())
@@ -351,6 +355,7 @@ private extension AppDelegate
}
self.runningApplications = []
self.backgroundRefreshContext = context
let identifiers = installedApps.compactMap { $0.bundleIdentifier }
print("Apps to refresh:", identifiers)
@@ -398,7 +403,7 @@ private extension AppDelegate
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
if let error = group.error
if let error = group.context.error
{
self.scheduleFinishedRefreshingNotification(for: .failure(error), identifier: identifier)
}
@@ -410,8 +415,8 @@ private extension AppDelegate
self.scheduleFinishedRefreshingNotification(for: .success(results), identifier: identifier)
}
}
group.completionHandler = { (result) in
completionHandler(result)
group.completionHandler = { (results) in
completionHandler(.success(results))
}
}
}
@@ -421,12 +426,12 @@ private extension AppDelegate
dispatchGroup.notify(queue: .main) {
if !UserDefaults.standard.isBackgroundRefreshEnabled
{
guard let fetchSourceResult = fetchSourceResult else {
guard let fetchSourcesResult = fetchSourcesResult else {
backgroundFetchCompletionHandler(.failed)
return
}
switch fetchSourceResult
switch fetchSourcesResult
{
case .failure: backgroundFetchCompletionHandler(.failed)
case .success: backgroundFetchCompletionHandler(.newData)
@@ -436,13 +441,13 @@ private extension AppDelegate
}
else
{
guard let fetchSourceResult = fetchSourceResult, let serversResult = serversResult else {
guard let fetchSourcesResult = fetchSourcesResult, let serversResult = serversResult else {
backgroundFetchCompletionHandler(.failed)
return
}
// Call completionHandler early to improve chances of refreshing in the background again.
switch (fetchSourceResult, serversResult)
switch (fetchSourcesResult, serversResult)
{
case (.success, .success): backgroundFetchCompletionHandler(.newData)
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)

View File

@@ -106,18 +106,20 @@ private extension AuthenticationViewController
self.signInButton.isIndicatingActivity = false
}
case .failure(let error):
case .failure(let error as NSError):
DispatchQueue.main.async {
let toastView = ToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription)
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Log In", comment: ""))
let toastView = ToastView(error: error)
toastView.textLabel.textColor = .altPink
toastView.detailTextLabel.textColor = .altPink
toastView.show(in: self.navigationController?.view ?? self.view)
toastView.show(in: self)
self.toastView = toastView
self.signInButton.isIndicatingActivity = false
}
case .success(let account, let session):
case .success((let account, let session)):
self.completionHandler?((account, session, password))
}

View File

@@ -13,8 +13,7 @@ import Roxas
class RefreshAltStoreViewController: UIViewController
{
var signer: ALTSigner!
var session: ALTAppleAPISession!
var context: AuthenticatedOperationContext!
var completionHandler: ((Result<Void, Error>) -> Void)?
@@ -42,23 +41,23 @@ private extension RefreshAltStoreViewController
{
sender.isIndicatingActivity = true
if let progress = AppManager.shared.refreshProgress(for: altStore) ?? AppManager.shared.installationProgress(for: altStore)
if let progress = AppManager.shared.installationProgress(for: altStore)
{
// Cancel pending AltStore refresh so we can start a new one.
// Cancel pending AltStore installation so we can start a new one.
progress.cancel()
}
let group = OperationGroup()
group.signer = self.signer // Prevent us from trying to authenticate a second time.
group.session = self.session // ^
group.completionHandler = { (result) in
if let error = result.error ?? result.value?.values.compactMap({ $0.error }).first
// Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
let progress = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
switch result
{
case .success: self.completionHandler?(.success(()))
case .failure(let error as NSError):
DispatchQueue.main.async {
sender.progress = nil
sender.isIndicatingActivity = false
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh AltStore", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh AltStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { (action) in
refresh()
}))
@@ -69,14 +68,9 @@ private extension RefreshAltStoreViewController
self.present(alertController, animated: true, completion: nil)
}
}
else
{
self.completionHandler?(.success(()))
}
}
_ = AppManager.shared.refresh([altStore], presentingViewController: self, group: group)
sender.progress = group.progress
sender.progress = progress
}
refresh()

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -64,7 +64,13 @@
<outlet property="delegate" destination="e3L-BF-iXp" id="Mdp-x4-hZe"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr"/>
<navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr">
<barButtonItem key="rightBarButtonItem" title="Sources" id="6Ul-JW-TMT">
<connections>
<segue destination="Qo4-72-Hmr" kind="presentation" id="de9-NH-aec"/>
</connections>
</barButtonItem>
</navigationItem>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
@@ -103,7 +109,7 @@
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8Tg-wk-r0u">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="oNk-OQ-r4M">
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="oNk-OQ-r4M">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
@@ -133,14 +139,14 @@
<visualEffectView opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tUK-0J-07U">
<rect key="frame" x="58" y="117" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="yyn-wP-xk4">
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="yyn-wP-xk4">
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JP7-6F-CoG">
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="UJ5-ia-PVA">
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="UJ5-ia-PVA">
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
@@ -628,13 +634,13 @@ World</string>
<color key="backgroundColor" name="Background"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="SB5-U0-jyy">
<size key="itemSize" width="375" height="60"/>
<size key="headerReferenceSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="50" height="60.5"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="AppCell" id="kMp-ym-2yu" customClass="InstalledAppCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="50" width="375" height="60"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
@@ -663,7 +669,7 @@ World</string>
</connections>
</collectionViewCell>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="NoUpdatesCell" id="h0f-XI-UA5" customClass="NoUpdatesCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="125" width="375" height="60"/>
<rect key="frame" x="0.0" y="75" width="375" height="60"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
@@ -723,35 +729,8 @@ World</string>
</connections>
</collectionViewCell>
</cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsHeader" id="Crb-NU-1Ye" customClass="InstalledAppsCollectionHeaderView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Installed" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BDU-hM-rro">
<rect key="frame" x="20" y="21" width="96.5" height="29"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="nxk-e8-ARx">
<rect key="frame" x="274" y="23" width="81" height="32"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/>
<state key="normal" title="Refresh All"/>
</button>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="BDU-hM-rro" secondAttribute="bottom" id="9iT-ur-A4W"/>
<constraint firstItem="BDU-hM-rro" firstAttribute="leading" secondItem="Crb-NU-1Ye" secondAttribute="leading" constant="20" id="F8e-9W-MC2"/>
<constraint firstAttribute="trailing" secondItem="nxk-e8-ARx" secondAttribute="trailing" constant="20" id="WxV-85-RcK"/>
<constraint firstItem="nxk-e8-ARx" firstAttribute="firstBaseline" secondItem="BDU-hM-rro" secondAttribute="firstBaseline" id="lIO-3C-ZPH"/>
</constraints>
<connections>
<outlet property="button" destination="nxk-e8-ARx" id="gwj-97-LVi"/>
<outlet property="textLabel" destination="BDU-hM-rro" id="CQM-8K-bcH"/>
</connections>
</collectionReusableView>
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsFooter" id="HYs-co-nJZ" customClass="InstalledAppsCollectionFooterView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="185" width="375" height="60.5"/>
<rect key="frame" x="0.0" y="135" width="375" height="60.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="900" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="GFQ-Wy-Qhy">
@@ -819,7 +798,7 @@ World</string>
<inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XWu-DU-xbh" customClass="AppIDCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XWu-DU-xbh" customClass="BannerCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="70" width="375" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
@@ -845,7 +824,7 @@ World</string>
</connections>
</collectionViewCell>
</cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="AppIDsCollectionReusableView" customModule="AltStore" customModuleProvider="target">
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
@@ -866,7 +845,7 @@ World</string>
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
</connections>
</collectionReusableView>
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="AppIDsCollectionReusableView" customModule="AltStore" customModuleProvider="target">
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="170" width="375" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
@@ -950,7 +929,114 @@ World</string>
</objects>
<point key="canvasLocation" x="2526" y="731"/>
</scene>
<!--Sources-->
<scene sceneID="0S1-zn-9KZ">
<objects>
<collectionViewController title="Sources" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="S36-hD-vu2">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="X20-b5-XEP">
<size key="itemSize" width="375" height="80"/>
<size key="headerReferenceSize" width="50" height="200"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XcN-o4-9qm" customClass="BannerCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="210" width="375" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
<accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/>
</accessibility>
</view>
</subviews>
</view>
<constraints>
<constraint firstAttribute="bottom" secondItem="LW1-CC-bWu" secondAttribute="bottom" id="Pkr-zO-0wx"/>
<constraint firstItem="LW1-CC-bWu" firstAttribute="leading" secondItem="XcN-o4-9qm" secondAttribute="leadingMargin" id="egJ-X3-yEz"/>
<constraint firstItem="LW1-CC-bWu" firstAttribute="top" secondItem="XcN-o4-9qm" secondAttribute="top" id="glF-aM-4xQ"/>
<constraint firstAttribute="trailingMargin" secondItem="LW1-CC-bWu" secondAttribute="trailing" id="tQx-yV-LTq"/>
</constraints>
<connections>
<outlet property="bannerView" destination="LW1-CC-bWu" id="mwO-Ne-L1L"/>
</connections>
</collectionViewCell>
</cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Manage sources to control which apps are available to download through AltStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TZv-TM-uJj">
<rect key="frame" x="8" y="14" width="359" height="171"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="TZv-TM-uJj" firstAttribute="top" secondItem="8N7-JY-mcA" secondAttribute="top" constant="14" id="2zE-UV-24S"/>
<constraint firstAttribute="bottom" secondItem="TZv-TM-uJj" secondAttribute="bottom" constant="15" id="Aml-PC-dko"/>
<constraint firstAttribute="trailingMargin" secondItem="TZv-TM-uJj" secondAttribute="trailing" id="V0U-al-5eb"/>
<constraint firstAttribute="leadingMargin" secondItem="TZv-TM-uJj" secondAttribute="leading" id="aS5-6Y-rMd"/>
</constraints>
<connections>
<outlet property="textLabel" destination="TZv-TM-uJj" id="kWV-Wv-5gz"/>
</connections>
</collectionReusableView>
<connections>
<outlet property="dataSource" destination="cHC-TX-KzQ" id="VHQ-ls-gde"/>
<outlet property="delegate" destination="cHC-TX-KzQ" id="MWr-Xg-N2k"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" title="Sources" id="QTB-W7-6BG">
<barButtonItem key="leftBarButtonItem" systemItem="add" id="kBB-5c-8gw">
<connections>
<action selector="addSource" destination="cHC-TX-KzQ" id="WiB-Jg-NzT"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="NQF-u2-PZv">
<connections>
<segue destination="zjS-Nr-VTw" kind="unwind" unwindAction="unwindToBrowseViewController:" id="VFy-hV-mNV"/>
</connections>
</barButtonItem>
</navigationItem>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="TrV-p3-ZAt" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="zjS-Nr-VTw" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="3302" y="1430"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="6NV-LQ-gKB">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="cHC-TX-KzQ" kind="relationship" relationship="rootViewController" id="BC5-Fs-dCj"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="4mO-93-4qk" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2526" y="1445"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="cnd-KK-o60"/>
</inferredMetricsTieBreakers>
<color key="tintColor" name="Primary"/>
<resources>
<image name="Back" width="18" height="18"/>
<image name="Browse" width="20" height="20"/>
@@ -967,8 +1053,4 @@ World</string>
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
<inferredMetricsTieBreakers>
<segue reference="cnd-KK-o60"/>
</inferredMetricsTieBreakers>
<color key="tintColor" name="Primary"/>
</document>

View File

@@ -31,6 +31,14 @@ class BrowseViewController: UICollectionViewController
{
super.viewDidLoad()
#if BETA
self.dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
self.navigationItem.searchController = self.dataSource.searchController
#else
// Hide Sources button for public version while in beta.
self.navigationItem.rightBarButtonItem = nil
#endif
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
@@ -50,6 +58,11 @@ class BrowseViewController: UICollectionViewController
self.fetchSource()
self.updateDataSource()
}
@IBAction private func unwindToBrowseViewController(_ segue: UIStoryboardSegue)
{
self.fetchSource()
}
}
private extension BrowseViewController
@@ -57,17 +70,12 @@ private extension BrowseViewController
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
{
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true), NSSortDescriptor(keyPath: \StoreApp.name, ascending: true)]
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
if let source = Source.fetchAltStoreSource(in: DatabaseManager.shared.viewContext)
{
fetchRequest.predicate = NSPredicate(format: "%K != %@ AND %K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID, #keyPath(StoreApp.source), source)
}
else
{
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
}
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
@@ -167,23 +175,25 @@ private extension BrowseViewController
{
self.loadingState = .loading
AppManager.shared.fetchSource() { (result) in
AppManager.shared.fetchSources() { (result) in
do
{
let source = try result.get()
try source.managedObjectContext?.save()
let sources = try result.get()
try sources.first?.managedObjectContext?.save()
DispatchQueue.main.async {
self.loadingState = .finished(.success(()))
}
}
catch
catch let error as NSError
{
DispatchQueue.main.async {
if self.dataSource.itemCount > 0
{
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Fetch Sources", comment: ""))
let toastView = ToastView(error: error)
toastView.show(in: self)
}
self.loadingState = .finished(.failure(error))

View File

@@ -1,14 +1,14 @@
//
// AppIDComponents.swift
// BannerCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 2/10/20.
// Created by Riley Testut on 3/23/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
class AppIDCollectionViewCell: UICollectionViewCell
class BannerCollectionViewCell: UICollectionViewCell
{
@IBOutlet var bannerView: AppBannerView!
@@ -18,13 +18,5 @@ class AppIDCollectionViewCell: UICollectionViewCell
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.contentView.preservesSuperviewLayoutMargins = true
self.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
self.bannerView.buttonLabel.isHidden = false
}
}
class AppIDsCollectionReusableView: UICollectionReusableView
{
@IBOutlet var textLabel: UILabel!
}

View File

@@ -0,0 +1,14 @@
//
// TextCollectionReusableView.swift
// AltStore
//
// Created by Riley Testut on 3/23/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
class TextCollectionReusableView: UICollectionReusableView
{
@IBOutlet var textLabel: UILabel!
}

View File

@@ -16,7 +16,7 @@ class ToastView: RSTToastView
{
if detailedText == nil
{
self.preferredDuration = 2.0
self.preferredDuration = 4.0
}
else
{
@@ -25,20 +25,41 @@ class ToastView: RSTToastView
super.init(text: text, detailText: detailedText)
self.layoutMargins = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
self.layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16)
self.setNeedsLayout()
if let stackView = self.textLabel.superview as? UIStackView
{
// RSTToastView does not expose stack view containing labels,
// so we access it indirectly as the labels' superview.
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0
}
}
convenience init(error: Error)
{
if let error = error as? LocalizedError
let error = error as NSError
let text: String
let detailText: String?
if let failure = error.localizedFailure
{
self.init(text: error.localizedDescription, detailText: error.recoverySuggestion ?? error.failureReason)
text = failure
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? error.localizedDescription
}
else if let reason = error.localizedFailureReason
{
text = reason
detailText = error.localizedRecoverySuggestion
}
else
{
self.init(text: error.localizedDescription, detailText: nil)
text = error.localizedDescription
detailText = nil
}
self.init(text: text, detailText: detailText)
}
required init(coder aDecoder: NSCoder) {
@@ -49,7 +70,9 @@ class ToastView: RSTToastView
{
super.layoutSubviews()
self.layer.cornerRadius = 16
// Rough calculation to determine height of ToastView with one-line textLabel.
let minimumHeight = self.textLabel.font.lineHeight.rounded() + 18
self.layer.cornerRadius = minimumHeight/2
}
func show(in viewController: UIViewController)

View File

@@ -1,29 +0,0 @@
//
// ALTApplication+AppExtensions.swift
// AltStore
//
// Created by Riley Testut on 2/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import AltSign
extension ALTApplication
{
var appExtensions: Set<ALTApplication> {
guard let bundle = Bundle(url: self.fileURL) else { return [] }
var appExtensions: Set<ALTApplication> = []
if let directory = bundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
{
for case let fileURL as URL in enumerator where fileURL.pathExtension.lowercased() == "appex"
{
guard let appExtension = ALTApplication(fileURL: fileURL) else { continue }
appExtensions.insert(appExtension)
}
}
return appExtensions
}
}

View File

@@ -0,0 +1,36 @@
//
// FileManager+DirectorySize.swift
// AltStore
//
// Created by Riley Testut on 3/31/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
extension FileManager
{
func directorySize(at directoryURL: URL) -> Int?
{
guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil }
var total: Int = 0
for case let fileURL as URL in enumerator
{
do
{
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
guard let fileSize = resourceValues.fileSize else { continue }
total += fileSize
}
catch
{
print("Failed to read file size for item: \(fileURL).", error)
}
}
return total
}
}

View File

@@ -1,36 +0,0 @@
//
// JSONDecoder+ManagedObjectContext.swift
// Harmony
//
// Created by Riley Testut on 10/3/18.
// Copyright © 2018 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
private extension CodingUserInfoKey
{
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
}
public extension JSONDecoder
{
var managedObjectContext: NSManagedObjectContext? {
get {
let managedObjectContext = self.userInfo[.managedObjectContext] as? NSManagedObjectContext
return managedObjectContext
}
set {
self.userInfo[.managedObjectContext] = newValue
}
}
}
public extension Decoder
{
var managedObjectContext: NSManagedObjectContext? {
let managedObjectContext = self.userInfo[.managedObjectContext] as? NSManagedObjectContext
return managedObjectContext
}
}

View File

@@ -0,0 +1,64 @@
//
// 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
}
}
}

View File

@@ -0,0 +1,27 @@
//
// NSError+LocalizedFailure.swift
// AltStore
//
// Created by Riley Testut on 3/11/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
extension NSError
{
@objc(alt_localizedFailure)
var localizedFailure: String? {
let localizedFailure = (self.userInfo[NSLocalizedFailureErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedFailureErrorKey) as? String)
return localizedFailure
}
func withLocalizedFailure(_ failure: String) -> NSError
{
var userInfo = self.userInfo
userInfo[NSLocalizedFailureErrorKey] = failure
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
return error
}
}

View File

@@ -10,6 +10,17 @@ 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)
{

View File

@@ -22,6 +22,23 @@ extension UserDefaults
@NSManaged var legacySideloadedApps: [String]?
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()
{
self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true])

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>AltStore 4.xcdatamodel</string>
<string>AltStore 6.xcdatamodel</string>
</dict>
</plist>

View File

@@ -0,0 +1,169 @@
<?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>

View File

@@ -0,0 +1,173 @@
<?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>

View File

@@ -158,6 +158,7 @@ private extension DatabaseManager
storeApp.source = altStoreSource
}
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
let installedApp: InstalledApp
if let app = storeApp.installedApp
@@ -166,7 +167,7 @@ private extension DatabaseManager
}
else
{
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, context: context)
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context)
installedApp.storeApp = storeApp
}
@@ -193,8 +194,21 @@ private extension DatabaseManager
}
}
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)
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
{

View File

@@ -11,6 +11,9 @@ 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 }
@@ -36,6 +39,10 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
@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?
@@ -45,12 +52,16 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
return self.storeApp == nil
}
var appIDCount: Int {
return 1 + self.appExtensions.count
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
init(resignedApp: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext)
init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext)
{
super.init(entity: InstalledApp.entity(), insertInto: context)
@@ -61,22 +72,29 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
self.update(resignedApp: resignedApp)
self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber)
}
func update(resignedApp: ALTApplication)
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.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate
self.update(provisioningProfile: provisioningProfile)
}
}
func update(provisioningProfile: ALTProvisioningProfile)
{
self.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate
}
}
extension InstalledApp
@@ -89,7 +107,15 @@ extension InstalledApp
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
{
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K != nil AND %K != %K", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.version))
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
}
@@ -101,9 +127,15 @@ extension InstalledApp
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 != %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
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
{
@@ -133,7 +165,8 @@ extension InstalledApp
// Date 6 hours before now.
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
var predicate = NSPredicate(format: "(%K < %@) AND (%K != %@)",
var predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
#keyPath(InstalledApp.isActive),
#keyPath(InstalledApp.refreshedDate), date as NSDate,
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
@@ -153,7 +186,7 @@ extension InstalledApp
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date
{
// Refresh AltStore last since it causes app to quit.
// Refresh AltStore last since it may cause app to quit.
installedApps.append(altStoreApp)
}

View File

@@ -55,10 +55,15 @@ class InstalledExtension: NSManagedObject, InstalledAppProtocol
if let provisioningProfile = resignedAppExtension.provisioningProfile
{
self.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate
self.update(provisioningProfile: provisioningProfile)
}
}
func update(provisioningProfile: ALTProvisioningProfile)
{
self.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate
}
}
extension InstalledExtension

View File

@@ -15,8 +15,33 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
open override func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws
{
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
return try super.resolve(constraintConflicts: conflicts)
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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@
//
import CoreData
import AltSign
@objc(InstalledAppToInstalledAppMigrationPolicy)
class InstalledAppToInstalledAppMigrationPolicy: NSEntityMigrationPolicy
@@ -27,4 +28,31 @@ class InstalledAppToInstalledAppMigrationPolicy: NSEntityMigrationPolicy
// 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)
}
}

View File

@@ -26,6 +26,7 @@ class NewsItem: NSManagedObject, Decodable, Fetchable
@NSManaged var externalURL: URL?
@NSManaged var appID: String?
@NSManaged var sourceIdentifier: String?
/* Relationships */
@NSManaged var storeApp: StoreApp?

View File

@@ -10,12 +10,28 @@ import CoreData
extension Source
{
#if ALPHA
static let altStoreIdentifier = "com.rileytestut.AltStore.Alpha"
#else
static let altStoreIdentifier = "com.rileytestut.AltStore"
#endif
#if STAGING
static let altStoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/apps-staging.json")!
#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://cdn.altstore.io/file/altstore/apps.json")!
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
}
@@ -70,55 +86,64 @@ class Source: NSManagedObject, Fetchable, Decodable
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: nil)
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)
self.sourceURL = try container.decode(URL.self, forKey: .sourceURL)
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)
}
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
for (index, item) in newsItems.enumerated()
{
item.sourceIdentifier = self.identifier
item.sortIndex = Int32(index)
}
context.insert(self)
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { (a, b) in return a })
for newsItem in newsItems
{
newsItem.source = self
{
guard let appID = newsItem.appID else { continue }
if let storeApp = appsByID[appID]
{
newsItem.storeApp = storeApp
}
else
{
newsItem.storeApp = nil
}
}
// Must assign after we're inserted into context.
self._apps = NSMutableOrderedSet(array: apps)
self._newsItems = NSMutableOrderedSet(array: newsItems)
print("Downloaded Order:", self.apps.map { $0.bundleIdentifier })
}
}
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)

View File

@@ -14,12 +14,15 @@ import AltSign
extension StoreApp
{
#if BETA
#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 alternativeAltStoreAppID = "com.rileytestut.AltStore"
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore", "com.rileytestut.AltStore.Alpha"]
#else
static let altstoreAppID = "com.rileytestut.AltStore"
static let alternativeAltStoreAppID = "com.rileytestut.AltStore.Beta"
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore.Beta", "com.rileytestut.AltStore.Alpha"]
#endif
}
@@ -46,12 +49,26 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
@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 source: Source?
@objc(permissions) @NSManaged var _permissions: NSOrderedSet
@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]

View File

@@ -0,0 +1,43 @@
//
// InstalledAppsCollectionHeaderView.swift
// AltStore
//
// Created by Riley Testut on 3/9/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
class InstalledAppsCollectionHeaderView: UICollectionReusableView
{
let textLabel: UILabel
let button: UIButton
override init(frame: CGRect)
{
self.textLabel = UILabel()
self.textLabel.translatesAutoresizingMaskIntoConstraints = false
self.textLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold)
self.button = UIButton(type: .system)
self.button.translatesAutoresizingMaskIntoConstraints = false
self.button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
super.init(frame: frame)
self.addSubview(self.textLabel)
self.addSubview(self.button)
NSLayoutConstraint.activate([self.textLabel.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor),
self.textLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor)])
NSLayoutConstraint.activate([self.button.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor),
self.button.firstBaselineAnchor.constraint(equalTo: self.textLabel.firstBaselineAnchor)])
self.preservesSuperviewLayoutMargins = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsHeader" id="eyV-eW-aLi" customClass="InstalledAppsCollectionHeaderView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Installed" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zhW-Re-WNf">
<rect key="frame" x="20" y="21" width="96.5" height="29"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iMf-wr-wRV">
<rect key="frame" x="274" y="23" width="81" height="32"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/>
<state key="normal" title="Refresh All"/>
</button>
</subviews>
<constraints>
<constraint firstItem="zhW-Re-WNf" firstAttribute="leading" secondItem="eyV-eW-aLi" secondAttribute="leading" constant="20" id="Fo0-fL-UpD"/>
<constraint firstAttribute="bottom" secondItem="zhW-Re-WNf" secondAttribute="bottom" id="OWw-FY-KOh"/>
<constraint firstAttribute="trailing" secondItem="iMf-wr-wRV" secondAttribute="trailing" constant="20" id="dJM-7c-k31"/>
<constraint firstItem="iMf-wr-wRV" firstAttribute="firstBaseline" secondItem="zhW-Re-WNf" secondAttribute="firstBaseline" id="iU7-F2-XDu"/>
</constraints>
<viewLayoutGuide key="safeArea" id="N3q-SZ-Vyv"/>
<connections>
<outlet property="button" destination="iMf-wr-wRV" id="kWT-cc-BjS"/>
<outlet property="textLabel" destination="zhW-Re-WNf" id="UOg-4X-rWx"/>
</connections>
<point key="canvasLocation" x="19.565217391304348" y="30.803571428571427"/>
</collectionReusableView>
</objects>
</document>

View File

@@ -7,9 +7,12 @@
//
import UIKit
import Roxas
class InstalledAppCollectionViewCell: UICollectionViewCell
{
private(set) var deactivateBadge: UIView?
@IBOutlet var bannerView: AppBannerView!
override func awakeFromNib()
@@ -19,17 +22,39 @@ class InstalledAppCollectionViewCell: UICollectionViewCell
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.contentView.preservesSuperviewLayoutMargins = true
self.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
self.bannerView.buttonLabel.isHidden = false
if #available(iOS 13.0, *)
{
let deactivateBadge = UIView()
deactivateBadge.translatesAutoresizingMaskIntoConstraints = false
deactivateBadge.isHidden = true
self.addSubview(deactivateBadge)
// Solid background to make the X opaque white.
let backgroundView = UIView()
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.backgroundColor = .white
deactivateBadge.addSubview(backgroundView)
let badgeView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
badgeView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(scale: .large)
badgeView.tintColor = .systemRed
deactivateBadge.addSubview(badgeView, pinningEdgesWith: .zero)
NSLayoutConstraint.activate([
deactivateBadge.centerXAnchor.constraint(equalTo: self.bannerView.iconImageView.trailingAnchor),
deactivateBadge.centerYAnchor.constraint(equalTo: self.bannerView.iconImageView.topAnchor),
backgroundView.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor),
backgroundView.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor),
backgroundView.widthAnchor.constraint(equalTo: badgeView.widthAnchor, multiplier: 0.5),
backgroundView.heightAnchor.constraint(equalTo: badgeView.heightAnchor, multiplier: 0.5)
])
self.deactivateBadge = deactivateBadge
}
}
}
class InstalledAppsCollectionHeaderView: UICollectionReusableView
{
@IBOutlet var textLabel: UILabel!
@IBOutlet var button: UIButton!
}
class InstalledAppsCollectionFooterView: UICollectionReusableView
{
@IBOutlet var textLabel: UILabel!

File diff suppressed because it is too large Load Diff

View File

@@ -56,6 +56,13 @@ class NewsViewController: UICollectionViewController
// Cache
private var cachedCellSizes = [String: CGSize]()
required init?(coder: NSCoder)
{
super.init(coder: coder)
NotificationCenter.default.addObserver(self, selector: #selector(NewsViewController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
}
override func viewDidLoad()
{
super.viewDidLoad()
@@ -99,9 +106,11 @@ private extension NewsViewController
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>
{
let fetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: false)]
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.date, ascending: false),
NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: true),
NSSortDescriptor(keyPath: \NewsItem.sourceIdentifier, ascending: true)]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.sortIndex), cacheName: nil)
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.objectID), cacheName: nil)
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>(fetchedResultsController: fetchedResultsController)
dataSource.proxy = self
@@ -165,23 +174,25 @@ private extension NewsViewController
{
self.loadingState = .loading
AppManager.shared.fetchSource() { (result) in
AppManager.shared.fetchSources() { (result) in
do
{
let source = try result.get()
try source.managedObjectContext?.save()
let sources = try result.get()
try sources.first?.managedObjectContext?.save()
DispatchQueue.main.async {
self.loadingState = .finished(.success(()))
}
}
catch
catch let error as NSError
{
DispatchQueue.main.async {
if self.dataSource.itemCount > 0
{
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Fetch Sources", comment: ""))
let toastView = ToastView(error: error)
toastView.show(in: self)
}
self.loadingState = .finished(.failure(error))
@@ -277,8 +288,8 @@ private extension NewsViewController
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
let toastView = ToastView(error: error)
toastView.show(in: self)
case .success: print("Installed app:", storeApp.bundleIdentifier)
}
@@ -300,6 +311,14 @@ private extension NewsViewController
}
}
private extension NewsViewController
{
@objc func importApp(_ notification: Notification)
{
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
extension NewsViewController
{
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)

View File

@@ -1,58 +0,0 @@
//
// Contexts.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import Network
import AltSign
class AppOperationContext
{
lazy var temporaryDirectory: URL = {
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) }
catch { self.error = error }
return temporaryDirectory
}()
var bundleIdentifier: String
var group: OperationGroup
var app: ALTApplication?
var resignedApp: ALTApplication?
var installationConnection: ServerConnection?
var installedApp: InstalledApp? {
didSet {
self.installedAppContext = self.installedApp?.managedObjectContext
}
}
private var installedAppContext: NSManagedObjectContext?
var isFinished = false
var error: Error? {
get {
return _error ?? self.group.error
}
set {
_error = newValue
}
}
private var _error: Error?
init(bundleIdentifier: String, group: OperationGroup)
{
self.bundleIdentifier = bundleIdentifier
self.group = group
}
}

View File

@@ -32,9 +32,9 @@ enum AuthenticationError: LocalizedError
}
@objc(AuthenticationOperation)
class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
{
let group: OperationGroup
let context: AuthenticatedOperationContext
private weak var presentingViewController: UIViewController?
@@ -56,22 +56,23 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
private var submitCodeAction: UIAlertAction?
init(group: OperationGroup, presentingViewController: UIViewController?)
init(context: AuthenticatedOperationContext, presentingViewController: UIViewController?)
{
self.group = group
self.context = context
self.presentingViewController = presentingViewController
super.init()
self.context.authenticationOperation = self
self.operationQueue.name = "com.altstore.AuthenticationOperation"
self.progress.totalUnitCount = 3
self.progress.totalUnitCount = 4
}
override func main()
{
super.main()
if let error = self.group.error
if let error = self.context.error
{
self.finish(.failure(error))
return
@@ -84,7 +85,8 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let account, let session):
case .success((let account, let session)):
self.context.session = session
self.progress.completedUnitCount += 1
// Fetch Team
@@ -95,6 +97,7 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
{
case .failure(let error): self.finish(.failure(error))
case .success(let team):
self.context.team = team
self.progress.completedUnitCount += 1
// Fetch Certificate
@@ -105,22 +108,33 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
{
case .failure(let error): self.finish(.failure(error))
case .success(let certificate):
self.context.certificate = certificate
self.progress.completedUnitCount += 1
// Save account/team to disk.
self.save(team) { (result) in
// Register Device
self.registerCurrentDevice(for: team, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
let signer = ALTSigner(team: team, certificate: certificate)
self.progress.completedUnitCount += 1
// Must cache App IDs _after_ saving account/team to disk.
self.cacheAppIDs(signer: signer, session: session) { (result) in
let result = result.map { _ in (signer, session) }
self.finish(result)
// Save account/team to disk.
self.save(team) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
// Must cache App IDs _after_ saving account/team to disk.
self.cacheAppIDs(team: team, session: session) { (result) in
let result = result.map { _ in (team, certificate, session) }
self.finish(result)
}
}
}
}
}
@@ -173,21 +187,21 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
}
}
override func finish(_ result: Result<(ALTSigner, ALTAppleAPISession), Error>)
override func finish(_ result: Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>)
{
guard !self.isFinished else { return }
print("Finished authenticating with result:", result)
print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.perform {
do
{
let (signer, session) = try result.get()
let (altTeam, altCertificate, session) = try result.get()
guard
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), signer.team.account.identifier), in: context),
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), signer.team.identifier), in: context)
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
else { throw AuthenticationError.noTeam }
// Account
@@ -214,24 +228,29 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
team.isActiveTeam = false
}
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.team == nil
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
{
// No team assigned to AltStore app yet, so assume this team was used to originally install it.
altStoreApp.team = team
UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit
}
else
{
UserDefaults.standard.activeAppsLimit = nil
}
// Save
try context.save()
// Update keychain
Keychain.shared.appleIDEmailAddress = signer.team.account.appleID
Keychain.shared.appleIDEmailAddress = altTeam.account.appleID
Keychain.shared.appleIDPassword = self.appleIDPassword
Keychain.shared.signingCertificate = signer.certificate.p12Data()
Keychain.shared.signingCertificatePassword = signer.certificate.machineIdentifier
Keychain.shared.signingCertificate = altCertificate.p12Data()
Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier
self.showInstructionsIfNecessary() { (didShowInstructions) in
let signer = ALTSigner(team: altTeam, certificate: altCertificate)
// Refresh screen must go last since a successful refresh will cause the app to quit.
self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in
super.finish(result)
@@ -320,7 +339,7 @@ private extension AuthenticationOperation
self.authenticate(appleID: appleID, password: password) { (result) in
switch result
{
case .success(let account, let session):
case .success((let account, let session)):
self.appleIDPassword = password
completionHandler(.success((account, session)))
@@ -340,7 +359,7 @@ private extension AuthenticationOperation
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
{
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(group: self.group)
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
fetchAnisetteDataOperation.resultHandler = { (result) in
switch result
{
@@ -557,13 +576,38 @@ private extension AuthenticationOperation
}
}
func cacheAppIDs(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
let group = OperationGroup()
group.signer = signer
group.session = session
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
return completionHandler(.failure(OperationError.unknownUDID))
}
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
if let device = devices.first(where: { $0.identifier == udid })
{
completionHandler(.success(device))
}
else
{
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team, session: session) { (device, error) in
completionHandler(Result(device, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
fetchAppIDsOperation.resultHandler = { (result) in
do
{
@@ -610,9 +654,11 @@ private extension AuthenticationOperation
completionHandler(false)
#else
DispatchQueue.main.async {
let context = AuthenticatedOperationContext(context: self.context)
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController
refreshViewController.signer = signer
refreshViewController.session = session
refreshViewController.context = context
refreshViewController.completionHandler = { _ in
completionHandler(true)
}

View File

@@ -0,0 +1,90 @@
//
// DeactivateAppOperation.swift
// AltStore
//
// Created by Riley Testut on 3/4/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
import AltKit
import Roxas
@objc(DeactivateAppOperation)
class DeactivateAppOperation: ResultOperation<InstalledApp>
{
let app: InstalledApp
let context: OperationContext
init(app: InstalledApp, context: OperationContext)
{
self.app = app
self.context = context
super.init()
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) }
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
print("Sending deactivate app request...")
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
let allIdentifiers = [installedApp.resignedBundleIdentifier] + appExtensionProfiles
let request = RemoveProvisioningProfilesRequest(udid: udid, bundleIdentifiers: Set(allIdentifiers))
connection.send(request) { (result) in
print("Sent deactive app request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
print("Waiting for deactivate app response...")
connection.receiveResponse() { (result) in
print("Receiving deactivate app response:", result)
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(.error(let response)): self.finish(.failure(response.error))
case .success(.removeProvisioningProfiles):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
self.progress.completedUnitCount += 1
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
installedApp.isActive = false
self.finish(.success(installedApp))
}
case .success: self.finish(.failure(ALTServerError(.unknownResponse)))
}
}
}
}
}
}
}
}
}

View File

@@ -16,26 +16,24 @@ import Roxas
@objc(FetchAnisetteDataOperation)
class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
{
let group: OperationGroup
let context: OperationContext
init(group: OperationGroup)
init(context: OperationContext)
{
self.group = group
super.init()
self.context = context
}
override func main()
{
super.main()
if let error = self.group.error
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let server = self.group.server else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
ServerManager.shared.connect(to: server) { (result) in
switch result
@@ -55,7 +53,7 @@ class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
case .success:
print("Waiting for anisette data...")
connection.receiveResponse() { (result) in
print("Receiving anisette data:", result)
print("Receiving anisette data:", result.error?.localizedDescription ?? "success")
switch result
{

View File

@@ -16,13 +16,13 @@ import Roxas
@objc(FetchAppIDsOperation)
class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
{
let group: OperationGroup
let context: NSManagedObjectContext
let context: AuthenticatedOperationContext
let managedObjectContext: NSManagedObjectContext
init(group: OperationGroup, context: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
init(context: AuthenticatedOperationContext, managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
{
self.group = group
self.context = context
self.managedObjectContext = managedObjectContext
super.init()
}
@@ -31,24 +31,24 @@ class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
{
super.main()
if let error = self.group.error
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard
let team = self.group.signer?.team,
let session = self.group.session
let team = self.context.team,
let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
self.context.perform {
self.managedObjectContext.perform {
do
{
let fetchedAppIDs = try Result(appIDs, error).get()
guard let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), team.identifier), in: self.context) else { throw OperationError.notAuthenticated }
guard let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), team.identifier), in: self.managedObjectContext) else { throw OperationError.notAuthenticated }
let fetchedIdentifiers = fetchedAppIDs.map { $0.identifier }
@@ -57,11 +57,11 @@ class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
#keyPath(AppID.team), team,
#keyPath(AppID.identifier), fetchedIdentifiers)
let deletedAppIDs = try self.context.fetch(deletedAppIDsRequest)
deletedAppIDs.forEach { self.context.delete($0) }
let deletedAppIDs = try self.managedObjectContext.fetch(deletedAppIDsRequest)
deletedAppIDs.forEach { self.managedObjectContext.delete($0) }
let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.context) }
self.finish(.success((appIDs, self.context)))
let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.managedObjectContext) }
self.finish(.success((appIDs, self.managedObjectContext)))
}
catch
{

View File

@@ -0,0 +1,431 @@
//
// FetchProvisioningProfilesOperation.swift
// AltStore
//
// Created by Riley Testut on 2/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltSign
@objc(FetchProvisioningProfilesOperation)
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
{
let context: AppOperationContext
private let appGroupsLock = NSLock()
init(context: AppOperationContext)
{
self.context = context
super.init()
self.progress.totalUnitCount = 1
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard
let app = self.context.app,
let team = self.context.team,
let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
do
{
self.progress.completedUnitCount += 1
let profile = try result.get()
var profiles = [app.bundleIdentifier: profile]
var error: Error?
let dispatchGroup = DispatchGroup()
for appExtension in app.appExtensions
{
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
}
dispatchGroup.leave()
self.progress.completedUnitCount += 1
}
}
dispatchGroup.notify(queue: .global()) {
if let error = error
{
self.finish(.failure(error))
}
else
{
self.finish(.success(profiles))
}
}
}
catch
{
self.finish(.failure(error))
}
}
}
func process<T>(_ result: Result<T, Error>) -> T?
{
switch result
{
case .failure(let error):
self.finish(.failure(error))
return nil
case .success(let value):
guard !self.isCancelled else {
self.finish(.failure(OperationError.cancelled))
return nil
}
return value
}
}
}
extension FetchProvisioningProfilesOperation
{
func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let preferredBundleID: String?
// Check if we have already installed this app with this team before.
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
if let installedApp = InstalledApp.first(satisfying: predicate, in: context)
{
// Teams match if installedApp.team has same identifier as team,
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
#if DEBUG
if app.bundleIdentifier == StoreApp.altstoreAppID || StoreApp.alternativeAltStoreAppIDs.contains(app.bundleIdentifier)
{
// Use legacy bundle ID format for AltStore.
preferredBundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
}
else
{
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
}
#else
if teamsMatch
{
// This app is already installed with the same team, so use the same resigned bundle identifier as before.
// This way, if we change the identifier format (again), AltStore will continue to use
// the old bundle identifier to prevent it from installing as a new app.
preferredBundleID = installedApp.resignedBundleIdentifier
}
else
{
preferredBundleID = nil
}
#endif
}
else
{
preferredBundleID = nil
}
let bundleID: String
if let preferredBundleID = preferredBundleID
{
bundleID = preferredBundleID
}
else
{
// This app isn't already installed, so create the resigned bundle identifier ourselves.
// Or, if the app _is_ installed but with a different team, we need to create a new
// bundle identifier anyway to prevent collisions with the previous team.
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
let updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
if app.bundleIdentifier == StoreApp.altstoreAppID || StoreApp.alternativeAltStoreAppIDs.contains(app.bundleIdentifier)
{
// Use legacy bundle ID format for AltStore.
bundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
}
else
{
bundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
}
}
let preferredName: String
if let parentApp = parentApp
{
preferredName = parentApp.name + " " + app.name
}
else
{
preferredName = app.name
}
// Register
self.registerAppID(for: app, name: preferredName, bundleIdentifier: bundleID, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update features
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update app groups
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Fetch Provisioning Profile
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
completionHandler(result)
}
}
}
}
}
}
}
}
}
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do
{
let appIDs = try Result(appIDs, error).get()
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleIdentifier })
{
completionHandler(.success(appID))
}
else
{
let requiredAppIDs = 1 + application.appExtensions.count
let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count)
let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 })
if team.type == .free
{
if requiredAppIDs > availableAppIDs
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
}
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
do
{
do
{
let appID = try Result(appID, error).get()
completionHandler(.success(appID))
}
catch ALTAppleAPIError.maximumAppIDLimitReached
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
return (feature, value)
}
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
{
features[.appGroups] = true
}
var updateFeatures = false
// Determine whether the required features are already enabled for the AppID.
for (feature, value) in features
{
if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue)
{
// AppID already has this feature enabled and the values are the same.
continue
}
else
{
// AppID either doesn't have this feature enabled or the value has changed,
// so we need to update it to reflect new values.
updateFeatures = true
break
}
}
if updateFeatures
{
let appID = appID.copy() as! ALTAppID
appID.features = features
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error))
}
}
else
{
completionHandler(.success(appID))
}
}
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
// TODO: Handle apps belonging to more than one app group.
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
return completionHandler(.success(appID))
}
func finish(_ result: Result<ALTAppGroup, Error>)
{
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let group):
// Assign App Group
// TODO: Determine whether app already belongs to app group.
ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
let result = result.map { _ in appID }
completionHandler(result)
}
}
}
// Dispatch onto global queue to prevent appGroupsLock deadlock.
DispatchQueue.global().async {
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
// Ensure we're not concurrently fetching and updating app groups,
// which can lead to race conditions such as adding an app group twice.
self.appGroupsLock.lock()
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
switch Result(groups, error)
{
case .failure(let error):
self.appGroupsLock.unlock()
completionHandler(.failure(error))
case .success(let groups):
if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
{
self.appGroupsLock.unlock()
finish(.success(group))
}
else
{
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
self.appGroupsLock.unlock()
finish(Result(group, error))
}
}
}
}
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
// Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success:
// Fetch new provisiong profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
}

View File

@@ -7,12 +7,15 @@
//
import Foundation
import CoreData
import Roxas
@objc(FetchSourceOperation)
class FetchSourceOperation: ResultOperation<Source>
{
let sourceURL: URL
let managedObjectContext: NSManagedObjectContext
private let session: URLSession
@@ -21,9 +24,10 @@ class FetchSourceOperation: ResultOperation<Source>
return dateFormatter
}()
init(sourceURL: URL)
init(sourceURL: URL, managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
{
self.sourceURL = sourceURL
self.managedObjectContext = managedObjectContext
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
@@ -37,7 +41,7 @@ class FetchSourceOperation: ResultOperation<Source>
super.main()
let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
self.managedObjectContext.perform {
do
{
let (data, _) = try Result((data, response), error).get()
@@ -64,19 +68,16 @@ class FetchSourceOperation: ResultOperation<Source>
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Date is in invalid format.")
})
decoder.managedObjectContext = context
decoder.managedObjectContext = self.managedObjectContext
decoder.sourceURL = self.sourceURL
let source = try decoder.decode(Source.self, from: data)
if let patreonAccessToken = source.userInfo?[.patreonAccessToken]
if source.identifier == Source.altStoreIdentifier, let patreonAccessToken = source.userInfo?[.patreonAccessToken]
{
Keychain.shared.patreonCreatorAccessToken = patreonAccessToken
}
#if STAGING
source.sourceURL = self.sourceURL
#endif
self.finish(.success(source))
}
catch

View File

@@ -23,27 +23,31 @@ private let ReceivedWiredServerConnectionResponse: @convention(c) (CFNotificatio
@objc(FindServerOperation)
class FindServerOperation: ResultOperation<Server>
{
let group: OperationGroup
let context: OperationContext
private var isWiredServerConnectionAvailable = false
init(group: OperationGroup)
{
self.group = group
super.init()
}
init(context: OperationContext = OperationContext())
{
self.context = context
}
override func main()
{
super.main()
if let error = self.group.error
if let error = self.context.error
{
self.finish(.failure(error))
return
}
if let server = self.context.server
{
self.finish(.success(server))
return
}
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
// Prepare observers to receive callback from wired server (if connected).

View File

@@ -16,11 +16,11 @@ import Roxas
@objc(InstallAppOperation)
class InstallAppOperation: ResultOperation<InstalledApp>
{
let context: AppOperationContext
let context: InstallAppOperationContext
private var didCleanUp = false
init(context: AppOperationContext)
init(context: InstallAppOperationContext)
{
self.context = context
@@ -40,6 +40,7 @@ class InstallAppOperation: ResultOperation<InstalledApp>
}
guard
let certificate = self.context.certificate,
let resignedApp = self.context.resignedApp,
let connection = self.context.installationConnection
else { return self.finish(.failure(OperationError.invalidParameters)) }
@@ -57,10 +58,10 @@ class InstallAppOperation: ResultOperation<InstalledApp>
}
else
{
installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, context: backgroundContext)
installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, certificateSerialNumber: certificate.serialNumber, context: backgroundContext)
}
installedApp.update(resignedApp: resignedApp)
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber)
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
{
@@ -108,9 +109,42 @@ class InstallAppOperation: ResultOperation<InstalledApp>
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
self.cleanUp()
self.context.group.beginInstallationHandler?(installedApp)
self.context.beginInstallationHandler?(installedApp)
let request = BeginInstallationRequest()
var activeProfiles: Set<String>?
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
{
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
let fetchRequest = InstalledApp.activeAppsFetchRequest()
fetchRequest.includesPendingChanges = false
var activeApps = InstalledApp.fetch(fetchRequest, in: backgroundContext)
if !activeApps.contains(installedApp)
{
let availableActiveApps = max(sideloadedAppsLimit - activeApps.count, 0)
let requiredActiveAppSlots = 1 + installedExtensions.count // As of iOS 13.3.1, app extensions count as "apps"
if requiredActiveAppSlots <= availableActiveApps
{
// This app has not been explicitly activated, but there are enough slots available,
// so implicitly activate it.
installedApp.isActive = true
activeApps.append(installedApp)
}
else
{
installedApp.isActive = false
}
}
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
})
}
let request = BeginInstallationRequest(activeProfiles: activeProfiles)
connection.send(request) { (result) in
switch result
{

View File

@@ -0,0 +1,108 @@
//
// Contexts.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import Network
import AltSign
class OperationContext
{
var server: Server?
var error: Error?
let operations: NSHashTable<Foundation.Operation>
init(server: Server? = nil, error: Error? = nil, operations: [Foundation.Operation] = [])
{
self.server = server
self.error = error
self.operations = NSHashTable<Foundation.Operation>.weakObjects()
for operation in operations
{
self.operations.add(operation)
}
}
convenience init(context: OperationContext)
{
self.init(server: context.server, error: context.error, operations: context.operations.allObjects)
}
}
class AuthenticatedOperationContext: OperationContext
{
var session: ALTAppleAPISession?
var team: ALTTeam?
var certificate: ALTCertificate?
weak var authenticationOperation: AuthenticationOperation?
convenience init(context: AuthenticatedOperationContext)
{
self.init(server: context.server, error: context.error, operations: context.operations.allObjects)
self.session = context.session
self.team = context.team
self.certificate = context.certificate
self.authenticationOperation = context.authenticationOperation
}
}
@dynamicMemberLookup
class AppOperationContext
{
let bundleIdentifier: String
private let authenticatedContext: AuthenticatedOperationContext
var app: ALTApplication?
var provisioningProfiles: [String: ALTProvisioningProfile]?
var isFinished = false
var error: Error? {
get {
return _error ?? self.authenticatedContext.error
}
set {
_error = newValue
}
}
private var _error: Error?
init(bundleIdentifier: String, authenticatedContext: AuthenticatedOperationContext)
{
self.bundleIdentifier = bundleIdentifier
self.authenticatedContext = authenticatedContext
}
subscript<T>(dynamicMember keyPath: WritableKeyPath<AuthenticatedOperationContext, T>) -> T
{
return self.authenticatedContext[keyPath: keyPath]
}
}
class InstallAppOperationContext: AppOperationContext
{
lazy var temporaryDirectory: URL = {
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) }
catch { self.error = error }
return temporaryDirectory
}()
var resignedApp: ALTApplication?
var installationConnection: ServerConnection?
var beginInstallationHandler: ((InstalledApp) -> Void)?
}

View File

@@ -24,12 +24,11 @@ enum OperationError: LocalizedError
case invalidParameters
case iOSVersionNotSupported(ALTApplication)
case sideloadingAppNotSupported(ALTApplication)
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
case noSources
var errorDescription: String? {
var failureReason: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
@@ -52,10 +51,6 @@ enum OperationError: LocalizedError
let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version)
return localizedDescription
case .sideloadingAppNotSupported(let app):
let localizedDescription = String(format: NSLocalizedString("Sideloading “%@” Not Supported", comment: ""), app.name)
return localizedDescription
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
}
}

View File

@@ -1,86 +0,0 @@
//
// OperationGroup.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
class OperationGroup
{
let progress = Progress.discreteProgress(totalUnitCount: 0)
var completionHandler: ((Result<[String: Result<InstalledApp, Error>], Error>) -> Void)?
var beginInstallationHandler: ((InstalledApp) -> Void)?
var session: ALTAppleAPISession?
var server: Server?
var signer: ALTSigner?
var error: Error?
var results = [String: Result<InstalledApp, Error>]()
private var progressByBundleIdentifier = [String: Progress]()
private let operationQueue = OperationQueue()
private let installOperationQueue = OperationQueue()
init()
{
// Enforce only one installation at a time.
self.installOperationQueue.maxConcurrentOperationCount = 1
}
func cancel()
{
self.operationQueue.cancelAllOperations()
self.installOperationQueue.cancelAllOperations()
}
func addOperations(_ operations: [Operation])
{
for operation in operations
{
if let installOperation = operation as? InstallAppOperation
{
if let previousOperation = self.installOperationQueue.operations.last
{
// Ensures they execute in the order they're added, since isReady is still false at this point.
installOperation.addDependency(previousOperation)
}
self.installOperationQueue.addOperation(installOperation)
}
else
{
self.operationQueue.addOperation(operation)
}
}
}
func set(_ progress: Progress, for app: AppProtocol)
{
self.progressByBundleIdentifier[app.bundleIdentifier] = progress
self.progress.totalUnitCount += 1
self.progress.addChild(progress, withPendingUnitCount: 1)
}
func progress(for app: AppProtocol) -> Progress?
{
return self.progress(forAppWithBundleIdentifier: app.bundleIdentifier)
}
func progress(forAppWithBundleIdentifier bundleIdentifier: String) -> Progress?
{
let progress = self.progressByBundleIdentifier[bundleIdentifier]
return progress
}
}

View File

@@ -1,81 +0,0 @@
//
// PrepareDeveloperAccountOperation.swift
// AltStore
//
// Created by Riley Testut on 1/7/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltSign
@objc(PrepareDeveloperAccountOperation)
class PrepareDeveloperAccountOperation: ResultOperation<Void>
{
let group: OperationGroup
init(group: OperationGroup)
{
self.group = group
super.init()
self.progress.totalUnitCount = 2
}
override func main()
{
super.main()
if let error = self.group.error
{
self.finish(.failure(error))
return
}
guard
let signer = self.group.signer,
let session = self.group.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
// Register Device
self.registerCurrentDevice(for: signer.team, session: session) { (result) in
let result = result.map { _ in () }
self.finish(result)
}
}
}
private extension PrepareDeveloperAccountOperation
{
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
return completionHandler(.failure(OperationError.unknownUDID))
}
ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
if let device = devices.first(where: { $0.identifier == udid })
{
completionHandler(.success(device))
}
else
{
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team, session: session) { (device, error) in
completionHandler(Result(device, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}

View File

@@ -0,0 +1,124 @@
//
// RefreshAppOperation.swift
// AltStore
//
// Created by Riley Testut on 2/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
import AltKit
import Roxas
@objc(RefreshAppOperation)
class RefreshAppOperation: ResultOperation<InstalledApp>
{
let context: AppOperationContext
// Strong reference to managedObjectContext to keep it alive until we're finished.
let managedObjectContext: NSManagedObjectContext
init(context: AppOperationContext)
{
self.context = context
self.managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
super.init()
}
override func main()
{
super.main()
do
{
if let error = self.context.error
{
throw error
}
guard
let server = self.context.server,
let app = self.context.app,
let profiles = self.context.provisioningProfiles
else { throw OperationError.invalidParameters }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
print("Sending refresh app request...")
var activeProfiles: Set<String>?
if UserDefaults.standard.activeAppsLimit != nil
{
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
let activeApps = InstalledApp.fetchActiveApps(in: context)
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
})
}
let request = InstallProvisioningProfilesRequest(udid: udid, provisioningProfiles: Set(profiles.values), activeProfiles: activeProfiles)
connection.send(request) { (result) in
print("Sent refresh app request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
print("Waiting for refresh app response...")
connection.receiveResponse() { (result) in
print("Receiving refresh app response:", result)
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(.error(let response)): self.finish(.failure(response.error))
case .success(.installProvisioningProfiles):
self.managedObjectContext.perform {
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
return self.finish(.failure(OperationError.invalidApp))
}
self.progress.completedUnitCount += 1
if let provisioningProfile = profiles[app.bundleIdentifier]
{
installedApp.update(provisioningProfile: provisioningProfile)
}
for installedExtension in installedApp.appExtensions
{
guard let provisioningProfile = profiles[installedExtension.bundleIdentifier] else { continue }
installedExtension.update(provisioningProfile: provisioningProfile)
}
self.finish(.success(installedApp))
}
case .success: self.finish(.failure(ALTServerError(.unknownRequest)))
}
}
}
}
}
}
}
}
catch
{
self.finish(.failure(error))
}
}
}

View File

@@ -0,0 +1,77 @@
//
// RefreshGroup.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
class RefreshGroup: NSObject
{
let context: AuthenticatedOperationContext
let progress = Progress.discreteProgress(totalUnitCount: 0)
var completionHandler: (([String: Result<InstalledApp, Error>]) -> Void)?
var beginInstallationHandler: ((InstalledApp) -> Void)?
private(set) var results = [String: Result<InstalledApp, Error>]()
private var isFinished = false
private let dispatchGroup = DispatchGroup()
private var operations: [Foundation.Operation] = []
init(context: AuthenticatedOperationContext = AuthenticatedOperationContext())
{
self.context = context
super.init()
}
func add(_ operations: [Foundation.Operation])
{
for operation in operations
{
self.dispatchGroup.enter()
operation.completionBlock = { [weak self] in
self?.dispatchGroup.leave()
}
}
if self.operations.isEmpty && !operations.isEmpty
{
self.dispatchGroup.notify(queue: .global()) {
self.finish()
}
}
self.operations.append(contentsOf: operations)
}
func set(_ result: Result<InstalledApp, Error>, forAppWithBundleIdentifier bundleIdentifier: String)
{
self.results[bundleIdentifier] = result
}
func cancel()
{
self.operations.forEach { $0.cancel() }
}
}
private extension RefreshGroup
{
func finish()
{
guard !self.isFinished else { return }
self.isFinished = true
self.completionHandler?(self.results)
}
}

View File

@@ -14,9 +14,9 @@ import AltSign
@objc(ResignAppOperation)
class ResignAppOperation: ResultOperation<ALTApplication>
{
let context: AppOperationContext
let context: InstallAppOperationContext
init(context: AppOperationContext)
init(context: InstallAppOperationContext)
{
self.context = context
@@ -37,46 +37,42 @@ class ResignAppOperation: ResultOperation<ALTApplication>
guard
let app = self.context.app,
let signer = self.context.group.signer,
let session = self.context.group.session
let profiles = self.context.provisioningProfiles,
let team = self.context.team,
let certificate = self.context.certificate
else { return self.finish(.failure(OperationError.invalidParameters)) }
// Prepare Provisioning Profiles
self.prepareProvisioningProfiles(app.fileURL, team: signer.team, session: session) { (result) in
guard let profiles = self.process(result) else { return }
// Prepare app bundle
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
guard let appBundleURL = self.process(result) else { return }
// Prepare app bundle
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
print("Resigning App:", self.context.bundleIdentifier)
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
guard let appBundleURL = self.process(result) else { return }
// Resign app bundle
let resignProgress = self.resignAppBundle(at: appBundleURL, team: team, certificate: certificate, profiles: Array(profiles.values)) { (result) in
guard let resignedURL = self.process(result) else { return }
print("Resigning App:", self.context.bundleIdentifier)
// Resign app bundle
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in
guard let resignedURL = self.process(result) else { return }
// Finish
do
{
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
// Finish
do
{
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
// Use appBundleURL since we need an app bundle, not .ipa.
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
self.finish(.success(resignedApplication))
}
catch
{
self.finish(.failure(error))
}
// Use appBundleURL since we need an app bundle, not .ipa.
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
self.finish(.success(resignedApplication))
}
catch
{
self.finish(.failure(error))
}
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
}
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
}
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
}
func process<T>(_ result: Result<T, Error>) -> T?
@@ -100,310 +96,6 @@ class ResignAppOperation: ResultOperation<ALTApplication>
private extension ResignAppOperation
{
func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
{
guard let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
var profiles = [app.bundleIdentifier: profile]
var error: Error?
let dispatchGroup = DispatchGroup()
for appExtension in app.appExtensions
{
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
if let error = error
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(profiles))
}
}
}
}
}
func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let preferredBundleID: String
// Check if we have already installed this app with this team before.
let predicate = NSPredicate(format: "%K == %@ AND %K == %@",
#keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier,
#keyPath(InstalledApp.team.identifier), team.identifier)
if let installedApp = InstalledApp.first(satisfying: predicate, in: context)
{
// This app is already installed, so use the same resigned bundle identifier as before.
// This way, if we change the identifier format (again), AltStore will continue to use
// the old bundle identifier to prevent it from installing as a new app.
preferredBundleID = installedApp.resignedBundleIdentifier
}
else
{
// This app isn't already installed, so create the resigned bundle identifier ourselves.
// Or, if the app _is_ installed but with a different team, we need to create a new
// bundle identifier anyway to prevent collisions with the previous team.
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
let updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
preferredBundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
}
let preferredName: String
if let parentApp = parentApp
{
preferredName = "\(parentApp.name) - \(app.name)"
}
else
{
preferredName = app.name
}
// Register
self.registerAppID(for: app, name: preferredName, bundleIdentifier: preferredBundleID, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update features
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update app groups
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Fetch Provisioning Profile
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
completionHandler(result)
}
}
}
}
}
}
}
}
}
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do
{
let appIDs = try Result(appIDs, error).get()
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleIdentifier })
{
completionHandler(.success(appID))
}
else
{
let requiredAppIDs = 1 + application.appExtensions.count
let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count)
let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 })
if team.type == .free
{
if requiredAppIDs > availableAppIDs
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
}
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
do
{
do
{
let appID = try Result(appID, error).get()
completionHandler(.success(appID))
}
catch ALTAppleAPIError.maximumAppIDLimitReached
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
return (feature, value)
}
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
{
features[.appGroups] = true
}
var updateFeatures = false
// Determine whether the required features are already enabled for the AppID.
for (feature, value) in features
{
if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue)
{
// AppID already has this feature enabled and the values are the same.
continue
}
else
{
// AppID either doesn't have this feature enabled or the value has changed,
// so we need to update it to reflect new values.
updateFeatures = true
break
}
}
if updateFeatures
{
let appID = appID.copy() as! ALTAppID
appID.features = features
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error))
}
}
else
{
completionHandler(.success(appID))
}
}
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
// TODO: Handle apps belonging to more than one app group.
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
return completionHandler(.success(appID))
}
func finish(_ result: Result<ALTAppGroup, Error>)
{
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let group):
// Assign App Group
// TODO: Determine whether app already belongs to app group.
ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
let result = result.map { _ in appID }
completionHandler(result)
}
}
}
let adjustedGroupIdentifier = "group.\(team.identifier)." + groupIdentifier
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
switch Result(groups, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let groups):
if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
{
finish(.success(group))
}
else
{
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
finish(Result(group, error))
}
}
}
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
// Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success:
// Fetch new provisiong profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
@@ -466,7 +158,7 @@ private extension ResignAppOperation
var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes]
if self.context.bundleIdentifier == StoreApp.altstoreAppID || self.context.bundleIdentifier == StoreApp.alternativeAltStoreAppID
if self.context.bundleIdentifier == StoreApp.altstoreAppID || StoreApp.alternativeAltStoreAppIDs.contains(self.context.bundleIdentifier)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
additionalValues[Bundle.Info.deviceID] = udid
@@ -511,8 +203,9 @@ private extension ResignAppOperation
return progress
}
func resignAppBundle(at fileURL: URL, signer: ALTSigner, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
func resignAppBundle(at fileURL: URL, team: ALTTeam, certificate: ALTCertificate, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let signer = ALTSigner(team: team, certificate: certificate)
let progress = signer.signApp(at: fileURL, provisioningProfiles: profiles) { (success, error) in
do
{

View File

@@ -39,7 +39,7 @@ class SendAppOperation: ResultOperation<ServerConnection>
return
}
guard let app = self.context.app, let server = self.context.group.server else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let app = self.context.app, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
let fileURL = InstalledApp.refreshedIPAURL(for: app)

View File

@@ -8,6 +8,7 @@
import Foundation
import AuthenticationServices
import CoreData
private let clientID = "ZMx0EGUWe4TVWYXNZZwK_fbIK5jHFVWoUf1Qb-sqNXmT-YzAGwDPxxq7ak3_W5Q2"
private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt"
@@ -115,7 +116,7 @@ extension PatreonAPI
switch result
{
case .failure(let error): completion(.failure(error))
case .success(let accessToken, let refreshToken):
case .success((let accessToken, let refreshToken)):
Keychain.shared.patreonAccessToken = accessToken
Keychain.shared.patreonRefreshToken = refreshToken
@@ -235,11 +236,13 @@ extension PatreonAPI
func signOut(completion: @escaping (Result<Void, Swift.Error>) -> Void)
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let accounts = PatreonAccount.all(in: context)
accounts.forEach(context.delete(_:))
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
@@ -253,10 +256,7 @@ extension PatreonAPI
}
}
}
}
extension PatreonAPI
{
func refreshPatreonAccount()
{
guard PatreonAPI.shared.isAuthenticated else { return }
@@ -265,6 +265,13 @@ extension PatreonAPI
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
@@ -391,6 +398,15 @@ private extension PatreonAPI
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, *)

View File

@@ -8,25 +8,43 @@
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) -> Self?
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, returnFirstResult: true)
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) -> [Self]
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, returnFirstResult: false)
let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, requestProperties: requestProperties, returnFirstResult: false)
return managedObjects
}
private static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext, returnFirstResult: Bool) -> [Self]
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 })
@@ -38,24 +56,24 @@ extension Fetchable
let fetchRequest = self.fetchRequest() as! NSFetchRequest<Self>
fetchRequest.predicate = predicate
fetchRequest.sortDescriptors = sortDescriptors
fetchRequest.returnsObjectsAsFaults = false
do
for (keyPath, value) in requestProperties
{
let managedObjects = try context.fetch(fetchRequest)
if let managedObject = managedObjects.first, returnFirstResult
{
return [managedObject]
}
else
{
return managedObjects
}
// Still no easy way to cast PartialKeyPath back to usable WritableKeyPath :(
guard let objcKeyString = keyPath._kvcKeyPathString else { continue }
fetchRequest.setValue(value, forKey: objcKeyString)
}
catch
let fetchedObjects = self.fetch(fetchRequest, in: context)
if let fetchedObject = fetchedObjects.first, returnFirstResult
{
print("Failed to fetch managed objects.", error)
return []
return [fetchedObject]
}
else
{
return fetchedObjects
}
}
}

View File

@@ -0,0 +1,75 @@
{
"name": "AltStore (Alpha)",
"identifier": "com.rileytestut.AltStore.Alpha",
"apps": [
{
"name": "AltStore (Alpha)",
"bundleIdentifier": "com.rileytestut.AltStore.Alpha",
"developerName": "Riley Testut",
"subtitle": "An alternative App Store for iOS.",
"version": "1.3a2",
"versionDate": "2020-03-31T15:00:00-07:00",
"versionDescription": "• Adds analytics for downloads, updates, and crashes.\n• Deactivates beta apps once you are no longer a patron\n• Removes \"Sideloading in Beta\" alert\n• Fixes sideloading apps from News tab\n• Fixes missing recovery suggestions for some errors\n• Fixes secondary AltStore appearing after refresh under certain circumstances\n",
"downloadURL": "https://f000.backblazeb2.com/file/altstore/sources/alpha/altstore/1_3_a2.ipa",
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore allows you to install Delta as well as any app (.ipa) directly from the Files app.",
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
"tintColor": "018084",
"size": 2184796,
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/65605563-2f009d00-df5e-11e9-9b40-1f36135d5c80.PNG",
"https://user-images.githubusercontent.com/705880/65605569-30ca6080-df5e-11e9-8dfb-15ebb00e10cb.PNG",
"https://user-images.githubusercontent.com/705880/65605577-332cba80-df5e-11e9-9f00-b369ce974f71.PNG"
],
"permissions": [
{
"type": "background-fetch",
"usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring."
},
{
"type": "background-audio",
"usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background."
}
]
},
{
"name": "Delta (Alpha)",
"bundleIdentifier": "com.rileytestut.Delta.Alpha",
"developerName": "Riley Testut",
"subtitle": "Classic games in your pocket.",
"version": "1.2a1",
"versionDate": "2020-03-27T17:30:00-07:00",
"versionDescription": "• Switches to melonDS from DeSmuME for Nintendo DS emulation.\n• Adds support for fast forwarding DS games\n• Adds ability to select different fast forward speeds by long pressing Fast Forward option in pause menu.",
"downloadURL": "https://f000.backblazeb2.com/file/altstore/alpha/delta/1_2_a1.ipa",
"localizedDescription": "The next console for Delta is coming: this beta version of Delta brings support for playing DS games!\n\nDS support currently includes:\n• Playing DS games\n• Save States\n• Hold Button\n\nFeatures I'm still working on:\n• Fast Forward\n• Cheats\n• Controller skin (using placeholder controller skin for now)\n\nPlease report any issues you find to support@altstore.io. Thanks!",
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
"tintColor": "8A28F7",
"size": 17631726,
"permissions": [
{
"type": "photos",
"usageDescription": "Allows Delta to use images from your Photo Library as game artwork."
}
],
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/65600448-f7d9be00-df54-11e9-9e3e-d4c31296da94.PNG",
"https://user-images.githubusercontent.com/705880/65601942-e5ad4f00-df57-11e9-9255-1463e0296e46.PNG",
"https://user-images.githubusercontent.com/705880/65813009-f2ae8600-e183-11e9-9eb7-704effc11173.png",
"https://user-images.githubusercontent.com/705880/65601117-58b5c600-df56-11e9-9c19-9a5ba5da54cf.PNG"
]
}
],
"news": [
{
"title": "Welcome to AltStore (Alpha)",
"identifier": "welcome-to-altstore-alpha",
"caption": "Please read the FAQ for help with installing apps.",
"tintColor": "018084",
"url": "https://altstore.io/faq/",
"date": "2019-09-28",
"notify": false
}
],
"userInfo": {
"patreonAccessToken": "BSLUtJEvetvJNgOsqz0v98-FmBO5JYDfuO90o-reGB8"
}
}

View File

@@ -7,14 +7,14 @@
"name": "AltStore",
"bundleIdentifier": "com.rileytestut.AltStore",
"developerName": "Riley Testut",
"version": "1.1.3",
"versionDate": "2020-01-29T12:30:00-08:00",
"versionDescription": "• Fixes crash when running on iOS 13.3.1",
"version": "1.2",
"versionDate": "2020-02-12T08:00:00-08:00",
"versionDescription": "FEATURES\n• Install and refresh apps over USB, not just WiFi (requires updating to latest AltServer version)\n• Supports sideloading unc0ver (support for sideloading any app coming soon)\n\nBUG FIXES\n• Fixes \"Device Already Registered\" error\n• Fixes \"Session Expired\" error when installing apps on slow connection",
"downloadURL": "https://f000.backblazeb2.com/file/altstore/altstore.ipa",
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis initial release of AltStore allows you to install Delta, an all-in-one emulator for iOS, with support for installing 3rd party apps coming soon.",
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
"tintColor": "018084",
"size": 1993423,
"size": 2114068,
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/65605563-2f009d00-df5e-11e9-9b40-1f36135d5c80.PNG",
"https://user-images.githubusercontent.com/705880/65605569-30ca6080-df5e-11e9-8dfb-15ebb00e10cb.PNG",
@@ -36,14 +36,14 @@
"bundleIdentifier": "com.rileytestut.AltStore.Beta",
"developerName": "Riley Testut",
"subtitle": "An alternative App Store for iOS.",
"version": "1.2b4",
"versionDate": "2020-02-11T18:30:00-08:00",
"versionDescription": "• View all registered App IDs for your account\n• Fixed inaccurate remaining App ID count\n\nThis is the final beta for AltStore 1.2 before the public release, which includes support for installing apps completely over USB. To test installing over USB, make sure to download the AltServer beta from https://altstore.io/altserver/beta",
"version": "1.3b3",
"versionDate": "2020-04-01T15:30:00-07:00",
"versionDescription": "** Requires latest AltServer beta to fully work. Download from https://altstore.io/altserver/beta **\n\nAltStore 1.3 is nearing completion, so this beta focuses on fixing the last remaining known issues with sideloading. Assuming nothing major is reported, this should be the final beta before the public release!\n\nNEW FEATURES\n• Adds search bar in Browse tab\n• Adds Visual Studio App Center analytics for automatic crash reporting\n\nIMPROVEMENTS\n• Deactivates beta apps once you are no longer a patron\n• Removes \"Sideloading in Beta\" alert\n\nBUG FIXES\n• Fixes sideloading apps from News tab\n• Fixes missing recovery suggestions for some errors\n• Fixes secondary AltStore appearing after refresh under certain circumstances\n\nNOTE: Support for 3rd party sources will be disabled in public versions of AltStore until it is finished.",
"downloadURL": "https://f000.backblazeb2.com/file/altstore/altstore-beta.ipa",
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore allows you to install Delta as well as any app (.ipa) directly from the Files app.",
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
"tintColor": "018084",
"size": 2114670,
"size": 2532618,
"beta": true,
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/65605563-2f009d00-df5e-11e9-9b40-1f36135d5c80.PNG",
@@ -231,6 +231,6 @@
}
],
"userInfo": {
"patreonAccessToken": "JLh5bpOQsPg-HIRe6FHnRAktoeeU4JT5-Xk-y7njQrM"
"patreonAccessToken": "sHEBO5oFWNVqi-bVtYcJn5TXftnAnatR9TVH4C-mUGQ"
}
}

View File

@@ -227,8 +227,8 @@ private extension PatreonViewController
catch
{
DispatchQueue.main.async {
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
}
@@ -250,8 +250,8 @@ private extension PatreonViewController
catch
{
DispatchQueue.main.async {
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
}

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -608,7 +608,7 @@
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" layoutMarginsFollowReadableWidth="YES" contentInsetAdjustmentBehavior="never" indicatorStyle="white" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oQQ-pR-oKc">
<rect key="frame" x="0.0" y="44" width="375" height="574"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<string key="text">Jay Freeman (ldid)
<mutableString key="text">Jay Freeman (ldid)
Copyright (C) 2007-2012 Jay Freeman (saurik)
libimobiledevice
@@ -629,6 +629,12 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Craig Hockenberry (MarkdownAttributedString)
Copyright (c) 2020 The Iconfactory, Inc. https://iconfactory.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The OpenSSL Project (OpenSSL)
Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
@@ -679,7 +685,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
ICONS
Settings by i cons from the Noun Project</string>
Settings by i cons from the Noun Project</mutableString>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>

View File

@@ -204,8 +204,8 @@ private extension SettingsViewController
break
case .failure(let error):
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
let toastView = ToastView(error: error)
toastView.show(in: self)
case .success: break
}
@@ -223,8 +223,8 @@ private extension SettingsViewController
DispatchQueue.main.async {
if let error = error
{
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
self.update()
@@ -433,7 +433,7 @@ extension SettingsViewController
else
{
let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
toastView.show(in: self)
}
case .refreshAttempts: break
@@ -450,8 +450,8 @@ extension SettingsViewController: MFMailComposeViewControllerDelegate
{
if let error = error
{
let toastView = ToastView(text: error.localizedDescription, detailText: "")
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
controller.dismiss(animated: true, completion: nil)

View File

@@ -0,0 +1,179 @@
//
// SourcesViewController.swift
// AltStore
//
// Created by Riley Testut on 3/17/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
import CoreData
import Roxas
class SourcesViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView.dataSource = self.dataSource
}
}
private extension SourcesViewController
{
func makeDataSource() -> RSTFetchedResultsCollectionViewDataSource<Source>
{
let fetchRequest = Source.fetchRequest() as NSFetchRequest<Source>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Source.name, ascending: true),
NSSortDescriptor(keyPath: \Source.sourceURL, ascending: true),
NSSortDescriptor(keyPath: \Source.identifier, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
let dataSource = RSTFetchedResultsCollectionViewDataSource<Source>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.proxy = self
dataSource.cellConfigurationHandler = { (cell, source, indexPath) in
let tintColor = UIColor.altPrimary
let cell = cell as! BannerCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = tintColor
cell.bannerView.iconImageView.isHidden = true
cell.bannerView.betaBadgeView.isHidden = true
cell.bannerView.buttonLabel.isHidden = true
cell.bannerView.button.isHidden = true
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.titleLabel.text = source.name
cell.bannerView.subtitleLabel.text = source.sourceURL.absoluteString
cell.bannerView.subtitleLabel.numberOfLines = 2
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
}
return dataSource
}
}
private extension SourcesViewController
{
@IBAction func addSource()
{
func addSource(url: URL)
{
AppManager.shared.fetchSource(sourceURL: url) { (result) in
do
{
let source = try result.get()
try source.managedObjectContext?.save()
}
catch let error as NSError
{
let error = error.withLocalizedFailure(NSLocalizedString("Could not add source.", comment: ""))
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
}
}
let alertController = UIAlertController(title: NSLocalizedString("Add Source", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.placeholder = "https://apps.altstore.io"
textField.textContentType = .URL
}
alertController.addAction(.cancel)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Add", comment: ""), style: .default) { (action) in
guard let text = alertController.textFields![0].text, let sourceURL = URL(string: text) else { return }
addSource(url: sourceURL)
})
self.present(alertController, animated: true, completion: nil)
}
}
extension SourcesViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: collectionView.bounds.width, height: 80)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
{
let indexPath = IndexPath(row: 0, section: section)
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
// Use this view to calculate the optimal size based on the collection view's width
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required, // Width is fixed
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
return size
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! TextCollectionReusableView
headerView.layoutMargins.left = self.view.layoutMargins.left
headerView.layoutMargins.right = self.view.layoutMargins.right
return headerView
}
}
@available(iOS 13, *)
extension SourcesViewController
{
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?
{
let source = self.dataSource.item(at: indexPath)
guard source.identifier != Source.altStoreIdentifier else { return nil }
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in
let deleteAction = UIAction(title: NSLocalizedString("Remove", comment: ""), image: UIImage(systemName: "trash"), attributes: [.destructive]) { (action) in
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let source = context.object(with: source.objectID) as! Source
context.delete(source)
do
{
try context.save()
}
catch
{
print("Failed to save source context.", error)
}
}
}
let menu = UIMenu(title: "", children: [deleteAction])
return menu
}
}
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
{
guard let indexPath = configuration.identifier as? NSIndexPath else { return nil }
guard let cell = collectionView.cellForItem(at: indexPath as IndexPath) as? BannerCollectionViewCell else { return nil }
let parameters = UIPreviewParameters()
parameters.backgroundColor = .clear
parameters.visiblePath = UIBezierPath(roundedRect: cell.bannerView.bounds, cornerRadius: cell.bannerView.layer.cornerRadius)
let preview = UITargetedPreview(view: cell.bannerView, parameters: parameters)
return preview
}
override func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
{
return self.collectionView(collectionView, previewForHighlightingContextMenuWithConfiguration: configuration)
}
}

View File

@@ -1 +1 @@
github "sindresorhus/LaunchAtLogin" "v2.5.0"
github "sindresorhus/LaunchAtLogin" "v3.0.0"

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
@@ -151,16 +151,16 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastUpgradeCheck = 0940;
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "Sindre Sorhus";
TargetAttributes = {
E32E9B621EB87D7B000FEEE9 = {
CreatedOnToolsVersion = 8.3.2;
LastSwiftMigration = "";
LastSwiftMigration = 1020;
};
E32E9B731EB87EA3000FEEE9 = {
CreatedOnToolsVersion = 8.3.2;
LastSwiftMigration = "";
LastSwiftMigration = 1020;
SystemCapabilities = {
com.apple.HardenedRuntime = {
enabled = 1;
@@ -173,8 +173,8 @@
};
};
buildConfigurationList = E32E9B5D1EB87D7B000FEEE9 /* Build configuration list for PBXProject "LaunchAtLogin" */;
compatibilityVersion = "Xcode 10.0";
developmentRegion = English;
compatibilityVersion = "Xcode 11.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
@@ -295,6 +295,7 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_COMPILATION_MODE = singlefile;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VALID_ARCHS = x86_64;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -350,6 +351,7 @@
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
VALID_ARCHS = x86_64;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -364,7 +366,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = YG56YK5RN5;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
@@ -376,13 +378,13 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.LaunchAtLogin;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = singlefile;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.2;
};
name = Debug;
};
@@ -394,7 +396,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = YG56YK5RN5;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
@@ -406,11 +408,11 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.LaunchAtLogin;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_VERSION = 4.2;
};
name = Release;
};
@@ -418,10 +420,10 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = LaunchAtLoginHelper/LaunchAtLoginHelper.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = YG56YK5RN5;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = LaunchAtLoginHelper/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -432,7 +434,6 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_VERSION = 4.2;
};
name = Debug;
};
@@ -440,10 +441,10 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = LaunchAtLoginHelper/LaunchAtLoginHelper.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = YG56YK5RN5;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = LaunchAtLoginHelper/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -454,7 +455,6 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_VERSION = 4.2;
};
name = Release;
};

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0940"
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0940"
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -13,7 +13,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.5.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>

View File

@@ -9,7 +9,9 @@ public struct LaunchAtLogin {
guard let jobs = (SMCopyAllJobDictionaries(kSMDomainUserLaunchd).takeRetainedValue() as? [[String: AnyObject]]) else {
return false
}
let job = jobs.first { $0["Label"] as! String == id }
return job?["OnDemand"] as? Bool ?? false
}
set {

View File

@@ -1,20 +1,16 @@
# LaunchAtLogin
> Add "Launch at Login" functionality to your macOS app in seconds
> Add Launch at Login functionality to your macOS app in seconds
It's usually quite a [convoluted and error-prone process](before-after.md) to add this. **No more!**
This package works with both sandboxed and non-sandboxed apps and it's App Store compatible and used in my [Lungo](https://blog.sindresorhus.com/lungo-b364a6c2745f) and [Battery Indicator](https://sindresorhus.com/battery-indicator) apps.
*You might also find my [`create-dmg`](https://github.com/sindresorhus/create-dmg) project useful if you're publishing your app outside the App Store.*
This package works with both sandboxed and non-sandboxed apps and it's App Store compatible and used in apps like [Plash](https://github.com/sindresorhus/Plash), [Dato](https://sindresorhus.com/dato), [Lungo](https://sindresorhus.com/lungo), and [Battery Indicator](https://sindresorhus.com/battery-indicator).
## Requirements
- macOS 10.12+
- Xcode 10+
- Swift 4.2+
- Xcode 11+
- Swift 5+
## Install
@@ -24,33 +20,14 @@ This package works with both sandboxed and non-sandboxed apps and it's App Store
github "sindresorhus/LaunchAtLogin"
```
#### CocoaPods
```ruby
pod 'LaunchAtLogin'
```
<a href="https://www.patreon.com/sindresorhus">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a>
## Usage
Add a new ["Run Script Phase"](http://stackoverflow.com/a/39633955/64949) below "Embed Frameworks" in "Build Phases" with the following:
Carthage:
```sh
"${PROJECT_DIR}/Carthage/Build/Mac/LaunchAtLogin.framework/Resources/copy-helper.sh"
```
CocoaPods:
```sh
"${PROJECT_DIR}/Pods/LaunchAtLogin/LaunchAtLogin/copy-helper.sh"
```
Use it in your app:
```swift
@@ -65,22 +42,47 @@ print(LaunchAtLogin.isEnabled)
//=> true
```
*Note that the [Mac App Store guidelines](https://developer.apple.com/app-store/review/guidelines/) requires "launch at login" functionality to be enabled in response to a user action. This is usually solved by making it a preference that is disabled by default.*
No need to store any state to UserDefaults.
*Note that the [Mac App Store guidelines](https://developer.apple.com/app-store/review/guidelines/) requires “launch at login” functionality to be enabled in response to a user action. This is usually solved by making it a preference that is disabled by default. Many apps also let the user activate it in a welcome screen.*
## How does it work?
The framework bundles the helper app needed to launch your app and copies it into your app at build time.
## FAQ
#### My app doesn't show up in “System Preferences Users & Groups Login Items”
[This is the expected behavior](https://stackoverflow.com/a/15104481/64949), unfortunately.
#### My app doesn't launch at login when testing
This is usually caused by having one or more older builds of your app laying around somewhere on the system, and macOS picking one of those instead, which doesn't have the launch helper, and thus fails to start.
Some things you can try:
- Bump the version & build of your app so macOS is more likely to pick it.
- Delete the [`DerivedData` directory](https://mgrebenets.github.io/mobile%20ci/2015/02/01/xcode-derived-data).
- Ensure you don't have any other builds laying around somewhere.
Some helpful Stack Overflow answers:
- https://stackoverflow.com/a/43281810/64949
- https://stackoverflow.com/a/51683190/64949
- https://stackoverflow.com/a/53110832/64949
- https://stackoverflow.com/a/53110852/64949
#### Can you support CocoaPods?
CocoaPods used to be supported, but [it did not work well](https://github.com/sindresorhus/LaunchAtLogin/issues/22) and there was no easy way to fix it, so support was dropped. Even though you mainly use CocoaPods, you can still use Carthage just for this package without any problems.
#### I'm getting a `'SMCopyAllJobDictionaries' was deprecated in OS X 10.10` warning
Apple deprecated that API without providing an alternative. Apple engineers have [stated that it's still the preferred API to use](https://github.com/alexzielenski/StartAtLoginController/issues/12#issuecomment-307525807). I plan to use it as long as it's available. There are workarounds I can implement if Apple ever removes the API, so rest assured, this module will be made to work even then. If you want to see this resolved, submit a [Feedback Assistant](https://feedbackassistant.apple.com) report with [the following text](https://github.com/feedback-assistant/reports/issues/16). There's unfortunately still [no way to suppress warnings in Swift](https://stackoverflow.com/a/32861678/64949).
## Related
- [Defaults](https://github.com/sindresorhus/Defaults) - Swifty and modern UserDefaults
- [Preferences](https://github.com/sindresorhus/Preferences) - Add a preferences window to your macOS app in minutes
- [DockProgress](https://github.com/sindresorhus/DockProgress) - Show progress in your app's Dock icon
- [create-dmg](https://github.com/sindresorhus/create-dmg) - Create a good-looking DMG for your macOS app in seconds
- [More…](https://github.com/search?q=user%3Asindresorhus+language%3Aswift)
## License
MIT © [Sindre Sorhus](https://sindresorhus.com)

View File

@@ -8,6 +8,7 @@ target 'AltStore' do
# Pods for AltStore
pod 'KeychainAccess', '~> 3.2.0'
pod 'Nuke', '~> 7.0'
pod 'AppCenter', '~> 3.1.0'
pod 'AltSign', :path => 'Dependencies/AltSign'
pod 'Roxas', :path => 'Dependencies/Roxas'
@@ -19,7 +20,7 @@ target 'AltServer' do
use_frameworks!
# Pods for AltServer
pod 'STPrivilegedTask'
pod 'STPrivilegedTask', :git => 'https://github.com/rileytestut/STPrivilegedTask.git'
pod 'Sparkle'
end

View File

@@ -10,41 +10,58 @@ PODS:
- AltSign/minizip (0.1)
- AltSign/OpenSSL (0.1)
- AltSign/plist (0.1)
- AppCenter (3.1.0):
- AppCenter/Analytics (= 3.1.0)
- AppCenter/Crashes (= 3.1.0)
- AppCenter/Analytics (3.1.0):
- AppCenter/Core
- AppCenter/Core (3.1.0)
- AppCenter/Crashes (3.1.0):
- AppCenter/Core
- KeychainAccess (3.2.0)
- Nuke (7.6.3)
- Roxas (0.1)
- Sparkle (1.21.3)
- STPrivilegedTask (1.0.1)
- STPrivilegedTask (1.0.7)
DEPENDENCIES:
- AltSign (from `Dependencies/AltSign`)
- AppCenter (~> 3.1.0)
- KeychainAccess (~> 3.2.0)
- Nuke (~> 7.0)
- Roxas (from `Dependencies/Roxas`)
- Sparkle
- STPrivilegedTask
- STPrivilegedTask (from `https://github.com/rileytestut/STPrivilegedTask.git`)
SPEC REPOS:
trunk:
- AppCenter
- KeychainAccess
- Nuke
- Sparkle
- STPrivilegedTask
EXTERNAL SOURCES:
AltSign:
:path: Dependencies/AltSign
Roxas:
:path: Dependencies/Roxas
STPrivilegedTask:
:git: https://github.com/rileytestut/STPrivilegedTask.git
CHECKOUT OPTIONS:
STPrivilegedTask:
:commit: c8dd3e41b23666d4010c86b052a921a8e5a320d0
:git: https://github.com/rileytestut/STPrivilegedTask.git
SPEC CHECKSUMS:
AltSign: c3693fa5a4b6d0ec6bedb74a5d768fe58bd67b87
AppCenter: a1c30c47b7882a04a615ffa5ab26c007326436d8
KeychainAccess: 3b1bf8a77eb4c6ea1ce9404c292e48f948954c6b
Nuke: 44130e95e09463f8773ae4b96b90de1eba6b3350
Roxas: 1990039f843f5dc284918dc82375feb80020ef62
Sparkle: 3f75576db8b0265adef36c43249d747f22d0b708
STPrivilegedTask: 103f97827454e786074640cf89d303be344498c7
STPrivilegedTask: 56c3397238a1ec07720fb877a044898373cd2c68
PODFILE CHECKSUM: 467b50b42949f001543841f547fa6b427e29dc53
PODFILE CHECKSUM: 1317f1da77af3fbb8c90b0d34845d2f0068d488c
COCOAPODS: 1.8.4

Binary file not shown.

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#import <Foundation/Foundation.h>
#import "MSAbstractLog.h"
#import "MSAppCenter.h"
#import "MSAppCenterErrors.h"
#import "MSChannelGroupProtocol.h"
#import "MSChannelProtocol.h"
#import "MSConstants.h"
#import "MSCustomProperties.h"
#import "MSDevice.h"
#import "MSEnable.h"
#import "MSLog.h"
#import "MSLogWithProperties.h"
#import "MSLogger.h"
#import "MSService.h"
#import "MSServiceAbstract.h"
#import "MSWrapperLogger.h"
#import "MSWrapperSdk.h"

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_ABSTRACT_LOG_H
#define MS_ABSTRACT_LOG_H
#import <Foundation/Foundation.h>
@interface MSAbstractLog : NSObject
@end
#endif

View File

@@ -0,0 +1,223 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#import <Foundation/Foundation.h>
#import "MSConstants.h"
@class MSWrapperSdk;
#if !TARGET_OS_TV
@class MSCustomProperties;
#endif
@interface MSAppCenter : NSObject
/**
* Returns the singleton instance of MSAppCenter.
*/
+ (instancetype)sharedInstance;
/**
* Configure the SDK with an application secret.
*
* @param appSecret A unique and secret key used to identify the application.
*
* @discussion This may be called only once per application process lifetime.
*/
+ (void)configureWithAppSecret:(NSString *)appSecret;
/**
* Configure the SDK.
*
* @discussion This may be called only once per application process lifetime.
*/
+ (void)configure;
/**
* Configure the SDK with an application secret and an array of services to start.
*
* @param appSecret A unique and secret key used to identify the application.
* @param services Array of services to start.
*
* @discussion This may be called only once per application process lifetime.
*/
+ (void)start:(NSString *)appSecret withServices:(NSArray<Class> *)services;
/**
* Start the SDK with an array of services.
*
* @param services Array of services to start.
*
* @discussion This may be called only once per application process lifetime.
*/
+ (void)startWithServices:(NSArray<Class> *)services;
/**
* Start a service.
*
* @param service A service to start.
*
* @discussion This may be called only once per service per application process lifetime.
*/
+ (void)startService:(Class)service;
/**
* Configure the SDK with an array of services to start from a library. This will not start the service at application level, it will enable
* the service only for the library.
*
* @param services Array of services to start.
*/
+ (void)startFromLibraryWithServices:(NSArray<Class> *)services;
/**
* Check whether the SDK has already been configured or not.
*
* @return YES if configured, NO otherwise.
*/
+ (BOOL)isConfigured;
/**
* Check whether app is running in App Center Test Cloud.
*
* @return true if running in App Center Test Cloud, false otherwise.
*/
+ (BOOL)isRunningInAppCenterTestCloud;
/**
* Change the base URL (schema + authority + port only) used to communicate with the backend.
*
* @param logUrl Base URL to use for backend communication.
*/
+ (void)setLogUrl:(NSString *)logUrl;
/**
* Enable or disable the SDK as a whole. In addition to AppCenter resources, it will also enable or disable all registered services.
* The state is persisted in the device's storage across application launches.
*
* @param isEnabled YES to enable, NO to disable.
*
* @see isEnabled
*/
+ (void)setEnabled:(BOOL)isEnabled;
/**
* Check whether the SDK is enabled or not as a whole.
*
* @return YES if enabled, NO otherwise.
*
* @see setEnabled:
*/
+ (BOOL)isEnabled;
/**
* Get log level.
*
* @return Log level.
*/
+ (MSLogLevel)logLevel;
/**
* Set log level.
*
* @param logLevel The log level.
*/
+ (void)setLogLevel:(MSLogLevel)logLevel;
/**
* Set log level handler.
*
* @param logHandler Handler.
*/
+ (void)setLogHandler:(MSLogHandler)logHandler;
/**
* Set wrapper SDK information to use when building device properties. This is intended in case you are building a SDK that uses the App
* Center SDK under the hood, e.g. our Xamarin SDK or ReactNative SDk.
*
* @param wrapperSdk Wrapper SDK information.
*/
+ (void)setWrapperSdk:(MSWrapperSdk *)wrapperSdk;
#if !TARGET_OS_TV
/**
* Set the custom properties.
*
* @param customProperties Custom properties object.
*/
+ (void)setCustomProperties:(MSCustomProperties *)customProperties;
#endif
/**
* Check whether the application delegate forwarder is enabled or not.
*
* @return YES if enabled, NO otherwise.
*
* @discussion The application delegate forwarder forwards messages that target your application delegate methods via swizzling to the SDK.
* It simplifies the SDK integration but may not be suitable to any situations. For
* instance it should be disabled if you or one of your third party SDK is doing message forwarding on the application delegate. Message
* forwarding usually implies the implementation of @see NSObject#forwardingTargetForSelector: or @see NSObject#forwardInvocation: methods.
* To disable the application delegate forwarder just add the `AppCenterAppDelegateForwarderEnabled` tag to your Info .plist file and set it
* to `0`. Then you will have to forward any application delegate needed by the SDK manually.
*/
+ (BOOL)isAppDelegateForwarderEnabled;
/**
* Get unique installation identifier.
*
* @return Unique installation identifier.
*/
+ (NSUUID *)installId;
/**
* Detect if a debugger is attached to the app process. This is only invoked once on app startup and can not detect
* if the debugger is being attached during runtime!
*
* @return BOOL if the debugger is attached.
*/
+ (BOOL)isDebuggerAttached;
/**
* Get the current version of AppCenter SDK.
*
* @return The current version of AppCenter SDK.
*/
+ (NSString *)sdkVersion;
/**
* Set the maximum size of the internal storage. This method must be called before App Center is started. This method is only intended for
* applications.
*
* @param sizeInBytes Maximum size of the internal storage in bytes. This will be rounded up to the nearest multiple of a SQLite page size
* (default is 4096 bytes). Values below 20,480 bytes (20 KiB) will be ignored.
*
* @param completionHandler Callback that is invoked when the database size has been set. The `BOOL` parameter is `YES` if changing the size
* is successful, and `NO` otherwise. This parameter can be null.
*
* @discussion This only sets the maximum size of the database, but App Center modules might store additional data.
* The value passed to this method is not persisted on disk. The default maximum database size is 10485760 bytes (10 MiB).
*/
+ (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(void (^)(BOOL))completionHandler;
/**
* Set the user identifier.
*
* @param userId User identifier.
*
* @discussion Set the user identifier for logs sent for the default target token when the secret passed in @c
* MSAppCenter:start:withServices: contains "target={targetToken}".
*
* For App Center backend the user identifier maximum length is 256 characters.
*
* AppCenter must be configured or started before this API can be used.
*/
+ (void)setUserId:(NSString *)userId;
/**
* Set country code to use when building device properties.
*
* @param countryCode The two-letter ISO country code. @see https://www.iso.org/obp/ui/#search for more information.
*/
+ (void)setCountryCode:(NSString *)countryCode;
@end

View File

@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_APP_CENTER_ERRORS_H
#define MS_APP_CENTER_ERRORS_H
#import <Foundation/Foundation.h>
#define MS_APP_CENTER_BASE_DOMAIN @"com.Microsoft.AppCenter."
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Domain
static NSString *const kMSACErrorDomain = MS_APP_CENTER_BASE_DOMAIN @"ErrorDomain";
#pragma mark - General
// Error codes.
NS_ENUM(NSInteger){MSACLogInvalidContainerErrorCode = 1, MSACCanceledErrorCode = 2, MSACDisabledErrorCode = 3};
// Error descriptions.
static NSString const *kMSACLogInvalidContainerErrorDesc = @"Invalid log container.";
static NSString const *kMSACCanceledErrorDesc = @"The operation was canceled.";
static NSString const *kMSACDisabledErrorDesc = @"The service is disabled.";
#pragma mark - Connection
// Error codes.
NS_ENUM(NSInteger){MSACConnectionPausedErrorCode = 100, MSACConnectionHttpErrorCode = 101};
// Error descriptions.
static NSString const *kMSACConnectionHttpErrorDesc = @"An HTTP error occured.";
static NSString const *kMSACConnectionPausedErrorDesc = @"Canceled, connection paused with log deletion.";
// Error user info keys.
static NSString const *kMSACConnectionHttpCodeErrorKey = @"MSACConnectionHttpCode";
NS_ASSUME_NONNULL_END
#endif

View File

@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_CHANNEL_GROUP_PROTOCOL_H
#define MS_CHANNEL_GROUP_PROTOCOL_H
#import <Foundation/Foundation.h>
#import "MSChannelProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@class MSChannelUnitConfiguration;
@protocol MSIngestionProtocol;
@protocol MSChannelUnitProtocol;
/**
* `MSChannelGroupProtocol` represents a kind of channel that contains constituent MSChannelUnit objects. When an operation from the
* `MSChannelProtocol` is performed on the group, that operation should be propagated to its constituent MSChannelUnit objects.
*/
@protocol MSChannelGroupProtocol <MSChannelProtocol>
/**
* Initialize a channel unit with the given configuration.
*
* @param configuration channel configuration.
*
* @return The added `MSChannelUnitProtocol`. Use this object to enqueue logs.
*/
- (id<MSChannelUnitProtocol>)addChannelUnitWithConfiguration:(MSChannelUnitConfiguration *)configuration;
/**
* Initialize a channel unit with the given configuration.
*
* @param configuration channel configuration.
* @param ingestion The alternative ingestion object
*
* @return The added `MSChannelUnitProtocol`. Use this object to enqueue logs.
*/
- (id<MSChannelUnitProtocol>)addChannelUnitWithConfiguration:(MSChannelUnitConfiguration *)configuration
withIngestion:(nullable id<MSIngestionProtocol>)ingestion;
/**
* Change the base URL (schema + authority + port only) used to communicate with the backend.
*
* @param logUrl base URL to use for backend communication.
*/
- (void)setLogUrl:(NSString *)logUrl;
/**
* Set the app secret.
*
* @param appSecret The app secret.
*/
- (void)setAppSecret:(NSString *)appSecret;
/**
* Set the maximum size of the internal storage. This method must be called before App Center is started.
*
* @discussion The default maximum database size is 10485760 bytes (10 MiB).
*
* @param sizeInBytes Maximum size of the internal storage in bytes. This will be rounded up to the nearest multiple of a SQLite page size
* (default is 4096 bytes). Values below 24576 bytes (24 KiB) will be ignored.
* @param completionHandler Callback that is invoked when the database size has been set. The `BOOL` parameter is `YES` if changing the size
* is successful, and `NO` otherwise.
*/
- (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(nullable void (^)(BOOL))completionHandler;
/**
* Return a channel unit instance for the given groupId.
*
* @param groupId The group ID for a channel unit.
*
* @return A channel unit instance or `nil`.
*/
- (id<MSChannelUnitProtocol>)channelUnitForGroupId:(NSString *)groupId;
@end
NS_ASSUME_NONNULL_END
#endif

View File

@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_CHANNEL_PROTOCOL_H
#define MS_CHANNEL_PROTOCOL_H
#import <Foundation/Foundation.h>
#import "MSEnable.h"
NS_ASSUME_NONNULL_BEGIN
@protocol MSChannelDelegate;
/**
* `MSChannelProtocol` contains the essential operations of a channel. Channels are broadly responsible for enqueuing logs to be sent to the
* backend and/or stored on disk.
*/
@protocol MSChannelProtocol <NSObject, MSEnable>
/**
* Add delegate.
*
* @param delegate delegate.
*/
- (void)addDelegate:(id<MSChannelDelegate>)delegate;
/**
* Remove delegate.
*
* @param delegate delegate.
*/
- (void)removeDelegate:(id<MSChannelDelegate>)delegate;
/**
* Pause operations, logs will be stored but not sent.
*
* @param identifyingObject Object used to identify the pause request.
*
* @discussion A paused channel doesn't forward logs to the ingestion. The identifying object used to pause the channel can be any unique
* object. The same identifying object must be used to call resume. For simplicity if the caller is the one owning the channel then @c self
* can be used as identifying object.
*
* @see resumeWithIdentifyingObject:
*/
- (void)pauseWithIdentifyingObject:(id<NSObject>)identifyingObject;
/**
* Resume operations, logs can be sent again.
*
* @param identifyingObject Object used to passed to the pause method.
*
* @discussion The channel only resume when all the outstanding identifying objects have been resumed.
*
* @see pauseWithIdentifyingObject:
*/
- (void)resumeWithIdentifyingObject:(id<NSObject>)identifyingObject;
@end
NS_ASSUME_NONNULL_END
#endif

View File

@@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#import <Foundation/Foundation.h>
/**
* Log Levels
*/
typedef NS_ENUM(NSUInteger, MSLogLevel) {
/**
* Logging will be very chatty
*/
MSLogLevelVerbose = 2,
/**
* Debug information will be logged
*/
MSLogLevelDebug = 3,
/**
* Information will be logged
*/
MSLogLevelInfo = 4,
/**
* Errors and warnings will be logged
*/
MSLogLevelWarning = 5,
/**
* Errors will be logged
*/
MSLogLevelError = 6,
/**
* Only critical errors will be logged
*/
MSLogLevelAssert = 7,
/**
* Logging is disabled
*/
MSLogLevelNone = 99
};
typedef NSString * (^MSLogMessageProvider)(void);
typedef void (^MSLogHandler)(MSLogMessageProvider messageProvider, MSLogLevel logLevel, NSString *tag, const char *file,
const char *function, uint line);
/**
* Channel priorities, check the kMSPriorityCount if you add a new value.
* The order matters here! Values NEED to range from low priority to high priority.
*/
typedef NS_ENUM(NSInteger, MSPriority) { MSPriorityBackground, MSPriorityDefault, MSPriorityHigh };
static short const kMSPriorityCount = MSPriorityHigh + 1;
/**
* The priority by which the modules are initialized.
* MSPriorityMax is reserved for only 1 module and this needs to be Crashes.
* Crashes needs to be initialized first to catch crashes in our other SDK Modules (which will hopefully never happen) and to avoid losing
* any log at crash time.
*/
typedef NS_ENUM(NSInteger, MSInitializationPriority) {
MSInitializationPriorityDefault = 500,
MSInitializationPriorityHigh = 750,
MSInitializationPriorityMax = 999
};
/**
* Enum with the different HTTP status codes.
*/
typedef NS_ENUM(NSInteger, MSHTTPCodesNo) {
// Invalid
MSHTTPCodesNo0XXInvalidUnknown = 0,
// Informational
MSHTTPCodesNo1XXInformationalUnknown = 1,
MSHTTPCodesNo100Continue = 100,
MSHTTPCodesNo101SwitchingProtocols = 101,
MSHTTPCodesNo102Processing = 102,
// Success
MSHTTPCodesNo2XXSuccessUnknown = 2,
MSHTTPCodesNo200OK = 200,
MSHTTPCodesNo201Created = 201,
MSHTTPCodesNo202Accepted = 202,
MSHTTPCodesNo203NonAuthoritativeInformation = 203,
MSHTTPCodesNo204NoContent = 204,
MSHTTPCodesNo205ResetContent = 205,
MSHTTPCodesNo206PartialContent = 206,
MSHTTPCodesNo207MultiStatus = 207,
MSHTTPCodesNo208AlreadyReported = 208,
MSHTTPCodesNo209IMUsed = 209,
// Redirection
MSHTTPCodesNo3XXSuccessUnknown = 3,
MSHTTPCodesNo300MultipleChoices = 300,
MSHTTPCodesNo301MovedPermanently = 301,
MSHTTPCodesNo302Found = 302,
MSHTTPCodesNo303SeeOther = 303,
MSHTTPCodesNo304NotModified = 304,
MSHTTPCodesNo305UseProxy = 305,
MSHTTPCodesNo306SwitchProxy = 306,
MSHTTPCodesNo307TemporaryRedirect = 307,
MSHTTPCodesNo308PermanentRedirect = 308,
// Client error
MSHTTPCodesNo4XXSuccessUnknown = 4,
MSHTTPCodesNo400BadRequest = 400,
MSHTTPCodesNo401Unauthorised = 401,
MSHTTPCodesNo402PaymentRequired = 402,
MSHTTPCodesNo403Forbidden = 403,
MSHTTPCodesNo404NotFound = 404,
MSHTTPCodesNo405MethodNotAllowed = 405,
MSHTTPCodesNo406NotAcceptable = 406,
MSHTTPCodesNo407ProxyAuthenticationRequired = 407,
MSHTTPCodesNo408RequestTimeout = 408,
MSHTTPCodesNo409Conflict = 409,
MSHTTPCodesNo410Gone = 410,
MSHTTPCodesNo411LengthRequired = 411,
MSHTTPCodesNo412PreconditionFailed = 412,
MSHTTPCodesNo413RequestEntityTooLarge = 413,
MSHTTPCodesNo414RequestURITooLong = 414,
MSHTTPCodesNo415UnsupportedMediaType = 415,
MSHTTPCodesNo416RequestedRangeNotSatisfiable = 416,
MSHTTPCodesNo417ExpectationFailed = 417,
MSHTTPCodesNo418IamATeapot = 418,
MSHTTPCodesNo419AuthenticationTimeout = 419,
MSHTTPCodesNo420MethodFailureSpringFramework = 420,
MSHTTPCodesNo420EnhanceYourCalmTwitter = 4200,
MSHTTPCodesNo422UnprocessableEntity = 422,
MSHTTPCodesNo423Locked = 423,
MSHTTPCodesNo424FailedDependency = 424,
MSHTTPCodesNo424MethodFailureWebDaw = 4240,
MSHTTPCodesNo425UnorderedCollection = 425,
MSHTTPCodesNo426UpgradeRequired = 426,
MSHTTPCodesNo428PreconditionRequired = 428,
MSHTTPCodesNo429TooManyRequests = 429,
MSHTTPCodesNo431RequestHeaderFieldsTooLarge = 431,
MSHTTPCodesNo444NoResponseNginx = 444,
MSHTTPCodesNo449RetryWithMicrosoft = 449,
MSHTTPCodesNo450BlockedByWindowsParentalControls = 450,
MSHTTPCodesNo451RedirectMicrosoft = 451,
MSHTTPCodesNo451UnavailableForLegalReasons = 4510,
MSHTTPCodesNo494RequestHeaderTooLargeNginx = 494,
MSHTTPCodesNo495CertErrorNginx = 495,
MSHTTPCodesNo496NoCertNginx = 496,
MSHTTPCodesNo497HTTPToHTTPSNginx = 497,
MSHTTPCodesNo499ClientClosedRequestNginx = 499,
// Server error
MSHTTPCodesNo5XXSuccessUnknown = 5,
MSHTTPCodesNo500InternalServerError = 500,
MSHTTPCodesNo501NotImplemented = 501,
MSHTTPCodesNo502BadGateway = 502,
MSHTTPCodesNo503ServiceUnavailable = 503,
MSHTTPCodesNo504GatewayTimeout = 504,
MSHTTPCodesNo505HTTPVersionNotSupported = 505,
MSHTTPCodesNo506VariantAlsoNegotiates = 506,
MSHTTPCodesNo507InsufficientStorage = 507,
MSHTTPCodesNo508LoopDetected = 508,
MSHTTPCodesNo509BandwidthLimitExceeded = 509,
MSHTTPCodesNo510NotExtended = 510,
MSHTTPCodesNo511NetworkAuthenticationRequired = 511,
MSHTTPCodesNo522ConnectionTimedOut = 522,
MSHTTPCodesNo598NetworkReadTimeoutErrorUnknown = 598,
MSHTTPCodesNo599NetworkConnectTimeoutErrorUnknown = 599
};

View File

@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_CUSTOM_PROPERTIES_H
#define MS_CUSTOM_PROPERTIES_H
#import <Foundation/Foundation.h>
/**
* Custom properties builder.
* Collects multiple properties to send in one log.
*/
@interface MSCustomProperties : NSObject
/**
* Set the specified property value with the specified key.
* If the properties previously contained a property for the key, the old value is replaced.
*
* @param key Key with which the specified value is to be set.
* @param value Value to be set with the specified key.
*
* @return This instance.
*/
- (instancetype)setString:(NSString *)value forKey:(NSString *)key;
/**
* Set the specified property value with the specified key.
* If the properties previously contained a property for the key, the old value is replaced.
*
* @param key Key with which the specified value is to be set.
* @param value Value to be set with the specified key.
*
* @return This instance.
*/
- (instancetype)setNumber:(NSNumber *)value forKey:(NSString *)key;
/**
* Set the specified property value with the specified key.
* If the properties previously contained a property for the key, the old value is replaced.
*
* @param key Key with which the specified value is to be set.
* @param value Value to be set with the specified key.
*
* @return This instance.
*/
- (instancetype)setBool:(BOOL)value forKey:(NSString *)key;
/**
* Set the specified property value with the specified key.
* If the properties previously contained a property for the key, the old value is replaced.
*
* @param key Key with which the specified value is to be set.
* @param value Value to be set with the specified key.
*
* @return This instance.
*/
- (instancetype)setDate:(NSDate *)value forKey:(NSString *)key;
/**
* Clear the property for the specified key.
*
* @param key Key whose mapping is to be cleared.
*
* @return This instance.
*/
- (instancetype)clearPropertyForKey:(NSString *)key;
@end
#endif

View File

@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_DEVICE_H
#define MS_DEVICE_H
#import <Foundation/Foundation.h>
#import "MSWrapperSdk.h"
@interface MSDevice : MSWrapperSdk
/*
* Name of the SDK. Consists of the name of the SDK and the platform, e.g. "appcenter.ios", "appcenter.android"
*/
@property(nonatomic, copy, readonly) NSString *sdkName;
/*
* Version of the SDK in semver format, e.g. "1.2.0" or "0.12.3-alpha.1".
*/
@property(nonatomic, copy, readonly) NSString *sdkVersion;
/*
* Device model (example: iPad2,3).
*/
@property(nonatomic, copy, readonly) NSString *model;
/*
* Device manufacturer (example: HTC).
*/
@property(nonatomic, copy, readonly) NSString *oemName;
/*
* OS name (example: iOS).
*/
@property(nonatomic, copy, readonly) NSString *osName;
/*
* OS version (example: 9.3.0).
*/
@property(nonatomic, copy, readonly) NSString *osVersion;
/*
* OS build code (example: LMY47X). [optional]
*/
@property(nonatomic, copy, readonly) NSString *osBuild;
/*
* API level when applicable like in Android (example: 15). [optional]
*/
@property(nonatomic, copy, readonly) NSNumber *osApiLevel;
/*
* Language code (example: en_US).
*/
@property(nonatomic, copy, readonly) NSString *locale;
/*
* The offset in minutes from UTC for the device time zone, including daylight savings time.
*/
@property(nonatomic, readonly, strong) NSNumber *timeZoneOffset;
/*
* Screen size of the device in pixels (example: 640x480).
*/
@property(nonatomic, copy, readonly) NSString *screenSize;
/*
* Application version name, e.g. 1.1.0
*/
@property(nonatomic, copy, readonly) NSString *appVersion;
/*
* Carrier name (for mobile devices). [optional]
*/
@property(nonatomic, copy, readonly) NSString *carrierName;
/*
* Carrier country code (for mobile devices). [optional]
*/
@property(nonatomic, copy, readonly) NSString *carrierCountry;
/*
* The app's build number, e.g. 42.
*/
@property(nonatomic, copy, readonly) NSString *appBuild;
/*
* The bundle identifier, package identifier, or namespace, depending on what the individual plattforms use, .e.g com.microsoft.example.
* [optional]
*/
@property(nonatomic, copy, readonly) NSString *appNamespace;
@end
#endif

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#ifndef MS_ENABLE_H
#define MS_ENABLE_H
#import <Foundation/Foundation.h>
/**
* Protocol to define an instance that can be enabled/disabled.
*/
@protocol MSEnable <NSObject>
@required
/**
* Enable/disable this instance and delete data on disabled state.
*
* @param isEnabled A boolean value set to YES to enable the instance or NO to disable it.
* @param deleteData A boolean value set to YES to delete data or NO to keep it.
*/
- (void)setEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deleteData;
@end
#endif

Some files were not shown because too many files have changed in this diff Show More