Merge pull request #100 from rileytestut/develop

AltStore 1.2
This commit is contained in:
Riley Testut
2020-02-12 12:03:54 -08:00
committed by GitHub
781 changed files with 14034 additions and 2528 deletions

View File

@@ -7,3 +7,6 @@
//
#import "NSError+ALTServerError.h"
#import "CFNotificationName+AltStore.h"
extern uint16_t ALTDeviceListeningSocket;

11
AltKit/AltKit.m Normal file
View File

@@ -0,0 +1,11 @@
//
// AltKit.m
// AltKit
//
// Created by Riley Testut on 1/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
uint16_t ALTDeviceListeningSocket = 28151;

View File

@@ -14,9 +14,11 @@ public extension Bundle
{
public static let deviceID = "ALTDeviceID"
public static let serverID = "ALTServerID"
public static let certificateID = "ALTCertificateID"
public static let appGroups = "ALTAppGroups"
public static let urlTypes = "CFBundleURLTypes"
public static let exportedUTIs = "UTExportedTypeDeclarations"
}
}
@@ -26,4 +28,9 @@ public extension Bundle
let infoPlistURL = self.bundleURL.appendingPathComponent("Info.plist")
return infoPlistURL
}
var certificateURL: URL {
let infoPlistURL = self.bundleURL.appendingPathComponent("ALTCertificate.p12")
return infoPlistURL
}
}

View File

@@ -0,0 +1,17 @@
//
// CFNotificationName+AltStore.h
// AltKit
//
// Created by Riley Testut on 1/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
extern CFNotificationName const ALTWiredServerConnectionAvailableRequest NS_SWIFT_NAME(wiredServerConnectionAvailableRequest);
extern CFNotificationName const ALTWiredServerConnectionAvailableResponse NS_SWIFT_NAME(wiredServerConnectionAvailableResponse);
extern CFNotificationName const ALTWiredServerConnectionStartRequest NS_SWIFT_NAME(wiredServerConnectionStartRequest);
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,13 @@
//
// CFNotificationName+AltStore.m
// AltKit
//
// Created by Riley Testut on 1/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import "CFNotificationName+AltStore.h"
CFNotificationName const ALTWiredServerConnectionAvailableRequest = CFSTR("io.altstore.Request.WiredServerConnectionAvailable");
CFNotificationName const ALTWiredServerConnectionAvailableResponse = CFSTR("io.altstore.Response.WiredServerConnectionAvailable");
CFNotificationName const ALTWiredServerConnectionStartRequest = CFSTR("io.altstore.Request.WiredServerConnectionStart");

View File

@@ -27,6 +27,12 @@ typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError)
ALTServerErrorInstallationFailed = 8,
ALTServerErrorMaximumFreeAppLimitReached = 9,
ALTServerErrorUnsupportediOSVersion = 10,
ALTServerErrorUnknownRequest = 11,
ALTServerErrorUnknownResponse = 12,
ALTServerErrorInvalidAnisetteData = 13,
ALTServerErrorPluginNotFound = 14
};
NS_ASSUME_NONNULL_BEGIN

View File

@@ -61,6 +61,18 @@ NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServ
case ALTServerErrorUnsupportediOSVersion:
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
case ALTServerErrorUnknownRequest:
return NSLocalizedString(@"AltServer does not support this request.", @"");
case ALTServerErrorUnknownResponse:
return NSLocalizedString(@"Received an unknown response from AltServer.", @"");
case ALTServerErrorInvalidAnisetteData:
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.", @"");
}
}

View File

@@ -7,22 +7,201 @@
//
import Foundation
import AltSign
public let ALTServerServiceType = "_altserver._tcp"
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {}
protocol ServerMessage: Codable
protocol ServerMessageProtocol: Codable
{
var version: Int { get }
var identifier: String { get }
}
public struct PrepareAppRequest: ServerMessage
public enum ServerRequest: Decodable
{
case anisetteData(AnisetteDataRequest)
case prepareApp(PrepareAppRequest)
case beginInstallation(BeginInstallationRequest)
case unknown(identifier: String, version: Int)
var identifier: String {
switch self
{
case .anisetteData(let request): return request.identifier
case .prepareApp(let request): return request.identifier
case .beginInstallation(let request): return request.identifier
case .unknown(let identifier, _): return identifier
}
}
var version: Int {
switch self
{
case .anisetteData(let request): return request.version
case .prepareApp(let request): return request.version
case .beginInstallation(let request): return request.version
case .unknown(_, let version): return version
}
}
private enum CodingKeys: String, CodingKey
{
case identifier
case version
}
public init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(Int.self, forKey: .version)
let identifier = try container.decode(String.self, forKey: .identifier)
switch identifier
{
case "AnisetteDataRequest":
let request = try AnisetteDataRequest(from: decoder)
self = .anisetteData(request)
case "PrepareAppRequest":
let request = try PrepareAppRequest(from: decoder)
self = .prepareApp(request)
case "BeginInstallationRequest":
let request = try BeginInstallationRequest(from: decoder)
self = .beginInstallation(request)
default:
self = .unknown(identifier: identifier, version: version)
}
}
}
public enum ServerResponse: Decodable
{
case anisetteData(AnisetteDataResponse)
case installationProgress(InstallationProgressResponse)
case error(ErrorResponse)
case unknown(identifier: String, version: Int)
var identifier: String {
switch self
{
case .anisetteData(let response): return response.identifier
case .installationProgress(let response): return response.identifier
case .error(let response): return response.identifier
case .unknown(let identifier, _): return identifier
}
}
var version: Int {
switch self
{
case .anisetteData(let response): return response.version
case .installationProgress(let response): return response.version
case .error(let response): return response.version
case .unknown(_, let version): return version
}
}
private enum CodingKeys: String, CodingKey
{
case identifier
case version
}
public init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(Int.self, forKey: .version)
let identifier = try container.decode(String.self, forKey: .identifier)
switch identifier
{
case "AnisetteDataResponse":
let response = try AnisetteDataResponse(from: decoder)
self = .anisetteData(response)
case "InstallationProgressResponse":
let response = try InstallationProgressResponse(from: decoder)
self = .installationProgress(response)
case "ErrorResponse":
let response = try ErrorResponse(from: decoder)
self = .error(response)
default:
self = .unknown(identifier: identifier, version: version)
}
}
}
public struct AnisetteDataRequest: ServerMessageProtocol
{
public var version = 1
public var identifier = "PrepareApp"
public var identifier = "AnisetteDataRequest"
public init()
{
}
}
public struct AnisetteDataResponse: ServerMessageProtocol
{
public var version = 1
public var identifier = "AnisetteDataResponse"
public var anisetteData: ALTAnisetteData
private enum CodingKeys: String, CodingKey
{
case identifier
case version
case anisetteData
}
public init(anisetteData: ALTAnisetteData)
{
self.anisetteData = anisetteData
}
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)
let json = try container.decode([String: String].self, forKey: .anisetteData)
if let anisetteData = ALTAnisetteData(json: json)
{
self.anisetteData = anisetteData
}
else
{
throw DecodingError.dataCorruptedError(forKey: CodingKeys.anisetteData, in: container, debugDescription: "Couuld not parse anisette data from JSON")
}
}
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)
let json = self.anisetteData.json()
try container.encode(json, forKey: .anisetteData)
}
}
public struct PrepareAppRequest: ServerMessageProtocol
{
public var version = 1
public var identifier = "PrepareAppRequest"
public var udid: String
public var contentSize: Int
@@ -34,37 +213,41 @@ public struct PrepareAppRequest: ServerMessage
}
}
public struct BeginInstallationRequest: ServerMessage
public struct BeginInstallationRequest: ServerMessageProtocol
{
public var version = 1
public var identifier = "BeginInstallation"
public var identifier = "BeginInstallationRequest"
public init()
{
}
}
public struct ServerResponse: ServerMessage
public struct ErrorResponse: ServerMessageProtocol
{
public var version = 1
public var identifier = "ServerResponse"
public var identifier = "ErrorResponse"
public var error: ALTServerError {
return ALTServerError(self.errorCode)
}
private var errorCode: ALTServerError.Code
public init(error: ALTServerError)
{
self.errorCode = error.code
}
}
public struct InstallationProgressResponse: ServerMessageProtocol
{
public var version = 1
public var identifier = "InstallationProgressResponse"
public var progress: Double
public var error: ALTServerError? {
get {
guard let code = self.errorCode else { return nil }
return ALTServerError(code)
}
set {
self.errorCode = newValue?.code
}
}
private var errorCode: ALTServerError.Code?
public init(progress: Double, error: ALTServerError?)
public init(progress: Double)
{
self.progress = progress
self.error = error
}
}

View File

@@ -0,0 +1,19 @@
//
// ALTPluginService.h
// AltPlugin
//
// Created by Riley Testut on 11/14/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ALTPluginService : NSObject
+ (instancetype)sharedService;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,99 @@
//
// ALTPluginService.m
// AltPlugin
//
// Created by Riley Testut on 11/14/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
#import "ALTPluginService.h"
#import <dlfcn.h>
#import "ALTAnisetteData.h"
@import AppKit;
@interface AKAppleIDSession : NSObject
- (id)appleIDHeadersForRequest:(id)arg1;
@end
@interface AKDevice
+ (AKDevice *)currentDevice;
- (NSString *)uniqueDeviceIdentifier;
- (NSString *)serialNumber;
- (NSString *)serverFriendlyDescription;
@end
@interface ALTPluginService ()
@property (nonatomic, readonly) NSISO8601DateFormatter *dateFormatter;
@end
@implementation ALTPluginService
+ (instancetype)sharedService
{
static ALTPluginService *_service = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_service = [[self alloc] init];
});
return _service;
}
- (instancetype)init
{
self = [super init];
if (self)
{
_dateFormatter = [[NSISO8601DateFormatter alloc] init];
}
return self;
}
+ (void)initialize
{
[[ALTPluginService sharedService] start];
}
- (void)start
{
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"com.rileytestut.AltServer.FetchAnisetteData" object:nil];
}
- (void)receiveNotification:(NSNotification *)notification
{
NSString *requestUUID = notification.userInfo[@"requestUUID"];
NSMutableURLRequest* req = [[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:@"https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA"]];
[req setHTTPMethod:@"POST"];
AKAppleIDSession *session = [[NSClassFromString(@"AKAppleIDSession") alloc] initWithIdentifier:@"com.apple.gs.xcode.auth"];
NSDictionary *headers = [session appleIDHeadersForRequest:req];
AKDevice *device = [NSClassFromString(@"AKDevice") currentDevice];
NSDate *date = [self.dateFormatter dateFromString:headers[@"X-Apple-I-Client-Time"]];
ALTAnisetteData *anisetteData = [[NSClassFromString(@"ALTAnisetteData") alloc] initWithMachineID:headers[@"X-Apple-I-MD-M"]
oneTimePassword:headers[@"X-Apple-I-MD"]
localUserID:headers[@"X-Apple-I-MD-LU"]
routingInfo:[headers[@"X-Apple-I-MD-RINFO"] longLongValue]
deviceUniqueIdentifier:device.uniqueDeviceIdentifier
deviceSerialNumber:device.serialNumber
deviceDescription:device.serverFriendlyDescription
date:date
locale:[NSLocale currentLocale]
timeZone:[NSTimeZone localTimeZone]];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:anisetteData requiringSecureCoding:YES error:nil];
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.rileytestut.AltServer.AnisetteDataResponse" object:nil userInfo:@{@"requestUUID": requestUUID, @"anisetteData": data} deliverImmediately:YES];
}
@end

62
AltPlugin/Info.plist Normal file
View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019 Riley Testut. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string>ALTPluginService</string>
<key>Supported10.14PluginCompatibilityUUIDs</key>
<array>
<string># UUIDs for versions from 10.12 to 99.99.99</string>
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
</array>
<key>Supported10.15PluginCompatibilityUUIDs</key>
<array>
<string># UUIDs for versions from 10.12 to 99.99.99</string>
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
</array>
</dict>
</plist>

Binary file not shown.

View File

@@ -3,3 +3,6 @@
//
#import "ALTDeviceManager.h"
#import "ALTWiredConnection.h"
#import "ALTNotificationConnection.h"
#import "AltKit.h"

View File

@@ -0,0 +1,79 @@
//
// AnisetteDataManager.swift
// AltServer
//
// Created by Riley Testut on 11/16/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import AltKit
class AnisetteDataManager: NSObject
{
static let shared = AnisetteDataManager()
private var anisetteDataCompletionHandlers: [String: (Result<ALTAnisetteData, Error>) -> Void] = [:]
private var anisetteDataTimers: [String: Timer] = [:]
private override init()
{
super.init()
DistributedNotificationCenter.default().addObserver(self, selector: #selector(AnisetteDataManager.handleAnisetteDataResponse(_:)), name: Notification.Name("com.rileytestut.AltServer.AnisetteDataResponse"), object: nil)
}
func requestAnisetteData(_ completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
{
let requestUUID = UUID().uuidString
self.anisetteDataCompletionHandlers[requestUUID] = completion
let timer = Timer(timeInterval: 1.0, repeats: false) { (timer) in
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.pluginNotFound)))
}
self.anisetteDataTimers[requestUUID] = timer
RunLoop.main.add(timer, forMode: .default)
DistributedNotificationCenter.default().postNotificationName(Notification.Name("com.rileytestut.AltServer.FetchAnisetteData"), object: nil, userInfo: ["requestUUID": requestUUID], options: .deliverImmediately)
}
}
private extension AnisetteDataManager
{
@objc func handleAnisetteDataResponse(_ notification: Notification)
{
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
if
let archivedAnisetteData = userInfo["anisetteData"] as? Data,
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData)
{
if let range = anisetteData.deviceDescription.lowercased().range(of: "(com.apple.mail")
{
var adjustedDescription = anisetteData.deviceDescription[..<range.lowerBound]
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"
anisetteData.deviceDescription = String(adjustedDescription)
}
self.finishRequest(forUUID: requestUUID, result: .success(anisetteData))
}
else
{
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.invalidAnisetteData)))
}
}
func finishRequest(forUUID requestUUID: String, result: Result<ALTAnisetteData, Error>)
{
let completionHandler = self.anisetteDataCompletionHandlers[requestUUID]
self.anisetteDataCompletionHandlers[requestUUID] = nil
let timer = self.anisetteDataTimers[requestUUID]
self.anisetteDataTimers[requestUUID] = nil
timer?.invalidate()
completionHandler?(result)
}
}

View File

@@ -12,6 +12,25 @@ import UserNotifications
import AltSign
import LaunchAtLogin
import STPrivilegedTask
enum PluginError: LocalizedError
{
case installationScriptNotFound
case failedToRun(Int)
case scriptError(String)
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
}
}
}
private let pluginURL = URL(fileURLWithPath: "/Library/Mail/Bundles/AltPlugin.mailbundle")
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
@@ -25,16 +44,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet private var appMenu: NSMenu!
@IBOutlet private var connectedDevicesMenu: NSMenu!
@IBOutlet private var launchAtLoginMenuItem: NSMenuItem!
@IBOutlet private var installMailPluginMenuItem: NSMenuItem!
private weak var authenticationAppleIDTextField: NSTextField?
private weak var authenticationPasswordTextField: NSSecureTextField?
private var isMailPluginInstalled: Bool {
let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path)
return isMailPluginInstalled
}
func applicationDidFinishLaunching(_ aNotification: Notification)
{
UserDefaults.standard.registerDefaults()
UNUserNotificationCenter.current().delegate = self
ConnectionManager.shared.start()
ALTDeviceManager.shared.start()
let item = NSStatusBar.system.statusItem(withLength: -1)
guard let button = item.button else { return }
@@ -47,16 +74,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.connectedDevicesMenu.delegate = self
if !UserDefaults.standard.didPresentInitialNotification
{
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("AltServer Running", comment: "")
content.body = NSLocalizedString("AltServer runs in the background as a menu bar app listening for AltStore.", comment: "")
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (success, error) in
guard success else { return }
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
UserDefaults.standard.didPresentInitialNotification = true
if !UserDefaults.standard.didPresentInitialNotification
{
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("AltServer Running", comment: "")
content.body = NSLocalizedString("AltServer runs in the background as a menu bar app listening for AltStore.", comment: "")
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
UserDefaults.standard.didPresentInitialNotification = true
}
}
}
@@ -72,11 +103,23 @@ private extension AppDelegate
{
guard let button = self.statusItem?.button, let superview = button.superview, let window = button.window else { return }
self.connectedDevices = ALTDeviceManager.shared.connectedDevices
self.connectedDevices = ALTDeviceManager.shared.availableDevices
self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:))
if FileManager.default.fileExists(atPath: pluginURL.path)
{
self.installMailPluginMenuItem.title = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
}
else
{
self.installMailPluginMenuItem.title = NSLocalizedString("Install Mail Plug-in", comment: "")
}
self.installMailPluginMenuItem.target = self
self.installMailPluginMenuItem.action = #selector(AppDelegate.handleInstallMailPluginMenuItem(_:))
let x = button.frame.origin.x
let y = button.frame.origin.y - 5
@@ -96,11 +139,7 @@ private extension AppDelegate
let alert = NSAlert()
alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "")
alert.informativeText = NSLocalizedString("""
Your Apple ID and password are not saved and are only sent to Apple for authentication.
If you have two-factor authentication enabled, please create an app-specific password for use with AltStore at https://appleid.apple.com.
""", comment: "")
alert.informativeText = NSLocalizedString("Your Apple ID and password are not saved and are only sent to Apple for authentication.", comment: "")
let textFieldSize = NSSize(width: 300, height: 22)
@@ -142,6 +181,13 @@ If you have two-factor authentication enabled, please create an app-specific pas
let password = passwordTextField.stringValue
let device = self.connectedDevices[index]
if !self.isMailPluginInstalled
{
let result = self.installMailPlugin()
guard result else { return }
}
ALTDeviceManager.shared.installAltStore(to: device, appleID: username, password: password) { (result) in
switch result
{
@@ -153,7 +199,7 @@ If you have two-factor authentication enabled, please create an app-specific pas
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
case .failure(InstallError.cancelled):
case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
break
@@ -192,6 +238,74 @@ If you have two-factor authentication enabled, please create an app-specific pas
LaunchAtLogin.isEnabled.toggle()
}
@objc func handleInstallMailPluginMenuItem(_ item: NSMenuItem)
{
installMailPlugin()
}
@discardableResult
func installMailPlugin() -> Bool
{
do
{
let previouslyInstalled = self.isMailPluginInstalled
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 }
}
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
}
}
}
extension AppDelegate: NSMenuDelegate

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15504"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -10,11 +11,11 @@
<objects>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="4" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" id="urc-xw-Dhc">
<rect key="frame" x="0.0" y="0.0" width="300" height="48"/>
<rect key="frame" x="0.0" y="0.0" width="300" height="46"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zLd-d8-ghZ">
<rect key="frame" x="0.0" y="26" width="300" height="22"/>
<rect key="frame" x="0.0" y="25" width="300" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Apple ID" drawsBackground="YES" id="BXa-Re-rs3">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@@ -26,7 +27,7 @@
</connections>
</textField>
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9rp-Vx-rvB">
<rect key="frame" x="0.0" y="0.0" width="300" height="22"/>
<rect key="frame" x="0.0" y="0.0" width="300" height="21"/>
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Password" drawsBackground="YES" usesSingleLineMode="YES" id="xqJ-wt-DlP">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@@ -61,9 +62,11 @@
<outlet property="authenticationAppleIDTextField" destination="zLd-d8-ghZ" id="wW5-0J-zdq"/>
<outlet property="authenticationPasswordTextField" destination="9rp-Vx-rvB" id="ZoC-DI-jzQ"/>
<outlet property="connectedDevicesMenu" destination="KJ9-WY-pW1" id="Mcv-64-iFU"/>
<outlet property="installMailPluginMenuItem" destination="3CM-gV-X2G" id="lio-ha-z0S"/>
<outlet property="launchAtLoginMenuItem" destination="IyR-FQ-upe" id="Fxn-EP-hwH"/>
</connections>
</customObject>
<customObject id="Arf-IC-5eb" customClass="SUUpdater"/>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
@@ -98,7 +101,17 @@
<menuItem title="Launch at Login" id="IyR-FQ-upe" userLabel="Launch At Login">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Install Mail Plug-in" id="3CM-gV-X2G" userLabel="Mail Plug-in">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="mVM-Nm-Zi9"/>
<menuItem title="Check for Updates..." id="Tnq-gD-Eic" userLabel="Check for Updates">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="checkForUpdates:" target="Arf-IC-5eb" id="7JG-du-nr4"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="hmG-xg-qgm"/>
<menuItem title="Quit AltServer" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>

View File

@@ -0,0 +1,24 @@
//
// ALTNotificationConnection+Private.h
// AltServer
//
// Created by Riley Testut on 1/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import "ALTNotificationConnection.h"
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/notification_proxy.h>
NS_ASSUME_NONNULL_BEGIN
@interface ALTNotificationConnection ()
@property (nonatomic, readonly) np_client_t client;
- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,30 @@
//
// ALTNotificationConnection.h
// AltServer
//
// Created by Riley Testut on 1/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import <AltSign/AltSign.h>
NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(NotificationConnection)
@interface ALTNotificationConnection : NSObject
@property (nonatomic, copy, readonly) ALTDevice *device;
@property (nonatomic, copy, nullable) void (^receivedNotificationHandler)(CFNotificationName notification);
- (void)startListeningForNotifications:(NSArray<NSString *> *)notifications
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
- (void)sendNotification:(CFNotificationName)notification
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
- (void)disconnect;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,93 @@
//
// ALTNotificationConnection.m
// AltServer
//
// Created by Riley Testut on 1/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import "ALTNotificationConnection+Private.h"
#import "AltKit.h"
void ALTDeviceReceivedNotification(const char *notification, void *user_data);
@implementation ALTNotificationConnection
- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client
{
self = [super init];
if (self)
{
_device = [device copy];
_client = client;
}
return self;
}
- (void)dealloc
{
[self disconnect];
}
- (void)disconnect
{
np_client_free(self.client);
_client = nil;
}
- (void)startListeningForNotifications:(NSArray<NSString *> *)notifications completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
const char **notificationNames = (const char **)malloc((notifications.count + 1) * sizeof(char *));
for (int i = 0; i < notifications.count; i++)
{
NSString *name = notifications[i];
notificationNames[i] = name.UTF8String;
}
notificationNames[notifications.count] = NULL; // Must have terminating NULL entry.
np_error_t result = np_observe_notifications(self.client, notificationNames);
if (result != NP_E_SUCCESS)
{
return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
}
result = np_set_notify_callback(self.client, ALTDeviceReceivedNotification, (__bridge void *)self);
if (result != NP_E_SUCCESS)
{
return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
}
completionHandler(YES, nil);
free(notificationNames);
});
}
- (void)sendNotification:(CFNotificationName)notification completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
np_error_t result = np_post_notification(self.client, [(__bridge NSString *)notification UTF8String]);
if (result == NP_E_SUCCESS)
{
completionHandler(YES, nil);
}
else
{
completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
}
});
}
@end
void ALTDeviceReceivedNotification(const char *notification, void *user_data)
{
ALTNotificationConnection *connection = (__bridge ALTNotificationConnection *)user_data;
if (connection.receivedNotificationHandler)
{
connection.receivedNotificationHandler((__bridge CFNotificationName)@(notification));
}
}

View File

@@ -0,0 +1,23 @@
//
// ALTWiredConnection+Private.h
// AltServer
//
// Created by Riley Testut on 1/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import "ALTWiredConnection.h"
#include <libimobiledevice/libimobiledevice.h>
NS_ASSUME_NONNULL_BEGIN
@interface ALTWiredConnection ()
@property (nonatomic, readonly) idevice_connection_t connection;
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,25 @@
//
// ALTWiredConnection.h
// AltServer
//
// Created by Riley Testut on 1/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import <AltSign/AltSign.h>
NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(WiredConnection)
@interface ALTWiredConnection : NSObject
@property (nonatomic, copy, readonly) ALTDevice *device;
- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler;
- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler;
- (void)disconnect;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,101 @@
//
// ALTWiredConnection.m
// AltServer
//
// Created by Riley Testut on 1/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import "ALTWiredConnection+Private.h"
#import "AltKit.h"
@implementation ALTWiredConnection
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection
{
self = [super init];
if (self)
{
_device = [device copy];
_connection = connection;
}
return self;
}
- (void)dealloc
{
[self disconnect];
}
- (void)disconnect
{
idevice_disconnect(self.connection);
_connection = nil;
}
- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
{
void (^finish)(NSError *error) = ^(NSError *error) {
if (error != nil)
{
NSLog(@"Send Error: %@", error);
completionHandler(NO, error);
}
else
{
completionHandler(YES, nil);
}
};
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
NSMutableData *mutableData = [data mutableCopy];
while (mutableData.length > 0)
{
uint32_t sentBytes = 0;
if (idevice_connection_send(self.connection, (const char *)mutableData.bytes, (int32_t)mutableData.length, &sentBytes) != IDEVICE_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
}
[mutableData replaceBytesInRange:NSMakeRange(0, sentBytes) withBytes:NULL length:0];
}
finish(nil);
});
}
- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler
{
void (^finish)(NSData *data, NSError *error) = ^(NSData *data, NSError *error) {
if (error != nil)
{
NSLog(@"Receive Data Error: %@", error);
}
completionHandler(data, error);
};
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
char bytes[4096];
NSMutableData *receivedData = [NSMutableData dataWithCapacity:expectedSize];
while (receivedData.length < expectedSize)
{
uint32_t size = MIN(4096, (uint32_t)expectedSize - (uint32_t)receivedData.length);
uint32_t receivedBytes = 0;
if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 0) != IDEVICE_E_SUCCESS)
{
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
}
NSData *data = [NSData dataWithBytesNoCopy:bytes length:receivedBytes freeWhenDone:NO];
[receivedData appendData:data];
}
finish(receivedData, nil);
});
}
@end

View File

