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 "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 deviceID = "ALTDeviceID"
public static let serverID = "ALTServerID" public static let serverID = "ALTServerID"
public static let certificateID = "ALTCertificateID"
public static let appGroups = "ALTAppGroups" public static let appGroups = "ALTAppGroups"
public static let urlTypes = "CFBundleURLTypes" public static let urlTypes = "CFBundleURLTypes"
public static let exportedUTIs = "UTExportedTypeDeclarations"
} }
} }
@@ -26,4 +28,9 @@ public extension Bundle
let infoPlistURL = self.bundleURL.appendingPathComponent("Info.plist") let infoPlistURL = self.bundleURL.appendingPathComponent("Info.plist")
return infoPlistURL 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, ALTServerErrorInstallationFailed = 8,
ALTServerErrorMaximumFreeAppLimitReached = 9, ALTServerErrorMaximumFreeAppLimitReached = 9,
ALTServerErrorUnsupportediOSVersion = 10, ALTServerErrorUnsupportediOSVersion = 10,
ALTServerErrorUnknownRequest = 11,
ALTServerErrorUnknownResponse = 12,
ALTServerErrorInvalidAnisetteData = 13,
ALTServerErrorPluginNotFound = 14
}; };
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN

View File

@@ -61,6 +61,18 @@ NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServ
case ALTServerErrorUnsupportediOSVersion: case ALTServerErrorUnsupportediOSVersion:
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @""); 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 Foundation
import AltSign
public let ALTServerServiceType = "_altserver._tcp" public let ALTServerServiceType = "_altserver._tcp"
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself // Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {} extension ALTServerError.Code: Codable {}
protocol ServerMessage: Codable protocol ServerMessageProtocol: Codable
{ {
var version: Int { get } var version: Int { get }
var identifier: String { 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 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 udid: String
public var contentSize: Int 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 version = 1
public var identifier = "BeginInstallation" public var identifier = "BeginInstallationRequest"
public init() public init()
{ {
} }
} }
public struct ServerResponse: ServerMessage public struct ErrorResponse: ServerMessageProtocol
{ {
public var version = 1 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 progress: Double
public var error: ALTServerError? { public init(progress: Double)
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?)
{ {
self.progress = progress 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 "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 AltSign
import LaunchAtLogin 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 @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
@@ -25,16 +44,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet private var appMenu: NSMenu! @IBOutlet private var appMenu: NSMenu!
@IBOutlet private var connectedDevicesMenu: NSMenu! @IBOutlet private var connectedDevicesMenu: NSMenu!
@IBOutlet private var launchAtLoginMenuItem: NSMenuItem! @IBOutlet private var launchAtLoginMenuItem: NSMenuItem!
@IBOutlet private var installMailPluginMenuItem: NSMenuItem!
private weak var authenticationAppleIDTextField: NSTextField? private weak var authenticationAppleIDTextField: NSTextField?
private weak var authenticationPasswordTextField: NSSecureTextField? private weak var authenticationPasswordTextField: NSSecureTextField?
private var isMailPluginInstalled: Bool {
let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path)
return isMailPluginInstalled
}
func applicationDidFinishLaunching(_ aNotification: Notification) func applicationDidFinishLaunching(_ aNotification: Notification)
{ {
UserDefaults.standard.registerDefaults() UserDefaults.standard.registerDefaults()
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
ConnectionManager.shared.start() ConnectionManager.shared.start()
ALTDeviceManager.shared.start()
let item = NSStatusBar.system.statusItem(withLength: -1) let item = NSStatusBar.system.statusItem(withLength: -1)
guard let button = item.button else { return } guard let button = item.button else { return }
@@ -47,16 +74,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.connectedDevicesMenu.delegate = self self.connectedDevicesMenu.delegate = self
if !UserDefaults.standard.didPresentInitialNotification UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (success, error) in
{ guard success else { return }
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) if !UserDefaults.standard.didPresentInitialNotification
UNUserNotificationCenter.current().add(request) {
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: "")
UserDefaults.standard.didPresentInitialNotification = true 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 } 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.state = LaunchAtLogin.isEnabled ? .on : .off
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:)) 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 x = button.frame.origin.x
let y = button.frame.origin.y - 5 let y = button.frame.origin.y - 5
@@ -96,11 +139,7 @@ private extension AppDelegate
let alert = NSAlert() let alert = NSAlert()
alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "") alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "")
alert.informativeText = NSLocalizedString(""" alert.informativeText = NSLocalizedString("Your Apple ID and password are not saved and are only sent to Apple for authentication.", comment: "")
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: "")
let textFieldSize = NSSize(width: 300, height: 22) 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 password = passwordTextField.stringValue
let device = self.connectedDevices[index] 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 ALTDeviceManager.shared.installAltStore(to: device, appleID: username, password: password) { (result) in
switch result 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) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
case .failure(InstallError.cancelled): case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore // Ignore
break break
@@ -192,6 +238,74 @@ If you have two-factor authentication enabled, please create an app-specific pas
LaunchAtLogin.isEnabled.toggle() 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 extension AppDelegate: NSMenuDelegate

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-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> <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"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@@ -10,11 +11,11 @@
<objects> <objects>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/> <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"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews> <subviews>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zLd-d8-ghZ"> <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"> <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"/> <font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@@ -26,7 +27,7 @@
</connections> </connections>
</textField> </textField>
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9rp-Vx-rvB"> <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"> <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"/> <font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <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="authenticationAppleIDTextField" destination="zLd-d8-ghZ" id="wW5-0J-zdq"/>
<outlet property="authenticationPasswordTextField" destination="9rp-Vx-rvB" id="ZoC-DI-jzQ"/> <outlet property="authenticationPasswordTextField" destination="9rp-Vx-rvB" id="ZoC-DI-jzQ"/>
<outlet property="connectedDevicesMenu" destination="KJ9-WY-pW1" id="Mcv-64-iFU"/> <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"/> <outlet property="launchAtLoginMenuItem" destination="IyR-FQ-upe" id="Fxn-EP-hwH"/>
</connections> </connections>
</customObject> </customObject>
<customObject id="Arf-IC-5eb" customClass="SUUpdater"/>
<application id="hnw-xV-0zn" sceneMemberID="viewController"> <application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6"> <menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items> <items>
@@ -98,7 +101,17 @@
<menuItem title="Launch at Login" id="IyR-FQ-upe" userLabel="Launch At Login"> <menuItem title="Launch at Login" id="IyR-FQ-upe" userLabel="Launch At Login">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
</menuItem> </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 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"> <menuItem title="Quit AltServer" keyEquivalent="q" id="4sb-4s-VLi">
<connections> <connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/> <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 Foundation
import Network import Network
import AppKit
import AltKit import AltKit
@@ -53,10 +54,13 @@ class ConnectionManager
private lazy var listener = self.makeListener() private lazy var listener = self.makeListener()
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltServer.connections", qos: .utility) 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() 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() func start()
@@ -76,6 +80,16 @@ class ConnectionManager
default: break 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 private extension ConnectionManager
@@ -126,68 +140,18 @@ private extension ConnectionManager
} }
listener.newConnectionHandler = { [weak self] (connection) in listener.newConnectionHandler = { [weak self] (connection) in
self?.awaitRequest(from: connection) self?.prepare(connection)
} }
return listener return listener
} }
func disconnect(_ connection: NWConnection) func prepare(_ connection: NWConnection)
{ {
switch connection.state let clientConnection = ClientConnection(connection: .wireless(connection))
{
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)
guard !self.connections.contains(where: { $0 === clientConnection }) else { return }
self.connections.append(clientConnection)
connection.stateUpdateHandler = { [weak self] (state) in connection.stateUpdateHandler = { [weak self] (state) in
switch state switch state
@@ -196,20 +160,17 @@ private extension ConnectionManager
case .ready: case .ready:
print("Connected to client:", connection.endpoint) print("Connected to client:", connection.endpoint)
self?.handleRequest(for: clientConnection)
self?.receiveApp(from: connection) { (result) in
self?.finish(connection: connection, error: result.error)
}
case .waiting: case .waiting:
print("Waiting for connection...") print("Waiting for connection...")
case .failed(let error): case .failed(let error):
print("Failed to connect to service \(connection.endpoint).", error) print("Failed to connect to service \(connection.endpoint).", error)
self?.disconnect(connection) self?.disconnect(clientConnection)
case .cancelled: case .cancelled:
self?.disconnect(connection) self?.disconnect(clientConnection)
@unknown default: break @unknown default: break
} }
@@ -217,8 +178,124 @@ private extension ConnectionManager
connection.start(queue: self.dispatchQueue) connection.start(queue: self.dispatchQueue)
} }
}
func receiveApp(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void) 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 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? var temporaryURL: URL?
@@ -230,82 +307,74 @@ private extension ConnectionManager
catch { print("Failed to remove .ipa.", error) } 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 self.receiveApp(for: request, from: connection) { (result) in
print("Received request with result:", result) print("Received app with result:", result)
switch result switch result
{ {
case .failure(let error): finish(.failure(error)) case .failure(let error): finish(.failure(error))
case .success(let request): case .success(let fileURL):
self.receiveApp(for: request, from: connection) { (result) in temporaryURL = fileURL
print("Received app with result:", result)
print("Awaiting begin installation request...")
connection.receiveRequest() { (result) in
print("Received begin installation request with result:", result)
switch result switch result
{ {
case .failure(let error): finish(.failure(error)) case .failure(let error): finish(.failure(error))
case .success(let request, let fileURL): case .success(.beginInstallation):
temporaryURL = fileURL 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 switch result
{ {
case .failure(let error): finish(.failure(error)) case .failure(let error): finish(.failure(error))
case .success: case .success: finish(.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:
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 connection.receiveData(expectedBytes: request.contentSize) { (result) in
{
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
do do
{ {
print("Received app data!") print("Received app data!")
let data = try self.process(data: data, error: error, from: connection) let data = try result.get()
print("Processed app data!")
guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) } guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
@@ -316,7 +385,7 @@ private extension ConnectionManager
print("Wrote app to URL:", temporaryURL) print("Wrote app to URL:", temporaryURL)
completionHandler(.success((request, temporaryURL))) completionHandler(.success(temporaryURL))
} }
catch 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) let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
var isSending = false var isSending = false
@@ -356,9 +425,9 @@ private extension ConnectionManager
isSending = true isSending = true
print("Progress:", progress.fractionCompleted) 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 { serialQueue.async {
isSending = false 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 guard let device = notification.object as? ALTDevice else { return }
{ self.startNotificationConnection(to: device)
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)))
}
} }
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 guard let device = notification.object as? ALTDevice else { return }
self.stopNotificationConnection(to: device)
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)))
}
}
} }
} }

View File

@@ -8,6 +8,13 @@
import Cocoa import Cocoa
import UserNotifications 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 enum InstallError: LocalizedError
{ {
@@ -49,126 +56,147 @@ extension ALTDeviceManager
try? FileManager.default.removeItem(at: destinationDirectoryURL) try? FileManager.default.removeItem(at: destinationDirectoryURL)
} }
self.authenticate(appleID: appleID, password: password) { (result) in AnisetteDataManager.shared.requestAnisetteData { (result) in
do 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 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 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 do
{ {
let certificate = try result.get() let device = try result.get()
let content = UNMutableNotificationContent() self.fetchCertificate(for: team, session: session) { (result) in
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
do 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 self.downloadApp { (result) in
{
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
do 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 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 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 self.updateFeatures(for: appID, app: application, team: team, session: session) { (result) in
finish(result.error, title: "Failed to Install AltStore") 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 catch
{ {
finish(error, title: "Failed to Fetch Provisioning Profile") finish(error, title: "Failed to Register App")
} }
} }
} }
catch catch
{ {
finish(error, title: "Failed to Update App ID") finish(error, title: "Failed to Refresh Anisette Data")
} }
} }
} }
catch catch
{ {
finish(error, title: "Failed to Register App") finish(error, title: "Failed to Download AltStore")
} }
} }
} }
catch catch
{ {
finish(error, title: "Failed to Download AltStore") finish(error, title: "Failed to Fetch Certificate")
return
} }
} }
} }
catch catch
{ {
finish(error, title: "Failed to Fetch Certificate") finish(error, title: "Failed to Register Device")
} }
} }
} }
catch catch
{ {
finish(error, title: "Failed to Register Device") finish(error, title: "Failed to Fetch Team")
} }
} }
} }
catch catch
{ {
finish(error, title: "Failed to Fetch Team") finish(error, title: "Failed to Authenticate")
} }
} }
} }
catch catch
{ {
finish(error, title: "Failed to Authenticate") finish(error, title: "Failed to Fetch Anisette Data")
} }
} }
} }
func downloadApp(completionHandler: @escaping (Result<URL, Error>) -> Void) 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 let downloadTask = URLSession.shared.downloadTask(with: appURL) { (fileURL, response, error) in
do do
{ {
@@ -184,15 +212,57 @@ extension ALTDeviceManager
downloadTask.resume() 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 func handleVerificationCode(_ completionHandler: @escaping (String?) -> Void)
let result = Result(account, error) {
completionHandler(result) 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>) 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 do
{ {
let teams = try Result(teams, error).get() 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 do
{ {
let certificates = try Result(certificates, error).get() 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 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 do
{ {
try Result(success, error).get() try Result(success, error).get()
self.fetchCertificate(for: team, completionHandler: completionHandler) self.fetchCertificate(for: team, session: session, completionHandler: completionHandler)
} }
catch catch
{ {
@@ -318,13 +388,13 @@ To prevent this from happening, feel free to try again with another Apple ID to
} }
else else
{ {
ALTAppleAPI.shared.addCertificate(machineName: "AltStore", to: team) { (certificate, error) in ALTAppleAPI.shared.addCertificate(machineName: "AltStore", to: team, session: session) { (certificate, error) in
do do
{ {
let certificate = try Result(certificate, error).get() let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw InstallError.missingPrivateKey } 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 do
{ {
let certificates = try Result(certificates, error).get() 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)" 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 do
{ {
let appIDs = try Result(appIDs, error).get() 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 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)) 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 let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
guard let feature = ALTFeature(entitlement: entitlement) else { return nil } 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 let appID = appID.copy() as! ALTAppID
appID.features = features 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)) 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 do
{ {
let devices = try Result(devices, error).get() 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 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)) 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)) completionHandler(Result(profile, error))
} }
} }
@@ -449,8 +519,17 @@ To prevent this from happening, feel free to try again with another Apple ID to
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
infoDictionary[Bundle.Info.deviceID] = device.identifier infoDictionary[Bundle.Info.deviceID] = device.identifier
infoDictionary[Bundle.Info.serverID] = UserDefaults.standard.serverID infoDictionary[Bundle.Info.serverID] = UserDefaults.standard.serverID
infoDictionary[Bundle.Info.certificateID] = certificate.serialNumber
try (infoDictionary as NSDictionary).write(to: infoPlistURL) 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) let resigner = ALTSigner(team: team, certificate: certificate)
resigner.signApp(at: application.fileURL, provisioningProfiles: [profile]) { (success, error) in resigner.signApp(at: application.fileURL, provisioningProfiles: [profile]) { (success, error) in
do do
@@ -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 <Foundation/Foundation.h>
#import <AltSign/AltSign.h> #import <AltSign/AltSign.h>
@class ALTWiredConnection;
@class ALTNotificationConnection;
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
extern NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidConnect);
extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidDisconnect);
@interface ALTDeviceManager : NSObject @interface ALTDeviceManager : NSObject
@property (class, nonatomic, readonly) ALTDeviceManager *sharedManager; @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 *> *connectedDevices;
@property (nonatomic, readonly) NSArray<ALTDevice *> *availableDevices; @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; - (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 @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@@ -7,7 +7,10 @@
// //
#import "ALTDeviceManager.h" #import "ALTDeviceManager.h"
#import "NSError+ALTServerError.h"
#import "AltKit.h"
#import "ALTWiredConnection+Private.h"
#import "ALTNotificationConnection+Private.h"
#include <libimobiledevice/libimobiledevice.h> #include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h> #include <libimobiledevice/lockdown.h>
@@ -17,8 +20,10 @@
#include <libimobiledevice/misagent.h> #include <libimobiledevice/misagent.h>
void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *udid); 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 () @interface ALTDeviceManager ()
@@ -26,6 +31,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSProgress *> *installationProgress; @property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSProgress *> *installationProgress;
@property (nonatomic, readonly) dispatch_queue_t installationQueue; @property (nonatomic, readonly) dispatch_queue_t installationQueue;
@property (nonatomic, readonly) NSMutableSet<ALTDevice *> *cachedDevices;
@end @end
@implementation ALTDeviceManager @implementation ALTDeviceManager
@@ -50,11 +57,20 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
_installationProgress = [NSMutableDictionary dictionary]; _installationProgress = [NSMutableDictionary dictionary];
_installationQueue = dispatch_queue_create("com.rileytestut.AltServer.InstallationQueue", DISPATCH_QUEUE_SERIAL); _installationQueue = dispatch_queue_create("com.rileytestut.AltServer.InstallationQueue", DISPATCH_QUEUE_SERIAL);
_cachedDevices = [NSMutableSet set];
} }
return self; 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 *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
{ {
NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:4]; NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:4];
@@ -109,6 +125,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
int code = misagent_get_status_code(mis); int code = misagent_get_status_code(mis);
NSLog(@"Failed to reinstall provisioning profile %@. (%@)", provisioningProfile.UUID, @(code)); NSLog(@"Failed to reinstall provisioning profile %@. (%@)", provisioningProfile.UUID, @(code));
} }
plist_free(pdata);
} }
[[NSFileManager defaultManager] removeItemAtURL:removedProfilesDirectoryURL error:nil]; [[NSFileManager defaultManager] removeItemAtURL:removedProfilesDirectoryURL error:nil];
@@ -279,13 +297,25 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
return finish(error); 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]); 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); uint32_t profileCount = plist_array_get_size(profiles);
for (int i = 0; i < profileCount; i++) for (int i = 0; i < profileCount; i++)
{ {
@@ -304,10 +334,10 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
continue; 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]; ALTProvisioningProfile *provisioningProfile = [[ALTProvisioningProfile alloc] initWithData:data];
if (![provisioningProfile.teamIdentifier isEqualToString:installationProvisioningProfile.teamIdentifier]) if (![provisioningProfile isFreeProvisioningProfile])
{ {
NSLog(@"Ignoring: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier); NSLog(@"Ignoring: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier);
continue; continue;
@@ -338,15 +368,18 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
if (misagent_remove(mis, provisioningProfile.UUID.UUIDString.lowercaseString.UTF8String) == MISAGENT_E_SUCCESS) 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 else
{ {
int code = misagent_get_status_code(mis); 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); lockdownd_client_free(client);
client = NULL; client = NULL;
} }
@@ -514,6 +547,89 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
return success; 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 - #pragma mark - Getters -
- (NSArray<ALTDevice *> *)connectedDevices - (NSArray<ALTDevice *> *)connectedDevices
@@ -670,3 +786,49 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
NSLog(@"Installation Progress: %@", @(percent)); 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> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.1</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>2</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key> <key>LSUIElement</key>
@@ -30,5 +30,7 @@
<string>Main</string> <string>Main</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>SUFeedURL</key>
<string>https://altstore.io/altserver/sparkle-macos.xml</string>
</dict> </dict>
</plist> </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; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 50; objectVersion = 51;
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 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, ); }; }; 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 */; }; 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 */; }; BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858222DE795100DE9F1E /* MyAppsViewController.swift */; };
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */; }; BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */; };
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C4EBC22A1BD8B009A2DD7 /* AppManager.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 */; }; BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */; };
BF100C54232D7DAE006A8926 /* StoreAppPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */; }; BF100C54232D7DAE006A8926 /* StoreAppPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */; };
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B0F022E25DF9005C4CF5 /* ToastView.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 */; }; BF1E315F22A0635900370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
BF1E316022A0636400370A3C /* 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 */; }; 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 */; }; BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; };
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; }; BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; };
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.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 */; }; BF45884A2298D55000BD7491 /* thread.c in Sources */ = {isa = PBXBuildFile; fileRef = BF4588482298D55000BD7491 /* thread.c */; };
BF45884B2298D55000BD7491 /* thread.h in Headers */ = {isa = PBXBuildFile; fileRef = BF4588492298D55000BD7491 /* thread.h */; }; BF45884B2298D55000BD7491 /* thread.h in Headers */ = {isa = PBXBuildFile; fileRef = BF4588492298D55000BD7491 /* thread.h */; };
BF4588882298DD3F00BD7491 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4588872298DD3F00BD7491 /* libxml2.tbd */; }; BF4588882298DD3F00BD7491 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4588872298DD3F00BD7491 /* libxml2.tbd */; };
BF4713A522976D1E00784A2F /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; }; BF4C7F2523801F0800B2556E /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9B63C5229DD44D002F0A62 /* AltSign.framework */; };
BF4713A622976D1E00784A2F /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF4C7F27238086EB00B2556E /* InstallPlugin.sh in Resources */ = {isa = PBXBuildFile; fileRef = BF4C7F26238086EB00B2556E /* InstallPlugin.sh */; };
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */; }; BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */; };
BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; }; BF56D2AA23DF88310006506D /* AppID.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2A923DF88310006506D /* AppID.swift */; };
BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 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 */; }; BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; };
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* AppOperationContext.swift */; }; BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* AppOperationContext.swift */; };
BF770E5622BC3C03002A40FE /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5522BC3C02002A40FE /* Server.swift */; }; BF770E5622BC3C03002A40FE /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5522BC3C02002A40FE /* Server.swift */; };
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.swift */; }; BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.swift */; };
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */; }; BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */; };
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; }; 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 */; }; BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C122E659F700049BA1 /* AppContentViewController.swift */; };
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C322E662D300049BA1 /* AppViewController.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 */; }; BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */; };
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */; }; BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */; };
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */; }; BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */; };
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4A22DD137F008935CF /* NavigationBar.swift */; }; BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4A22DD137F008935CF /* NavigationBar.swift */; };
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* PillButton.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 */; }; 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 */; }; BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; };
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; }; BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; };
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; }; BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; };
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; }; 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 */; }; BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21A23186D640022A802 /* NewsItem.swift */; };
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21D231870160022A802 /* NewsViewController.swift */; }; BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21D231870160022A802 /* NewsViewController.swift */; };
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21F231870B00022A802 /* NewsCollectionViewCell.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 */; }; BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; };
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; }; BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; };
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247782284B9A700981D42 /* LaunchScreen.storyboard */; }; 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 */; }; BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478B2284C4C300981D42 /* AppIconImageView.swift */; };
BFD2478F2284C8F900981D42 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478E2284C8F900981D42 /* Button.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 */; }; 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 */; }; BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F3230DDB0A007955AB /* Campaign.swift */; };
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F5230DDB12007955AB /* Tier.swift */; }; BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F5230DDB12007955AB /* Tier.swift */; };
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD6B03222DFF20800B86064 /* MyAppsComponents.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 */; }; BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */; };
BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */; }; BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */; };
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */; }; 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 */; }; BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DC22F0E7F3002E24B9 /* Source.swift */; };
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */; }; BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */; };
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338E722F10E56002E24B9 /* LaunchViewController.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 */; }; BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE60737231ADF49002B0E8E /* Settings.storyboard */; };
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE60739231ADF82002B0E8E /* SettingsViewController.swift */; }; BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE60739231ADF82002B0E8E /* SettingsViewController.swift */; };
BFE6073C231AE1E7002B0E8E /* SettingsHeaderFooterView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFE6073B231AE1E7002B0E8E /* SettingsHeaderFooterView.xib */; }; 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 */; }; BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; };
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; }; BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.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 */; }; BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; };
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; }; BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; };
BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */; }; 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 */; }; BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B695232242D3007A79E1 /* LicensesViewController.swift */; };
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6972322CAB8007A79E1 /* InstructionsViewController.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 */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -240,6 +262,13 @@
remoteGlobalIDString = BF45872A2298D31600BD7491; remoteGlobalIDString = BF45872A2298D31600BD7491;
remoteInfo = libimobiledevice; remoteInfo = libimobiledevice;
}; };
BFBFFB262380C72F00993A4A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = BFD247622284B9A500981D42 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BF5C5FC4237DF5AE00EDD0C6;
remoteInfo = AltPlugin;
};
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
@@ -265,23 +294,21 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
BFD247842284BB2C00981D42 /* Embed Frameworks */ = { BF5C5FE9237E438C00EDD0C6 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
dstPath = ""; dstPath = "";
dstSubfolderSpec = 10; dstSubfolderSpec = 10;
files = ( files = (
BF4713A622976D1E00784A2F /* openssl.framework in Embed Frameworks */,
BFD247882284BB4200981D42 /* Roxas.framework in Embed Frameworks */,
BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */,
); );
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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; }; 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; }; 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>"; }; 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>"; }; 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; }; 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>"; }; BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = "<group>"; };
BF770E5322BC044E002A40FE /* AppOperationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOperationContext.swift; sourceTree = "<group>"; }; BF770E5322BC044E002A40FE /* 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>"; }; BF770E5522BC3C02002A40FE /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
BF770E5722BC3D0F002A40FE /* OperationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationGroup.swift; sourceTree = "<group>"; }; BF770E5722BC3D0F002A40FE /* 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; }; BFB1169A2293274D00BB457C /* JSONDecoder+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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AboutPatreonHeaderView.xib; sourceTree = "<group>"; };
@@ -516,6 +581,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
BF4C7F2523801F0800B2556E /* AltSign.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -529,6 +595,14 @@
BF4588472298D4B000BD7491 /* libimobiledevice.a in Frameworks */, BF4588472298D4B000BD7491 /* libimobiledevice.a in Frameworks */,
BF0201BD22C2EFBC000B93E4 /* openssl.framework in Frameworks */, BF0201BD22C2EFBC000B93E4 /* openssl.framework in Frameworks */,
BF0201BA22C2EFA3000B93E4 /* AltSign.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; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -537,10 +611,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */, BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */,
BF4713A522976D1E00784A2F /* openssl.framework in Frameworks */, 01100C7036F0EBAC5B30984B /* libPods-AltStore.a in Frameworks */,
BFD247872284BB4200981D42 /* Roxas.framework in Frameworks */,
BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */,
DBAC68F8EC03F4A41D62EDE1 /* Pods_AltStore.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -552,6 +623,8 @@
children = ( children = (
EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */, EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */,
A136EE677716B80768E9F0A2 /* Pods-AltStore.release.xcconfig */, A136EE677716B80768E9F0A2 /* Pods-AltStore.release.xcconfig */,
11611D46F8A7C8B928E8156B /* Pods-AltServer.debug.xcconfig */,
589BA531D903B28F292063E5 /* Pods-AltServer.release.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -577,6 +650,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */, BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */,
BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */,
BF0F5FC623F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel */,
); );
path = "Mapping Models"; path = "Mapping Models";
sourceTree = "<group>"; sourceTree = "<group>";
@@ -585,6 +660,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */, BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */,
BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */,
); );
path = Policies; path = Policies;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -593,11 +669,14 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BFD52BD222A06EFB000B7ED1 /* AltKit.h */, BFD52BD222A06EFB000B7ED1 /* AltKit.h */,
BF718BD723C93DB700A89F2D /* AltKit.m */,
BFBAC8852295C90300587369 /* Result+Conveniences.swift */, BFBAC8852295C90300587369 /* Result+Conveniences.swift */,
BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */, BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */,
BF1E3128229F474900370A3C /* ServerProtocol.swift */, BF1E3128229F474900370A3C /* ServerProtocol.swift */,
BF1E314822A060F400370A3C /* NSError+ALTServerError.h */, BF1E314822A060F400370A3C /* NSError+ALTServerError.h */,
BF1E314922A060F400370A3C /* NSError+ALTServerError.m */, BF1E314922A060F400370A3C /* NSError+ALTServerError.m */,
BF718BC723C919CC00A89F2D /* CFNotificationName+AltStore.h */,
BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */,
); );
path = AltKit; path = AltKit;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -609,6 +688,8 @@
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */, BF3D648C22E79AC800E9056B /* ALTAppPermission.m */,
BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */, BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */,
BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */, BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */,
BF26A0DF2370C5D400F53F9F /* ALTSourceUserInfoKey.h */,
BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */,
BF41B807233433C100C593A3 /* LoadingState.swift */, BF41B807233433C100C593A3 /* LoadingState.swift */,
); );
path = Types; path = Types;
@@ -630,6 +711,7 @@
children = ( children = (
BF45868F229872EA00BD7491 /* AppDelegate.swift */, BF45868F229872EA00BD7491 /* AppDelegate.swift */,
BF458695229872EA00BD7491 /* Main.storyboard */, BF458695229872EA00BD7491 /* Main.storyboard */,
BFE48974238007CE003239E0 /* AnisetteDataManager.swift */,
BF703195229F36FF006E110F /* Devices */, BF703195229F36FF006E110F /* Devices */,
BFD52BDC22A0A659000B7ED1 /* Connections */, BFD52BDC22A0A659000B7ED1 /* Connections */,
BF055B4A233B528B0086DEA9 /* Extensions */, BF055B4A233B528B0086DEA9 /* Extensions */,
@@ -778,10 +860,34 @@
name = libcnary; name = libcnary;
sourceTree = "<group>"; 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 */ = { BF703194229F36F6006E110F /* Resources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BF458693229872EA00BD7491 /* Assets.xcassets */, BF458693229872EA00BD7491 /* Assets.xcassets */,
BF914C252383703800E713BA /* AltPlugin.mailbundle.zip */,
BF4C7F26238086EB00B2556E /* InstallPlugin.sh */,
BFD80D562380C0F700B9C227 /* UninstallPlugin.sh */,
); );
name = Resources; name = Resources;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -843,6 +949,7 @@
children = ( children = (
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */, BFD52BD322A0800A000B7ED1 /* ServerManager.swift */,
BF770E5522BC3C02002A40FE /* Server.swift */, BF770E5522BC3C02002A40FE /* Server.swift */,
BFA8172823C56042001B5953 /* ServerConnection.swift */,
); );
path = Server; path = Server;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -854,6 +961,7 @@
BF45868E229872EA00BD7491 /* AltServer */, BF45868E229872EA00BD7491 /* AltServer */,
BF1E315122A0616100370A3C /* AltKit */, BF1E315122A0616100370A3C /* AltKit */,
BF45872C2298D31600BD7491 /* libimobiledevice */, BF45872C2298D31600BD7491 /* libimobiledevice */,
BF5C5FC6237DF5AE00EDD0C6 /* AltPlugin */,
BFD247852284BB3300981D42 /* Frameworks */, BFD247852284BB3300981D42 /* Frameworks */,
BFD2476B2284B9A500981D42 /* Products */, BFD2476B2284B9A500981D42 /* Products */,
4460E048E3AC1C9708C4FA33 /* Pods */, 4460E048E3AC1C9708C4FA33 /* Pods */,
@@ -867,6 +975,7 @@
BF45868D229872EA00BD7491 /* AltServer.app */, BF45868D229872EA00BD7491 /* AltServer.app */,
BF45872B2298D31600BD7491 /* libimobiledevice.a */, BF45872B2298D31600BD7491 /* libimobiledevice.a */,
BF1E315022A0616100370A3C /* libAltKit.a */, BF1E315022A0616100370A3C /* libAltKit.a */,
BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -887,6 +996,7 @@
BFDB69FB22A9A7A6007EA6D6 /* Settings */, BFDB69FB22A9A7A6007EA6D6 /* Settings */,
BFD5D6E6230CC94B007955AB /* Patreon */, BFD5D6E6230CC94B007955AB /* Patreon */,
BFD2478A2284C49000981D42 /* Managing Apps */, BFD2478A2284C49000981D42 /* Managing Apps */,
BF56D2AD23DF9E170006506D /* App IDs */,
BFC51D7922972F1F00388324 /* Server */, BFC51D7922972F1F00388324 /* Server */,
BFD247982284D7FC00981D42 /* Model */, BFD247982284D7FC00981D42 /* Model */,
BFDB6A0922AAEDA1007EA6D6 /* Operations */, BFDB6A0922AAEDA1007EA6D6 /* Operations */,
@@ -911,8 +1021,8 @@
BFD247862284BB3B00981D42 /* Roxas.framework */, BFD247862284BB3B00981D42 /* Roxas.framework */,
BF5AB3A72285FE6C00DC914B /* AltSign.framework */, BF5AB3A72285FE6C00DC914B /* AltSign.framework */,
BF4713A422976CFC00784A2F /* openssl.framework */, BF4713A422976CFC00784A2F /* openssl.framework */,
1039C07E517311FC499A0B64 /* Pods_AltStore.framework */,
FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */, FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */,
0DE618FA97EA42C3F468D186 /* libPods-AltStore.a */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -929,6 +1039,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BFD2478B2284C4C300981D42 /* AppIconImageView.swift */, BFD2478B2284C4C300981D42 /* AppIconImageView.swift */,
BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */,
BFD2478E2284C8F900981D42 /* Button.swift */, BFD2478E2284C8F900981D42 /* Button.swift */,
BF43002D22A714AF0051E2BC /* Keychain.swift */, BF43002D22A714AF0051E2BC /* Keychain.swift */,
BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */, BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */,
@@ -969,8 +1080,10 @@
BFB11691229322E400BB457C /* DatabaseManager.swift */, BFB11691229322E400BB457C /* DatabaseManager.swift */,
BF3D64A122E8031100E9056B /* MergePolicy.swift */, BF3D64A122E8031100E9056B /* MergePolicy.swift */,
BFE6326722A858F300F30809 /* Account.swift */, BFE6326722A858F300F30809 /* Account.swift */,
BF56D2A923DF88310006506D /* AppID.swift */,
BF3D648722E79A3700E9056B /* AppPermission.swift */, BF3D648722E79A3700E9056B /* AppPermission.swift */,
BFBBE2E022931F81002097FA /* InstalledApp.swift */, BFBBE2E022931F81002097FA /* InstalledApp.swift */,
BFA8172C23C5823E001B5953 /* InstalledExtension.swift */,
BFB6B21A23186D640022A802 /* NewsItem.swift */, BFB6B21A23186D640022A802 /* NewsItem.swift */,
BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */, BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */,
BF02419322F2156E00129732 /* RefreshAttempt.swift */, BF02419322F2156E00129732 /* RefreshAttempt.swift */,
@@ -991,6 +1104,7 @@
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */, BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */,
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */, BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */,
BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */, BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */,
BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -999,6 +1113,13 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
BF1E3129229F474900370A3C /* ConnectionManager.swift */, 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; path = Connections;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1050,11 +1171,14 @@
BF770E5322BC044E002A40FE /* AppOperationContext.swift */, BF770E5322BC044E002A40FE /* AppOperationContext.swift */,
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */, BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */, BFB364592325985F00CD0EB1 /* FindServerOperation.swift */,
BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */,
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */, BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */, BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */, BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */, BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */, BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
); );
path = Operations; path = Operations;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1065,6 +1189,7 @@
BFE6325922A83BEB00F30809 /* Authentication.storyboard */, BFE6325922A83BEB00F30809 /* Authentication.storyboard */,
BFF0B6932321CB85007A79E1 /* AuthenticationViewController.swift */, BFF0B6932321CB85007A79E1 /* AuthenticationViewController.swift */,
BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */, BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */,
BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */,
); );
path = Authentication; path = Authentication;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1141,15 +1266,19 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = BF45869A229872EA00BD7491 /* Build configuration list for PBXNativeTarget "AltServer" */; buildConfigurationList = BF45869A229872EA00BD7491 /* Build configuration list for PBXNativeTarget "AltServer" */;
buildPhases = ( buildPhases = (
FACBF95CCAAAB7121E1D92C8 /* [CP] Check Pods Manifest.lock */,
BF458689229872EA00BD7491 /* Sources */, BF458689229872EA00BD7491 /* Sources */,
BF45868B229872EA00BD7491 /* Resources */, BF45868B229872EA00BD7491 /* Resources */,
BF4588462298D4AA00BD7491 /* Frameworks */, BF4588462298D4AA00BD7491 /* Frameworks */,
BF0201BC22C2EFA3000B93E4 /* Embed Frameworks */, BF0201BC22C2EFA3000B93E4 /* Embed Frameworks */,
BF7FDA2C23203B6B00B5D3A4 /* Copy Launcher App */, BF7FDA2C23203B6B00B5D3A4 /* Copy Launcher App */,
98BF22D155DBAEA97544E3E6 /* [CP] Embed Pods Frameworks */,
BF914C242383659400E713BA /* Sign Frameworks */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
BFBFFB272380C72F00993A4A /* PBXTargetDependency */,
BF1E315E22A0621F00370A3C /* PBXTargetDependency */, BF1E315E22A0621F00370A3C /* PBXTargetDependency */,
BF4588452298D48B00BD7491 /* PBXTargetDependency */, BF4588452298D48B00BD7491 /* PBXTargetDependency */,
); );
@@ -1175,6 +1304,24 @@
productReference = BF45872B2298D31600BD7491 /* libimobiledevice.a */; productReference = BF45872B2298D31600BD7491 /* libimobiledevice.a */;
productType = "com.apple.product-type.library.static"; 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 */ = { BFD247692284B9A500981D42 /* AltStore */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = BFD2477E2284B9A700981D42 /* Build configuration list for PBXNativeTarget "AltStore" */; buildConfigurationList = BFD2477E2284B9A700981D42 /* Build configuration list for PBXNativeTarget "AltStore" */;
@@ -1183,8 +1330,7 @@
BFD247662284B9A500981D42 /* Sources */, BFD247662284B9A500981D42 /* Sources */,
BFD247672284B9A500981D42 /* Frameworks */, BFD247672284B9A500981D42 /* Frameworks */,
BFD247682284B9A500981D42 /* Resources */, BFD247682284B9A500981D42 /* Resources */,
BFD247842284BB2C00981D42 /* Embed Frameworks */, 8C9013C41DD92A1476195C0E /* [CP] Copy Pods Resources */,
B8F37E08B55D2C9C4E2B1B4E /* [CP] Embed Pods Frameworks */,
); );
buildRules = ( buildRules = (
); );
@@ -1202,7 +1348,7 @@
BFD247622284B9A500981D42 /* Project object */ = { BFD247622284B9A500981D42 /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1020; LastSwiftUpdateCheck = 1120;
LastUpgradeCheck = 1020; LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "Riley Testut"; ORGANIZATIONNAME = "Riley Testut";
TargetAttributes = { TargetAttributes = {
@@ -1224,6 +1370,10 @@
BF45872A2298D31600BD7491 = { BF45872A2298D31600BD7491 = {
CreatedOnToolsVersion = 10.2.1; CreatedOnToolsVersion = 10.2.1;
}; };
BF5C5FC4237DF5AE00EDD0C6 = {
CreatedOnToolsVersion = 11.2;
LastSwiftMigration = 1120;
};
BFD247692284B9A500981D42 = { BFD247692284B9A500981D42 = {
CreatedOnToolsVersion = 10.2.1; CreatedOnToolsVersion = 10.2.1;
LastSwiftMigration = 1020; LastSwiftMigration = 1020;
@@ -1255,6 +1405,7 @@
BF45868C229872EA00BD7491 /* AltServer */, BF45868C229872EA00BD7491 /* AltServer */,
BF1E314F22A0616100370A3C /* AltKit */, BF1E314F22A0616100370A3C /* AltKit */,
BF45872A2298D31600BD7491 /* libimobiledevice */, BF45872A2298D31600BD7491 /* libimobiledevice */,
BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -1264,8 +1415,18 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
BF914C262383703800E713BA /* AltPlugin.mailbundle.zip in Resources */,
BFD80D572380C0F700B9C227 /* UninstallPlugin.sh in Resources */,
BF458694229872EA00BD7491 /* Assets.xcassets in Resources */, BF458694229872EA00BD7491 /* Assets.xcassets in Resources */,
BF458697229872EA00BD7491 /* Main.storyboard in Resources */, BF458697229872EA00BD7491 /* Main.storyboard in Resources */,
BF4C7F27238086EB00B2556E /* InstallPlugin.sh in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
BF5C5FC3237DF5AE00EDD0C6 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -1291,28 +1452,38 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
B8F37E08B55D2C9C4E2B1B4E /* [CP] Embed Pods Frameworks */ = { 8C9013C41DD92A1476195C0E /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = ( name = "[CP] Copy Pods Resources";
"${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";
outputFileListPaths = ( outputFileListPaths = (
); "${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-resources-${CONFIGURATION}-output-files.xcfilelist",
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nuke.framework",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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; showEnvVarsInLog = 0;
}; };
BF7FDA2C23203B6B00B5D3A4 /* Copy Launcher App */ = { BF7FDA2C23203B6B00B5D3A4 /* Copy Launcher App */ = {
@@ -1333,6 +1504,46 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PROJECT_DIR}/Carthage/Build/Mac/LaunchAtLogin.framework/Resources/copy-helper.sh\"\n"; 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 */ = { FFB93342C7EB2021A1FFFB6A /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -1362,9 +1573,11 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
BF718BD823C93DB700A89F2D /* AltKit.m in Sources */,
BF1E315A22A0620000370A3C /* NSError+ALTServerError.m in Sources */, BF1E315A22A0620000370A3C /* NSError+ALTServerError.m in Sources */,
BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */, BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */,
BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */, BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */,
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */,
BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */, BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -1374,10 +1587,14 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
BF3F786422CAA41E008FBD20 /* ALTDeviceManager+Installation.swift in Sources */, BF3F786422CAA41E008FBD20 /* ALTDeviceManager+Installation.swift in Sources */,
BF718BD523C928A300A89F2D /* ALTNotificationConnection.m in Sources */,
BF1E312B229F474900370A3C /* ConnectionManager.swift in Sources */, BF1E312B229F474900370A3C /* ConnectionManager.swift in Sources */,
BF9A03C623C7DD0D000D08DB /* ClientConnection.swift in Sources */,
BF718BD123C91BD300A89F2D /* ALTWiredConnection.m in Sources */,
BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */, BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */,
BF4586C52298CDB800BD7491 /* ALTDeviceManager.mm in Sources */, BF4586C52298CDB800BD7491 /* ALTDeviceManager.mm in Sources */,
BF0241AA22F29CCD00129732 /* UserDefaults+AltServer.swift in Sources */, BF0241AA22F29CCD00129732 /* UserDefaults+AltServer.swift in Sources */,
BFE48975238007CE003239E0 /* AnisetteDataManager.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -1442,16 +1659,29 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
BF5C5FC1237DF5AE00EDD0C6 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BFB49AAA23834CF900D542D9 /* ALTAnisetteData.m in Sources */,
BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
BFD247662284B9A500981D42 /* Sources */ = { BFD247662284B9A500981D42 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */, BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */, BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */, BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */,
BFB11692229322E400BB457C /* DatabaseManager.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 */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */, BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */,
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */, BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
@@ -1463,6 +1693,7 @@
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */, BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */, BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */,
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */, BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */,
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */, BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */,
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */, BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */, BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
@@ -1474,6 +1705,7 @@
BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */, BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */,
BFE6326822A858F300F30809 /* Account.swift in Sources */, BFE6326822A858F300F30809 /* Account.swift in Sources */,
BFE6326622A857C200F30809 /* Team.swift in Sources */, BFE6326622A857C200F30809 /* Team.swift in Sources */,
BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */, BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */, BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
@@ -1482,9 +1714,12 @@
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */, BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */, BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */, BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */, BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */,
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */, BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */, BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */,
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */, BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */, BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */,
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */, BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */,
@@ -1493,11 +1728,14 @@
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */, BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */,
BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */, BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */,
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */, BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
BF7C627423DBB78C00515A2D /* InstalledAppPolicy.swift in Sources */,
BFA8172F23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift in Sources */,
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */, BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */, BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */,
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */, BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */,
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */, BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */, BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */,
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */, BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */, BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */, BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */,
@@ -1505,6 +1743,7 @@
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */, BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */, BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
BF3D64A222E8031100E9056B /* MergePolicy.swift in Sources */, BF3D64A222E8031100E9056B /* MergePolicy.swift in Sources */,
BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */, BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */, BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
@@ -1522,6 +1761,8 @@
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */, BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */,
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */, BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
BF770E5622BC3C03002A40FE /* Server.swift in Sources */, BF770E5622BC3C03002A40FE /* Server.swift in Sources */,
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */,
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */,
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */, BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -1544,6 +1785,11 @@
target = BF45872A2298D31600BD7491 /* libimobiledevice */; target = BF45872A2298D31600BD7491 /* libimobiledevice */;
targetProxy = BF4588442298D48B00BD7491 /* PBXContainerItemProxy */; targetProxy = BF4588442298D48B00BD7491 /* PBXContainerItemProxy */;
}; };
BFBFFB272380C72F00993A4A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */;
targetProxy = BFBFFB262380C72F00993A4A /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
@@ -1610,12 +1856,15 @@
}; };
BF45869B229872EA00BD7491 /* Debug */ = { BF45869B229872EA00BD7491 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 11611D46F8A7C8B928E8156B /* Pods-AltServer.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = AltServer/AltServer.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_TEAM = 6XVY5G3U44; DEVELOPMENT_TEAM = 6XVY5G3U44;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -1648,6 +1897,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.14.4; MACOSX_DEPLOYMENT_TARGET = 10.14.4;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx; SDKROOT = macosx;
@@ -1659,12 +1909,15 @@
}; };
BF45869C229872EA00BD7491 /* Release */ = { BF45869C229872EA00BD7491 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 589BA531D903B28F292063E5 /* Pods-AltServer.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = AltServer/AltServer.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_TEAM = 6XVY5G3U44; DEVELOPMENT_TEAM = 6XVY5G3U44;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -1697,6 +1950,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.14.4; MACOSX_DEPLOYMENT_TARGET = 10.14.4;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx; SDKROOT = macosx;
@@ -1779,6 +2033,63 @@
}; };
name = Release; 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 */ = { BFD2477C2284B9A700981D42 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@@ -1913,6 +2224,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1939,6 +2251,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1978,6 +2291,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; 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" */ = { BFD247652284B9A500981D42 /* Build configuration list for PBXProject "AltStore" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
@@ -2002,10 +2324,12 @@
BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */ = { BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */ = {
isa = XCVersionGroup; isa = XCVersionGroup;
children = ( children = (
BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */,
BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */,
BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */, BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */,
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */, BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */,
); );
currentVersion = BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */; currentVersion = BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */;
path = AltStore.xcdatamodeld; path = AltStore.xcdatamodeld;
sourceTree = "<group>"; sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel; 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. // 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 "ALTAppPermission.h"
#import "ALTPatreonBenefitType.h" #import "ALTPatreonBenefitType.h"
#import "ALTSourceUserInfoKey.h"

View File

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

View File

@@ -27,18 +27,11 @@ class AppViewController: UIViewController
@IBOutlet private var scrollView: UIScrollView! @IBOutlet private var scrollView: UIScrollView!
@IBOutlet private var contentView: UIView! @IBOutlet private var contentView: UIView!
@IBOutlet private var headerView: UIView! @IBOutlet private var bannerView: AppBannerView!
@IBOutlet private var headerContentView: UIView!
@IBOutlet private var backButton: UIButton! @IBOutlet private var backButton: UIButton!
@IBOutlet private var backButtonContainerView: UIVisualEffectView! @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 backgroundAppIconImageView: UIImageView!
@IBOutlet private var backgroundBlurView: UIVisualEffectView! @IBOutlet private var backgroundBlurView: UIVisualEffectView!
@@ -51,6 +44,12 @@ class AppViewController: UIViewController
private var _backgroundBlurEffect: UIBlurEffect? private var _backgroundBlurEffect: UIBlurEffect?
private var _backgroundBlurTintColor: UIColor? private var _backgroundBlurTintColor: UIColor?
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
override var preferredStatusBarStyle: UIStatusBarStyle {
return _preferredStatusBarStyle
}
override func viewDidLoad() override func viewDidLoad()
{ {
super.viewDidLoad() super.viewDidLoad()
@@ -75,21 +74,22 @@ class AppViewController: UIViewController
self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer) self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
self.contentViewController.tableView.showsVerticalScrollIndicator = false 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. // Bring to front so the scroll indicators are visible.
self.view.bringSubviewToFront(self.scrollView) self.view.bringSubviewToFront(self.scrollView)
self.scrollView.isUserInteractionEnabled = false self.scrollView.isUserInteractionEnabled = false
self.nameLabel.text = self.app.name self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
self.developerLabel.text = self.app.developerName self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
self.developerLabel.textColor = self.app.tintColor self.bannerView.backgroundEffectView.backgroundColor = .clear
self.appIconImageView.image = nil self.bannerView.titleLabel.text = self.app.name
self.appIconImageView.tintColor = self.app.tintColor self.bannerView.subtitleLabel.text = self.app.developerName
self.downloadButton.tintColor = self.app.tintColor self.bannerView.iconImageView.image = nil
self.betaBadgeView.isHidden = !self.app.isBeta 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 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.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.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._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
// Load Images // Load Images
for imageView in [self.appIconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!] for imageView in [self.bannerView.iconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
{ {
imageView.isIndicatingActivity = true imageView.isIndicatingActivity = true
@@ -219,7 +220,7 @@ class AppViewController: UIViewController
var backButtonFrame = CGRect(x: inset, y: statusBarHeight, var backButtonFrame = CGRect(x: inset, y: statusBarHeight,
width: backButtonSize.width + 20, height: backButtonSize.height + 20) 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 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) 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. // Set frames.
self.contentViewController.view.superview?.frame = contentFrame self.contentViewController.view.superview?.frame = contentFrame
self.headerView.frame = headerFrame self.bannerView.frame = headerFrame
self.backgroundAppIconImageView.frame = backgroundIconFrame self.backgroundAppIconImageView.frame = backgroundIconFrame
self.backgroundBlurView.frame = backgroundIconFrame self.backgroundBlurView.frame = backgroundIconFrame
self.backButtonContainerView.frame = backButtonFrame 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.contentViewControllerShadowView.frame = self.contentViewController.view.frame
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
@@ -325,6 +325,14 @@ class AppViewController: UIViewController
self.scrollView.contentSize = contentSize self.scrollView.contentSize = contentSize
self.scrollView.contentOffset = contentOffset self.scrollView.contentOffset = contentOffset
self.bannerView.backgroundEffectView.backgroundColor = .clear
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
{
super.traitCollectionDidChange(previousTraitCollection)
self._shouldResetLayout = true
} }
deinit deinit
@@ -350,7 +358,7 @@ private extension AppViewController
{ {
func update() func update()
{ {
for button in [self.downloadButton!, self.navigationBarDownloadButton!] for button in [self.bannerView.button!, self.navigationBarDownloadButton!]
{ {
button.tintColor = self.app.tintColor button.tintColor = self.app.tintColor
button.isIndicatingActivity = false button.isIndicatingActivity = false
@@ -358,12 +366,10 @@ private extension AppViewController
if self.app.installedApp == nil if self.app.installedApp == nil
{ {
button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal) button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
button.isInverted = false
} }
else else
{ {
button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal) button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
button.isInverted = true
} }
let progress = AppManager.shared.installationProgress(for: self.app) let progress = AppManager.shared.installationProgress(for: self.app)
@@ -372,12 +378,12 @@ private extension AppViewController
if Date() < self.app.versionDate if Date() < self.app.versionDate
{ {
self.downloadButton.countdownDate = self.app.versionDate self.bannerView.button.countdownDate = self.app.versionDate
self.navigationBarDownloadButton.countdownDate = self.app.versionDate self.navigationBarDownloadButton.countdownDate = self.app.versionDate
} }
else else
{ {
self.downloadButton.countdownDate = nil self.bannerView.button.countdownDate = nil
self.navigationBarDownloadButton.countdownDate = nil self.navigationBarDownloadButton.countdownDate = nil
} }
@@ -389,18 +395,29 @@ private extension AppViewController
func showNavigationBar(for navigationController: UINavigationController? = nil) func showNavigationBar(for navigationController: UINavigationController? = nil)
{ {
let navigationController = navigationController ?? self.navigationController let navigationController = navigationController ?? self.navigationController
navigationController?.navigationBar.barStyle = .default
navigationController?.navigationBar.alpha = 1.0 navigationController?.navigationBar.alpha = 1.0
navigationController?.navigationBar.barTintColor = .white
navigationController?.navigationBar.tintColor = .altPrimary 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) func hideNavigationBar(for navigationController: UINavigationController? = nil)
{ {
let navigationController = navigationController ?? self.navigationController let navigationController = navigationController ?? self.navigationController
navigationController?.navigationBar.barStyle = .black
navigationController?.navigationBar.alpha = 0.0 navigationController?.navigationBar.alpha = 0.0
navigationController?.navigationBar.barTintColor = .white
self._preferredStatusBarStyle = .lightContent
navigationController?.setNeedsStatusBarAppearanceUpdate()
} }
func prepareBlur() func prepareBlur()
@@ -445,7 +462,7 @@ private extension AppViewController
self.navigationBarAnimator = nil self.navigationBarAnimator = nil
self.hideNavigationBar() self.hideNavigationBar()
self.navigationController?.navigationBar.barTintColor = .white
self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
} }
} }
@@ -485,18 +502,20 @@ extension AppViewController
catch catch
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(text: error.localizedDescription, detailText: nil) let toastView = ToastView(error: error)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2) toastView.show(in: self)
} }
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.downloadButton.progress = nil self.bannerView.button.progress = nil
self.navigationBarDownloadButton.progress = nil
self.update() self.update()
} }
} }
self.downloadButton.progress = progress self.bannerView.button.progress = progress
self.navigationBarDownloadButton.progress = progress
} }
func open(_ installedApp: InstalledApp) func open(_ installedApp: InstalledApp)
@@ -522,6 +541,15 @@ private extension AppViewController
self._shouldResetLayout = true self._shouldResetLayout = true
self.view.setNeedsLayout() 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 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"?> <?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"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <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="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="Primary"/> <color key="barTintColor" name="SettingsBackground"/>
<textAttributes key="titleTextAttributes"> <textAttributes key="titleTextAttributes">
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textAttributes> </textAttributes>
@@ -51,7 +51,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/> <rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh"> <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> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome"> <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"/> <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"> <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"/> <rect key="frame" x="0.0" y="47" width="308.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <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"/> <nil key="highlightedColor"/>
</label> </label>
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf"> <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> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7"> <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> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID"> <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"/> <rect key="frame" x="0.0" y="0.0" width="343" height="72"/>
@@ -118,7 +118,7 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password"> <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> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95"> <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"/> <rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
@@ -156,31 +156,19 @@
</constraints> </constraints>
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="14"/> <edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="14"/>
</view> </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> </subviews>
</stackView> </stackView>
</subviews> </subviews>
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj"> <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"/> <rect key="frame" x="0.0" y="191" width="343" height="51"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/> <constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
</constraints> </constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
<state key="normal" title="Sign in"> <state key="normal" title="Sign in">
<color key="titleColor" name="Pink"/> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state> </state>
<connections> <connections>
<action selector="authenticate" destination="yO1-iT-7NP" eventType="primaryActionTriggered" id="LER-a2-CbC"/> <action selector="authenticate" destination="yO1-iT-7NP" eventType="primaryActionTriggered" id="LER-a2-CbC"/>
@@ -222,7 +210,7 @@
</constraints> </constraints>
</scrollView> </scrollView>
</subviews> </subviews>
<color key="backgroundColor" name="Primary"/> <color key="backgroundColor" name="SettingsBackground"/>
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/> <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"/> <constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/>
@@ -407,20 +395,20 @@
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK"> <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"/> <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> <constraints>
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/> <constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
</constraints> </constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
<state key="normal" title="Got it"> <state key="normal" title="Got it">
<color key="titleColor" name="Pink"/> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state> </state>
<connections> <connections>
<action selector="dismiss" destination="aFi-fb-W0B" eventType="primaryActionTriggered" id="sBq-zj-Mln"/> <action selector="dismiss" destination="aFi-fb-W0B" eventType="primaryActionTriggered" id="sBq-zj-Mln"/>
</connections> </connections>
</button> </button>
</subviews> </subviews>
<color key="backgroundColor" name="Primary"/> <color key="backgroundColor" name="SettingsBackground"/>
<constraints> <constraints>
<constraint firstItem="qZ9-AR-2zK" firstAttribute="top" secondItem="bp6-55-IG2" secondAttribute="bottom" id="3yt-cr-swd"/> <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"/> <constraint firstItem="bp6-55-IG2" firstAttribute="top" secondItem="Zek-aC-HOO" secondAttribute="top" id="42S-q2-YZn"/>
@@ -443,14 +431,78 @@
</objects> </objects>
<point key="canvasLocation" x="1353" y="736"/> <point key="canvasLocation" x="1353" y="736"/>
</scene> </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> </scenes>
<resources> <resources>
<namedColor name="Pink"> <namedColor name="SettingsBackground">
<color red="0.92549019607843142" green="0.25490196078431371" blue="0.69803921568627447" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Primary">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="SettingsHighlighted">
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources> </resources>
<color key="tintColor" name="Primary"/> <color key="tintColor" name="Primary"/>
</document> </document>

View File

@@ -12,7 +12,8 @@ import AltSign
class AuthenticationViewController: UIViewController 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? private weak var toastView: ToastView?
@@ -30,6 +31,8 @@ class AuthenticationViewController: UIViewController
{ {
super.viewDidLoad() super.viewDidLoad()
self.signInButton.activityIndicatorView.style = .white
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!] for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
{ {
view.clipsToBounds = true view.clipsToBounds = true
@@ -94,14 +97,16 @@ private extension AuthenticationViewController
self.signInButton.isIndicatingActivity = true self.signInButton.isIndicatingActivity = true
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in self.authenticationHandler?(emailAddress, password) { (result) in
do switch result
{
let account = try Result(account, error).get()
self.authenticationHandler?((account, password))
}
catch
{ {
case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
DispatchQueue.main.async {
self.signInButton.isIndicatingActivity = false
}
case .failure(let error):
DispatchQueue.main.async { DispatchQueue.main.async {
let toastView = ToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription) let toastView = ToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription)
toastView.textLabel.textColor = .altPink toastView.textLabel.textColor = .altPink
@@ -111,6 +116,9 @@ private extension AuthenticationViewController
self.signInButton.isIndicatingActivity = false self.signInButton.isIndicatingActivity = false
} }
case .success(let account, let session):
self.completionHandler?((account, session, password))
} }
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -121,7 +129,7 @@ private extension AuthenticationViewController
@IBAction func cancel(_ sender: UIBarButtonItem) @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 contentStackView: UIStackView!
@IBOutlet private var dismissButton: UIButton! @IBOutlet private var dismissButton: UIButton!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidLoad() override func viewDidLoad()
{ {
super.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"?> <?xml version="1.0" encoding="UTF-8"?>
<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"> <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> <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="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@@ -11,15 +14,38 @@
<objects> <objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController"> <viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> <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"/> <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"/> <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view> </view>
<tabBarItem key="tabBarItem" title="" id="RiK-sx-Kgv"/>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </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> </scene>
</scenes> </scenes>
<resources>
<namedColor name="Background">
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document> </document>

View File

@@ -1,12 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <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="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" 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"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@@ -20,9 +19,6 @@
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<viewLayoutGuide key="safeArea" id="sZd-sc-Bvn"/> <viewLayoutGuide key="safeArea" id="sZd-sc-Bvn"/>
</view> </view>
<connections>
<segue destination="49e-Tb-3d3" kind="presentation" identifier="finishLaunching" modalTransitionStyle="crossDissolve" id="6Ov-Kc-Van"/>
</connections>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="vOq-mm-rY5" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="vOq-mm-rY5" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
@@ -31,7 +27,7 @@
<!--Tab Bar Controller--> <!--Tab Bar Controller-->
<scene sceneID="yl2-sM-qoP"> <scene sceneID="yl2-sM-qoP">
<objects> <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"> <tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
<rect key="frame" x="0.0" y="975" width="768" height="49"/> <rect key="frame" x="0.0" y="975" width="768" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <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="0.0" minimumInteritemSpacing="10" id="e0H-IH-rng"> <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="50" minimumInteritemSpacing="10" id="e0H-IH-rng">
<size key="itemSize" width="375" height="400"/> <size key="itemSize" width="375" height="400"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/> <size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" 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> </collectionViewFlowLayout>
<cells/> <cells/>
<connections> <connections>
@@ -110,10 +106,9 @@
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="oNk-OQ-r4M"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="oNk-OQ-r4M">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view> </view>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<blurEffect style="light"/> <blurEffect style="regular"/>
</visualEffectView> </visualEffectView>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" contentInsetAdjustmentBehavior="never" translatesAutoresizingMaskIntoConstraints="NO" id="Ci9-Iw-aR2"> <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"/> <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"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews> <subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="mgO-eN-SxQ"> <view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="38" y="287" width="300" height="93"/> <rect key="frame" x="37" y="287" width="300" height="93"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="yIo-bR-OBC"> </view>
<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>
<containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FIv-I9-5uW"> <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"/> <rect key="frame" x="0.0" y="450" width="375" height="217"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
@@ -201,22 +137,35 @@
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/> <rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <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"/> <rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<state key="normal" image="Back"/> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="UJ5-ia-PVA">
<connections> <rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
<action selector="popViewController:" destination="0V6-N4-hTO" eventType="primaryActionTriggered" id="F6Z-xz-qCk"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</connections> <subviews>
</button> <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> </subviews>
</view> </view>
<blurEffect style="extraLight"/> <blurEffect style="prominent"/>
</visualEffectView> </visualEffectView>
</subviews> </subviews>
</view> </view>
</subviews> </subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" name="Background"/>
<constraints> <constraints>
<constraint firstItem="Ci9-Iw-aR2" firstAttribute="top" secondItem="0cR-li-tCB" secondAttribute="top" id="015-fz-v3B"/> <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"/> <constraint firstAttribute="top" secondItem="Qlg-m3-lXg" secondAttribute="top" id="8tb-sY-MOu"/>
@@ -248,18 +197,12 @@
</barButtonItem> </barButtonItem>
</navigationItem> </navigationItem>
<connections> <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="backButton" destination="mkD-3C-WMV" id="3m8-P7-yvT"/>
<outlet property="backButtonContainerView" destination="tUK-0J-07U" id="POZ-dP-f12"/> <outlet property="backButtonContainerView" destination="tUK-0J-07U" id="POZ-dP-f12"/>
<outlet property="backgroundAppIconImageView" destination="CUB-SN-zdM" id="dFx-py-yMm"/> <outlet property="backgroundAppIconImageView" destination="CUB-SN-zdM" id="dFx-py-yMm"/>
<outlet property="backgroundBlurView" destination="8Tg-wk-r0u" id="B8c-ng-nI5"/> <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="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="navigationBarAppIconImageView" destination="j1W-Jn-HFI" id="2YU-ka-w9R"/>
<outlet property="navigationBarAppNameLabel" destination="DTD-1Y-76c" id="z9z-pp-dC4"/> <outlet property="navigationBarAppNameLabel" destination="DTD-1Y-76c" id="z9z-pp-dC4"/>
<outlet property="navigationBarDownloadButton" destination="grk-xM-YWA" id="Yrg-S0-tIM"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" name="Background"/>
<sections> <sections>
<tableViewSection id="rfR-32-T0h"> <tableViewSection id="rfR-32-T0h">
<cells> <cells>
@@ -303,6 +246,7 @@
<constraint firstItem="BsL-O2-UjD" firstAttribute="top" secondItem="8PX-jQ-nHd" secondAttribute="top" constant="20" id="dRc-WY-Jbk"/> <constraint firstItem="BsL-O2-UjD" firstAttribute="top" secondItem="8PX-jQ-nHd" secondAttribute="top" constant="20" id="dRc-WY-Jbk"/>
</constraints> </constraints>
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/> <inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="nI6-wC-H2d"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="nI6-wC-H2d">
@@ -314,7 +258,7 @@
<subviews> <subviews>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="ppk-lL-at8"> <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"/> <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"> <collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="15" id="ace-Ns-Jd2">
<size key="itemSize" width="189" height="406"/> <size key="itemSize" width="189" height="406"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/> <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"/> <constraint firstItem="ppk-lL-at8" firstAttribute="top" secondItem="Z4y-vb-Z4Q" secondAttribute="top" id="xY5-w8-roA"/>
</constraints> </constraints>
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/> <inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="EL5-UC-RIw" customClass="AppContentTableViewCell" customModule="AltStore" customModuleProvider="target"> <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> <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"> <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"/> <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"/> <fontDescription key="fontDescription" type="system" pointSize="15"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView> </textView>
@@ -363,6 +308,7 @@
<constraint firstAttribute="trailing" secondItem="Pyt-8D-BZA" secondAttribute="trailing" constant="20" id="Wq4-Ql-wvN"/> <constraint firstAttribute="trailing" secondItem="Pyt-8D-BZA" secondAttribute="trailing" constant="20" id="Wq4-Ql-wvN"/>
</constraints> </constraints>
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/> <inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="47M-El-a4G" customClass="AppContentTableViewCell" customModule="AltStore" customModuleProvider="target"> <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> </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"> <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"/> <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"/> <fontDescription key="fontDescription" type="system" pointSize="15"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView> </textView>
@@ -431,6 +377,7 @@
<constraint firstAttribute="bottom" secondItem="n9R-39-Glq" secondAttribute="bottom" priority="999" id="Zol-57-Lbq"/> <constraint firstAttribute="bottom" secondItem="n9R-39-Glq" secondAttribute="bottom" priority="999" id="Zol-57-Lbq"/>
</constraints> </constraints>
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/> <inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="149" id="nM7-vJ-W8b"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="149" id="nM7-vJ-W8b">
@@ -451,7 +398,7 @@
</label> </label>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="r8T-dj-wQX"> <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"/> <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> <constraints>
<constraint firstAttribute="height" constant="88" id="6Lk-OO-MsA"/> <constraint firstAttribute="height" constant="88" id="6Lk-OO-MsA"/>
</constraints> </constraints>
@@ -521,6 +468,7 @@ World</string>
<constraint firstItem="Jvb-r8-XrY" firstAttribute="top" secondItem="cQ2-Jd-pRK" secondAttribute="top" id="Urh-Qr-vrS"/> <constraint firstItem="Jvb-r8-XrY" firstAttribute="top" secondItem="cQ2-Jd-pRK" secondAttribute="top" id="Urh-Qr-vrS"/>
</constraints> </constraints>
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" name="Background"/>
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/> <inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell> </tableViewCell>
</cells> </cells>
@@ -575,7 +523,7 @@ World</string>
<edgeInsets key="layoutMargins" top="0.0" left="0.0" bottom="0.0" right="0.0"/> <edgeInsets key="layoutMargins" top="0.0" left="0.0" bottom="0.0" right="0.0"/>
</stackView> </stackView>
</subviews> </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> <constraints>
<constraint firstItem="xnC-tS-ZdV" firstAttribute="leading" secondItem="c7x-ee-3HH" secondAttribute="leading" constant="20" id="LO8-Au-SYF"/> <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"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <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"> <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="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" 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> </collectionViewFlowLayout>
<cells/> <cells/>
<connections> <connections>
@@ -632,7 +580,7 @@ World</string>
<!--Browse--> <!--Browse-->
<scene sceneID="VHa-uP-bFU"> <scene sceneID="VHa-uP-bFU">
<objects> <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"/> <tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target"> <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--> <!--My Apps-->
<scene sceneID="nhh-BJ-XiT"> <scene sceneID="nhh-BJ-XiT">
<objects> <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"> <tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y">
<color key="badgeColor" name="Primary"/> <color key="badgeColor" name="Primary"/>
</tabBarItem> </tabBarItem>
@@ -677,112 +625,102 @@ World</string>
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <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"> <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="SB5-U0-jyy">
<size key="itemSize" width="375" height="60"/> <size key="itemSize" width="375" height="60"/>
<size key="headerReferenceSize" width="50" height="50"/> <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"/> <inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout> </collectionViewFlowLayout>
<cells> <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"/> <rect key="frame" x="0.0" y="50" width="375" height="60"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d6d-uV-GFi" userLabel="App Info"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="20" y="0.0" width="335" height="60"/> <rect key="frame" x="8" y="0.0" width="359" height="60"/>
<subviews> <accessibility key="accessibilityConfiguration">
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="H12-ip-Bbl" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target"> <bool key="isElement" value="YES"/>
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/> </accessibility>
<constraints> </view>
<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>
</subviews> </subviews>
</view> </view>
<constraints> <constraints>
<constraint firstItem="4Kc-4f-KYr" firstAttribute="centerX" secondItem="dh4-fU-DFx" secondAttribute="centerX" id="9Uf-Qu-bhZ"/> <constraint firstAttribute="trailingMargin" secondItem="mos-e4-dQ7" secondAttribute="trailing" id="TKN-0r-5ON"/>
<constraint firstItem="d6d-uV-GFi" firstAttribute="leading" secondItem="kMp-ym-2yu" secondAttribute="leading" constant="20" id="fV7-0C-Hop"/> <constraint firstItem="mos-e4-dQ7" firstAttribute="top" secondItem="kMp-ym-2yu" secondAttribute="top" id="TUp-Xe-CHP"/>
<constraint firstItem="d6d-uV-GFi" firstAttribute="top" secondItem="kMp-ym-2yu" secondAttribute="top" id="rCI-7z-0mR"/> <constraint firstAttribute="bottom" secondItem="mos-e4-dQ7" secondAttribute="bottom" id="gO1-mC-cTz"/>
<constraint firstItem="dh4-fU-DFx" firstAttribute="top" secondItem="4Kc-4f-KYr" secondAttribute="bottom" constant="3" id="rmM-9v-G5C"/> <constraint firstItem="mos-e4-dQ7" firstAttribute="leading" secondItem="kMp-ym-2yu" secondAttribute="leadingMargin" id="i49-Gc-w7s"/>
<constraint firstAttribute="trailing" secondItem="d6d-uV-GFi" secondAttribute="trailing" constant="20" id="s7H-ei-AEn"/>
</constraints> </constraints>
<connections> <connections>
<outlet property="appIconImageView" destination="H12-ip-Bbl" id="61F-4i-4Q3"/> <outlet property="bannerView" destination="mos-e4-dQ7" id="z01-3x-alE"/>
<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"/>
<segue destination="0V6-N4-hTO" kind="show" identifier="showApp" id="cnd-KK-o60"> <segue destination="0V6-N4-hTO" kind="show" identifier="showApp" id="cnd-KK-o60">
<segue key="commit" inheritsFrom="parent" id="YdR-Ct-SlK"/> <segue key="commit" inheritsFrom="parent" id="YdR-Ct-SlK"/>
<segue key="preview" inheritsFrom="commit" id="GSg-SY-gai"/> <segue key="preview" inheritsFrom="commit" id="GSg-SY-gai"/>
</segue> </segue>
</connections> </connections>
</collectionViewCell> </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"/> <rect key="frame" x="0.0" y="125" width="375" height="60"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <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"> <visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7iO-O4-Mr9">
<rect key="frame" x="104" y="20" width="167" height="20.5"/> <rect key="frame" x="8" y="0.0" width="359" height="60"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="d2X-wj-EhR">
<color key="textColor" name="Primary"/> <rect key="frame" x="0.0" y="0.0" width="359" height="60"/>
<nil key="highlightedColor"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</label> <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> </subviews>
</view> </view>
<constraints> <constraints>
<constraint firstItem="z04-yg-x1t" firstAttribute="centerY" secondItem="h0f-XI-UA5" secondAttribute="centerY" id="3dw-fe-ACP"/> <constraint firstItem="7iO-O4-Mr9" firstAttribute="leading" secondItem="h0f-XI-UA5" secondAttribute="leadingMargin" id="4Kn-tp-E7l"/>
<constraint firstItem="z04-yg-x1t" firstAttribute="centerX" secondItem="h0f-XI-UA5" secondAttribute="centerX" id="AIh-kx-SmK"/> <constraint firstItem="7iO-O4-Mr9" firstAttribute="top" secondItem="h0f-XI-UA5" secondAttribute="top" id="Cxd-IB-cmI"/>
<constraint firstItem="z04-yg-x1t" firstAttribute="top" relation="greaterThanOrEqual" secondItem="h0f-XI-UA5" secondAttribute="top" constant="10" id="QwS-y9-ahl"/> <constraint firstAttribute="bottom" secondItem="7iO-O4-Mr9" secondAttribute="bottom" id="Xk3-SQ-iHD"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="z04-yg-x1t" secondAttribute="bottom" constant="10" id="uQI-7x-E3b"/> <constraint firstAttribute="trailingMargin" secondItem="7iO-O4-Mr9" secondAttribute="trailing" id="ZwB-wX-siW"/>
</constraints> </constraints>
<connections>
<outlet property="blurView" destination="7iO-O4-Mr9" id="kQ4-9N-nnv"/>
</connections>
</collectionViewCell> </collectionViewCell>
</cells> </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"> <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"/> <outlet property="textLabel" destination="BDU-hM-rro" id="CQM-8K-bcH"/>
</connections> </connections>
</collectionReusableView> </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> <connections>
<outlet property="dataSource" destination="hv7-Ar-voT" id="YOx-f4-chF"/> <outlet property="dataSource" destination="hv7-Ar-voT" id="YOx-f4-chF"/>
<outlet property="delegate" destination="hv7-Ar-voT" id="1PN-pf-cZK"/> <outlet property="delegate" destination="hv7-Ar-voT" id="1PN-pf-cZK"/>
@@ -830,12 +802,120 @@ World</string>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kiO-UO-esV" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="kiO-UO-esV" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </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> </scene>
<!--News--> <!--News-->
<scene sceneID="BV8-6J-nIv"> <scene sceneID="BV8-6J-nIv">
<objects> <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"/> <tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target"> <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> </objects>
<point key="canvasLocation" x="962" y="-752"/> <point key="canvasLocation" x="962" y="-752"/>
</scene> </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> </scenes>
<resources> <resources>
<image name="Back" width="18" height="18"/> <image name="Back" width="18" height="18"/>
<image name="BetaBadge" width="41" height="17"/>
<image name="Browse" width="20" height="20"/> <image name="Browse" width="20" height="20"/>
<image name="MyApps" width="20" height="20"/> <image name="MyApps" width="20" height="20"/>
<image name="News" width="19" height="20"/> <image name="News" width="19" height="20"/>
<image name="Settings" width="20" 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"> <namedColor name="Primary">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
</resources> </resources>
<inferredMetricsTieBreakers> <inferredMetricsTieBreakers>
<segue reference="dzt-2e-VM9"/> <segue reference="cnd-KK-o60"/>
</inferredMetricsTieBreakers> </inferredMetricsTieBreakers>
<color key="tintColor" name="Primary"/> <color key="tintColor" name="Primary"/>
</document> </document>

View File

@@ -21,39 +21,23 @@ import Nuke
} }
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
@IBOutlet var nameLabel: UILabel! @IBOutlet var bannerView: AppBannerView!
@IBOutlet var developerLabel: UILabel!
@IBOutlet var appIconImageView: UIImageView!
@IBOutlet var actionButton: PillButton!
@IBOutlet var subtitleLabel: UILabel! @IBOutlet var subtitleLabel: UILabel!
@IBOutlet var screenshotsCollectionView: UICollectionView! @IBOutlet private(set) var screenshotsCollectionView: UICollectionView!
@IBOutlet var betaBadgeView: UIImageView!
@IBOutlet private var screenshotsContentView: UIView!
override func awakeFromNib() override func awakeFromNib()
{ {
super.awakeFromNib() super.awakeFromNib()
self.contentView.preservesSuperviewLayoutMargins = true
// Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷. // Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷.
self.screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier) self.screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.screenshotsCollectionView.delegate = self self.screenshotsCollectionView.delegate = self
self.screenshotsCollectionView.dataSource = self.dataSource self.screenshotsCollectionView.dataSource = self.dataSource
self.screenshotsCollectionView.prefetchDataSource = 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 return dataSource
} }
private func update()
{
self.subtitleLabel.textColor = self.tintColor
self.screenshotsContentView.backgroundColor = self.tintColor.withAlphaComponent(0.1)
}
} }
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout

View File

@@ -1,131 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <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"> <device id="retina6_1" orientation="portrait" appearance="light"/>
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <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"> <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="400"/> <rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO"> <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"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="Y3g-Md-6xH" userLabel="App Info"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
<rect key="frame" x="20" y="20" width="335" height="79"/> <rect key="frame" x="16" y="0.0" width="343" height="369"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="F2j-pX-09A" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="7" width="65" height="65"/> <rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints> <constraints>
<constraint firstAttribute="width" secondItem="F2j-pX-09A" secondAttribute="height" multiplier="1:1" id="c2j-8O-Diw"/> <constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/>
<constraint firstAttribute="height" constant="65" id="ufl-3d-nkT"/>
</constraints> </constraints>
</imageView> </view>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="zkp-KH-OyV"> <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="76" y="21" width="176" height="37"/> <rect key="frame" x="0.0" y="103" width="343" height="17"/>
<subviews> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Ykl-yo-ncv"> <nil key="textColor"/>
<rect key="frame" x="0.0" y="0.0" width="127.5" height="20.5"/> <nil key="highlightedColor"/>
<subviews> </label>
<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"> <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="0.0" width="80.5" height="20.5"/> <rect key="frame" x="0.0" y="135" width="343" height="234"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="textColor"/> <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="15" id="jH9-Jo-IHA">
<nil key="highlightedColor"/> <size key="itemSize" width="120" height="213"/>
</label> <size key="headerReferenceSize" width="0.0" height="0.0"/>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="5gN-I2-QOB"> <size key="footerReferenceSize" width="0.0" height="0.0"/>
<rect key="frame" x="86.5" y="0.0" width="41" height="20.5"/> <inset key="sectionInset" minX="8" minY="0.0" maxX="8" maxY="0.0"/>
</imageView> </collectionViewFlowLayout>
</subviews> <cells/>
</stackView> </collectionView>
<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>
</subviews> </subviews>
</stackView> </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> </subviews>
</view> </view>
<constraints> <constraints>
<constraint firstAttribute="trailing" secondItem="w1r-LJ-TDs" secondAttribute="trailing" constant="15" id="4ns-Zq-D4j"/> <constraint firstItem="5gU-g3-Fsy" firstAttribute="top" secondItem="ln4-pC-7KY" secondAttribute="top" id="DnT-vq-BOc"/>
<constraint firstItem="w1r-LJ-TDs" firstAttribute="leading" secondItem="ln4-pC-7KY" secondAttribute="leading" constant="15" id="G1K-up-08u"/> <constraint firstItem="5gU-g3-Fsy" firstAttribute="leading" secondItem="ln4-pC-7KY" secondAttribute="leadingMargin" id="YPy-xL-iUn"/>
<constraint firstAttribute="bottom" secondItem="w1r-LJ-TDs" secondAttribute="bottom" constant="20" id="Kk0-dF-4OW"/> <constraint firstAttribute="bottom" secondItem="5gU-g3-Fsy" secondAttribute="bottom" id="gRu-Hz-CNL"/>
<constraint firstItem="Y3g-Md-6xH" firstAttribute="top" secondItem="ln4-pC-7KY" secondAttribute="top" constant="20" id="PRR-aX-AiM"/> <constraint firstAttribute="trailingMargin" secondItem="5gU-g3-Fsy" secondAttribute="trailing" id="vf4-ql-4Vq"/>
<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"/>
</constraints> </constraints>
<viewLayoutGuide key="safeArea" id="btu-iP-81i"/>
<connections> <connections>
<outlet property="actionButton" destination="DeC-Y2-fvR" id="VDk-4D-STy"/> <outlet property="bannerView" destination="ziA-mP-AY2" id="yxo-ar-Cha"/>
<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="screenshotsCollectionView" destination="RFs-qp-Ca4" id="xfi-AN-l17"/> <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"/> <outlet property="subtitleLabel" destination="Imx-Le-bcy" id="JVW-ZZ-51O"/>
</connections> </connections>
<point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/>
</collectionViewCell> </collectionViewCell>
</objects> </objects>
<resources>
<image name="BetaBadge" width="41" height="17"/>
</resources>
</document> </document>

View File

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

View File

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

View File

@@ -1,21 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <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"> <device id="retina6_1" orientation="portrait" appearance="light"/>
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <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"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target"> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<connections> <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="betaBadgeView" destination="qQl-Ez-zC5" id="6O1-Cx-7qz"/>
<outlet property="button" destination="tVx-3G-dcu" id="joa-AH-syX"/> <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="iconImageView" destination="avS-dx-4iy" id="TQs-Ej-gin"/>
<outlet property="subtitleLabel" destination="oN5-vu-Dnw" id="gA4-iJ-Tix"/> <outlet property="subtitleLabel" destination="oN5-vu-Dnw" id="gA4-iJ-Tix"/>
<outlet property="titleLabel" destination="mFe-zJ-eva" id="2OH-f8-cid"/> <outlet property="titleLabel" destination="mFe-zJ-eva" id="2OH-f8-cid"/>
<outlet property="vibrancyView" destination="fC4-1V-iMn" id="PXE-2B-A7w"/>
</connections> </connections>
</placeholder> </placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
@@ -23,6 +25,15 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/> <rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<subviews> <subviews>
@@ -34,54 +45,97 @@
</constraints> </constraints>
</imageView> </imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="caL-vN-Svn"> <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> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="1Es-pv-zwd"> <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="135" height="21.5"/> <rect key="frame" x="0.0" y="0.0" width="167" height="34"/>
<subviews> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="79" height="34"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/> <accessibility key="accessibilityConfiguration" identifier="NameLabel"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="qQl-Ez-zC5"> <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> </imageView>
</subviews> </subviews>
</stackView> </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"> <visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fC4-1V-iMn">
<rect key="frame" x="0.0" y="23.5" width="66" height="17"/> <rect key="frame" x="0.0" y="36" width="190" height="16"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="LQh-pN-ePC">
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
<nil key="highlightedColor"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</label> <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> </subviews>
</stackView> </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> </subviews>
<edgeInsets key="layoutMargins" top="14" left="14" bottom="14" right="12"/> <edgeInsets key="layoutMargins" top="14" left="14" bottom="14" right="12"/>
</stackView> </stackView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="d1T-UD-gWG" secondAttribute="bottom" id="B9e-Mf-cy5"/> <constraint firstItem="d1T-UD-gWG" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="5tv-QN-ZWU"/>
<constraint firstAttribute="trailing" secondItem="d1T-UD-gWG" secondAttribute="trailing" id="HcT-2k-z0H"/> <constraint firstItem="rZk-be-tiI" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="N6R-B2-Rie"/>
<constraint firstItem="d1T-UD-gWG" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="PIM-W5-dkh"/> <constraint firstAttribute="trailing" secondItem="d1T-UD-gWG" secondAttribute="trailing" id="mlG-w3-Ly6"/>
<constraint firstItem="d1T-UD-gWG" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="RHn-ZK-jgl"/> <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> </constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/> <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="139.85507246376812" y="152.67857142857142"/>
</view> </view>
</objects> </objects>
<resources> <resources>
<image name="BetaBadge" width="41" height="17"/> <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> </resources>
</document> </document>

View File

@@ -19,13 +19,16 @@ class AppIconImageView: UIImageView
self.backgroundColor = .white self.backgroundColor = .white
self.layer.borderWidth = 0.5 if #available(iOS 13, *)
self.layer.borderColor = self.tintColor.cgColor
// Allows us to match system look for app icons.
if self.layer.responds(to: Selector(("continuousCorners")))
{ {
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 let radius = self.bounds.height / 5
self.layer.cornerRadius = radius 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 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 class Keychain
{ {
static let shared = 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() private init()
{ {
@@ -29,66 +86,3 @@ class Keychain
self.signingCertificateSerialNumber = nil 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() private func initialize()
{ {
self.shadowImage = UIImage() if #available(iOS 13, *)
if let tintColor = self.barTintColor
{ {
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. let edgeAppearance = UINavigationBarAppearance()
// Bottom = -1 to prevent a flickering gray line from appearing. edgeAppearance.configureWithOpaqueBackground()
self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0)) 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 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

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

View File

@@ -10,11 +10,35 @@ import Roxas
class ToastView: RSTToastView class ToastView: RSTToastView
{ {
var preferredDuration: TimeInterval
override init(text: String, detailText detailedText: String?) 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) super.init(text: text, detailText: detailedText)
self.layoutMargins = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) 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) { required init(coder aDecoder: NSCoder) {
@@ -27,4 +51,14 @@ class ToastView: RSTToastView
self.layer.cornerRadius = 16 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 isDebugModeEnabled: Bool
@NSManaged var presentedLaunchReminderNotification: Bool @NSManaged var presentedLaunchReminderNotification: Bool
@NSManaged var legacySideloadedApps: [String]?
func registerDefaults() func registerDefaults()
{ {
self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true]) self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true])

View File

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

View File

@@ -11,6 +11,10 @@ import Roxas
class LaunchViewController: RSTLaunchViewController class LaunchViewController: RSTLaunchViewController
{ {
private var didFinishLaunching = false
private var destinationViewController: UIViewController!
override var launchConditions: [RSTLaunchCondition] { override var launchConditions: [RSTLaunchCondition] {
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
DatabaseManager.shared.start(completionHandler: completionHandler) DatabaseManager.shared.start(completionHandler: completionHandler)
@@ -18,6 +22,22 @@ class LaunchViewController: RSTLaunchViewController
return [isDatabaseStarted] 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 extension LaunchViewController
@@ -44,9 +64,23 @@ extension LaunchViewController
{ {
super.finishLaunching() super.finishLaunching()
guard !self.didFinishLaunching else { return }
AppManager.shared.update() AppManager.shared.update()
PatreonAPI.shared.refreshPatreonAccount() 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 Foundation
import UIKit import UIKit
import UserNotifications import UserNotifications
import MobileCoreServices
import AltSign import AltSign
import AltKit import AltKit
@@ -20,6 +21,8 @@ extension AppManager
static let didFetchSourceNotification = Notification.Name("com.altstore.AppManager.didFetchSource") static let didFetchSourceNotification = Notification.Name("com.altstore.AppManager.didFetchSource")
static let expirationWarningNotificationID = "altstore-expiration-warning" static let expirationWarningNotificationID = "altstore-expiration-warning"
static let whitelistedSideloadingBundleIDs: Set<String> = ["science.xnu.undecimus"]
} }
class AppManager class AppManager
@@ -55,16 +58,32 @@ extension AppManager
do do
{ {
let installedApps = try context.fetch(fetchRequest) 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 if app.bundleIdentifier == StoreApp.altstoreAppID
{ {
self.scheduleExpirationWarningLocalNotification(for: app) self.scheduleExpirationWarningLocalNotification(for: app)
} }
else 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) context.delete(app)
} }
} }
@@ -80,13 +99,37 @@ extension AppManager
#endif #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 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) completionHandler(result)
} }
authenticationOperation.addDependency(findServerOperation)
self.operationQueue.addOperation(authenticationOperation) self.operationQueue.addOperation(authenticationOperation)
return group
} }
} }
@@ -114,6 +157,23 @@ extension AppManager
self.operationQueue.addOperation(fetchSourceOperation) 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 extension AppManager
@@ -149,7 +209,7 @@ extension AppManager
func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup 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) let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group)
@@ -183,18 +243,6 @@ private extension AppManager
let group = group ?? OperationGroup() let group = group ?? OperationGroup()
var operations = [Operation]() 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 */ /* Find Server */
let findServerOperation = FindServerOperation(group: group) let findServerOperation = FindServerOperation(group: group)
findServerOperation.resultHandler = { (result) in findServerOperation.resultHandler = { (result) in
@@ -204,9 +252,55 @@ private extension AppManager
case .success(let server): group.server = server case .success(let server): group.server = server
} }
} }
findServerOperation.addDependency(authenticationOperation)
operations.append(findServerOperation) 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 for app in apps
{ {
@@ -220,7 +314,7 @@ private extension AppManager
guard let resignedApp = self.process(result, context: context) else { return } guard let resignedApp = self.process(result, context: context) else { return }
context.resignedApp = resignedApp context.resignedApp = resignedApp
} }
resignAppOperation.addDependency(findServerOperation) resignAppOperation.addDependency(prepareDeveloperAccountOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20) progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
operations.append(resignAppOperation) operations.append(resignAppOperation)
@@ -267,8 +361,8 @@ private extension AppManager
/* Send */ /* Send */
let sendAppOperation = SendAppOperation(context: context) let sendAppOperation = SendAppOperation(context: context)
sendAppOperation.resultHandler = { (result) in sendAppOperation.resultHandler = { (result) in
guard let connection = self.process(result, context: context) else { return } guard let installationConnection = self.process(result, context: context) else { return }
context.connection = connection context.installationConnection = installationConnection
} }
progress.addChild(sendAppOperation.progress, withPendingUnitCount: 10) progress.addChild(sendAppOperation.progress, withPendingUnitCount: 10)
sendAppOperation.addDependency(resignAppOperation) sendAppOperation.addDependency(resignAppOperation)
@@ -312,6 +406,28 @@ private extension AppManager
group.set(progress, for: app) 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) group.addOperations(operations)
return group return group
@@ -344,7 +460,11 @@ private extension AppManager
guard !context.isFinished else { return } guard !context.isFinished else { return }
context.isFinished = true 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 if let error = context.error
{ {
@@ -376,10 +496,13 @@ private extension AppManager
do { try installedApp.managedObjectContext?.save() } do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) } catch { print("Error saving installed app.", error) }
} }
}
do { try FileManager.default.removeItem(at: context.temporaryDirectory) } if let index = UserDefaults.standard.legacySideloadedApps?.firstIndex(of: installedApp.bundleIdentifier)
catch { print("Failed to remove temporary directory.", error) } {
// No longer a legacy sideloaded app, so remove it from cached list.
UserDefaults.standard.legacySideloadedApps?.remove(at: index)
}
}
print("Finished operation!", context.bundleIdentifier) print("Finished operation!", context.bundleIdentifier)

View File

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

View File

@@ -3,6 +3,6 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>_XCCurrentVersionName</key> <key>_XCCurrentVersionName</key>
<string>AltStore 2.xcdatamodel</string> <string>AltStore 4.xcdatamodel</string>
</dict> </dict>
</plist> </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. // Must go after comparing versions to see if we need to update our cached AltStore app bundle.
installedApp.version = localApp.version installedApp.update(resignedApp: localApp)
if let provisioningProfile = localApp.provisioningProfile
{
installedApp.refreshedDate = provisioningProfile.creationDate
installedApp.expirationDate = provisioningProfile.expirationDate
}
do do
{ {

View File

@@ -11,8 +11,20 @@ import CoreData
import AltSign 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) @objc(InstalledApp)
class InstalledApp: NSManagedObject, Fetchable class InstalledApp: NSManagedObject, InstalledAppProtocol
{ {
/* Properties */ /* Properties */
@NSManaged var name: String @NSManaged var name: String
@@ -22,9 +34,12 @@ class InstalledApp: NSManagedObject, Fetchable
@NSManaged var refreshedDate: Date @NSManaged var refreshedDate: Date
@NSManaged var expirationDate: Date @NSManaged var expirationDate: Date
@NSManaged var installedDate: Date
/* Relationships */ /* Relationships */
@NSManaged var storeApp: StoreApp? @NSManaged var storeApp: StoreApp?
@NSManaged var team: Team?
@NSManaged var appExtensions: Set<InstalledExtension>
var isSideloaded: Bool { var isSideloaded: Bool {
return self.storeApp == nil return self.storeApp == nil
@@ -39,10 +54,21 @@ class InstalledApp: NSManagedObject, Fetchable
{ {
super.init(entity: InstalledApp.entity(), insertInto: context) super.init(entity: InstalledApp.entity(), insertInto: context)
self.name = resignedApp.name
self.bundleIdentifier = originalBundleIdentifier 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 self.version = resignedApp.version
if let provisioningProfile = resignedApp.provisioningProfile if let provisioningProfile = resignedApp.provisioningProfile
@@ -50,11 +76,6 @@ class InstalledApp: NSManagedObject, Fetchable
self.refreshedDate = provisioningProfile.creationDate self.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate 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 return directoryURL
} }
class func installedAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
{
let installedAppUTI = "io.altstore.Installed." + bundleIdentifier
return installedAppUTI
}
var directoryURL: URL { var directoryURL: URL {
return InstalledApp.directoryURL(for: self) return InstalledApp.directoryURL(for: self)
} }
@@ -198,4 +225,8 @@ extension InstalledApp
var refreshedIPAURL: URL { var refreshedIPAURL: URL {
return InstalledApp.refreshedIPAURL(for: self) 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 extension Source
{ {
static let altStoreIdentifier = "com.rileytestut.AltStore" 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")! static let altStoreSourceURL = URL(string: "https://cdn.altstore.io/file/altstore/apps.json")!
#endif
} }
@objc(Source) @objc(Source)
@@ -22,6 +27,9 @@ class Source: NSManagedObject, Fetchable, Decodable
@NSManaged var identifier: String @NSManaged var identifier: String
@NSManaged var sourceURL: URL @NSManaged var sourceURL: URL
/* Non-Core Data Properties */
var userInfo: [ALTSourceUserInfoKey: String]?
/* Relationships */ /* Relationships */
@objc(apps) @NSManaged private(set) var _apps: NSOrderedSet @objc(apps) @NSManaged private(set) var _apps: NSOrderedSet
@objc(newsItems) @NSManaged private(set) var _newsItems: NSOrderedSet @objc(newsItems) @NSManaged private(set) var _newsItems: NSOrderedSet
@@ -49,6 +57,7 @@ class Source: NSManagedObject, Fetchable, Decodable
case name case name
case identifier case identifier
case sourceURL case sourceURL
case userInfo
case apps case apps
case news case news
} }
@@ -69,6 +78,9 @@ class Source: NSManagedObject, Fetchable, Decodable
self.identifier = try container.decode(String.self, forKey: .identifier) self.identifier = try container.decode(String.self, forKey: .identifier)
self.sourceURL = try container.decode(URL.self, forKey: .sourceURL) self.sourceURL = try container.decode(URL.self, forKey: .sourceURL)
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? [] let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? []
for (index, app) in apps.enumerated() for (index, app) in apps.enumerated()
{ {

View File

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

View File

@@ -10,11 +10,18 @@ import UIKit
class InstalledAppCollectionViewCell: UICollectionViewCell class InstalledAppCollectionViewCell: UICollectionViewCell
{ {
@IBOutlet var appIconImageView: UIImageView! @IBOutlet var bannerView: AppBannerView!
@IBOutlet var nameLabel: UILabel!
@IBOutlet var developerLabel: UILabel! override func awakeFromNib()
@IBOutlet var refreshButton: PillButton! {
@IBOutlet var betaBadgeView: UIImageView! 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 class InstalledAppsCollectionHeaderView: UICollectionReusableView
@@ -23,6 +30,24 @@ class InstalledAppsCollectionHeaderView: UICollectionReusableView
@IBOutlet var button: UIButton! @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 class UpdatesCollectionHeaderView: UICollectionReusableView
{ {
let button = PillButton(type: .system) let button = PillButton(type: .system)

View File

@@ -67,6 +67,17 @@ class MyAppsViewController: UICollectionViewController
{ {
super.viewDidLoad() 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. // Allows us to intercept delegate callbacks.
self.updatesDataSource.fetchedResultsController.delegate = self self.updatesDataSource.fetchedResultsController.delegate = self
@@ -74,7 +85,6 @@ class MyAppsViewController: UICollectionViewController
self.collectionView.prefetchDataSource = self.dataSource self.collectionView.prefetchDataSource = self.dataSource
self.prototypeUpdateCell = UpdateCollectionViewCell.instantiate(with: UpdateCollectionViewCell.nib!) self.prototypeUpdateCell = UpdateCollectionViewCell.instantiate(with: UpdateCollectionViewCell.nib!)
self.prototypeUpdateCell.translatesAutoresizingMaskIntoConstraints = false
self.prototypeUpdateCell.contentView.translatesAutoresizingMaskIntoConstraints = false self.prototypeUpdateCell.contentView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.register(UpdateCollectionViewCell.nib, forCellWithReuseIdentifier: "UpdateCell") self.collectionView.register(UpdateCollectionViewCell.nib, forCellWithReuseIdentifier: "UpdateCell")
@@ -85,10 +95,6 @@ class MyAppsViewController: UICollectionViewController
self.sideloadingProgressView.progressTintColor = .altPrimary self.sideloadingProgressView.progressTintColor = .altPrimary
self.sideloadingProgressView.progress = 0 self.sideloadingProgressView.progress = 0
#if !BETA
self.navigationItem.leftBarButtonItem = nil
#endif
if let navigationBar = self.navigationController?.navigationBar if let navigationBar = self.navigationController?.navigationBar
{ {
navigationBar.addSubview(self.sideloadingProgressView) navigationBar.addSubview(self.sideloadingProgressView)
@@ -109,6 +115,10 @@ class MyAppsViewController: UICollectionViewController
super.viewWillAppear(animated) super.viewWillAppear(animated)
self.updateDataSource() self.updateDataSource()
#if BETA
self.fetchAppIDs()
#endif
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) override func prepare(for segue: UIStoryboardSegue, sender: Any?)
@@ -138,6 +148,10 @@ class MyAppsViewController: UICollectionViewController
let installedApp = self.dataSource.item(at: indexPath) let installedApp = self.dataSource.item(at: indexPath)
return !installedApp.isSideloaded return !installedApp.isSideloaded
} }
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
{
}
} }
private extension MyAppsViewController private extension MyAppsViewController
@@ -156,9 +170,13 @@ private extension MyAppsViewController
dynamicDataSource.numberOfItemsHandler = { _ in self.updatesDataSource.itemCount == 0 ? 1 : 0 } dynamicDataSource.numberOfItemsHandler = { _ in self.updatesDataSource.itemCount == 0 ? 1 : 0 }
dynamicDataSource.cellIdentifierHandler = { _ in "NoUpdatesCell" } dynamicDataSource.cellIdentifierHandler = { _ in "NoUpdatesCell" }
dynamicDataSource.cellConfigurationHandler = { (cell, _, indexPath) in dynamicDataSource.cellConfigurationHandler = { (cell, _, indexPath) in
cell.layer.cornerRadius = 20 let cell = cell as! NoUpdatesCollectionViewCell
cell.layer.masksToBounds = true cell.layoutMargins.left = self.view.layoutMargins.left
cell.contentView.backgroundColor = UIColor.altPrimary.withAlphaComponent(0.15) cell.layoutMargins.right = self.view.layoutMargins.right
cell.blurView.layer.cornerRadius = 20
cell.blurView.layer.masksToBounds = true
cell.blurView.backgroundColor = .altPrimary
} }
return dynamicDataSource return dynamicDataSource
@@ -179,15 +197,19 @@ private extension MyAppsViewController
guard let app = installedApp.storeApp else { return } guard let app = installedApp.storeApp else { return }
let cell = cell as! UpdateCollectionViewCell let cell = cell as! UpdateCollectionViewCell
cell.tintColor = app.tintColor ?? .altPrimary cell.layoutMargins.left = self.view.layoutMargins.left
cell.nameLabel.text = app.name cell.layoutMargins.right = self.view.layoutMargins.right
cell.versionDescriptionTextView.text = app.versionDescription
cell.appIconImageView.image = nil
cell.appIconImageView.isIndicatingActivity = true
cell.betaBadgeView.isHidden = !app.isBeta
cell.updateButton.isIndicatingActivity = false cell.tintColor = app.tintColor ?? .altPrimary
cell.updateButton.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered) 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) if self.expandedAppUpdates.contains(app.bundleIdentifier)
{ {
@@ -201,9 +223,9 @@ private extension MyAppsViewController
cell.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered) cell.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
let progress = AppManager.shared.installationProgress(for: app) 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() cell.setNeedsLayout()
} }
@@ -227,8 +249,8 @@ private extension MyAppsViewController
} }
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! UpdateCollectionViewCell let cell = cell as! UpdateCollectionViewCell
cell.appIconImageView.isIndicatingActivity = false cell.bannerView.iconImageView.isIndicatingActivity = false
cell.appIconImageView.image = image cell.bannerView.iconImageView.image = image
if let error = error if let error = error
{ {
@@ -254,12 +276,15 @@ private extension MyAppsViewController
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
let cell = cell as! InstalledAppCollectionViewCell let cell = cell as! InstalledAppCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = tintColor cell.tintColor = tintColor
cell.appIconImageView.isIndicatingActivity = true
cell.betaBadgeView.isHidden = !(installedApp.storeApp?.isBeta ?? false)
cell.refreshButton.isIndicatingActivity = false cell.bannerView.iconImageView.isIndicatingActivity = true
cell.refreshButton.addTarget(self, action: #selector(MyAppsViewController.refreshApp(_:)), for: .primaryActionTriggered) 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() let currentDate = Date()
@@ -267,34 +292,34 @@ private extension MyAppsViewController
if numberOfDays == 1 if numberOfDays == 1
{ {
cell.refreshButton.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal) cell.bannerView.button.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal)
} }
else 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.bannerView.titleLabel.text = installedApp.name
cell.developerLabel.text = installedApp.storeApp?.developerName ?? NSLocalizedString("Sideloaded", comment: "") cell.bannerView.subtitleLabel.text = installedApp.storeApp?.developerName ?? NSLocalizedString("Sideloaded", comment: "")
// Make sure refresh button is correct size. // Make sure refresh button is correct size.
cell.layoutIfNeeded() cell.layoutIfNeeded()
switch numberOfDays switch numberOfDays
{ {
case 2...3: cell.refreshButton.tintColor = .refreshOrange case 2...3: cell.bannerView.button.tintColor = .refreshOrange
case 4...5: cell.refreshButton.tintColor = .refreshYellow case 4...5: cell.bannerView.button.tintColor = .refreshYellow
case 6...: cell.refreshButton.tintColor = .refreshGreen case 6...: cell.bannerView.button.tintColor = .refreshGreen
default: cell.refreshButton.tintColor = .refreshRed default: cell.bannerView.button.tintColor = .refreshRed
} }
if let refreshGroup = self.refreshGroup, let progress = refreshGroup.progress(for: installedApp), progress.fractionCompleted < 1.0 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 else
{ {
cell.refreshButton.progress = nil cell.bannerView.button.progress = nil
} }
} }
dataSource.prefetchHandler = { (item, indexPath, completion) in dataSource.prefetchHandler = { (item, indexPath, completion) in
@@ -312,8 +337,8 @@ private extension MyAppsViewController
} }
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! InstalledAppCollectionViewCell let cell = cell as! InstalledAppCollectionViewCell
cell.appIconImageView.image = image cell.bannerView.iconImageView.image = image
cell.appIconImageView.isIndicatingActivity = false cell.bannerView.iconImageView.isIndicatingActivity = false
} }
return dataSource 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(_ installedApps: [InstalledApp], completionHandler: @escaping (Result<[String : Result<InstalledApp, Error>], Error>) -> Void)
{ {
func refresh() func refresh()
@@ -368,9 +408,8 @@ private extension MyAppsViewController
switch result switch result
{ {
case .failure(let error): case .failure(let error):
let toastView = ToastView(text: error.localizedDescription, detailText: nil) let toastView = ToastView(error: error)
toastView.setNeedsLayout() toastView.show(in: self)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
case .success(let results): case .success(let results):
let failures = results.compactMapValues { (result) -> Error? in let failures = results.compactMapValues { (result) -> Error? in
@@ -384,22 +423,32 @@ private extension MyAppsViewController
guard !failures.isEmpty else { break } guard !failures.isEmpty else { break }
let localizedText: String let toastView: ToastView
let detailText: String?
if let failure = failures.first, failures.count == 1 if let failure = failures.first, results.count == 1
{ {
localizedText = failure.value.localizedDescription toastView = ToastView(error: failure.value)
detailText = nil
} }
else else
{ {
localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count)) let localizedText: String
detailText = failures.first?.value.localizedDescription
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)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
} }
self.refreshGroup = nil self.refreshGroup = nil
@@ -559,8 +608,8 @@ private extension MyAppsViewController
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
case .failure(let error): case .failure(let error):
let toastView = ToastView(text: error.localizedDescription, detailText: nil) let toastView = ToastView(error: error)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2) toastView.show(in: self)
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
@@ -591,7 +640,7 @@ private extension MyAppsViewController
func presentSideloadingAlert(completion: @escaping (Bool) -> Void) 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 alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .default, handler: { (action) in
completion(true) completion(true)
})) }))
@@ -601,7 +650,7 @@ private extension MyAppsViewController
self.present(alertController, animated: true, completion: nil) 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 self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
@@ -614,7 +663,11 @@ private extension MyAppsViewController
let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory) 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 self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in
try? FileManager.default.removeItem(at: temporaryDirectory) try? FileManager.default.removeItem(at: temporaryDirectory)
@@ -622,8 +675,8 @@ private extension MyAppsViewController
DispatchQueue.main.async { DispatchQueue.main.async {
if let error = result.error if let error = result.error
{ {
let toastView = ToastView(text: error.localizedDescription, detailText: nil) let toastView = ToastView(error: error)
toastView.show(in: self.view, duration: 2.0) toastView.show(in: self)
} }
else else
{ {
@@ -648,7 +701,31 @@ private extension MyAppsViewController
{ {
try? FileManager.default.removeItem(at: temporaryDirectory) 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)) completion(.failure(error))
} }
@@ -700,14 +777,22 @@ private extension MyAppsViewController
else { return } else { return }
let installedApp = self.dataSource.item(at: indexPath) let installedApp = self.dataSource.item(at: indexPath)
guard installedApp.storeApp == nil else { return }
#if DEBUG
self.presentAlert(for: installedApp) 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) @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 let fileURL = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return }
guard self.presentedViewController == nil else { return } guard self.presentedViewController == nil else { return }
@@ -724,10 +809,12 @@ private extension MyAppsViewController
} }
} }
#if BETA
self.presentSideloadingAlert { (shouldContinue) in self.presentSideloadingAlert { (shouldContinue) in
if shouldContinue if shouldContinue
{ {
self.installApp(at: fileURL) { (result) in self.sideloadApp(at: fileURL) { (result) in
finish() finish()
} }
} }
@@ -737,6 +824,12 @@ private extension MyAppsViewController
} }
} }
#else
self.sideloadApp(at: fileURL) { (result) in
finish()
}
#endif #endif
} }
} }
@@ -776,7 +869,7 @@ extension MyAppsViewController
return headerView return headerView
case .installedApps: case .installedApps where kind == UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InstalledAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InstalledAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
UIView.performWithoutAnimation { UIView.performWithoutAnimation {
@@ -792,6 +885,35 @@ extension MyAppsViewController
} }
return headerView 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 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] let section = Section.allCases[indexPath.section]
switch section switch section
{ {
case .noUpdates: case .noUpdates:
let size = CGSize(width: width, height: 44) let size = CGSize(width: collectionView.bounds.width, height: 44)
return size return size
case .updates: case .updates:
@@ -831,7 +950,10 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
return previousHeight 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]) NSLayoutConstraint.activate([widthConstraint])
defer { NSLayoutConstraint.deactivate([widthConstraint]) } defer { NSLayoutConstraint.deactivate([widthConstraint]) }
@@ -842,7 +964,7 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
return size return size
case .installedApps: 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] let section = Section.allCases[section]
switch section switch section
{ {
case .noUpdates: case .noUpdates: return .zero
guard self.updatesDataSource.itemCount == 0 else { return .zero } case .updates: return .zero
return UIEdgeInsets(top: 12, left: 15, bottom: 20, right: 15) case .installedApps:
#if BETA
guard let _ = DatabaseManager.shared.activeTeam() else { return .zero }
case .updates: let indexPath = IndexPath(row: 0, section: section.rawValue)
guard self.updatesDataSource.itemCount > 0 else { return .zero } let footerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionFooter, at: indexPath) as! InstalledAppsCollectionFooterView
return UIEdgeInsets(top: 12, left: 15, bottom: 20, right: 15)
case .installedApps: return UIEdgeInsets(top: 12, left: 0, bottom: 20, right: 0) 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 } 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) print("Sideloaded app at \(fileURL) with result:", result)
} }
} }

View File

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

View File

@@ -1,130 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <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"> <device id="retina6_1" orientation="portrait" appearance="light"/>
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <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"> <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="133.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO"> <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"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dmf-hv-bwx"> <view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dmf-hv-bwx">
<rect key="frame" x="0.0" y="0.0" width="375" height="133.5"/> <rect key="frame" x="16" y="0.0" width="343" height="125"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="57X-Ep-rfq"> <visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1xN-9h-DFd">
<rect key="frame" x="20" y="20" width="340" height="93.5"/> <rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
<subviews> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="9iq-CR-Xc4">
<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="343" height="125"/>
<rect key="frame" x="0.0" y="0.0" width="340" height="65"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="jg6-wi-ngb" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="uYl-PH-DuP">
<rect key="frame" x="0.0" y="0.0" width="65" height="65"/> <rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
<constraints> <subviews>
<constraint firstAttribute="height" constant="65" id="W3C-hH-1Ii"/> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<constraint firstAttribute="width" secondItem="jg6-wi-ngb" secondAttribute="height" multiplier="1:1" id="vt3-Qt-m21"/> <rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
</constraints> <constraints>
</imageView> <constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="2Ii-Hu-4ru"> </constraints>
<rect key="frame" x="76" y="14" width="172" height="37"/> </view>
<subviews> <stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="9Zk-Mp-JI7"> <rect key="frame" x="0.0" y="88" width="343" height="37"/>
<rect key="frame" x="0.0" y="0.0" width="89.5" height="20.5"/> <subviews>
<subviews> <visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RKU-pY-wmQ">
<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="15" y="0.0" width="65" height="22"/>
<rect key="frame" x="0.0" y="0.0" width="42.5" height="20.5"/> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="4GQ-XP-i7X">
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <rect key="frame" x="0.0" y="0.0" width="65" height="22"/>
<nil key="textColor"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<nil key="highlightedColor"/> <subviews>
</label> <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">
<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="0.0" y="0.0" width="65" height="22"/>
<rect key="frame" x="48.5" y="0.0" width="41" height="20.5"/> <constraints>
</imageView> <constraint firstAttribute="width" constant="65" id="C7Y-nh-TKJ"/>
</subviews> </constraints>
</stackView> <fontDescription key="fontDescription" type="system" pointSize="11"/>
<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"> <nil key="textColor"/>
<rect key="frame" x="0.0" y="22.5" width="57.5" height="14.5"/> <nil key="highlightedColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/> </label>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> </subviews>
<nil key="highlightedColor"/> <constraints>
</label> <constraint firstItem="h1u-nj-qsP" firstAttribute="leading" secondItem="4GQ-XP-i7X" secondAttribute="leading" id="3cO-Mj-Yua"/>
</subviews> <constraint firstAttribute="trailing" secondItem="h1u-nj-qsP" secondAttribute="trailing" id="Hek-OE-YMc"/>
</stackView> <constraint firstAttribute="bottom" secondItem="h1u-nj-qsP" secondAttribute="bottom" id="bLg-Ut-aEb"/>
<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"> <constraint firstItem="h1u-nj-qsP" firstAttribute="top" secondItem="4GQ-XP-i7X" secondAttribute="top" id="beL-ob-CQ7"/>
<rect key="frame" x="259" y="17" width="81" height="31"/> </constraints>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/> </view>
<constraints> <vibrancyEffect style="secondaryLabel">
<constraint firstAttribute="width" constant="81" id="3yj-p0-NuE"/> <blurEffect style="systemChromeMaterial"/>
<constraint firstAttribute="height" constant="31" id="KbP-M6-N3w"/> </vibrancyEffect>
</constraints> </visualEffectView>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/> <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">
<state key="normal" title="UPDATE"/> <rect key="frame" x="90" y="0.0" width="238" height="22"/>
</button> <color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</subviews> <fontDescription key="fontDescription" type="system" pointSize="13"/>
</stackView> <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes"> </textView>
<rect key="frame" x="0.0" y="79" width="340" height="14.5"/> </subviews>
<subviews> <edgeInsets key="layoutMargins" top="0.0" left="15" bottom="15" right="15"/>
<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"> </stackView>
<rect key="frame" x="0.0" y="0.0" width="65" height="13.5"/> </subviews>
<constraints> </stackView>
<constraint firstAttribute="width" constant="65" id="C7Y-nh-TKJ"/> </subviews>
</constraints> <color key="backgroundColor" name="BlurTint"/>
<fontDescription key="fontDescription" type="system" pointSize="11"/> <constraints>
<nil key="textColor"/> <constraint firstAttribute="trailing" secondItem="uYl-PH-DuP" secondAttribute="trailing" id="51O-j6-eoh"/>
<nil key="highlightedColor"/> <constraint firstAttribute="bottom" secondItem="uYl-PH-DuP" secondAttribute="bottom" id="IGs-MS-vnM"/>
</label> <constraint firstItem="uYl-PH-DuP" firstAttribute="top" secondItem="9iq-CR-Xc4" secondAttribute="top" id="hnr-wG-XRY"/>
<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"> <constraint firstItem="uYl-PH-DuP" firstAttribute="leading" secondItem="9iq-CR-Xc4" secondAttribute="leading" id="usR-Ia-LMy"/>
<rect key="frame" x="75" y="-10" width="265" height="24.5"/> </constraints>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> </view>
<fontDescription key="fontDescription" type="system" pointSize="13"/> <blurEffect style="systemChromeMaterial"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> </visualEffectView>
</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>
</subviews> </subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="57X-Ep-rfq" secondAttribute="bottom" constant="20" id="ArC-R2-jtc"/> <constraint firstItem="1xN-9h-DFd" firstAttribute="top" secondItem="dmf-hv-bwx" secondAttribute="top" id="6rb-Bw-UVn"/>
<constraint firstItem="57X-Ep-rfq" firstAttribute="leading" secondItem="mdL-JE-wCe" secondAttribute="leading" constant="20" id="PvV-gg-7us"/> <constraint firstAttribute="bottom" secondItem="1xN-9h-DFd" secondAttribute="bottom" id="dnI-NB-BKv"/>
<constraint firstItem="57X-Ep-rfq" firstAttribute="top" secondItem="dmf-hv-bwx" secondAttribute="top" constant="20" id="QHM-k8-g0x"/> <constraint firstAttribute="trailing" secondItem="1xN-9h-DFd" secondAttribute="trailing" id="kbY-Z6-V86"/>
<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="leading" secondItem="dmf-hv-bwx" secondAttribute="leading" id="ofk-a7-m0Y"/>
</constraints> </constraints>
<edgeInsets key="layoutMargins" top="20" left="20" bottom="20" right="20"/> <edgeInsets key="layoutMargins" top="20" left="20" bottom="20" right="20"/>
<viewLayoutGuide key="safeArea" id="mdL-JE-wCe"/>
</view> </view>
</subviews> </subviews>
</view> </view>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints> <constraints>
<constraint firstItem="dmf-hv-bwx" firstAttribute="top" secondItem="Kqf-Pv-ca3" secondAttribute="top" id="7yY-05-eHt"/> <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 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 firstItem="dmf-hv-bwx" firstAttribute="leading" secondItem="Kqf-Pv-ca3" secondAttribute="leadingMargin" id="W0V-sT-tXo"/>
<constraint firstAttribute="trailing" secondItem="dmf-hv-bwx" secondAttribute="trailing" id="tgy-Zi-iZF"/> <constraint firstAttribute="trailingMargin" secondItem="dmf-hv-bwx" secondAttribute="trailing" id="tgy-Zi-iZF"/>
</constraints> </constraints>
<viewLayoutGuide key="safeArea" id="C6r-zO-INg"/>
<connections> <connections>
<outlet property="appIconImageView" destination="jg6-wi-ngb" id="j83-Dl-GT6"/> <outlet property="bannerView" destination="Nop-pL-Icx" id="GiX-K1-5oz"/>
<outlet property="betaBadgeView" destination="4LS-dp-4VA" id="Q2Z-AG-Y19"/> <outlet property="blurView" destination="1xN-9h-DFd" id="HBI-nT-xYh"/>
<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="versionDescriptionTextView" destination="rNs-2O-k3V" id="4TC-A3-oxb"/> <outlet property="versionDescriptionTextView" destination="rNs-2O-k3V" id="4TC-A3-oxb"/>
<outlet property="versionDescriptionTitleLabel" destination="h1u-nj-qsP" id="dnz-Yv-BdY"/> <outlet property="versionDescriptionTitleLabel" destination="h1u-nj-qsP" id="dnz-Yv-BdY"/>
</connections> </connections>
@@ -132,6 +114,8 @@
</collectionViewCell> </collectionViewCell>
</objects> </objects>
<resources> <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> </resources>
</document> </document>

View File

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

View File

@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <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"> <device id="retina4_7" orientation="portrait" appearance="light"/>
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <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"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
@@ -18,23 +16,26 @@
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/> <rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xba-Qs-SQo"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="azr-Ea-luN">
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/> <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> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="tNk-9u-1tk"> <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> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="akF-Tr-G5M"> <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> <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"/> <rect key="frame" x="25" y="25" width="54.5" height="26.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </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"> <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="17"/> <rect key="frame" x="25" y="61.5" width="35.5" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -43,9 +44,9 @@
<edgeInsets key="layoutMargins" top="25" left="25" bottom="20" right="25"/> <edgeInsets key="layoutMargins" top="25" left="25" bottom="20" right="25"/>
</stackView> </stackView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="335" placeholderIntrinsicHeight="200" translatesAutoresizingMaskIntoConstraints="NO" id="l36-Bm-De0"> <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> <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> </constraints>
</imageView> </imageView>
</subviews> </subviews>
@@ -63,15 +64,21 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints> <constraints>
<constraint firstItem="Xba-Qs-SQo" firstAttribute="top" secondItem="wRF-2R-NUG" secondAttribute="top" id="0xe-Rt-MhF"/> <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 firstItem="Xba-Qs-SQo" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leadingMargin" id="5MO-c0-5rG"/>
<constraint firstAttribute="trailing" secondItem="Xba-Qs-SQo" secondAttribute="trailing" id="DNL-Jj-3By"/> <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="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> </constraints>
<connections> <connections>
<outlet property="captionLabel" destination="SHB-kk-YhL" id="zY3-qQ-9oY"/> <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="imageView" destination="l36-Bm-De0" id="3do-aQ-5r4"/>
<outlet property="titleLabel" destination="AkN-BE-I1a" id="hA2-3O-q5J"/> <outlet property="titleLabel" destination="AkN-BE-I1a" id="hA2-3O-q5J"/>
</connections> </connections>
<point key="canvasLocation" x="138" y="153"/>
</collectionViewCell> </collectionViewCell>
</objects> </objects>
</document> </document>

View File

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

View File

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

View File

@@ -8,7 +8,9 @@
import Foundation import Foundation
import Roxas import Roxas
import Network
import AltKit
import AltSign import AltSign
enum AuthenticationError: LocalizedError enum AuthenticationError: LocalizedError
@@ -30,13 +32,18 @@ enum AuthenticationError: LocalizedError
} }
@objc(AuthenticationOperation) @objc(AuthenticationOperation)
class AuthenticationOperation: ResultOperation<ALTSigner> class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
{ {
let group: OperationGroup
private weak var presentingViewController: UIViewController? private weak var presentingViewController: UIViewController?
private lazy var navigationController: UINavigationController = { private lazy var navigationController: UINavigationController = {
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
navigationController.presentationController?.delegate = self if #available(iOS 13.0, *)
{
navigationController.isModalInPresentation = true
}
return navigationController return navigationController
}() }()
@@ -45,14 +52,18 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
private var appleIDPassword: String? private var appleIDPassword: String?
private var shouldShowInstructions = false 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 self.presentingViewController = presentingViewController
super.init() super.init()
self.operationQueue.name = "com.altstore.AuthenticationOperation"
self.progress.totalUnitCount = 3 self.progress.totalUnitCount = 3
} }
@@ -60,18 +71,24 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
{ {
super.main() super.main()
if let error = self.group.error
{
self.finish(.failure(error))
return
}
// Sign In // Sign In
self.signIn { (result) in self.signIn() { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result switch result
{ {
case .failure(let error): self.finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success(let account): case .success(let account, let session):
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
// Fetch Team // 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)) } guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result switch result
@@ -81,7 +98,7 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
// Fetch Certificate // 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)) } guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result switch result
@@ -90,11 +107,22 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
case .success(let certificate): case .success(let certificate):
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
let signer = ALTSigner(team: team, certificate: certificate) // Save account/team to disk.
self.signer = signer self.save(team) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
self.showInstructionsIfNecessary() { (didShowInstructions) in switch result
self.finish(.success(signer)) {
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 } guard !self.isFinished else { return }
print("Finished authenticating with result:", result) print("Finished authenticating with result:", result)
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait { context.perform {
do do
{ {
let signer = try result.get() let (signer, session) = try result.get()
let altAccount = signer.team.account
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 // Account
let account = Account(altAccount, context: context)
account.isActiveAccount = true account.isActiveAccount = true
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account> let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
@@ -131,7 +203,6 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
} }
// Team // Team
let team = Team(signer.team, account: account, context: context)
team.isActiveTeam = true team.isActiveTeam = true
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team> let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
@@ -143,25 +214,41 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
team.isActiveTeam = false 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 // Save
try context.save() try context.save()
// Update keychain // 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.appleIDPassword = self.appleIDPassword
Keychain.shared.signingCertificateSerialNumber = signer.certificate.serialNumber Keychain.shared.signingCertificate = signer.certificate.p12Data()
Keychain.shared.signingCertificatePrivateKey = signer.certificate.privateKey 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 catch
{ {
super.finish(.failure(error)) super.finish(result)
}
DispatchQueue.main.async { DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil) self.navigationController.dismiss(animated: true, completion: nil)
}
} }
} }
} }
@@ -194,21 +281,26 @@ private extension AuthenticationOperation
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() func authenticate()
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
authenticationViewController.authenticationHandler = { (result) in authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
if let (account, password) = result 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. // We presented the Auth UI and the user signed in.
// In this case, we'll assume we should show the instructions again. // In this case, we'll assume we should show the instructions again.
self.shouldShowInstructions = true self.shouldShowInstructions = true
self.appleIDPassword = password self.appleIDPassword = password
completionHandler(.success(account)) completionHandler(.success((account, session)))
} }
else else
{ {
@@ -225,24 +317,17 @@ private extension AuthenticationOperation
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
{ {
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in self.authenticate(appleID: appleID, password: password) { (result) in
do switch result
{ {
case .success(let account, let session):
self.appleIDPassword = password self.appleIDPassword = password
completionHandler(.success((account, session)))
let account = try Result(account, error).get() case .failure(ALTAppleAPIError.incorrectCredentials), .failure(ALTAppleAPIError.appSpecificPasswordRequired):
completionHandler(.success(account))
}
catch ALTAppleAPIError.incorrectCredentials
{
authenticate() authenticate()
}
catch ALTAppleAPIError.appSpecificPasswordRequired case .failure(let error):
{
authenticate()
}
catch
{
completionHandler(.failure(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]) 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) switch Result(teams, error)
{ {
case .failure(let error): completionHandler(.failure(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() func requestCertificate()
{ {
let machineName = "AltStore - " + UIDevice.current.name 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 do
{ {
let certificate = try Result(certificate, error).get() let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey } 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 do
{ {
let certificates = try Result(certificates, error).get() 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)) } 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 if let error = error, !success
{ {
completionHandler(.failure(error)) 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 do
{ {
let certificates = try Result(certificates, error).get() let certificates = try Result(certificates, error).get()
if 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 serialNumber = Keychain.shared.signingCertificateSerialNumber,
let privateKey = Keychain.shared.signingCertificatePrivateKey, let privateKey = Keychain.shared.signingCertificatePrivateKey,
let certificate = certificates.first(where: { $0.serialNumber == serialNumber }) 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 certificate.privateKey = privateKey
completionHandler(.success(certificate)) 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 else if certificates.isEmpty
{ {
// No certificates, so request a new one.
requestCertificate() requestCertificate()
} }
else 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) 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) func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
{ {
guard self.shouldShowInstructions else { return completionHandler(false) } guard self.shouldShowInstructions else { return completionHandler(false) }
@@ -392,19 +598,40 @@ private extension AuthenticationOperation
} }
} }
} }
}
extension AuthenticationOperation: UIAdaptivePresentationControllerDelegate func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
{
func presentationControllerWillDismiss(_ presentationController: UIPresentationController)
{ {
if let signer = self.signer guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) }
{
self.finish(.success(signer)) // 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) }
else
{ #if DEBUG
self.finish(.failure(OperationError.cancelled)) 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 decoder.managedObjectContext = context
let source = try decoder.decode(Source.self, from: data) 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)) self.finish(.success(source))
} }
catch catch

View File

@@ -7,13 +7,26 @@
// //
import Foundation import Foundation
import AltKit
import Roxas 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) @objc(FindServerOperation)
class FindServerOperation: ResultOperation<Server> class FindServerOperation: ResultOperation<Server>
{ {
let group: OperationGroup let group: OperationGroup
private var isWiredServerConnectionAvailable = false
init(group: OperationGroup) init(group: OperationGroup)
{ {
self.group = group self.group = group
@@ -31,21 +44,49 @@ class FindServerOperation: ResultOperation<Server>
return return
} }
if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred }) let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
{
// Preferred server. // Prepare observers to receive callback from wired server (if connected).
self.finish(.success(server)) CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedWiredServerConnectionResponse, CFNotificationName.wiredServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately)
} NotificationCenter.default.addObserver(self, selector: #selector(FindServerOperation.didReceiveWiredServerConnectionResponse(_:)), name: .didReceiveWiredServerConnectionResponse, object: nil)
else if let server = ServerManager.shared.discoveredServers.first
{ // Post notification.
// Any available server. CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionAvailableRequest, nil, nil, true)
self.finish(.success(server))
} // Wait for either callback or timeout.
else DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
{ if self.isWiredServerConnectionAvailable
// No servers. {
self.finish(.failure(ConnectionError.serverNotFound)) 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 let context: AppOperationContext
private var didCleanUp = false
init(context: AppOperationContext) init(context: AppOperationContext)
{ {
self.context = context self.context = context
@@ -39,12 +41,13 @@ class InstallAppOperation: ResultOperation<InstalledApp>
guard guard
let resignedApp = self.context.resignedApp, let resignedApp = self.context.resignedApp,
let connection = self.context.connection, let connection = self.context.installationConnection
let server = self.context.group.server
else { return self.finish(.failure(OperationError.invalidParameters)) } else { return self.finish(.failure(OperationError.invalidParameters)) }
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
backgroundContext.perform { backgroundContext.perform {
/* App */
let installedApp: InstalledApp let installedApp: InstalledApp
// Fetch + update rather than insert + resolve merge conflicts to prevent potential context-level conflicts. // 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 = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, context: backgroundContext)
} }
installedApp.version = resignedApp.version installedApp.update(resignedApp: resignedApp)
if let profile = resignedApp.provisioningProfile if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
{ {
installedApp.refreshedDate = profile.creationDate installedApp.team = team
installedApp.expirationDate = profile.expirationDate
} }
/* 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) self.context.group.beginInstallationHandler?(installedApp)
let request = BeginInstallationRequest() let request = BeginInstallationRequest()
server.send(request, via: connection) { (result) in connection.send(request) { (result) in
switch result switch result
{ {
case .failure(let error): self.finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success: case .success:
self.receive(from: connection, server: server) { (result) in self.receive(from: connection) { (result) in
switch result switch result
{ {
case .success: 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 do
{ {
let response = try result.get() let response = try result.get()
print(response) print(response)
if let error = response.error switch response
{ {
completionHandler(.failure(error)) case .installationProgress(let response):
} if response.progress == 1.0
else if response.progress == 1.0 {
{ self.progress.completedUnitCount = self.progress.totalUnitCount
self.progress.completedUnitCount = self.progress.totalUnitCount completionHandler(.success(()))
completionHandler(.success(())) }
} else
else {
{ self.progress.completedUnitCount = Int64(response.progress * 100)
self.progress.completedUnitCount = Int64(response.progress * 100) self.receive(from: connection, completionHandler: completionHandler)
self.receive(from: connection, server: server, completionHandler: completionHandler) }
case .error(let response):
completionHandler(.failure(response.error))
default:
completionHandler(.failure(ALTServerError(.unknownRequest)))
} }
} }
catch 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 invalidParameters
case iOSVersionNotSupported(ALTApplication) case iOSVersionNotSupported(ALTApplication)
case sideloadingAppNotSupported(ALTApplication)
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
case noSources case noSources
@@ -49,6 +51,53 @@ enum OperationError: LocalizedError
let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version) let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version)
return localizedDescription 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 completionHandler: ((Result<[String: Result<InstalledApp, Error>], Error>) -> Void)?
var beginInstallationHandler: ((InstalledApp) -> Void)? var beginInstallationHandler: ((InstalledApp) -> Void)?
var session: ALTAppleAPISession?
var server: Server? var server: Server?
var signer: ALTSigner? var signer: ALTSigner?
@@ -73,7 +75,12 @@ class OperationGroup
func progress(for app: AppProtocol) -> Progress? 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 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 guard
let app = self.context.app, 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)) } else { return self.finish(.failure(OperationError.invalidParameters)) }
// Register Device // Prepare Provisioning Profiles
self.registerCurrentDevice(for: signer.team) { (result) in self.prepareProvisioningProfiles(app.fileURL, team: signer.team, session: session) { (result) in
guard let _ = self.process(result) else { return } guard let profiles = self.process(result) else { return }
// Prepare Provisioning Profiles // Prepare app bundle
self.prepareProvisioningProfiles(app.fileURL, team: signer.team) { (result) in let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
guard let profiles = self.process(result) else { return } self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
// Prepare app bundle let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2) guard let appBundleURL = self.process(result) else { return }
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in print("Resigning App:", self.context.bundleIdentifier)
guard let appBundleURL = 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 }
// Resign app bundle // Finish
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in do
guard let resignedURL = self.process(result) else { return } {
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
// Finish // Use appBundleURL since we need an app bundle, not .ipa.
do guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
{ self.finish(.success(resignedApplication))
let destinationURL = InstalledApp.refreshedIPAURL(for: app) }
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true) catch
{
// Use appBundleURL since we need an app bundle, not .ipa. self.finish(.failure(error))
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 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 { guard let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
return completionHandler(.failure(OperationError.unknownUDID))
}
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) 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
switch result switch result
{ {
case .failure(let error): completionHandler(.failure(error)) 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 let dispatchGroup = DispatchGroup()
self.updateFeatures(for: appID, app: app, team: team) { (result) in
switch result for appExtension in app.appExtensions
{
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
if let error = error
{ {
case .failure(let error): completionHandler(.failure(error)) completionHandler(.failure(error))
case .success(let appID): }
else
{
completionHandler(.success(profiles))
}
}
}
}
}
// Update app groups func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
self.updateAppGroups(for: appID, app: app, team: team) { (result) in {
switch result DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Fetch Provisioning Profile let preferredBundleID: String
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
completionHandler(result) // 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 ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
let bundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
do do
{ {
let appIDs = try Result(appIDs, error).get() 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)) completionHandler(.success(appID))
} }
else else
{ {
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in let requiredAppIDs = 1 + application.appExtensions.count
completionHandler(Result(appID, error)) 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 let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
guard let feature = ALTFeature(entitlement: entitlement) else { return nil } guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
@@ -263,15 +297,41 @@ private extension ResignAppOperation
features[.appGroups] = true features[.appGroups] = true
} }
let appID = appID.copy() as! ALTAppID var updateFeatures = false
appID.features = features
ALTAppleAPI.shared.update(appID, team: team) { (appID, error) in // Determine whether the required features are already enabled for the AppID.
completionHandler(Result(appID, error)) 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. // TODO: Handle apps belonging to more than one app group.
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else { guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
@@ -287,7 +347,7 @@ private extension ResignAppOperation
// Assign App Group // Assign App Group
// TODO: Determine whether app already belongs to 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 } let result = result.map { _ in appID }
completionHandler(result) completionHandler(result)
} }
@@ -296,7 +356,7 @@ private extension ResignAppOperation
let adjustedGroupIdentifier = "group.\(team.identifier)." + groupIdentifier 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) switch Result(groups, error)
{ {
case .failure(let error): completionHandler(.failure(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). // Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ") 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)) 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) switch Result(profile, error)
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
case .success(let profile): case .success(let profile):
// Delete existing 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) switch Result(success, error)
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
case .success: case .success:
// Fetch new provisiong profile // 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)) completionHandler(Result(profile, error))
} }
} }
@@ -371,6 +431,17 @@ private extension ResignAppOperation
infoDictionary[Bundle.Info.appGroups] = appGroups 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) 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 } guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
additionalValues[Bundle.Info.deviceID] = udid additionalValues[Bundle.Info.deviceID] = udid
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID 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 // Prepare app

View File

@@ -12,13 +12,13 @@ import Network
import AltKit import AltKit
@objc(SendAppOperation) @objc(SendAppOperation)
class SendAppOperation: ResultOperation<NWConnection> class SendAppOperation: ResultOperation<ServerConnection>
{ {
let context: AppOperationContext let context: AppOperationContext
private let dispatchQueue = DispatchQueue(label: "com.altstore.SendAppOperation") private let dispatchQueue = DispatchQueue(label: "com.altstore.SendAppOperation")
private var connection: NWConnection? private var serverConnection: ServerConnection?
init(context: AppOperationContext) init(context: AppOperationContext)
{ {
@@ -45,21 +45,21 @@ class SendAppOperation: ResultOperation<NWConnection>
let fileURL = InstalledApp.refreshedIPAURL(for: app) let fileURL = InstalledApp.refreshedIPAURL(for: app)
// Connect to server. // Connect to server.
self.connect(to: server) { (result) in ServerManager.shared.connect(to: server) { (result) in
switch result switch result
{ {
case .failure(let error): self.finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success(let connection): case .success(let serverConnection):
self.connection = connection self.serverConnection = serverConnection
// Send app to server. // Send app to server.
self.sendApp(at: fileURL, via: connection, server: server) { (result) in self.sendApp(at: fileURL, via: serverConnection) { (result) in
switch result switch result
{ {
case .failure(let error): self.finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success: case .success:
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
self.finish(.success(connection)) self.finish(.success(serverConnection))
} }
} }
} }
@@ -69,34 +69,7 @@ class SendAppOperation: ResultOperation<NWConnection>
private extension SendAppOperation private extension SendAppOperation
{ {
func connect(to server: Server, completionHandler: @escaping (Result<NWConnection, Error>) -> Void) func sendApp(at fileURL: URL, via connection: ServerConnection, completionHandler: @escaping (Result<Void, 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)
{ {
do do
{ {
@@ -106,14 +79,14 @@ private extension SendAppOperation
let request = PrepareAppRequest(udid: udid, contentSize: appData.count) let request = PrepareAppRequest(udid: udid, contentSize: appData.count)
print("Sending request \(request)") print("Sending request \(request)")
server.send(request, via: connection) { (result) in connection.send(request) { (result) in
switch result switch result
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): completionHandler(.failure(error))
case .success: case .success:
print("Sending app data (\(appData.count) bytes)") 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 switch result
{ {
case .failure(let error): completionHandler(.failure(error)) 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 clientID = "ZMx0EGUWe4TVWYXNZZwK_fbIK5jHFVWoUf1Qb-sqNXmT-YzAGwDPxxq7ak3_W5Q2"
private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt" private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt"
private let creatorAccessToken = "NSX1ts9Rf9IzKRCu8GjbwsZ6wll8bDtoJxNbPbp2eZo"
private let campaignID = "2863968" private let campaignID = "2863968"
@@ -71,7 +70,7 @@ extension PatreonAPI
} }
} }
class PatreonAPI class PatreonAPI: NSObject
{ {
static let shared = PatreonAPI() static let shared = PatreonAPI()
@@ -84,8 +83,9 @@ class PatreonAPI
private let session = URLSession(configuration: .ephemeral) private let session = URLSession(configuration: .ephemeral)
private let baseURL = URL(string: "https://www.patreon.com/")! 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() self.authenticationSession?.start()
} }
@@ -165,7 +170,8 @@ extension PatreonAPI
var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(campaignID)/members")! var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(campaignID)/members")!
components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"), components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"),
URLQueryItem(name: "fields[tier]", value: "title"), 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)! let requestURL = components.url(relativeTo: self.baseURL)!
@@ -342,7 +348,9 @@ private extension PatreonAPI
{ {
case .none: break case .none: break
case .creator: case .creator:
guard let creatorAccessToken = Keychain.shared.patreonCreatorAccessToken else { return completion(.failure(Error.invalidAccessToken)) }
request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization") request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization")
case .user: case .user:
guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(Error.notAuthenticated)) } guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(Error.notAuthenticated)) }
request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization") request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
@@ -384,3 +392,12 @@ private extension PatreonAPI
task.resume() 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", "name": "AltStore",
"bundleIdentifier": "com.rileytestut.AltStore", "bundleIdentifier": "com.rileytestut.AltStore",
"developerName": "Riley Testut", "developerName": "Riley Testut",
"version": "1.0.1", "version": "1.2",
"versionDate": "2019-09-28T03:00:00-07:00", "versionDate": "2020-02-12T08:00:00-08:00",
"versionDescription": "Fixes Patreon bugs.", "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", "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.", "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", "iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
"tintColor": "018084", "tintColor": "018084",
"size": 3481256, "size": 2114068,
"screenshotURLs": [ "screenshotURLs": [
"https://user-images.githubusercontent.com/705880/65605563-2f009d00-df5e-11e9-9b40-1f36135d5c80.PNG", "https://user-images.githubusercontent.com/705880/65605563-2f009d00-df5e-11e9-9b40-1f36135d5c80.PNG",
"https://user-images.githubusercontent.com/705880/65605569-30ca6080-df5e-11e9-8dfb-15ebb00e10cb.PNG", "https://user-images.githubusercontent.com/705880/65605569-30ca6080-df5e-11e9-8dfb-15ebb00e10cb.PNG",
@@ -35,14 +35,15 @@
"name": "AltStore", "name": "AltStore",
"bundleIdentifier": "com.rileytestut.AltStore.Beta", "bundleIdentifier": "com.rileytestut.AltStore.Beta",
"developerName": "Riley Testut", "developerName": "Riley Testut",
"version": "1.0.1b", "subtitle": "An alternative App Store for iOS.",
"versionDate": "2019-09-28T03:00:00-07:00", "version": "1.2b4",
"versionDescription": "- Adds support for sideloading apps via \"Open In...\"\n- Fixes Patreon bugs", "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", "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.", "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", "iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
"tintColor": "018084", "tintColor": "018084",
"size": 3481256, "size": 2114670,
"beta": true, "beta": true,
"screenshotURLs": [ "screenshotURLs": [
"https://user-images.githubusercontent.com/705880/65605563-2f009d00-df5e-11e9-9b40-1f36135d5c80.PNG", "https://user-images.githubusercontent.com/705880/65605563-2f009d00-df5e-11e9-9b40-1f36135d5c80.PNG",
@@ -65,14 +66,14 @@
"bundleIdentifier": "com.rileytestut.Delta", "bundleIdentifier": "com.rileytestut.Delta",
"developerName": "Riley Testut", "developerName": "Riley Testut",
"subtitle": "Classic games in your pocket.", "subtitle": "Classic games in your pocket.",
"version": "1.0", "version": "1.1.2",
"versionDate": "2019-09-28T12:00:00-07:00", "versionDate": "2020-02-04T15:30:00-08:00",
"versionDescription": "Initial version.", "versionDescription": "• Fixes crash when running on iOS 13.3.1",
"downloadURL": "https://f000.backblazeb2.com/file/altstore/delta.ipa", "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.**", "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", "iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
"tintColor": "8A28F7", "tintColor": "8A28F7",
"size": 23075523, "size": 17542718,
"permissions": [ "permissions": [
{ {
"type": "photos", "type": "photos",
@@ -91,14 +92,14 @@
"bundleIdentifier": "com.rileytestut.Delta.Beta", "bundleIdentifier": "com.rileytestut.Delta.Beta",
"developerName": "Riley Testut", "developerName": "Riley Testut",
"subtitle": "Classic games in your pocket.", "subtitle": "Classic games in your pocket.",
"version": "1.0b", "version": "1.2b",
"versionDate": "2019-09-28T12:00:00-07:00", "versionDate": "2020-02-11T16:30:00-08:00",
"versionDescription": "Includes initial support for DS games.", "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", "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!", "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", "iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
"tintColor": "8A28F7", "tintColor": "8A28F7",
"size": 23075071, "size": 17631726,
"beta": true, "beta": true,
"permissions": [ "permissions": [
{ {
@@ -114,6 +115,31 @@
"https://user-images.githubusercontent.com/705880/65601117-58b5c600-df56-11e9-9c19-9a5ba5da54cf.PNG" "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", "name": "Delta Lite",
"bundleIdentifier": "com.rileytestut.Delta.Lite", "bundleIdentifier": "com.rileytestut.Delta.Lite",
@@ -194,12 +220,17 @@
"notify": false "notify": false
}, },
{ {
"title": "Prevent AltStore Expiring", "title": "Coming Soon: Clip",
"identifier": "altstore-expiring-bug-fix", "identifier": "clip-coming-soon",
"caption": "Refresh AltStore at least once in “My Apps” to prevent it from expiring early. An update is coming soon to fix this bug.", "caption": "A clipboard manager that can run in the background. Beta available now for all Patrons.",
"tintColor": "fd423a", "tintColor": "EC008C",
"date": "2019-10-09", "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 "notify": false
} }
] ],
"userInfo": {
"patreonAccessToken": "JLh5bpOQsPg-HIRe6FHnRAktoeeU4JT5-Xk-y7njQrM"
}
} }

View File

@@ -44,139 +44,22 @@ enum ConnectionError: LocalizedError
struct Server: Equatable struct Server: Equatable
{ {
var identifier: String var identifier: String? = nil
var service: NetService var service: NetService? = nil
var isPreferred = false 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) init?(service: NetService, txtData: Data)
{ {
let txtDictionary = NetService.dictionary(fromTXTRecord: txtData) let txtDictionary = NetService.dictionary(fromTXTRecord: txtData)
guard let identifierData = txtDictionary["serverID"], let identifier = String(data: identifierData, encoding: .utf8) else { return nil } guard let identifierData = txtDictionary["serverID"], let identifier = String(data: identifierData, encoding: .utf8) else { return nil }
self.identifier = identifier
self.service = service self.service = service
} self.identifier = identifier
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.")
}
} }
} }

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