@@ -0,0 +1,231 @@
//
// ClientConnection.swift
// AltServer
//
// Created by Riley Testut on 1/9/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Network
import AltKit
import AltSign
extension ClientConnection
{
enum Connection
{
case wireless(NWConnection)
case wired(WiredConnection)
}
}
class ClientConnection
{
let connection: Connection
init(connection: Connection)
{
self.connection = connection
}
func disconnect()
{
switch self.connection
{
case .wireless(let connection):
switch connection.state
{
case .cancelled, .failed:
print("Disconnecting from \(connection.endpoint)...")
default:
// State update handler might call this method again.
connection.cancel()
}
case .wired(let connection):
connection.disconnect()
}
}
func send<T: Encodable>(_ response: T, shouldDisconnect: Bool = false, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
func finish(_ result: Result<Void, ALTServerError>)
{
completionHandler(result)
if shouldDisconnect
{
// Add short delay to prevent us from dropping connection too quickly.
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
self.disconnect()
}
}
}
do
{
let data = try JSONEncoder().encode(response)
let responseSize = withUnsafeBytes(of: Int32(data.count)) { Data($0) }
self.send(responseSize) { (result) in
switch result
{
case .failure: finish(.failure(.init(.lostConnection)))
case .success:
self.send(data) { (result) in
switch result
{
case .failure: finish(.failure(.init(.lostConnection)))
case .success: finish(.success(()))
}
}
}
}
}
catch
{
finish(.failure(.init(.invalidResponse)))
}
}
func receiveRequest(completionHandler: @escaping (Result<ServerRequest, ALTServerError>) -> Void)
{
let size = MemoryLayout<Int32>.size
print("Receiving request size")
self.receiveData(expectedBytes: size) { (result) in
do
{
let data = try result.get()
print("Receiving request...")
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
self.receiveData(expectedBytes: expectedBytes) { (result) in
do
{
let data = try result.get()
let request = try JSONDecoder().decode(ServerRequest.self, from: data)
print("Received installation request:", request)
completionHandler(.success(request))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
func send(_ data: Data, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
switch self.connection
{
case .wireless(let connection):
connection.send(content: data, completion: .contentProcessed { (error) in
if let error = error
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(()))
}
})
case .wired(let connection):
connection.send(data) { (success, error) in
if !success
{
completionHandler(.failure(ALTServerError(.lostConnection)))
}
else
{
completionHandler(.success(()))
}
}
}
}
func receiveData(expectedBytes: Int, completionHandler: @escaping (Result<Data, Error>) -> Void)
{
func finish(data: Data?, error: Error?)
{
do
{
let data = try self.process(data: data, error: error)
completionHandler(.success(data))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
switch self.connection
{
case .wireless(let connection):
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
finish(data: data, error: error)
}
case .wired(let connection):
connection.receiveData(withExpectedSize: expectedBytes) { (data, error) in
finish(data: data, error: error)
}
}
}
}
extension ClientConnection: CustomStringConvertible
{
var description: String {
switch self.connection
{
case .wireless(let connection): return "\(connection.endpoint) (Wireless)"
case .wired(let connection): return "\(connection.device.name) (Wired)"
}
}
}
private extension ClientConnection
{
func process(data: Data?, error: Error?) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
}
}

View File

@@ -8,6 +8,7 @@
import Foundation
import Network
import AppKit
import AltKit
@@ -53,10 +54,13 @@ class ConnectionManager
private lazy var listener = self.makeListener()
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltServer.connections", qos: .utility)
private var connections = [NWConnection]()
private var connections = [ClientConnection]()
private var notificationConnections = [ALTDevice: NotificationConnection]()
private init()
{
NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidConnect(_:)), name: .deviceManagerDeviceDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidDisconnect(_:)), name: .deviceManagerDeviceDidDisconnect, object: nil)
}
func start()
@@ -76,6 +80,16 @@ class ConnectionManager
default: break
}
}
func disconnect(_ connection: ClientConnection)
{
connection.disconnect()
if let index = self.connections.firstIndex(where: { $0 === connection })
{
self.connections.remove(at: index)
}
}
}
private extension ConnectionManager
@@ -126,68 +140,18 @@ private extension ConnectionManager
}
listener.newConnectionHandler = { [weak self] (connection) in
self?.awaitRequest(from: connection)
self?.prepare(connection)
}
return listener
}
func disconnect(_ connection: NWConnection)
func prepare(_ connection: NWConnection)
{
switch connection.state
{
case .cancelled, .failed:
print("Disconnecting from \(connection.endpoint)...")
if let index = self.connections.firstIndex(where: { $0 === connection })
{
self.connections.remove(at: index)
}
default:
// State update handler will call this method again.
connection.cancel()
}
}
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
}
}
private extension ConnectionManager
{
func awaitRequest(from connection: NWConnection)
{
guard !self.connections.contains(where: { $0 === connection }) else { return }
self.connections.append(connection)
let clientConnection = ClientConnection(connection: .wireless(connection))
guard !self.connections.contains(where: { $0 === clientConnection }) else { return }
self.connections.append(clientConnection)
connection.stateUpdateHandler = { [weak self] (state) in
switch state
@@ -196,20 +160,17 @@ private extension ConnectionManager
case .ready:
print("Connected to client:", connection.endpoint)
self?.receiveApp(from: connection) { (result) in
self?.finish(connection: connection, error: result.error)
}
self?.handleRequest(for: clientConnection)
case .waiting:
print("Waiting for connection...")
case .failed(let error):
print("Failed to connect to service \(connection.endpoint).", error)
self?.disconnect(connection)
self?.disconnect(clientConnection)
case .cancelled:
self?.disconnect(connection)
self?.disconnect(clientConnection)
@unknown default: break
}
@@ -217,8 +178,124 @@ private extension ConnectionManager
connection.start(queue: self.dispatchQueue)
}
}
private extension ConnectionManager
{
func startNotificationConnection(to device: ALTDevice)
{
ALTDeviceManager.shared.startNotificationConnection(to: device) { (connection, error) in
guard let connection = connection else { return }
let notifications: [CFNotificationName] = [.wiredServerConnectionAvailableRequest, .wiredServerConnectionStartRequest]
connection.startListening(forNotifications: notifications.map { String($0.rawValue) }) { (success, error) in
guard success else { return }
connection.receivedNotificationHandler = { [weak self, weak connection] (notification) in
guard let self = self, let connection = connection else { return }
self.handle(notification, for: connection)
}
self.notificationConnections[device] = connection
}
}
}
func receiveApp(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
func stopNotificationConnection(to device: ALTDevice)
{
guard let connection = self.notificationConnections[device] else { return }
connection.disconnect()
self.notificationConnections[device] = nil
}
func handle(_ notification: CFNotificationName, for connection: NotificationConnection)
{
switch notification
{
case .wiredServerConnectionAvailableRequest:
connection.sendNotification(.wiredServerConnectionAvailableResponse) { (success, error) in
if let error = error, !success
{
print("Error sending wired server connection response.", error)
}
else
{
print("Sent wired server connection available response!")
}
}
case .wiredServerConnectionStartRequest:
ALTDeviceManager.shared.startWiredConnection(to: connection.device) { (wiredConnection, error) in
if let wiredConnection = wiredConnection
{
print("Started wired server connection!")
let clientConnection = ClientConnection(connection: .wired(wiredConnection))
self.handleRequest(for: clientConnection)
}
else if let error = error
{
print("Error starting wired server connection.", error)
}
}
default: break
}
}
}
private extension ConnectionManager
{
func handleRequest(for connection: ClientConnection)
{
connection.receiveRequest() { (result) in
print("Received initial request with result:", result)
switch result
{
case .failure(let error):
let response = ErrorResponse(error: ALTServerError(error))
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent error response with result:", result)
}
case .success(.anisetteData(let request)):
self.handleAnisetteDataRequest(request, for: connection)
case .success(.prepareApp(let request)):
self.handlePrepareAppRequest(request, for: connection)
case .success:
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent unknown request response with result:", result)
}
}
}
}
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: ClientConnection)
{
AnisetteDataManager.shared.requestAnisetteData { (result) in
switch result
{
case .failure(let error):
let errorResponse = ErrorResponse(error: ALTServerError(error))
connection.send(errorResponse, shouldDisconnect: true) { (result) in
print("Sent anisette data error response with result:", result)
}
case .success(let anisetteData):
let response = AnisetteDataResponse(anisetteData: anisetteData)
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent anisette data response with result:", result)
}
}
}
}
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: ClientConnection)
{
var temporaryURL: URL?
@@ -230,83 +307,75 @@ private extension ConnectionManager
catch { print("Failed to remove .ipa.", error) }
}
completionHandler(result)
switch result
{
case .failure(let error):
print("Failed to process request from \(connection).", error)
let response = ErrorResponse(error: ALTServerError(error))
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent install app error response to \(connection) with result:", result)
}
case .success:
print("Processed request from \(connection).")
let response = InstallationProgressResponse(progress: 1.0)
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent install app response to \(connection) with result:", result)
}
}
}
self.receive(PrepareAppRequest.self, from: connection) { (result) in
print("Received request with result:", result)
self.receiveApp(for: request, from: connection) { (result) in
print("Received app with result:", result)
switch result
{
case .failure(let error): finish(.failure(error))
case .success(let request):
self.receiveApp(for: request, from: connection) { (result) in
print("Received app with result:", result)
case .success(let fileURL):
temporaryURL = fileURL
print("Awaiting begin installation request...")
connection.receiveRequest() { (result) in
print("Received begin installation request with result:", result)
switch result
{
case .failure(let error): finish(.failure(error))
case .success(let request, let fileURL):
temporaryURL = fileURL
case .success(.beginInstallation):
print("Installing to device \(request.udid)...")
print("Awaiting begin installation request...")
self.receive(BeginInstallationRequest.self, from: connection) { (result) in
print("Received begin installation request with result:", result)
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in
print("Installed to device with result:", result)
switch result
{
case .failure(let error): finish(.failure(error))
case .success:
print("Installing to device \(request.udid)...")
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in
print("Installed to device with result:", result)
switch result
{
case .failure(let error): finish(.failure(error))
case .success: finish(.success(()))
}
}
case .success: finish(.success(()))
}
}
case .success:
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent unknown request error response to \(connection) with result:", result)
}
}
}
}
}
}
func finish(connection: NWConnection, error: ALTServerError?)
func receiveApp(for request: PrepareAppRequest, from connection: ClientConnection, completionHandler: @escaping (Result<URL, ALTServerError>) -> Void)
{
if let error = error
{
print("Failed to process request from \(connection.endpoint).", error)
}
else
{
print("Processed request from \(connection.endpoint).")
}
let response = ServerResponse(progress: 1.0, error: error)
self.send(response, to: connection) { (result) in
print("Sent response to \(connection.endpoint) with result:", result)
self.disconnect(connection)
}
}
func receiveApp(for request: PrepareAppRequest, from connection: NWConnection, completionHandler: @escaping (Result<(PrepareAppRequest, URL), ALTServerError>) -> Void)
{
connection.receive(minimumIncompleteLength: request.contentSize, maximumLength: request.contentSize) { (data, _, _, error) in
connection.receiveData(expectedBytes: request.contentSize) { (result) in
do
{
print("Received app data!")
let data = try self.process(data: data, error: error, from: connection)
print("Processed app data!")
let data = try result.get()
guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
print("Writing app data...")
@@ -316,7 +385,7 @@ private extension ConnectionManager
print("Wrote app to URL:", temporaryURL)
completionHandler(.success((request, temporaryURL)))
completionHandler(.success(temporaryURL))
}
catch
{
@@ -327,7 +396,7 @@ private extension ConnectionManager
}
}
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, connection: ClientConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
var isSending = false
@@ -356,9 +425,9 @@ private extension ConnectionManager
isSending = true
print("Progress:", progress.fractionCompleted)
let response = ServerResponse(progress: progress.fractionCompleted, error: nil)
let response = InstallationProgressResponse(progress: progress.fractionCompleted)
self.send(response, to: connection) { (result) in
connection.send(response) { (result) in
serialQueue.async {
isSending = false
}
@@ -366,79 +435,19 @@ private extension ConnectionManager
}
})
}
}
func send<T: Encodable>(_ response: T, to connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
private extension ConnectionManager
{
@objc func deviceDidConnect(_ notification: Notification)
{
do
{
let data = try JSONEncoder().encode(response)
let responseSize = withUnsafeBytes(of: Int32(data.count)) { Data($0) }
connection.send(content: responseSize, completion: .contentProcessed { (error) in
do
{
if let error = error
{
throw error
}
connection.send(content: data, completion: .contentProcessed { (error) in
if error != nil
{
completionHandler(.failure(.init(.lostConnection)))
}
else
{
completionHandler(.success(()))
}
})
}
catch
{
completionHandler(.failure(.init(.lostConnection)))
}
})
}
catch
{
completionHandler(.failure(.init(.invalidResponse)))
}
guard let device = notification.object as? ALTDevice else { return }
self.startNotificationConnection(to: device)
}
func receive<T: Decodable>(_ responseType: T.Type, from connection: NWConnection, completionHandler: @escaping (Result<T, ALTServerError>) -> Void)
@objc func deviceDidDisconnect(_ notification: Notification)
{
let size = MemoryLayout<Int32>.size
print("Receiving request size")
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
print("Receiving request...")
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let request = try JSONDecoder().decode(T.self, from: data)
print("Received installation request:", request)
completionHandler(.success(request))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
guard let device = notification.object as? ALTDevice else { return }
self.stopNotificationConnection(to: device)
}
}

View File

@@ -8,6 +8,13 @@
import Cocoa
import UserNotifications
import ObjectiveC
#if STAGING
private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altstore.ipa")!
#else
private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")!
#endif
enum InstallError: LocalizedError
{
@@ -49,126 +56,147 @@ extension ALTDeviceManager
try? FileManager.default.removeItem(at: destinationDirectoryURL)
}
self.authenticate(appleID: appleID, password: password) { (result) in
AnisetteDataManager.shared.requestAnisetteData { (result) in
do
{
let account = try result.get()
let anisetteData = try result.get()
self.fetchTeam(for: account) { (result) in
self.authenticate(appleID: appleID, password: password, anisetteData: anisetteData) { (result) in
do
{
let team = try result.get()
let (account, session) = try result.get()
self.register(device, team: team) { (result) in
self.fetchTeam(for: account, session: session) { (result) in
do
{
let device = try result.get()
let team = try result.get()
self.fetchCertificate(for: team) { (result) in
self.register(device, team: team, session: session) { (result) in
do
{
let certificate = try result.get()
let device = try result.get()
let content = UNMutableNotificationContent()
content.title = String(format: NSLocalizedString("Installing AltStore to %@...", comment: ""), device.name)
content.body = NSLocalizedString("This may take a few seconds.", comment: "")
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
self.downloadApp { (result) in
self.fetchCertificate(for: team, session: session) { (result) in
do
{
let fileURL = try result.get()
let certificate = try result.get()
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let content = UNMutableNotificationContent()
content.title = String(format: NSLocalizedString("Installing AltStore to %@...", comment: ""), device.name)
content.body = NSLocalizedString("This may take a few seconds.", comment: "")
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
do
{
try FileManager.default.removeItem(at: fileURL)
}
catch
{
print("Failed to remove downloaded .ipa.", error)
}
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
self.registerAppID(name: "AltStore", identifier: "com.rileytestut.AltStore", team: team) { (result) in
self.downloadApp { (result) in
do
{
let appID = try result.get()
let fileURL = try result.get()
self.updateFeatures(for: appID, app: application, team: team) { (result) in
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
do
{
try FileManager.default.removeItem(at: fileURL)
}
catch
{
print("Failed to remove downloaded .ipa.", error)
}
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
// Refresh anisette data to prevent session timeouts.
AnisetteDataManager.shared.requestAnisetteData { (result) in
do
{
let appID = try result.get()
let anisetteData = try result.get()
session.anisetteData = anisetteData
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
self.registerAppID(name: "AltStore", identifier: "com.rileytestut.AltStore", team: team, session: session) { (result) in
do
{
let provisioningProfile = try result.get()
let appID = try result.get()
self.install(application, to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile) { (result) in
finish(result.error, title: "Failed to Install AltStore")
self.updateFeatures(for: appID, app: application, team: team, session: session) { (result) in
do
{
let appID = try result.get()
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
do
{
let provisioningProfile = try result.get()
self.install(application, to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile) { (result) in
finish(result.error, title: "Failed to Install AltStore")
}
}
catch
{
finish(error, title: "Failed to Fetch Provisioning Profile")
}
}
}
catch
{
finish(error, title: "Failed to Update App ID")
}
}
}
catch
{
finish(error, title: "Failed to Fetch Provisioning Profile")
finish(error, title: "Failed to Register App")
}
}
}
catch
{
finish(error, title: "Failed to Update App ID")
finish(error, title: "Failed to Refresh Anisette Data")
}
}
}
catch
{
finish(error, title: "Failed to Register App")
finish(error, title: "Failed to Download AltStore")
}
}
}
catch
{
finish(error, title: "Failed to Download AltStore")
return
finish(error, title: "Failed to Fetch Certificate")
}
}
}
catch
{
finish(error, title: "Failed to Fetch Certificate")
finish(error, title: "Failed to Register Device")
}
}
}
catch
{
finish(error, title: "Failed to Register Device")
finish(error, title: "Failed to Fetch Team")
}
}
}
catch
{
finish(error, title: "Failed to Fetch Team")
finish(error, title: "Failed to Authenticate")
}
}
}
catch
{
finish(error, title: "Failed to Authenticate")
finish(error, title: "Failed to Fetch Anisette Data")
}
}
}
func downloadApp(completionHandler: @escaping (Result<URL, Error>) -> Void)
{
let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")!
let downloadTask = URLSession.shared.downloadTask(with: appURL) { (fileURL, response, error) in
do
{
@@ -184,15 +212,57 @@ extension ALTDeviceManager
downloadTask.resume()
}
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<ALTAccount, Error>) -> Void)
func authenticate(appleID: String, password: String, anisetteData: ALTAnisetteData, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void)
{
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in
let result = Result(account, error)
completionHandler(result)
func handleVerificationCode(_ completionHandler: @escaping (String?) -> Void)
{
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Two-Factor Authentication Enabled", comment: "")
alert.informativeText = NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: "")
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 22))
textField.delegate = self
textField.translatesAutoresizingMaskIntoConstraints = false
textField.placeholderString = NSLocalizedString("123456", comment: "")
alert.accessoryView = textField
alert.window.initialFirstResponder = textField
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
self.securityCodeAlert = alert
self.securityCodeTextField = textField
self.validate()
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let response = alert.runModal()
if response == .alertFirstButtonReturn
{
let code = textField.stringValue
completionHandler(code)
}
else
{
completionHandler(nil)
}
}
}
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData, verificationHandler: handleVerificationCode) { (account, session, error) in
if let account = account, let session = session
{
completionHandler(.success((account, session)))
}
else
{
completionHandler(.failure(error ?? ALTAppleAPIError(.unknown)))
}
}
}
func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
{
func finish(_ result: Result<ALTTeam, Error>)
{
@@ -238,7 +308,7 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
}
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
do
{
let teams = try Result(teams, error).get()
@@ -267,9 +337,9 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
}
func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
{
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
@@ -304,11 +374,11 @@ To prevent this from happening, feel free to try again with another Apple ID to
if let certificate = certificates.first
{
ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
do
{
try Result(success, error).get()
self.fetchCertificate(for: team, completionHandler: completionHandler)
self.fetchCertificate(for: team, session: session, completionHandler: completionHandler)
}
catch
{
@@ -318,13 +388,13 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
else
{
ALTAppleAPI.shared.addCertificate(machineName: "AltStore", to: team) { (certificate, error) in
ALTAppleAPI.shared.addCertificate(machineName: "AltStore", to: team, session: session) { (certificate, error) in
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw InstallError.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
@@ -357,11 +427,11 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
}
func registerAppID(name appName: String, identifier: String, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
func registerAppID(name appName: String, identifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
let bundleID = "com.\(team.identifier).\(identifier)"
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do
{
let appIDs = try Result(appIDs, error).get()
@@ -372,7 +442,7 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
else
{
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error))
}
}
@@ -384,7 +454,7 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
}
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
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 }
@@ -401,14 +471,14 @@ To prevent this from happening, feel free to try again with another Apple ID to
let appID = appID.copy() as! ALTAppID
appID.features = features
ALTAppleAPI.shared.update(appID, team: team) { (appID, error) in
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error))
}
}
func register(_ device: ALTDevice, team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
func register(_ device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
@@ -419,7 +489,7 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
else
{
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, team: team) { (device, error) in
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, team: team, session: session) { (device, error) in
completionHandler(Result(device, error))
}
}
@@ -431,9 +501,9 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
@@ -449,7 +519,16 @@ To prevent this from happening, feel free to try again with another Apple ID to
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
infoDictionary[Bundle.Info.deviceID] = device.identifier
infoDictionary[Bundle.Info.serverID] = UserDefaults.standard.serverID
infoDictionary[Bundle.Info.certificateID] = certificate.serialNumber
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
if
let machineIdentifier = certificate.machineIdentifier,
let encryptedData = certificate.encryptedP12Data(withPassword: machineIdentifier)
{
let certificateURL = application.fileURL.appendingPathComponent("ALTCertificate.p12")
try encryptedData.write(to: certificateURL, options: .atomic)
}
let resigner = ALTSigner(team: team, certificate: certificate)
resigner.signApp(at: application.fileURL, provisioningProfiles: [profile]) { (success, error) in
@@ -476,3 +555,45 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
}
}
private var securityCodeAlertKey = 0
private var securityCodeTextFieldKey = 0
extension ALTDeviceManager: NSTextFieldDelegate
{
var securityCodeAlert: NSAlert? {
get { return objc_getAssociatedObject(self, &securityCodeAlertKey) as? NSAlert }
set { objc_setAssociatedObject(self, &securityCodeAlertKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
var securityCodeTextField: NSTextField? {
get { return objc_getAssociatedObject(self, &securityCodeTextFieldKey) as? NSTextField }
set { objc_setAssociatedObject(self, &securityCodeTextFieldKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
public func controlTextDidChange(_ obj: Notification)
{
self.validate()
}
public func controlTextDidEndEditing(_ obj: Notification)
{
self.validate()
}
private func validate()
{
guard let code = self.securityCodeTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) else { return }
if code.count == 6
{
self.securityCodeAlert?.buttons.first?.isEnabled = true
}
else
{
self.securityCodeAlert?.buttons.first?.isEnabled = false
}
self.securityCodeAlert?.layout()
}
}

View File

@@ -9,8 +9,14 @@
#import <Foundation/Foundation.h>
#import <AltSign/AltSign.h>
@class ALTWiredConnection;
@class ALTNotificationConnection;
NS_ASSUME_NONNULL_BEGIN
extern NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidConnect);
extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidDisconnect);
@interface ALTDeviceManager : NSObject
@property (class, nonatomic, readonly) ALTDeviceManager *sharedManager;
@@ -18,8 +24,15 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) NSArray<ALTDevice *> *connectedDevices;
@property (nonatomic, readonly) NSArray<ALTDevice *> *availableDevices;
- (void)start;
/* App Installation */
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
/* Connections */
- (void)startWiredConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTWiredConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
- (void)startNotificationConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTNotificationConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
@end
NS_ASSUME_NONNULL_END

View File

@@ -7,7 +7,10 @@
//
#import "ALTDeviceManager.h"
#import "NSError+ALTServerError.h"
#import "AltKit.h"
#import "ALTWiredConnection+Private.h"
#import "ALTNotificationConnection+Private.h"
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
@@ -17,8 +20,10 @@
#include <libimobiledevice/misagent.h>
void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *udid);
void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data);
NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification = @"ALTDeviceManagerDeviceDidConnectNotification";
NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALTDeviceManagerDeviceDidDisconnectNotification";
@interface ALTDeviceManager ()
@@ -26,6 +31,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSProgress *> *installationProgress;
@property (nonatomic, readonly) dispatch_queue_t installationQueue;
@property (nonatomic, readonly) NSMutableSet<ALTDevice *> *cachedDevices;
@end
@implementation ALTDeviceManager
@@ -50,11 +57,20 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
_installationProgress = [NSMutableDictionary dictionary];
_installationQueue = dispatch_queue_create("com.rileytestut.AltServer.InstallationQueue", DISPATCH_QUEUE_SERIAL);
_cachedDevices = [NSMutableSet set];
}
return self;
}
- (void)start
{
idevice_event_subscribe(ALTDeviceDidChangeConnectionStatus, nil);
}
#pragma mark - App Installation -
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
{
NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:4];
@@ -109,6 +125,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
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];
@@ -279,13 +297,25 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
return finish(error);
}
plist_t profiles = NULL;
plist_t rawProfiles = NULL;
if (misagent_copy_all(mis, &profiles) != MISAGENT_E_SUCCESS)
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++)
{
@@ -294,7 +324,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
{
continue;
}
char *bytes = NULL;
uint64_t length = 0;
@@ -304,10 +334,10 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
continue;
}
NSData *data = [NSData dataWithBytes:(const void *)bytes length:length];
NSData *data = [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:YES];
ALTProvisioningProfile *provisioningProfile = [[ALTProvisioningProfile alloc] initWithData:data];
if (![provisioningProfile.teamIdentifier isEqualToString:installationProvisioningProfile.teamIdentifier])
if (![provisioningProfile isFreeProvisioningProfile])
{
NSLog(@"Ignoring: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier);
continue;
@@ -338,14 +368,17 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
if (misagent_remove(mis, provisioningProfile.UUID.UUIDString.lowercaseString.UTF8String) == MISAGENT_E_SUCCESS)
{
NSLog(@"Removed provisioning profile: %@", provisioningProfile.UUID);
NSLog(@"Removed provisioning profile: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier);
}
else
{
int code = misagent_get_status_code(mis);
NSLog(@"Failed to remove provisioning profile %@. Error Code: %@", provisioningProfile.UUID, @(code));
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;
@@ -514,6 +547,89 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
return success;
}
#pragma mark - Connections -
- (void)startWiredConnectionToDevice:(ALTDevice *)altDevice completionHandler:(void (^)(ALTWiredConnection * _Nullable, NSError * _Nullable))completionHandler
{
void (^finish)(ALTWiredConnection *connection, NSError *error) = ^(ALTWiredConnection *connection, NSError *error) {
if (error != nil)
{
NSLog(@"Wired Connection Error: %@", error);
}
completionHandler(connection, error);
};
idevice_t device = NULL;
idevice_connection_t connection = NULL;
/* Find Device */
if (idevice_new_ignore_network(&device, altDevice.identifier.UTF8String) != IDEVICE_E_SUCCESS)
{
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
}
/* Connect to Listening Socket */
if (idevice_connect(device, ALTDeviceListeningSocket, &connection) != IDEVICE_E_SUCCESS)
{
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
idevice_free(device);
ALTWiredConnection *wiredConnection = [[ALTWiredConnection alloc] initWithDevice:altDevice connection:connection];
finish(wiredConnection, nil);
}
- (void)startNotificationConnectionToDevice:(ALTDevice *)altDevice completionHandler:(void (^)(ALTNotificationConnection * _Nullable, NSError * _Nullable))completionHandler
{
void (^finish)(ALTNotificationConnection *, NSError *) = ^(ALTNotificationConnection *connection, NSError *error) {
if (error != nil)
{
NSLog(@"Notification Connection Error: %@", error);
}
completionHandler(connection, error);
};
idevice_t device = NULL;
lockdownd_client_t lockdownClient = NULL;
lockdownd_service_descriptor_t service = NULL;
np_client_t client = NULL;
/* Find Device */
if (idevice_new_ignore_network(&device, altDevice.identifier.UTF8String) != IDEVICE_E_SUCCESS)
{
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
}
/* Connect to Device */
if (lockdownd_client_new_with_handshake(device, &lockdownClient, "altserver") != LOCKDOWN_E_SUCCESS)
{
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
/* Connect to Notification Proxy */
if ((lockdownd_start_service(lockdownClient, "com.apple.mobile.notification_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
{
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
/* Connect to Client */
if (np_client_new(device, service, &client) != NP_E_SUCCESS)
{
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
lockdownd_service_descriptor_free(service);
lockdownd_client_free(lockdownClient);
idevice_free(device);
ALTNotificationConnection *notificationConnection = [[ALTNotificationConnection alloc] initWithDevice:altDevice client:client];
completionHandler(notificationConnection, nil);
}
#pragma mark - Getters -
- (NSArray<ALTDevice *> *)connectedDevices
@@ -670,3 +786,49 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
NSLog(@"Installation Progress: %@", @(percent));
}
}
void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data)
{
ALTDevice * (^deviceForUDID)(NSString *, NSArray<ALTDevice *> *) = ^ALTDevice *(NSString *udid, NSArray<ALTDevice *> *devices) {
for (ALTDevice *device in devices)
{
if ([device.identifier isEqualToString:udid])
{
return device;
}
}
return nil;
};
switch (event->event)
{
case IDEVICE_DEVICE_ADD:
{
ALTDevice *device = deviceForUDID(@(event->udid), ALTDeviceManager.sharedManager.connectedDevices);
[[NSNotificationCenter defaultCenter] postNotificationName:ALTDeviceManagerDeviceDidConnectNotification object:device];
if (device)
{
[ALTDeviceManager.sharedManager.cachedDevices addObject:device];
}
break;
}
case IDEVICE_DEVICE_REMOVE:
{
ALTDevice *device = deviceForUDID(@(event->udid), ALTDeviceManager.sharedManager.cachedDevices.allObjects);
[[NSNotificationCenter defaultCenter] postNotificationName:ALTDeviceManagerDeviceDidDisconnectNotification object:device];
if (device)
{
[ALTDeviceManager.sharedManager.cachedDevices removeObject:device];
}
break;
}
default: break;
}
}

View File

@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>2</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
@@ -30,5 +30,7 @@
<string>Main</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>SUFeedURL</key>
<string>https://altstore.io/altserver/sparkle-macos.xml</string>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
#!/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

@@ -0,0 +1,9 @@
#!/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

@@ -3,10 +3,12 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
01100C7036F0EBAC5B30984B /* libPods-AltStore.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0DE618FA97EA42C3F468D186 /* libPods-AltStore.a */; };
A8BCEBEAC0620CF80A2FD26D /* Pods_AltServer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */; };
BF0201BA22C2EFA3000B93E4 /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; };
BF0201BB22C2EFA3000B93E4 /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BF0201BD22C2EFBC000B93E4 /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; };
@@ -17,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 */; };
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 */; };
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B0F022E25DF9005C4CF5 /* ToastView.swift */; };
@@ -28,6 +31,7 @@
BF1E315F22A0635900370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF258CE222EBAE2800023032 /* AppProtocol.swift */; };
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 */; };
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; };
@@ -110,29 +114,46 @@
BF45884A2298D55000BD7491 /* thread.c in Sources */ = {isa = PBXBuildFile; fileRef = BF4588482298D55000BD7491 /* thread.c */; };
BF45884B2298D55000BD7491 /* thread.h in Headers */ = {isa = PBXBuildFile; fileRef = BF4588492298D55000BD7491 /* thread.h */; };
BF4588882298DD3F00BD7491 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4588872298DD3F00BD7491 /* libxml2.tbd */; };
BF4713A522976D1E00784A2F /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; };
BF4713A622976D1E00784A2F /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
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 */; };
BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; };
BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
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 */; };
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 */; };
BF718BD523C928A300A89F2D /* ALTNotificationConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD423C928A300A89F2D /* ALTNotificationConnection.m */; };
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 */; };
BF770E5622BC3C03002A40FE /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5522BC3C02002A40FE /* Server.swift */; };
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.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 */; };
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */; };
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 */; };
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 */; };
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 */; };
BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21A23186D640022A802 /* NewsItem.swift */; };
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21D231870160022A802 /* NewsViewController.swift */; };
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21F231870B00022A802 /* NewsCollectionViewCell.swift */; };
@@ -145,8 +166,6 @@
BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; };
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; };
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247782284B9A700981D42 /* LaunchScreen.storyboard */; };
BFD247872284BB4200981D42 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFD247862284BB3B00981D42 /* Roxas.framework */; };
BFD247882284BB4200981D42 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFD247862284BB3B00981D42 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
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 */; };
@@ -189,6 +208,7 @@
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 */; };
@@ -199,6 +219,7 @@
BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DC22F0E7F3002E24B9 /* Source.swift */; };
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */; };
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338E722F10E56002E24B9 /* LaunchViewController.swift */; };
BFE48975238007CE003239E0 /* AnisetteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE48974238007CE003239E0 /* AnisetteDataManager.swift */; };
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE60737231ADF49002B0E8E /* Settings.storyboard */; };
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE60739231ADF82002B0E8E /* SettingsViewController.swift */; };
BFE6073C231AE1E7002B0E8E /* SettingsHeaderFooterView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFE6073B231AE1E7002B0E8E /* SettingsHeaderFooterView.xib */; };
@@ -208,6 +229,8 @@
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 */; };
@@ -215,7 +238,6 @@
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B695232242D3007A79E1 /* LicensesViewController.swift */; };
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */; };
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */; };
DBAC68F8EC03F4A41D62EDE1 /* Pods_AltStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1039C07E517311FC499A0B64 /* Pods_AltStore.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -240,6 +262,13 @@
remoteGlobalIDString = BF45872A2298D31600BD7491;
remoteInfo = libimobiledevice;
};
BFBFFB262380C72F00993A4A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = BFD247622284B9A500981D42 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BF5C5FC4237DF5AE00EDD0C6;
remoteInfo = AltPlugin;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -265,23 +294,21 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
BFD247842284BB2C00981D42 /* Embed Frameworks */ = {
BF5C5FE9237E438C00EDD0C6 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
BF4713A622976D1E00784A2F /* openssl.framework in Embed Frameworks */,
BFD247882284BB4200981D42 /* Roxas.framework in Embed Frameworks */,
BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1039C07E517311FC499A0B64 /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0DE618FA97EA42C3F468D186 /* libPods-AltStore.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AltStore.a"; sourceTree = BUILT_PRODUCTS_DIR; };
11611D46F8A7C8B928E8156B /* Pods-AltServer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltServer.debug.xcconfig"; path = "Target Support Files/Pods-AltServer/Pods-AltServer.debug.xcconfig"; sourceTree = "<group>"; };
589BA531D903B28F292063E5 /* Pods-AltServer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltServer.release.xcconfig"; path = "Target Support Files/Pods-AltServer/Pods-AltServer.release.xcconfig"; sourceTree = "<group>"; };
A136EE677716B80768E9F0A2 /* Pods-AltStore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.release.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.release.xcconfig"; sourceTree = "<group>"; };
BF02419322F2156E00129732 /* RefreshAttempt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttempt.swift; sourceTree = "<group>"; };
BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttemptsViewController.swift; sourceTree = "<group>"; };
@@ -289,6 +316,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>"; };
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>"; };
BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreAppPolicy.swift; sourceTree = "<group>"; };
@@ -302,6 +330,8 @@
BF1E315022A0616100370A3C /* libAltKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libAltKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
BF219A7E22CAC431007676A6 /* AltStore.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltStore.entitlements; sourceTree = "<group>"; };
BF258CE222EBAE2800023032 /* AppProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = "<group>"; };
BF26A0DF2370C5D400F53F9F /* ALTSourceUserInfoKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTSourceUserInfoKey.h; sourceTree = "<group>"; };
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>"; };
BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; };
@@ -392,17 +422,42 @@
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>"; };
BF56D2A923DF88310006506D /* AppID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppID.swift; sourceTree = "<group>"; };
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppIDsOperation.swift; sourceTree = "<group>"; };
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDsViewController.swift; sourceTree = "<group>"; };
BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AltPlugin.mailbundle; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
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>"; };
BF718BCF23C91BD300A89F2D /* ALTWiredConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWiredConnection.h; sourceTree = "<group>"; };
BF718BD023C91BD300A89F2D /* ALTWiredConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTWiredConnection.m; sourceTree = "<group>"; };
BF718BD223C91C7000A89F2D /* ALTWiredConnection+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ALTWiredConnection+Private.h"; sourceTree = "<group>"; };
BF718BD323C928A300A89F2D /* ALTNotificationConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTNotificationConnection.h; sourceTree = "<group>"; };
BF718BD423C928A300A89F2D /* ALTNotificationConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTNotificationConnection.m; sourceTree = "<group>"; };
BF718BD623C92B3700A89F2D /* ALTNotificationConnection+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ALTNotificationConnection+Private.h"; sourceTree = "<group>"; };
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>"; };
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>"; };
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>"; };
BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore2ToAltStore3.xcmappingmodel; sourceTree = "<group>"; };
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>"; };
BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotCollectionViewCell.swift; sourceTree = "<group>"; };
@@ -410,11 +465,17 @@
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; };
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>"; };
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>"; };
BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ALTAnisetteData.m; path = "Dependencies/AltSign/AltSign/Model/Apple API/ALTAnisetteData.m"; sourceTree = SOURCE_ROOT; };
BFB49AA923834CF900D542D9 /* ALTAnisetteData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ALTAnisetteData.h; path = "Dependencies/AltSign/AltSign/Model/Apple API/ALTAnisetteData.h"; sourceTree = SOURCE_ROOT; };
BFB6B21A23186D640022A802 /* NewsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsItem.swift; sourceTree = "<group>"; };
BFB6B21D231870160022A802 /* NewsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsViewController.swift; sourceTree = "<group>"; };
BFB6B21F231870B00022A802 /* NewsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsCollectionViewCell.swift; sourceTree = "<group>"; };
@@ -474,6 +535,7 @@
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>"; };
@@ -484,6 +546,7 @@
BFE338DC22F0E7F3002E24B9 /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = "<group>"; };
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSourceOperation.swift; sourceTree = "<group>"; };
BFE338E722F10E56002E24B9 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
BFE48974238007CE003239E0 /* AnisetteDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnisetteDataManager.swift; sourceTree = "<group>"; };
BFE60737231ADF49002B0E8E /* Settings.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = "<group>"; };
BFE60739231ADF82002B0E8E /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
BFE6073B231AE1E7002B0E8E /* SettingsHeaderFooterView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SettingsHeaderFooterView.xib; sourceTree = "<group>"; };
@@ -493,6 +556,8 @@
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>"; };
@@ -516,6 +581,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
BF4C7F2523801F0800B2556E /* AltSign.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -529,6 +595,14 @@
BF4588472298D4B000BD7491 /* libimobiledevice.a in Frameworks */,
BF0201BD22C2EFBC000B93E4 /* openssl.framework in Frameworks */,
BF0201BA22C2EFA3000B93E4 /* AltSign.framework in Frameworks */,
A8BCEBEAC0620CF80A2FD26D /* Pods_AltServer.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
BF5C5FC2237DF5AE00EDD0C6 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -537,10 +611,7 @@
buildActionMask = 2147483647;
files = (
BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */,
BF4713A522976D1E00784A2F /* openssl.framework in Frameworks */,
BFD247872284BB4200981D42 /* Roxas.framework in Frameworks */,
BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */,
DBAC68F8EC03F4A41D62EDE1 /* Pods_AltStore.framework in Frameworks */,
01100C7036F0EBAC5B30984B /* libPods-AltStore.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -552,6 +623,8 @@
children = (
EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */,
A136EE677716B80768E9F0A2 /* Pods-AltStore.release.xcconfig */,
11611D46F8A7C8B928E8156B /* Pods-AltServer.debug.xcconfig */,
589BA531D903B28F292063E5 /* Pods-AltServer.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -577,6 +650,8 @@
isa = PBXGroup;
children = (
BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */,
BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */,
BF0F5FC623F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel */,
);
path = "Mapping Models";
sourceTree = "<group>";
@@ -585,6 +660,7 @@
isa = PBXGroup;
children = (
BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */,
BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */,
);
path = Policies;
sourceTree = "<group>";
@@ -593,11 +669,14 @@
isa = PBXGroup;
children = (
BFD52BD222A06EFB000B7ED1 /* AltKit.h */,
BF718BD723C93DB700A89F2D /* AltKit.m */,
BFBAC8852295C90300587369 /* Result+Conveniences.swift */,
BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */,
BF1E3128229F474900370A3C /* ServerProtocol.swift */,
BF1E314822A060F400370A3C /* NSError+ALTServerError.h */,
BF1E314922A060F400370A3C /* NSError+ALTServerError.m */,
BF718BC723C919CC00A89F2D /* CFNotificationName+AltStore.h */,
BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */,
);
path = AltKit;
sourceTree = "<group>";
@@ -609,6 +688,8 @@
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */,
BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */,
BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */,
BF26A0DF2370C5D400F53F9F /* ALTSourceUserInfoKey.h */,
BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */,
BF41B807233433C100C593A3 /* LoadingState.swift */,
);
path = Types;
@@ -630,6 +711,7 @@
children = (
BF45868F229872EA00BD7491 /* AppDelegate.swift */,
BF458695229872EA00BD7491 /* Main.storyboard */,
BFE48974238007CE003239E0 /* AnisetteDataManager.swift */,
BF703195229F36FF006E110F /* Devices */,
BFD52BDC22A0A659000B7ED1 /* Connections */,
BF055B4A233B528B0086DEA9 /* Extensions */,
@@ -778,10 +860,34 @@
name = libcnary;
sourceTree = "<group>";
};
BF56D2AD23DF9E170006506D /* App IDs */ = {
isa = PBXGroup;
children = (
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */,
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */,
);
path = "App IDs";
sourceTree = "<group>";
};
BF5C5FC6237DF5AE00EDD0C6 /* AltPlugin */ = {
isa = PBXGroup;
children = (
BF5C5FCD237DF69100EDD0C6 /* ALTPluginService.h */,
BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */,
BFB49AA923834CF900D542D9 /* ALTAnisetteData.h */,
BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */,
BF5C5FC7237DF5AE00EDD0C6 /* Info.plist */,
);
path = AltPlugin;
sourceTree = "<group>";
};
BF703194229F36F6006E110F /* Resources */ = {
isa = PBXGroup;
children = (
BF458693229872EA00BD7491 /* Assets.xcassets */,
BF914C252383703800E713BA /* AltPlugin.mailbundle.zip */,
BF4C7F26238086EB00B2556E /* InstallPlugin.sh */,
BFD80D562380C0F700B9C227 /* UninstallPlugin.sh */,
);
name = Resources;
sourceTree = "<group>";
@@ -843,6 +949,7 @@
children = (
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */,
BF770E5522BC3C02002A40FE /* Server.swift */,
BFA8172823C56042001B5953 /* ServerConnection.swift */,
);
path = Server;
sourceTree = "<group>";
@@ -854,6 +961,7 @@
BF45868E229872EA00BD7491 /* AltServer */,
BF1E315122A0616100370A3C /* AltKit */,
BF45872C2298D31600BD7491 /* libimobiledevice */,
BF5C5FC6237DF5AE00EDD0C6 /* AltPlugin */,
BFD247852284BB3300981D42 /* Frameworks */,
BFD2476B2284B9A500981D42 /* Products */,
4460E048E3AC1C9708C4FA33 /* Pods */,
@@ -867,6 +975,7 @@
BF45868D229872EA00BD7491 /* AltServer.app */,
BF45872B2298D31600BD7491 /* libimobiledevice.a */,
BF1E315022A0616100370A3C /* libAltKit.a */,
BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */,
);
name = Products;
sourceTree = "<group>";
@@ -887,6 +996,7 @@
BFDB69FB22A9A7A6007EA6D6 /* Settings */,
BFD5D6E6230CC94B007955AB /* Patreon */,
BFD2478A2284C49000981D42 /* Managing Apps */,
BF56D2AD23DF9E170006506D /* App IDs */,
BFC51D7922972F1F00388324 /* Server */,
BFD247982284D7FC00981D42 /* Model */,
BFDB6A0922AAEDA1007EA6D6 /* Operations */,
@@ -911,8 +1021,8 @@
BFD247862284BB3B00981D42 /* Roxas.framework */,
BF5AB3A72285FE6C00DC914B /* AltSign.framework */,
BF4713A422976CFC00784A2F /* openssl.framework */,
1039C07E517311FC499A0B64 /* Pods_AltStore.framework */,
FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */,
0DE618FA97EA42C3F468D186 /* libPods-AltStore.a */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -929,6 +1039,7 @@
isa = PBXGroup;
children = (
BFD2478B2284C4C300981D42 /* AppIconImageView.swift */,
BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */,
BFD2478E2284C8F900981D42 /* Button.swift */,
BF43002D22A714AF0051E2BC /* Keychain.swift */,
BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */,
@@ -969,8 +1080,10 @@
BFB11691229322E400BB457C /* DatabaseManager.swift */,
BF3D64A122E8031100E9056B /* MergePolicy.swift */,
BFE6326722A858F300F30809 /* Account.swift */,
BF56D2A923DF88310006506D /* AppID.swift */,
BF3D648722E79A3700E9056B /* AppPermission.swift */,
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
BFA8172C23C5823E001B5953 /* InstalledExtension.swift */,
BFB6B21A23186D640022A802 /* NewsItem.swift */,
BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */,
BF02419322F2156E00129732 /* RefreshAttempt.swift */,
@@ -991,6 +1104,7 @@
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */,
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */,
BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */,
BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -999,6 +1113,13 @@
isa = PBXGroup;
children = (
BF1E3129229F474900370A3C /* ConnectionManager.swift */,
BF9A03C523C7DD0D000D08DB /* ClientConnection.swift */,
BF718BCF23C91BD300A89F2D /* ALTWiredConnection.h */,
BF718BD223C91C7000A89F2D /* ALTWiredConnection+Private.h */,
BF718BD023C91BD300A89F2D /* ALTWiredConnection.m */,
BF718BD323C928A300A89F2D /* ALTNotificationConnection.h */,
BF718BD623C92B3700A89F2D /* ALTNotificationConnection+Private.h */,
BF718BD423C928A300A89F2D /* ALTNotificationConnection.m */,
);
path = Connections;
sourceTree = "<group>";
@@ -1050,11 +1171,14 @@
BF770E5322BC044E002A40FE /* AppOperationContext.swift */,
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */,
BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */,
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
);
path = Operations;
sourceTree = "<group>";
@@ -1065,6 +1189,7 @@
BFE6325922A83BEB00F30809 /* Authentication.storyboard */,
BFF0B6932321CB85007A79E1 /* AuthenticationViewController.swift */,
BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */,
BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */,
);
path = Authentication;
sourceTree = "<group>";
@@ -1141,15 +1266,19 @@
isa = PBXNativeTarget;
buildConfigurationList = BF45869A229872EA00BD7491 /* Build configuration list for PBXNativeTarget "AltServer" */;
buildPhases = (
FACBF95CCAAAB7121E1D92C8 /* [CP] Check Pods Manifest.lock */,
BF458689229872EA00BD7491 /* Sources */,
BF45868B229872EA00BD7491 /* Resources */,
BF4588462298D4AA00BD7491 /* Frameworks */,
BF0201BC22C2EFA3000B93E4 /* Embed Frameworks */,
BF7FDA2C23203B6B00B5D3A4 /* Copy Launcher App */,
98BF22D155DBAEA97544E3E6 /* [CP] Embed Pods Frameworks */,
BF914C242383659400E713BA /* Sign Frameworks */,
);
buildRules = (
);
dependencies = (
BFBFFB272380C72F00993A4A /* PBXTargetDependency */,
BF1E315E22A0621F00370A3C /* PBXTargetDependency */,
BF4588452298D48B00BD7491 /* PBXTargetDependency */,
);
@@ -1175,6 +1304,24 @@
productReference = BF45872B2298D31600BD7491 /* libimobiledevice.a */;
productType = "com.apple.product-type.library.static";
};
BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */ = {
isa = PBXNativeTarget;
buildConfigurationList = BF5C5FC8237DF5AE00EDD0C6 /* Build configuration list for PBXNativeTarget "AltPlugin" */;
buildPhases = (
BF5C5FC1237DF5AE00EDD0C6 /* Sources */,
BF5C5FC2237DF5AE00EDD0C6 /* Frameworks */,
BF5C5FC3237DF5AE00EDD0C6 /* Resources */,
BF5C5FE9237E438C00EDD0C6 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = AltPlugin;
productName = AltPlugin;
productReference = BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */;
productType = "com.apple.product-type.bundle";
};
BFD247692284B9A500981D42 /* AltStore */ = {
isa = PBXNativeTarget;
buildConfigurationList = BFD2477E2284B9A700981D42 /* Build configuration list for PBXNativeTarget "AltStore" */;
@@ -1183,8 +1330,7 @@
BFD247662284B9A500981D42 /* Sources */,
BFD247672284B9A500981D42 /* Frameworks */,
BFD247682284B9A500981D42 /* Resources */,
BFD247842284BB2C00981D42 /* Embed Frameworks */,
B8F37E08B55D2C9C4E2B1B4E /* [CP] Embed Pods Frameworks */,
8C9013C41DD92A1476195C0E /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -1202,7 +1348,7 @@
BFD247622284B9A500981D42 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1020;
LastSwiftUpdateCheck = 1120;
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "Riley Testut";
TargetAttributes = {
@@ -1224,6 +1370,10 @@
BF45872A2298D31600BD7491 = {
CreatedOnToolsVersion = 10.2.1;
};
BF5C5FC4237DF5AE00EDD0C6 = {
CreatedOnToolsVersion = 11.2;
LastSwiftMigration = 1120;
};
BFD247692284B9A500981D42 = {
CreatedOnToolsVersion = 10.2.1;
LastSwiftMigration = 1020;
@@ -1255,6 +1405,7 @@
BF45868C229872EA00BD7491 /* AltServer */,
BF1E314F22A0616100370A3C /* AltKit */,
BF45872A2298D31600BD7491 /* libimobiledevice */,
BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */,
);
};
/* End PBXProject section */
@@ -1264,8 +1415,18 @@
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;
};
BF5C5FC3237DF5AE00EDD0C6 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1291,28 +1452,38 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
B8F37E08B55D2C9C4E2B1B4E /* [CP] Embed Pods Frameworks */ = {
8C9013C41DD92A1476195C0E /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework",
"${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework",
);
name = "[CP] Embed Pods Frameworks";
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
);
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nuke.framework",
"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-frameworks.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-resources.sh\"\n";
showEnvVarsInLog = 0;
};
98BF22D155DBAEA97544E3E6 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-AltServer/Pods-AltServer-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-AltServer/Pods-AltServer-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AltServer/Pods-AltServer-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
BF7FDA2C23203B6B00B5D3A4 /* Copy Launcher App */ = {
@@ -1333,6 +1504,46 @@
shellPath = /bin/sh;
shellScript = "\"${PROJECT_DIR}/Carthage/Build/Mac/LaunchAtLogin.framework/Resources/copy-helper.sh\"\n";
};
BF914C242383659400E713BA /* Sign Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Sign Frameworks";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "LOCATION=\"${BUILT_PRODUCTS_DIR}\"/\"${FRAMEWORKS_FOLDER_PATH}\"\nIDENTITY=${EXPANDED_CODE_SIGN_IDENTITY_NAME}\n\ncodesign --verbose --force --deep -o runtime --sign \"$IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A/Resources/AutoUpdate.app\"\ncodesign --verbose --force -o runtime --sign \"$IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A\"\n";
};
FACBF95CCAAAB7121E1D92C8 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-AltServer-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
FFB93342C7EB2021A1FFFB6A /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -1362,9 +1573,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BF718BD823C93DB700A89F2D /* AltKit.m in Sources */,
BF1E315A22A0620000370A3C /* NSError+ALTServerError.m in Sources */,
BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */,
BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */,
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */,
BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1374,10 +1587,14 @@
buildActionMask = 2147483647;
files = (
BF3F786422CAA41E008FBD20 /* ALTDeviceManager+Installation.swift in Sources */,
BF718BD523C928A300A89F2D /* ALTNotificationConnection.m in Sources */,
BF1E312B229F474900370A3C /* ConnectionManager.swift in Sources */,
BF9A03C623C7DD0D000D08DB /* ClientConnection.swift in Sources */,
BF718BD123C91BD300A89F2D /* ALTWiredConnection.m in Sources */,
BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */,
BF4586C52298CDB800BD7491 /* ALTDeviceManager.mm in Sources */,
BF0241AA22F29CCD00129732 /* UserDefaults+AltServer.swift in Sources */,
BFE48975238007CE003239E0 /* AnisetteDataManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1442,16 +1659,29 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
BF5C5FC1237DF5AE00EDD0C6 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BFB49AAA23834CF900D542D9 /* ALTAnisetteData.m in Sources */,
BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
BFD247662284B9A500981D42 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
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 */,
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
@@ -1463,6 +1693,7 @@
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 */,
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
@@ -1474,6 +1705,7 @@
BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */,
BFE6326822A858F300F30809 /* Account.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 */,
@@ -1482,9 +1714,12 @@
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */,
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */,
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */,
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */,
@@ -1493,11 +1728,14 @@
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */,
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 */,
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 */,
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */,
@@ -1505,6 +1743,7 @@
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
BF3D64A222E8031100E9056B /* MergePolicy.swift in Sources */,
BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
@@ -1522,6 +1761,8 @@
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */,
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
BF770E5622BC3C03002A40FE /* Server.swift in Sources */,
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */,
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */,
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1544,6 +1785,11 @@
target = BF45872A2298D31600BD7491 /* libimobiledevice */;
targetProxy = BF4588442298D48B00BD7491 /* PBXContainerItemProxy */;
};
BFBFFB272380C72F00993A4A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */;
targetProxy = BFBFFB262380C72F00993A4A /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -1610,12 +1856,15 @@
};
BF45869B229872EA00BD7491 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 11611D46F8A7C8B928E8156B /* Pods-AltServer.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = AltServer/AltServer.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_TEAM = 6XVY5G3U44;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
@@ -1648,6 +1897,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14.4;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
@@ -1659,12 +1909,15 @@
};
BF45869C229872EA00BD7491 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 589BA531D903B28F292063E5 /* Pods-AltServer.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = AltServer/AltServer.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_TEAM = 6XVY5G3U44;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
@@ -1697,6 +1950,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14.4;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
@@ -1779,6 +2033,63 @@
};
name = Release;
};
BF5C5FC9237DF5AE00EDD0C6 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = AltPlugin/Info.plist;
INSTALL_PATH = "$(HOME)/Library/Mail/Bundles/";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
"/Users/Riley/Library/Developer/Xcode/DerivedData/AltStore-bhqnkrahfutztzeudtxhcxccgtlo/Build/Products/Debug",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltPlugin;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
WRAPPER_EXTENSION = mailbundle;
};
name = Debug;
};
BF5C5FCA237DF5AE00EDD0C6 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = AltPlugin/Info.plist;
INSTALL_PATH = "$(HOME)/Library/Mail/Bundles/";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
"/Users/Riley/Library/Developer/Xcode/DerivedData/AltStore-bhqnkrahfutztzeudtxhcxccgtlo/Build/Products/Debug",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltPlugin;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
WRAPPER_EXTENSION = mailbundle;
};
name = Release;
};
BFD2477C2284B9A700981D42 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -1913,6 +2224,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1939,6 +2251,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1978,6 +2291,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
BF5C5FC8237DF5AE00EDD0C6 /* Build configuration list for PBXNativeTarget "AltPlugin" */ = {
isa = XCConfigurationList;
buildConfigurations = (
BF5C5FC9237DF5AE00EDD0C6 /* Debug */,
BF5C5FCA237DF5AE00EDD0C6 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
BFD247652284B9A500981D42 /* Build configuration list for PBXProject "AltStore" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -2002,10 +2324,12 @@
BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */,
BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */,
BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */,
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */,
);
currentVersion = BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */;
currentVersion = BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */;
path = AltStore.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
BuildableName = "libAltKit.a"
BlueprintName = "AltKit"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
BuildableName = "libAltKit.a"
BlueprintName = "AltKit"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1120"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "AltPlugin.mailbundle"
BlueprintName = "AltPlugin"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "1"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "AltPlugin.mailbundle"
BlueprintName = "AltPlugin"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -2,6 +2,8 @@
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "NSError+ALTServerError.h"
#import "AltKit.h"
#import "ALTAppPermission.h"
#import "ALTPatreonBenefitType.h"
#import "ALTSourceUserInfoKey.h"

View File

@@ -73,6 +73,8 @@ class AppContentViewController: UITableViewController
self.tableView.contentInset.bottom = 20
self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
self.screenshotsCollectionView.prefetchDataSource = self.screenshotsDataSource
self.permissionsCollectionView.dataSource = self.permissionsDataSource
self.subtitleLabel.text = self.app.subtitle

View File

@@ -27,18 +27,11 @@ class AppViewController: UIViewController
@IBOutlet private var scrollView: UIScrollView!
@IBOutlet private var contentView: UIView!
@IBOutlet private var headerView: UIView!
@IBOutlet private var headerContentView: UIView!
@IBOutlet private var bannerView: AppBannerView!
@IBOutlet private var backButton: UIButton!
@IBOutlet private var backButtonContainerView: UIVisualEffectView!
@IBOutlet private var nameLabel: UILabel!
@IBOutlet private var developerLabel: UILabel!
@IBOutlet private var downloadButton: PillButton!
@IBOutlet private var appIconImageView: UIImageView!
@IBOutlet private var betaBadgeView: UIImageView!
@IBOutlet private var backgroundAppIconImageView: UIImageView!
@IBOutlet private var backgroundBlurView: UIVisualEffectView!
@@ -51,6 +44,12 @@ class AppViewController: UIViewController
private var _backgroundBlurEffect: UIBlurEffect?
private var _backgroundBlurTintColor: UIColor?
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
override var preferredStatusBarStyle: UIStatusBarStyle {
return _preferredStatusBarStyle
}
override func viewDidLoad()
{
super.viewDidLoad()
@@ -75,21 +74,22 @@ class AppViewController: UIViewController
self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
self.contentViewController.tableView.showsVerticalScrollIndicator = false
self.headerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
self.headerView.layer.cornerRadius = 24
self.headerView.layer.masksToBounds = true
// Bring to front so the scroll indicators are visible.
self.view.bringSubviewToFront(self.scrollView)
self.scrollView.isUserInteractionEnabled = false
self.nameLabel.text = self.app.name
self.developerLabel.text = self.app.developerName
self.developerLabel.textColor = self.app.tintColor
self.appIconImageView.image = nil
self.appIconImageView.tintColor = self.app.tintColor
self.downloadButton.tintColor = self.app.tintColor
self.betaBadgeView.isHidden = !self.app.isBeta
self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
self.bannerView.backgroundEffectView.backgroundColor = .clear
self.bannerView.titleLabel.text = self.app.name
self.bannerView.subtitleLabel.text = self.app.developerName
self.bannerView.iconImageView.image = nil
self.bannerView.iconImageView.tintColor = self.app.tintColor
self.bannerView.button.tintColor = self.app.tintColor
self.bannerView.betaBadgeView.isHidden = !self.app.isBeta
self.bannerView.tintColor = self.app.tintColor
self.bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
self.backButtonContainerView.tintColor = self.app.tintColor
@@ -107,12 +107,13 @@ class AppViewController: UIViewController
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didChangeApp(_:)), name: .NSManagedObjectContextObjectsDidChange, object: DatabaseManager.shared.viewContext)
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
// Load Images
for imageView in [self.appIconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
for imageView in [self.bannerView.iconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
{
imageView.isIndicatingActivity = true
@@ -219,7 +220,7 @@ class AppViewController: UIViewController
var backButtonFrame = CGRect(x: inset, y: statusBarHeight,
width: backButtonSize.width + 20, height: backButtonSize.height + 20)
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.headerView.bounds.height)
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.bannerView.bounds.height)
var contentFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
var backgroundIconFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width)
@@ -305,12 +306,11 @@ class AppViewController: UIViewController
// Set frames.
self.contentViewController.view.superview?.frame = contentFrame
self.headerView.frame = headerFrame
self.bannerView.frame = headerFrame
self.backgroundAppIconImageView.frame = backgroundIconFrame
self.backgroundBlurView.frame = backgroundIconFrame
self.backButtonContainerView.frame = backButtonFrame
self.headerContentView.frame = CGRect(x: 0, y: 0, width: self.headerView.bounds.width, height: self.headerView.bounds.height)
self.contentViewControllerShadowView.frame = self.contentViewController.view.frame
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
@@ -325,6 +325,14 @@ class AppViewController: UIViewController
self.scrollView.contentSize = contentSize
self.scrollView.contentOffset = contentOffset
self.bannerView.backgroundEffectView.backgroundColor = .clear
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
{
super.traitCollectionDidChange(previousTraitCollection)
self._shouldResetLayout = true
}
deinit
@@ -350,7 +358,7 @@ private extension AppViewController
{
func update()
{
for button in [self.downloadButton!, self.navigationBarDownloadButton!]
for button in [self.bannerView.button!, self.navigationBarDownloadButton!]
{
button.tintColor = self.app.tintColor
button.isIndicatingActivity = false
@@ -358,12 +366,10 @@ private extension AppViewController
if self.app.installedApp == nil
{
button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
button.isInverted = false
}
else
{
button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
button.isInverted = true
}
let progress = AppManager.shared.installationProgress(for: self.app)
@@ -372,12 +378,12 @@ private extension AppViewController
if Date() < self.app.versionDate
{
self.downloadButton.countdownDate = self.app.versionDate
self.bannerView.button.countdownDate = self.app.versionDate
self.navigationBarDownloadButton.countdownDate = self.app.versionDate
}
else
{
self.downloadButton.countdownDate = nil
self.bannerView.button.countdownDate = nil
self.navigationBarDownloadButton.countdownDate = nil
}
@@ -389,18 +395,29 @@ private extension AppViewController
func showNavigationBar(for navigationController: UINavigationController? = nil)
{
let navigationController = navigationController ?? self.navigationController
navigationController?.navigationBar.barStyle = .default
navigationController?.navigationBar.alpha = 1.0
navigationController?.navigationBar.barTintColor = .white
navigationController?.navigationBar.tintColor = .altPrimary
navigationController?.navigationBar.setNeedsLayout()
if self.traitCollection.userInterfaceStyle == .dark
{
self._preferredStatusBarStyle = .lightContent
}
else
{
self._preferredStatusBarStyle = .default
}
navigationController?.setNeedsStatusBarAppearanceUpdate()
}
func hideNavigationBar(for navigationController: UINavigationController? = nil)
{
let navigationController = navigationController ?? self.navigationController
navigationController?.navigationBar.barStyle = .black
navigationController?.navigationBar.alpha = 0.0
navigationController?.navigationBar.barTintColor = .white
self._preferredStatusBarStyle = .lightContent
navigationController?.setNeedsStatusBarAppearanceUpdate()
}
func prepareBlur()
@@ -445,7 +462,7 @@ private extension AppViewController
self.navigationBarAnimator = nil
self.hideNavigationBar()
self.navigationController?.navigationBar.barTintColor = .white
self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
}
}
@@ -485,18 +502,20 @@ extension AppViewController
catch
{
DispatchQueue.main.async {
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)
}
}
DispatchQueue.main.async {
self.downloadButton.progress = nil
self.bannerView.button.progress = nil
self.navigationBarDownloadButton.progress = nil
self.update()
}
}
self.downloadButton.progress = progress
self.bannerView.button.progress = progress
self.navigationBarDownloadButton.progress = progress
}
func open(_ installedApp: InstalledApp)
@@ -522,6 +541,15 @@ private extension AppViewController
self._shouldResetLayout = true
self.view.setNeedsLayout()
}
@objc func didBecomeActive(_ notification: Notification)
{
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
// Fixes Navigation Bar appearing after app becomes inactive -> active again.
self._shouldResetLayout = true
self.view.setNeedsLayout()
}
}
extension AppViewController: UIScrollViewDelegate

View File

@@ -0,0 +1,30 @@
//
// AppIDComponents.swift
// AltStore
//
// Created by Riley Testut on 2/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
class AppIDCollectionViewCell: UICollectionViewCell
{
@IBOutlet var bannerView: AppBannerView!
override func awakeFromNib()
{
super.awakeFromNib()
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,225 @@
//
// AppIDsViewController.swift
// AltStore
//
// Created by Riley Testut on 1/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class AppIDsViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
private var didInitialFetch = false
private var isLoading = false {
didSet {
self.update()
}
}
@IBOutlet var activityIndicatorBarButtonItem: UIBarButtonItem!
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView.dataSource = self.dataSource
self.activityIndicatorBarButtonItem.isIndicatingActivity = true
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(AppIDsViewController.fetchAppIDs), for: .primaryActionTriggered)
self.collectionView.refreshControl = refreshControl
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if !self.didInitialFetch
{
self.fetchAppIDs()
}
}
}
private extension AppIDsViewController
{
func makeDataSource() -> RSTFetchedResultsCollectionViewDataSource<AppID>
{
let fetchRequest = AppID.fetchRequest() as NSFetchRequest<AppID>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AppID.name, ascending: true),
NSSortDescriptor(keyPath: \AppID.bundleIdentifier, ascending: true),
NSSortDescriptor(keyPath: \AppID.expirationDate, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
if let team = DatabaseManager.shared.activeTeam()
{
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(AppID.team), team)
}
else
{
fetchRequest.predicate = NSPredicate(value: false)
}
let dataSource = RSTFetchedResultsCollectionViewDataSource<AppID>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.proxy = self
dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in
let tintColor = UIColor.altPrimary
let cell = cell as! AppIDCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = tintColor
cell.bannerView.iconImageView.isHidden = true
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.betaBadgeView.isHidden = true
if let expirationDate = appID.expirationDate
{
cell.bannerView.button.isHidden = false
cell.bannerView.button.isUserInteractionEnabled = false
cell.bannerView.buttonLabel.isHidden = false
let currentDate = Date()
let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate)
if numberOfDays == 1
{
cell.bannerView.button.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal)
}
else
{
cell.bannerView.button.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal)
}
}
else
{
cell.bannerView.button.isHidden = true
cell.bannerView.buttonLabel.isHidden = true
}
cell.bannerView.titleLabel.text = appID.name
cell.bannerView.subtitleLabel.text = appID.bundleIdentifier
cell.bannerView.subtitleLabel.numberOfLines = 2
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
}
return dataSource
}
@objc func fetchAppIDs()
{
guard !self.isLoading else { return }
self.isLoading = true
AppManager.shared.fetchAppIDs { (result) in
do
{
let (_, context) = try result.get()
try context.save()
}
catch
{
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
DispatchQueue.main.async {
self.isLoading = false
}
}
}
func update()
{
if !self.isLoading
{
self.collectionView.refreshControl?.endRefreshing()
self.activityIndicatorBarButtonItem.isIndicatingActivity = false
}
}
}
extension AppIDsViewController: 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
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
{
return CGSize(width: collectionView.bounds.width, height: 50)
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
switch kind
{
case UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! AppIDsCollectionReusableView
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 = """
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.
"""
}
else
{
headerView.textLabel.text = """
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.
"""
}
return headerView
case UICollectionView.elementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! AppIDsCollectionReusableView
let count = self.dataSource.itemCount
if count == 1
{
footerView.textLabel.text = NSLocalizedString("1 App ID", comment: "")
}
else
{
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs", comment: ""), NSNumber(value: count))
}
return footerView
default: fatalError()
}
}
}

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="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15703"/>
<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"/>
@@ -17,7 +17,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="Primary"/>
<color key="barTintColor" name="SettingsBackground"/>
<textAttributes key="titleTextAttributes">
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textAttributes>
@@ -51,7 +51,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
<rect key="frame" x="16" y="6" width="343" height="397"/>
<rect key="frame" x="16" y="6" width="343" height="359.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome">
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
@@ -65,16 +65,16 @@
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
<rect key="frame" x="0.0" y="47" width="308.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf">
<rect key="frame" x="0.0" y="117.5" width="343" height="279.5"/>
<rect key="frame" x="0.0" y="117.5" width="343" height="242"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7">
<rect key="frame" x="0.0" y="0.0" width="343" height="196.5"/>
<rect key="frame" x="0.0" y="0.0" width="343" height="159"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID">
<rect key="frame" x="0.0" y="0.0" width="343" height="72"/>
@@ -118,7 +118,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password">
<rect key="frame" x="0.0" y="87" width="343" height="109.5"/>
<rect key="frame" x="0.0" y="87" width="343" height="72"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95">
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
@@ -156,31 +156,19 @@
</constraints>
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="14"/>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Glz-dw-2Eg">
<rect key="frame" x="0.0" y="76" width="343" height="33.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="If you used an app-specific password to install AltStore, please use that same password again." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="a51-OQ-f3j">
<rect key="frame" x="14" y="0.0" width="315" height="33.5"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="14"/>
</stackView>
</subviews>
</stackView>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
<rect key="frame" x="0.0" y="228.5" width="343" height="51"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<rect key="frame" x="0.0" y="191" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
<state key="normal" title="Sign in">
<color key="titleColor" name="Pink"/>
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="authenticate" destination="yO1-iT-7NP" eventType="primaryActionTriggered" id="LER-a2-CbC"/>
@@ -222,7 +210,7 @@
</constraints>
</scrollView>
</subviews>
<color key="backgroundColor" name="Primary"/>
<color key="backgroundColor" name="SettingsBackground"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
<constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/>
@@ -407,20 +395,20 @@
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
<rect key="frame" x="16" y="608" width="343" height="51"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
<state key="normal" title="Got it">
<color key="titleColor" name="Pink"/>
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="dismiss" destination="aFi-fb-W0B" eventType="primaryActionTriggered" id="sBq-zj-Mln"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" name="Primary"/>
<color key="backgroundColor" name="SettingsBackground"/>
<constraints>
<constraint firstItem="qZ9-AR-2zK" firstAttribute="top" secondItem="bp6-55-IG2" secondAttribute="bottom" id="3yt-cr-swd"/>
<constraint firstItem="bp6-55-IG2" firstAttribute="top" secondItem="Zek-aC-HOO" secondAttribute="top" id="42S-q2-YZn"/>
@@ -443,14 +431,78 @@
</objects>
<point key="canvasLocation" x="1353" y="736"/>
</scene>
<!--Refresh AltStore-->
<scene sceneID="9Vh-dM-OqX">
<objects>
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
<rect key="frame" x="16" y="570" width="343" height="89"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
<constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
<color key="tintColor" name="SettingsHighlighted"/>
<state key="normal" title="Refresh Now">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="refreshAltStore:" destination="aoK-yE-UVT" eventType="primaryActionTriggered" id="WQu-9b-Zgg"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
<rect key="frame" x="0.0" y="59" width="343" height="30"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" title="Refresh Later">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="cancel:" destination="aoK-yE-UVT" eventType="primaryActionTriggered" id="ffO-0a-LdE"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" name="SettingsBackground"/>
<constraints>
<constraint firstItem="fpO-Bf-gFY" firstAttribute="leading" secondItem="iwE-xE-ziz" secondAttribute="leading" id="A77-nX-Wg2"/>
<constraint firstAttribute="trailingMargin" secondItem="tDQ-ao-1Jg" secondAttribute="trailing" id="KPg-sO-Rnc"/>
<constraint firstItem="fpO-Bf-gFY" firstAttribute="trailing" secondItem="iwE-xE-ziz" secondAttribute="trailing" id="SGI-1D-Eaw"/>
<constraint firstItem="fpO-Bf-gFY" firstAttribute="bottom" secondItem="R83-kV-365" secondAttribute="bottom" id="cHl-7X-dW1"/>
<constraint firstAttribute="bottomMargin" secondItem="tDQ-ao-1Jg" secondAttribute="bottom" id="kLN-e7-BJE"/>
<constraint firstItem="fpO-Bf-gFY" firstAttribute="top" secondItem="R83-kV-365" secondAttribute="top" id="oKo-10-7kD"/>
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
</constraints>
<viewLayoutGuide key="safeArea" id="iwE-xE-ziz"/>
</view>
<navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Chr-7g-qEw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2101.5999999999999" y="733.5832083958021"/>
</scene>
</scenes>
<resources>
<namedColor name="Pink">
<color red="0.92549019607843142" green="0.25490196078431371" blue="0.69803921568627447" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Primary">
<namedColor name="SettingsBackground">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SettingsHighlighted">
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
<color key="tintColor" name="Primary"/>
</document>

View File

@@ -12,7 +12,8 @@ import AltSign
class AuthenticationViewController: UIViewController
{
var authenticationHandler: (((ALTAccount, String)?) -> Void)?
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
private weak var toastView: ToastView?
@@ -30,6 +31,8 @@ class AuthenticationViewController: UIViewController
{
super.viewDidLoad()
self.signInButton.activityIndicatorView.style = .white
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
{
view.clipsToBounds = true
@@ -94,14 +97,16 @@ private extension AuthenticationViewController
self.signInButton.isIndicatingActivity = true
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in
do
{
let account = try Result(account, error).get()
self.authenticationHandler?((account, password))
}
catch
self.authenticationHandler?(emailAddress, password) { (result) in
switch result
{
case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
DispatchQueue.main.async {
self.signInButton.isIndicatingActivity = false
}
case .failure(let error):
DispatchQueue.main.async {
let toastView = ToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription)
toastView.textLabel.textColor = .altPink
@@ -111,6 +116,9 @@ private extension AuthenticationViewController
self.signInButton.isIndicatingActivity = false
}
case .success(let account, let session):
self.completionHandler?((account, session, password))
}
DispatchQueue.main.async {
@@ -121,7 +129,7 @@ private extension AuthenticationViewController
@IBAction func cancel(_ sender: UIBarButtonItem)
{
self.authenticationHandler?(nil)
self.completionHandler?(nil)
}
}

View File

@@ -17,6 +17,10 @@ class InstructionsViewController: UIViewController
@IBOutlet private var contentStackView: UIStackView!
@IBOutlet private var dismissButton: UIButton!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidLoad()
{
super.viewDidLoad()

View File

@@ -0,0 +1,89 @@
//
// RefreshAltStoreViewController.swift
// AltStore
//
// Created by Riley Testut on 10/26/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltSign
import Roxas
class RefreshAltStoreViewController: UIViewController
{
var signer: ALTSigner!
var session: ALTAppleAPISession!
var completionHandler: ((Result<Void, Error>) -> Void)?
@IBOutlet private var placeholderView: RSTPlaceholderView!
override func viewDidLoad()
{
super.viewDidLoad()
self.placeholderView.textLabel.isHidden = true
self.placeholderView.detailTextLabel.textAlignment = .left
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
self.placeholderView.detailTextLabel.text = NSLocalizedString("AltStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including AltStore.\n\nTo prevent AltStore from expiring early, please refresh the app now. AltStore will quit once refreshing is complete.", comment: "")
}
}
private extension RefreshAltStoreViewController
{
@IBAction func refreshAltStore(_ sender: PillButton)
{
guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return }
func refresh()
{
sender.isIndicatingActivity = true
if let progress = AppManager.shared.refreshProgress(for: altStore) ?? AppManager.shared.installationProgress(for: altStore)
{
// Cancel pending AltStore refresh 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
{
DispatchQueue.main.async {
sender.progress = nil
sender.isIndicatingActivity = false
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh AltStore", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { (action) in
refresh()
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { (action) in
self.completionHandler?(.failure(error))
}))
self.present(alertController, animated: true, completion: nil)
}
}
else
{
self.completionHandler?(.success(()))
}
}
_ = AppManager.shared.refresh([altStore], presentingViewController: self, group: group)
sender.progress = group.progress
}
refresh()
}
@IBAction func cancel(_ sender: UIButton)
{
self.completionHandler?(.failure(OperationError.cancelled))
}
}

View File

@@ -1,7 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="6NO-wl-tj1">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<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"/>
</dependencies>
@@ -11,15 +14,38 @@
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" name="Background"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
<tabBarItem key="tabBarItem" title="" id="RiK-sx-Kgv"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
<point key="canvasLocation" x="962.31884057971024" y="375"/>
</scene>
<!--Tab Bar Controller-->
<scene sceneID="9Yy-ze-Trt">
<objects>
<tabBarController automaticallyAdjustsScrollViewInsets="NO" id="6NO-wl-tj1" sceneMemberID="viewController">
<toolbarItems/>
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="4lc-l2-vDf">
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<connections>
<segue destination="01J-lp-oVM" kind="relationship" relationship="viewControllers" id="2qH-aa-n0z"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="pxX-hL-ovw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="52.173913043478265" y="375"/>
</scene>
</scenes>
<resources>
<namedColor name="Background">
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@@ -1,12 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14868" 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="15705" 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="14824"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -20,9 +19,6 @@
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<viewLayoutGuide key="safeArea" id="sZd-sc-Bvn"/>
</view>
<connections>
<segue destination="49e-Tb-3d3" kind="presentation" identifier="finishLaunching" modalTransitionStyle="crossDissolve" id="6Ov-Kc-Van"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="vOq-mm-rY5" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
@@ -31,7 +27,7 @@
<!--Tab Bar Controller-->
<scene sceneID="yl2-sM-qoP">
<objects>
<tabBarController modalPresentationStyle="fullScreen" id="49e-Tb-3d3" customClass="TabBarController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" id="49e-Tb-3d3" customClass="TabBarController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
<rect key="frame" x="0.0" y="975" width="768" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
@@ -55,12 +51,12 @@
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="10" id="e0H-IH-rng">
<color key="backgroundColor" name="Background"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="50" minimumInteritemSpacing="10" id="e0H-IH-rng">
<size key="itemSize" width="375" height="400"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
<inset key="sectionInset" minX="0.0" minY="8" maxX="0.0" maxY="20"/>
</collectionViewFlowLayout>
<cells/>
<connections>
@@ -110,10 +106,9 @@
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" 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"/>
<color key="backgroundColor" white="0.0" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<blurEffect style="light"/>
<blurEffect style="regular"/>
</visualEffectView>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" contentInsetAdjustmentBehavior="never" translatesAutoresizingMaskIntoConstraints="NO" id="Ci9-Iw-aR2">
<rect key="frame" x="0.0" y="0.0" width="375" height="618"/>
@@ -124,69 +119,10 @@
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="mgO-eN-SxQ">
<rect key="frame" x="38" y="287" width="300" height="93"/>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="37" y="287" width="300" height="93"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="yIo-bR-OBC">
<rect key="frame" x="0.0" y="0.0" width="300" height="93"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacingType="standard" id="LZw-eU-5SO" userLabel="App Info">
<rect key="frame" x="0.0" y="0.0" width="300" height="93"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="3Ey-6S-HJx" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="14" y="14" width="65" height="65"/>
<constraints>
<constraint firstAttribute="height" constant="65" id="AIz-49-Wuj"/>
<constraint firstAttribute="width" secondItem="3Ey-6S-HJx" secondAttribute="height" multiplier="1:1" id="GCk-a1-dDk"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="bR7-SO-m8f">
<rect key="frame" x="87" y="26.5" width="121" height="40.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="9z7-I4-q6g">
<rect key="frame" x="0.0" y="0.0" width="121" height="21.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="dNE-IO-y3o">
<rect key="frame" x="0.0" y="0.0" width="74" height="21.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="2XC-Fe-yG4">
<rect key="frame" x="80" y="0.0" width="41" height="21.5"/>
</imageView>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NKT-el-rRF">
<rect key="frame" x="0.0" y="23.5" width="66" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mgB-Gs-bik" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="216" y="31" width="72" height="31"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="72" id="j44-T1-0dc"/>
<constraint firstAttribute="height" constant="31" id="qY2-Ng-KJy"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<state key="normal" title="FREE"/>
<connections>
<action selector="performAppAction:" destination="0V6-N4-hTO" eventType="primaryActionTriggered" id="wPd-Kn-6fI"/>
</connections>
</button>
</subviews>
<edgeInsets key="layoutMargins" top="14" left="14" bottom="14" right="12"/>
</stackView>
</subviews>
</view>
<blurEffect style="extraLight"/>
</visualEffectView>
</view>
<containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FIv-I9-5uW">
<rect key="frame" x="0.0" y="450" width="375" height="217"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
@@ -201,22 +137,35 @@
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mkD-3C-WMV">
<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"/>
<state key="normal" image="Back"/>
<connections>
<action selector="popViewController:" destination="0V6-N4-hTO" eventType="primaryActionTriggered" id="F6Z-xz-qCk"/>
</connections>
</button>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" 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>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mkD-3C-WMV">
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<state key="normal" image="Back"/>
<connections>
<action selector="popViewController:" destination="0V6-N4-hTO" eventType="primaryActionTriggered" id="F6Z-xz-qCk"/>
</connections>
</button>
</subviews>
</view>
<vibrancyEffect style="fill">
<blurEffect style="prominent"/>
</vibrancyEffect>
</visualEffectView>
</subviews>
</view>
<blurEffect style="extraLight"/>
<blurEffect style="prominent"/>
</visualEffectView>
</subviews>
</view>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" name="Background"/>
<constraints>
<constraint firstItem="Ci9-Iw-aR2" firstAttribute="top" secondItem="0cR-li-tCB" secondAttribute="top" id="015-fz-v3B"/>
<constraint firstAttribute="top" secondItem="Qlg-m3-lXg" secondAttribute="top" id="8tb-sY-MOu"/>
@@ -248,18 +197,12 @@
</barButtonItem>
</navigationItem>
<connections>
<outlet property="appIconImageView" destination="3Ey-6S-HJx" id="5FB-mn-E29"/>
<outlet property="backButton" destination="mkD-3C-WMV" id="3m8-P7-yvT"/>
<outlet property="backButtonContainerView" destination="tUK-0J-07U" id="POZ-dP-f12"/>
<outlet property="backgroundAppIconImageView" destination="CUB-SN-zdM" id="dFx-py-yMm"/>
<outlet property="backgroundBlurView" destination="8Tg-wk-r0u" id="B8c-ng-nI5"/>
<outlet property="betaBadgeView" destination="2XC-Fe-yG4" id="FCf-t9-Aab"/>
<outlet property="bannerView" destination="NEy-yr-cLS" id="MTr-hK-LIR"/>
<outlet property="contentView" destination="Qlg-m3-lXg" id="JhH-hh-vBN"/>
<outlet property="developerLabel" destination="NKT-el-rRF" id="GUc-jy-kvv"/>
<outlet property="downloadButton" destination="mgB-Gs-bik" id="x95-gu-NBy"/>
<outlet property="headerContentView" destination="LZw-eU-5SO" id="hk1-xG-2kJ"/>
<outlet property="headerView" destination="mgO-eN-SxQ" id="iIi-D7-XRt"/>
<outlet property="nameLabel" destination="dNE-IO-y3o" id="tp1-IT-ByH"/>
<outlet property="navigationBarAppIconImageView" destination="j1W-Jn-HFI" id="2YU-ka-w9R"/>
<outlet property="navigationBarAppNameLabel" destination="DTD-1Y-76c" id="z9z-pp-dC4"/>
<outlet property="navigationBarDownloadButton" destination="grk-xM-YWA" id="Yrg-S0-tIM"/>
@@ -278,7 +221,7 @@
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" name="Background"/>
<sections>
<tableViewSection id="rfR-32-T0h">
<cells>
@@ -303,6 +246,7 @@
<constraint firstItem="BsL-O2-UjD" firstAttribute="top" secondItem="8PX-jQ-nHd" secondAttribute="top" constant="20" id="dRc-WY-Jbk"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="nI6-wC-H2d">
@@ -314,7 +258,7 @@
<subviews>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="ppk-lL-at8">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" name="Background"/>
<collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="15" id="ace-Ns-Jd2">
<size key="itemSize" width="189" height="406"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
@@ -340,6 +284,7 @@
<constraint firstItem="ppk-lL-at8" firstAttribute="top" secondItem="Z4y-vb-Z4Q" secondAttribute="top" id="xY5-w8-roA"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="EL5-UC-RIw" customClass="AppContentTableViewCell" customModule="AltStore" customModuleProvider="target">
@@ -351,7 +296,7 @@
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="20" y="20" width="335" height="34"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" name="Background"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
@@ -363,6 +308,7 @@
<constraint firstAttribute="trailing" secondItem="Pyt-8D-BZA" secondAttribute="trailing" constant="20" id="Wq4-Ql-wvN"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="47M-El-a4G" customClass="AppContentTableViewCell" customModule="AltStore" customModuleProvider="target">
@@ -416,7 +362,7 @@
</stackView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="20" y="16" width="335" height="0.0"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" name="Background"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
@@ -431,6 +377,7 @@
<constraint firstAttribute="bottom" secondItem="n9R-39-Glq" secondAttribute="bottom" priority="999" id="Zol-57-Lbq"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="149" id="nM7-vJ-W8b">
@@ -451,7 +398,7 @@
</label>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="r8T-dj-wQX">
<rect key="frame" x="0.0" y="41" width="375" height="88"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" name="Background"/>
<constraints>
<constraint firstAttribute="height" constant="88" id="6Lk-OO-MsA"/>
</constraints>
@@ -521,6 +468,7 @@ World</string>
<constraint firstItem="Jvb-r8-XrY" firstAttribute="top" secondItem="cQ2-Jd-pRK" secondAttribute="top" id="Urh-Qr-vrS"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell>
</cells>
@@ -575,7 +523,7 @@ World</string>
<edgeInsets key="layoutMargins" top="0.0" left="0.0" bottom="0.0" right="0.0"/>
</stackView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" systemColor="tertiarySystemBackgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="xnC-tS-ZdV" firstAttribute="leading" secondItem="c7x-ee-3HH" secondAttribute="leading" constant="20" id="LO8-Au-SYF"/>
<constraint firstItem="c7x-ee-3HH" firstAttribute="bottom" secondItem="xnC-tS-ZdV" secondAttribute="bottom" constant="10" id="NZ9-iG-E10"/>
@@ -610,12 +558,12 @@ World</string>
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" name="Background"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="40" minimumInteritemSpacing="40" id="63d-78-Y24">
<size key="itemSize" width="335" height="300"/>
<size key="itemSize" width="375" height="300"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="20" minY="40" maxX="20" maxY="13"/>
<inset key="sectionInset" minX="0.0" minY="40" maxX="0.0" maxY="13"/>
</collectionViewFlowLayout>
<cells/>
<connections>
@@ -632,7 +580,7 @@ World</string>
<!--Browse-->
<scene sceneID="VHa-uP-bFU">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" sceneMemberID="viewController">
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
@@ -652,7 +600,7 @@ World</string>
<!--My Apps-->
<scene sceneID="nhh-BJ-XiT">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="3Ew-ox-i4n" sceneMemberID="viewController">
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y">
<color key="badgeColor" name="Primary"/>
</tabBarItem>
@@ -677,112 +625,102 @@ World</string>
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<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="footerReferenceSize" 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" reuseIdentifier="AppCell" id="kMp-ym-2yu" customClass="InstalledAppCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<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"/>
<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"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d6d-uV-GFi" userLabel="App Info">
<rect key="frame" x="20" y="0.0" width="335" height="60"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="H12-ip-Bbl" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
<constraints>
<constraint firstAttribute="height" constant="60" id="SOy-Xe-y2x"/>
<constraint firstAttribute="width" secondItem="H12-ip-Bbl" secondAttribute="height" multiplier="1:1" id="ZIR-f8-Jc4"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="7iy-Zp-LEj">
<rect key="frame" x="71" y="12" width="203" height="36"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="MRz-3W-aTM">
<rect key="frame" x="0.0" y="0.0" width="85" height="18"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="Short" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Nhl-6I-9gW">
<rect key="frame" x="0.0" y="0.0" width="38" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="mtL-iA-JnD">
<rect key="frame" x="44" y="0.0" width="41" height="18"/>
</imageView>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Hp4-uP-55T">
<rect key="frame" x="0.0" y="20" width="62" height="16"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dh4-fU-DFx" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="285" y="15.5" width="50" height="29"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<state key="normal" title="7 DAYS"/>
</button>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Expires in" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4Kc-4f-KYr">
<rect key="frame" x="306.5" y="0.5" width="47" height="12"/>
<fontDescription key="fontDescription" type="system" pointSize="10"/>
<color key="textColor" red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.45000000000000001" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="60"/>
<accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/>
</accessibility>
</view>
</subviews>
</view>
<constraints>
<constraint firstItem="4Kc-4f-KYr" firstAttribute="centerX" secondItem="dh4-fU-DFx" secondAttribute="centerX" id="9Uf-Qu-bhZ"/>
<constraint firstItem="d6d-uV-GFi" firstAttribute="leading" secondItem="kMp-ym-2yu" secondAttribute="leading" constant="20" id="fV7-0C-Hop"/>
<constraint firstItem="d6d-uV-GFi" firstAttribute="top" secondItem="kMp-ym-2yu" secondAttribute="top" id="rCI-7z-0mR"/>
<constraint firstItem="dh4-fU-DFx" firstAttribute="top" secondItem="4Kc-4f-KYr" secondAttribute="bottom" constant="3" id="rmM-9v-G5C"/>
<constraint firstAttribute="trailing" secondItem="d6d-uV-GFi" secondAttribute="trailing" constant="20" id="s7H-ei-AEn"/>
<constraint firstAttribute="trailingMargin" secondItem="mos-e4-dQ7" secondAttribute="trailing" id="TKN-0r-5ON"/>
<constraint firstItem="mos-e4-dQ7" firstAttribute="top" secondItem="kMp-ym-2yu" secondAttribute="top" id="TUp-Xe-CHP"/>
<constraint firstAttribute="bottom" secondItem="mos-e4-dQ7" secondAttribute="bottom" id="gO1-mC-cTz"/>
<constraint firstItem="mos-e4-dQ7" firstAttribute="leading" secondItem="kMp-ym-2yu" secondAttribute="leadingMargin" id="i49-Gc-w7s"/>
</constraints>
<connections>
<outlet property="appIconImageView" destination="H12-ip-Bbl" id="61F-4i-4Q3"/>
<outlet property="betaBadgeView" destination="mtL-iA-JnD" id="v8W-bc-EB7"/>
<outlet property="developerLabel" destination="Hp4-uP-55T" id="Cqx-3O-knq"/>
<outlet property="nameLabel" destination="Nhl-6I-9gW" id="lzd-pp-PEQ"/>
<outlet property="refreshButton" destination="dh4-fU-DFx" id="KWX-9y-2w8"/>
<outlet property="bannerView" destination="mos-e4-dQ7" id="z01-3x-alE"/>
<segue destination="0V6-N4-hTO" kind="show" identifier="showApp" id="cnd-KK-o60">
<segue key="commit" inheritsFrom="parent" id="YdR-Ct-SlK"/>
<segue key="preview" inheritsFrom="commit" id="GSg-SY-gai"/>
</segue>
</connections>
</collectionViewCell>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="NoUpdatesCell" id="h0f-XI-UA5">
<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"/>
<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"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No Updates Available" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z04-yg-x1t">
<rect key="frame" x="104" y="20" width="167" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
<color key="textColor" name="Primary"/>
<nil key="highlightedColor"/>
</label>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7iO-O4-Mr9">
<rect key="frame" x="8" y="0.0" width="359" height="60"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="d2X-wj-EhR">
<rect key="frame" x="0.0" y="0.0" width="359" height="60"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="zAy-K2-jA4">
<rect key="frame" x="0.0" y="0.0" width="359" height="60"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="F8U-ab-fOM">
<rect key="frame" x="0.0" y="0.0" width="359" height="60"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No Updates Available" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z04-yg-x1t">
<rect key="frame" x="96" y="20" width="167" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
<color key="textColor" name="Primary"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="z04-yg-x1t" firstAttribute="centerY" secondItem="F8U-ab-fOM" secondAttribute="centerY" id="9w9-Z0-jZl"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="z04-yg-x1t" secondAttribute="bottom" constant="10" id="IWL-Ei-QC2"/>
<constraint firstItem="z04-yg-x1t" firstAttribute="top" relation="greaterThanOrEqual" secondItem="F8U-ab-fOM" secondAttribute="top" constant="10" id="fLp-au-PLf"/>
<constraint firstItem="z04-yg-x1t" firstAttribute="centerX" secondItem="F8U-ab-fOM" secondAttribute="centerX" id="fiy-Zt-GmB"/>
</constraints>
</view>
<vibrancyEffect style="secondaryLabel">
<blurEffect style="systemChromeMaterial"/>
</vibrancyEffect>
</visualEffectView>
</subviews>
<color key="backgroundColor" name="BlurTint"/>
<constraints>
<constraint firstItem="zAy-K2-jA4" firstAttribute="top" secondItem="d2X-wj-EhR" secondAttribute="top" id="3GP-KH-ao8"/>
<constraint firstAttribute="trailing" secondItem="zAy-K2-jA4" secondAttribute="trailing" id="H29-aK-27e"/>
<constraint firstAttribute="bottom" secondItem="zAy-K2-jA4" secondAttribute="bottom" id="Ha4-Od-VHk"/>
<constraint firstItem="zAy-K2-jA4" firstAttribute="leading" secondItem="d2X-wj-EhR" secondAttribute="leading" id="rmG-C1-DoK"/>
</constraints>
</view>
<blurEffect style="systemChromeMaterial"/>
</visualEffectView>
</subviews>
</view>
<constraints>
<constraint firstItem="z04-yg-x1t" firstAttribute="centerY" secondItem="h0f-XI-UA5" secondAttribute="centerY" id="3dw-fe-ACP"/>
<constraint firstItem="z04-yg-x1t" firstAttribute="centerX" secondItem="h0f-XI-UA5" secondAttribute="centerX" id="AIh-kx-SmK"/>
<constraint firstItem="z04-yg-x1t" firstAttribute="top" relation="greaterThanOrEqual" secondItem="h0f-XI-UA5" secondAttribute="top" constant="10" id="QwS-y9-ahl"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="z04-yg-x1t" secondAttribute="bottom" constant="10" id="uQI-7x-E3b"/>
<constraint firstItem="7iO-O4-Mr9" firstAttribute="leading" secondItem="h0f-XI-UA5" secondAttribute="leadingMargin" id="4Kn-tp-E7l"/>
<constraint firstItem="7iO-O4-Mr9" firstAttribute="top" secondItem="h0f-XI-UA5" secondAttribute="top" id="Cxd-IB-cmI"/>
<constraint firstAttribute="bottom" secondItem="7iO-O4-Mr9" secondAttribute="bottom" id="Xk3-SQ-iHD"/>
<constraint firstAttribute="trailingMargin" secondItem="7iO-O4-Mr9" secondAttribute="trailing" id="ZwB-wX-siW"/>
</constraints>
<connections>
<outlet property="blurView" destination="7iO-O4-Mr9" id="kQ4-9N-nnv"/>
</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">
@@ -812,6 +750,40 @@ World</string>
<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"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="900" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="GFQ-Wy-Qhy">
<rect key="frame" x="138.5" y="0.0" width="98" height="52.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="5/10 App IDs" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LLv-8I-6Of">
<rect key="frame" x="0.0" y="0.0" width="98" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NHb-0F-cHZ">
<rect key="frame" x="0.0" y="20.5" width="98" height="32"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<state key="normal" title="View App IDs"/>
<connections>
<segue destination="IXk-qg-mFJ" kind="presentation" identifier="showAppIDs" id="yZB-Fh-cTL"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="GFQ-Wy-Qhy" secondAttribute="bottom" constant="8" id="HGl-P6-G2v"/>
<constraint firstItem="GFQ-Wy-Qhy" firstAttribute="top" secondItem="HYs-co-nJZ" secondAttribute="top" id="gg9-XU-2ej"/>
<constraint firstItem="GFQ-Wy-Qhy" firstAttribute="centerX" secondItem="HYs-co-nJZ" secondAttribute="centerX" id="vyo-h4-yD9"/>
</constraints>
<connections>
<outlet property="button" destination="NHb-0F-cHZ" id="wOh-Ee-jhN"/>
<outlet property="textLabel" destination="LLv-8I-6Of" id="t2D-f1-5pC"/>
</connections>
</collectionReusableView>
<connections>
<outlet property="dataSource" destination="hv7-Ar-voT" id="YOx-f4-chF"/>
<outlet property="delegate" destination="hv7-Ar-voT" id="1PN-pf-cZK"/>
@@ -830,12 +802,120 @@ World</string>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kiO-UO-esV" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1730" y="717"/>
<point key="canvasLocation" x="1728.8" y="716.49175412293857"/>
</scene>
<!--App IDs-->
<scene sceneID="kvf-US-rRe">
<objects>
<collectionViewController title="App IDs" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h">
<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="Wzt-qc-XG8">
<size key="itemSize" width="375" height="80"/>
<size key="headerReferenceSize" width="50" height="60"/>
<size key="footerReferenceSize" width="50" height="50"/>
<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">
<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">
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" 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="trailingMargin" secondItem="1w8-fI-98T" secondAttribute="trailing" id="0bS-49-dqo"/>
<constraint firstAttribute="bottom" secondItem="1w8-fI-98T" secondAttribute="bottom" id="Bif-xB-0gt"/>
<constraint firstItem="1w8-fI-98T" firstAttribute="top" secondItem="XWu-DU-xbh" secondAttribute="top" id="aEf-KK-MHU"/>
<constraint firstItem="1w8-fI-98T" firstAttribute="leading" secondItem="XWu-DU-xbh" secondAttribute="leadingMargin" id="mFW-ti-cVB"/>
</constraints>
<connections>
<outlet property="bannerView" destination="1w8-fI-98T" id="OH8-L9-TZn"/>
</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">
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="App IDs Description" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="83Z-Ih-nOW">
<rect key="frame" x="8" y="14" width="359" height="31"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="83Z-Ih-nOW" secondAttribute="bottom" constant="15" id="CQA-og-LZ2"/>
<constraint firstItem="83Z-Ih-nOW" firstAttribute="top" secondItem="th0-G6-bRt" secondAttribute="top" constant="14" id="e0J-MA-eH5"/>
<constraint firstAttribute="leadingMargin" secondItem="83Z-Ih-nOW" secondAttribute="leading" id="nGf-Rh-mnk"/>
<constraint firstAttribute="trailingMargin" secondItem="83Z-Ih-nOW" secondAttribute="trailing" id="sYg-nT-ror"/>
</constraints>
<connections>
<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">
<rect key="frame" x="0.0" y="170" width="375" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="10 App IDs" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Zna-7n-kBz">
<rect key="frame" x="146" y="0.0" width="83" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="Zna-7n-kBz" firstAttribute="centerX" secondItem="xMh-lD-r6C" secondAttribute="centerX" id="7RS-ua-XzZ"/>
<constraint firstItem="Zna-7n-kBz" firstAttribute="top" secondItem="xMh-lD-r6C" secondAttribute="top" id="RvY-z8-XI6"/>
</constraints>
<connections>
<outlet property="textLabel" destination="Zna-7n-kBz" id="LK5-BR-skx"/>
</connections>
</collectionReusableView>
<connections>
<outlet property="dataSource" destination="y1A-Nm-mw7" id="U8O-CF-Jhv"/>
<outlet property="delegate" destination="y1A-Nm-mw7" id="a8i-FA-aUq"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
<barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
<rect key="frame" x="16" y="7" width="83" height="42"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</view>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="Ekd-oC-gOr">
<connections>
<segue destination="eS1-sQ-VUA" kind="unwind" unwindAction="unwindToMyAppsViewController:" id="VHS-kt-woS"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="activityIndicatorBarButtonItem" destination="Aqs-QK-Ups" id="2I7-rT-muy"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Lvd-jC-AZO" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="eS1-sQ-VUA" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="3301.5999999999999" y="715.59220389805103"/>
</scene>
<!--News-->
<scene sceneID="BV8-6J-nIv">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" sceneMemberID="viewController">
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
@@ -852,20 +932,43 @@ World</string>
</objects>
<point key="canvasLocation" x="962" y="-752"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="1Gj-mS-BaN">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="y1A-Nm-mw7" kind="relationship" relationship="rootViewController" id="ZYf-6x-9a0"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="3LN-mt-qWn" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2526" y="731"/>
</scene>
</scenes>
<resources>
<image name="Back" width="18" height="18"/>
<image name="BetaBadge" width="41" height="17"/>
<image name="Browse" width="20" height="20"/>
<image name="MyApps" width="20" height="20"/>
<image name="News" width="19" height="20"/>
<image name="Settings" width="20" height="20"/>
<namedColor name="Background">
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Primary">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
<inferredMetricsTieBreakers>
<segue reference="dzt-2e-VM9"/>
<segue reference="cnd-KK-o60"/>
</inferredMetricsTieBreakers>
<color key="tintColor" name="Primary"/>
</document>

View File

@@ -20,40 +20,24 @@ import Nuke
}
}
private lazy var dataSource = self.makeDataSource()
@IBOutlet var nameLabel: UILabel!
@IBOutlet var developerLabel: UILabel!
@IBOutlet var appIconImageView: UIImageView!
@IBOutlet var actionButton: PillButton!
@IBOutlet var bannerView: AppBannerView!
@IBOutlet var subtitleLabel: UILabel!
@IBOutlet var screenshotsCollectionView: UICollectionView!
@IBOutlet var betaBadgeView: UIImageView!
@IBOutlet private var screenshotsContentView: UIView!
@IBOutlet private(set) var screenshotsCollectionView: UICollectionView!
override func awakeFromNib()
{
super.awakeFromNib()
self.contentView.preservesSuperviewLayoutMargins = true
// Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷.
self.screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.screenshotsCollectionView.delegate = self
self.screenshotsCollectionView.dataSource = self.dataSource
self.screenshotsCollectionView.prefetchDataSource = self.dataSource
self.screenshotsContentView.layer.cornerRadius = 20
self.screenshotsContentView.layer.masksToBounds = true
self.update()
}
override func tintColorDidChange()
{
super.tintColorDidChange()
self.update()
}
}
@@ -96,12 +80,6 @@ private extension BrowseCollectionViewCell
return dataSource
}
private func update()
{
self.subtitleLabel.textColor = self.tintColor
self.screenshotsContentView.backgroundColor = self.tintColor.withAlphaComponent(0.1)
}
}
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout

View File

@@ -1,131 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" 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="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<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"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
<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="400"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="Y3g-Md-6xH" userLabel="App Info">
<rect key="frame" x="20" y="20" width="335" height="79"/>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
<rect key="frame" x="16" y="0.0" width="343" height="369"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="F2j-pX-09A" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="7" width="65" height="65"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="width" secondItem="F2j-pX-09A" secondAttribute="height" multiplier="1:1" id="c2j-8O-Diw"/>
<constraint firstAttribute="height" constant="65" id="ufl-3d-nkT"/>
<constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="zkp-KH-OyV">
<rect key="frame" x="76" y="21" width="176" height="37"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Ykl-yo-ncv">
<rect key="frame" x="0.0" y="0.0" width="127.5" height="20.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="xni-8I-ewW">
<rect key="frame" x="0.0" y="0.0" width="80.5" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="5gN-I2-QOB">
<rect key="frame" x="86.5" y="0.0" width="41" height="20.5"/>
</imageView>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="B5S-HI-tWJ">
<rect key="frame" x="0.0" y="22.5" width="57.5" height="14.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DeC-Y2-fvR" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="263" y="24" width="72" height="31"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="72" id="X7D-DN-WnD"/>
<constraint firstAttribute="height" constant="31" id="svo-Sc-wpR"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<state key="normal" title="OPEN"/>
</button>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Classic Nintendo games in your pocket." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Imx-Le-bcy">
<rect key="frame" x="0.0" y="103" width="343" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" scrollEnabled="NO" contentInsetAdjustmentBehavior="never" dataMode="none" translatesAutoresizingMaskIntoConstraints="NO" id="RFs-qp-Ca4">
<rect key="frame" x="0.0" y="135" width="343" height="234"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="15" id="jH9-Jo-IHA">
<size key="itemSize" width="120" height="213"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="8" minY="0.0" maxX="8" maxY="0.0"/>
</collectionViewFlowLayout>
<cells/>
</collectionView>
</subviews>
</stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="w1r-LJ-TDs" userLabel="Screenshots">
<rect key="frame" x="15" y="114" width="345" height="266"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="hRR-84-Owd">
<rect key="frame" x="0.0" y="0.0" width="345" height="266"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Classic Nintendo games in your pocket." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Imx-Le-bcy">
<rect key="frame" x="20" y="15" width="305" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="1" green="0.14901960780000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" contentInsetAdjustmentBehavior="never" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="RFs-qp-Ca4">
<rect key="frame" x="20" y="47" width="305" height="185"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="10" id="jH9-Jo-IHA">
<size key="itemSize" width="120" height="213"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells/>
</collectionView>
</subviews>
<edgeInsets key="layoutMargins" top="15" left="20" bottom="20" right="20"/>
</stackView>
</subviews>
<color key="backgroundColor" red="1" green="0.14901960780000001" blue="0.0" alpha="0.050000000000000003" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="hRR-84-Owd" firstAttribute="leading" secondItem="w1r-LJ-TDs" secondAttribute="leading" id="3us-zR-peW"/>
<constraint firstItem="hRR-84-Owd" firstAttribute="top" secondItem="w1r-LJ-TDs" secondAttribute="top" id="HWW-aS-Scd"/>
<constraint firstAttribute="trailing" secondItem="hRR-84-Owd" secondAttribute="trailing" id="lbU-TC-jhJ"/>
<constraint firstAttribute="bottom" secondItem="hRR-84-Owd" secondAttribute="bottom" id="nOI-Qj-lbm"/>
</constraints>
</view>
</subviews>
</view>
<constraints>
<constraint firstAttribute="trailing" secondItem="w1r-LJ-TDs" secondAttribute="trailing" constant="15" id="4ns-Zq-D4j"/>
<constraint firstItem="w1r-LJ-TDs" firstAttribute="leading" secondItem="ln4-pC-7KY" secondAttribute="leading" constant="15" id="G1K-up-08u"/>
<constraint firstAttribute="bottom" secondItem="w1r-LJ-TDs" secondAttribute="bottom" constant="20" id="Kk0-dF-4OW"/>
<constraint firstItem="Y3g-Md-6xH" firstAttribute="top" secondItem="ln4-pC-7KY" secondAttribute="top" constant="20" id="PRR-aX-AiM"/>
<constraint firstAttribute="trailing" secondItem="Y3g-Md-6xH" secondAttribute="trailing" constant="20" id="g1Q-lg-I9O"/>
<constraint firstItem="w1r-LJ-TDs" firstAttribute="top" secondItem="Y3g-Md-6xH" secondAttribute="bottom" constant="15" id="i9W-bl-J9R"/>
<constraint firstItem="Y3g-Md-6xH" firstAttribute="leading" secondItem="ln4-pC-7KY" secondAttribute="leading" constant="20" id="j6L-IY-ALs"/>
<constraint firstItem="5gU-g3-Fsy" firstAttribute="top" secondItem="ln4-pC-7KY" secondAttribute="top" id="DnT-vq-BOc"/>
<constraint firstItem="5gU-g3-Fsy" firstAttribute="leading" secondItem="ln4-pC-7KY" secondAttribute="leadingMargin" id="YPy-xL-iUn"/>
<constraint firstAttribute="bottom" secondItem="5gU-g3-Fsy" secondAttribute="bottom" id="gRu-Hz-CNL"/>
<constraint firstAttribute="trailingMargin" secondItem="5gU-g3-Fsy" secondAttribute="trailing" id="vf4-ql-4Vq"/>
</constraints>
<viewLayoutGuide key="safeArea" id="btu-iP-81i"/>
<connections>
<outlet property="actionButton" destination="DeC-Y2-fvR" id="VDk-4D-STy"/>
<outlet property="appIconImageView" destination="F2j-pX-09A" id="COe-74-adn"/>
<outlet property="betaBadgeView" destination="5gN-I2-QOB" id="hu7-Ax-Wbc"/>
<outlet property="developerLabel" destination="B5S-HI-tWJ" id="QGh-1g-fFv"/>
<outlet property="nameLabel" destination="xni-8I-ewW" id="V56-ZT-vFa"/>
<outlet property="bannerView" destination="ziA-mP-AY2" id="yxo-ar-Cha"/>
<outlet property="screenshotsCollectionView" destination="RFs-qp-Ca4" id="xfi-AN-l17"/>
<outlet property="screenshotsContentView" destination="w1r-LJ-TDs" id="iWJ-52-rbA"/>
<outlet property="subtitleLabel" destination="Imx-Le-bcy" id="JVW-ZZ-51O"/>
</connections>
<point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/>
</collectionViewCell>
</objects>
<resources>
<image name="BetaBadge" width="41" height="17"/>
</resources>
</document>

View File

@@ -72,47 +72,49 @@ private extension BrowseViewController
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
let cell = cell as! BrowseCollectionViewCell
cell.nameLabel.text = app.name
cell.developerLabel.text = app.developerName
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.subtitleLabel.text = app.subtitle
cell.imageURLs = Array(app.screenshotURLs.prefix(2))
cell.appIconImageView.image = nil
cell.appIconImageView.isIndicatingActivity = true
cell.betaBadgeView.isHidden = !app.isBeta
cell.bannerView.titleLabel.text = app.name
cell.bannerView.subtitleLabel.text = app.developerName
cell.bannerView.betaBadgeView.isHidden = !app.isBeta
cell.actionButton.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
cell.actionButton.activityIndicatorView.style = .white
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
cell.bannerView.button.activityIndicatorView.style = .white
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
// Otherwise, cell reuse can mess up some cached values.
cell.actionButton.isIndicatingActivity = false
cell.bannerView.button.isIndicatingActivity = false
let tintColor = app.tintColor ?? .altPrimary
cell.tintColor = tintColor
if app.installedApp == nil
{
cell.actionButton.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
cell.bannerView.button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
let progress = AppManager.shared.installationProgress(for: app)
cell.actionButton.progress = progress
cell.actionButton.isInverted = false
cell.bannerView.button.progress = progress
if Date() < app.versionDate
{
cell.actionButton.countdownDate = app.versionDate
cell.bannerView.button.countdownDate = app.versionDate
}
else
{
cell.actionButton.countdownDate = nil
cell.bannerView.button.countdownDate = nil
}
}
else
{
cell.actionButton.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
cell.actionButton.progress = nil
cell.actionButton.isInverted = true
cell.actionButton.countdownDate = nil
cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
cell.bannerView.button.progress = nil
cell.bannerView.button.countdownDate = nil
}
}
dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
@@ -135,8 +137,8 @@ private extension BrowseViewController
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! BrowseCollectionViewCell
cell.appIconImageView.isIndicatingActivity = false
cell.appIconImageView.image = image
cell.bannerView.iconImageView.isIndicatingActivity = false
cell.bannerView.iconImageView.image = image
if let error = error
{
@@ -253,8 +255,8 @@ private extension BrowseViewController
{
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:", app.bundleIdentifier)
}
@@ -286,8 +288,8 @@ extension BrowseViewController: UICollectionViewDelegateFlowLayout
let maxVisibleScreenshots = 2 as CGFloat
let aspectRatio: CGFloat = 16.0 / 9.0
let layout = collectionViewLayout as! UICollectionViewFlowLayout
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1))
let layout = self.prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
@@ -295,6 +297,8 @@ extension BrowseViewController: UICollectionViewDelegateFlowLayout
widthConstraint.isActive = true
defer { widthConstraint.isActive = false }
// Manually update cell width & layout so we can accurately calculate screenshot sizes.
self.prototypeCell.frame.size.width = widthConstraint.constant
self.prototypeCell.layoutIfNeeded()
let collectionViewWidth = self.prototypeCell.screenshotsCollectionView.bounds.width
@@ -302,6 +306,7 @@ extension BrowseViewController: UICollectionViewDelegateFlowLayout
let screenshotHeight = screenshotWidth * aspectRatio
let heightConstraint = self.prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error.
heightConstraint.isActive = true
defer { heightConstraint.isActive = false }

View File

@@ -11,16 +11,27 @@ import Roxas
class AppBannerView: RSTNibView
{
private var originalTintColor: UIColor?
@IBOutlet var titleLabel: UILabel!
@IBOutlet var subtitleLabel: UILabel!
@IBOutlet var iconImageView: AppIconImageView!
@IBOutlet var button: PillButton!
@IBOutlet var buttonLabel: UILabel!
@IBOutlet var betaBadgeView: UIView!
@IBOutlet var backgroundEffectView: UIVisualEffectView!
@IBOutlet private var vibrancyView: UIVisualEffectView!
override func tintColorDidChange()
{
super.tintColorDidChange()
if self.tintAdjustmentMode != .dimmed
{
self.originalTintColor = self.tintColor
}
self.update()
}
}
@@ -32,9 +43,7 @@ private extension AppBannerView
self.clipsToBounds = true
self.layer.cornerRadius = 22
self.subtitleLabel.textColor = self.tintColor
self.button.tintColor = self.tintColor
self.backgroundColor = self.tintColor.withAlphaComponent(0.1)
self.subtitleLabel.textColor = self.originalTintColor ?? self.tintColor
self.backgroundEffectView.backgroundColor = self.originalTintColor ?? self.tintColor
}
}

View File

@@ -1,21 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" 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="14490.49"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Named colors" 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" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<connections>
<outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/>
<outlet property="betaBadgeView" destination="qQl-Ez-zC5" id="6O1-Cx-7qz"/>
<outlet property="button" destination="tVx-3G-dcu" id="joa-AH-syX"/>
<outlet property="buttonLabel" destination="Yd9-jw-faD" id="o7g-Gb-CIt"/>
<outlet property="iconImageView" destination="avS-dx-4iy" id="TQs-Ej-gin"/>
<outlet property="subtitleLabel" destination="oN5-vu-Dnw" id="gA4-iJ-Tix"/>
<outlet property="titleLabel" destination="mFe-zJ-eva" id="2OH-f8-cid"/>
<outlet property="vibrancyView" destination="fC4-1V-iMn" id="PXE-2B-A7w"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
@@ -23,6 +25,15 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rZk-be-tiI">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="b8k-up-HtI">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" name="BlurTint"/>
</view>
<blurEffect style="systemChromeMaterial"/>
</visualEffectView>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<subviews>
@@ -34,54 +45,97 @@
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="caL-vN-Svn">
<rect key="frame" x="85" y="24" width="195" height="40.5"/>
<rect key="frame" x="85" y="18" width="190" height="52"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="1Es-pv-zwd">
<rect key="frame" x="0.0" y="0.0" width="135" height="21.5"/>
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="500" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="1Es-pv-zwd">
<rect key="frame" x="0.0" y="0.0" width="167" height="34"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="mFe-zJ-eva">
<rect key="frame" x="0.0" y="0.0" width="88" height="21.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
<rect key="frame" x="0.0" y="0.0" width="79" height="34"/>
<accessibility key="accessibilityConfiguration" identifier="NameLabel"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="qQl-Ez-zC5">
<rect key="frame" x="94" y="0.0" width="41" height="21.5"/>
<rect key="frame" x="85" y="0.0" width="82" height="34"/>
<accessibility key="accessibilityConfiguration" identifier="Beta Badge">
<accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
<bool key="isElement" value="YES"/>
</accessibility>
</imageView>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="oN5-vu-Dnw">
<rect key="frame" x="0.0" y="23.5" width="66" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fC4-1V-iMn">
<rect key="frame" x="0.0" y="36" width="190" height="16"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="LQh-pN-ePC">
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="750" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="oN5-vu-Dnw">
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
<accessibility key="accessibilityConfiguration" label="Developer"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="oN5-vu-Dnw" firstAttribute="top" secondItem="LQh-pN-ePC" secondAttribute="top" id="7RH-WP-LzL"/>
<constraint firstItem="oN5-vu-Dnw" firstAttribute="leading" secondItem="LQh-pN-ePC" secondAttribute="leading" id="By8-cR-kTu"/>
<constraint firstAttribute="trailing" secondItem="oN5-vu-Dnw" secondAttribute="trailing" id="Hiv-6y-XrH"/>
<constraint firstAttribute="bottom" secondItem="oN5-vu-Dnw" secondAttribute="bottom" id="yc2-Dr-Qnv"/>
</constraints>
</view>
<vibrancyEffect style="secondaryLabel">
<blurEffect style="systemChromeMaterial"/>
</vibrancyEffect>
</visualEffectView>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3ox-Q2-Rnd" userLabel="Button Stack View">
<rect key="frame" x="286" y="28.5" width="77" height="31"/>
<subviews>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Yd9-jw-faD">
<rect key="frame" x="0.0" y="0.0" width="77" height="0.0"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="10"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="31" id="Zwh-yQ-GTu"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="77" id="eGc-Dk-QbL"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<state key="normal" title="FREE"/>
</button>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="291" y="28.5" width="72" height="31"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="31" id="Zwh-yQ-GTu"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="72" id="eGc-Dk-QbL"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<state key="normal" title="FREE"/>
</button>
</subviews>
<edgeInsets key="layoutMargins" top="14" left="14" bottom="14" right="12"/>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="d1T-UD-gWG" secondAttribute="bottom" id="B9e-Mf-cy5"/>
<constraint firstAttribute="trailing" secondItem="d1T-UD-gWG" secondAttribute="trailing" id="HcT-2k-z0H"/>
<constraint firstItem="d1T-UD-gWG" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="PIM-W5-dkh"/>
<constraint firstItem="d1T-UD-gWG" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="RHn-ZK-jgl"/>
<constraint firstItem="d1T-UD-gWG" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="5tv-QN-ZWU"/>
<constraint firstItem="rZk-be-tiI" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="N6R-B2-Rie"/>
<constraint firstAttribute="trailing" secondItem="d1T-UD-gWG" secondAttribute="trailing" id="mlG-w3-Ly6"/>
<constraint firstAttribute="trailing" secondItem="rZk-be-tiI" secondAttribute="trailing" id="nEy-pm-Fcs"/>
<constraint firstAttribute="bottom" secondItem="d1T-UD-gWG" secondAttribute="bottom" id="nJo-To-LmX"/>
<constraint firstItem="rZk-be-tiI" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="pGD-Tl-U4c"/>
<constraint firstItem="d1T-UD-gWG" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="q2p-0S-Nv5"/>
<constraint firstAttribute="bottom" secondItem="rZk-be-tiI" secondAttribute="bottom" id="yk0-pw-joP"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="139.85507246376812" y="152.67857142857142"/>
</view>
</objects>
<resources>
<image name="BetaBadge" width="41" height="17"/>
<namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@@ -19,13 +19,16 @@ class AppIconImageView: UIImageView
self.backgroundColor = .white
self.layer.borderWidth = 0.5
self.layer.borderColor = self.tintColor.cgColor
// Allows us to match system look for app icons.
if self.layer.responds(to: Selector(("continuousCorners")))
if #available(iOS 13, *)
{
self.layer.setValue(true, forKey: "continuousCorners")
self.layer.cornerCurve = .continuous
}
else
{
if self.layer.responds(to: Selector(("continuousCorners")))
{
self.layer.setValue(true, forKey: "continuousCorners")
}
}
}
@@ -37,11 +40,4 @@ class AppIconImageView: UIImageView
let radius = self.bounds.height / 5
self.layer.cornerRadius = radius
}
override func tintColorDidChange()
{
super.tintColorDidChange()
self.layer.borderColor = self.tintColor.cgColor
}
}

View File

@@ -0,0 +1,20 @@
//
// ForwardingNavigationController.swift
// AltStore
//
// Created by Riley Testut on 10/24/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
class ForwardingNavigationController: UINavigationController
{
override var childForStatusBarStyle: UIViewController? {
return self.topViewController
}
override var childForStatusBarHidden: UIViewController? {
return self.topViewController
}
}

View File

@@ -11,11 +11,68 @@ import KeychainAccess
import AltSign
@propertyWrapper
struct KeychainItem<Value>
{
let key: String
var wrappedValue: Value? {
get {
switch Value.self
{
case is Data.Type: return try? Keychain.shared.keychain.getData(self.key) as? Value
case is String.Type: return try? Keychain.shared.keychain.getString(self.key) as? Value
default: return nil
}
}
set {
switch Value.self
{
case is Data.Type: Keychain.shared.keychain[data: self.key] = newValue as? Data
case is String.Type: Keychain.shared.keychain[self.key] = newValue as? String
default: break
}
}
}
init(key: String)
{
self.key = key
}
}
class Keychain
{
static let shared = Keychain()
private let keychain = KeychainAccess.Keychain(service: "com.rileytestut.AltStore").accessibility(.afterFirstUnlock).synchronizable(true)
fileprivate let keychain = KeychainAccess.Keychain(service: "com.rileytestut.AltStore").accessibility(.afterFirstUnlock).synchronizable(true)
@KeychainItem(key: "appleIDEmailAddress")
var appleIDEmailAddress: String?
@KeychainItem(key: "appleIDPassword")
var appleIDPassword: String?
@KeychainItem(key: "signingCertificatePrivateKey")
var signingCertificatePrivateKey: Data?
@KeychainItem(key: "signingCertificateSerialNumber")
var signingCertificateSerialNumber: String?
@KeychainItem(key: "signingCertificate")
var signingCertificate: Data?
@KeychainItem(key: "signingCertificatePassword")
var signingCertificatePassword: String?
@KeychainItem(key: "patreonAccessToken")
var patreonAccessToken: String?
@KeychainItem(key: "patreonRefreshToken")
var patreonRefreshToken: String?
@KeychainItem(key: "patreonCreatorAccessToken")
var patreonCreatorAccessToken: String?
private init()
{
@@ -29,66 +86,3 @@ class Keychain
self.signingCertificateSerialNumber = nil
}
}
extension Keychain
{
var appleIDEmailAddress: String? {
get {
let emailAddress = try? self.keychain.get("appleIDEmailAddress")
return emailAddress
}
set {
self.keychain["appleIDEmailAddress"] = newValue
}
}
var appleIDPassword: String? {
get {
let password = try? self.keychain.get("appleIDPassword")
return password
}
set {
self.keychain["appleIDPassword"] = newValue
}
}
var signingCertificatePrivateKey: Data? {
get {
let privateKey = try? self.keychain.getData("signingCertificatePrivateKey")
return privateKey
}
set {
self.keychain[data: "signingCertificatePrivateKey"] = newValue
}
}
var signingCertificateSerialNumber: String? {
get {
let serialNumber = try? self.keychain.get("signingCertificateSerialNumber")
return serialNumber
}
set {
self.keychain["signingCertificateSerialNumber"] = newValue
}
}
var patreonAccessToken: String? {
get {
let accessToken = try? self.keychain.get("patreonAccessToken")
return accessToken
}
set {
self.keychain["patreonAccessToken"] = newValue
}
}
var patreonRefreshToken: String? {
get {
let refreshToken = try? self.keychain.get("patreonRefreshToken")
return refreshToken
}
set {
self.keychain["patreonRefreshToken"] = newValue
}
}
}

View File

@@ -32,19 +32,52 @@ class NavigationBar: UINavigationBar
private func initialize()
{
self.shadowImage = UIImage()
if let tintColor = self.barTintColor
if #available(iOS 13, *)
{
self.backgroundColorView.backgroundColor = tintColor
let standardAppearance = UINavigationBarAppearance()
standardAppearance.configureWithDefaultBackground()
standardAppearance.shadowColor = nil
// Top = -50 to cover status bar area above navigation bar on any device.
// Bottom = -1 to prevent a flickering gray line from appearing.
self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
let edgeAppearance = UINavigationBarAppearance()
edgeAppearance.configureWithOpaqueBackground()
edgeAppearance.backgroundColor = self.barTintColor
edgeAppearance.shadowColor = nil
if let tintColor = self.barTintColor
{
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
standardAppearance.backgroundColor = tintColor
standardAppearance.titleTextAttributes = textAttributes
standardAppearance.largeTitleTextAttributes = textAttributes
edgeAppearance.titleTextAttributes = textAttributes
edgeAppearance.largeTitleTextAttributes = textAttributes
}
else
{
standardAppearance.backgroundColor = nil
}
self.scrollEdgeAppearance = edgeAppearance
self.standardAppearance = standardAppearance
}
else
{
self.barTintColor = .white
self.shadowImage = UIImage()
if let tintColor = self.barTintColor
{
self.backgroundColorView.backgroundColor = tintColor
// Top = -50 to cover status bar area above navigation bar on any device.
// Bottom = -1 to prevent a flickering gray line from appearing.
self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
}
else
{
self.barTintColor = .white
}
}
}

View File

@@ -11,13 +11,15 @@ import UIKit
class PillButton: UIButton
{
var progress: Progress? {
didSet {
didSet {
self.progressView.progress = Float(self.progress?.fractionCompleted ?? 0)
self.progressView.observedProgress = self.progress
let isUserInteractionEnabled = self.isUserInteractionEnabled
self.isIndicatingActivity = (self.progress != nil)
self.isUserInteractionEnabled = isUserInteractionEnabled
self.update()
}
}
@@ -30,12 +32,6 @@ class PillButton: UIButton
}
}
var isInverted: Bool = false {
didSet {
self.update()
}
}
var countdownDate: Date? {
didSet {
self.isEnabled = (self.countdownDate == nil)
@@ -120,18 +116,18 @@ private extension PillButton
{
func update()
{
if self.isInverted
if self.progress == nil
{
self.setTitleColor(.white, for: .normal)
self.backgroundColor = self.tintColor
self.progressView.progressTintColor = self.tintColor.withAlphaComponent(0.15)
}
else
{
self.setTitleColor(self.tintColor, for: .normal)
self.backgroundColor = self.tintColor.withAlphaComponent(0.15)
self.progressView.progressTintColor = self.tintColor
}
self.progressView.progressTintColor = self.tintColor
}
@objc func updateCountdown()

View File

@@ -10,11 +10,35 @@ import Roxas
class ToastView: RSTToastView
{
var preferredDuration: TimeInterval
override init(text: String, detailText detailedText: String?)
{
if detailedText == nil
{
self.preferredDuration = 2.0
}
else
{
self.preferredDuration = 8.0
}
super.init(text: text, detailText: detailedText)
self.layoutMargins = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
self.setNeedsLayout()
}
convenience init(error: Error)
{
if let error = error as? LocalizedError
{
self.init(text: error.localizedDescription, detailText: error.recoverySuggestion ?? error.failureReason)
}
else
{
self.init(text: error.localizedDescription, detailText: nil)
}
}
required init(coder aDecoder: NSCoder) {
@@ -27,4 +51,14 @@ class ToastView: RSTToastView
self.layer.cornerRadius = 16
}
func show(in viewController: UIViewController)
{
self.show(in: viewController.navigationController?.view ?? viewController.view, duration: self.preferredDuration)
}
override func show(in view: UIView)
{
self.show(in: view, duration: self.preferredDuration)
}
}

View File

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

@@ -20,6 +20,8 @@ extension UserDefaults
@NSManaged var isDebugModeEnabled: Bool
@NSManaged var presentedLaunchReminderNotification: Bool
@NSManaged var legacySideloadedApps: [String]?
func registerDefaults()
{
self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true])

View File

@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>ALTDeviceID</key>
<string>1c3416b7b0ab68773e6e7eb7f0d110f7c9353acc</string>
<string>00008030-001948590202802E</string>
<key>ALTServerID</key>
<string>1AAAB6FD-E8CE-4B70-8F26-4073215C03B0</string>
<key>CFBundleDevelopmentRegion</key>
@@ -36,7 +36,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
@@ -120,7 +120,5 @@
</dict>
</dict>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
</dict>
</plist>

View File

@@ -11,6 +11,10 @@ import Roxas
class LaunchViewController: RSTLaunchViewController
{
private var didFinishLaunching = false
private var destinationViewController: UIViewController!
override var launchConditions: [RSTLaunchCondition] {
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
DatabaseManager.shared.start(completionHandler: completionHandler)
@@ -18,6 +22,22 @@ class LaunchViewController: RSTLaunchViewController
return [isDatabaseStarted]
}
override var childForStatusBarStyle: UIViewController? {
return self.children.first
}
override var childForStatusBarHidden: UIViewController? {
return self.children.first
}
override func viewDidLoad()
{
super.viewDidLoad()
// Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
}
}
extension LaunchViewController
@@ -44,9 +64,23 @@ extension LaunchViewController
{
super.finishLaunching()
guard !self.didFinishLaunching else { return }
AppManager.shared.update()
PatreonAPI.shared.refreshPatreonAccount()
self.performSegue(withIdentifier: "finishLaunching", sender: nil)
// Add view controller as child (rather than presenting modally)
// so tint adjustment + card presentations works correctly.
self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
self.destinationViewController.view.alpha = 0.0
self.addChild(self.destinationViewController)
self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
self.destinationViewController.didMove(toParent: self)
UIView.animate(withDuration: 0.2) {
self.destinationViewController.view.alpha = 1.0
}
self.didFinishLaunching = true
}
}

View File

@@ -9,6 +9,7 @@
import Foundation
import UIKit
import UserNotifications
import MobileCoreServices
import AltSign
import AltKit
@@ -20,6 +21,8 @@ extension AppManager
static let didFetchSourceNotification = Notification.Name("com.altstore.AppManager.didFetchSource")
static let expirationWarningNotificationID = "altstore-expiration-warning"
static let whitelistedSideloadingBundleIDs: Set<String> = ["science.xnu.undecimus"]
}
class AppManager
@@ -55,16 +58,32 @@ extension AppManager
do
{
let installedApps = try context.fetch(fetchRequest)
for app in installedApps where app.storeApp != nil
if UserDefaults.standard.legacySideloadedApps == nil
{
// First time updating apps since updating AltStore to use custom UTIs,
// so cache all existing apps temporarily to prevent us from accidentally
// deleting them due to their custom UTI not existing (yet).
let apps = installedApps.map { $0.bundleIdentifier }
UserDefaults.standard.legacySideloadedApps = apps
}
let legacySideloadedApps = Set(UserDefaults.standard.legacySideloadedApps ?? [])
for app in installedApps
{
let uti = UTTypeCopyDeclaration(app.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
if app.bundleIdentifier == StoreApp.altstoreAppID
{
self.scheduleExpirationWarningLocalNotification(for: app)
}
else
{
if !UIApplication.shared.canOpenURL(app.openAppURL)
if uti == nil && !legacySideloadedApps.contains(app.bundleIdentifier)
{
// This UTI is not declared by any apps, which means this app has been deleted by the user.
// This app is also not a legacy sideloaded app, so we can assume it's fine to delete it.
context.delete(app)
}
}
@@ -80,13 +99,37 @@ extension AppManager
#endif
}
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTSigner, Error>) -> Void)
@discardableResult
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTSigner, ALTAppleAPISession), Error>) -> Void) -> OperationGroup
{
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
let group = OperationGroup()
let findServerOperation = FindServerOperation(group: group)
findServerOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): group.error = error
case .success(let server): group.server = server
}
}
self.operationQueue.addOperation(findServerOperation)
let authenticationOperation = AuthenticationOperation(group: group, presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): group.error = error
case .success(let signer, let session):
group.signer = signer
group.session = session
}
completionHandler(result)
}
authenticationOperation.addDependency(findServerOperation)
self.operationQueue.addOperation(authenticationOperation)
return group
}
}
@@ -114,6 +157,23 @@ extension AppManager
self.operationQueue.addOperation(fetchSourceOperation)
}
}
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
{
var group: OperationGroup!
group = self.authenticate(presentingViewController: nil) { (result) in
switch result
{
case .failure(let error):
completionHandler(.failure(error))
case .success:
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
fetchAppIDsOperation.resultHandler = completionHandler
self.operationQueue.addOperation(fetchAppIDsOperation)
}
}
}
}
extension AppManager
@@ -149,7 +209,7 @@ extension AppManager
func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup
{
let apps = installedApps.filter { self.refreshProgress(for: $0) == nil }
let apps = installedApps.filter { self.refreshProgress(for: $0) == nil || self.refreshProgress(for: $0)?.isCancelled == true }
let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group)
@@ -183,18 +243,6 @@ private extension AppManager
let group = group ?? OperationGroup()
var operations = [Operation]()
/* Authenticate */
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): group.error = error
case .success(let signer): group.signer = signer
}
}
operations.append(authenticationOperation)
/* Find Server */
let findServerOperation = FindServerOperation(group: group)
findServerOperation.resultHandler = { (result) in
@@ -204,9 +252,55 @@ private extension AppManager
case .success(let server): group.server = server
}
}
findServerOperation.addDependency(authenticationOperation)
operations.append(findServerOperation)
let authenticationOperation: AuthenticationOperation?
if group.signer == nil || group.session == nil
{
/* Authenticate */
let operation = AuthenticationOperation(group: group, presentingViewController: presentingViewController)
operation.resultHandler = { (result) in
switch result
{
case .failure(let error): group.error = error
case .success(let signer, let session):
group.signer = signer
group.session = session
}
}
operations.append(operation)
operation.addDependency(findServerOperation)
authenticationOperation = operation
}
else
{
authenticationOperation = nil
}
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(group: group)
refreshAnisetteDataOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): group.error = error
case .success(let anisetteData): group.session?.anisetteData = anisetteData
}
}
refreshAnisetteDataOperation.addDependency(authenticationOperation ?? findServerOperation)
operations.append(refreshAnisetteDataOperation)
/* Prepare Developer Account */
let prepareDeveloperAccountOperation = PrepareDeveloperAccountOperation(group: group)
prepareDeveloperAccountOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): group.error = error
case .success: break
}
}
prepareDeveloperAccountOperation.addDependency(refreshAnisetteDataOperation)
operations.append(prepareDeveloperAccountOperation)
for app in apps
{
@@ -220,7 +314,7 @@ private extension AppManager
guard let resignedApp = self.process(result, context: context) else { return }
context.resignedApp = resignedApp
}
resignAppOperation.addDependency(findServerOperation)
resignAppOperation.addDependency(prepareDeveloperAccountOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
operations.append(resignAppOperation)
@@ -267,8 +361,8 @@ private extension AppManager
/* Send */
let sendAppOperation = SendAppOperation(context: context)
sendAppOperation.resultHandler = { (result) in
guard let connection = self.process(result, context: context) else { return }
context.connection = connection
guard let installationConnection = self.process(result, context: context) else { return }
context.installationConnection = installationConnection
}
progress.addChild(sendAppOperation.progress, withPendingUnitCount: 10)
sendAppOperation.addDependency(resignAppOperation)
@@ -312,6 +406,28 @@ private extension AppManager
group.set(progress, for: app)
}
// Refresh anisette data after downloading all apps to prevent session from expiring.
for case let downloadOperation as DownloadAppOperation in operations
{
refreshAnisetteDataOperation.addDependency(downloadOperation)
}
/* Cache App IDs */
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
fetchAppIDsOperation.resultHandler = { (result) in
do
{
let (_, context) = try result.get()
try context.save()
}
catch
{
print("Failed to fetch App IDs.", error)
}
}
operations.forEach { fetchAppIDsOperation.addDependency($0) }
operations.append(fetchAppIDsOperation)
group.addOperations(operations)
return group
@@ -344,7 +460,11 @@ private extension AppManager
guard !context.isFinished else { return }
context.isFinished = true
self.refreshProgress[context.bundleIdentifier] = nil
if let progress = self.refreshProgress[context.bundleIdentifier], progress == context.group.progress(forAppWithBundleIdentifier: context.bundleIdentifier)
{
// Only remove progress if it hasn't been replaced by another one.
self.refreshProgress[context.bundleIdentifier] = nil
}
if let error = context.error
{
@@ -376,10 +496,13 @@ private extension AppManager
do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) }
}
}
do { try FileManager.default.removeItem(at: context.temporaryDirectory) }
catch { print("Failed to remove temporary directory.", error) }
if let index = UserDefaults.standard.legacySideloadedApps?.firstIndex(of: installedApp.bundleIdentifier)
{
// No longer a legacy sideloaded app, so remove it from cached list.
UserDefaults.standard.legacySideloadedApps?.remove(at: index)
}
}
print("Finished operation!", context.bundleIdentifier)

View File

@@ -44,6 +44,11 @@ class Account: NSManagedObject, Fetchable
{
super.init(entity: Account.entity(), insertInto: context)
self.update(account: account)
}
func update(account: ALTAccount)
{
self.appleID = account.appleID
self.identifier = account.identifier

View File

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

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="193"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="133"/>
</elements>
</model>

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="sourceURL" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="screenshotURLs" attributeType="Transformable"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
<attribute name="version" attributeType="String"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="193"/>
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
</elements>
</model>

View File

@@ -0,0 +1,52 @@
//
// AppID.swift
// AltStore
//
// Created by Riley Testut on 1/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
@objc(AppID)
class AppID: NSManagedObject, Fetchable
{
/* Properties */
@NSManaged var name: String
@NSManaged var identifier: String
@NSManaged var bundleIdentifier: String
@NSManaged var features: [ALTFeature: Any]
@NSManaged var expirationDate: Date?
/* Relationships */
@NSManaged private(set) var team: Team?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
init(_ appID: ALTAppID, team: Team, context: NSManagedObjectContext)
{
super.init(entity: AppID.entity(), insertInto: context)
self.name = appID.name
self.identifier = appID.identifier
self.bundleIdentifier = appID.bundleIdentifier
self.features = appID.features
self.expirationDate = appID.expirationDate
self.team = team
}
}
extension AppID
{
@nonobjc class func fetchRequest() -> NSFetchRequest<AppID>
{
return NSFetchRequest<AppID>(entityName: "AppID")
}
}

View File

@@ -194,13 +194,7 @@ private extension DatabaseManager
}
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
installedApp.version = localApp.version
if let provisioningProfile = localApp.provisioningProfile
{
installedApp.refreshedDate = provisioningProfile.creationDate
installedApp.expirationDate = provisioningProfile.expirationDate
}
installedApp.update(resignedApp: localApp)
do
{

View File

@@ -11,8 +11,20 @@ import CoreData
import AltSign
protocol InstalledAppProtocol: Fetchable
{
var name: String { get }
var bundleIdentifier: String { get }
var resignedBundleIdentifier: String { get }
var version: String { get }
var refreshedDate: Date { get }
var expirationDate: Date { get }
var installedDate: Date { get }
}
@objc(InstalledApp)
class InstalledApp: NSManagedObject, Fetchable
class InstalledApp: NSManagedObject, InstalledAppProtocol
{
/* Properties */
@NSManaged var name: String
@@ -22,9 +34,12 @@ class InstalledApp: NSManagedObject, Fetchable
@NSManaged var refreshedDate: Date
@NSManaged var expirationDate: Date
@NSManaged var installedDate: Date
/* Relationships */
@NSManaged var storeApp: StoreApp?
@NSManaged var team: Team?
@NSManaged var appExtensions: Set<InstalledExtension>
var isSideloaded: Bool {
return self.storeApp == nil
@@ -39,10 +54,21 @@ class InstalledApp: NSManagedObject, Fetchable
{
super.init(entity: InstalledApp.entity(), insertInto: context)
self.name = resignedApp.name
self.bundleIdentifier = originalBundleIdentifier
self.resignedBundleIdentifier = resignedApp.bundleIdentifier
self.refreshedDate = Date()
self.installedDate = Date()
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
self.update(resignedApp: resignedApp)
}
func update(resignedApp: ALTApplication)
{
self.name = resignedApp.name
self.resignedBundleIdentifier = resignedApp.bundleIdentifier
self.version = resignedApp.version
if let provisioningProfile = resignedApp.provisioningProfile
@@ -50,11 +76,6 @@ class InstalledApp: NSManagedObject, Fetchable
self.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate
}
else
{
self.refreshedDate = Date()
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
}
}
}
@@ -187,6 +208,12 @@ extension InstalledApp
return directoryURL
}
class func installedAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
{
let installedAppUTI = "io.altstore.Installed." + bundleIdentifier
return installedAppUTI
}
var directoryURL: URL {
return InstalledApp.directoryURL(for: self)
}
@@ -198,4 +225,8 @@ extension InstalledApp
var refreshedIPAURL: URL {
return InstalledApp.refreshedIPAURL(for: self)
}
var installedAppUTI: String {
return InstalledApp.installedAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
}
}

View File

@@ -0,0 +1,70 @@
//
// InstalledExtension.swift
// AltStore
//
// Created by Riley Testut on 1/7/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
@objc(InstalledExtension)
class InstalledExtension: NSManagedObject, InstalledAppProtocol
{
/* Properties */
@NSManaged var name: String
@NSManaged var bundleIdentifier: String
@NSManaged var resignedBundleIdentifier: String
@NSManaged var version: String
@NSManaged var refreshedDate: Date
@NSManaged var expirationDate: Date
@NSManaged var installedDate: Date
/* Relationships */
@NSManaged var parentApp: InstalledApp?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
init(resignedAppExtension: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext)
{
super.init(entity: InstalledExtension.entity(), insertInto: context)
self.bundleIdentifier = originalBundleIdentifier
self.refreshedDate = Date()
self.installedDate = Date()
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
self.update(resignedAppExtension: resignedAppExtension)
}
func update(resignedAppExtension: ALTApplication)
{
self.name = resignedAppExtension.name
self.resignedBundleIdentifier = resignedAppExtension.bundleIdentifier
self.version = resignedAppExtension.version
if let provisioningProfile = resignedAppExtension.provisioningProfile
{
self.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate
}
}
}
extension InstalledExtension
{
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledExtension>
{
return NSFetchRequest<InstalledExtension>(entityName: "InstalledExtension")
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
//
// InstalledAppPolicy.swift
// AltStore
//
// Created by Riley Testut on 1/24/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import CoreData
@objc(InstalledAppToInstalledAppMigrationPolicy)
class InstalledAppToInstalledAppMigrationPolicy: NSEntityMigrationPolicy
{
override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
{
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
// Entity must be in manager.destinationContext.
let entity = NSEntityDescription.entity(forEntityName: "Team", in: manager.destinationContext)
let fetchRequest = NSFetchRequest<NSManagedObject>()
fetchRequest.entity = entity
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
let teams = try manager.destinationContext.fetch(fetchRequest)
// Cannot use NSManagedObject subclasses during migration, so fallback to using KVC instead.
dInstance.setValue(teams.first, forKey: #keyPath(InstalledApp.team))
}
}

View File

@@ -11,7 +11,12 @@ import CoreData
extension Source
{
static let altStoreIdentifier = "com.rileytestut.AltStore"
#if STAGING
static let altStoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/apps-staging.json")!
#else
static let altStoreSourceURL = URL(string: "https://cdn.altstore.io/file/altstore/apps.json")!
#endif
}
@objc(Source)
@@ -22,6 +27,9 @@ class Source: NSManagedObject, Fetchable, Decodable
@NSManaged var identifier: String
@NSManaged var sourceURL: URL
/* Non-Core Data Properties */
var userInfo: [ALTSourceUserInfoKey: String]?
/* Relationships */
@objc(apps) @NSManaged private(set) var _apps: NSOrderedSet
@objc(newsItems) @NSManaged private(set) var _newsItems: NSOrderedSet
@@ -49,6 +57,7 @@ class Source: NSManagedObject, Fetchable, Decodable
case name
case identifier
case sourceURL
case userInfo
case apps
case news
}
@@ -69,6 +78,9 @@ class Source: NSManagedObject, Fetchable, Decodable
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) ?? []
for (index, app) in apps.enumerated()
{

View File

@@ -17,7 +17,7 @@ extension ALTTeamType
switch self
{
case .free: return NSLocalizedString("Free Developer Account", comment: "")
case .individual: return NSLocalizedString("Individual", comment: "")
case .individual: return NSLocalizedString("Developer", comment: "")
case .organization: return NSLocalizedString("Organization", comment: "")
case .unknown: fallthrough
@unknown default: return NSLocalizedString("Unknown", comment: "")
@@ -25,6 +25,11 @@ extension ALTTeamType
}
}
extension Team
{
static let maximumFreeAppIDs = 10
}
@objc(Team)
class Team: NSManagedObject, Fetchable
{
@@ -37,6 +42,8 @@ class Team: NSManagedObject, Fetchable
/* Relationships */
@NSManaged private(set) var account: Account!
@NSManaged var installedApps: Set<InstalledApp>
@NSManaged private(set) var appIDs: Set<AppID>
var altTeam: ALTTeam?
@@ -49,13 +56,18 @@ class Team: NSManagedObject, Fetchable
{
super.init(entity: Team.entity(), insertInto: context)
self.account = account
self.update(team: team)
}
func update(team: ALTTeam)
{
self.altTeam = team
self.name = team.name
self.identifier = team.identifier
self.type = team.type
self.account = account
}
}

View File

@@ -10,11 +10,18 @@ import UIKit
class InstalledAppCollectionViewCell: UICollectionViewCell
{
@IBOutlet var appIconImageView: UIImageView!
@IBOutlet var nameLabel: UILabel!
@IBOutlet var developerLabel: UILabel!
@IBOutlet var refreshButton: PillButton!
@IBOutlet var betaBadgeView: UIImageView!
@IBOutlet var bannerView: AppBannerView!
override func awakeFromNib()
{
super.awakeFromNib()
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.contentView.preservesSuperviewLayoutMargins = true
self.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
self.bannerView.buttonLabel.isHidden = false
}
}
class InstalledAppsCollectionHeaderView: UICollectionReusableView
@@ -23,6 +30,24 @@ class InstalledAppsCollectionHeaderView: UICollectionReusableView
@IBOutlet var button: UIButton!
}
class InstalledAppsCollectionFooterView: UICollectionReusableView
{
@IBOutlet var textLabel: UILabel!
@IBOutlet var button: UIButton!
}
class NoUpdatesCollectionViewCell: UICollectionViewCell
{
@IBOutlet var blurView: UIVisualEffectView!
override func awakeFromNib()
{
super.awakeFromNib()
self.contentView.preservesSuperviewLayoutMargins = true
}
}
class UpdatesCollectionHeaderView: UICollectionReusableView
{
let button = PillButton(type: .system)

View File

@@ -67,6 +67,17 @@ class MyAppsViewController: UICollectionViewController
{
super.viewDidLoad()
#if !BETA
// Set leftBarButtonItem to invisible UIBarButtonItem so we can still use it
// to show an activity indicator while sideloading whitelisted apps.
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
#endif
if #available(iOS 13.0, *)
{
self.navigationItem.leftBarButtonItem?.activityIndicatorView.style = .medium
}
// Allows us to intercept delegate callbacks.
self.updatesDataSource.fetchedResultsController.delegate = self
@@ -74,7 +85,6 @@ class MyAppsViewController: UICollectionViewController
self.collectionView.prefetchDataSource = self.dataSource
self.prototypeUpdateCell = UpdateCollectionViewCell.instantiate(with: UpdateCollectionViewCell.nib!)
self.prototypeUpdateCell.translatesAutoresizingMaskIntoConstraints = false
self.prototypeUpdateCell.contentView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.register(UpdateCollectionViewCell.nib, forCellWithReuseIdentifier: "UpdateCell")
@@ -85,10 +95,6 @@ class MyAppsViewController: UICollectionViewController
self.sideloadingProgressView.progressTintColor = .altPrimary
self.sideloadingProgressView.progress = 0
#if !BETA
self.navigationItem.leftBarButtonItem = nil
#endif
if let navigationBar = self.navigationController?.navigationBar
{
navigationBar.addSubview(self.sideloadingProgressView)
@@ -109,6 +115,10 @@ class MyAppsViewController: UICollectionViewController
super.viewWillAppear(animated)
self.updateDataSource()
#if BETA
self.fetchAppIDs()
#endif
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
@@ -138,6 +148,10 @@ class MyAppsViewController: UICollectionViewController
let installedApp = self.dataSource.item(at: indexPath)
return !installedApp.isSideloaded
}
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
{
}
}
private extension MyAppsViewController
@@ -156,9 +170,13 @@ private extension MyAppsViewController
dynamicDataSource.numberOfItemsHandler = { _ in self.updatesDataSource.itemCount == 0 ? 1 : 0 }
dynamicDataSource.cellIdentifierHandler = { _ in "NoUpdatesCell" }
dynamicDataSource.cellConfigurationHandler = { (cell, _, indexPath) in
cell.layer.cornerRadius = 20
cell.layer.masksToBounds = true
cell.contentView.backgroundColor = UIColor.altPrimary.withAlphaComponent(0.15)
let cell = cell as! NoUpdatesCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.blurView.layer.cornerRadius = 20
cell.blurView.layer.masksToBounds = true
cell.blurView.backgroundColor = .altPrimary
}
return dynamicDataSource
@@ -179,15 +197,19 @@ private extension MyAppsViewController
guard let app = installedApp.storeApp else { return }
let cell = cell as! UpdateCollectionViewCell
cell.tintColor = app.tintColor ?? .altPrimary
cell.nameLabel.text = app.name
cell.versionDescriptionTextView.text = app.versionDescription
cell.appIconImageView.image = nil
cell.appIconImageView.isIndicatingActivity = true
cell.betaBadgeView.isHidden = !app.isBeta
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.updateButton.isIndicatingActivity = false
cell.updateButton.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
cell.tintColor = app.tintColor ?? .altPrimary
cell.versionDescriptionTextView.text = app.versionDescription
cell.bannerView.titleLabel.text = app.name
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.betaBadgeView.isHidden = !app.isBeta
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
if self.expandedAppUpdates.contains(app.bundleIdentifier)
{
@@ -201,9 +223,9 @@ private extension MyAppsViewController
cell.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
let progress = AppManager.shared.installationProgress(for: app)
cell.updateButton.progress = progress
cell.bannerView.button.progress = progress
cell.dateLabel.text = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter)
cell.bannerView.subtitleLabel.text = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter)
cell.setNeedsLayout()
}
@@ -227,8 +249,8 @@ private extension MyAppsViewController
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! UpdateCollectionViewCell
cell.appIconImageView.isIndicatingActivity = false
cell.appIconImageView.image = image
cell.bannerView.iconImageView.isIndicatingActivity = false
cell.bannerView.iconImageView.image = image
if let error = error
{
@@ -254,12 +276,15 @@ private extension MyAppsViewController
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
let cell = cell as! InstalledAppCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = tintColor
cell.appIconImageView.isIndicatingActivity = true
cell.betaBadgeView.isHidden = !(installedApp.storeApp?.isBeta ?? false)
cell.refreshButton.isIndicatingActivity = false
cell.refreshButton.addTarget(self, action: #selector(MyAppsViewController.refreshApp(_:)), for: .primaryActionTriggered)
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.betaBadgeView.isHidden = !(installedApp.storeApp?.isBeta ?? false)
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.refreshApp(_:)), for: .primaryActionTriggered)
let currentDate = Date()
@@ -267,34 +292,34 @@ private extension MyAppsViewController
if numberOfDays == 1
{
cell.refreshButton.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal)
cell.bannerView.button.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal)
}
else
{
cell.refreshButton.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal)
cell.bannerView.button.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal)
}
cell.nameLabel.text = installedApp.name
cell.developerLabel.text = installedApp.storeApp?.developerName ?? NSLocalizedString("Sideloaded", comment: "")
cell.bannerView.titleLabel.text = installedApp.name
cell.bannerView.subtitleLabel.text = installedApp.storeApp?.developerName ?? NSLocalizedString("Sideloaded", comment: "")
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
switch numberOfDays
{
case 2...3: cell.refreshButton.tintColor = .refreshOrange
case 4...5: cell.refreshButton.tintColor = .refreshYellow
case 6...: cell.refreshButton.tintColor = .refreshGreen
default: cell.refreshButton.tintColor = .refreshRed
case 2...3: cell.bannerView.button.tintColor = .refreshOrange
case 4...5: cell.bannerView.button.tintColor = .refreshYellow
case 6...: cell.bannerView.button.tintColor = .refreshGreen
default: cell.bannerView.button.tintColor = .refreshRed
}
if let refreshGroup = self.refreshGroup, let progress = refreshGroup.progress(for: installedApp), progress.fractionCompleted < 1.0
{
cell.refreshButton.progress = progress
cell.bannerView.button.progress = progress
}
else
{
cell.refreshButton.progress = nil
cell.bannerView.button.progress = nil
}
}
dataSource.prefetchHandler = { (item, indexPath, completion) in
@@ -312,8 +337,8 @@ private extension MyAppsViewController
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! InstalledAppCollectionViewCell
cell.appIconImageView.image = image
cell.appIconImageView.isIndicatingActivity = false
cell.bannerView.iconImageView.image = image
cell.bannerView.iconImageView.isIndicatingActivity = false
}
return dataSource
@@ -358,6 +383,21 @@ private extension MyAppsViewController
}
}
func fetchAppIDs()
{
AppManager.shared.fetchAppIDs { (result) in
do
{
let (_, context) = try result.get()
try context.save()
}
catch
{
print("Failed to fetch App IDs.", error)
}
}
}
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping (Result<[String : Result<InstalledApp, Error>], Error>) -> Void)
{
func refresh()
@@ -368,9 +408,8 @@ private extension MyAppsViewController
switch result
{
case .failure(let error):
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.setNeedsLayout()
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
let toastView = ToastView(error: error)
toastView.show(in: self)
case .success(let results):
let failures = results.compactMapValues { (result) -> Error? in
@@ -384,22 +423,32 @@ private extension MyAppsViewController
guard !failures.isEmpty else { break }
let localizedText: String
let detailText: String?
let toastView: ToastView
if let failure = failures.first, failures.count == 1
if let failure = failures.first, results.count == 1
{
localizedText = failure.value.localizedDescription
detailText = nil
toastView = ToastView(error: failure.value)
}
else
{
localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count))
detailText = failures.first?.value.localizedDescription
let localizedText: String
if failures.count == 1
{
localizedText = NSLocalizedString("Failed to refresh 1 app.", comment: "")
}
else
{
localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count))
}
let detailText = failures.first?.value.localizedDescription
toastView = ToastView(text: localizedText, detailText: detailText)
toastView.preferredDuration = 2.0
}
let toastView = ToastView(text: localizedText, detailText: detailText)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
toastView.show(in: self)
}
self.refreshGroup = nil
@@ -559,8 +608,8 @@ private extension MyAppsViewController
self.collectionView.reloadItems(at: [indexPath])
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)
self.collectionView.reloadItems(at: [indexPath])
@@ -591,7 +640,7 @@ private extension MyAppsViewController
func presentSideloadingAlert(completion: @escaping (Bool) -> Void)
{
let alertController = UIAlertController(title: NSLocalizedString("Sideload Apps (Beta)", comment: ""), message: NSLocalizedString("You may only install 10 apps + app extensions per week due to Apple's restrictions.\n\nIf you encounter an app that is not able to be sideloaded, please report the app to support@altstore.io.", comment: ""), preferredStyle: .alert)
let alertController = UIAlertController(title: NSLocalizedString("Sideload Apps (Beta)", comment: ""), message: NSLocalizedString("If you encounter an app that is not able to be sideloaded, please report the app to support@altstore.io.", comment: ""), preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .default, handler: { (action) in
completion(true)
}))
@@ -601,7 +650,7 @@ private extension MyAppsViewController
self.present(alertController, animated: true, completion: nil)
}
func installApp(at fileURL: URL, completion: @escaping (Result<Void, Error>) -> Void)
func sideloadApp(at fileURL: URL, completion: @escaping (Result<Void, Error>) -> Void)
{
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
@@ -614,7 +663,11 @@ private extension MyAppsViewController
let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory)
guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { return }
guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { throw OperationError.invalidApp }
#if !BETA
guard AppManager.whitelistedSideloadingBundleIDs.contains(application.bundleIdentifier) else { throw OperationError.sideloadingAppNotSupported(application) }
#endif
self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in
try? FileManager.default.removeItem(at: temporaryDirectory)
@@ -622,8 +675,8 @@ private extension MyAppsViewController
DispatchQueue.main.async {
if let error = result.error
{
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.view, duration: 2.0)
let toastView = ToastView(error: error)
toastView.show(in: self)
}
else
{
@@ -648,7 +701,31 @@ private extension MyAppsViewController
{
try? FileManager.default.removeItem(at: temporaryDirectory)
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
DispatchQueue.main.async {
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
if let localizedError = error as? OperationError, case OperationError.sideloadingAppNotSupported = localizedError
{
let message = NSLocalizedString("""
Sideloading apps is in beta, and is currently limited to a small number of apps. This restriction is temporary, and you will be able to sideload any app once the feature is finished.
In the meantime, you can help us beta test sideloading apps by becoming a Patron.
""", comment: "")
let alertController = UIAlertController(title: localizedError.localizedDescription, message: message, preferredStyle: .alert)
alertController.addAction(.cancel)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Become a Patron", comment: ""), style: .default, handler: { (action) in
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
}))
self.present(alertController, animated: true, completion: nil)
}
else
{
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
completion(.failure(error))
}
@@ -700,14 +777,22 @@ private extension MyAppsViewController
else { return }
let installedApp = self.dataSource.item(at: indexPath)
guard installedApp.storeApp == nil else { return }
#if DEBUG
self.presentAlert(for: installedApp)
#else
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
{
// Only display alert for legacy sideloaded apps.
self.presentAlert(for: installedApp)
}
#endif
}
@objc func importApp(_ notification: Notification)
{
#if BETA
// Make sure left UIBarButtonItem has been set.
self.loadViewIfNeeded()
guard let fileURL = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return }
guard self.presentedViewController == nil else { return }
@@ -724,10 +809,12 @@ private extension MyAppsViewController
}
}
#if BETA
self.presentSideloadingAlert { (shouldContinue) in
if shouldContinue
{
self.installApp(at: fileURL) { (result) in
self.sideloadApp(at: fileURL) { (result) in
finish()
}
}
@@ -737,6 +824,12 @@ private extension MyAppsViewController
}
}
#else
self.sideloadApp(at: fileURL) { (result) in
finish()
}
#endif
}
}
@@ -776,7 +869,7 @@ extension MyAppsViewController
return headerView
case .installedApps:
case .installedApps where kind == UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InstalledAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
UIView.performWithoutAnimation {
@@ -792,6 +885,35 @@ extension MyAppsViewController
}
return headerView
case .installedApps:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "InstalledAppsFooter", for: indexPath) as! InstalledAppsCollectionFooterView
guard let team = DatabaseManager.shared.activeTeam() else { return footerView }
switch team.type
{
case .free:
let registeredAppIDs = team.appIDs.count
let maximumAppIDCount = 10
let remainingAppIDs = max(maximumAppIDCount - registeredAppIDs, 0)
if remainingAppIDs == 1
{
footerView.textLabel.text = String(format: NSLocalizedString("1 App ID Remaining", comment: ""))
}
else
{
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs))
}
footerView.textLabel.isHidden = false
case .individual, .organization, .unknown: footerView.textLabel.isHidden = true
@unknown default: break
}
return footerView
}
}
@@ -813,14 +935,11 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let padding = 30 as CGFloat
let width = collectionView.bounds.width - padding
let section = Section.allCases[indexPath.section]
switch section
{
case .noUpdates:
let size = CGSize(width: width, height: 44)
let size = CGSize(width: collectionView.bounds.width, height: 44)
return size
case .updates:
@@ -831,7 +950,10 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
return previousHeight
}
let widthConstraint = self.prototypeUpdateCell.contentView.widthAnchor.constraint(equalToConstant: width)
// Manually change cell's width to prevent conflicting with UIView-Encapsulated-Layout-Width constraints.
self.prototypeUpdateCell.frame.size.width = collectionView.bounds.width
let widthConstraint = self.prototypeUpdateCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
NSLayoutConstraint.activate([widthConstraint])
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
@@ -842,7 +964,7 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
return size
case .installedApps:
return CGSize(width: collectionView.bounds.width, height: 60)
return CGSize(width: collectionView.bounds.width, height: 88)
}
}
@@ -860,20 +982,38 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
{
let section = Section.allCases[section]
switch section
{
case .noUpdates:
guard self.updatesDataSource.itemCount == 0 else { return .zero }
return UIEdgeInsets(top: 12, left: 15, bottom: 20, right: 15)
case .noUpdates: return .zero
case .updates: return .zero
case .installedApps:
#if BETA
guard let _ = DatabaseManager.shared.activeTeam() else { return .zero }
case .updates:
guard self.updatesDataSource.itemCount > 0 else { return .zero }
return UIEdgeInsets(top: 12, left: 15, bottom: 20, right: 15)
case .installedApps: return UIEdgeInsets(top: 12, left: 0, bottom: 20, right: 0)
let indexPath = IndexPath(row: 0, section: section.rawValue)
let footerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionFooter, at: indexPath) as! InstalledAppsCollectionFooterView
let size = footerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
return size
#else
return .zero
#endif
}
}
func collectionView(_ myCV: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
{
let section = Section.allCases[section]
switch section
{
case .noUpdates where self.updatesDataSource.itemCount != 0: return .zero
case .updates where self.updatesDataSource.itemCount == 0: return .zero
default: return UIEdgeInsets(top: 12, left: 0, bottom: 20, right: 0)
}
}
}
@@ -930,7 +1070,7 @@ extension MyAppsViewController: UIDocumentPickerDelegate
{
guard let fileURL = urls.first else { return }
self.installApp(at: fileURL) { (result) in
self.sideloadApp(at: fileURL) { (result) in
print("Sideloaded app at \(fileURL) with result:", result)
}
}

View File

@@ -25,20 +25,27 @@ extension UpdateCollectionViewCell
}
}
@IBOutlet var appIconImageView: UIImageView!
@IBOutlet var nameLabel: UILabel!
@IBOutlet var dateLabel: UILabel!
@IBOutlet var updateButton: PillButton!
@IBOutlet var bannerView: AppBannerView!
@IBOutlet var versionDescriptionTitleLabel: UILabel!
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
@IBOutlet var betaBadgeView: UIImageView!
@IBOutlet private var blurView: UIVisualEffectView!
private var originalTintColor: UIColor?
override func awakeFromNib()
{
super.awakeFromNib()
self.contentView.layer.cornerRadius = 20
self.contentView.layer.masksToBounds = true
// Prevent temporary unsatisfiable constraint errors due to UIView-Encapsulated-Layout constraints.
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.contentView.preservesSuperviewLayoutMargins = true
self.bannerView.backgroundEffectView.isHidden = true
self.bannerView.button.setTitle(NSLocalizedString("UPDATE", comment: ""), for: .normal)
self.blurView.layer.cornerRadius = 20
self.blurView.layer.masksToBounds = true
self.update()
}
@@ -47,6 +54,11 @@ extension UpdateCollectionViewCell
{
super.tintColorDidChange()
if self.tintAdjustmentMode != .dimmed
{
self.originalTintColor = self.tintColor
}
self.update()
}
@@ -86,12 +98,9 @@ private extension UpdateCollectionViewCell
case .expanded: self.versionDescriptionTextView.isCollapsed = false
}
self.versionDescriptionTitleLabel.textColor = self.tintColor
self.contentView.backgroundColor = self.tintColor.withAlphaComponent(0.1)
self.updateButton.setTitleColor(self.tintColor, for: .normal)
self.updateButton.backgroundColor = self.tintColor.withAlphaComponent(0.15)
self.updateButton.progressTintColor = self.tintColor
self.versionDescriptionTitleLabel.textColor = self.originalTintColor ?? self.tintColor
self.blurView.backgroundColor = self.originalTintColor ?? self.tintColor
self.bannerView.button.progressTintColor = self.originalTintColor ?? self.tintColor
self.setNeedsLayout()
self.layoutIfNeeded()

View File

@@ -1,130 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" 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="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<capability name="Named colors" 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"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="133.5"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
<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="133.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dmf-hv-bwx">
<rect key="frame" x="0.0" y="0.0" width="375" height="133.5"/>
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dmf-hv-bwx">
<rect key="frame" x="16" y="0.0" width="343" height="125"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="57X-Ep-rfq">
<rect key="frame" x="20" y="20" width="340" height="93.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="H0T-dR-3In" userLabel="App Info">
<rect key="frame" x="0.0" y="0.0" width="340" height="65"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="jg6-wi-ngb" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="65" height="65"/>
<constraints>
<constraint firstAttribute="height" constant="65" id="W3C-hH-1Ii"/>
<constraint firstAttribute="width" secondItem="jg6-wi-ngb" secondAttribute="height" multiplier="1:1" id="vt3-Qt-m21"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="2Ii-Hu-4ru">
<rect key="frame" x="76" y="14" width="172" height="37"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="9Zk-Mp-JI7">
<rect key="frame" x="0.0" y="0.0" width="89.5" height="20.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="Short" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qmI-m4-Mra">
<rect key="frame" x="0.0" y="0.0" width="42.5" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="4LS-dp-4VA">
<rect key="frame" x="48.5" y="0.0" width="41" height="20.5"/>
</imageView>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xaB-Kc-Par">
<rect key="frame" x="0.0" y="22.5" width="57.5" height="14.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="OSL-U2-BKa" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="259" y="17" width="81" height="31"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" constant="81" id="3yj-p0-NuE"/>
<constraint firstAttribute="height" constant="31" id="KbP-M6-N3w"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<state key="normal" title="UPDATE"/>
</button>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
<rect key="frame" x="0.0" y="79" width="340" height="14.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" text="What's New" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h1u-nj-qsP">
<rect key="frame" x="0.0" y="0.0" width="65" height="13.5"/>
<constraints>
<constraint firstAttribute="width" constant="65" id="C7Y-nh-TKJ"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="11"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="75" y="-10" width="265" height="24.5"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="H0T-dR-3In" firstAttribute="width" secondItem="57X-Ep-rfq" secondAttribute="width" id="DYI-fa-Egk"/>
<constraint firstItem="RSR-5W-7tt" firstAttribute="width" secondItem="57X-Ep-rfq" secondAttribute="width" id="d3x-mH-ODQ"/>
</constraints>
</stackView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1xN-9h-DFd">
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="9iq-CR-Xc4">
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="uYl-PH-DuP">
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
<constraints>
<constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
<rect key="frame" x="0.0" y="88" width="343" height="37"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RKU-pY-wmQ">
<rect key="frame" x="15" y="0.0" width="65" height="22"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="4GQ-XP-i7X">
<rect key="frame" x="0.0" y="0.0" width="65" height="22"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" text="What's New" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h1u-nj-qsP">
<rect key="frame" x="0.0" y="0.0" width="65" height="22"/>
<constraints>
<constraint firstAttribute="width" constant="65" id="C7Y-nh-TKJ"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="11"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="h1u-nj-qsP" firstAttribute="leading" secondItem="4GQ-XP-i7X" secondAttribute="leading" id="3cO-Mj-Yua"/>
<constraint firstAttribute="trailing" secondItem="h1u-nj-qsP" secondAttribute="trailing" id="Hek-OE-YMc"/>
<constraint firstAttribute="bottom" secondItem="h1u-nj-qsP" secondAttribute="bottom" id="bLg-Ut-aEb"/>
<constraint firstItem="h1u-nj-qsP" firstAttribute="top" secondItem="4GQ-XP-i7X" secondAttribute="top" id="beL-ob-CQ7"/>
</constraints>
</view>
<vibrancyEffect style="secondaryLabel">
<blurEffect style="systemChromeMaterial"/>
</vibrancyEffect>
</visualEffectView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="90" y="0.0" width="238" height="22"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<edgeInsets key="layoutMargins" top="0.0" left="15" bottom="15" right="15"/>
</stackView>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" name="BlurTint"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="uYl-PH-DuP" secondAttribute="trailing" id="51O-j6-eoh"/>
<constraint firstAttribute="bottom" secondItem="uYl-PH-DuP" secondAttribute="bottom" id="IGs-MS-vnM"/>
<constraint firstItem="uYl-PH-DuP" firstAttribute="top" secondItem="9iq-CR-Xc4" secondAttribute="top" id="hnr-wG-XRY"/>
<constraint firstItem="uYl-PH-DuP" firstAttribute="leading" secondItem="9iq-CR-Xc4" secondAttribute="leading" id="usR-Ia-LMy"/>
</constraints>
</view>
<blurEffect style="systemChromeMaterial"/>
</visualEffectView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="57X-Ep-rfq" secondAttribute="bottom" constant="20" id="ArC-R2-jtc"/>
<constraint firstItem="57X-Ep-rfq" firstAttribute="leading" secondItem="mdL-JE-wCe" secondAttribute="leading" constant="20" id="PvV-gg-7us"/>
<constraint firstItem="57X-Ep-rfq" firstAttribute="top" secondItem="dmf-hv-bwx" secondAttribute="top" constant="20" id="QHM-k8-g0x"/>
<constraint firstItem="mdL-JE-wCe" firstAttribute="trailing" secondItem="57X-Ep-rfq" secondAttribute="trailing" constant="15" id="sGL-bx-qIk"/>
<constraint firstItem="1xN-9h-DFd" firstAttribute="top" secondItem="dmf-hv-bwx" secondAttribute="top" id="6rb-Bw-UVn"/>
<constraint firstAttribute="bottom" secondItem="1xN-9h-DFd" secondAttribute="bottom" id="dnI-NB-BKv"/>
<constraint firstAttribute="trailing" secondItem="1xN-9h-DFd" secondAttribute="trailing" id="kbY-Z6-V86"/>
<constraint firstItem="1xN-9h-DFd" firstAttribute="leading" secondItem="dmf-hv-bwx" secondAttribute="leading" id="ofk-a7-m0Y"/>
</constraints>
<edgeInsets key="layoutMargins" top="20" left="20" bottom="20" right="20"/>
<viewLayoutGuide key="safeArea" id="mdL-JE-wCe"/>
</view>
</subviews>
</view>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="dmf-hv-bwx" firstAttribute="top" secondItem="Kqf-Pv-ca3" secondAttribute="top" id="7yY-05-eHt"/>
<constraint firstAttribute="bottom" secondItem="dmf-hv-bwx" secondAttribute="bottom" id="Rrx-0k-6He"/>
<constraint firstItem="dmf-hv-bwx" firstAttribute="leading" secondItem="Kqf-Pv-ca3" secondAttribute="leading" id="W0V-sT-tXo"/>
<constraint firstAttribute="trailing" secondItem="dmf-hv-bwx" secondAttribute="trailing" id="tgy-Zi-iZF"/>
<constraint firstItem="dmf-hv-bwx" firstAttribute="leading" secondItem="Kqf-Pv-ca3" secondAttribute="leadingMargin" id="W0V-sT-tXo"/>
<constraint firstAttribute="trailingMargin" secondItem="dmf-hv-bwx" secondAttribute="trailing" id="tgy-Zi-iZF"/>
</constraints>
<viewLayoutGuide key="safeArea" id="C6r-zO-INg"/>
<connections>
<outlet property="appIconImageView" destination="jg6-wi-ngb" id="j83-Dl-GT6"/>
<outlet property="betaBadgeView" destination="4LS-dp-4VA" id="Q2Z-AG-Y19"/>
<outlet property="dateLabel" destination="xaB-Kc-Par" id="mfG-3C-r7j"/>
<outlet property="nameLabel" destination="qmI-m4-Mra" id="LQz-w7-HNb"/>
<outlet property="updateButton" destination="OSL-U2-BKa" id="WbI-96-Nel"/>
<outlet property="bannerView" destination="Nop-pL-Icx" id="GiX-K1-5oz"/>
<outlet property="blurView" destination="1xN-9h-DFd" id="HBI-nT-xYh"/>
<outlet property="versionDescriptionTextView" destination="rNs-2O-k3V" id="4TC-A3-oxb"/>
<outlet property="versionDescriptionTitleLabel" destination="h1u-nj-qsP" id="dnz-Yv-BdY"/>
</connections>
@@ -132,6 +114,8 @@
</collectionViewCell>
</objects>
<resources>
<image name="BetaBadge" width="41" height="17"/>
<namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@@ -13,13 +13,16 @@ class NewsCollectionViewCell: UICollectionViewCell
@IBOutlet var titleLabel: UILabel!
@IBOutlet var captionLabel: UILabel!
@IBOutlet var imageView: UIImageView!
@IBOutlet var contentBackgroundView: UIView!
override func awakeFromNib()
{
super.awakeFromNib()
self.contentView.layer.cornerRadius = 30
self.contentView.clipsToBounds = true
self.contentView.preservesSuperviewLayoutMargins = true
self.contentBackgroundView.layer.cornerRadius = 30
self.contentBackgroundView.clipsToBounds = true
self.imageView.layer.cornerRadius = 30
self.imageView.clipsToBounds = true

View File

@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -18,23 +16,26 @@
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xba-Qs-SQo">
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="azr-Ea-luN">
<rect key="frame" x="16" y="0.0" width="303" height="299"/>
</view>
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xba-Qs-SQo">
<rect key="frame" x="16" y="0.0" width="303" height="299"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="tNk-9u-1tk">
<rect key="frame" x="0.0" y="0.0" width="335" height="298.5"/>
<rect key="frame" x="0.0" y="0.0" width="303" height="298.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="akF-Tr-G5M">
<rect key="frame" x="0.0" y="0.0" width="335" height="98.5"/>
<rect key="frame" x="0.0" y="0.0" width="303" height="117.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Delta" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AkN-BE-I1a">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Delta" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="AkN-BE-I1a">
<rect key="frame" x="25" y="25" width="54.5" height="26.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" alpha="0.75" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="999" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SHB-kk-YhL">
<rect key="frame" x="25" y="61.5" width="35.5" height="17"/>
<label opaque="NO" userInteractionEnabled="NO" alpha="0.75" contentMode="left" horizontalHuggingPriority="251" verticalCompressionResistancePriority="999" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SHB-kk-YhL">
<rect key="frame" x="25" y="61.5" width="35.5" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -43,9 +44,9 @@
<edgeInsets key="layoutMargins" top="25" left="25" bottom="20" right="25"/>
</stackView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="335" placeholderIntrinsicHeight="200" translatesAutoresizingMaskIntoConstraints="NO" id="l36-Bm-De0">
<rect key="frame" x="0.0" y="98.5" width="335" height="200"/>
<rect key="frame" x="0.0" y="117.5" width="303" height="181"/>
<constraints>
<constraint firstAttribute="width" secondItem="l36-Bm-De0" secondAttribute="height" multiplier="67:40" id="QGD-YE-Hw2"/>
<constraint firstAttribute="width" secondItem="l36-Bm-De0" secondAttribute="height" multiplier="67:40" priority="999" id="QGD-YE-Hw2"/>
</constraints>
</imageView>
</subviews>
@@ -63,15 +64,21 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Xba-Qs-SQo" firstAttribute="top" secondItem="wRF-2R-NUG" secondAttribute="top" id="0xe-Rt-MhF"/>
<constraint firstItem="Xba-Qs-SQo" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leading" id="5MO-c0-5rG"/>
<constraint firstAttribute="trailing" secondItem="Xba-Qs-SQo" secondAttribute="trailing" id="DNL-Jj-3By"/>
<constraint firstItem="Xba-Qs-SQo" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leadingMargin" id="5MO-c0-5rG"/>
<constraint firstItem="azr-Ea-luN" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leadingMargin" id="8Ck-dI-nJy"/>
<constraint firstAttribute="trailingMargin" secondItem="Xba-Qs-SQo" secondAttribute="trailing" id="DNL-Jj-3By"/>
<constraint firstAttribute="bottom" secondItem="Xba-Qs-SQo" secondAttribute="bottom" id="Ecj-fN-hZv"/>
<constraint firstAttribute="bottom" secondItem="azr-Ea-luN" secondAttribute="bottom" priority="999" id="e56-UD-DRT"/>
<constraint firstItem="azr-Ea-luN" firstAttribute="top" secondItem="wRF-2R-NUG" secondAttribute="top" id="h2k-WE-Esg"/>
<constraint firstAttribute="trailingMargin" secondItem="azr-Ea-luN" secondAttribute="trailing" priority="999" id="hsS-zC-A58"/>
</constraints>
<connections>
<outlet property="captionLabel" destination="SHB-kk-YhL" id="zY3-qQ-9oY"/>
<outlet property="contentBackgroundView" destination="azr-Ea-luN" id="2Pl-11-YvR"/>
<outlet property="imageView" destination="l36-Bm-De0" id="3do-aQ-5r4"/>
<outlet property="titleLabel" destination="AkN-BE-I1a" id="hA2-3O-q5J"/>
</connections>
<point key="canvasLocation" x="138" y="153"/>
</collectionViewCell>
</objects>
</document>

View File

@@ -22,8 +22,17 @@ private class AppBannerFooterView: UICollectionReusableView
{
super.init(frame: frame)
self.addSubview(self.bannerView, pinningEdgesWith: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20))
self.addGestureRecognizer(self.tapGestureRecognizer)
self.bannerView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.bannerView)
NSLayoutConstraint.activate([
self.bannerView.topAnchor.constraint(equalTo: self.topAnchor),
self.bannerView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
self.bannerView.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor),
self.bannerView.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
@@ -52,7 +61,6 @@ class NewsViewController: UICollectionViewController
super.viewDidLoad()
self.prototypeCell = NewsCollectionViewCell.instantiate(with: NewsCollectionViewCell.nib!)
self.prototypeCell.translatesAutoresizingMaskIntoConstraints = false
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.dataSource = self.dataSource
@@ -93,15 +101,18 @@ private extension NewsViewController
let fetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: false)]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.date), cacheName: nil)
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.sortIndex), cacheName: nil)
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>(fetchedResultsController: fetchedResultsController)
dataSource.proxy = self
dataSource.cellConfigurationHandler = { (cell, newsItem, indexPath) in
let cell = cell as! NewsCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.titleLabel.text = newsItem.title
cell.captionLabel.text = newsItem.caption
cell.contentView.backgroundColor = newsItem.tintColor
cell.contentBackgroundView.backgroundColor = newsItem.tintColor
cell.imageView.image = nil
@@ -315,6 +326,9 @@ extension NewsViewController
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner", for: indexPath) as! AppBannerFooterView
guard let storeApp = item.storeApp else { return footerView }
footerView.layoutMargins.left = self.view.layoutMargins.left
footerView.layoutMargins.right = self.view.layoutMargins.right
footerView.bannerView.titleLabel.text = storeApp.name
footerView.bannerView.subtitleLabel.text = storeApp.developerName
footerView.bannerView.tintColor = storeApp.tintColor
@@ -330,7 +344,6 @@ extension NewsViewController
let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress
footerView.bannerView.button.isInverted = false
if Date() < storeApp.versionDate
{
@@ -345,7 +358,6 @@ extension NewsViewController
{
footerView.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
footerView.bannerView.button.progress = nil
footerView.bannerView.button.isInverted = true
footerView.bannerView.button.countdownDate = nil
}
@@ -358,10 +370,7 @@ extension NewsViewController
extension NewsViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let padding = 40 as CGFloat
let width = collectionView.bounds.width - padding
{
let item = self.dataSource.item(at: indexPath)
if let previousSize = self.cachedCellSizes[item.identifier]
@@ -369,7 +378,7 @@ extension NewsViewController: UICollectionViewDelegateFlowLayout
return previousSize
}
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: width)
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
NSLayoutConstraint.activate([widthConstraint])
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
@@ -396,7 +405,7 @@ extension NewsViewController: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
{
var insets = UIEdgeInsets(top: 30, left: 20, bottom: 13, right: 20)
var insets = UIEdgeInsets(top: 30, left: 0, bottom: 13, right: 0)
if section == 0
{

View File

@@ -29,7 +29,7 @@ class AppOperationContext
var app: ALTApplication?
var resignedApp: ALTApplication?
var connection: NWConnection?
var installationConnection: ServerConnection?
var installedApp: InstalledApp? {
didSet {

View File

@@ -8,7 +8,9 @@
import Foundation
import Roxas
import Network
import AltKit
import AltSign
enum AuthenticationError: LocalizedError
@@ -30,13 +32,18 @@ enum AuthenticationError: LocalizedError
}
@objc(AuthenticationOperation)
class AuthenticationOperation: ResultOperation<ALTSigner>
class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
{
let group: OperationGroup
private weak var presentingViewController: UIViewController?
private lazy var navigationController: UINavigationController = {
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
navigationController.presentationController?.delegate = self
if #available(iOS 13.0, *)
{
navigationController.isModalInPresentation = true
}
return navigationController
}()
@@ -45,14 +52,18 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
private var appleIDPassword: String?
private var shouldShowInstructions = false
private var signer: ALTSigner?
private let operationQueue = OperationQueue()
init(presentingViewController: UIViewController?)
private var submitCodeAction: UIAlertAction?
init(group: OperationGroup, presentingViewController: UIViewController?)
{
self.group = group
self.presentingViewController = presentingViewController
super.init()
self.operationQueue.name = "com.altstore.AuthenticationOperation"
self.progress.totalUnitCount = 3
}
@@ -60,18 +71,24 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
{
super.main()
if let error = self.group.error
{
self.finish(.failure(error))
return
}
// Sign In
self.signIn { (result) in
self.signIn() { (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 account):
case .success(let account, let session):
self.progress.completedUnitCount += 1
// Fetch Team
self.fetchTeam(for: account) { (result) in
self.fetchTeam(for: account, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
@@ -81,7 +98,7 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
self.progress.completedUnitCount += 1
// Fetch Certificate
self.fetchCertificate(for: team) { (result) in
self.fetchCertificate(for: team, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
@@ -89,13 +106,24 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
case .failure(let error): self.finish(.failure(error))
case .success(let certificate):
self.progress.completedUnitCount += 1
let signer = ALTSigner(team: team, certificate: certificate)
self.signer = signer
self.showInstructionsIfNecessary() { (didShowInstructions) in
self.finish(.success(signer))
}
// 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:
let signer = ALTSigner(team: team, certificate: certificate)
// 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)
}
}
}
}
}
}
@@ -104,21 +132,65 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
}
}
override func finish(_ result: Result<ALTSigner, Error>)
func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
do
{
let account: Account
let team: Team
if let tempAccount = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context)
{
account = tempAccount
}
else
{
account = Account(altTeam.account, context: context)
}
if let tempTeam = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
{
team = tempTeam
}
else
{
team = Team(altTeam, account: account, context: context)
}
account.update(account: altTeam.account)
team.update(team: altTeam)
try context.save()
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
}
override func finish(_ result: Result<(ALTSigner, ALTAppleAPISession), Error>)
{
guard !self.isFinished else { return }
print("Finished authenticating with result:", result)
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
context.perform {
do
{
let signer = try result.get()
let altAccount = signer.team.account
let (signer, 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)
else { throw AuthenticationError.noTeam }
// Account
let account = Account(altAccount, context: context)
account.isActiveAccount = true
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
@@ -131,7 +203,6 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
}
// Team
let team = Team(signer.team, account: account, context: context)
team.isActiveTeam = true
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
@@ -143,25 +214,41 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
team.isActiveTeam = false
}
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.team == nil
{
// No team assigned to AltStore app yet, so assume this team was used to originally install it.
altStoreApp.team = team
}
// Save
try context.save()
// Update keychain
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
Keychain.shared.appleIDEmailAddress = signer.team.account.appleID
Keychain.shared.appleIDPassword = self.appleIDPassword
Keychain.shared.signingCertificateSerialNumber = signer.certificate.serialNumber
Keychain.shared.signingCertificatePrivateKey = signer.certificate.privateKey
Keychain.shared.signingCertificate = signer.certificate.p12Data()
Keychain.shared.signingCertificatePassword = signer.certificate.machineIdentifier
super.finish(.success(signer))
self.showInstructionsIfNecessary() { (didShowInstructions) in
// 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)
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
}
}
}
}
catch
{
super.finish(.failure(error))
}
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
super.finish(result)
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
}
}
}
}
@@ -194,21 +281,26 @@ private extension AuthenticationOperation
private extension AuthenticationOperation
{
func signIn(completionHandler: @escaping (Result<ALTAccount, Swift.Error>) -> Void)
func signIn(completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
{
func authenticate()
{
DispatchQueue.main.async {
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
authenticationViewController.authenticationHandler = { (result) in
if let (account, password) = result
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
self.authenticate(appleID: appleID, password: password) { (result) in
completionHandler(result)
}
}
authenticationViewController.completionHandler = { (result) in
if let (account, session, password) = result
{
// We presented the Auth UI and the user signed in.
// In this case, we'll assume we should show the instructions again.
self.shouldShowInstructions = true
self.appleIDPassword = password
completionHandler(.success(account))
completionHandler(.success((account, session)))
}
else
{
@@ -225,24 +317,17 @@ private extension AuthenticationOperation
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
{
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in
do
self.authenticate(appleID: appleID, password: password) { (result) in
switch result
{
case .success(let account, let session):
self.appleIDPassword = password
completionHandler(.success((account, session)))
let account = try Result(account, error).get()
completionHandler(.success(account))
}
catch ALTAppleAPIError.incorrectCredentials
{
case .failure(ALTAppleAPIError.incorrectCredentials), .failure(ALTAppleAPIError.appSpecificPasswordRequired):
authenticate()
}
catch ALTAppleAPIError.appSpecificPasswordRequired
{
authenticate()
}
catch
{
case .failure(let error):
completionHandler(.failure(error))
}
}
@@ -253,7 +338,78 @@ private extension AuthenticationOperation
}
}
func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
{
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(group: self.group)
fetchAnisetteDataOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let anisetteData):
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
if let presentingViewController = self.presentingViewController
{
verificationHandler = { (completionHandler) in
DispatchQueue.main.async {
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.keyboardType = .numberPad
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
}
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
let textField = alertController.textFields?.first
let code = textField?.text ?? ""
completionHandler(code)
}
submitAction.isEnabled = false
alertController.addAction(submitAction)
self.submitCodeAction = submitAction
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
completionHandler(nil)
})
if self.navigationController.presentingViewController != nil
{
self.navigationController.present(alertController, animated: true, completion: nil)
}
else
{
presentingViewController.present(alertController, animated: true, completion: nil)
}
}
}
}
else
{
// No view controller to present security code alert, so don't provide verificationHandler.
verificationHandler = nil
}
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
verificationHandler: verificationHandler) { (account, session, error) in
if let account = account, let session = session
{
completionHandler(.success((account, session)))
}
else
{
completionHandler(.failure(error ?? OperationError.unknown))
}
}
}
}
self.operationQueue.addOperation(fetchAnisetteDataOperation)
}
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
{
func selectTeam(from teams: [ALTTeam])
{
@@ -275,7 +431,7 @@ private extension AuthenticationOperation
}
}
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
switch Result(teams, error)
{
case .failure(let error): completionHandler(.failure(error))
@@ -294,18 +450,18 @@ private extension AuthenticationOperation
}
}
func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
{
func requestCertificate()
{
let machineName = "AltStore - " + UIDevice.current.name
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team) { (certificate, error) in
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
@@ -334,7 +490,7 @@ private extension AuthenticationOperation
{
guard let certificate = certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
if let error = error, !success
{
completionHandler(.failure(error))
@@ -346,25 +502,51 @@ private extension AuthenticationOperation
}
}
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
if
let data = Keychain.shared.signingCertificate,
let localCertificate = ALTCertificate(p12Data: data, password: nil),
let certificate = certificates.first(where: { $0.serialNumber == localCertificate.serialNumber })
{
// We have a certificate stored in the keychain and it hasn't been revoked.
localCertificate.machineIdentifier = certificate.machineIdentifier
completionHandler(.success(localCertificate))
}
else if
let serialNumber = Keychain.shared.signingCertificateSerialNumber,
let privateKey = Keychain.shared.signingCertificatePrivateKey,
let certificate = certificates.first(where: { $0.serialNumber == serialNumber })
{
// LEGACY
// We have the private key for one of the certificates, so add it to certificate and use it.
certificate.privateKey = privateKey
completionHandler(.success(certificate))
}
else if
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String,
let certificate = certificates.first(where: { $0.serialNumber == serialNumber }),
let machineIdentifier = certificate.machineIdentifier,
FileManager.default.fileExists(atPath: Bundle.main.certificateURL.path),
let data = try? Data(contentsOf: Bundle.main.certificateURL),
let localCertificate = ALTCertificate(p12Data: data, password: machineIdentifier)
{
// We have an embedded certificate that hasn't been revoked.
localCertificate.machineIdentifier = machineIdentifier
completionHandler(.success(localCertificate))
}
else if certificates.isEmpty
{
// No certificates, so request a new one.
requestCertificate()
}
else
{
// We don't have private keys for any of the certificates,
// so we need to revoke one and create a new one.
replaceCertificate(from: certificates)
}
}
@@ -375,6 +557,30 @@ private extension AuthenticationOperation
}
}
func cacheAppIDs(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let group = OperationGroup()
group.signer = signer
group.session = session
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
fetchAppIDsOperation.resultHandler = { (result) in
do
{
let (_, context) = try result.get()
try context.save()
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
self.operationQueue.addOperation(fetchAppIDsOperation)
}
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
{
guard self.shouldShowInstructions else { return completionHandler(false) }
@@ -392,19 +598,40 @@ private extension AuthenticationOperation
}
}
}
}
extension AuthenticationOperation: UIAdaptivePresentationControllerDelegate
{
func presentationControllerWillDismiss(_ presentationController: UIPresentationController)
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
{
if let signer = self.signer
{
self.finish(.success(signer))
}
else
{
self.finish(.failure(OperationError.cancelled))
guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) }
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
#if DEBUG
completionHandler(false)
#else
DispatchQueue.main.async {
let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController
refreshViewController.signer = signer
refreshViewController.session = session
refreshViewController.completionHandler = { _ in
completionHandler(true)
}
if !self.present(refreshViewController)
{
completionHandler(false)
}
}
#endif
}
}
extension AuthenticationOperation
{
@objc func textFieldTextDidChange(_ notification: Notification)
{
guard let textField = notification.object as? UITextField else { return }
self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6
}
}

View File

@@ -0,0 +1,73 @@
//
// FetchAnisetteDataOperation.swift
// AltStore
//
// Created by Riley Testut on 1/7/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
import AltKit
import Roxas
@objc(FetchAnisetteDataOperation)
class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
{
let group: OperationGroup
init(group: OperationGroup)
{
self.group = group
super.init()
}
override func main()
{
super.main()
if let error = self.group.error
{
self.finish(.failure(error))
return
}
guard let server = self.group.server else { return self.finish(.failure(OperationError.invalidParameters)) }
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error):
self.finish(.failure(error))
case .success(let connection):
print("Sending anisette data request...")
let request = AnisetteDataRequest()
connection.send(request) { (result) in
print("Sent anisette data request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
print("Waiting for anisette data...")
connection.receiveResponse() { (result) in
print("Receiving anisette data:", result)
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(.error(let response)): self.finish(.failure(response.error))
case .success(.anisetteData(let response)): self.finish(.success(response.anisetteData))
case .success: self.finish(.failure(ALTServerError(.unknownRequest)))
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,73 @@
//
// FetchAppIDsOperation.swift
// AltStore
//
// Created by Riley Testut on 1/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
import AltKit
import Roxas
@objc(FetchAppIDsOperation)
class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
{
let group: OperationGroup
let context: NSManagedObjectContext
init(group: OperationGroup, context: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
{
self.group = group
self.context = context
super.init()
}
override func main()
{
super.main()
if let error = self.group.error
{
self.finish(.failure(error))
return
}
guard
let team = self.group.signer?.team,
let session = self.group.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
self.context.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 }
let fetchedIdentifiers = fetchedAppIDs.map { $0.identifier }
let deletedAppIDsRequest = AppID.fetchRequest() as NSFetchRequest<AppID>
deletedAppIDsRequest.predicate = NSPredicate(format: "%K == %@ AND NOT (%K IN %@)",
#keyPath(AppID.team), team,
#keyPath(AppID.identifier), fetchedIdentifiers)
let deletedAppIDs = try self.context.fetch(deletedAppIDsRequest)
deletedAppIDs.forEach { self.context.delete($0) }
let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.context) }
self.finish(.success((appIDs, self.context)))
}
catch
{
self.finish(.failure(error))
}
}
}
}
}

View File

@@ -67,6 +67,16 @@ class FetchSourceOperation: ResultOperation<Source>
decoder.managedObjectContext = context
let source = try decoder.decode(Source.self, from: data)
if let patreonAccessToken = source.userInfo?[.patreonAccessToken]
{
Keychain.shared.patreonCreatorAccessToken = patreonAccessToken
}
#if STAGING
source.sourceURL = self.sourceURL
#endif
self.finish(.success(source))
}
catch

View File

@@ -7,13 +7,26 @@
//
import Foundation
import AltKit
import Roxas
private extension Notification.Name
{
static let didReceiveWiredServerConnectionResponse = Notification.Name("io.altstore.didReceiveWiredServerConnectionResponse")
}
private let ReceivedWiredServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{ (center, observer, name, object, userInfo) in
NotificationCenter.default.post(name: .didReceiveWiredServerConnectionResponse, object: nil)
}
@objc(FindServerOperation)
class FindServerOperation: ResultOperation<Server>
{
let group: OperationGroup
private var isWiredServerConnectionAvailable = false
init(group: OperationGroup)
{
self.group = group
@@ -31,21 +44,49 @@ class FindServerOperation: ResultOperation<Server>
return
}
if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred })
{
// Preferred server.
self.finish(.success(server))
}
else if let server = ServerManager.shared.discoveredServers.first
{
// Any available server.
self.finish(.success(server))
}
else
{
// No servers.
self.finish(.failure(ConnectionError.serverNotFound))
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
// Prepare observers to receive callback from wired server (if connected).
CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedWiredServerConnectionResponse, CFNotificationName.wiredServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately)
NotificationCenter.default.addObserver(self, selector: #selector(FindServerOperation.didReceiveWiredServerConnectionResponse(_:)), name: .didReceiveWiredServerConnectionResponse, object: nil)
// Post notification.
CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionAvailableRequest, nil, nil, true)
// Wait for either callback or timeout.
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
if self.isWiredServerConnectionAvailable
{
let server = Server(isWiredConnection: true)
self.finish(.success(server))
}
else
{
if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred })
{
// Preferred server.
self.finish(.success(server))
}
else if let server = ServerManager.shared.discoveredServers.first
{
// Any available server.
self.finish(.success(server))
}
else
{
// No servers.
self.finish(.failure(ConnectionError.serverNotFound))
}
}
}
}
}
private extension FindServerOperation
{
@objc func didReceiveWiredServerConnectionResponse(_ notification: Notification)
{
self.isWiredServerConnectionAvailable = true
}
}

View File

@@ -18,6 +18,8 @@ class InstallAppOperation: ResultOperation<InstalledApp>
{
let context: AppOperationContext
private var didCleanUp = false
init(context: AppOperationContext)
{
self.context = context
@@ -39,12 +41,13 @@ class InstallAppOperation: ResultOperation<InstalledApp>
guard
let resignedApp = self.context.resignedApp,
let connection = self.context.connection,
let server = self.context.group.server
let connection = self.context.installationConnection
else { return self.finish(.failure(OperationError.invalidParameters)) }
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
backgroundContext.perform {
/* App */
let installedApp: InstalledApp
// Fetch + update rather than insert + resolve merge conflicts to prevent potential context-level conflicts.
@@ -57,24 +60,64 @@ class InstallAppOperation: ResultOperation<InstalledApp>
installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, context: backgroundContext)
}
installedApp.version = resignedApp.version
if let profile = resignedApp.provisioningProfile
installedApp.update(resignedApp: resignedApp)
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
{
installedApp.refreshedDate = profile.creationDate
installedApp.expirationDate = profile.expirationDate
installedApp.team = team
}
/* App Extensions */
var installedExtensions = Set<InstalledExtension>()
if
let bundle = Bundle(url: resignedApp.fileURL),
let directory = bundle.builtInPlugInsURL,
let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
{
for case let fileURL as URL in enumerator
{
guard let appExtensionBundle = Bundle(url: fileURL) else { continue }
guard let appExtension = ALTApplication(fileURL: appExtensionBundle.bundleURL) else { continue }
let parentBundleID = self.context.bundleIdentifier
let resignedParentBundleID = resignedApp.bundleIdentifier
let resignedBundleID = appExtension.bundleIdentifier
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
let installedExtension: InstalledExtension
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
{
installedExtension = appExtension
}
else
{
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: backgroundContext)
}
installedExtension.update(resignedAppExtension: appExtension)
installedExtensions.insert(installedExtension)
}
}
installedApp.appExtensions = installedExtensions
// 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)
let request = BeginInstallationRequest()
server.send(request, via: connection) { (result) in
connection.send(request) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
self.receive(from: connection, server: server) { (result) in
self.receive(from: connection) { (result) in
switch result
{
case .success:
@@ -92,27 +135,43 @@ class InstallAppOperation: ResultOperation<InstalledApp>
}
}
func receive(from connection: NWConnection, server: Server, completionHandler: @escaping (Result<Void, Error>) -> Void)
override func finish(_ result: Result<InstalledApp, Error>)
{
server.receive(ServerResponse.self, from: connection) { (result) in
self.cleanUp()
super.finish(result)
}
}
private extension InstallAppOperation
{
func receive(from connection: ServerConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
connection.receiveResponse() { (result) in
do
{
let response = try result.get()
print(response)
if let error = response.error
switch response
{
completionHandler(.failure(error))
}
else if response.progress == 1.0
{
self.progress.completedUnitCount = self.progress.totalUnitCount
completionHandler(.success(()))
}
else
{
self.progress.completedUnitCount = Int64(response.progress * 100)
self.receive(from: connection, server: server, completionHandler: completionHandler)
case .installationProgress(let response):
if response.progress == 1.0
{
self.progress.completedUnitCount = self.progress.totalUnitCount
completionHandler(.success(()))
}
else
{
self.progress.completedUnitCount = Int64(response.progress * 100)
self.receive(from: connection, completionHandler: completionHandler)
}
case .error(let response):
completionHandler(.failure(response.error))
default:
completionHandler(.failure(ALTServerError(.unknownRequest)))
}
}
catch
@@ -121,4 +180,25 @@ class InstallAppOperation: ResultOperation<InstalledApp>
}
}
}
func cleanUp()
{
guard !self.didCleanUp else { return }
self.didCleanUp = true
do
{
try FileManager.default.removeItem(at: self.context.temporaryDirectory)
if let app = self.context.app
{
let fileURL = InstalledApp.refreshedIPAURL(for: app)
try FileManager.default.removeItem(at: fileURL)
}
}
catch
{
print("Failed to remove temporary directory.", error)
}
}
}

View File

@@ -24,6 +24,8 @@ enum OperationError: LocalizedError
case invalidParameters
case iOSVersionNotSupported(ALTApplication)
case sideloadingAppNotSupported(ALTApplication)
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
case noSources
@@ -49,6 +51,53 @@ 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: "")
}
}
var recoverySuggestion: String? {
switch self
{
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
let message: String
if requiredAppIDs > 1
{
let availableText: String
switch availableAppIDs
{
case 0: availableText = NSLocalizedString("none are available", comment: "")
case 1: availableText = NSLocalizedString("only 1 is available", comment: "")
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
}
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
message = prefixMessage + " " + baseMessage
}
else
{
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.maximumUnitCount = 1
dateComponentsFormatter.unitsStyle = .full
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
message = baseMessage + " " + remainingTimeMessage
}
return message
default: return nil
}
}
}

View File

@@ -18,6 +18,8 @@ class OperationGroup
var completionHandler: ((Result<[String: Result<InstalledApp, Error>], Error>) -> Void)?
var beginInstallationHandler: ((InstalledApp) -> Void)?
var session: ALTAppleAPISession?
var server: Server?
var signer: ALTSigner?
@@ -73,7 +75,12 @@ class OperationGroup
func progress(for app: AppProtocol) -> Progress?
{
let progress = self.progressByBundleIdentifier[app.bundleIdentifier]
return self.progress(forAppWithBundleIdentifier: app.bundleIdentifier)
}
func progress(forAppWithBundleIdentifier bundleIdentifier: String) -> Progress?
{
let progress = self.progressByBundleIdentifier[bundleIdentifier]
return progress
}
}

View File

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

@@ -37,49 +37,45 @@ class ResignAppOperation: ResultOperation<ALTApplication>
guard
let app = self.context.app,
let signer = self.context.group.signer
let signer = self.context.group.signer,
let session = self.context.group.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
// Register Device
self.registerCurrentDevice(for: signer.team) { (result) in
guard let _ = self.process(result) else { return }
// Prepare Provisioning Profiles
self.prepareProvisioningProfiles(app.fileURL, team: signer.team, session: session) { (result) in
guard let profiles = self.process(result) else { return }
// Prepare Provisioning Profiles
self.prepareProvisioningProfiles(app.fileURL, team: signer.team) { (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, signer: signer, 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)
}
}
@@ -104,114 +100,113 @@ class ResignAppOperation: ResultOperation<ALTApplication>
private extension ResignAppOperation
{
func registerCurrentDevice(for team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
return completionHandler(.failure(OperationError.unknownUDID))
}
guard let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
ALTAppleAPI.shared.fetchDevices(for: team) { (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) { (device, error) in
completionHandler(Result(device, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
{
guard let bundle = Bundle(url: fileURL), let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
let dispatchGroup = DispatchGroup()
var profiles = [String: ALTProvisioningProfile]()
var error: Error?
dispatchGroup.enter()
self.prepareProvisioningProfile(for: app, team: team) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile):
profiles[app.bundleIdentifier] = profile
}
dispatchGroup.leave()
}
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 }
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, team: team) { (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, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
// Register
self.register(app, team: team) { (result) in
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
case .success(let profile):
var profiles = [app.bundleIdentifier: profile]
var error: Error?
// Update features
self.updateFeatures(for: appID, app: app, team: team) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
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
}
// Update app groups
self.updateAppGroups(for: appID, app: app, team: team) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Fetch Provisioning Profile
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
completionHandler(result)
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)
}
}
}
}
@@ -221,24 +216,63 @@ private extension ResignAppOperation
}
}
func register(_ app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
let appName = app.name
let bundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
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 == bundleID })
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleIdentifier })
{
completionHandler(.success(appID))
}
else
{
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
completionHandler(Result(appID, error))
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))
}
}
}
}
@@ -249,7 +283,7 @@ private extension ResignAppOperation
}
}
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
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 }
@@ -263,15 +297,41 @@ private extension ResignAppOperation
features[.appGroups] = true
}
let appID = appID.copy() as! ALTAppID
appID.features = features
var updateFeatures = false
ALTAppleAPI.shared.update(appID, team: team) { (appID, error) in
completionHandler(Result(appID, error))
// 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, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
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 {
@@ -287,7 +347,7 @@ private extension ResignAppOperation
// Assign App Group
// TODO: Determine whether app already belongs to app group.
ALTAppleAPI.shared.add(appID, to: group, team: team) { (success, error) in
ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
let result = result.map { _ in appID }
completionHandler(result)
}
@@ -296,7 +356,7 @@ private extension ResignAppOperation
let adjustedGroupIdentifier = "group.\(team.identifier)." + groupIdentifier
ALTAppleAPI.shared.fetchAppGroups(for: team) { (groups, error) in
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
switch Result(groups, error)
{
case .failure(let error): completionHandler(.failure(error))
@@ -311,7 +371,7 @@ private extension ResignAppOperation
// 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) { (group, error) in
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
finish(Result(group, error))
}
}
@@ -319,23 +379,23 @@ private extension ResignAppOperation
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
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) { (success, error) in
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) { (profile, error) in
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
@@ -371,6 +431,17 @@ private extension ResignAppOperation
infoDictionary[Bundle.Info.appGroups] = appGroups
}
// Add app-specific exported UTI so we can check later if this app (extension) is installed or not.
let installedAppUTI = ["UTTypeConformsTo": [],
"UTTypeDescription": "AltStore Installed App",
"UTTypeIconFiles": [],
"UTTypeIdentifier": InstalledApp.installedAppUTI(forBundleIdentifier: profile.bundleIdentifier),
"UTTypeTagSpecification": [:]] as [String : Any]
var exportedUTIs = infoDictionary[Bundle.Info.exportedUTIs] as? [[String: Any]] ?? []
exportedUTIs.append(installedAppUTI)
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
}
@@ -400,6 +471,21 @@ private extension ResignAppOperation
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
additionalValues[Bundle.Info.deviceID] = udid
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
if
let data = Keychain.shared.signingCertificate,
let signingCertificate = ALTCertificate(p12Data: data, password: nil),
let encryptingPassword = Keychain.shared.signingCertificatePassword
{
additionalValues[Bundle.Info.certificateID] = signingCertificate.serialNumber
let encryptedData = signingCertificate.encryptedP12Data(withPassword: encryptingPassword)
try encryptedData?.write(to: appBundle.certificateURL, options: .atomic)
}
else
{
// The embedded certificate + certificate identifier are already in app bundle, no need to update them.
}
}
// Prepare app

View File

@@ -12,13 +12,13 @@ import Network
import AltKit
@objc(SendAppOperation)
class SendAppOperation: ResultOperation<NWConnection>
class SendAppOperation: ResultOperation<ServerConnection>
{
let context: AppOperationContext
private let dispatchQueue = DispatchQueue(label: "com.altstore.SendAppOperation")
private var connection: NWConnection?
private var serverConnection: ServerConnection?
init(context: AppOperationContext)
{
@@ -45,21 +45,21 @@ class SendAppOperation: ResultOperation<NWConnection>
let fileURL = InstalledApp.refreshedIPAURL(for: app)
// Connect to server.
self.connect(to: server) { (result) in
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
self.connection = connection
case .success(let serverConnection):
self.serverConnection = serverConnection
// Send app to server.
self.sendApp(at: fileURL, via: connection, server: server) { (result) in
self.sendApp(at: fileURL, via: serverConnection) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
self.progress.completedUnitCount += 1
self.finish(.success(connection))
self.finish(.success(serverConnection))
}
}
}
@@ -69,34 +69,7 @@ class SendAppOperation: ResultOperation<NWConnection>
private extension SendAppOperation
{
func connect(to server: Server, completionHandler: @escaping (Result<NWConnection, Error>) -> Void)
{
let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp)
connection.stateUpdateHandler = { [unowned connection] (state) in
switch state
{
case .failed(let error):
print("Failed to connect to service \(server.service.name).", error)
completionHandler(.failure(ConnectionError.connectionFailed))
case .cancelled:
completionHandler(.failure(OperationError.cancelled))
case .ready:
completionHandler(.success(connection))
case .waiting: break
case .setup: break
case .preparing: break
@unknown default: break
}
}
connection.start(queue: self.dispatchQueue)
}
func sendApp(at fileURL: URL, via connection: NWConnection, server: Server, completionHandler: @escaping (Result<Void, Error>) -> Void)
func sendApp(at fileURL: URL, via connection: ServerConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
@@ -106,14 +79,14 @@ private extension SendAppOperation
let request = PrepareAppRequest(udid: udid, contentSize: appData.count)
print("Sending request \(request)")
server.send(request, via: connection) { (result) in
connection.send(request) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success:
print("Sending app data (\(appData.count) bytes)")
server.send(appData, via: connection, prependSize: false) { (result) in
connection.send(appData, prependSize: false) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))

View File

@@ -11,7 +11,6 @@ import AuthenticationServices
private let clientID = "ZMx0EGUWe4TVWYXNZZwK_fbIK5jHFVWoUf1Qb-sqNXmT-YzAGwDPxxq7ak3_W5Q2"
private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt"
private let creatorAccessToken = "NSX1ts9Rf9IzKRCu8GjbwsZ6wll8bDtoJxNbPbp2eZo"
private let campaignID = "2863968"
@@ -71,7 +70,7 @@ extension PatreonAPI
}
}
class PatreonAPI
class PatreonAPI: NSObject
{
static let shared = PatreonAPI()
@@ -84,8 +83,9 @@ class PatreonAPI
private let session = URLSession(configuration: .ephemeral)
private let baseURL = URL(string: "https://www.patreon.com/")!
private init()
private override init()
{
super.init()
}
}
@@ -129,6 +129,11 @@ extension PatreonAPI
}
}
if #available(iOS 13.0, *)
{
self.authenticationSession?.presentationContextProvider = self
}
self.authenticationSession?.start()
}
@@ -165,7 +170,8 @@ extension PatreonAPI
var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(campaignID)/members")!
components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"),
URLQueryItem(name: "fields[tier]", value: "title"),
URLQueryItem(name: "fields[member]", value: "full_name,patron_status")]
URLQueryItem(name: "fields[member]", value: "full_name,patron_status"),
URLQueryItem(name: "page[size]", value: "1000")]
let requestURL = components.url(relativeTo: self.baseURL)!
@@ -342,7 +348,9 @@ private extension PatreonAPI
{
case .none: break
case .creator:
guard let creatorAccessToken = Keychain.shared.patreonCreatorAccessToken else { return completion(.failure(Error.invalidAccessToken)) }
request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization")
case .user:
guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(Error.notAuthenticated)) }
request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
@@ -384,3 +392,12 @@ private extension PatreonAPI
task.resume()
}
}
@available(iOS 13.0, *)
extension PatreonAPI: ASWebAuthenticationPresentationContextProviding
{
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor
{
return UIApplication.shared.keyWindow ?? UIWindow()
}
}

View File

@@ -0,0 +1,38 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "1.000",
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "28",
"alpha" : "1.000",
"blue" : "30",
"green" : "28"
}
}
}
]
}

View File

@@ -0,0 +1,38 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "255",
"alpha" : "0.300",
"blue" : "255",
"green" : "255"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0",
"alpha" : "0.300",
"blue" : "0",
"green" : "0"
}
}
}
]
}

View File

@@ -0,0 +1,38 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "1",
"alpha" : "1.000",
"blue" : "132",
"green" : "128"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "2",
"alpha" : "1.000",
"blue" : "103",
"green" : "82"
}
}
}
]
}

View File

@@ -0,0 +1,38 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.008",
"alpha" : "1.000",
"blue" : "0.404",
"green" : "0.322"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.004",
"alpha" : "1.000",
"blue" : "0.518",
"green" : "0.502"
}
}
}
]
}

View File

@@ -7,14 +7,14 @@
"name": "AltStore",
"bundleIdentifier": "com.rileytestut.AltStore",
"developerName": "Riley Testut",
"version": "1.0.1",
"versionDate": "2019-09-28T03:00:00-07:00",
"versionDescription": "Fixes Patreon bugs.",
"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": 3481256,
"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",
@@ -35,14 +35,15 @@
"name": "AltStore",
"bundleIdentifier": "com.rileytestut.AltStore.Beta",
"developerName": "Riley Testut",
"version": "1.0.1b",
"versionDate": "2019-09-28T03:00:00-07:00",
"versionDescription": "- Adds support for sideloading apps via \"Open In...\"\n- Fixes Patreon bugs",
"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",
"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": 3481256,
"size": 2114670,
"beta": true,
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/65605563-2f009d00-df5e-11e9-9b40-1f36135d5c80.PNG",
@@ -65,14 +66,14 @@
"bundleIdentifier": "com.rileytestut.Delta",
"developerName": "Riley Testut",
"subtitle": "Classic games in your pocket.",
"version": "1.0",
"versionDate": "2019-09-28T12:00:00-07:00",
"versionDescription": "Initial version.",
"version": "1.1.2",
"versionDate": "2020-02-04T15:30:00-08:00",
"versionDescription": "• Fixes crash when running on iOS 13.3.1",
"downloadURL": "https://f000.backblazeb2.com/file/altstore/delta.ipa",
"localizedDescription": "Delta is an all-in-one emulator for iOS. Delta builds upon the strengths of its predecessor, GBA4iOS, while expanding to include support for more game systems such as NES, SNES, and N64.\n\nFEATURES\n\nSupported Game Systems\n• Nintendo Entertainment System\n• Super Nintendo Entertainment System\n• Nintendo 64\n• Game Boy (Color)\n• Game Boy Advance\n• And plenty more to come!\n\nController Support\n• Supports PS4, Xbox One S, and MFi game controllers.\n• Supports bluetooth (and wired) keyboards, as well as the Apple Smart Keyboard.\n• Completely customize button mappings on a per-system, per-controller basis.\n• Map buttons to special “Quick Save”, “Quick Load,” and “Fast Forward” actions.\n\nSave States\n• Save and load save states for any game from the pause menu.\n• Lock save states to prevent them from being accidentally overwritten.\n• Automatically makes backup save states to ensure you never lose your progress.\n• Support for “Quick Saves,” save states that can be quickly saved/loaded with a single button press (requires external controller).\n\nCheats\n• Supports various types of cheat codes for each supported system:\n• NES: Game Genie\n• SNES: Game Genie, Pro Action Replay\n• N64: GameShark\n• GBC: Game Genie, GameShark\n• GBA: Action Replay, Code Breaker, GameShark\n\nDelta Sync\n• Sync your games, game saves, save states, cheats, controller skins, and controller mappings between devices.\n• View version histories of everything you sync and optionally restore them to earlier versions.\n• Supports both Google Drive and Dropbox.\n\nCustom Controller Skins\n• Beautiful built-in controller skins for all systems.\n• Import controller skins made by others, or even make your own to share with the world!\n\nHold Button\n• Choose buttons for Delta to hold down on your behalf, freeing up your thumbs to press other buttons instead.\n• Perfect for games that typically require one button be held down constantly (ex: run button in Mario games, or the A button in Mario Kart).\n\nFast Forward\n• Speed through slower parts of games by running the game much faster than normal.\n• Easily enable or disable from the pause menu, or optionally with a mapped button on an external controller.\n\n3D/Haptic Touch\n• Use 3D or Haptic Touch to “peek” at games, save states, and cheat codes.\n• App icon shortcuts allow quick access to your most recently played games, or optionally customize the shortcuts to always include certain games.\n\nGame Artwork\n• Automatically displays appropriate box art for imported games.\n• Change a games artwork to anything you want, or select from the built-in game artwork database.\n\nMisc.\n• Gyroscope support for WarioWare: Twisted!\n• Support for delta:// URL scheme to jump directly into a specific game.\n\n**Delta and AltStore LLC are in no way affiliated with Nintendo. The name \"Nintendo\" and all associated game console names are registered trademarks of Nintendo Co., Ltd.**",
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
"tintColor": "8A28F7",
"size": 23075523,
"size": 17542718,
"permissions": [
{
"type": "photos",
@@ -91,14 +92,14 @@
"bundleIdentifier": "com.rileytestut.Delta.Beta",
"developerName": "Riley Testut",
"subtitle": "Classic games in your pocket.",
"version": "1.0b",
"versionDate": "2019-09-28T12:00:00-07:00",
"versionDescription": "Includes initial support for DS games.",
"version": "1.2b",
"versionDate": "2020-02-11T16:30:00-08:00",
"versionDescription": "GENERAL\n• Replaces 3D Touch peek/pop with iOS 13 context menus\n\nCONTROLLER SKINS\n• Brand new Nintendo DS controller skin\n• New Quick Save, Quick Load, and Fast Forward inputs for controller skins\n• Apply video filters with controller skins\n• Assign controller skins to individual games",
"downloadURL": "https://f000.backblazeb2.com/file/altstore/delta-beta.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": 23075071,
"size": 17631726,
"beta": true,
"permissions": [
{
@@ -114,6 +115,31 @@
"https://user-images.githubusercontent.com/705880/65601117-58b5c600-df56-11e9-9c19-9a5ba5da54cf.PNG"
]
},
{
"name": "Clip",
"bundleIdentifier": "com.rileytestut.Clip.Beta",
"subtitle": "Manage your clipboard history with ease.",
"developerName": "Riley Testut",
"version": "1.0b",
"versionDate": "2019-01-24T18:20:00-08:00",
"versionDescription": "• Disables notification vibration when sound is disabled for notifications.\n\nAssuming all goes well, this should be the final beta before the public launch! Please report any last minute bugs you find to support@altstore.io.",
"downloadURL": "https://f000.backblazeb2.com/file/altstore/clip-beta.ipa",
"localizedDescription": "Clip is a simple clipboard manager for iOS. \n\nUnlike other clipboard managers, Clip can continue monitoring your clipboard while in the background. No longer do you need to remember to manually open or share to an app to save your clipboard; just copy and paste as you would normally do, and Clip will have your back.\n\nIn addition to background monitoring, Clip also has these features:\n\n• Save text, URLs, and images copied to the clipboard.\n• Copy, delete, or share any clippings saved to Clip.\n• Customizable history limit.\n\nDownload Clip today, and never worry about losing your clipboard again!",
"iconURL": "https://user-images.githubusercontent.com/705880/63391981-5326f800-c37a-11e9-99d8-760fd06bb601.png",
"tintColor": "EC008C",
"size": 462771,
"beta": true,
"permissions": [
{
"type": "background-audio",
"usageDescription": "Allows Clip to continuously monitor your clipboard in the background."
}
],
"screenshotURLs": [
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png",
"https://user-images.githubusercontent.com/705880/70830209-8e738980-1da4-11ea-8b3b-6e5fbc78adff.png"
]
},
{
"name": "Delta Lite",
"bundleIdentifier": "com.rileytestut.Delta.Lite",
@@ -194,12 +220,17 @@
"notify": false
},
{
"title": "Prevent AltStore Expiring",
"identifier": "altstore-expiring-bug-fix",
"caption": "Refresh AltStore at least once in “My Apps” to prevent it from expiring early. An update is coming soon to fix this bug.",
"tintColor": "fd423a",
"date": "2019-10-09",
"title": "Coming Soon: Clip",
"identifier": "clip-coming-soon",
"caption": "A clipboard manager that can run in the background. Beta available now for all Patrons.",
"tintColor": "EC008C",
"url": "https://twitter.com/altstoreio/status/1205597959699582977",
"imageURL": "https://user-images.githubusercontent.com/705880/65606598-04afdf00-df60-11e9-8f93-af6345d39557.png",
"date": "2019-12-16",
"notify": false
}
]
],
"userInfo": {
"patreonAccessToken": "JLh5bpOQsPg-HIRe6FHnRAktoeeU4JT5-Xk-y7njQrM"
}
}

View File

@@ -44,139 +44,22 @@ enum ConnectionError: LocalizedError
struct Server: Equatable
{
var identifier: String
var service: NetService
var identifier: String? = nil
var service: NetService? = nil
var isPreferred = false
var isWiredConnection = false
}
extension Server
{
// Defined in extension so we can still use the automatically synthesized initializer.
init?(service: NetService, txtData: Data)
{
{
let txtDictionary = NetService.dictionary(fromTXTRecord: txtData)
guard let identifierData = txtDictionary["serverID"], let identifier = String(data: identifierData, encoding: .utf8) else { return nil }
self.identifier = identifier
self.service = service
}
func send<T: Encodable>(_ payload: T, via connection: NWConnection, prependSize: Bool = true, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
let data: Data
if let payload = payload as? Data
{
data = payload
}
else
{
data = try JSONEncoder().encode(payload)
}
func process(_ error: Error?) -> Bool
{
if error != nil
{
completionHandler(.failure(ConnectionError.connectionDropped))
return false
}
else
{
return true
}
}
if prependSize
{
let requestSize = Int32(data.count)
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
guard process(error) else { return }
connection.send(content: data, completion: .contentProcessed { (error) in
guard process(error) else { return }
completionHandler(.success(()))
})
})
}
else
{
connection.send(content: data, completion: .contentProcessed { (error) in
guard process(error) else { return }
completionHandler(.success(()))
})
}
}
catch
{
print("Invalid request.", error)
completionHandler(.failure(ALTServerError(.invalidRequest)))
}
}
func receive<T: Decodable>(_ type: T.Type, from connection: NWConnection, completionHandler: @escaping (Result<T, Error>) -> Void)
{
let size = MemoryLayout<Int32>.size
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let response = try JSONDecoder().decode(T.self, from: data)
completionHandler(.success(response))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
}
private extension Server
{
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
self.identifier = identifier
}
}

View File

@@ -0,0 +1,146 @@
//
// ServerConnection.swift
// AltStore
//
// Created by Riley Testut on 1/7/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Network
import AltKit
class ServerConnection
{
var server: Server
var connection: NWConnection
init(server: Server, connection: NWConnection)
{
self.server = server
self.connection = connection
}
func send<T: Encodable>(_ payload: T, prependSize: Bool = true, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
let data: Data
if let payload = payload as? Data
{
data = payload
}
else
{
data = try JSONEncoder().encode(payload)
}
func process(_ error: Error?) -> Bool
{
if error != nil
{
completionHandler(.failure(ConnectionError.connectionDropped))
return false
}
else
{
return true
}
}
if prependSize
{
let requestSize = Int32(data.count)
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
self.connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
guard process(error) else { return }
self.connection.send(content: data, completion: .contentProcessed { (error) in
guard process(error) else { return }
completionHandler(.success(()))
})
})
}
else
{
connection.send(content: data, completion: .contentProcessed { (error) in
guard process(error) else { return }
completionHandler(.success(()))
})
}
}
catch
{
print("Invalid request.", error)
completionHandler(.failure(ALTServerError(.invalidRequest)))
}
}
func receiveResponse(completionHandler: @escaping (Result<ServerResponse, Error>) -> Void)
{
let size = MemoryLayout<Int32>.size
self.connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error)
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
self.connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error)
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
completionHandler(.success(response))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
}
private extension ServerConnection
{
func process(data: Data?, error: NWError?) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
}
}

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