mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
@@ -7,3 +7,6 @@
|
||||
//
|
||||
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "CFNotificationName+AltStore.h"
|
||||
|
||||
extern uint16_t ALTDeviceListeningSocket;
|
||||
|
||||
11
AltKit/AltKit.m
Normal file
11
AltKit/AltKit.m
Normal 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;
|
||||
@@ -14,9 +14,11 @@ public extension Bundle
|
||||
{
|
||||
public static let deviceID = "ALTDeviceID"
|
||||
public static let serverID = "ALTServerID"
|
||||
public static let certificateID = "ALTCertificateID"
|
||||
public static let appGroups = "ALTAppGroups"
|
||||
|
||||
public static let urlTypes = "CFBundleURLTypes"
|
||||
public static let exportedUTIs = "UTExportedTypeDeclarations"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +28,9 @@ public extension Bundle
|
||||
let infoPlistURL = self.bundleURL.appendingPathComponent("Info.plist")
|
||||
return infoPlistURL
|
||||
}
|
||||
|
||||
var certificateURL: URL {
|
||||
let infoPlistURL = self.bundleURL.appendingPathComponent("ALTCertificate.p12")
|
||||
return infoPlistURL
|
||||
}
|
||||
}
|
||||
|
||||
17
AltKit/CFNotificationName+AltStore.h
Normal file
17
AltKit/CFNotificationName+AltStore.h
Normal 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
|
||||
13
AltKit/CFNotificationName+AltStore.m
Normal file
13
AltKit/CFNotificationName+AltStore.m
Normal 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");
|
||||
@@ -27,6 +27,12 @@ typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError)
|
||||
ALTServerErrorInstallationFailed = 8,
|
||||
ALTServerErrorMaximumFreeAppLimitReached = 9,
|
||||
ALTServerErrorUnsupportediOSVersion = 10,
|
||||
|
||||
ALTServerErrorUnknownRequest = 11,
|
||||
ALTServerErrorUnknownResponse = 12,
|
||||
|
||||
ALTServerErrorInvalidAnisetteData = 13,
|
||||
ALTServerErrorPluginNotFound = 14
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -61,6 +61,18 @@ NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServ
|
||||
|
||||
case ALTServerErrorUnsupportediOSVersion:
|
||||
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
|
||||
|
||||
case ALTServerErrorUnknownRequest:
|
||||
return NSLocalizedString(@"AltServer does not support this request.", @"");
|
||||
|
||||
case ALTServerErrorUnknownResponse:
|
||||
return NSLocalizedString(@"Received an unknown response from AltServer.", @"");
|
||||
|
||||
case ALTServerErrorInvalidAnisetteData:
|
||||
return NSLocalizedString(@"Invalid anisette data.", @"");
|
||||
|
||||
case ALTServerErrorPluginNotFound:
|
||||
return NSLocalizedString(@"Could not connect to Mail plug-in. Please make sure the plug-in is installed and Mail is running, then try again.", @"");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,22 +7,201 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltSign
|
||||
|
||||
public let ALTServerServiceType = "_altserver._tcp"
|
||||
|
||||
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
|
||||
extension ALTServerError.Code: Codable {}
|
||||
|
||||
protocol ServerMessage: Codable
|
||||
protocol ServerMessageProtocol: Codable
|
||||
{
|
||||
var version: Int { get }
|
||||
var identifier: String { get }
|
||||
}
|
||||
|
||||
public struct PrepareAppRequest: ServerMessage
|
||||
public enum ServerRequest: Decodable
|
||||
{
|
||||
case anisetteData(AnisetteDataRequest)
|
||||
case prepareApp(PrepareAppRequest)
|
||||
case beginInstallation(BeginInstallationRequest)
|
||||
case unknown(identifier: String, version: Int)
|
||||
|
||||
var identifier: String {
|
||||
switch self
|
||||
{
|
||||
case .anisetteData(let request): return request.identifier
|
||||
case .prepareApp(let request): return request.identifier
|
||||
case .beginInstallation(let request): return request.identifier
|
||||
case .unknown(let identifier, _): return identifier
|
||||
}
|
||||
}
|
||||
|
||||
var version: Int {
|
||||
switch self
|
||||
{
|
||||
case .anisetteData(let request): return request.version
|
||||
case .prepareApp(let request): return request.version
|
||||
case .beginInstallation(let request): return request.version
|
||||
case .unknown(_, let version): return version
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case identifier
|
||||
case version
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let version = try container.decode(Int.self, forKey: .version)
|
||||
|
||||
let identifier = try container.decode(String.self, forKey: .identifier)
|
||||
switch identifier
|
||||
{
|
||||
case "AnisetteDataRequest":
|
||||
let request = try AnisetteDataRequest(from: decoder)
|
||||
self = .anisetteData(request)
|
||||
|
||||
case "PrepareAppRequest":
|
||||
let request = try PrepareAppRequest(from: decoder)
|
||||
self = .prepareApp(request)
|
||||
|
||||
case "BeginInstallationRequest":
|
||||
let request = try BeginInstallationRequest(from: decoder)
|
||||
self = .beginInstallation(request)
|
||||
|
||||
default:
|
||||
self = .unknown(identifier: identifier, version: version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ServerResponse: Decodable
|
||||
{
|
||||
case anisetteData(AnisetteDataResponse)
|
||||
case installationProgress(InstallationProgressResponse)
|
||||
case error(ErrorResponse)
|
||||
case unknown(identifier: String, version: Int)
|
||||
|
||||
var identifier: String {
|
||||
switch self
|
||||
{
|
||||
case .anisetteData(let response): return response.identifier
|
||||
case .installationProgress(let response): return response.identifier
|
||||
case .error(let response): return response.identifier
|
||||
case .unknown(let identifier, _): return identifier
|
||||
}
|
||||
}
|
||||
|
||||
var version: Int {
|
||||
switch self
|
||||
{
|
||||
case .anisetteData(let response): return response.version
|
||||
case .installationProgress(let response): return response.version
|
||||
case .error(let response): return response.version
|
||||
case .unknown(_, let version): return version
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case identifier
|
||||
case version
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let version = try container.decode(Int.self, forKey: .version)
|
||||
|
||||
let identifier = try container.decode(String.self, forKey: .identifier)
|
||||
switch identifier
|
||||
{
|
||||
case "AnisetteDataResponse":
|
||||
let response = try AnisetteDataResponse(from: decoder)
|
||||
self = .anisetteData(response)
|
||||
|
||||
case "InstallationProgressResponse":
|
||||
let response = try InstallationProgressResponse(from: decoder)
|
||||
self = .installationProgress(response)
|
||||
|
||||
case "ErrorResponse":
|
||||
let response = try ErrorResponse(from: decoder)
|
||||
self = .error(response)
|
||||
|
||||
default:
|
||||
self = .unknown(identifier: identifier, version: version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct AnisetteDataRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "PrepareApp"
|
||||
public var identifier = "AnisetteDataRequest"
|
||||
|
||||
public init()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public struct AnisetteDataResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "AnisetteDataResponse"
|
||||
|
||||
public var anisetteData: ALTAnisetteData
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case identifier
|
||||
case version
|
||||
case anisetteData
|
||||
}
|
||||
|
||||
public init(anisetteData: ALTAnisetteData)
|
||||
{
|
||||
self.anisetteData = anisetteData
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.version = try container.decode(Int.self, forKey: .version)
|
||||
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||
|
||||
let json = try container.decode([String: String].self, forKey: .anisetteData)
|
||||
|
||||
if let anisetteData = ALTAnisetteData(json: json)
|
||||
{
|
||||
self.anisetteData = anisetteData
|
||||
}
|
||||
else
|
||||
{
|
||||
throw DecodingError.dataCorruptedError(forKey: CodingKeys.anisetteData, in: container, debugDescription: "Couuld not parse anisette data from JSON")
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws
|
||||
{
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.version, forKey: .version)
|
||||
try container.encode(self.identifier, forKey: .identifier)
|
||||
|
||||
let json = self.anisetteData.json()
|
||||
try container.encode(json, forKey: .anisetteData)
|
||||
}
|
||||
}
|
||||
|
||||
public struct PrepareAppRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "PrepareAppRequest"
|
||||
|
||||
public var udid: String
|
||||
public var contentSize: Int
|
||||
@@ -34,37 +213,41 @@ public struct PrepareAppRequest: ServerMessage
|
||||
}
|
||||
}
|
||||
|
||||
public struct BeginInstallationRequest: ServerMessage
|
||||
public struct BeginInstallationRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "BeginInstallation"
|
||||
public var identifier = "BeginInstallationRequest"
|
||||
|
||||
public init()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public struct ServerResponse: ServerMessage
|
||||
public struct ErrorResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "ServerResponse"
|
||||
public var identifier = "ErrorResponse"
|
||||
|
||||
public var error: ALTServerError {
|
||||
return ALTServerError(self.errorCode)
|
||||
}
|
||||
private var errorCode: ALTServerError.Code
|
||||
|
||||
public init(error: ALTServerError)
|
||||
{
|
||||
self.errorCode = error.code
|
||||
}
|
||||
}
|
||||
|
||||
public struct InstallationProgressResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "InstallationProgressResponse"
|
||||
|
||||
public var progress: Double
|
||||
|
||||
public var error: ALTServerError? {
|
||||
get {
|
||||
guard let code = self.errorCode else { return nil }
|
||||
return ALTServerError(code)
|
||||
}
|
||||
set {
|
||||
self.errorCode = newValue?.code
|
||||
}
|
||||
}
|
||||
private var errorCode: ALTServerError.Code?
|
||||
|
||||
public init(progress: Double, error: ALTServerError?)
|
||||
public init(progress: Double)
|
||||
{
|
||||
self.progress = progress
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
|
||||
19
AltPlugin/ALTPluginService.h
Normal file
19
AltPlugin/ALTPluginService.h
Normal 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
|
||||
99
AltPlugin/ALTPluginService.m
Normal file
99
AltPlugin/ALTPluginService.m
Normal 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
62
AltPlugin/Info.plist
Normal 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>
|
||||
BIN
AltServer/AltPlugin.mailbundle.zip
Normal file
BIN
AltServer/AltPlugin.mailbundle.zip
Normal file
Binary file not shown.
@@ -3,3 +3,6 @@
|
||||
//
|
||||
|
||||
#import "ALTDeviceManager.h"
|
||||
#import "ALTWiredConnection.h"
|
||||
#import "ALTNotificationConnection.h"
|
||||
#import "AltKit.h"
|
||||
|
||||
79
AltServer/AnisetteDataManager.swift
Normal file
79
AltServer/AnisetteDataManager.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,25 @@ import UserNotifications
|
||||
import AltSign
|
||||
|
||||
import LaunchAtLogin
|
||||
import STPrivilegedTask
|
||||
|
||||
enum PluginError: LocalizedError
|
||||
{
|
||||
case installationScriptNotFound
|
||||
case failedToRun(Int)
|
||||
case scriptError(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .installationScriptNotFound: return NSLocalizedString("The installation script could not be found.", comment: "")
|
||||
case .failedToRun(let errorCode): return String(format: NSLocalizedString("The installation script could not be run. (%@)", comment: ""), NSNumber(value: errorCode))
|
||||
case .scriptError(let output): return output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let pluginURL = URL(fileURLWithPath: "/Library/Mail/Bundles/AltPlugin.mailbundle")
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@@ -25,16 +44,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@IBOutlet private var appMenu: NSMenu!
|
||||
@IBOutlet private var connectedDevicesMenu: NSMenu!
|
||||
@IBOutlet private var launchAtLoginMenuItem: NSMenuItem!
|
||||
@IBOutlet private var installMailPluginMenuItem: NSMenuItem!
|
||||
|
||||
private weak var authenticationAppleIDTextField: NSTextField?
|
||||
private weak var authenticationPasswordTextField: NSSecureTextField?
|
||||
|
||||
|
||||
private var isMailPluginInstalled: Bool {
|
||||
let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
return isMailPluginInstalled
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification)
|
||||
{
|
||||
UserDefaults.standard.registerDefaults()
|
||||
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
ConnectionManager.shared.start()
|
||||
ALTDeviceManager.shared.start()
|
||||
|
||||
let item = NSStatusBar.system.statusItem(withLength: -1)
|
||||
guard let button = item.button else { return }
|
||||
@@ -47,16 +74,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
self.connectedDevicesMenu.delegate = self
|
||||
|
||||
if !UserDefaults.standard.didPresentInitialNotification
|
||||
{
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("AltServer Running", comment: "")
|
||||
content.body = NSLocalizedString("AltServer runs in the background as a menu bar app listening for AltStore.", comment: "")
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (success, error) in
|
||||
guard success else { return }
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
UserDefaults.standard.didPresentInitialNotification = true
|
||||
if !UserDefaults.standard.didPresentInitialNotification
|
||||
{
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("AltServer Running", comment: "")
|
||||
content.body = NSLocalizedString("AltServer runs in the background as a menu bar app listening for AltStore.", comment: "")
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
UserDefaults.standard.didPresentInitialNotification = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +103,23 @@ private extension AppDelegate
|
||||
{
|
||||
guard let button = self.statusItem?.button, let superview = button.superview, let window = button.window else { return }
|
||||
|
||||
self.connectedDevices = ALTDeviceManager.shared.connectedDevices
|
||||
self.connectedDevices = ALTDeviceManager.shared.availableDevices
|
||||
|
||||
self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off
|
||||
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:))
|
||||
|
||||
if FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
{
|
||||
self.installMailPluginMenuItem.title = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
self.installMailPluginMenuItem.title = NSLocalizedString("Install Mail Plug-in", comment: "")
|
||||
}
|
||||
|
||||
self.installMailPluginMenuItem.target = self
|
||||
self.installMailPluginMenuItem.action = #selector(AppDelegate.handleInstallMailPluginMenuItem(_:))
|
||||
|
||||
let x = button.frame.origin.x
|
||||
let y = button.frame.origin.y - 5
|
||||
|
||||
@@ -96,11 +139,7 @@ private extension AppDelegate
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "")
|
||||
alert.informativeText = NSLocalizedString("""
|
||||
Your Apple ID and password are not saved and are only sent to Apple for authentication.
|
||||
|
||||
If you have two-factor authentication enabled, please create an app-specific password for use with AltStore at https://appleid.apple.com.
|
||||
""", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Your Apple ID and password are not saved and are only sent to Apple for authentication.", comment: "")
|
||||
|
||||
let textFieldSize = NSSize(width: 300, height: 22)
|
||||
|
||||
@@ -142,6 +181,13 @@ If you have two-factor authentication enabled, please create an app-specific pas
|
||||
let password = passwordTextField.stringValue
|
||||
|
||||
let device = self.connectedDevices[index]
|
||||
|
||||
if !self.isMailPluginInstalled
|
||||
{
|
||||
let result = self.installMailPlugin()
|
||||
guard result else { return }
|
||||
}
|
||||
|
||||
ALTDeviceManager.shared.installAltStore(to: device, appleID: username, password: password) { (result) in
|
||||
switch result
|
||||
{
|
||||
@@ -153,7 +199,7 @@ If you have two-factor authentication enabled, please create an app-specific pas
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
case .failure(InstallError.cancelled):
|
||||
case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
||||
// Ignore
|
||||
break
|
||||
|
||||
@@ -192,6 +238,74 @@ If you have two-factor authentication enabled, please create an app-specific pas
|
||||
|
||||
LaunchAtLogin.isEnabled.toggle()
|
||||
}
|
||||
|
||||
@objc func handleInstallMailPluginMenuItem(_ item: NSMenuItem)
|
||||
{
|
||||
installMailPlugin()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func installMailPlugin() -> Bool
|
||||
{
|
||||
do
|
||||
{
|
||||
let previouslyInstalled = self.isMailPluginInstalled
|
||||
|
||||
if !previouslyInstalled
|
||||
{
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "")
|
||||
alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
guard response == .alertFirstButtonReturn else { return false }
|
||||
}
|
||||
|
||||
guard let scriptURL = Bundle.main.url(forResource: self.isMailPluginInstalled ? "UninstallPlugin" : "InstallPlugin", withExtension: "sh") else { throw PluginError.installationScriptNotFound }
|
||||
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o777], ofItemAtPath: scriptURL.path)
|
||||
|
||||
let task = STPrivilegedTask()
|
||||
task.setLaunchPath(scriptURL.path)
|
||||
task.setCurrentDirectoryPath(scriptURL.deletingLastPathComponent().path)
|
||||
|
||||
let errorCode = task.launch()
|
||||
guard errorCode == 0 else { throw PluginError.failedToRun(Int(errorCode)) }
|
||||
|
||||
task.waitUntilExit()
|
||||
|
||||
if
|
||||
let outputData = task.outputFileHandle()?.readDataToEndOfFile(),
|
||||
let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
|
||||
{
|
||||
throw PluginError.scriptError(outputString)
|
||||
}
|
||||
|
||||
if !previouslyInstalled && self.isMailPluginInstalled
|
||||
{
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
|
||||
alert.runModal()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
catch
|
||||
{
|
||||
let alert = NSAlert()
|
||||
alert.messageText = self.isMailPluginInstalled ? NSLocalizedString("Failed to Uninstall Mail Plug-in", comment: "") : NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.runModal()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: NSMenuDelegate
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15504"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -10,11 +11,11 @@
|
||||
<objects>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="4" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" id="urc-xw-Dhc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="48"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="46"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zLd-d8-ghZ">
|
||||
<rect key="frame" x="0.0" y="26" width="300" height="22"/>
|
||||
<rect key="frame" x="0.0" y="25" width="300" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Apple ID" drawsBackground="YES" id="BXa-Re-rs3">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -26,7 +27,7 @@
|
||||
</connections>
|
||||
</textField>
|
||||
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9rp-Vx-rvB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="22"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="21"/>
|
||||
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Password" drawsBackground="YES" usesSingleLineMode="YES" id="xqJ-wt-DlP">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -61,9 +62,11 @@
|
||||
<outlet property="authenticationAppleIDTextField" destination="zLd-d8-ghZ" id="wW5-0J-zdq"/>
|
||||
<outlet property="authenticationPasswordTextField" destination="9rp-Vx-rvB" id="ZoC-DI-jzQ"/>
|
||||
<outlet property="connectedDevicesMenu" destination="KJ9-WY-pW1" id="Mcv-64-iFU"/>
|
||||
<outlet property="installMailPluginMenuItem" destination="3CM-gV-X2G" id="lio-ha-z0S"/>
|
||||
<outlet property="launchAtLoginMenuItem" destination="IyR-FQ-upe" id="Fxn-EP-hwH"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="Arf-IC-5eb" customClass="SUUpdater"/>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
@@ -98,7 +101,17 @@
|
||||
<menuItem title="Launch at Login" id="IyR-FQ-upe" userLabel="Launch At Login">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Install Mail Plug-in" id="3CM-gV-X2G" userLabel="Mail Plug-in">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="mVM-Nm-Zi9"/>
|
||||
<menuItem title="Check for Updates..." id="Tnq-gD-Eic" userLabel="Check for Updates">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="checkForUpdates:" target="Arf-IC-5eb" id="7JG-du-nr4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="hmG-xg-qgm"/>
|
||||
<menuItem title="Quit AltServer" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
|
||||
24
AltServer/Connections/ALTNotificationConnection+Private.h
Normal file
24
AltServer/Connections/ALTNotificationConnection+Private.h
Normal 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
|
||||
30
AltServer/Connections/ALTNotificationConnection.h
Normal file
30
AltServer/Connections/ALTNotificationConnection.h
Normal 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
|
||||
93
AltServer/Connections/ALTNotificationConnection.m
Normal file
93
AltServer/Connections/ALTNotificationConnection.m
Normal 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));
|
||||
}
|
||||
}
|
||||
23
AltServer/Connections/ALTWiredConnection+Private.h
Normal file
23
AltServer/Connections/ALTWiredConnection+Private.h
Normal 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
|
||||
25
AltServer/Connections/ALTWiredConnection.h
Normal file
25
AltServer/Connections/ALTWiredConnection.h
Normal 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
|
||||
101
AltServer/Connections/ALTWiredConnection.m
Normal file
101
AltServer/Connections/ALTWiredConnection.m
Normal 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
|
||||
231
AltServer/Connections/ClientConnection.swift
Normal file
231
AltServer/Connections/ClientConnection.swift
Normal 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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import AppKit
|
||||
|
||||
import AltKit
|
||||
|
||||
@@ -53,10 +54,13 @@ class ConnectionManager
|
||||
private lazy var listener = self.makeListener()
|
||||
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltServer.connections", qos: .utility)
|
||||
|
||||
private var connections = [NWConnection]()
|
||||
private var connections = [ClientConnection]()
|
||||
private var notificationConnections = [ALTDevice: NotificationConnection]()
|
||||
|
||||
private init()
|
||||
{
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidConnect(_:)), name: .deviceManagerDeviceDidConnect, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidDisconnect(_:)), name: .deviceManagerDeviceDidDisconnect, object: nil)
|
||||
}
|
||||
|
||||
func start()
|
||||
@@ -76,6 +80,16 @@ class ConnectionManager
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect(_ connection: ClientConnection)
|
||||
{
|
||||
connection.disconnect()
|
||||
|
||||
if let index = self.connections.firstIndex(where: { $0 === connection })
|
||||
{
|
||||
self.connections.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConnectionManager
|
||||
@@ -126,68 +140,18 @@ private extension ConnectionManager
|
||||
}
|
||||
|
||||
listener.newConnectionHandler = { [weak self] (connection) in
|
||||
self?.awaitRequest(from: connection)
|
||||
self?.prepare(connection)
|
||||
}
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
func disconnect(_ connection: NWConnection)
|
||||
func prepare(_ connection: NWConnection)
|
||||
{
|
||||
switch connection.state
|
||||
{
|
||||
case .cancelled, .failed:
|
||||
print("Disconnecting from \(connection.endpoint)...")
|
||||
|
||||
if let index = self.connections.firstIndex(where: { $0 === connection })
|
||||
{
|
||||
self.connections.remove(at: index)
|
||||
}
|
||||
|
||||
default:
|
||||
// State update handler will call this method again.
|
||||
connection.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
|
||||
{
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let data = data else { throw error ?? ALTServerError(.unknown) }
|
||||
return data
|
||||
}
|
||||
catch let error as NWError
|
||||
{
|
||||
print("Error receiving data from connection \(connection)", error)
|
||||
|
||||
throw ALTServerError(.lostConnection)
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw error
|
||||
}
|
||||
}
|
||||
catch let error as ALTServerError
|
||||
{
|
||||
throw error
|
||||
}
|
||||
catch
|
||||
{
|
||||
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConnectionManager
|
||||
{
|
||||
func awaitRequest(from connection: NWConnection)
|
||||
{
|
||||
guard !self.connections.contains(where: { $0 === connection }) else { return }
|
||||
self.connections.append(connection)
|
||||
let clientConnection = ClientConnection(connection: .wireless(connection))
|
||||
|
||||
guard !self.connections.contains(where: { $0 === clientConnection }) else { return }
|
||||
self.connections.append(clientConnection)
|
||||
|
||||
connection.stateUpdateHandler = { [weak self] (state) in
|
||||
switch state
|
||||
@@ -196,20 +160,17 @@ private extension ConnectionManager
|
||||
|
||||
case .ready:
|
||||
print("Connected to client:", connection.endpoint)
|
||||
|
||||
self?.receiveApp(from: connection) { (result) in
|
||||
self?.finish(connection: connection, error: result.error)
|
||||
}
|
||||
self?.handleRequest(for: clientConnection)
|
||||
|
||||
case .waiting:
|
||||
print("Waiting for connection...")
|
||||
|
||||
case .failed(let error):
|
||||
print("Failed to connect to service \(connection.endpoint).", error)
|
||||
self?.disconnect(connection)
|
||||
self?.disconnect(clientConnection)
|
||||
|
||||
case .cancelled:
|
||||
self?.disconnect(connection)
|
||||
self?.disconnect(clientConnection)
|
||||
|
||||
@unknown default: break
|
||||
}
|
||||
@@ -217,8 +178,124 @@ private extension ConnectionManager
|
||||
|
||||
connection.start(queue: self.dispatchQueue)
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConnectionManager
|
||||
{
|
||||
func startNotificationConnection(to device: ALTDevice)
|
||||
{
|
||||
ALTDeviceManager.shared.startNotificationConnection(to: device) { (connection, error) in
|
||||
guard let connection = connection else { return }
|
||||
|
||||
let notifications: [CFNotificationName] = [.wiredServerConnectionAvailableRequest, .wiredServerConnectionStartRequest]
|
||||
connection.startListening(forNotifications: notifications.map { String($0.rawValue) }) { (success, error) in
|
||||
guard success else { return }
|
||||
|
||||
connection.receivedNotificationHandler = { [weak self, weak connection] (notification) in
|
||||
guard let self = self, let connection = connection else { return }
|
||||
self.handle(notification, for: connection)
|
||||
}
|
||||
|
||||
self.notificationConnections[device] = connection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receiveApp(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
func stopNotificationConnection(to device: ALTDevice)
|
||||
{
|
||||
guard let connection = self.notificationConnections[device] else { return }
|
||||
connection.disconnect()
|
||||
|
||||
self.notificationConnections[device] = nil
|
||||
}
|
||||
|
||||
func handle(_ notification: CFNotificationName, for connection: NotificationConnection)
|
||||
{
|
||||
switch notification
|
||||
{
|
||||
case .wiredServerConnectionAvailableRequest:
|
||||
connection.sendNotification(.wiredServerConnectionAvailableResponse) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Error sending wired server connection response.", error)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Sent wired server connection available response!")
|
||||
}
|
||||
}
|
||||
|
||||
case .wiredServerConnectionStartRequest:
|
||||
ALTDeviceManager.shared.startWiredConnection(to: connection.device) { (wiredConnection, error) in
|
||||
if let wiredConnection = wiredConnection
|
||||
{
|
||||
print("Started wired server connection!")
|
||||
|
||||
let clientConnection = ClientConnection(connection: .wired(wiredConnection))
|
||||
self.handleRequest(for: clientConnection)
|
||||
}
|
||||
else if let error = error
|
||||
{
|
||||
print("Error starting wired server connection.", error)
|
||||
}
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConnectionManager
|
||||
{
|
||||
func handleRequest(for connection: ClientConnection)
|
||||
{
|
||||
connection.receiveRequest() { (result) in
|
||||
print("Received initial request with result:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
let response = ErrorResponse(error: ALTServerError(error))
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent error response with result:", result)
|
||||
}
|
||||
|
||||
case .success(.anisetteData(let request)):
|
||||
self.handleAnisetteDataRequest(request, for: connection)
|
||||
|
||||
case .success(.prepareApp(let request)):
|
||||
self.handlePrepareAppRequest(request, for: connection)
|
||||
|
||||
case .success:
|
||||
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent unknown request response with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: ClientConnection)
|
||||
{
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
let errorResponse = ErrorResponse(error: ALTServerError(error))
|
||||
connection.send(errorResponse, shouldDisconnect: true) { (result) in
|
||||
print("Sent anisette data error response with result:", result)
|
||||
}
|
||||
|
||||
case .success(let anisetteData):
|
||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent anisette data response with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: ClientConnection)
|
||||
{
|
||||
var temporaryURL: URL?
|
||||
|
||||
@@ -230,83 +307,75 @@ private extension ConnectionManager
|
||||
catch { print("Failed to remove .ipa.", error) }
|
||||
}
|
||||
|
||||
completionHandler(result)
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to process request from \(connection).", error)
|
||||
|
||||
let response = ErrorResponse(error: ALTServerError(error))
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent install app error response to \(connection) with result:", result)
|
||||
}
|
||||
|
||||
case .success:
|
||||
print("Processed request from \(connection).")
|
||||
|
||||
let response = InstallationProgressResponse(progress: 1.0)
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent install app response to \(connection) with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.receive(PrepareAppRequest.self, from: connection) { (result) in
|
||||
print("Received request with result:", result)
|
||||
self.receiveApp(for: request, from: connection) { (result) in
|
||||
print("Received app with result:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let request):
|
||||
self.receiveApp(for: request, from: connection) { (result) in
|
||||
print("Received app with result:", result)
|
||||
case .success(let fileURL):
|
||||
temporaryURL = fileURL
|
||||
|
||||
print("Awaiting begin installation request...")
|
||||
|
||||
connection.receiveRequest() { (result) in
|
||||
print("Received begin installation request with result:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let request, let fileURL):
|
||||
temporaryURL = fileURL
|
||||
case .success(.beginInstallation):
|
||||
print("Installing to device \(request.udid)...")
|
||||
|
||||
print("Awaiting begin installation request...")
|
||||
|
||||
self.receive(BeginInstallationRequest.self, from: connection) { (result) in
|
||||
print("Received begin installation request with result:", result)
|
||||
|
||||
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in
|
||||
print("Installed to device with result:", result)
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success:
|
||||
print("Installing to device \(request.udid)...")
|
||||
|
||||
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in
|
||||
print("Installed to device with result:", result)
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success: finish(.success(()))
|
||||
}
|
||||
}
|
||||
case .success: finish(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
case .success:
|
||||
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent unknown request error response to \(connection) with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func finish(connection: NWConnection, error: ALTServerError?)
|
||||
func receiveApp(for request: PrepareAppRequest, from connection: ClientConnection, completionHandler: @escaping (Result<URL, ALTServerError>) -> Void)
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
print("Failed to process request from \(connection.endpoint).", error)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Processed request from \(connection.endpoint).")
|
||||
}
|
||||
|
||||
let response = ServerResponse(progress: 1.0, error: error)
|
||||
|
||||
self.send(response, to: connection) { (result) in
|
||||
print("Sent response to \(connection.endpoint) with result:", result)
|
||||
|
||||
self.disconnect(connection)
|
||||
}
|
||||
}
|
||||
|
||||
func receiveApp(for request: PrepareAppRequest, from connection: NWConnection, completionHandler: @escaping (Result<(PrepareAppRequest, URL), ALTServerError>) -> Void)
|
||||
{
|
||||
connection.receive(minimumIncompleteLength: request.contentSize, maximumLength: request.contentSize) { (data, _, _, error) in
|
||||
connection.receiveData(expectedBytes: request.contentSize) { (result) in
|
||||
do
|
||||
{
|
||||
print("Received app data!")
|
||||
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
|
||||
print("Processed app data!")
|
||||
|
||||
let data = try result.get()
|
||||
|
||||
guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
|
||||
|
||||
print("Writing app data...")
|
||||
@@ -316,7 +385,7 @@ private extension ConnectionManager
|
||||
|
||||
print("Wrote app to URL:", temporaryURL)
|
||||
|
||||
completionHandler(.success((request, temporaryURL)))
|
||||
completionHandler(.success(temporaryURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -327,7 +396,7 @@ private extension ConnectionManager
|
||||
}
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, connection: ClientConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
{
|
||||
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
|
||||
var isSending = false
|
||||
@@ -356,9 +425,9 @@ private extension ConnectionManager
|
||||
isSending = true
|
||||
|
||||
print("Progress:", progress.fractionCompleted)
|
||||
let response = ServerResponse(progress: progress.fractionCompleted, error: nil)
|
||||
let response = InstallationProgressResponse(progress: progress.fractionCompleted)
|
||||
|
||||
self.send(response, to: connection) { (result) in
|
||||
connection.send(response) { (result) in
|
||||
serialQueue.async {
|
||||
isSending = false
|
||||
}
|
||||
@@ -366,79 +435,19 @@ private extension ConnectionManager
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func send<T: Encodable>(_ response: T, to connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
private extension ConnectionManager
|
||||
{
|
||||
@objc func deviceDidConnect(_ notification: Notification)
|
||||
{
|
||||
do
|
||||
{
|
||||
let data = try JSONEncoder().encode(response)
|
||||
let responseSize = withUnsafeBytes(of: Int32(data.count)) { Data($0) }
|
||||
|
||||
connection.send(content: responseSize, completion: .contentProcessed { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
connection.send(content: data, completion: .contentProcessed { (error) in
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(.init(.lostConnection)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
})
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(.init(.lostConnection)))
|
||||
}
|
||||
})
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(.init(.invalidResponse)))
|
||||
}
|
||||
guard let device = notification.object as? ALTDevice else { return }
|
||||
self.startNotificationConnection(to: device)
|
||||
}
|
||||
|
||||
func receive<T: Decodable>(_ responseType: T.Type, from connection: NWConnection, completionHandler: @escaping (Result<T, ALTServerError>) -> Void)
|
||||
@objc func deviceDidDisconnect(_ notification: Notification)
|
||||
{
|
||||
let size = MemoryLayout<Int32>.size
|
||||
|
||||
print("Receiving request size")
|
||||
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
|
||||
print("Receiving request...")
|
||||
|
||||
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
|
||||
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
|
||||
let request = try JSONDecoder().decode(T.self, from: data)
|
||||
|
||||
print("Received installation request:", request)
|
||||
|
||||
completionHandler(.success(request))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
guard let device = notification.object as? ALTDevice else { return }
|
||||
self.stopNotificationConnection(to: device)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
|
||||
import Cocoa
|
||||
import UserNotifications
|
||||
import ObjectiveC
|
||||
|
||||
#if STAGING
|
||||
private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altstore.ipa")!
|
||||
#else
|
||||
private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")!
|
||||
#endif
|
||||
|
||||
enum InstallError: LocalizedError
|
||||
{
|
||||
@@ -49,126 +56,147 @@ extension ALTDeviceManager
|
||||
try? FileManager.default.removeItem(at: destinationDirectoryURL)
|
||||
}
|
||||
|
||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
do
|
||||
{
|
||||
let account = try result.get()
|
||||
let anisetteData = try result.get()
|
||||
|
||||
self.fetchTeam(for: account) { (result) in
|
||||
self.authenticate(appleID: appleID, password: password, anisetteData: anisetteData) { (result) in
|
||||
do
|
||||
{
|
||||
let team = try result.get()
|
||||
let (account, session) = try result.get()
|
||||
|
||||
self.register(device, team: team) { (result) in
|
||||
self.fetchTeam(for: account, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let device = try result.get()
|
||||
let team = try result.get()
|
||||
|
||||
self.fetchCertificate(for: team) { (result) in
|
||||
self.register(device, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let certificate = try result.get()
|
||||
let device = try result.get()
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = String(format: NSLocalizedString("Installing AltStore to %@...", comment: ""), device.name)
|
||||
content.body = NSLocalizedString("This may take a few seconds.", comment: "")
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
self.downloadApp { (result) in
|
||||
self.fetchCertificate(for: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let fileURL = try result.get()
|
||||
let certificate = try result.get()
|
||||
|
||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = String(format: NSLocalizedString("Installing AltStore to %@...", comment: ""), device.name)
|
||||
content.body = NSLocalizedString("This may take a few seconds.", comment: "")
|
||||
|
||||
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove downloaded .ipa.", error)
|
||||
}
|
||||
|
||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
|
||||
|
||||
self.registerAppID(name: "AltStore", identifier: "com.rileytestut.AltStore", team: team) { (result) in
|
||||
self.downloadApp { (result) in
|
||||
do
|
||||
{
|
||||
let appID = try result.get()
|
||||
let fileURL = try result.get()
|
||||
|
||||
self.updateFeatures(for: appID, app: application, team: team) { (result) in
|
||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove downloaded .ipa.", error)
|
||||
}
|
||||
|
||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
|
||||
|
||||
// Refresh anisette data to prevent session timeouts.
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
do
|
||||
{
|
||||
let appID = try result.get()
|
||||
let anisetteData = try result.get()
|
||||
session.anisetteData = anisetteData
|
||||
|
||||
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
|
||||
self.registerAppID(name: "AltStore", identifier: "com.rileytestut.AltStore", team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let provisioningProfile = try result.get()
|
||||
let appID = try result.get()
|
||||
|
||||
self.install(application, to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile) { (result) in
|
||||
finish(result.error, title: "Failed to Install AltStore")
|
||||
self.updateFeatures(for: appID, app: application, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let appID = try result.get()
|
||||
|
||||
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let provisioningProfile = try result.get()
|
||||
|
||||
self.install(application, to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile) { (result) in
|
||||
finish(result.error, title: "Failed to Install AltStore")
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Fetch Provisioning Profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Update App ID")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Fetch Provisioning Profile")
|
||||
finish(error, title: "Failed to Register App")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Update App ID")
|
||||
finish(error, title: "Failed to Refresh Anisette Data")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Register App")
|
||||
finish(error, title: "Failed to Download AltStore")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Download AltStore")
|
||||
return
|
||||
finish(error, title: "Failed to Fetch Certificate")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Fetch Certificate")
|
||||
finish(error, title: "Failed to Register Device")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Register Device")
|
||||
finish(error, title: "Failed to Fetch Team")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Fetch Team")
|
||||
finish(error, title: "Failed to Authenticate")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Authenticate")
|
||||
finish(error, title: "Failed to Fetch Anisette Data")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadApp(completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")!
|
||||
|
||||
let downloadTask = URLSession.shared.downloadTask(with: appURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
@@ -184,15 +212,57 @@ extension ALTDeviceManager
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<ALTAccount, Error>) -> Void)
|
||||
func authenticate(appleID: String, password: String, anisetteData: ALTAnisetteData, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in
|
||||
let result = Result(account, error)
|
||||
completionHandler(result)
|
||||
func handleVerificationCode(_ completionHandler: @escaping (String?) -> Void)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Two-Factor Authentication Enabled", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: "")
|
||||
|
||||
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 22))
|
||||
textField.delegate = self
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
textField.placeholderString = NSLocalizedString("123456", comment: "")
|
||||
alert.accessoryView = textField
|
||||
alert.window.initialFirstResponder = textField
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
self.securityCodeAlert = alert
|
||||
self.securityCodeTextField = textField
|
||||
self.validate()
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
if response == .alertFirstButtonReturn
|
||||
{
|
||||
let code = textField.stringValue
|
||||
completionHandler(code)
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData, verificationHandler: handleVerificationCode) { (account, session, error) in
|
||||
if let account = account, let session = session
|
||||
{
|
||||
completionHandler(.success((account, session)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(error ?? ALTAppleAPIError(.unknown)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
|
||||
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
|
||||
{
|
||||
func finish(_ result: Result<ALTTeam, Error>)
|
||||
{
|
||||
@@ -238,7 +308,7 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
|
||||
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
|
||||
do
|
||||
{
|
||||
let teams = try Result(teams, error).get()
|
||||
@@ -267,9 +337,9 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
}
|
||||
}
|
||||
|
||||
func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
|
||||
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
@@ -304,11 +374,11 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
|
||||
if let certificate = certificates.first
|
||||
{
|
||||
ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in
|
||||
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
||||
do
|
||||
{
|
||||
try Result(success, error).get()
|
||||
self.fetchCertificate(for: team, completionHandler: completionHandler)
|
||||
self.fetchCertificate(for: team, session: session, completionHandler: completionHandler)
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -318,13 +388,13 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.addCertificate(machineName: "AltStore", to: team) { (certificate, error) in
|
||||
ALTAppleAPI.shared.addCertificate(machineName: "AltStore", to: team, session: session) { (certificate, error) in
|
||||
do
|
||||
{
|
||||
let certificate = try Result(certificate, error).get()
|
||||
guard let privateKey = certificate.privateKey else { throw InstallError.missingPrivateKey }
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
@@ -357,11 +427,11 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
}
|
||||
}
|
||||
|
||||
func registerAppID(name appName: String, identifier: String, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
func registerAppID(name appName: String, identifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let bundleID = "com.\(team.identifier).\(identifier)"
|
||||
|
||||
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
|
||||
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
||||
do
|
||||
{
|
||||
let appIDs = try Result(appIDs, error).get()
|
||||
@@ -372,7 +442,7 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
|
||||
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
@@ -384,7 +454,7 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
||||
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
|
||||
@@ -401,14 +471,14 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
let appID = appID.copy() as! ALTAppID
|
||||
appID.features = features
|
||||
|
||||
ALTAppleAPI.shared.update(appID, team: team) { (appID, error) in
|
||||
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
|
||||
func register(_ device: ALTDevice, team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
func register(_ device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
|
||||
ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
|
||||
do
|
||||
{
|
||||
let devices = try Result(devices, error).get()
|
||||
@@ -419,7 +489,7 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, team: team) { (device, error) in
|
||||
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, team: team, session: session) { (device, error) in
|
||||
completionHandler(Result(device, error))
|
||||
}
|
||||
}
|
||||
@@ -431,9 +501,9 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
}
|
||||
@@ -449,7 +519,16 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
||||
infoDictionary[Bundle.Info.deviceID] = device.identifier
|
||||
infoDictionary[Bundle.Info.serverID] = UserDefaults.standard.serverID
|
||||
infoDictionary[Bundle.Info.certificateID] = certificate.serialNumber
|
||||
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
|
||||
|
||||
if
|
||||
let machineIdentifier = certificate.machineIdentifier,
|
||||
let encryptedData = certificate.encryptedP12Data(withPassword: machineIdentifier)
|
||||
{
|
||||
let certificateURL = application.fileURL.appendingPathComponent("ALTCertificate.p12")
|
||||
try encryptedData.write(to: certificateURL, options: .atomic)
|
||||
}
|
||||
|
||||
let resigner = ALTSigner(team: team, certificate: certificate)
|
||||
resigner.signApp(at: application.fileURL, provisioningProfiles: [profile]) { (success, error) in
|
||||
@@ -476,3 +555,45 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var securityCodeAlertKey = 0
|
||||
private var securityCodeTextFieldKey = 0
|
||||
|
||||
extension ALTDeviceManager: NSTextFieldDelegate
|
||||
{
|
||||
var securityCodeAlert: NSAlert? {
|
||||
get { return objc_getAssociatedObject(self, &securityCodeAlertKey) as? NSAlert }
|
||||
set { objc_setAssociatedObject(self, &securityCodeAlertKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
||||
}
|
||||
|
||||
var securityCodeTextField: NSTextField? {
|
||||
get { return objc_getAssociatedObject(self, &securityCodeTextFieldKey) as? NSTextField }
|
||||
set { objc_setAssociatedObject(self, &securityCodeTextFieldKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
||||
}
|
||||
|
||||
public func controlTextDidChange(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
public func controlTextDidEndEditing(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
private func validate()
|
||||
{
|
||||
guard let code = self.securityCodeTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) else { return }
|
||||
|
||||
if code.count == 6
|
||||
{
|
||||
self.securityCodeAlert?.buttons.first?.isEnabled = true
|
||||
}
|
||||
else
|
||||
{
|
||||
self.securityCodeAlert?.buttons.first?.isEnabled = false
|
||||
}
|
||||
|
||||
self.securityCodeAlert?.layout()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,14 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AltSign/AltSign.h>
|
||||
|
||||
@class ALTWiredConnection;
|
||||
@class ALTNotificationConnection;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidConnect);
|
||||
extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidDisconnect);
|
||||
|
||||
@interface ALTDeviceManager : NSObject
|
||||
|
||||
@property (class, nonatomic, readonly) ALTDeviceManager *sharedManager;
|
||||
@@ -18,8 +24,15 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *connectedDevices;
|
||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *availableDevices;
|
||||
|
||||
- (void)start;
|
||||
|
||||
/* App Installation */
|
||||
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
/* Connections */
|
||||
- (void)startWiredConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTWiredConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
|
||||
- (void)startNotificationConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTNotificationConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
//
|
||||
|
||||
#import "ALTDeviceManager.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
#import "AltKit.h"
|
||||
#import "ALTWiredConnection+Private.h"
|
||||
#import "ALTNotificationConnection+Private.h"
|
||||
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
#include <libimobiledevice/lockdown.h>
|
||||
@@ -17,8 +20,10 @@
|
||||
#include <libimobiledevice/misagent.h>
|
||||
|
||||
void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *udid);
|
||||
void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data);
|
||||
|
||||
NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification = @"ALTDeviceManagerDeviceDidConnectNotification";
|
||||
NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALTDeviceManagerDeviceDidDisconnectNotification";
|
||||
|
||||
@interface ALTDeviceManager ()
|
||||
|
||||
@@ -26,6 +31,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSProgress *> *installationProgress;
|
||||
@property (nonatomic, readonly) dispatch_queue_t installationQueue;
|
||||
|
||||
@property (nonatomic, readonly) NSMutableSet<ALTDevice *> *cachedDevices;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ALTDeviceManager
|
||||
@@ -50,11 +57,20 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
_installationProgress = [NSMutableDictionary dictionary];
|
||||
|
||||
_installationQueue = dispatch_queue_create("com.rileytestut.AltServer.InstallationQueue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_cachedDevices = [NSMutableSet set];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
idevice_event_subscribe(ALTDeviceDidChangeConnectionStatus, nil);
|
||||
}
|
||||
|
||||
#pragma mark - App Installation -
|
||||
|
||||
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:4];
|
||||
@@ -109,6 +125,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
int code = misagent_get_status_code(mis);
|
||||
NSLog(@"Failed to reinstall provisioning profile %@. (%@)", provisioningProfile.UUID, @(code));
|
||||
}
|
||||
|
||||
plist_free(pdata);
|
||||
}
|
||||
|
||||
[[NSFileManager defaultManager] removeItemAtURL:removedProfilesDirectoryURL error:nil];
|
||||
@@ -279,13 +297,25 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
return finish(error);
|
||||
}
|
||||
|
||||
plist_t profiles = NULL;
|
||||
plist_t rawProfiles = NULL;
|
||||
|
||||
if (misagent_copy_all(mis, &profiles) != MISAGENT_E_SUCCESS)
|
||||
if (misagent_copy_all(mis, &rawProfiles) != MISAGENT_E_SUCCESS)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
|
||||
// For some reason, libplist now fails to parse `rawProfiles` correctly.
|
||||
// Specifically, it no longer recognizes the nodes in the plist array as "data" nodes.
|
||||
// However, if we encode it as XML then decode it again, it'll work ¯\_(ツ)_/¯
|
||||
char *plistXML = nullptr;
|
||||
uint32_t plistLength = 0;
|
||||
plist_to_xml(rawProfiles, &plistXML, &plistLength);
|
||||
|
||||
plist_t profiles = NULL;
|
||||
plist_from_xml(plistXML, plistLength, &profiles);
|
||||
|
||||
free(plistXML);
|
||||
|
||||
uint32_t profileCount = plist_array_get_size(profiles);
|
||||
for (int i = 0; i < profileCount; i++)
|
||||
{
|
||||
@@ -294,7 +324,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
char *bytes = NULL;
|
||||
uint64_t length = 0;
|
||||
|
||||
@@ -304,10 +334,10 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
continue;
|
||||
}
|
||||
|
||||
NSData *data = [NSData dataWithBytes:(const void *)bytes length:length];
|
||||
NSData *data = [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:YES];
|
||||
ALTProvisioningProfile *provisioningProfile = [[ALTProvisioningProfile alloc] initWithData:data];
|
||||
|
||||
if (![provisioningProfile.teamIdentifier isEqualToString:installationProvisioningProfile.teamIdentifier])
|
||||
if (![provisioningProfile isFreeProvisioningProfile])
|
||||
{
|
||||
NSLog(@"Ignoring: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier);
|
||||
continue;
|
||||
@@ -338,14 +368,17 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
|
||||
if (misagent_remove(mis, provisioningProfile.UUID.UUIDString.lowercaseString.UTF8String) == MISAGENT_E_SUCCESS)
|
||||
{
|
||||
NSLog(@"Removed provisioning profile: %@", provisioningProfile.UUID);
|
||||
NSLog(@"Removed provisioning profile: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
int code = misagent_get_status_code(mis);
|
||||
NSLog(@"Failed to remove provisioning profile %@. Error Code: %@", provisioningProfile.UUID, @(code));
|
||||
NSLog(@"Failed to remove provisioning profile %@ (Team: %@). Error Code: %@", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier, @(code));
|
||||
}
|
||||
}
|
||||
|
||||
plist_free(rawProfiles);
|
||||
plist_free(profiles);
|
||||
|
||||
lockdownd_client_free(client);
|
||||
client = NULL;
|
||||
@@ -514,6 +547,89 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
return success;
|
||||
}
|
||||
|
||||
#pragma mark - Connections -
|
||||
|
||||
- (void)startWiredConnectionToDevice:(ALTDevice *)altDevice completionHandler:(void (^)(ALTWiredConnection * _Nullable, NSError * _Nullable))completionHandler
|
||||
{
|
||||
void (^finish)(ALTWiredConnection *connection, NSError *error) = ^(ALTWiredConnection *connection, NSError *error) {
|
||||
if (error != nil)
|
||||
{
|
||||
NSLog(@"Wired Connection Error: %@", error);
|
||||
}
|
||||
|
||||
completionHandler(connection, error);
|
||||
};
|
||||
|
||||
idevice_t device = NULL;
|
||||
idevice_connection_t connection = NULL;
|
||||
|
||||
/* Find Device */
|
||||
if (idevice_new_ignore_network(&device, altDevice.identifier.UTF8String) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
|
||||
}
|
||||
|
||||
/* Connect to Listening Socket */
|
||||
if (idevice_connect(device, ALTDeviceListeningSocket, &connection) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
idevice_free(device);
|
||||
|
||||
ALTWiredConnection *wiredConnection = [[ALTWiredConnection alloc] initWithDevice:altDevice connection:connection];
|
||||
finish(wiredConnection, nil);
|
||||
}
|
||||
|
||||
- (void)startNotificationConnectionToDevice:(ALTDevice *)altDevice completionHandler:(void (^)(ALTNotificationConnection * _Nullable, NSError * _Nullable))completionHandler
|
||||
{
|
||||
void (^finish)(ALTNotificationConnection *, NSError *) = ^(ALTNotificationConnection *connection, NSError *error) {
|
||||
if (error != nil)
|
||||
{
|
||||
NSLog(@"Notification Connection Error: %@", error);
|
||||
}
|
||||
|
||||
completionHandler(connection, error);
|
||||
};
|
||||
|
||||
idevice_t device = NULL;
|
||||
lockdownd_client_t lockdownClient = NULL;
|
||||
lockdownd_service_descriptor_t service = NULL;
|
||||
|
||||
np_client_t client = NULL;
|
||||
|
||||
/* Find Device */
|
||||
if (idevice_new_ignore_network(&device, altDevice.identifier.UTF8String) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
|
||||
}
|
||||
|
||||
/* Connect to Device */
|
||||
if (lockdownd_client_new_with_handshake(device, &lockdownClient, "altserver") != LOCKDOWN_E_SUCCESS)
|
||||
{
|
||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
/* Connect to Notification Proxy */
|
||||
if ((lockdownd_start_service(lockdownClient, "com.apple.mobile.notification_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
|
||||
{
|
||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
/* Connect to Client */
|
||||
if (np_client_new(device, service, &client) != NP_E_SUCCESS)
|
||||
{
|
||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
lockdownd_service_descriptor_free(service);
|
||||
lockdownd_client_free(lockdownClient);
|
||||
idevice_free(device);
|
||||
|
||||
ALTNotificationConnection *notificationConnection = [[ALTNotificationConnection alloc] initWithDevice:altDevice client:client];
|
||||
completionHandler(notificationConnection, nil);
|
||||
}
|
||||
|
||||
#pragma mark - Getters -
|
||||
|
||||
- (NSArray<ALTDevice *> *)connectedDevices
|
||||
@@ -670,3 +786,49 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
|
||||
NSLog(@"Installation Progress: %@", @(percent));
|
||||
}
|
||||
}
|
||||
|
||||
void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data)
|
||||
{
|
||||
ALTDevice * (^deviceForUDID)(NSString *, NSArray<ALTDevice *> *) = ^ALTDevice *(NSString *udid, NSArray<ALTDevice *> *devices) {
|
||||
for (ALTDevice *device in devices)
|
||||
{
|
||||
if ([device.identifier isEqualToString:udid])
|
||||
{
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
};
|
||||
|
||||
switch (event->event)
|
||||
{
|
||||
case IDEVICE_DEVICE_ADD:
|
||||
{
|
||||
ALTDevice *device = deviceForUDID(@(event->udid), ALTDeviceManager.sharedManager.connectedDevices);
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ALTDeviceManagerDeviceDidConnectNotification object:device];
|
||||
|
||||
if (device)
|
||||
{
|
||||
[ALTDeviceManager.sharedManager.cachedDevices addObject:device];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IDEVICE_DEVICE_REMOVE:
|
||||
{
|
||||
ALTDevice *device = deviceForUDID(@(event->udid), ALTDeviceManager.sharedManager.cachedDevices.allObjects);
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ALTDeviceManagerDeviceDidDisconnectNotification object:device];
|
||||
|
||||
if (device)
|
||||
{
|
||||
[ALTDeviceManager.sharedManager.cachedDevices removeObject:device];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.1</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>LSUIElement</key>
|
||||
@@ -30,5 +30,7 @@
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://altstore.io/altserver/sparkle-macos.xml</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
13
AltServer/InstallPlugin.sh
Normal file
13
AltServer/InstallPlugin.sh
Normal 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
|
||||
9
AltServer/UninstallPlugin.sh
Normal file
9
AltServer/UninstallPlugin.sh
Normal 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
|
||||
@@ -3,10 +3,12 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objectVersion = 51;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
01100C7036F0EBAC5B30984B /* libPods-AltStore.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0DE618FA97EA42C3F468D186 /* libPods-AltStore.a */; };
|
||||
A8BCEBEAC0620CF80A2FD26D /* Pods_AltServer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */; };
|
||||
BF0201BA22C2EFA3000B93E4 /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; };
|
||||
BF0201BB22C2EFA3000B93E4 /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF0201BD22C2EFBC000B93E4 /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; };
|
||||
@@ -17,6 +19,7 @@
|
||||
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858222DE795100DE9F1E /* MyAppsViewController.swift */; };
|
||||
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */; };
|
||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */; };
|
||||
BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF0F5FC623F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel */; };
|
||||
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */; };
|
||||
BF100C54232D7DAE006A8926 /* StoreAppPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */; };
|
||||
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B0F022E25DF9005C4CF5 /* ToastView.swift */; };
|
||||
@@ -28,6 +31,7 @@
|
||||
BF1E315F22A0635900370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
|
||||
BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
|
||||
BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF258CE222EBAE2800023032 /* AppProtocol.swift */; };
|
||||
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */; };
|
||||
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; };
|
||||
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; };
|
||||
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; };
|
||||
@@ -110,29 +114,46 @@
|
||||
BF45884A2298D55000BD7491 /* thread.c in Sources */ = {isa = PBXBuildFile; fileRef = BF4588482298D55000BD7491 /* thread.c */; };
|
||||
BF45884B2298D55000BD7491 /* thread.h in Headers */ = {isa = PBXBuildFile; fileRef = BF4588492298D55000BD7491 /* thread.h */; };
|
||||
BF4588882298DD3F00BD7491 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4588872298DD3F00BD7491 /* libxml2.tbd */; };
|
||||
BF4713A522976D1E00784A2F /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; };
|
||||
BF4713A622976D1E00784A2F /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF4C7F2523801F0800B2556E /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9B63C5229DD44D002F0A62 /* AltSign.framework */; };
|
||||
BF4C7F27238086EB00B2556E /* InstallPlugin.sh in Resources */ = {isa = PBXBuildFile; fileRef = BF4C7F26238086EB00B2556E /* InstallPlugin.sh */; };
|
||||
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */; };
|
||||
BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; };
|
||||
BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF56D2AA23DF88310006506D /* AppID.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2A923DF88310006506D /* AppID.swift */; };
|
||||
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */; };
|
||||
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */; };
|
||||
BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; };
|
||||
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */; };
|
||||
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */; };
|
||||
BF718BD123C91BD300A89F2D /* ALTWiredConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD023C91BD300A89F2D /* ALTWiredConnection.m */; };
|
||||
BF718BD523C928A300A89F2D /* ALTNotificationConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD423C928A300A89F2D /* ALTNotificationConnection.m */; };
|
||||
BF718BD823C93DB700A89F2D /* AltKit.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD723C93DB700A89F2D /* AltKit.m */; };
|
||||
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */; };
|
||||
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; };
|
||||
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* AppOperationContext.swift */; };
|
||||
BF770E5622BC3C03002A40FE /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5522BC3C02002A40FE /* Server.swift */; };
|
||||
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.swift */; };
|
||||
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */; };
|
||||
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; };
|
||||
BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */; };
|
||||
BF7C627423DBB78C00515A2D /* InstalledAppPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */; };
|
||||
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C122E659F700049BA1 /* AppContentViewController.swift */; };
|
||||
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C322E662D300049BA1 /* AppViewController.swift */; };
|
||||
BF914C262383703800E713BA /* AltPlugin.mailbundle.zip in Resources */ = {isa = PBXBuildFile; fileRef = BF914C252383703800E713BA /* AltPlugin.mailbundle.zip */; };
|
||||
BF9A03C623C7DD0D000D08DB /* ClientConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9A03C523C7DD0D000D08DB /* ClientConnection.swift */; };
|
||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */; };
|
||||
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */; };
|
||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */; };
|
||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4A22DD137F008935CF /* NavigationBar.swift */; };
|
||||
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4C22DD16DE008935CF /* PillButton.swift */; };
|
||||
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */; };
|
||||
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172823C56042001B5953 /* ServerConnection.swift */; };
|
||||
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; };
|
||||
BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172C23C5823E001B5953 /* InstalledExtension.swift */; };
|
||||
BFA8172F23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */; };
|
||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; };
|
||||
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; };
|
||||
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; };
|
||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; };
|
||||
BFB49AAA23834CF900D542D9 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */; };
|
||||
BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21A23186D640022A802 /* NewsItem.swift */; };
|
||||
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21D231870160022A802 /* NewsViewController.swift */; };
|
||||
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21F231870B00022A802 /* NewsCollectionViewCell.swift */; };
|
||||
@@ -145,8 +166,6 @@
|
||||
BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; };
|
||||
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; };
|
||||
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247782284B9A700981D42 /* LaunchScreen.storyboard */; };
|
||||
BFD247872284BB4200981D42 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFD247862284BB3B00981D42 /* Roxas.framework */; };
|
||||
BFD247882284BB4200981D42 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFD247862284BB3B00981D42 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478B2284C4C300981D42 /* AppIconImageView.swift */; };
|
||||
BFD2478F2284C8F900981D42 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478E2284C8F900981D42 /* Button.swift */; };
|
||||
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */; };
|
||||
@@ -189,6 +208,7 @@
|
||||
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F3230DDB0A007955AB /* Campaign.swift */; };
|
||||
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F5230DDB12007955AB /* Tier.swift */; };
|
||||
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */; };
|
||||
BFD80D572380C0F700B9C227 /* UninstallPlugin.sh in Resources */ = {isa = PBXBuildFile; fileRef = BFD80D562380C0F700B9C227 /* UninstallPlugin.sh */; };
|
||||
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */; };
|
||||
BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */; };
|
||||
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */; };
|
||||
@@ -199,6 +219,7 @@
|
||||
BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DC22F0E7F3002E24B9 /* Source.swift */; };
|
||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */; };
|
||||
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338E722F10E56002E24B9 /* LaunchViewController.swift */; };
|
||||
BFE48975238007CE003239E0 /* AnisetteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE48974238007CE003239E0 /* AnisetteDataManager.swift */; };
|
||||
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE60737231ADF49002B0E8E /* Settings.storyboard */; };
|
||||
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE60739231ADF82002B0E8E /* SettingsViewController.swift */; };
|
||||
BFE6073C231AE1E7002B0E8E /* SettingsHeaderFooterView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFE6073B231AE1E7002B0E8E /* SettingsHeaderFooterView.xib */; };
|
||||
@@ -208,6 +229,8 @@
|
||||
BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; };
|
||||
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
|
||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; };
|
||||
BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */; };
|
||||
BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE944023F22AA100CDA07D /* AppIDComponents.swift */; };
|
||||
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; };
|
||||
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; };
|
||||
BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */; };
|
||||
@@ -215,7 +238,6 @@
|
||||
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B695232242D3007A79E1 /* LicensesViewController.swift */; };
|
||||
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */; };
|
||||
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */; };
|
||||
DBAC68F8EC03F4A41D62EDE1 /* Pods_AltStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1039C07E517311FC499A0B64 /* Pods_AltStore.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -240,6 +262,13 @@
|
||||
remoteGlobalIDString = BF45872A2298D31600BD7491;
|
||||
remoteInfo = libimobiledevice;
|
||||
};
|
||||
BFBFFB262380C72F00993A4A /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BFD247622284B9A500981D42 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = BF5C5FC4237DF5AE00EDD0C6;
|
||||
remoteInfo = AltPlugin;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -265,23 +294,21 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BFD247842284BB2C00981D42 /* Embed Frameworks */ = {
|
||||
BF5C5FE9237E438C00EDD0C6 /* CopyFiles */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
BF4713A622976D1E00784A2F /* openssl.framework in Embed Frameworks */,
|
||||
BFD247882284BB4200981D42 /* Roxas.framework in Embed Frameworks */,
|
||||
BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1039C07E517311FC499A0B64 /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0DE618FA97EA42C3F468D186 /* libPods-AltStore.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AltStore.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
11611D46F8A7C8B928E8156B /* Pods-AltServer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltServer.debug.xcconfig"; path = "Target Support Files/Pods-AltServer/Pods-AltServer.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
589BA531D903B28F292063E5 /* Pods-AltServer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltServer.release.xcconfig"; path = "Target Support Files/Pods-AltServer/Pods-AltServer.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A136EE677716B80768E9F0A2 /* Pods-AltStore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.release.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.release.xcconfig"; sourceTree = "<group>"; };
|
||||
BF02419322F2156E00129732 /* RefreshAttempt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttempt.swift; sourceTree = "<group>"; };
|
||||
BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttemptsViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -289,6 +316,7 @@
|
||||
BF08858222DE795100DE9F1E /* MyAppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewController.swift; sourceTree = "<group>"; };
|
||||
BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = "<group>"; };
|
||||
BF0F5FC623F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore3ToAltStore4.xcmappingmodel; sourceTree = "<group>"; };
|
||||
BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 2.xcdatamodel"; sourceTree = "<group>"; };
|
||||
BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStoreToAltStore2.xcmappingmodel; sourceTree = "<group>"; };
|
||||
BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreAppPolicy.swift; sourceTree = "<group>"; };
|
||||
@@ -302,6 +330,8 @@
|
||||
BF1E315022A0616100370A3C /* libAltKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libAltKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF219A7E22CAC431007676A6 /* AltStore.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltStore.entitlements; sourceTree = "<group>"; };
|
||||
BF258CE222EBAE2800023032 /* AppProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = "<group>"; };
|
||||
BF26A0DF2370C5D400F53F9F /* ALTSourceUserInfoKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTSourceUserInfoKey.h; sourceTree = "<group>"; };
|
||||
BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = "<group>"; };
|
||||
BF29012E2318F6B100D88A45 /* AppBannerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AppBannerView.xib; sourceTree = "<group>"; };
|
||||
BF2901302318F7A800D88A45 /* AppBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBannerView.swift; sourceTree = "<group>"; };
|
||||
BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; };
|
||||
@@ -392,17 +422,42 @@
|
||||
BF4588872298DD3F00BD7491 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
BF4588962298DE6E00BD7491 /* libzip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libzip.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF4713A422976CFC00784A2F /* openssl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = openssl.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF4C7F26238086EB00B2556E /* InstallPlugin.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = InstallPlugin.sh; sourceTree = "<group>"; };
|
||||
BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = "<group>"; };
|
||||
BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = "<group>"; };
|
||||
BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 4.xcdatamodel"; sourceTree = "<group>"; };
|
||||
BF56D2A923DF88310006506D /* AppID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppID.swift; sourceTree = "<group>"; };
|
||||
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppIDsOperation.swift; sourceTree = "<group>"; };
|
||||
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDsViewController.swift; sourceTree = "<group>"; };
|
||||
BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AltPlugin.mailbundle; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF5C5FC7237DF5AE00EDD0C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BF5C5FCD237DF69100EDD0C6 /* ALTPluginService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPluginService.h; sourceTree = "<group>"; };
|
||||
BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPluginService.m; sourceTree = "<group>"; };
|
||||
BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAltStoreViewController.swift; sourceTree = "<group>"; };
|
||||
BF718BC723C919CC00A89F2D /* CFNotificationName+AltStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CFNotificationName+AltStore.h"; sourceTree = "<group>"; };
|
||||
BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CFNotificationName+AltStore.m"; sourceTree = "<group>"; };
|
||||
BF718BCF23C91BD300A89F2D /* ALTWiredConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWiredConnection.h; sourceTree = "<group>"; };
|
||||
BF718BD023C91BD300A89F2D /* ALTWiredConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTWiredConnection.m; sourceTree = "<group>"; };
|
||||
BF718BD223C91C7000A89F2D /* ALTWiredConnection+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ALTWiredConnection+Private.h"; sourceTree = "<group>"; };
|
||||
BF718BD323C928A300A89F2D /* ALTNotificationConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTNotificationConnection.h; sourceTree = "<group>"; };
|
||||
BF718BD423C928A300A89F2D /* ALTNotificationConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTNotificationConnection.m; sourceTree = "<group>"; };
|
||||
BF718BD623C92B3700A89F2D /* ALTNotificationConnection+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ALTNotificationConnection+Private.h"; sourceTree = "<group>"; };
|
||||
BF718BD723C93DB700A89F2D /* AltKit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AltKit.m; sourceTree = "<group>"; };
|
||||
BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardingNavigationController.swift; sourceTree = "<group>"; };
|
||||
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = "<group>"; };
|
||||
BF770E5322BC044E002A40FE /* AppOperationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOperationContext.swift; sourceTree = "<group>"; };
|
||||
BF770E5522BC3C02002A40FE /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
|
||||
BF770E5722BC3D0F002A40FE /* OperationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationGroup.swift; sourceTree = "<group>"; };
|
||||
BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = "<group>"; };
|
||||
BF770E6822BD57DD002A40FE /* Silence.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Silence.m4a; sourceTree = "<group>"; };
|
||||
BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 3.xcdatamodel"; sourceTree = "<group>"; };
|
||||
BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore2ToAltStore3.xcmappingmodel; sourceTree = "<group>"; };
|
||||
BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppPolicy.swift; sourceTree = "<group>"; };
|
||||
BF8F69C122E659F700049BA1 /* AppContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContentViewController.swift; sourceTree = "<group>"; };
|
||||
BF8F69C322E662D300049BA1 /* AppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = "<group>"; };
|
||||
BF914C252383703800E713BA /* AltPlugin.mailbundle.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = AltPlugin.mailbundle.zip; sourceTree = "<group>"; };
|
||||
BF9A03C523C7DD0D000D08DB /* ClientConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConnection.swift; sourceTree = "<group>"; };
|
||||
BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewController.swift; sourceTree = "<group>"; };
|
||||
BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
@@ -410,11 +465,17 @@
|
||||
BF9ABA4C22DD16DE008935CF /* PillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButton.swift; sourceTree = "<group>"; };
|
||||
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Hex.swift"; sourceTree = "<group>"; };
|
||||
BF9B63C5229DD44D002F0A62 /* AltSign.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFA8172823C56042001B5953 /* ServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnection.swift; sourceTree = "<group>"; };
|
||||
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnisetteDataOperation.swift; sourceTree = "<group>"; };
|
||||
BFA8172C23C5823E001B5953 /* InstalledExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledExtension.swift; sourceTree = "<group>"; };
|
||||
BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrepareDeveloperAccountOperation.swift; sourceTree = "<group>"; };
|
||||
BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
|
||||
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; };
|
||||
BFB1169C22932DB100BB457C /* apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apps.json; sourceTree = "<group>"; };
|
||||
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindServerOperation.swift; sourceTree = "<group>"; };
|
||||
BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UpdateCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ALTAnisetteData.m; path = "Dependencies/AltSign/AltSign/Model/Apple API/ALTAnisetteData.m"; sourceTree = SOURCE_ROOT; };
|
||||
BFB49AA923834CF900D542D9 /* ALTAnisetteData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ALTAnisetteData.h; path = "Dependencies/AltSign/AltSign/Model/Apple API/ALTAnisetteData.h"; sourceTree = SOURCE_ROOT; };
|
||||
BFB6B21A23186D640022A802 /* NewsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsItem.swift; sourceTree = "<group>"; };
|
||||
BFB6B21D231870160022A802 /* NewsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsViewController.swift; sourceTree = "<group>"; };
|
||||
BFB6B21F231870B00022A802 /* NewsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
@@ -474,6 +535,7 @@
|
||||
BFD5D6F3230DDB0A007955AB /* Campaign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Campaign.swift; sourceTree = "<group>"; };
|
||||
BFD5D6F5230DDB12007955AB /* Tier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tier.swift; sourceTree = "<group>"; };
|
||||
BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsComponents.swift; sourceTree = "<group>"; };
|
||||
BFD80D562380C0F700B9C227 /* UninstallPlugin.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = UninstallPlugin.sh; sourceTree = "<group>"; };
|
||||
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+RelativeDate.swift"; sourceTree = "<group>"; };
|
||||
BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BrowseCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = "<group>"; };
|
||||
@@ -484,6 +546,7 @@
|
||||
BFE338DC22F0E7F3002E24B9 /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = "<group>"; };
|
||||
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSourceOperation.swift; sourceTree = "<group>"; };
|
||||
BFE338E722F10E56002E24B9 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
|
||||
BFE48974238007CE003239E0 /* AnisetteDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnisetteDataManager.swift; sourceTree = "<group>"; };
|
||||
BFE60737231ADF49002B0E8E /* Settings.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = "<group>"; };
|
||||
BFE60739231ADF82002B0E8E /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
||||
BFE6073B231AE1E7002B0E8E /* SettingsHeaderFooterView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SettingsHeaderFooterView.xib; sourceTree = "<group>"; };
|
||||
@@ -493,6 +556,8 @@
|
||||
BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = "<group>"; };
|
||||
BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
|
||||
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = "<group>"; };
|
||||
BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTApplication+AppExtensions.swift"; sourceTree = "<group>"; };
|
||||
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDComponents.swift; sourceTree = "<group>"; };
|
||||
BFF0B68D23219520007A79E1 /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = "<group>"; };
|
||||
BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonComponents.swift; sourceTree = "<group>"; };
|
||||
BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AboutPatreonHeaderView.xib; sourceTree = "<group>"; };
|
||||
@@ -516,6 +581,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BF4C7F2523801F0800B2556E /* AltSign.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -529,6 +595,14 @@
|
||||
BF4588472298D4B000BD7491 /* libimobiledevice.a in Frameworks */,
|
||||
BF0201BD22C2EFBC000B93E4 /* openssl.framework in Frameworks */,
|
||||
BF0201BA22C2EFA3000B93E4 /* AltSign.framework in Frameworks */,
|
||||
A8BCEBEAC0620CF80A2FD26D /* Pods_AltServer.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF5C5FC2237DF5AE00EDD0C6 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -537,10 +611,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */,
|
||||
BF4713A522976D1E00784A2F /* openssl.framework in Frameworks */,
|
||||
BFD247872284BB4200981D42 /* Roxas.framework in Frameworks */,
|
||||
BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */,
|
||||
DBAC68F8EC03F4A41D62EDE1 /* Pods_AltStore.framework in Frameworks */,
|
||||
01100C7036F0EBAC5B30984B /* libPods-AltStore.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -552,6 +623,8 @@
|
||||
children = (
|
||||
EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */,
|
||||
A136EE677716B80768E9F0A2 /* Pods-AltStore.release.xcconfig */,
|
||||
11611D46F8A7C8B928E8156B /* Pods-AltServer.debug.xcconfig */,
|
||||
589BA531D903B28F292063E5 /* Pods-AltServer.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -577,6 +650,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */,
|
||||
BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */,
|
||||
BF0F5FC623F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel */,
|
||||
);
|
||||
path = "Mapping Models";
|
||||
sourceTree = "<group>";
|
||||
@@ -585,6 +660,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */,
|
||||
BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */,
|
||||
);
|
||||
path = Policies;
|
||||
sourceTree = "<group>";
|
||||
@@ -593,11 +669,14 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFD52BD222A06EFB000B7ED1 /* AltKit.h */,
|
||||
BF718BD723C93DB700A89F2D /* AltKit.m */,
|
||||
BFBAC8852295C90300587369 /* Result+Conveniences.swift */,
|
||||
BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */,
|
||||
BF1E3128229F474900370A3C /* ServerProtocol.swift */,
|
||||
BF1E314822A060F400370A3C /* NSError+ALTServerError.h */,
|
||||
BF1E314922A060F400370A3C /* NSError+ALTServerError.m */,
|
||||
BF718BC723C919CC00A89F2D /* CFNotificationName+AltStore.h */,
|
||||
BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */,
|
||||
);
|
||||
path = AltKit;
|
||||
sourceTree = "<group>";
|
||||
@@ -609,6 +688,8 @@
|
||||
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */,
|
||||
BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */,
|
||||
BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */,
|
||||
BF26A0DF2370C5D400F53F9F /* ALTSourceUserInfoKey.h */,
|
||||
BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */,
|
||||
BF41B807233433C100C593A3 /* LoadingState.swift */,
|
||||
);
|
||||
path = Types;
|
||||
@@ -630,6 +711,7 @@
|
||||
children = (
|
||||
BF45868F229872EA00BD7491 /* AppDelegate.swift */,
|
||||
BF458695229872EA00BD7491 /* Main.storyboard */,
|
||||
BFE48974238007CE003239E0 /* AnisetteDataManager.swift */,
|
||||
BF703195229F36FF006E110F /* Devices */,
|
||||
BFD52BDC22A0A659000B7ED1 /* Connections */,
|
||||
BF055B4A233B528B0086DEA9 /* Extensions */,
|
||||
@@ -778,10 +860,34 @@
|
||||
name = libcnary;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF56D2AD23DF9E170006506D /* App IDs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */,
|
||||
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */,
|
||||
);
|
||||
path = "App IDs";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF5C5FC6237DF5AE00EDD0C6 /* AltPlugin */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF5C5FCD237DF69100EDD0C6 /* ALTPluginService.h */,
|
||||
BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */,
|
||||
BFB49AA923834CF900D542D9 /* ALTAnisetteData.h */,
|
||||
BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */,
|
||||
BF5C5FC7237DF5AE00EDD0C6 /* Info.plist */,
|
||||
);
|
||||
path = AltPlugin;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF703194229F36F6006E110F /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF458693229872EA00BD7491 /* Assets.xcassets */,
|
||||
BF914C252383703800E713BA /* AltPlugin.mailbundle.zip */,
|
||||
BF4C7F26238086EB00B2556E /* InstallPlugin.sh */,
|
||||
BFD80D562380C0F700B9C227 /* UninstallPlugin.sh */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
@@ -843,6 +949,7 @@
|
||||
children = (
|
||||
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */,
|
||||
BF770E5522BC3C02002A40FE /* Server.swift */,
|
||||
BFA8172823C56042001B5953 /* ServerConnection.swift */,
|
||||
);
|
||||
path = Server;
|
||||
sourceTree = "<group>";
|
||||
@@ -854,6 +961,7 @@
|
||||
BF45868E229872EA00BD7491 /* AltServer */,
|
||||
BF1E315122A0616100370A3C /* AltKit */,
|
||||
BF45872C2298D31600BD7491 /* libimobiledevice */,
|
||||
BF5C5FC6237DF5AE00EDD0C6 /* AltPlugin */,
|
||||
BFD247852284BB3300981D42 /* Frameworks */,
|
||||
BFD2476B2284B9A500981D42 /* Products */,
|
||||
4460E048E3AC1C9708C4FA33 /* Pods */,
|
||||
@@ -867,6 +975,7 @@
|
||||
BF45868D229872EA00BD7491 /* AltServer.app */,
|
||||
BF45872B2298D31600BD7491 /* libimobiledevice.a */,
|
||||
BF1E315022A0616100370A3C /* libAltKit.a */,
|
||||
BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -887,6 +996,7 @@
|
||||
BFDB69FB22A9A7A6007EA6D6 /* Settings */,
|
||||
BFD5D6E6230CC94B007955AB /* Patreon */,
|
||||
BFD2478A2284C49000981D42 /* Managing Apps */,
|
||||
BF56D2AD23DF9E170006506D /* App IDs */,
|
||||
BFC51D7922972F1F00388324 /* Server */,
|
||||
BFD247982284D7FC00981D42 /* Model */,
|
||||
BFDB6A0922AAEDA1007EA6D6 /* Operations */,
|
||||
@@ -911,8 +1021,8 @@
|
||||
BFD247862284BB3B00981D42 /* Roxas.framework */,
|
||||
BF5AB3A72285FE6C00DC914B /* AltSign.framework */,
|
||||
BF4713A422976CFC00784A2F /* openssl.framework */,
|
||||
1039C07E517311FC499A0B64 /* Pods_AltStore.framework */,
|
||||
FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */,
|
||||
0DE618FA97EA42C3F468D186 /* libPods-AltStore.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -929,6 +1039,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFD2478B2284C4C300981D42 /* AppIconImageView.swift */,
|
||||
BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */,
|
||||
BFD2478E2284C8F900981D42 /* Button.swift */,
|
||||
BF43002D22A714AF0051E2BC /* Keychain.swift */,
|
||||
BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */,
|
||||
@@ -969,8 +1080,10 @@
|
||||
BFB11691229322E400BB457C /* DatabaseManager.swift */,
|
||||
BF3D64A122E8031100E9056B /* MergePolicy.swift */,
|
||||
BFE6326722A858F300F30809 /* Account.swift */,
|
||||
BF56D2A923DF88310006506D /* AppID.swift */,
|
||||
BF3D648722E79A3700E9056B /* AppPermission.swift */,
|
||||
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
|
||||
BFA8172C23C5823E001B5953 /* InstalledExtension.swift */,
|
||||
BFB6B21A23186D640022A802 /* NewsItem.swift */,
|
||||
BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */,
|
||||
BF02419322F2156E00129732 /* RefreshAttempt.swift */,
|
||||
@@ -991,6 +1104,7 @@
|
||||
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */,
|
||||
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */,
|
||||
BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */,
|
||||
BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -999,6 +1113,13 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF1E3129229F474900370A3C /* ConnectionManager.swift */,
|
||||
BF9A03C523C7DD0D000D08DB /* ClientConnection.swift */,
|
||||
BF718BCF23C91BD300A89F2D /* ALTWiredConnection.h */,
|
||||
BF718BD223C91C7000A89F2D /* ALTWiredConnection+Private.h */,
|
||||
BF718BD023C91BD300A89F2D /* ALTWiredConnection.m */,
|
||||
BF718BD323C928A300A89F2D /* ALTNotificationConnection.h */,
|
||||
BF718BD623C92B3700A89F2D /* ALTNotificationConnection+Private.h */,
|
||||
BF718BD423C928A300A89F2D /* ALTNotificationConnection.m */,
|
||||
);
|
||||
path = Connections;
|
||||
sourceTree = "<group>";
|
||||
@@ -1050,11 +1171,14 @@
|
||||
BF770E5322BC044E002A40FE /* AppOperationContext.swift */,
|
||||
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
|
||||
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */,
|
||||
BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */,
|
||||
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
|
||||
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
|
||||
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
|
||||
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
|
||||
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
|
||||
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
|
||||
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
|
||||
);
|
||||
path = Operations;
|
||||
sourceTree = "<group>";
|
||||
@@ -1065,6 +1189,7 @@
|
||||
BFE6325922A83BEB00F30809 /* Authentication.storyboard */,
|
||||
BFF0B6932321CB85007A79E1 /* AuthenticationViewController.swift */,
|
||||
BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */,
|
||||
BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */,
|
||||
);
|
||||
path = Authentication;
|
||||
sourceTree = "<group>";
|
||||
@@ -1141,15 +1266,19 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BF45869A229872EA00BD7491 /* Build configuration list for PBXNativeTarget "AltServer" */;
|
||||
buildPhases = (
|
||||
FACBF95CCAAAB7121E1D92C8 /* [CP] Check Pods Manifest.lock */,
|
||||
BF458689229872EA00BD7491 /* Sources */,
|
||||
BF45868B229872EA00BD7491 /* Resources */,
|
||||
BF4588462298D4AA00BD7491 /* Frameworks */,
|
||||
BF0201BC22C2EFA3000B93E4 /* Embed Frameworks */,
|
||||
BF7FDA2C23203B6B00B5D3A4 /* Copy Launcher App */,
|
||||
98BF22D155DBAEA97544E3E6 /* [CP] Embed Pods Frameworks */,
|
||||
BF914C242383659400E713BA /* Sign Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
BFBFFB272380C72F00993A4A /* PBXTargetDependency */,
|
||||
BF1E315E22A0621F00370A3C /* PBXTargetDependency */,
|
||||
BF4588452298D48B00BD7491 /* PBXTargetDependency */,
|
||||
);
|
||||
@@ -1175,6 +1304,24 @@
|
||||
productReference = BF45872B2298D31600BD7491 /* libimobiledevice.a */;
|
||||
productType = "com.apple.product-type.library.static";
|
||||
};
|
||||
BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BF5C5FC8237DF5AE00EDD0C6 /* Build configuration list for PBXNativeTarget "AltPlugin" */;
|
||||
buildPhases = (
|
||||
BF5C5FC1237DF5AE00EDD0C6 /* Sources */,
|
||||
BF5C5FC2237DF5AE00EDD0C6 /* Frameworks */,
|
||||
BF5C5FC3237DF5AE00EDD0C6 /* Resources */,
|
||||
BF5C5FE9237E438C00EDD0C6 /* CopyFiles */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = AltPlugin;
|
||||
productName = AltPlugin;
|
||||
productReference = BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */;
|
||||
productType = "com.apple.product-type.bundle";
|
||||
};
|
||||
BFD247692284B9A500981D42 /* AltStore */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BFD2477E2284B9A700981D42 /* Build configuration list for PBXNativeTarget "AltStore" */;
|
||||
@@ -1183,8 +1330,7 @@
|
||||
BFD247662284B9A500981D42 /* Sources */,
|
||||
BFD247672284B9A500981D42 /* Frameworks */,
|
||||
BFD247682284B9A500981D42 /* Resources */,
|
||||
BFD247842284BB2C00981D42 /* Embed Frameworks */,
|
||||
B8F37E08B55D2C9C4E2B1B4E /* [CP] Embed Pods Frameworks */,
|
||||
8C9013C41DD92A1476195C0E /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -1202,7 +1348,7 @@
|
||||
BFD247622284B9A500981D42 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1020;
|
||||
LastSwiftUpdateCheck = 1120;
|
||||
LastUpgradeCheck = 1020;
|
||||
ORGANIZATIONNAME = "Riley Testut";
|
||||
TargetAttributes = {
|
||||
@@ -1224,6 +1370,10 @@
|
||||
BF45872A2298D31600BD7491 = {
|
||||
CreatedOnToolsVersion = 10.2.1;
|
||||
};
|
||||
BF5C5FC4237DF5AE00EDD0C6 = {
|
||||
CreatedOnToolsVersion = 11.2;
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
BFD247692284B9A500981D42 = {
|
||||
CreatedOnToolsVersion = 10.2.1;
|
||||
LastSwiftMigration = 1020;
|
||||
@@ -1255,6 +1405,7 @@
|
||||
BF45868C229872EA00BD7491 /* AltServer */,
|
||||
BF1E314F22A0616100370A3C /* AltKit */,
|
||||
BF45872A2298D31600BD7491 /* libimobiledevice */,
|
||||
BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -1264,8 +1415,18 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BF914C262383703800E713BA /* AltPlugin.mailbundle.zip in Resources */,
|
||||
BFD80D572380C0F700B9C227 /* UninstallPlugin.sh in Resources */,
|
||||
BF458694229872EA00BD7491 /* Assets.xcassets in Resources */,
|
||||
BF458697229872EA00BD7491 /* Main.storyboard in Resources */,
|
||||
BF4C7F27238086EB00B2556E /* InstallPlugin.sh in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF5C5FC3237DF5AE00EDD0C6 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1291,28 +1452,38 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
B8F37E08B55D2C9C4E2B1B4E /* [CP] Embed Pods Frameworks */ = {
|
||||
8C9013C41DD92A1476195C0E /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-frameworks.sh",
|
||||
"${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nuke.framework",
|
||||
"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-frameworks.sh\"\n";
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
98BF22D155DBAEA97544E3E6 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-AltServer/Pods-AltServer-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-AltServer/Pods-AltServer-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AltServer/Pods-AltServer-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
BF7FDA2C23203B6B00B5D3A4 /* Copy Launcher App */ = {
|
||||
@@ -1333,6 +1504,46 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PROJECT_DIR}/Carthage/Build/Mac/LaunchAtLogin.framework/Resources/copy-helper.sh\"\n";
|
||||
};
|
||||
BF914C242383659400E713BA /* Sign Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Sign Frameworks";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "LOCATION=\"${BUILT_PRODUCTS_DIR}\"/\"${FRAMEWORKS_FOLDER_PATH}\"\nIDENTITY=${EXPANDED_CODE_SIGN_IDENTITY_NAME}\n\ncodesign --verbose --force --deep -o runtime --sign \"$IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A/Resources/AutoUpdate.app\"\ncodesign --verbose --force -o runtime --sign \"$IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A\"\n";
|
||||
};
|
||||
FACBF95CCAAAB7121E1D92C8 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-AltServer-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
FFB93342C7EB2021A1FFFB6A /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -1362,9 +1573,11 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BF718BD823C93DB700A89F2D /* AltKit.m in Sources */,
|
||||
BF1E315A22A0620000370A3C /* NSError+ALTServerError.m in Sources */,
|
||||
BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */,
|
||||
BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */,
|
||||
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */,
|
||||
BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1374,10 +1587,14 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BF3F786422CAA41E008FBD20 /* ALTDeviceManager+Installation.swift in Sources */,
|
||||
BF718BD523C928A300A89F2D /* ALTNotificationConnection.m in Sources */,
|
||||
BF1E312B229F474900370A3C /* ConnectionManager.swift in Sources */,
|
||||
BF9A03C623C7DD0D000D08DB /* ClientConnection.swift in Sources */,
|
||||
BF718BD123C91BD300A89F2D /* ALTWiredConnection.m in Sources */,
|
||||
BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */,
|
||||
BF4586C52298CDB800BD7491 /* ALTDeviceManager.mm in Sources */,
|
||||
BF0241AA22F29CCD00129732 /* UserDefaults+AltServer.swift in Sources */,
|
||||
BFE48975238007CE003239E0 /* AnisetteDataManager.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1442,16 +1659,29 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF5C5FC1237DF5AE00EDD0C6 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BFB49AAA23834CF900D542D9 /* ALTAnisetteData.m in Sources */,
|
||||
BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BFD247662284B9A500981D42 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
|
||||
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
|
||||
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
|
||||
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */,
|
||||
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
|
||||
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */,
|
||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
|
||||
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
|
||||
BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */,
|
||||
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */,
|
||||
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
|
||||
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */,
|
||||
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
|
||||
@@ -1463,6 +1693,7 @@
|
||||
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
|
||||
BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */,
|
||||
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
|
||||
BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */,
|
||||
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */,
|
||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
||||
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
|
||||
@@ -1474,6 +1705,7 @@
|
||||
BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */,
|
||||
BFE6326822A858F300F30809 /* Account.swift in Sources */,
|
||||
BFE6326622A857C200F30809 /* Team.swift in Sources */,
|
||||
BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */,
|
||||
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
|
||||
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
|
||||
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
|
||||
@@ -1482,9 +1714,12 @@
|
||||
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
||||
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
||||
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
|
||||
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
|
||||
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */,
|
||||
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
|
||||
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
|
||||
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */,
|
||||
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
|
||||
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */,
|
||||
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */,
|
||||
@@ -1493,11 +1728,14 @@
|
||||
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */,
|
||||
BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */,
|
||||
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
|
||||
BF7C627423DBB78C00515A2D /* InstalledAppPolicy.swift in Sources */,
|
||||
BFA8172F23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift in Sources */,
|
||||
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
|
||||
BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */,
|
||||
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */,
|
||||
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
|
||||
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
|
||||
BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */,
|
||||
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
|
||||
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
|
||||
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */,
|
||||
@@ -1505,6 +1743,7 @@
|
||||
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
|
||||
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
||||
BF3D64A222E8031100E9056B /* MergePolicy.swift in Sources */,
|
||||
BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
|
||||
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
|
||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
||||
@@ -1522,6 +1761,8 @@
|
||||
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */,
|
||||
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
|
||||
BF770E5622BC3C03002A40FE /* Server.swift in Sources */,
|
||||
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */,
|
||||
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */,
|
||||
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1544,6 +1785,11 @@
|
||||
target = BF45872A2298D31600BD7491 /* libimobiledevice */;
|
||||
targetProxy = BF4588442298D48B00BD7491 /* PBXContainerItemProxy */;
|
||||
};
|
||||
BFBFFB272380C72F00993A4A /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */;
|
||||
targetProxy = BFBFFB262380C72F00993A4A /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
@@ -1610,12 +1856,15 @@
|
||||
};
|
||||
BF45869B229872EA00BD7491 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 11611D46F8A7C8B928E8156B /* Pods-AltServer.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AltServer/AltServer.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -1648,6 +1897,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14.4;
|
||||
MARKETING_VERSION = 1.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
@@ -1659,12 +1909,15 @@
|
||||
};
|
||||
BF45869C229872EA00BD7491 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 589BA531D903B28F292063E5 /* Pods-AltServer.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AltServer/AltServer.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -1697,6 +1950,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14.4;
|
||||
MARKETING_VERSION = 1.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
@@ -1779,6 +2033,63 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BF5C5FC9237DF5AE00EDD0C6 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = AltPlugin/Info.plist;
|
||||
INSTALL_PATH = "$(HOME)/Library/Mail/Bundles/";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
"/Users/Riley/Library/Developer/Xcode/DerivedData/AltStore-bhqnkrahfutztzeudtxhcxccgtlo/Build/Products/Debug",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltPlugin;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
WRAPPER_EXTENSION = mailbundle;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
BF5C5FCA237DF5AE00EDD0C6 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = AltPlugin/Info.plist;
|
||||
INSTALL_PATH = "$(HOME)/Library/Mail/Bundles/";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
"/Users/Riley/Library/Developer/Xcode/DerivedData/AltStore-bhqnkrahfutztzeudtxhcxccgtlo/Build/Products/Debug",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltPlugin;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
WRAPPER_EXTENSION = mailbundle;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BFD2477C2284B9A700981D42 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -1913,6 +2224,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -1939,6 +2251,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -1978,6 +2291,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BF5C5FC8237DF5AE00EDD0C6 /* Build configuration list for PBXNativeTarget "AltPlugin" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
BF5C5FC9237DF5AE00EDD0C6 /* Debug */,
|
||||
BF5C5FCA237DF5AE00EDD0C6 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BFD247652284B9A500981D42 /* Build configuration list for PBXProject "AltStore" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
@@ -2002,10 +2324,12 @@
|
||||
BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */,
|
||||
BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */,
|
||||
BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */,
|
||||
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */,
|
||||
);
|
||||
currentVersion = BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */;
|
||||
currentVersion = BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */;
|
||||
path = AltStore.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
|
||||
67
AltStore.xcodeproj/xcshareddata/xcschemes/AltKit.xcscheme
Normal file
67
AltStore.xcodeproj/xcshareddata/xcschemes/AltKit.xcscheme
Normal 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>
|
||||
67
AltStore.xcodeproj/xcshareddata/xcschemes/AltPlugin.xcscheme
Normal file
67
AltStore.xcodeproj/xcshareddata/xcschemes/AltPlugin.xcscheme
Normal 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>
|
||||
@@ -2,6 +2,8 @@
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "AltKit.h"
|
||||
|
||||
#import "ALTAppPermission.h"
|
||||
#import "ALTPatreonBenefitType.h"
|
||||
#import "ALTSourceUserInfoKey.h"
|
||||
|
||||
@@ -73,6 +73,8 @@ class AppContentViewController: UITableViewController
|
||||
self.tableView.contentInset.bottom = 20
|
||||
|
||||
self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
|
||||
self.screenshotsCollectionView.prefetchDataSource = self.screenshotsDataSource
|
||||
|
||||
self.permissionsCollectionView.dataSource = self.permissionsDataSource
|
||||
|
||||
self.subtitleLabel.text = self.app.subtitle
|
||||
|
||||
@@ -27,18 +27,11 @@ class AppViewController: UIViewController
|
||||
@IBOutlet private var scrollView: UIScrollView!
|
||||
@IBOutlet private var contentView: UIView!
|
||||
|
||||
@IBOutlet private var headerView: UIView!
|
||||
@IBOutlet private var headerContentView: UIView!
|
||||
@IBOutlet private var bannerView: AppBannerView!
|
||||
|
||||
@IBOutlet private var backButton: UIButton!
|
||||
@IBOutlet private var backButtonContainerView: UIVisualEffectView!
|
||||
|
||||
@IBOutlet private var nameLabel: UILabel!
|
||||
@IBOutlet private var developerLabel: UILabel!
|
||||
@IBOutlet private var downloadButton: PillButton!
|
||||
@IBOutlet private var appIconImageView: UIImageView!
|
||||
@IBOutlet private var betaBadgeView: UIImageView!
|
||||
|
||||
@IBOutlet private var backgroundAppIconImageView: UIImageView!
|
||||
@IBOutlet private var backgroundBlurView: UIVisualEffectView!
|
||||
|
||||
@@ -51,6 +44,12 @@ class AppViewController: UIViewController
|
||||
private var _backgroundBlurEffect: UIBlurEffect?
|
||||
private var _backgroundBlurTintColor: UIColor?
|
||||
|
||||
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return _preferredStatusBarStyle
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
@@ -75,21 +74,22 @@ class AppViewController: UIViewController
|
||||
self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
|
||||
self.contentViewController.tableView.showsVerticalScrollIndicator = false
|
||||
|
||||
self.headerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
|
||||
self.headerView.layer.cornerRadius = 24
|
||||
self.headerView.layer.masksToBounds = true
|
||||
|
||||
// Bring to front so the scroll indicators are visible.
|
||||
self.view.bringSubviewToFront(self.scrollView)
|
||||
self.scrollView.isUserInteractionEnabled = false
|
||||
|
||||
self.nameLabel.text = self.app.name
|
||||
self.developerLabel.text = self.app.developerName
|
||||
self.developerLabel.textColor = self.app.tintColor
|
||||
self.appIconImageView.image = nil
|
||||
self.appIconImageView.tintColor = self.app.tintColor
|
||||
self.downloadButton.tintColor = self.app.tintColor
|
||||
self.betaBadgeView.isHidden = !self.app.isBeta
|
||||
self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
|
||||
self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
|
||||
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
||||
self.bannerView.titleLabel.text = self.app.name
|
||||
self.bannerView.subtitleLabel.text = self.app.developerName
|
||||
self.bannerView.iconImageView.image = nil
|
||||
self.bannerView.iconImageView.tintColor = self.app.tintColor
|
||||
self.bannerView.button.tintColor = self.app.tintColor
|
||||
self.bannerView.betaBadgeView.isHidden = !self.app.isBeta
|
||||
self.bannerView.tintColor = self.app.tintColor
|
||||
|
||||
self.bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
|
||||
self.backButtonContainerView.tintColor = self.app.tintColor
|
||||
|
||||
@@ -107,12 +107,13 @@ class AppViewController: UIViewController
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didChangeApp(_:)), name: .NSManagedObjectContextObjectsDidChange, object: DatabaseManager.shared.viewContext)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
|
||||
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
|
||||
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
|
||||
|
||||
// Load Images
|
||||
for imageView in [self.appIconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
|
||||
for imageView in [self.bannerView.iconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
|
||||
{
|
||||
imageView.isIndicatingActivity = true
|
||||
|
||||
@@ -219,7 +220,7 @@ class AppViewController: UIViewController
|
||||
var backButtonFrame = CGRect(x: inset, y: statusBarHeight,
|
||||
width: backButtonSize.width + 20, height: backButtonSize.height + 20)
|
||||
|
||||
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.headerView.bounds.height)
|
||||
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.bannerView.bounds.height)
|
||||
var contentFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
|
||||
var backgroundIconFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width)
|
||||
|
||||
@@ -305,12 +306,11 @@ class AppViewController: UIViewController
|
||||
|
||||
// Set frames.
|
||||
self.contentViewController.view.superview?.frame = contentFrame
|
||||
self.headerView.frame = headerFrame
|
||||
self.bannerView.frame = headerFrame
|
||||
self.backgroundAppIconImageView.frame = backgroundIconFrame
|
||||
self.backgroundBlurView.frame = backgroundIconFrame
|
||||
self.backButtonContainerView.frame = backButtonFrame
|
||||
|
||||
self.headerContentView.frame = CGRect(x: 0, y: 0, width: self.headerView.bounds.width, height: self.headerView.bounds.height)
|
||||
self.contentViewControllerShadowView.frame = self.contentViewController.view.frame
|
||||
|
||||
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
|
||||
@@ -325,6 +325,14 @@ class AppViewController: UIViewController
|
||||
|
||||
self.scrollView.contentSize = contentSize
|
||||
self.scrollView.contentOffset = contentOffset
|
||||
|
||||
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
|
||||
{
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
self._shouldResetLayout = true
|
||||
}
|
||||
|
||||
deinit
|
||||
@@ -350,7 +358,7 @@ private extension AppViewController
|
||||
{
|
||||
func update()
|
||||
{
|
||||
for button in [self.downloadButton!, self.navigationBarDownloadButton!]
|
||||
for button in [self.bannerView.button!, self.navigationBarDownloadButton!]
|
||||
{
|
||||
button.tintColor = self.app.tintColor
|
||||
button.isIndicatingActivity = false
|
||||
@@ -358,12 +366,10 @@ private extension AppViewController
|
||||
if self.app.installedApp == nil
|
||||
{
|
||||
button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
||||
button.isInverted = false
|
||||
}
|
||||
else
|
||||
{
|
||||
button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||
button.isInverted = true
|
||||
}
|
||||
|
||||
let progress = AppManager.shared.installationProgress(for: self.app)
|
||||
@@ -372,12 +378,12 @@ private extension AppViewController
|
||||
|
||||
if Date() < self.app.versionDate
|
||||
{
|
||||
self.downloadButton.countdownDate = self.app.versionDate
|
||||
self.bannerView.button.countdownDate = self.app.versionDate
|
||||
self.navigationBarDownloadButton.countdownDate = self.app.versionDate
|
||||
}
|
||||
else
|
||||
{
|
||||
self.downloadButton.countdownDate = nil
|
||||
self.bannerView.button.countdownDate = nil
|
||||
self.navigationBarDownloadButton.countdownDate = nil
|
||||
}
|
||||
|
||||
@@ -389,18 +395,29 @@ private extension AppViewController
|
||||
func showNavigationBar(for navigationController: UINavigationController? = nil)
|
||||
{
|
||||
let navigationController = navigationController ?? self.navigationController
|
||||
navigationController?.navigationBar.barStyle = .default
|
||||
navigationController?.navigationBar.alpha = 1.0
|
||||
navigationController?.navigationBar.barTintColor = .white
|
||||
navigationController?.navigationBar.tintColor = .altPrimary
|
||||
navigationController?.navigationBar.setNeedsLayout()
|
||||
|
||||
if self.traitCollection.userInterfaceStyle == .dark
|
||||
{
|
||||
self._preferredStatusBarStyle = .lightContent
|
||||
}
|
||||
else
|
||||
{
|
||||
self._preferredStatusBarStyle = .default
|
||||
}
|
||||
|
||||
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
|
||||
func hideNavigationBar(for navigationController: UINavigationController? = nil)
|
||||
{
|
||||
let navigationController = navigationController ?? self.navigationController
|
||||
navigationController?.navigationBar.barStyle = .black
|
||||
navigationController?.navigationBar.alpha = 0.0
|
||||
navigationController?.navigationBar.barTintColor = .white
|
||||
|
||||
self._preferredStatusBarStyle = .lightContent
|
||||
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
|
||||
func prepareBlur()
|
||||
@@ -445,7 +462,7 @@ private extension AppViewController
|
||||
self.navigationBarAnimator = nil
|
||||
|
||||
self.hideNavigationBar()
|
||||
self.navigationController?.navigationBar.barTintColor = .white
|
||||
|
||||
self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
||||
}
|
||||
}
|
||||
@@ -485,18 +502,20 @@ extension AppViewController
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.downloadButton.progress = nil
|
||||
self.bannerView.button.progress = nil
|
||||
self.navigationBarDownloadButton.progress = nil
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
self.downloadButton.progress = progress
|
||||
self.bannerView.button.progress = progress
|
||||
self.navigationBarDownloadButton.progress = progress
|
||||
}
|
||||
|
||||
func open(_ installedApp: InstalledApp)
|
||||
@@ -522,6 +541,15 @@ private extension AppViewController
|
||||
self._shouldResetLayout = true
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
|
||||
@objc func didBecomeActive(_ notification: Notification)
|
||||
{
|
||||
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
||||
|
||||
// Fixes Navigation Bar appearing after app becomes inactive -> active again.
|
||||
self._shouldResetLayout = true
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
extension AppViewController: UIScrollViewDelegate
|
||||
|
||||
30
AltStore/App IDs/AppIDComponents.swift
Normal file
30
AltStore/App IDs/AppIDComponents.swift
Normal 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!
|
||||
}
|
||||
225
AltStore/App IDs/AppIDsViewController.swift
Normal file
225
AltStore/App IDs/AppIDsViewController.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15703"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -17,7 +17,7 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="barTintColor" name="Primary"/>
|
||||
<color key="barTintColor" name="SettingsBackground"/>
|
||||
<textAttributes key="titleTextAttributes">
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</textAttributes>
|
||||
@@ -51,7 +51,7 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
|
||||
<rect key="frame" x="16" y="6" width="343" height="397"/>
|
||||
<rect key="frame" x="16" y="6" width="343" height="359.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
|
||||
@@ -65,16 +65,16 @@
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
|
||||
<rect key="frame" x="0.0" y="47" width="308.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf">
|
||||
<rect key="frame" x="0.0" y="117.5" width="343" height="279.5"/>
|
||||
<rect key="frame" x="0.0" y="117.5" width="343" height="242"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="196.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="159"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="72"/>
|
||||
@@ -118,7 +118,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password">
|
||||
<rect key="frame" x="0.0" y="87" width="343" height="109.5"/>
|
||||
<rect key="frame" x="0.0" y="87" width="343" height="72"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
|
||||
@@ -156,31 +156,19 @@
|
||||
</constraints>
|
||||
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="14"/>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Glz-dw-2Eg">
|
||||
<rect key="frame" x="0.0" y="76" width="343" height="33.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="If you used an app-specific password to install AltStore, please use that same password again." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="a51-OQ-f3j">
|
||||
<rect key="frame" x="14" y="0.0" width="315" height="33.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="14"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
|
||||
<rect key="frame" x="0.0" y="228.5" width="343" height="51"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<rect key="frame" x="0.0" y="191" width="343" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
|
||||
<state key="normal" title="Sign in">
|
||||
<color key="titleColor" name="Pink"/>
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="authenticate" destination="yO1-iT-7NP" eventType="primaryActionTriggered" id="LER-a2-CbC"/>
|
||||
@@ -222,7 +210,7 @@
|
||||
</constraints>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" name="Primary"/>
|
||||
<color key="backgroundColor" name="SettingsBackground"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
|
||||
<constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/>
|
||||
@@ -407,20 +395,20 @@
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
|
||||
<rect key="frame" x="16" y="608" width="343" height="51"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
|
||||
<state key="normal" title="Got it">
|
||||
<color key="titleColor" name="Pink"/>
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="dismiss" destination="aFi-fb-W0B" eventType="primaryActionTriggered" id="sBq-zj-Mln"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" name="Primary"/>
|
||||
<color key="backgroundColor" name="SettingsBackground"/>
|
||||
<constraints>
|
||||
<constraint firstItem="qZ9-AR-2zK" firstAttribute="top" secondItem="bp6-55-IG2" secondAttribute="bottom" id="3yt-cr-swd"/>
|
||||
<constraint firstItem="bp6-55-IG2" firstAttribute="top" secondItem="Zek-aC-HOO" secondAttribute="top" id="42S-q2-YZn"/>
|
||||
@@ -443,14 +431,78 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1353" y="736"/>
|
||||
</scene>
|
||||
<!--Refresh AltStore-->
|
||||
<scene sceneID="9Vh-dM-OqX">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
|
||||
<rect key="frame" x="16" y="570" width="343" height="89"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
|
||||
<color key="backgroundColor" name="SettingsHighlighted"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
|
||||
<color key="tintColor" name="SettingsHighlighted"/>
|
||||
<state key="normal" title="Refresh Now">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="refreshAltStore:" destination="aoK-yE-UVT" eventType="primaryActionTriggered" id="WQu-9b-Zgg"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
|
||||
<rect key="frame" x="0.0" y="59" width="343" height="30"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" title="Refresh Later">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="cancel:" destination="aoK-yE-UVT" eventType="primaryActionTriggered" id="ffO-0a-LdE"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" name="SettingsBackground"/>
|
||||
<constraints>
|
||||
<constraint firstItem="fpO-Bf-gFY" firstAttribute="leading" secondItem="iwE-xE-ziz" secondAttribute="leading" id="A77-nX-Wg2"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="tDQ-ao-1Jg" secondAttribute="trailing" id="KPg-sO-Rnc"/>
|
||||
<constraint firstItem="fpO-Bf-gFY" firstAttribute="trailing" secondItem="iwE-xE-ziz" secondAttribute="trailing" id="SGI-1D-Eaw"/>
|
||||
<constraint firstItem="fpO-Bf-gFY" firstAttribute="bottom" secondItem="R83-kV-365" secondAttribute="bottom" id="cHl-7X-dW1"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="tDQ-ao-1Jg" secondAttribute="bottom" id="kLN-e7-BJE"/>
|
||||
<constraint firstItem="fpO-Bf-gFY" firstAttribute="top" secondItem="R83-kV-365" secondAttribute="top" id="oKo-10-7kD"/>
|
||||
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="iwE-xE-ziz"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
<connections>
|
||||
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Chr-7g-qEw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2101.5999999999999" y="733.5832083958021"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<namedColor name="Pink">
|
||||
<color red="0.92549019607843142" green="0.25490196078431371" blue="0.69803921568627447" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="Primary">
|
||||
<namedColor name="SettingsBackground">
|
||||
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="SettingsHighlighted">
|
||||
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
<color key="tintColor" name="Primary"/>
|
||||
</document>
|
||||
|
||||
@@ -12,7 +12,8 @@ import AltSign
|
||||
|
||||
class AuthenticationViewController: UIViewController
|
||||
{
|
||||
var authenticationHandler: (((ALTAccount, String)?) -> Void)?
|
||||
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
|
||||
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
|
||||
|
||||
private weak var toastView: ToastView?
|
||||
|
||||
@@ -30,6 +31,8 @@ class AuthenticationViewController: UIViewController
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.signInButton.activityIndicatorView.style = .white
|
||||
|
||||
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
|
||||
{
|
||||
view.clipsToBounds = true
|
||||
@@ -94,14 +97,16 @@ private extension AuthenticationViewController
|
||||
|
||||
self.signInButton.isIndicatingActivity = true
|
||||
|
||||
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in
|
||||
do
|
||||
{
|
||||
let account = try Result(account, error).get()
|
||||
self.authenticationHandler?((account, password))
|
||||
}
|
||||
catch
|
||||
self.authenticationHandler?(emailAddress, password) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
||||
// Ignore
|
||||
DispatchQueue.main.async {
|
||||
self.signInButton.isIndicatingActivity = false
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription)
|
||||
toastView.textLabel.textColor = .altPink
|
||||
@@ -111,6 +116,9 @@ private extension AuthenticationViewController
|
||||
|
||||
self.signInButton.isIndicatingActivity = false
|
||||
}
|
||||
|
||||
case .success(let account, let session):
|
||||
self.completionHandler?((account, session, password))
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
@@ -121,7 +129,7 @@ private extension AuthenticationViewController
|
||||
|
||||
@IBAction func cancel(_ sender: UIBarButtonItem)
|
||||
{
|
||||
self.authenticationHandler?(nil)
|
||||
self.completionHandler?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ class InstructionsViewController: UIViewController
|
||||
@IBOutlet private var contentStackView: UIStackView!
|
||||
@IBOutlet private var dismissButton: UIButton!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
89
AltStore/Authentication/RefreshAltStoreViewController.swift
Normal file
89
AltStore/Authentication/RefreshAltStoreViewController.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="6NO-wl-tj1">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@@ -11,15 +14,38 @@
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" title="" id="RiK-sx-Kgv"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
<point key="canvasLocation" x="962.31884057971024" y="375"/>
|
||||
</scene>
|
||||
<!--Tab Bar Controller-->
|
||||
<scene sceneID="9Yy-ze-Trt">
|
||||
<objects>
|
||||
<tabBarController automaticallyAdjustsScrollViewInsets="NO" id="6NO-wl-tj1" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="4lc-l2-vDf">
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</tabBar>
|
||||
<connections>
|
||||
<segue destination="01J-lp-oVM" kind="relationship" relationship="viewControllers" id="2qH-aa-n0z"/>
|
||||
</connections>
|
||||
</tabBarController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="pxX-hL-ovw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="52.173913043478265" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<namedColor name="Background">
|
||||
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -20,9 +19,6 @@
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<viewLayoutGuide key="safeArea" id="sZd-sc-Bvn"/>
|
||||
</view>
|
||||
<connections>
|
||||
<segue destination="49e-Tb-3d3" kind="presentation" identifier="finishLaunching" modalTransitionStyle="crossDissolve" id="6Ov-Kc-Van"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="vOq-mm-rY5" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@@ -31,7 +27,7 @@
|
||||
<!--Tab Bar Controller-->
|
||||
<scene sceneID="yl2-sM-qoP">
|
||||
<objects>
|
||||
<tabBarController modalPresentationStyle="fullScreen" id="49e-Tb-3d3" customClass="TabBarController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" id="49e-Tb-3d3" customClass="TabBarController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
|
||||
<rect key="frame" x="0.0" y="975" width="768" height="49"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
@@ -55,12 +51,12 @@
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="10" id="e0H-IH-rng">
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="50" minimumInteritemSpacing="10" id="e0H-IH-rng">
|
||||
<size key="itemSize" width="375" height="400"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="8" maxX="0.0" maxY="20"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells/>
|
||||
<connections>
|
||||
@@ -110,10 +106,9 @@
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="oNk-OQ-r4M">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<blurEffect style="light"/>
|
||||
<blurEffect style="regular"/>
|
||||
</visualEffectView>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" contentInsetAdjustmentBehavior="never" translatesAutoresizingMaskIntoConstraints="NO" id="Ci9-Iw-aR2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="618"/>
|
||||
@@ -124,69 +119,10 @@
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="mgO-eN-SxQ">
|
||||
<rect key="frame" x="38" y="287" width="300" height="93"/>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="37" y="287" width="300" height="93"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="yIo-bR-OBC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="93"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacingType="standard" id="LZw-eU-5SO" userLabel="App Info">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="93"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="3Ey-6S-HJx" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="14" y="14" width="65" height="65"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="65" id="AIz-49-Wuj"/>
|
||||
<constraint firstAttribute="width" secondItem="3Ey-6S-HJx" secondAttribute="height" multiplier="1:1" id="GCk-a1-dDk"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="bR7-SO-m8f">
|
||||
<rect key="frame" x="87" y="26.5" width="121" height="40.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="9z7-I4-q6g">
|
||||
<rect key="frame" x="0.0" y="0.0" width="121" height="21.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="dNE-IO-y3o">
|
||||
<rect key="frame" x="0.0" y="0.0" width="74" height="21.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="2XC-Fe-yG4">
|
||||
<rect key="frame" x="80" y="0.0" width="41" height="21.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NKT-el-rRF">
|
||||
<rect key="frame" x="0.0" y="23.5" width="66" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mgB-Gs-bik" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="216" y="31" width="72" height="31"/>
|
||||
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="72" id="j44-T1-0dc"/>
|
||||
<constraint firstAttribute="height" constant="31" id="qY2-Ng-KJy"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
|
||||
<state key="normal" title="FREE"/>
|
||||
<connections>
|
||||
<action selector="performAppAction:" destination="0V6-N4-hTO" eventType="primaryActionTriggered" id="wPd-Kn-6fI"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<edgeInsets key="layoutMargins" top="14" left="14" bottom="14" right="12"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</view>
|
||||
<blurEffect style="extraLight"/>
|
||||
</visualEffectView>
|
||||
</view>
|
||||
<containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FIv-I9-5uW">
|
||||
<rect key="frame" x="0.0" y="450" width="375" height="217"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
@@ -201,22 +137,35 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mkD-3C-WMV">
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JP7-6F-CoG">
|
||||
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<state key="normal" image="Back"/>
|
||||
<connections>
|
||||
<action selector="popViewController:" destination="0V6-N4-hTO" eventType="primaryActionTriggered" id="F6Z-xz-qCk"/>
|
||||
</connections>
|
||||
</button>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="UJ5-ia-PVA">
|
||||
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mkD-3C-WMV">
|
||||
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<state key="normal" image="Back"/>
|
||||
<connections>
|
||||
<action selector="popViewController:" destination="0V6-N4-hTO" eventType="primaryActionTriggered" id="F6Z-xz-qCk"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</view>
|
||||
<vibrancyEffect style="fill">
|
||||
<blurEffect style="prominent"/>
|
||||
</vibrancyEffect>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
</view>
|
||||
<blurEffect style="extraLight"/>
|
||||
<blurEffect style="prominent"/>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Ci9-Iw-aR2" firstAttribute="top" secondItem="0cR-li-tCB" secondAttribute="top" id="015-fz-v3B"/>
|
||||
<constraint firstAttribute="top" secondItem="Qlg-m3-lXg" secondAttribute="top" id="8tb-sY-MOu"/>
|
||||
@@ -248,18 +197,12 @@
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="appIconImageView" destination="3Ey-6S-HJx" id="5FB-mn-E29"/>
|
||||
<outlet property="backButton" destination="mkD-3C-WMV" id="3m8-P7-yvT"/>
|
||||
<outlet property="backButtonContainerView" destination="tUK-0J-07U" id="POZ-dP-f12"/>
|
||||
<outlet property="backgroundAppIconImageView" destination="CUB-SN-zdM" id="dFx-py-yMm"/>
|
||||
<outlet property="backgroundBlurView" destination="8Tg-wk-r0u" id="B8c-ng-nI5"/>
|
||||
<outlet property="betaBadgeView" destination="2XC-Fe-yG4" id="FCf-t9-Aab"/>
|
||||
<outlet property="bannerView" destination="NEy-yr-cLS" id="MTr-hK-LIR"/>
|
||||
<outlet property="contentView" destination="Qlg-m3-lXg" id="JhH-hh-vBN"/>
|
||||
<outlet property="developerLabel" destination="NKT-el-rRF" id="GUc-jy-kvv"/>
|
||||
<outlet property="downloadButton" destination="mgB-Gs-bik" id="x95-gu-NBy"/>
|
||||
<outlet property="headerContentView" destination="LZw-eU-5SO" id="hk1-xG-2kJ"/>
|
||||
<outlet property="headerView" destination="mgO-eN-SxQ" id="iIi-D7-XRt"/>
|
||||
<outlet property="nameLabel" destination="dNE-IO-y3o" id="tp1-IT-ByH"/>
|
||||
<outlet property="navigationBarAppIconImageView" destination="j1W-Jn-HFI" id="2YU-ka-w9R"/>
|
||||
<outlet property="navigationBarAppNameLabel" destination="DTD-1Y-76c" id="z9z-pp-dC4"/>
|
||||
<outlet property="navigationBarDownloadButton" destination="grk-xM-YWA" id="Yrg-S0-tIM"/>
|
||||
@@ -278,7 +221,7 @@
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<sections>
|
||||
<tableViewSection id="rfR-32-T0h">
|
||||
<cells>
|
||||
@@ -303,6 +246,7 @@
|
||||
<constraint firstItem="BsL-O2-UjD" firstAttribute="top" secondItem="8PX-jQ-nHd" secondAttribute="top" constant="20" id="dRc-WY-Jbk"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="nI6-wC-H2d">
|
||||
@@ -314,7 +258,7 @@
|
||||
<subviews>
|
||||
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="ppk-lL-at8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="15" id="ace-Ns-Jd2">
|
||||
<size key="itemSize" width="189" height="406"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
@@ -340,6 +284,7 @@
|
||||
<constraint firstItem="ppk-lL-at8" firstAttribute="top" secondItem="Z4y-vb-Z4Q" secondAttribute="top" id="xY5-w8-roA"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="EL5-UC-RIw" customClass="AppContentTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
@@ -351,7 +296,7 @@
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="20" width="335" height="34"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
@@ -363,6 +308,7 @@
|
||||
<constraint firstAttribute="trailing" secondItem="Pyt-8D-BZA" secondAttribute="trailing" constant="20" id="Wq4-Ql-wvN"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="47M-El-a4G" customClass="AppContentTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
@@ -416,7 +362,7 @@
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="16" width="335" height="0.0"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
@@ -431,6 +377,7 @@
|
||||
<constraint firstAttribute="bottom" secondItem="n9R-39-Glq" secondAttribute="bottom" priority="999" id="Zol-57-Lbq"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="149" id="nM7-vJ-W8b">
|
||||
@@ -451,7 +398,7 @@
|
||||
</label>
|
||||
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="r8T-dj-wQX">
|
||||
<rect key="frame" x="0.0" y="41" width="375" height="88"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="88" id="6Lk-OO-MsA"/>
|
||||
</constraints>
|
||||
@@ -521,6 +468,7 @@ World</string>
|
||||
<constraint firstItem="Jvb-r8-XrY" firstAttribute="top" secondItem="cQ2-Jd-pRK" secondAttribute="top" id="Urh-Qr-vrS"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<inset key="separatorInset" minX="1000" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
@@ -575,7 +523,7 @@ World</string>
|
||||
<edgeInsets key="layoutMargins" top="0.0" left="0.0" bottom="0.0" right="0.0"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" systemColor="tertiarySystemBackgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="xnC-tS-ZdV" firstAttribute="leading" secondItem="c7x-ee-3HH" secondAttribute="leading" constant="20" id="LO8-Au-SYF"/>
|
||||
<constraint firstItem="c7x-ee-3HH" firstAttribute="bottom" secondItem="xnC-tS-ZdV" secondAttribute="bottom" constant="10" id="NZ9-iG-E10"/>
|
||||
@@ -610,12 +558,12 @@ World</string>
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="40" minimumInteritemSpacing="40" id="63d-78-Y24">
|
||||
<size key="itemSize" width="335" height="300"/>
|
||||
<size key="itemSize" width="375" height="300"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="20" minY="40" maxX="20" maxY="13"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="40" maxX="0.0" maxY="13"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells/>
|
||||
<connections>
|
||||
@@ -632,7 +580,7 @@ World</string>
|
||||
<!--Browse-->
|
||||
<scene sceneID="VHa-uP-bFU">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" sceneMemberID="viewController">
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||
@@ -652,7 +600,7 @@ World</string>
|
||||
<!--My Apps-->
|
||||
<scene sceneID="nhh-BJ-XiT">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="3Ew-ox-i4n" sceneMemberID="viewController">
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y">
|
||||
<color key="badgeColor" name="Primary"/>
|
||||
</tabBarItem>
|
||||
@@ -677,112 +625,102 @@ World</string>
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="SB5-U0-jyy">
|
||||
<size key="itemSize" width="375" height="60"/>
|
||||
<size key="headerReferenceSize" width="50" height="50"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="50" height="60.5"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="AppCell" id="kMp-ym-2yu" customClass="InstalledAppCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="AppCell" id="kMp-ym-2yu" customClass="InstalledAppCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="50" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d6d-uV-GFi" userLabel="App Info">
|
||||
<rect key="frame" x="20" y="0.0" width="335" height="60"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="H12-ip-Bbl" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="60" id="SOy-Xe-y2x"/>
|
||||
<constraint firstAttribute="width" secondItem="H12-ip-Bbl" secondAttribute="height" multiplier="1:1" id="ZIR-f8-Jc4"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="7iy-Zp-LEj">
|
||||
<rect key="frame" x="71" y="12" width="203" height="36"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="MRz-3W-aTM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="85" height="18"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="Short" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Nhl-6I-9gW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="38" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="mtL-iA-JnD">
|
||||
<rect key="frame" x="44" y="0.0" width="41" height="18"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Hp4-uP-55T">
|
||||
<rect key="frame" x="0.0" y="20" width="62" height="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dh4-fU-DFx" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="285" y="15.5" width="50" height="29"/>
|
||||
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
|
||||
<state key="normal" title="7 DAYS"/>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Expires in" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4Kc-4f-KYr">
|
||||
<rect key="frame" x="306.5" y="0.5" width="47" height="12"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="10"/>
|
||||
<color key="textColor" red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.45000000000000001" colorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="0.0" width="359" height="60"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
</view>
|
||||
</subviews>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstItem="4Kc-4f-KYr" firstAttribute="centerX" secondItem="dh4-fU-DFx" secondAttribute="centerX" id="9Uf-Qu-bhZ"/>
|
||||
<constraint firstItem="d6d-uV-GFi" firstAttribute="leading" secondItem="kMp-ym-2yu" secondAttribute="leading" constant="20" id="fV7-0C-Hop"/>
|
||||
<constraint firstItem="d6d-uV-GFi" firstAttribute="top" secondItem="kMp-ym-2yu" secondAttribute="top" id="rCI-7z-0mR"/>
|
||||
<constraint firstItem="dh4-fU-DFx" firstAttribute="top" secondItem="4Kc-4f-KYr" secondAttribute="bottom" constant="3" id="rmM-9v-G5C"/>
|
||||
<constraint firstAttribute="trailing" secondItem="d6d-uV-GFi" secondAttribute="trailing" constant="20" id="s7H-ei-AEn"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="mos-e4-dQ7" secondAttribute="trailing" id="TKN-0r-5ON"/>
|
||||
<constraint firstItem="mos-e4-dQ7" firstAttribute="top" secondItem="kMp-ym-2yu" secondAttribute="top" id="TUp-Xe-CHP"/>
|
||||
<constraint firstAttribute="bottom" secondItem="mos-e4-dQ7" secondAttribute="bottom" id="gO1-mC-cTz"/>
|
||||
<constraint firstItem="mos-e4-dQ7" firstAttribute="leading" secondItem="kMp-ym-2yu" secondAttribute="leadingMargin" id="i49-Gc-w7s"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="appIconImageView" destination="H12-ip-Bbl" id="61F-4i-4Q3"/>
|
||||
<outlet property="betaBadgeView" destination="mtL-iA-JnD" id="v8W-bc-EB7"/>
|
||||
<outlet property="developerLabel" destination="Hp4-uP-55T" id="Cqx-3O-knq"/>
|
||||
<outlet property="nameLabel" destination="Nhl-6I-9gW" id="lzd-pp-PEQ"/>
|
||||
<outlet property="refreshButton" destination="dh4-fU-DFx" id="KWX-9y-2w8"/>
|
||||
<outlet property="bannerView" destination="mos-e4-dQ7" id="z01-3x-alE"/>
|
||||
<segue destination="0V6-N4-hTO" kind="show" identifier="showApp" id="cnd-KK-o60">
|
||||
<segue key="commit" inheritsFrom="parent" id="YdR-Ct-SlK"/>
|
||||
<segue key="preview" inheritsFrom="commit" id="GSg-SY-gai"/>
|
||||
</segue>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="NoUpdatesCell" id="h0f-XI-UA5">
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="NoUpdatesCell" id="h0f-XI-UA5" customClass="NoUpdatesCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="125" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No Updates Available" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z04-yg-x1t">
|
||||
<rect key="frame" x="104" y="20" width="167" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
|
||||
<color key="textColor" name="Primary"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7iO-O4-Mr9">
|
||||
<rect key="frame" x="8" y="0.0" width="359" height="60"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="d2X-wj-EhR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="359" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="zAy-K2-jA4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="359" height="60"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="F8U-ab-fOM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="359" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No Updates Available" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z04-yg-x1t">
|
||||
<rect key="frame" x="96" y="20" width="167" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
|
||||
<color key="textColor" name="Primary"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="z04-yg-x1t" firstAttribute="centerY" secondItem="F8U-ab-fOM" secondAttribute="centerY" id="9w9-Z0-jZl"/>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="z04-yg-x1t" secondAttribute="bottom" constant="10" id="IWL-Ei-QC2"/>
|
||||
<constraint firstItem="z04-yg-x1t" firstAttribute="top" relation="greaterThanOrEqual" secondItem="F8U-ab-fOM" secondAttribute="top" constant="10" id="fLp-au-PLf"/>
|
||||
<constraint firstItem="z04-yg-x1t" firstAttribute="centerX" secondItem="F8U-ab-fOM" secondAttribute="centerX" id="fiy-Zt-GmB"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<vibrancyEffect style="secondaryLabel">
|
||||
<blurEffect style="systemChromeMaterial"/>
|
||||
</vibrancyEffect>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" name="BlurTint"/>
|
||||
<constraints>
|
||||
<constraint firstItem="zAy-K2-jA4" firstAttribute="top" secondItem="d2X-wj-EhR" secondAttribute="top" id="3GP-KH-ao8"/>
|
||||
<constraint firstAttribute="trailing" secondItem="zAy-K2-jA4" secondAttribute="trailing" id="H29-aK-27e"/>
|
||||
<constraint firstAttribute="bottom" secondItem="zAy-K2-jA4" secondAttribute="bottom" id="Ha4-Od-VHk"/>
|
||||
<constraint firstItem="zAy-K2-jA4" firstAttribute="leading" secondItem="d2X-wj-EhR" secondAttribute="leading" id="rmG-C1-DoK"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<blurEffect style="systemChromeMaterial"/>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstItem="z04-yg-x1t" firstAttribute="centerY" secondItem="h0f-XI-UA5" secondAttribute="centerY" id="3dw-fe-ACP"/>
|
||||
<constraint firstItem="z04-yg-x1t" firstAttribute="centerX" secondItem="h0f-XI-UA5" secondAttribute="centerX" id="AIh-kx-SmK"/>
|
||||
<constraint firstItem="z04-yg-x1t" firstAttribute="top" relation="greaterThanOrEqual" secondItem="h0f-XI-UA5" secondAttribute="top" constant="10" id="QwS-y9-ahl"/>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="z04-yg-x1t" secondAttribute="bottom" constant="10" id="uQI-7x-E3b"/>
|
||||
<constraint firstItem="7iO-O4-Mr9" firstAttribute="leading" secondItem="h0f-XI-UA5" secondAttribute="leadingMargin" id="4Kn-tp-E7l"/>
|
||||
<constraint firstItem="7iO-O4-Mr9" firstAttribute="top" secondItem="h0f-XI-UA5" secondAttribute="top" id="Cxd-IB-cmI"/>
|
||||
<constraint firstAttribute="bottom" secondItem="7iO-O4-Mr9" secondAttribute="bottom" id="Xk3-SQ-iHD"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="7iO-O4-Mr9" secondAttribute="trailing" id="ZwB-wX-siW"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="blurView" destination="7iO-O4-Mr9" id="kQ4-9N-nnv"/>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsHeader" id="Crb-NU-1Ye" customClass="InstalledAppsCollectionHeaderView" customModule="AltStore" customModuleProvider="target">
|
||||
@@ -812,6 +750,40 @@ World</string>
|
||||
<outlet property="textLabel" destination="BDU-hM-rro" id="CQM-8K-bcH"/>
|
||||
</connections>
|
||||
</collectionReusableView>
|
||||
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsFooter" id="HYs-co-nJZ" customClass="InstalledAppsCollectionFooterView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="185" width="375" height="60.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="900" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="GFQ-Wy-Qhy">
|
||||
<rect key="frame" x="138.5" y="0.0" width="98" height="52.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="5/10 App IDs" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LLv-8I-6Of">
|
||||
<rect key="frame" x="0.0" y="0.0" width="98" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NHb-0F-cHZ">
|
||||
<rect key="frame" x="0.0" y="20.5" width="98" height="32"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<state key="normal" title="View App IDs"/>
|
||||
<connections>
|
||||
<segue destination="IXk-qg-mFJ" kind="presentation" identifier="showAppIDs" id="yZB-Fh-cTL"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="GFQ-Wy-Qhy" secondAttribute="bottom" constant="8" id="HGl-P6-G2v"/>
|
||||
<constraint firstItem="GFQ-Wy-Qhy" firstAttribute="top" secondItem="HYs-co-nJZ" secondAttribute="top" id="gg9-XU-2ej"/>
|
||||
<constraint firstItem="GFQ-Wy-Qhy" firstAttribute="centerX" secondItem="HYs-co-nJZ" secondAttribute="centerX" id="vyo-h4-yD9"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="button" destination="NHb-0F-cHZ" id="wOh-Ee-jhN"/>
|
||||
<outlet property="textLabel" destination="LLv-8I-6Of" id="t2D-f1-5pC"/>
|
||||
</connections>
|
||||
</collectionReusableView>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="hv7-Ar-voT" id="YOx-f4-chF"/>
|
||||
<outlet property="delegate" destination="hv7-Ar-voT" id="1PN-pf-cZK"/>
|
||||
@@ -830,12 +802,120 @@ World</string>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="kiO-UO-esV" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1730" y="717"/>
|
||||
<point key="canvasLocation" x="1728.8" y="716.49175412293857"/>
|
||||
</scene>
|
||||
<!--App IDs-->
|
||||
<scene sceneID="kvf-US-rRe">
|
||||
<objects>
|
||||
<collectionViewController title="App IDs" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="Wzt-qc-XG8">
|
||||
<size key="itemSize" width="375" height="80"/>
|
||||
<size key="headerReferenceSize" width="50" height="60"/>
|
||||
<size key="footerReferenceSize" width="50" height="50"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XWu-DU-xbh" customClass="AppIDCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="70" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
</view>
|
||||
</subviews>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="1w8-fI-98T" secondAttribute="trailing" id="0bS-49-dqo"/>
|
||||
<constraint firstAttribute="bottom" secondItem="1w8-fI-98T" secondAttribute="bottom" id="Bif-xB-0gt"/>
|
||||
<constraint firstItem="1w8-fI-98T" firstAttribute="top" secondItem="XWu-DU-xbh" secondAttribute="top" id="aEf-KK-MHU"/>
|
||||
<constraint firstItem="1w8-fI-98T" firstAttribute="leading" secondItem="XWu-DU-xbh" secondAttribute="leadingMargin" id="mFW-ti-cVB"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="bannerView" destination="1w8-fI-98T" id="OH8-L9-TZn"/>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="AppIDsCollectionReusableView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="App IDs Description" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="83Z-Ih-nOW">
|
||||
<rect key="frame" x="8" y="14" width="359" height="31"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
|
||||
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="83Z-Ih-nOW" secondAttribute="bottom" constant="15" id="CQA-og-LZ2"/>
|
||||
<constraint firstItem="83Z-Ih-nOW" firstAttribute="top" secondItem="th0-G6-bRt" secondAttribute="top" constant="14" id="e0J-MA-eH5"/>
|
||||
<constraint firstAttribute="leadingMargin" secondItem="83Z-Ih-nOW" secondAttribute="leading" id="nGf-Rh-mnk"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="83Z-Ih-nOW" secondAttribute="trailing" id="sYg-nT-ror"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
|
||||
</connections>
|
||||
</collectionReusableView>
|
||||
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="AppIDsCollectionReusableView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="170" width="375" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="10 App IDs" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Zna-7n-kBz">
|
||||
<rect key="frame" x="146" y="0.0" width="83" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Zna-7n-kBz" firstAttribute="centerX" secondItem="xMh-lD-r6C" secondAttribute="centerX" id="7RS-ua-XzZ"/>
|
||||
<constraint firstItem="Zna-7n-kBz" firstAttribute="top" secondItem="xMh-lD-r6C" secondAttribute="top" id="RvY-z8-XI6"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="textLabel" destination="Zna-7n-kBz" id="LK5-BR-skx"/>
|
||||
</connections>
|
||||
</collectionReusableView>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="y1A-Nm-mw7" id="U8O-CF-Jhv"/>
|
||||
<outlet property="delegate" destination="y1A-Nm-mw7" id="a8i-FA-aUq"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
|
||||
<barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
|
||||
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
|
||||
<rect key="frame" x="16" y="7" width="83" height="42"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</view>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="Ekd-oC-gOr">
|
||||
<connections>
|
||||
<segue destination="eS1-sQ-VUA" kind="unwind" unwindAction="unwindToMyAppsViewController:" id="VHS-kt-woS"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="activityIndicatorBarButtonItem" destination="Aqs-QK-Ups" id="2I7-rT-muy"/>
|
||||
</connections>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Lvd-jC-AZO" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<exit id="eS1-sQ-VUA" userLabel="Exit" sceneMemberID="exit"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3301.5999999999999" y="715.59220389805103"/>
|
||||
</scene>
|
||||
<!--News-->
|
||||
<scene sceneID="BV8-6J-nIv">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" sceneMemberID="viewController">
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||
@@ -852,20 +932,43 @@ World</string>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="962" y="-752"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="1Gj-mS-BaN">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="y1A-Nm-mw7" kind="relationship" relationship="rootViewController" id="ZYf-6x-9a0"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="3LN-mt-qWn" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2526" y="731"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Back" width="18" height="18"/>
|
||||
<image name="BetaBadge" width="41" height="17"/>
|
||||
<image name="Browse" width="20" height="20"/>
|
||||
<image name="MyApps" width="20" height="20"/>
|
||||
<image name="News" width="19" height="20"/>
|
||||
<image name="Settings" width="20" height="20"/>
|
||||
<namedColor name="Background">
|
||||
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="BlurTint">
|
||||
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<namedColor name="Primary">
|
||||
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="dzt-2e-VM9"/>
|
||||
<segue reference="cnd-KK-o60"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<color key="tintColor" name="Primary"/>
|
||||
</document>
|
||||
|
||||
@@ -20,40 +20,24 @@ import Nuke
|
||||
}
|
||||
}
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
@IBOutlet var nameLabel: UILabel!
|
||||
@IBOutlet var developerLabel: UILabel!
|
||||
@IBOutlet var appIconImageView: UIImageView!
|
||||
@IBOutlet var actionButton: PillButton!
|
||||
|
||||
@IBOutlet var bannerView: AppBannerView!
|
||||
@IBOutlet var subtitleLabel: UILabel!
|
||||
|
||||
@IBOutlet var screenshotsCollectionView: UICollectionView!
|
||||
@IBOutlet var betaBadgeView: UIImageView!
|
||||
|
||||
@IBOutlet private var screenshotsContentView: UIView!
|
||||
@IBOutlet private(set) var screenshotsCollectionView: UICollectionView!
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
// Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷♂️.
|
||||
self.screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
self.screenshotsCollectionView.delegate = self
|
||||
self.screenshotsCollectionView.dataSource = self.dataSource
|
||||
self.screenshotsCollectionView.prefetchDataSource = self.dataSource
|
||||
|
||||
self.screenshotsContentView.layer.cornerRadius = 20
|
||||
self.screenshotsContentView.layer.masksToBounds = true
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func tintColorDidChange()
|
||||
{
|
||||
super.tintColorDidChange()
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,12 +80,6 @@ private extension BrowseCollectionViewCell
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
private func update()
|
||||
{
|
||||
self.subtitleLabel.textColor = self.tintColor
|
||||
self.screenshotsContentView.backgroundColor = self.tintColor.withAlphaComponent(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout
|
||||
|
||||
@@ -1,131 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="Y3g-Md-6xH" userLabel="App Info">
|
||||
<rect key="frame" x="20" y="20" width="335" height="79"/>
|
||||
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="369"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="F2j-pX-09A" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="7" width="65" height="65"/>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="F2j-pX-09A" secondAttribute="height" multiplier="1:1" id="c2j-8O-Diw"/>
|
||||
<constraint firstAttribute="height" constant="65" id="ufl-3d-nkT"/>
|
||||
<constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="zkp-KH-OyV">
|
||||
<rect key="frame" x="76" y="21" width="176" height="37"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Ykl-yo-ncv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="127.5" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="xni-8I-ewW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="80.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="5gN-I2-QOB">
|
||||
<rect key="frame" x="86.5" y="0.0" width="41" height="20.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="B5S-HI-tWJ">
|
||||
<rect key="frame" x="0.0" y="22.5" width="57.5" height="14.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DeC-Y2-fvR" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="263" y="24" width="72" height="31"/>
|
||||
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="72" id="X7D-DN-WnD"/>
|
||||
<constraint firstAttribute="height" constant="31" id="svo-Sc-wpR"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
|
||||
<state key="normal" title="OPEN"/>
|
||||
</button>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Classic Nintendo games in your pocket." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Imx-Le-bcy">
|
||||
<rect key="frame" x="0.0" y="103" width="343" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" scrollEnabled="NO" contentInsetAdjustmentBehavior="never" dataMode="none" translatesAutoresizingMaskIntoConstraints="NO" id="RFs-qp-Ca4">
|
||||
<rect key="frame" x="0.0" y="135" width="343" height="234"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="15" id="jH9-Jo-IHA">
|
||||
<size key="itemSize" width="120" height="213"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="8" minY="0.0" maxX="8" maxY="0.0"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells/>
|
||||
</collectionView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="w1r-LJ-TDs" userLabel="Screenshots">
|
||||
<rect key="frame" x="15" y="114" width="345" height="266"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="hRR-84-Owd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="345" height="266"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Classic Nintendo games in your pocket." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Imx-Le-bcy">
|
||||
<rect key="frame" x="20" y="15" width="305" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" red="1" green="0.14901960780000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" contentInsetAdjustmentBehavior="never" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="RFs-qp-Ca4">
|
||||
<rect key="frame" x="20" y="47" width="305" height="185"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="10" id="jH9-Jo-IHA">
|
||||
<size key="itemSize" width="120" height="213"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells/>
|
||||
</collectionView>
|
||||
</subviews>
|
||||
<edgeInsets key="layoutMargins" top="15" left="20" bottom="20" right="20"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="0.14901960780000001" blue="0.0" alpha="0.050000000000000003" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="hRR-84-Owd" firstAttribute="leading" secondItem="w1r-LJ-TDs" secondAttribute="leading" id="3us-zR-peW"/>
|
||||
<constraint firstItem="hRR-84-Owd" firstAttribute="top" secondItem="w1r-LJ-TDs" secondAttribute="top" id="HWW-aS-Scd"/>
|
||||
<constraint firstAttribute="trailing" secondItem="hRR-84-Owd" secondAttribute="trailing" id="lbU-TC-jhJ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="hRR-84-Owd" secondAttribute="bottom" id="nOI-Qj-lbm"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="w1r-LJ-TDs" secondAttribute="trailing" constant="15" id="4ns-Zq-D4j"/>
|
||||
<constraint firstItem="w1r-LJ-TDs" firstAttribute="leading" secondItem="ln4-pC-7KY" secondAttribute="leading" constant="15" id="G1K-up-08u"/>
|
||||
<constraint firstAttribute="bottom" secondItem="w1r-LJ-TDs" secondAttribute="bottom" constant="20" id="Kk0-dF-4OW"/>
|
||||
<constraint firstItem="Y3g-Md-6xH" firstAttribute="top" secondItem="ln4-pC-7KY" secondAttribute="top" constant="20" id="PRR-aX-AiM"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Y3g-Md-6xH" secondAttribute="trailing" constant="20" id="g1Q-lg-I9O"/>
|
||||
<constraint firstItem="w1r-LJ-TDs" firstAttribute="top" secondItem="Y3g-Md-6xH" secondAttribute="bottom" constant="15" id="i9W-bl-J9R"/>
|
||||
<constraint firstItem="Y3g-Md-6xH" firstAttribute="leading" secondItem="ln4-pC-7KY" secondAttribute="leading" constant="20" id="j6L-IY-ALs"/>
|
||||
<constraint firstItem="5gU-g3-Fsy" firstAttribute="top" secondItem="ln4-pC-7KY" secondAttribute="top" id="DnT-vq-BOc"/>
|
||||
<constraint firstItem="5gU-g3-Fsy" firstAttribute="leading" secondItem="ln4-pC-7KY" secondAttribute="leadingMargin" id="YPy-xL-iUn"/>
|
||||
<constraint firstAttribute="bottom" secondItem="5gU-g3-Fsy" secondAttribute="bottom" id="gRu-Hz-CNL"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="5gU-g3-Fsy" secondAttribute="trailing" id="vf4-ql-4Vq"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="btu-iP-81i"/>
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="DeC-Y2-fvR" id="VDk-4D-STy"/>
|
||||
<outlet property="appIconImageView" destination="F2j-pX-09A" id="COe-74-adn"/>
|
||||
<outlet property="betaBadgeView" destination="5gN-I2-QOB" id="hu7-Ax-Wbc"/>
|
||||
<outlet property="developerLabel" destination="B5S-HI-tWJ" id="QGh-1g-fFv"/>
|
||||
<outlet property="nameLabel" destination="xni-8I-ewW" id="V56-ZT-vFa"/>
|
||||
<outlet property="bannerView" destination="ziA-mP-AY2" id="yxo-ar-Cha"/>
|
||||
<outlet property="screenshotsCollectionView" destination="RFs-qp-Ca4" id="xfi-AN-l17"/>
|
||||
<outlet property="screenshotsContentView" destination="w1r-LJ-TDs" id="iWJ-52-rbA"/>
|
||||
<outlet property="subtitleLabel" destination="Imx-Le-bcy" id="JVW-ZZ-51O"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="BetaBadge" width="41" height="17"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -72,47 +72,49 @@ private extension BrowseViewController
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
||||
let cell = cell as! BrowseCollectionViewCell
|
||||
cell.nameLabel.text = app.name
|
||||
cell.developerLabel.text = app.developerName
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
cell.subtitleLabel.text = app.subtitle
|
||||
cell.imageURLs = Array(app.screenshotURLs.prefix(2))
|
||||
cell.appIconImageView.image = nil
|
||||
cell.appIconImageView.isIndicatingActivity = true
|
||||
cell.betaBadgeView.isHidden = !app.isBeta
|
||||
cell.bannerView.titleLabel.text = app.name
|
||||
cell.bannerView.subtitleLabel.text = app.developerName
|
||||
cell.bannerView.betaBadgeView.isHidden = !app.isBeta
|
||||
|
||||
cell.actionButton.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
cell.actionButton.activityIndicatorView.style = .white
|
||||
cell.bannerView.iconImageView.image = nil
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
|
||||
cell.bannerView.button.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
cell.bannerView.button.activityIndicatorView.style = .white
|
||||
|
||||
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
|
||||
// Otherwise, cell reuse can mess up some cached values.
|
||||
cell.actionButton.isIndicatingActivity = false
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
|
||||
let tintColor = app.tintColor ?? .altPrimary
|
||||
cell.tintColor = tintColor
|
||||
|
||||
if app.installedApp == nil
|
||||
{
|
||||
cell.actionButton.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
||||
cell.bannerView.button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
||||
|
||||
let progress = AppManager.shared.installationProgress(for: app)
|
||||
cell.actionButton.progress = progress
|
||||
cell.actionButton.isInverted = false
|
||||
cell.bannerView.button.progress = progress
|
||||
|
||||
if Date() < app.versionDate
|
||||
{
|
||||
cell.actionButton.countdownDate = app.versionDate
|
||||
cell.bannerView.button.countdownDate = app.versionDate
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.actionButton.countdownDate = nil
|
||||
cell.bannerView.button.countdownDate = nil
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.actionButton.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||
cell.actionButton.progress = nil
|
||||
cell.actionButton.isInverted = true
|
||||
cell.actionButton.countdownDate = nil
|
||||
cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||
cell.bannerView.button.progress = nil
|
||||
cell.bannerView.button.countdownDate = nil
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
|
||||
@@ -135,8 +137,8 @@ private extension BrowseViewController
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! BrowseCollectionViewCell
|
||||
cell.appIconImageView.isIndicatingActivity = false
|
||||
cell.appIconImageView.image = image
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
cell.bannerView.iconImageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
@@ -253,8 +255,8 @@ private extension BrowseViewController
|
||||
{
|
||||
case .failure(OperationError.cancelled): break // Ignore
|
||||
case .failure(let error):
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
|
||||
case .success: print("Installed app:", app.bundleIdentifier)
|
||||
}
|
||||
@@ -286,8 +288,8 @@ extension BrowseViewController: UICollectionViewDelegateFlowLayout
|
||||
let maxVisibleScreenshots = 2 as CGFloat
|
||||
let aspectRatio: CGFloat = 16.0 / 9.0
|
||||
|
||||
let layout = collectionViewLayout as! UICollectionViewFlowLayout
|
||||
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1))
|
||||
let layout = self.prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right
|
||||
|
||||
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
|
||||
|
||||
@@ -295,6 +297,8 @@ extension BrowseViewController: UICollectionViewDelegateFlowLayout
|
||||
widthConstraint.isActive = true
|
||||
defer { widthConstraint.isActive = false }
|
||||
|
||||
// Manually update cell width & layout so we can accurately calculate screenshot sizes.
|
||||
self.prototypeCell.frame.size.width = widthConstraint.constant
|
||||
self.prototypeCell.layoutIfNeeded()
|
||||
|
||||
let collectionViewWidth = self.prototypeCell.screenshotsCollectionView.bounds.width
|
||||
@@ -302,6 +306,7 @@ extension BrowseViewController: UICollectionViewDelegateFlowLayout
|
||||
let screenshotHeight = screenshotWidth * aspectRatio
|
||||
|
||||
let heightConstraint = self.prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
|
||||
heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error.
|
||||
heightConstraint.isActive = true
|
||||
defer { heightConstraint.isActive = false }
|
||||
|
||||
|
||||
@@ -11,16 +11,27 @@ import Roxas
|
||||
|
||||
class AppBannerView: RSTNibView
|
||||
{
|
||||
private var originalTintColor: UIColor?
|
||||
|
||||
@IBOutlet var titleLabel: UILabel!
|
||||
@IBOutlet var subtitleLabel: UILabel!
|
||||
@IBOutlet var iconImageView: AppIconImageView!
|
||||
@IBOutlet var button: PillButton!
|
||||
@IBOutlet var buttonLabel: UILabel!
|
||||
@IBOutlet var betaBadgeView: UIView!
|
||||
|
||||
@IBOutlet var backgroundEffectView: UIVisualEffectView!
|
||||
@IBOutlet private var vibrancyView: UIVisualEffectView!
|
||||
|
||||
override func tintColorDidChange()
|
||||
{
|
||||
super.tintColorDidChange()
|
||||
|
||||
if self.tintAdjustmentMode != .dimmed
|
||||
{
|
||||
self.originalTintColor = self.tintColor
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
@@ -32,9 +43,7 @@ private extension AppBannerView
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 22
|
||||
|
||||
self.subtitleLabel.textColor = self.tintColor
|
||||
self.button.tintColor = self.tintColor
|
||||
|
||||
self.backgroundColor = self.tintColor.withAlphaComponent(0.1)
|
||||
self.subtitleLabel.textColor = self.originalTintColor ?? self.tintColor
|
||||
self.backgroundEffectView.backgroundColor = self.originalTintColor ?? self.tintColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/>
|
||||
<outlet property="betaBadgeView" destination="qQl-Ez-zC5" id="6O1-Cx-7qz"/>
|
||||
<outlet property="button" destination="tVx-3G-dcu" id="joa-AH-syX"/>
|
||||
<outlet property="buttonLabel" destination="Yd9-jw-faD" id="o7g-Gb-CIt"/>
|
||||
<outlet property="iconImageView" destination="avS-dx-4iy" id="TQs-Ej-gin"/>
|
||||
<outlet property="subtitleLabel" destination="oN5-vu-Dnw" id="gA4-iJ-Tix"/>
|
||||
<outlet property="titleLabel" destination="mFe-zJ-eva" id="2OH-f8-cid"/>
|
||||
<outlet property="vibrancyView" destination="fC4-1V-iMn" id="PXE-2B-A7w"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
@@ -23,6 +25,15 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rZk-be-tiI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="b8k-up-HtI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" name="BlurTint"/>
|
||||
</view>
|
||||
<blurEffect style="systemChromeMaterial"/>
|
||||
</visualEffectView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||
<subviews>
|
||||
@@ -34,54 +45,97 @@
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="caL-vN-Svn">
|
||||
<rect key="frame" x="85" y="24" width="195" height="40.5"/>
|
||||
<rect key="frame" x="85" y="18" width="190" height="52"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="1Es-pv-zwd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="135" height="21.5"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="500" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="1Es-pv-zwd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="167" height="34"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="mFe-zJ-eva">
|
||||
<rect key="frame" x="0.0" y="0.0" width="88" height="21.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="79" height="34"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="NameLabel"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="qQl-Ez-zC5">
|
||||
<rect key="frame" x="94" y="0.0" width="41" height="21.5"/>
|
||||
<rect key="frame" x="85" y="0.0" width="82" height="34"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="Beta Badge">
|
||||
<accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="oN5-vu-Dnw">
|
||||
<rect key="frame" x="0.0" y="23.5" width="66" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fC4-1V-iMn">
|
||||
<rect key="frame" x="0.0" y="36" width="190" height="16"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="LQh-pN-ePC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="750" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="oN5-vu-Dnw">
|
||||
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Developer"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="oN5-vu-Dnw" firstAttribute="top" secondItem="LQh-pN-ePC" secondAttribute="top" id="7RH-WP-LzL"/>
|
||||
<constraint firstItem="oN5-vu-Dnw" firstAttribute="leading" secondItem="LQh-pN-ePC" secondAttribute="leading" id="By8-cR-kTu"/>
|
||||
<constraint firstAttribute="trailing" secondItem="oN5-vu-Dnw" secondAttribute="trailing" id="Hiv-6y-XrH"/>
|
||||
<constraint firstAttribute="bottom" secondItem="oN5-vu-Dnw" secondAttribute="bottom" id="yc2-Dr-Qnv"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<vibrancyEffect style="secondaryLabel">
|
||||
<blurEffect style="systemChromeMaterial"/>
|
||||
</vibrancyEffect>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3ox-Q2-Rnd" userLabel="Button Stack View">
|
||||
<rect key="frame" x="286" y="28.5" width="77" height="31"/>
|
||||
<subviews>
|
||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Yd9-jw-faD">
|
||||
<rect key="frame" x="0.0" y="0.0" width="77" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="10"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
|
||||
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="31" id="Zwh-yQ-GTu"/>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="77" id="eGc-Dk-QbL"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
|
||||
<state key="normal" title="FREE"/>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="291" y="28.5" width="72" height="31"/>
|
||||
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="31" id="Zwh-yQ-GTu"/>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="72" id="eGc-Dk-QbL"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
|
||||
<state key="normal" title="FREE"/>
|
||||
</button>
|
||||
</subviews>
|
||||
<edgeInsets key="layoutMargins" top="14" left="14" bottom="14" right="12"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="d1T-UD-gWG" secondAttribute="bottom" id="B9e-Mf-cy5"/>
|
||||
<constraint firstAttribute="trailing" secondItem="d1T-UD-gWG" secondAttribute="trailing" id="HcT-2k-z0H"/>
|
||||
<constraint firstItem="d1T-UD-gWG" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="PIM-W5-dkh"/>
|
||||
<constraint firstItem="d1T-UD-gWG" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="RHn-ZK-jgl"/>
|
||||
<constraint firstItem="d1T-UD-gWG" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="5tv-QN-ZWU"/>
|
||||
<constraint firstItem="rZk-be-tiI" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="N6R-B2-Rie"/>
|
||||
<constraint firstAttribute="trailing" secondItem="d1T-UD-gWG" secondAttribute="trailing" id="mlG-w3-Ly6"/>
|
||||
<constraint firstAttribute="trailing" secondItem="rZk-be-tiI" secondAttribute="trailing" id="nEy-pm-Fcs"/>
|
||||
<constraint firstAttribute="bottom" secondItem="d1T-UD-gWG" secondAttribute="bottom" id="nJo-To-LmX"/>
|
||||
<constraint firstItem="rZk-be-tiI" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="pGD-Tl-U4c"/>
|
||||
<constraint firstItem="d1T-UD-gWG" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="q2p-0S-Nv5"/>
|
||||
<constraint firstAttribute="bottom" secondItem="rZk-be-tiI" secondAttribute="bottom" id="yk0-pw-joP"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="139.85507246376812" y="152.67857142857142"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="BetaBadge" width="41" height="17"/>
|
||||
<namedColor name="BlurTint">
|
||||
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -19,13 +19,16 @@ class AppIconImageView: UIImageView
|
||||
|
||||
self.backgroundColor = .white
|
||||
|
||||
self.layer.borderWidth = 0.5
|
||||
self.layer.borderColor = self.tintColor.cgColor
|
||||
|
||||
// Allows us to match system look for app icons.
|
||||
if self.layer.responds(to: Selector(("continuousCorners")))
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
self.layer.setValue(true, forKey: "continuousCorners")
|
||||
self.layer.cornerCurve = .continuous
|
||||
}
|
||||
else
|
||||
{
|
||||
if self.layer.responds(to: Selector(("continuousCorners")))
|
||||
{
|
||||
self.layer.setValue(true, forKey: "continuousCorners")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +40,4 @@ class AppIconImageView: UIImageView
|
||||
let radius = self.bounds.height / 5
|
||||
self.layer.cornerRadius = radius
|
||||
}
|
||||
|
||||
override func tintColorDidChange()
|
||||
{
|
||||
super.tintColorDidChange()
|
||||
|
||||
self.layer.borderColor = self.tintColor.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
20
AltStore/Components/ForwardingNavigationController.swift
Normal file
20
AltStore/Components/ForwardingNavigationController.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,68 @@ import KeychainAccess
|
||||
|
||||
import AltSign
|
||||
|
||||
@propertyWrapper
|
||||
struct KeychainItem<Value>
|
||||
{
|
||||
let key: String
|
||||
|
||||
var wrappedValue: Value? {
|
||||
get {
|
||||
switch Value.self
|
||||
{
|
||||
case is Data.Type: return try? Keychain.shared.keychain.getData(self.key) as? Value
|
||||
case is String.Type: return try? Keychain.shared.keychain.getString(self.key) as? Value
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
set {
|
||||
switch Value.self
|
||||
{
|
||||
case is Data.Type: Keychain.shared.keychain[data: self.key] = newValue as? Data
|
||||
case is String.Type: Keychain.shared.keychain[self.key] = newValue as? String
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(key: String)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
}
|
||||
|
||||
class Keychain
|
||||
{
|
||||
static let shared = Keychain()
|
||||
|
||||
private let keychain = KeychainAccess.Keychain(service: "com.rileytestut.AltStore").accessibility(.afterFirstUnlock).synchronizable(true)
|
||||
fileprivate let keychain = KeychainAccess.Keychain(service: "com.rileytestut.AltStore").accessibility(.afterFirstUnlock).synchronizable(true)
|
||||
|
||||
@KeychainItem(key: "appleIDEmailAddress")
|
||||
var appleIDEmailAddress: String?
|
||||
|
||||
@KeychainItem(key: "appleIDPassword")
|
||||
var appleIDPassword: String?
|
||||
|
||||
@KeychainItem(key: "signingCertificatePrivateKey")
|
||||
var signingCertificatePrivateKey: Data?
|
||||
|
||||
@KeychainItem(key: "signingCertificateSerialNumber")
|
||||
var signingCertificateSerialNumber: String?
|
||||
|
||||
@KeychainItem(key: "signingCertificate")
|
||||
var signingCertificate: Data?
|
||||
|
||||
@KeychainItem(key: "signingCertificatePassword")
|
||||
var signingCertificatePassword: String?
|
||||
|
||||
@KeychainItem(key: "patreonAccessToken")
|
||||
var patreonAccessToken: String?
|
||||
|
||||
@KeychainItem(key: "patreonRefreshToken")
|
||||
var patreonRefreshToken: String?
|
||||
|
||||
@KeychainItem(key: "patreonCreatorAccessToken")
|
||||
var patreonCreatorAccessToken: String?
|
||||
|
||||
private init()
|
||||
{
|
||||
@@ -29,66 +86,3 @@ class Keychain
|
||||
self.signingCertificateSerialNumber = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Keychain
|
||||
{
|
||||
var appleIDEmailAddress: String? {
|
||||
get {
|
||||
let emailAddress = try? self.keychain.get("appleIDEmailAddress")
|
||||
return emailAddress
|
||||
}
|
||||
set {
|
||||
self.keychain["appleIDEmailAddress"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var appleIDPassword: String? {
|
||||
get {
|
||||
let password = try? self.keychain.get("appleIDPassword")
|
||||
return password
|
||||
}
|
||||
set {
|
||||
self.keychain["appleIDPassword"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var signingCertificatePrivateKey: Data? {
|
||||
get {
|
||||
let privateKey = try? self.keychain.getData("signingCertificatePrivateKey")
|
||||
return privateKey
|
||||
}
|
||||
set {
|
||||
self.keychain[data: "signingCertificatePrivateKey"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var signingCertificateSerialNumber: String? {
|
||||
get {
|
||||
let serialNumber = try? self.keychain.get("signingCertificateSerialNumber")
|
||||
return serialNumber
|
||||
}
|
||||
set {
|
||||
self.keychain["signingCertificateSerialNumber"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var patreonAccessToken: String? {
|
||||
get {
|
||||
let accessToken = try? self.keychain.get("patreonAccessToken")
|
||||
return accessToken
|
||||
}
|
||||
set {
|
||||
self.keychain["patreonAccessToken"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var patreonRefreshToken: String? {
|
||||
get {
|
||||
let refreshToken = try? self.keychain.get("patreonRefreshToken")
|
||||
return refreshToken
|
||||
}
|
||||
set {
|
||||
self.keychain["patreonRefreshToken"] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,19 +32,52 @@ class NavigationBar: UINavigationBar
|
||||
|
||||
private func initialize()
|
||||
{
|
||||
self.shadowImage = UIImage()
|
||||
|
||||
if let tintColor = self.barTintColor
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
self.backgroundColorView.backgroundColor = tintColor
|
||||
let standardAppearance = UINavigationBarAppearance()
|
||||
standardAppearance.configureWithDefaultBackground()
|
||||
standardAppearance.shadowColor = nil
|
||||
|
||||
// Top = -50 to cover status bar area above navigation bar on any device.
|
||||
// Bottom = -1 to prevent a flickering gray line from appearing.
|
||||
self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
|
||||
let edgeAppearance = UINavigationBarAppearance()
|
||||
edgeAppearance.configureWithOpaqueBackground()
|
||||
edgeAppearance.backgroundColor = self.barTintColor
|
||||
edgeAppearance.shadowColor = nil
|
||||
|
||||
if let tintColor = self.barTintColor
|
||||
{
|
||||
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
|
||||
|
||||
standardAppearance.backgroundColor = tintColor
|
||||
standardAppearance.titleTextAttributes = textAttributes
|
||||
standardAppearance.largeTitleTextAttributes = textAttributes
|
||||
|
||||
edgeAppearance.titleTextAttributes = textAttributes
|
||||
edgeAppearance.largeTitleTextAttributes = textAttributes
|
||||
}
|
||||
else
|
||||
{
|
||||
standardAppearance.backgroundColor = nil
|
||||
}
|
||||
|
||||
self.scrollEdgeAppearance = edgeAppearance
|
||||
self.standardAppearance = standardAppearance
|
||||
}
|
||||
else
|
||||
{
|
||||
self.barTintColor = .white
|
||||
self.shadowImage = UIImage()
|
||||
|
||||
if let tintColor = self.barTintColor
|
||||
{
|
||||
self.backgroundColorView.backgroundColor = tintColor
|
||||
|
||||
// Top = -50 to cover status bar area above navigation bar on any device.
|
||||
// Bottom = -1 to prevent a flickering gray line from appearing.
|
||||
self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.barTintColor = .white
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,15 @@ import UIKit
|
||||
class PillButton: UIButton
|
||||
{
|
||||
var progress: Progress? {
|
||||
didSet {
|
||||
didSet {
|
||||
self.progressView.progress = Float(self.progress?.fractionCompleted ?? 0)
|
||||
self.progressView.observedProgress = self.progress
|
||||
|
||||
let isUserInteractionEnabled = self.isUserInteractionEnabled
|
||||
self.isIndicatingActivity = (self.progress != nil)
|
||||
self.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +32,6 @@ class PillButton: UIButton
|
||||
}
|
||||
}
|
||||
|
||||
var isInverted: Bool = false {
|
||||
didSet {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
var countdownDate: Date? {
|
||||
didSet {
|
||||
self.isEnabled = (self.countdownDate == nil)
|
||||
@@ -120,18 +116,18 @@ private extension PillButton
|
||||
{
|
||||
func update()
|
||||
{
|
||||
if self.isInverted
|
||||
if self.progress == nil
|
||||
{
|
||||
self.setTitleColor(.white, for: .normal)
|
||||
self.backgroundColor = self.tintColor
|
||||
self.progressView.progressTintColor = self.tintColor.withAlphaComponent(0.15)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.setTitleColor(self.tintColor, for: .normal)
|
||||
self.backgroundColor = self.tintColor.withAlphaComponent(0.15)
|
||||
self.progressView.progressTintColor = self.tintColor
|
||||
}
|
||||
|
||||
self.progressView.progressTintColor = self.tintColor
|
||||
}
|
||||
|
||||
@objc func updateCountdown()
|
||||
|
||||
@@ -10,11 +10,35 @@ import Roxas
|
||||
|
||||
class ToastView: RSTToastView
|
||||
{
|
||||
var preferredDuration: TimeInterval
|
||||
|
||||
override init(text: String, detailText detailedText: String?)
|
||||
{
|
||||
if detailedText == nil
|
||||
{
|
||||
self.preferredDuration = 2.0
|
||||
}
|
||||
else
|
||||
{
|
||||
self.preferredDuration = 8.0
|
||||
}
|
||||
|
||||
super.init(text: text, detailText: detailedText)
|
||||
|
||||
self.layoutMargins = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
convenience init(error: Error)
|
||||
{
|
||||
if let error = error as? LocalizedError
|
||||
{
|
||||
self.init(text: error.localizedDescription, detailText: error.recoverySuggestion ?? error.failureReason)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.init(text: error.localizedDescription, detailText: nil)
|
||||
}
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
@@ -27,4 +51,14 @@ class ToastView: RSTToastView
|
||||
|
||||
self.layer.cornerRadius = 16
|
||||
}
|
||||
|
||||
func show(in viewController: UIViewController)
|
||||
{
|
||||
self.show(in: viewController.navigationController?.view ?? viewController.view, duration: self.preferredDuration)
|
||||
}
|
||||
|
||||
override func show(in view: UIView)
|
||||
{
|
||||
self.show(in: view, duration: self.preferredDuration)
|
||||
}
|
||||
}
|
||||
|
||||
29
AltStore/Extensions/ALTApplication+AppExtensions.swift
Normal file
29
AltStore/Extensions/ALTApplication+AppExtensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ extension UserDefaults
|
||||
@NSManaged var isDebugModeEnabled: Bool
|
||||
@NSManaged var presentedLaunchReminderNotification: Bool
|
||||
|
||||
@NSManaged var legacySideloadedApps: [String]?
|
||||
|
||||
func registerDefaults()
|
||||
{
|
||||
self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true])
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ALTDeviceID</key>
|
||||
<string>1c3416b7b0ab68773e6e7eb7f0d110f7c9353acc</string>
|
||||
<string>00008030-001948590202802E</string>
|
||||
<key>ALTServerID</key>
|
||||
<string>1AAAB6FD-E8CE-4B70-8F26-4073215C03B0</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
@@ -36,7 +36,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.1</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -120,7 +120,5 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -11,6 +11,10 @@ import Roxas
|
||||
|
||||
class LaunchViewController: RSTLaunchViewController
|
||||
{
|
||||
private var didFinishLaunching = false
|
||||
|
||||
private var destinationViewController: UIViewController!
|
||||
|
||||
override var launchConditions: [RSTLaunchCondition] {
|
||||
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
|
||||
DatabaseManager.shared.start(completionHandler: completionHandler)
|
||||
@@ -18,6 +22,22 @@ class LaunchViewController: RSTLaunchViewController
|
||||
|
||||
return [isDatabaseStarted]
|
||||
}
|
||||
|
||||
override var childForStatusBarStyle: UIViewController? {
|
||||
return self.children.first
|
||||
}
|
||||
|
||||
override var childForStatusBarHidden: UIViewController? {
|
||||
return self.children.first
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
// Create destinationViewController now so view controllers can register for receiving Notifications.
|
||||
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||
}
|
||||
}
|
||||
|
||||
extension LaunchViewController
|
||||
@@ -44,9 +64,23 @@ extension LaunchViewController
|
||||
{
|
||||
super.finishLaunching()
|
||||
|
||||
guard !self.didFinishLaunching else { return }
|
||||
|
||||
AppManager.shared.update()
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
|
||||
self.performSegue(withIdentifier: "finishLaunching", sender: nil)
|
||||
// Add view controller as child (rather than presenting modally)
|
||||
// so tint adjustment + card presentations works correctly.
|
||||
self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
|
||||
self.destinationViewController.view.alpha = 0.0
|
||||
self.addChild(self.destinationViewController)
|
||||
self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
|
||||
self.destinationViewController.didMove(toParent: self)
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.destinationViewController.view.alpha = 1.0
|
||||
}
|
||||
|
||||
self.didFinishLaunching = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import MobileCoreServices
|
||||
|
||||
import AltSign
|
||||
import AltKit
|
||||
@@ -20,6 +21,8 @@ extension AppManager
|
||||
static let didFetchSourceNotification = Notification.Name("com.altstore.AppManager.didFetchSource")
|
||||
|
||||
static let expirationWarningNotificationID = "altstore-expiration-warning"
|
||||
|
||||
static let whitelistedSideloadingBundleIDs: Set<String> = ["science.xnu.undecimus"]
|
||||
}
|
||||
|
||||
class AppManager
|
||||
@@ -55,16 +58,32 @@ extension AppManager
|
||||
do
|
||||
{
|
||||
let installedApps = try context.fetch(fetchRequest)
|
||||
for app in installedApps where app.storeApp != nil
|
||||
|
||||
if UserDefaults.standard.legacySideloadedApps == nil
|
||||
{
|
||||
// First time updating apps since updating AltStore to use custom UTIs,
|
||||
// so cache all existing apps temporarily to prevent us from accidentally
|
||||
// deleting them due to their custom UTI not existing (yet).
|
||||
let apps = installedApps.map { $0.bundleIdentifier }
|
||||
UserDefaults.standard.legacySideloadedApps = apps
|
||||
}
|
||||
|
||||
let legacySideloadedApps = Set(UserDefaults.standard.legacySideloadedApps ?? [])
|
||||
|
||||
for app in installedApps
|
||||
{
|
||||
let uti = UTTypeCopyDeclaration(app.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
||||
|
||||
if app.bundleIdentifier == StoreApp.altstoreAppID
|
||||
{
|
||||
self.scheduleExpirationWarningLocalNotification(for: app)
|
||||
}
|
||||
else
|
||||
{
|
||||
if !UIApplication.shared.canOpenURL(app.openAppURL)
|
||||
if uti == nil && !legacySideloadedApps.contains(app.bundleIdentifier)
|
||||
{
|
||||
// This UTI is not declared by any apps, which means this app has been deleted by the user.
|
||||
// This app is also not a legacy sideloaded app, so we can assume it's fine to delete it.
|
||||
context.delete(app)
|
||||
}
|
||||
}
|
||||
@@ -80,13 +99,37 @@ extension AppManager
|
||||
#endif
|
||||
}
|
||||
|
||||
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTSigner, Error>) -> Void)
|
||||
@discardableResult
|
||||
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTSigner, ALTAppleAPISession), Error>) -> Void) -> OperationGroup
|
||||
{
|
||||
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
||||
let group = OperationGroup()
|
||||
|
||||
let findServerOperation = FindServerOperation(group: group)
|
||||
findServerOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): group.error = error
|
||||
case .success(let server): group.server = server
|
||||
}
|
||||
}
|
||||
self.operationQueue.addOperation(findServerOperation)
|
||||
|
||||
let authenticationOperation = AuthenticationOperation(group: group, presentingViewController: presentingViewController)
|
||||
authenticationOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): group.error = error
|
||||
case .success(let signer, let session):
|
||||
group.signer = signer
|
||||
group.session = session
|
||||
}
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
authenticationOperation.addDependency(findServerOperation)
|
||||
self.operationQueue.addOperation(authenticationOperation)
|
||||
|
||||
return group
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +157,23 @@ extension AppManager
|
||||
self.operationQueue.addOperation(fetchSourceOperation)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
|
||||
{
|
||||
var group: OperationGroup!
|
||||
group = self.authenticate(presentingViewController: nil) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
|
||||
fetchAppIDsOperation.resultHandler = completionHandler
|
||||
self.operationQueue.addOperation(fetchAppIDsOperation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppManager
|
||||
@@ -149,7 +209,7 @@ extension AppManager
|
||||
|
||||
func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup
|
||||
{
|
||||
let apps = installedApps.filter { self.refreshProgress(for: $0) == nil }
|
||||
let apps = installedApps.filter { self.refreshProgress(for: $0) == nil || self.refreshProgress(for: $0)?.isCancelled == true }
|
||||
|
||||
let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group)
|
||||
|
||||
@@ -183,18 +243,6 @@ private extension AppManager
|
||||
let group = group ?? OperationGroup()
|
||||
var operations = [Operation]()
|
||||
|
||||
|
||||
/* Authenticate */
|
||||
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
||||
authenticationOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): group.error = error
|
||||
case .success(let signer): group.signer = signer
|
||||
}
|
||||
}
|
||||
operations.append(authenticationOperation)
|
||||
|
||||
/* Find Server */
|
||||
let findServerOperation = FindServerOperation(group: group)
|
||||
findServerOperation.resultHandler = { (result) in
|
||||
@@ -204,9 +252,55 @@ private extension AppManager
|
||||
case .success(let server): group.server = server
|
||||
}
|
||||
}
|
||||
findServerOperation.addDependency(authenticationOperation)
|
||||
operations.append(findServerOperation)
|
||||
|
||||
let authenticationOperation: AuthenticationOperation?
|
||||
|
||||
if group.signer == nil || group.session == nil
|
||||
{
|
||||
/* Authenticate */
|
||||
let operation = AuthenticationOperation(group: group, presentingViewController: presentingViewController)
|
||||
operation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): group.error = error
|
||||
case .success(let signer, let session):
|
||||
group.signer = signer
|
||||
group.session = session
|
||||
}
|
||||
}
|
||||
operations.append(operation)
|
||||
operation.addDependency(findServerOperation)
|
||||
|
||||
authenticationOperation = operation
|
||||
}
|
||||
else
|
||||
{
|
||||
authenticationOperation = nil
|
||||
}
|
||||
|
||||
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(group: group)
|
||||
refreshAnisetteDataOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): group.error = error
|
||||
case .success(let anisetteData): group.session?.anisetteData = anisetteData
|
||||
}
|
||||
}
|
||||
refreshAnisetteDataOperation.addDependency(authenticationOperation ?? findServerOperation)
|
||||
operations.append(refreshAnisetteDataOperation)
|
||||
|
||||
/* Prepare Developer Account */
|
||||
let prepareDeveloperAccountOperation = PrepareDeveloperAccountOperation(group: group)
|
||||
prepareDeveloperAccountOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): group.error = error
|
||||
case .success: break
|
||||
}
|
||||
}
|
||||
prepareDeveloperAccountOperation.addDependency(refreshAnisetteDataOperation)
|
||||
operations.append(prepareDeveloperAccountOperation)
|
||||
|
||||
for app in apps
|
||||
{
|
||||
@@ -220,7 +314,7 @@ private extension AppManager
|
||||
guard let resignedApp = self.process(result, context: context) else { return }
|
||||
context.resignedApp = resignedApp
|
||||
}
|
||||
resignAppOperation.addDependency(findServerOperation)
|
||||
resignAppOperation.addDependency(prepareDeveloperAccountOperation)
|
||||
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
|
||||
operations.append(resignAppOperation)
|
||||
|
||||
@@ -267,8 +361,8 @@ private extension AppManager
|
||||
/* Send */
|
||||
let sendAppOperation = SendAppOperation(context: context)
|
||||
sendAppOperation.resultHandler = { (result) in
|
||||
guard let connection = self.process(result, context: context) else { return }
|
||||
context.connection = connection
|
||||
guard let installationConnection = self.process(result, context: context) else { return }
|
||||
context.installationConnection = installationConnection
|
||||
}
|
||||
progress.addChild(sendAppOperation.progress, withPendingUnitCount: 10)
|
||||
sendAppOperation.addDependency(resignAppOperation)
|
||||
@@ -312,6 +406,28 @@ private extension AppManager
|
||||
group.set(progress, for: app)
|
||||
}
|
||||
|
||||
// Refresh anisette data after downloading all apps to prevent session from expiring.
|
||||
for case let downloadOperation as DownloadAppOperation in operations
|
||||
{
|
||||
refreshAnisetteDataOperation.addDependency(downloadOperation)
|
||||
}
|
||||
|
||||
/* Cache App IDs */
|
||||
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
|
||||
fetchAppIDsOperation.resultHandler = { (result) in
|
||||
do
|
||||
{
|
||||
let (_, context) = try result.get()
|
||||
try context.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to fetch App IDs.", error)
|
||||
}
|
||||
}
|
||||
operations.forEach { fetchAppIDsOperation.addDependency($0) }
|
||||
operations.append(fetchAppIDsOperation)
|
||||
|
||||
group.addOperations(operations)
|
||||
|
||||
return group
|
||||
@@ -344,7 +460,11 @@ private extension AppManager
|
||||
guard !context.isFinished else { return }
|
||||
context.isFinished = true
|
||||
|
||||
self.refreshProgress[context.bundleIdentifier] = nil
|
||||
if let progress = self.refreshProgress[context.bundleIdentifier], progress == context.group.progress(forAppWithBundleIdentifier: context.bundleIdentifier)
|
||||
{
|
||||
// Only remove progress if it hasn't been replaced by another one.
|
||||
self.refreshProgress[context.bundleIdentifier] = nil
|
||||
}
|
||||
|
||||
if let error = context.error
|
||||
{
|
||||
@@ -376,10 +496,13 @@ private extension AppManager
|
||||
do { try installedApp.managedObjectContext?.save() }
|
||||
catch { print("Error saving installed app.", error) }
|
||||
}
|
||||
}
|
||||
|
||||
do { try FileManager.default.removeItem(at: context.temporaryDirectory) }
|
||||
catch { print("Failed to remove temporary directory.", error) }
|
||||
|
||||
if let index = UserDefaults.standard.legacySideloadedApps?.firstIndex(of: installedApp.bundleIdentifier)
|
||||
{
|
||||
// No longer a legacy sideloaded app, so remove it from cached list.
|
||||
UserDefaults.standard.legacySideloadedApps?.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
print("Finished operation!", context.bundleIdentifier)
|
||||
|
||||
|
||||
@@ -44,6 +44,11 @@ class Account: NSManagedObject, Fetchable
|
||||
{
|
||||
super.init(entity: Account.entity(), insertInto: context)
|
||||
|
||||
self.update(account: account)
|
||||
}
|
||||
|
||||
func update(account: ALTAccount)
|
||||
{
|
||||
self.appleID = account.appleID
|
||||
self.identifier = account.identifier
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>AltStore 2.xcdatamodel</string>
|
||||
<string>AltStore 4.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
52
AltStore/Model/AppID.swift
Normal file
52
AltStore/Model/AppID.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -194,13 +194,7 @@ private extension DatabaseManager
|
||||
}
|
||||
|
||||
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
|
||||
installedApp.version = localApp.version
|
||||
|
||||
if let provisioningProfile = localApp.provisioningProfile
|
||||
{
|
||||
installedApp.refreshedDate = provisioningProfile.creationDate
|
||||
installedApp.expirationDate = provisioningProfile.expirationDate
|
||||
}
|
||||
installedApp.update(resignedApp: localApp)
|
||||
|
||||
do
|
||||
{
|
||||
|
||||
@@ -11,8 +11,20 @@ import CoreData
|
||||
|
||||
import AltSign
|
||||
|
||||
protocol InstalledAppProtocol: Fetchable
|
||||
{
|
||||
var name: String { get }
|
||||
var bundleIdentifier: String { get }
|
||||
var resignedBundleIdentifier: String { get }
|
||||
var version: String { get }
|
||||
|
||||
var refreshedDate: Date { get }
|
||||
var expirationDate: Date { get }
|
||||
var installedDate: Date { get }
|
||||
}
|
||||
|
||||
@objc(InstalledApp)
|
||||
class InstalledApp: NSManagedObject, Fetchable
|
||||
class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var name: String
|
||||
@@ -22,9 +34,12 @@ class InstalledApp: NSManagedObject, Fetchable
|
||||
|
||||
@NSManaged var refreshedDate: Date
|
||||
@NSManaged var expirationDate: Date
|
||||
@NSManaged var installedDate: Date
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var storeApp: StoreApp?
|
||||
@NSManaged var team: Team?
|
||||
@NSManaged var appExtensions: Set<InstalledExtension>
|
||||
|
||||
var isSideloaded: Bool {
|
||||
return self.storeApp == nil
|
||||
@@ -39,10 +54,21 @@ class InstalledApp: NSManagedObject, Fetchable
|
||||
{
|
||||
super.init(entity: InstalledApp.entity(), insertInto: context)
|
||||
|
||||
self.name = resignedApp.name
|
||||
self.bundleIdentifier = originalBundleIdentifier
|
||||
self.resignedBundleIdentifier = resignedApp.bundleIdentifier
|
||||
|
||||
self.refreshedDate = Date()
|
||||
self.installedDate = Date()
|
||||
|
||||
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
|
||||
|
||||
self.update(resignedApp: resignedApp)
|
||||
}
|
||||
|
||||
func update(resignedApp: ALTApplication)
|
||||
{
|
||||
self.name = resignedApp.name
|
||||
|
||||
self.resignedBundleIdentifier = resignedApp.bundleIdentifier
|
||||
self.version = resignedApp.version
|
||||
|
||||
if let provisioningProfile = resignedApp.provisioningProfile
|
||||
@@ -50,11 +76,6 @@ class InstalledApp: NSManagedObject, Fetchable
|
||||
self.refreshedDate = provisioningProfile.creationDate
|
||||
self.expirationDate = provisioningProfile.expirationDate
|
||||
}
|
||||
else
|
||||
{
|
||||
self.refreshedDate = Date()
|
||||
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +208,12 @@ extension InstalledApp
|
||||
return directoryURL
|
||||
}
|
||||
|
||||
class func installedAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
|
||||
{
|
||||
let installedAppUTI = "io.altstore.Installed." + bundleIdentifier
|
||||
return installedAppUTI
|
||||
}
|
||||
|
||||
var directoryURL: URL {
|
||||
return InstalledApp.directoryURL(for: self)
|
||||
}
|
||||
@@ -198,4 +225,8 @@ extension InstalledApp
|
||||
var refreshedIPAURL: URL {
|
||||
return InstalledApp.refreshedIPAURL(for: self)
|
||||
}
|
||||
|
||||
var installedAppUTI: String {
|
||||
return InstalledApp.installedAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
70
AltStore/Model/InstalledExtension.swift
Normal file
70
AltStore/Model/InstalledExtension.swift
Normal 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
30
AltStore/Model/Migrations/Policies/InstalledAppPolicy.swift
Normal file
30
AltStore/Model/Migrations/Policies/InstalledAppPolicy.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,12 @@ import CoreData
|
||||
extension Source
|
||||
{
|
||||
static let altStoreIdentifier = "com.rileytestut.AltStore"
|
||||
|
||||
#if STAGING
|
||||
static let altStoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/apps-staging.json")!
|
||||
#else
|
||||
static let altStoreSourceURL = URL(string: "https://cdn.altstore.io/file/altstore/apps.json")!
|
||||
#endif
|
||||
}
|
||||
|
||||
@objc(Source)
|
||||
@@ -22,6 +27,9 @@ class Source: NSManagedObject, Fetchable, Decodable
|
||||
@NSManaged var identifier: String
|
||||
@NSManaged var sourceURL: URL
|
||||
|
||||
/* Non-Core Data Properties */
|
||||
var userInfo: [ALTSourceUserInfoKey: String]?
|
||||
|
||||
/* Relationships */
|
||||
@objc(apps) @NSManaged private(set) var _apps: NSOrderedSet
|
||||
@objc(newsItems) @NSManaged private(set) var _newsItems: NSOrderedSet
|
||||
@@ -49,6 +57,7 @@ class Source: NSManagedObject, Fetchable, Decodable
|
||||
case name
|
||||
case identifier
|
||||
case sourceURL
|
||||
case userInfo
|
||||
case apps
|
||||
case news
|
||||
}
|
||||
@@ -69,6 +78,9 @@ class Source: NSManagedObject, Fetchable, Decodable
|
||||
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||
self.sourceURL = try container.decode(URL.self, forKey: .sourceURL)
|
||||
|
||||
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
|
||||
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
|
||||
|
||||
let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? []
|
||||
for (index, app) in apps.enumerated()
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ extension ALTTeamType
|
||||
switch self
|
||||
{
|
||||
case .free: return NSLocalizedString("Free Developer Account", comment: "")
|
||||
case .individual: return NSLocalizedString("Individual", comment: "")
|
||||
case .individual: return NSLocalizedString("Developer", comment: "")
|
||||
case .organization: return NSLocalizedString("Organization", comment: "")
|
||||
case .unknown: fallthrough
|
||||
@unknown default: return NSLocalizedString("Unknown", comment: "")
|
||||
@@ -25,6 +25,11 @@ extension ALTTeamType
|
||||
}
|
||||
}
|
||||
|
||||
extension Team
|
||||
{
|
||||
static let maximumFreeAppIDs = 10
|
||||
}
|
||||
|
||||
@objc(Team)
|
||||
class Team: NSManagedObject, Fetchable
|
||||
{
|
||||
@@ -37,6 +42,8 @@ class Team: NSManagedObject, Fetchable
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged private(set) var account: Account!
|
||||
@NSManaged var installedApps: Set<InstalledApp>
|
||||
@NSManaged private(set) var appIDs: Set<AppID>
|
||||
|
||||
var altTeam: ALTTeam?
|
||||
|
||||
@@ -49,13 +56,18 @@ class Team: NSManagedObject, Fetchable
|
||||
{
|
||||
super.init(entity: Team.entity(), insertInto: context)
|
||||
|
||||
self.account = account
|
||||
|
||||
self.update(team: team)
|
||||
}
|
||||
|
||||
func update(team: ALTTeam)
|
||||
{
|
||||
self.altTeam = team
|
||||
|
||||
self.name = team.name
|
||||
self.identifier = team.identifier
|
||||
self.type = team.type
|
||||
|
||||
self.account = account
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,18 @@ import UIKit
|
||||
|
||||
class InstalledAppCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
@IBOutlet var appIconImageView: UIImageView!
|
||||
@IBOutlet var nameLabel: UILabel!
|
||||
@IBOutlet var developerLabel: UILabel!
|
||||
@IBOutlet var refreshButton: PillButton!
|
||||
@IBOutlet var betaBadgeView: UIImageView!
|
||||
@IBOutlet var bannerView: AppBannerView!
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
self.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
|
||||
self.bannerView.buttonLabel.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
||||
@@ -23,6 +30,24 @@ class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
||||
@IBOutlet var button: UIButton!
|
||||
}
|
||||
|
||||
class InstalledAppsCollectionFooterView: UICollectionReusableView
|
||||
{
|
||||
@IBOutlet var textLabel: UILabel!
|
||||
@IBOutlet var button: UIButton!
|
||||
}
|
||||
|
||||
class NoUpdatesCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
@IBOutlet var blurView: UIVisualEffectView!
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
}
|
||||
}
|
||||
|
||||
class UpdatesCollectionHeaderView: UICollectionReusableView
|
||||
{
|
||||
let button = PillButton(type: .system)
|
||||
|
||||
@@ -67,6 +67,17 @@ class MyAppsViewController: UICollectionViewController
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
#if !BETA
|
||||
// Set leftBarButtonItem to invisible UIBarButtonItem so we can still use it
|
||||
// to show an activity indicator while sideloading whitelisted apps.
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
|
||||
#endif
|
||||
|
||||
if #available(iOS 13.0, *)
|
||||
{
|
||||
self.navigationItem.leftBarButtonItem?.activityIndicatorView.style = .medium
|
||||
}
|
||||
|
||||
// Allows us to intercept delegate callbacks.
|
||||
self.updatesDataSource.fetchedResultsController.delegate = self
|
||||
|
||||
@@ -74,7 +85,6 @@ class MyAppsViewController: UICollectionViewController
|
||||
self.collectionView.prefetchDataSource = self.dataSource
|
||||
|
||||
self.prototypeUpdateCell = UpdateCollectionViewCell.instantiate(with: UpdateCollectionViewCell.nib!)
|
||||
self.prototypeUpdateCell.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.prototypeUpdateCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
self.collectionView.register(UpdateCollectionViewCell.nib, forCellWithReuseIdentifier: "UpdateCell")
|
||||
@@ -85,10 +95,6 @@ class MyAppsViewController: UICollectionViewController
|
||||
self.sideloadingProgressView.progressTintColor = .altPrimary
|
||||
self.sideloadingProgressView.progress = 0
|
||||
|
||||
#if !BETA
|
||||
self.navigationItem.leftBarButtonItem = nil
|
||||
#endif
|
||||
|
||||
if let navigationBar = self.navigationController?.navigationBar
|
||||
{
|
||||
navigationBar.addSubview(self.sideloadingProgressView)
|
||||
@@ -109,6 +115,10 @@ class MyAppsViewController: UICollectionViewController
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.updateDataSource()
|
||||
|
||||
#if BETA
|
||||
self.fetchAppIDs()
|
||||
#endif
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||
@@ -138,6 +148,10 @@ class MyAppsViewController: UICollectionViewController
|
||||
let installedApp = self.dataSource.item(at: indexPath)
|
||||
return !installedApp.isSideloaded
|
||||
}
|
||||
|
||||
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private extension MyAppsViewController
|
||||
@@ -156,9 +170,13 @@ private extension MyAppsViewController
|
||||
dynamicDataSource.numberOfItemsHandler = { _ in self.updatesDataSource.itemCount == 0 ? 1 : 0 }
|
||||
dynamicDataSource.cellIdentifierHandler = { _ in "NoUpdatesCell" }
|
||||
dynamicDataSource.cellConfigurationHandler = { (cell, _, indexPath) in
|
||||
cell.layer.cornerRadius = 20
|
||||
cell.layer.masksToBounds = true
|
||||
cell.contentView.backgroundColor = UIColor.altPrimary.withAlphaComponent(0.15)
|
||||
let cell = cell as! NoUpdatesCollectionViewCell
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
cell.blurView.layer.cornerRadius = 20
|
||||
cell.blurView.layer.masksToBounds = true
|
||||
cell.blurView.backgroundColor = .altPrimary
|
||||
}
|
||||
|
||||
return dynamicDataSource
|
||||
@@ -179,15 +197,19 @@ private extension MyAppsViewController
|
||||
guard let app = installedApp.storeApp else { return }
|
||||
|
||||
let cell = cell as! UpdateCollectionViewCell
|
||||
cell.tintColor = app.tintColor ?? .altPrimary
|
||||
cell.nameLabel.text = app.name
|
||||
cell.versionDescriptionTextView.text = app.versionDescription
|
||||
cell.appIconImageView.image = nil
|
||||
cell.appIconImageView.isIndicatingActivity = true
|
||||
cell.betaBadgeView.isHidden = !app.isBeta
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
cell.updateButton.isIndicatingActivity = false
|
||||
cell.updateButton.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
|
||||
cell.tintColor = app.tintColor ?? .altPrimary
|
||||
cell.versionDescriptionTextView.text = app.versionDescription
|
||||
|
||||
cell.bannerView.titleLabel.text = app.name
|
||||
cell.bannerView.iconImageView.image = nil
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
cell.bannerView.betaBadgeView.isHidden = !app.isBeta
|
||||
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
|
||||
|
||||
if self.expandedAppUpdates.contains(app.bundleIdentifier)
|
||||
{
|
||||
@@ -201,9 +223,9 @@ private extension MyAppsViewController
|
||||
cell.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
|
||||
|
||||
let progress = AppManager.shared.installationProgress(for: app)
|
||||
cell.updateButton.progress = progress
|
||||
cell.bannerView.button.progress = progress
|
||||
|
||||
cell.dateLabel.text = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter)
|
||||
cell.bannerView.subtitleLabel.text = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter)
|
||||
|
||||
cell.setNeedsLayout()
|
||||
}
|
||||
@@ -227,8 +249,8 @@ private extension MyAppsViewController
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! UpdateCollectionViewCell
|
||||
cell.appIconImageView.isIndicatingActivity = false
|
||||
cell.appIconImageView.image = image
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
cell.bannerView.iconImageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
@@ -254,12 +276,15 @@ private extension MyAppsViewController
|
||||
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
|
||||
|
||||
let cell = cell as! InstalledAppCollectionViewCell
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
cell.tintColor = tintColor
|
||||
cell.appIconImageView.isIndicatingActivity = true
|
||||
cell.betaBadgeView.isHidden = !(installedApp.storeApp?.isBeta ?? false)
|
||||
|
||||
cell.refreshButton.isIndicatingActivity = false
|
||||
cell.refreshButton.addTarget(self, action: #selector(MyAppsViewController.refreshApp(_:)), for: .primaryActionTriggered)
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||
cell.bannerView.betaBadgeView.isHidden = !(installedApp.storeApp?.isBeta ?? false)
|
||||
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.refreshApp(_:)), for: .primaryActionTriggered)
|
||||
|
||||
let currentDate = Date()
|
||||
|
||||
@@ -267,34 +292,34 @@ private extension MyAppsViewController
|
||||
|
||||
if numberOfDays == 1
|
||||
{
|
||||
cell.refreshButton.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal)
|
||||
cell.bannerView.button.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal)
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.refreshButton.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal)
|
||||
cell.bannerView.button.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal)
|
||||
}
|
||||
|
||||
cell.nameLabel.text = installedApp.name
|
||||
cell.developerLabel.text = installedApp.storeApp?.developerName ?? NSLocalizedString("Sideloaded", comment: "")
|
||||
cell.bannerView.titleLabel.text = installedApp.name
|
||||
cell.bannerView.subtitleLabel.text = installedApp.storeApp?.developerName ?? NSLocalizedString("Sideloaded", comment: "")
|
||||
|
||||
// Make sure refresh button is correct size.
|
||||
cell.layoutIfNeeded()
|
||||
|
||||
switch numberOfDays
|
||||
{
|
||||
case 2...3: cell.refreshButton.tintColor = .refreshOrange
|
||||
case 4...5: cell.refreshButton.tintColor = .refreshYellow
|
||||
case 6...: cell.refreshButton.tintColor = .refreshGreen
|
||||
default: cell.refreshButton.tintColor = .refreshRed
|
||||
case 2...3: cell.bannerView.button.tintColor = .refreshOrange
|
||||
case 4...5: cell.bannerView.button.tintColor = .refreshYellow
|
||||
case 6...: cell.bannerView.button.tintColor = .refreshGreen
|
||||
default: cell.bannerView.button.tintColor = .refreshRed
|
||||
}
|
||||
|
||||
if let refreshGroup = self.refreshGroup, let progress = refreshGroup.progress(for: installedApp), progress.fractionCompleted < 1.0
|
||||
{
|
||||
cell.refreshButton.progress = progress
|
||||
cell.bannerView.button.progress = progress
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.refreshButton.progress = nil
|
||||
cell.bannerView.button.progress = nil
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||
@@ -312,8 +337,8 @@ private extension MyAppsViewController
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! InstalledAppCollectionViewCell
|
||||
cell.appIconImageView.image = image
|
||||
cell.appIconImageView.isIndicatingActivity = false
|
||||
cell.bannerView.iconImageView.image = image
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
}
|
||||
|
||||
return dataSource
|
||||
@@ -358,6 +383,21 @@ private extension MyAppsViewController
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAppIDs()
|
||||
{
|
||||
AppManager.shared.fetchAppIDs { (result) in
|
||||
do
|
||||
{
|
||||
let (_, context) = try result.get()
|
||||
try context.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to fetch App IDs.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping (Result<[String : Result<InstalledApp, Error>], Error>) -> Void)
|
||||
{
|
||||
func refresh()
|
||||
@@ -368,9 +408,8 @@ private extension MyAppsViewController
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
toastView.setNeedsLayout()
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
|
||||
case .success(let results):
|
||||
let failures = results.compactMapValues { (result) -> Error? in
|
||||
@@ -384,22 +423,32 @@ private extension MyAppsViewController
|
||||
|
||||
guard !failures.isEmpty else { break }
|
||||
|
||||
let localizedText: String
|
||||
let detailText: String?
|
||||
let toastView: ToastView
|
||||
|
||||
if let failure = failures.first, failures.count == 1
|
||||
if let failure = failures.first, results.count == 1
|
||||
{
|
||||
localizedText = failure.value.localizedDescription
|
||||
detailText = nil
|
||||
toastView = ToastView(error: failure.value)
|
||||
}
|
||||
else
|
||||
{
|
||||
localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count))
|
||||
detailText = failures.first?.value.localizedDescription
|
||||
let localizedText: String
|
||||
|
||||
if failures.count == 1
|
||||
{
|
||||
localizedText = NSLocalizedString("Failed to refresh 1 app.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count))
|
||||
}
|
||||
|
||||
let detailText = failures.first?.value.localizedDescription
|
||||
|
||||
toastView = ToastView(text: localizedText, detailText: detailText)
|
||||
toastView.preferredDuration = 2.0
|
||||
}
|
||||
|
||||
let toastView = ToastView(text: localizedText, detailText: detailText)
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
|
||||
self.refreshGroup = nil
|
||||
@@ -559,8 +608,8 @@ private extension MyAppsViewController
|
||||
self.collectionView.reloadItems(at: [indexPath])
|
||||
|
||||
case .failure(let error):
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
|
||||
self.collectionView.reloadItems(at: [indexPath])
|
||||
|
||||
@@ -591,7 +640,7 @@ private extension MyAppsViewController
|
||||
|
||||
func presentSideloadingAlert(completion: @escaping (Bool) -> Void)
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Sideload Apps (Beta)", comment: ""), message: NSLocalizedString("You may only install 10 apps + app extensions per week due to Apple's restrictions.\n\nIf you encounter an app that is not able to be sideloaded, please report the app to support@altstore.io.", comment: ""), preferredStyle: .alert)
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Sideload Apps (Beta)", comment: ""), message: NSLocalizedString("If you encounter an app that is not able to be sideloaded, please report the app to support@altstore.io.", comment: ""), preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .default, handler: { (action) in
|
||||
completion(true)
|
||||
}))
|
||||
@@ -601,7 +650,7 @@ private extension MyAppsViewController
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func sideloadApp(at fileURL: URL, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
|
||||
|
||||
@@ -614,7 +663,11 @@ private extension MyAppsViewController
|
||||
|
||||
let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory)
|
||||
|
||||
guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { return }
|
||||
guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { throw OperationError.invalidApp }
|
||||
|
||||
#if !BETA
|
||||
guard AppManager.whitelistedSideloadingBundleIDs.contains(application.bundleIdentifier) else { throw OperationError.sideloadingAppNotSupported(application) }
|
||||
#endif
|
||||
|
||||
self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in
|
||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||
@@ -622,8 +675,8 @@ private extension MyAppsViewController
|
||||
DispatchQueue.main.async {
|
||||
if let error = result.error
|
||||
{
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
toastView.show(in: self.view, duration: 2.0)
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -648,7 +701,31 @@ private extension MyAppsViewController
|
||||
{
|
||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
|
||||
DispatchQueue.main.async {
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
|
||||
|
||||
if let localizedError = error as? OperationError, case OperationError.sideloadingAppNotSupported = localizedError
|
||||
{
|
||||
let message = NSLocalizedString("""
|
||||
Sideloading apps is in beta, and is currently limited to a small number of apps. This restriction is temporary, and you will be able to sideload any app once the feature is finished.
|
||||
|
||||
In the meantime, you can help us beta test sideloading apps by becoming a Patron.
|
||||
""", comment: "")
|
||||
|
||||
let alertController = UIAlertController(title: localizedError.localizedDescription, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Become a Patron", comment: ""), style: .default, handler: { (action) in
|
||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||
}))
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
}
|
||||
|
||||
completion(.failure(error))
|
||||
}
|
||||
@@ -700,14 +777,22 @@ private extension MyAppsViewController
|
||||
else { return }
|
||||
|
||||
let installedApp = self.dataSource.item(at: indexPath)
|
||||
guard installedApp.storeApp == nil else { return }
|
||||
|
||||
#if DEBUG
|
||||
self.presentAlert(for: installedApp)
|
||||
#else
|
||||
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
|
||||
{
|
||||
// Only display alert for legacy sideloaded apps.
|
||||
self.presentAlert(for: installedApp)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@objc func importApp(_ notification: Notification)
|
||||
{
|
||||
#if BETA
|
||||
// Make sure left UIBarButtonItem has been set.
|
||||
self.loadViewIfNeeded()
|
||||
|
||||
guard let fileURL = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return }
|
||||
guard self.presentedViewController == nil else { return }
|
||||
@@ -724,10 +809,12 @@ private extension MyAppsViewController
|
||||
}
|
||||
}
|
||||
|
||||
#if BETA
|
||||
|
||||
self.presentSideloadingAlert { (shouldContinue) in
|
||||
if shouldContinue
|
||||
{
|
||||
self.installApp(at: fileURL) { (result) in
|
||||
self.sideloadApp(at: fileURL) { (result) in
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -737,6 +824,12 @@ private extension MyAppsViewController
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
self.sideloadApp(at: fileURL) { (result) in
|
||||
finish()
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -776,7 +869,7 @@ extension MyAppsViewController
|
||||
|
||||
return headerView
|
||||
|
||||
case .installedApps:
|
||||
case .installedApps where kind == UICollectionView.elementKindSectionHeader:
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InstalledAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
@@ -792,6 +885,35 @@ extension MyAppsViewController
|
||||
}
|
||||
|
||||
return headerView
|
||||
|
||||
case .installedApps:
|
||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "InstalledAppsFooter", for: indexPath) as! InstalledAppsCollectionFooterView
|
||||
|
||||
guard let team = DatabaseManager.shared.activeTeam() else { return footerView }
|
||||
switch team.type
|
||||
{
|
||||
case .free:
|
||||
let registeredAppIDs = team.appIDs.count
|
||||
|
||||
let maximumAppIDCount = 10
|
||||
let remainingAppIDs = max(maximumAppIDCount - registeredAppIDs, 0)
|
||||
|
||||
if remainingAppIDs == 1
|
||||
{
|
||||
footerView.textLabel.text = String(format: NSLocalizedString("1 App ID Remaining", comment: ""))
|
||||
}
|
||||
else
|
||||
{
|
||||
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs Remaining", comment: ""), NSNumber(value: remainingAppIDs))
|
||||
}
|
||||
|
||||
footerView.textLabel.isHidden = false
|
||||
|
||||
case .individual, .organization, .unknown: footerView.textLabel.isHidden = true
|
||||
@unknown default: break
|
||||
}
|
||||
|
||||
return footerView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,14 +935,11 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
|
||||
{
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||
{
|
||||
let padding = 30 as CGFloat
|
||||
let width = collectionView.bounds.width - padding
|
||||
|
||||
let section = Section.allCases[indexPath.section]
|
||||
switch section
|
||||
{
|
||||
case .noUpdates:
|
||||
let size = CGSize(width: width, height: 44)
|
||||
let size = CGSize(width: collectionView.bounds.width, height: 44)
|
||||
return size
|
||||
|
||||
case .updates:
|
||||
@@ -831,7 +950,10 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
|
||||
return previousHeight
|
||||
}
|
||||
|
||||
let widthConstraint = self.prototypeUpdateCell.contentView.widthAnchor.constraint(equalToConstant: width)
|
||||
// Manually change cell's width to prevent conflicting with UIView-Encapsulated-Layout-Width constraints.
|
||||
self.prototypeUpdateCell.frame.size.width = collectionView.bounds.width
|
||||
|
||||
let widthConstraint = self.prototypeUpdateCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
||||
NSLayoutConstraint.activate([widthConstraint])
|
||||
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
|
||||
|
||||
@@ -842,7 +964,7 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
|
||||
return size
|
||||
|
||||
case .installedApps:
|
||||
return CGSize(width: collectionView.bounds.width, height: 60)
|
||||
return CGSize(width: collectionView.bounds.width, height: 88)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,20 +982,38 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
||||
{
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case .noUpdates:
|
||||
guard self.updatesDataSource.itemCount == 0 else { return .zero }
|
||||
return UIEdgeInsets(top: 12, left: 15, bottom: 20, right: 15)
|
||||
case .noUpdates: return .zero
|
||||
case .updates: return .zero
|
||||
case .installedApps:
|
||||
#if BETA
|
||||
guard let _ = DatabaseManager.shared.activeTeam() else { return .zero }
|
||||
|
||||
case .updates:
|
||||
guard self.updatesDataSource.itemCount > 0 else { return .zero }
|
||||
return UIEdgeInsets(top: 12, left: 15, bottom: 20, right: 15)
|
||||
|
||||
case .installedApps: return UIEdgeInsets(top: 12, left: 0, bottom: 20, right: 0)
|
||||
let indexPath = IndexPath(row: 0, section: section.rawValue)
|
||||
let footerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionFooter, at: indexPath) as! InstalledAppsCollectionFooterView
|
||||
|
||||
let size = footerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
|
||||
withHorizontalFittingPriority: .required,
|
||||
verticalFittingPriority: .fittingSizeLevel)
|
||||
return size
|
||||
#else
|
||||
return .zero
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ myCV: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
|
||||
{
|
||||
let section = Section.allCases[section]
|
||||
switch section
|
||||
{
|
||||
case .noUpdates where self.updatesDataSource.itemCount != 0: return .zero
|
||||
case .updates where self.updatesDataSource.itemCount == 0: return .zero
|
||||
default: return UIEdgeInsets(top: 12, left: 0, bottom: 20, right: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -930,7 +1070,7 @@ extension MyAppsViewController: UIDocumentPickerDelegate
|
||||
{
|
||||
guard let fileURL = urls.first else { return }
|
||||
|
||||
self.installApp(at: fileURL) { (result) in
|
||||
self.sideloadApp(at: fileURL) { (result) in
|
||||
print("Sideloaded app at \(fileURL) with result:", result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,20 +25,27 @@ extension UpdateCollectionViewCell
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet var appIconImageView: UIImageView!
|
||||
@IBOutlet var nameLabel: UILabel!
|
||||
@IBOutlet var dateLabel: UILabel!
|
||||
@IBOutlet var updateButton: PillButton!
|
||||
@IBOutlet var bannerView: AppBannerView!
|
||||
@IBOutlet var versionDescriptionTitleLabel: UILabel!
|
||||
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
|
||||
@IBOutlet var betaBadgeView: UIImageView!
|
||||
|
||||
@IBOutlet private var blurView: UIVisualEffectView!
|
||||
|
||||
private var originalTintColor: UIColor?
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
self.contentView.layer.cornerRadius = 20
|
||||
self.contentView.layer.masksToBounds = true
|
||||
// Prevent temporary unsatisfiable constraint errors due to UIView-Encapsulated-Layout constraints.
|
||||
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
self.bannerView.backgroundEffectView.isHidden = true
|
||||
self.bannerView.button.setTitle(NSLocalizedString("UPDATE", comment: ""), for: .normal)
|
||||
|
||||
self.blurView.layer.cornerRadius = 20
|
||||
self.blurView.layer.masksToBounds = true
|
||||
|
||||
self.update()
|
||||
}
|
||||
@@ -47,6 +54,11 @@ extension UpdateCollectionViewCell
|
||||
{
|
||||
super.tintColorDidChange()
|
||||
|
||||
if self.tintAdjustmentMode != .dimmed
|
||||
{
|
||||
self.originalTintColor = self.tintColor
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
@@ -86,12 +98,9 @@ private extension UpdateCollectionViewCell
|
||||
case .expanded: self.versionDescriptionTextView.isCollapsed = false
|
||||
}
|
||||
|
||||
self.versionDescriptionTitleLabel.textColor = self.tintColor
|
||||
self.contentView.backgroundColor = self.tintColor.withAlphaComponent(0.1)
|
||||
|
||||
self.updateButton.setTitleColor(self.tintColor, for: .normal)
|
||||
self.updateButton.backgroundColor = self.tintColor.withAlphaComponent(0.15)
|
||||
self.updateButton.progressTintColor = self.tintColor
|
||||
self.versionDescriptionTitleLabel.textColor = self.originalTintColor ?? self.tintColor
|
||||
self.blurView.backgroundColor = self.originalTintColor ?? self.tintColor
|
||||
self.bannerView.button.progressTintColor = self.originalTintColor ?? self.tintColor
|
||||
|
||||
self.setNeedsLayout()
|
||||
self.layoutIfNeeded()
|
||||
|
||||
@@ -1,130 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="133.5"/>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="133.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dmf-hv-bwx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="133.5"/>
|
||||
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dmf-hv-bwx">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="125"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="57X-Ep-rfq">
|
||||
<rect key="frame" x="20" y="20" width="340" height="93.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="H0T-dR-3In" userLabel="App Info">
|
||||
<rect key="frame" x="0.0" y="0.0" width="340" height="65"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="jg6-wi-ngb" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="65" height="65"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="65" id="W3C-hH-1Ii"/>
|
||||
<constraint firstAttribute="width" secondItem="jg6-wi-ngb" secondAttribute="height" multiplier="1:1" id="vt3-Qt-m21"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="2Ii-Hu-4ru">
|
||||
<rect key="frame" x="76" y="14" width="172" height="37"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="9Zk-Mp-JI7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="89.5" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="Short" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qmI-m4-Mra">
|
||||
<rect key="frame" x="0.0" y="0.0" width="42.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="4LS-dp-4VA">
|
||||
<rect key="frame" x="48.5" y="0.0" width="41" height="20.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xaB-Kc-Par">
|
||||
<rect key="frame" x="0.0" y="22.5" width="57.5" height="14.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="OSL-U2-BKa" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="259" y="17" width="81" height="31"/>
|
||||
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="81" id="3yj-p0-NuE"/>
|
||||
<constraint firstAttribute="height" constant="31" id="KbP-M6-N3w"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
|
||||
<state key="normal" title="UPDATE"/>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
|
||||
<rect key="frame" x="0.0" y="79" width="340" height="14.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" text="What's New" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h1u-nj-qsP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="65" height="13.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="65" id="C7Y-nh-TKJ"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="11"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="75" y="-10" width="265" height="24.5"/>
|
||||
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="H0T-dR-3In" firstAttribute="width" secondItem="57X-Ep-rfq" secondAttribute="width" id="DYI-fa-Egk"/>
|
||||
<constraint firstItem="RSR-5W-7tt" firstAttribute="width" secondItem="57X-Ep-rfq" secondAttribute="width" id="d3x-mH-ODQ"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1xN-9h-DFd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="9iq-CR-Xc4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="uYl-PH-DuP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
|
||||
<rect key="frame" x="0.0" y="88" width="343" height="37"/>
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RKU-pY-wmQ">
|
||||
<rect key="frame" x="15" y="0.0" width="65" height="22"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="4GQ-XP-i7X">
|
||||
<rect key="frame" x="0.0" y="0.0" width="65" height="22"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" text="What's New" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h1u-nj-qsP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="65" height="22"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="65" id="C7Y-nh-TKJ"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="11"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="h1u-nj-qsP" firstAttribute="leading" secondItem="4GQ-XP-i7X" secondAttribute="leading" id="3cO-Mj-Yua"/>
|
||||
<constraint firstAttribute="trailing" secondItem="h1u-nj-qsP" secondAttribute="trailing" id="Hek-OE-YMc"/>
|
||||
<constraint firstAttribute="bottom" secondItem="h1u-nj-qsP" secondAttribute="bottom" id="bLg-Ut-aEb"/>
|
||||
<constraint firstItem="h1u-nj-qsP" firstAttribute="top" secondItem="4GQ-XP-i7X" secondAttribute="top" id="beL-ob-CQ7"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<vibrancyEffect style="secondaryLabel">
|
||||
<blurEffect style="systemChromeMaterial"/>
|
||||
</vibrancyEffect>
|
||||
</visualEffectView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="90" y="0.0" width="238" height="22"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<edgeInsets key="layoutMargins" top="0.0" left="15" bottom="15" right="15"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" name="BlurTint"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="uYl-PH-DuP" secondAttribute="trailing" id="51O-j6-eoh"/>
|
||||
<constraint firstAttribute="bottom" secondItem="uYl-PH-DuP" secondAttribute="bottom" id="IGs-MS-vnM"/>
|
||||
<constraint firstItem="uYl-PH-DuP" firstAttribute="top" secondItem="9iq-CR-Xc4" secondAttribute="top" id="hnr-wG-XRY"/>
|
||||
<constraint firstItem="uYl-PH-DuP" firstAttribute="leading" secondItem="9iq-CR-Xc4" secondAttribute="leading" id="usR-Ia-LMy"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<blurEffect style="systemChromeMaterial"/>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="57X-Ep-rfq" secondAttribute="bottom" constant="20" id="ArC-R2-jtc"/>
|
||||
<constraint firstItem="57X-Ep-rfq" firstAttribute="leading" secondItem="mdL-JE-wCe" secondAttribute="leading" constant="20" id="PvV-gg-7us"/>
|
||||
<constraint firstItem="57X-Ep-rfq" firstAttribute="top" secondItem="dmf-hv-bwx" secondAttribute="top" constant="20" id="QHM-k8-g0x"/>
|
||||
<constraint firstItem="mdL-JE-wCe" firstAttribute="trailing" secondItem="57X-Ep-rfq" secondAttribute="trailing" constant="15" id="sGL-bx-qIk"/>
|
||||
<constraint firstItem="1xN-9h-DFd" firstAttribute="top" secondItem="dmf-hv-bwx" secondAttribute="top" id="6rb-Bw-UVn"/>
|
||||
<constraint firstAttribute="bottom" secondItem="1xN-9h-DFd" secondAttribute="bottom" id="dnI-NB-BKv"/>
|
||||
<constraint firstAttribute="trailing" secondItem="1xN-9h-DFd" secondAttribute="trailing" id="kbY-Z6-V86"/>
|
||||
<constraint firstItem="1xN-9h-DFd" firstAttribute="leading" secondItem="dmf-hv-bwx" secondAttribute="leading" id="ofk-a7-m0Y"/>
|
||||
</constraints>
|
||||
<edgeInsets key="layoutMargins" top="20" left="20" bottom="20" right="20"/>
|
||||
<viewLayoutGuide key="safeArea" id="mdL-JE-wCe"/>
|
||||
</view>
|
||||
</subviews>
|
||||
</view>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="dmf-hv-bwx" firstAttribute="top" secondItem="Kqf-Pv-ca3" secondAttribute="top" id="7yY-05-eHt"/>
|
||||
<constraint firstAttribute="bottom" secondItem="dmf-hv-bwx" secondAttribute="bottom" id="Rrx-0k-6He"/>
|
||||
<constraint firstItem="dmf-hv-bwx" firstAttribute="leading" secondItem="Kqf-Pv-ca3" secondAttribute="leading" id="W0V-sT-tXo"/>
|
||||
<constraint firstAttribute="trailing" secondItem="dmf-hv-bwx" secondAttribute="trailing" id="tgy-Zi-iZF"/>
|
||||
<constraint firstItem="dmf-hv-bwx" firstAttribute="leading" secondItem="Kqf-Pv-ca3" secondAttribute="leadingMargin" id="W0V-sT-tXo"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="dmf-hv-bwx" secondAttribute="trailing" id="tgy-Zi-iZF"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="C6r-zO-INg"/>
|
||||
<connections>
|
||||
<outlet property="appIconImageView" destination="jg6-wi-ngb" id="j83-Dl-GT6"/>
|
||||
<outlet property="betaBadgeView" destination="4LS-dp-4VA" id="Q2Z-AG-Y19"/>
|
||||
<outlet property="dateLabel" destination="xaB-Kc-Par" id="mfG-3C-r7j"/>
|
||||
<outlet property="nameLabel" destination="qmI-m4-Mra" id="LQz-w7-HNb"/>
|
||||
<outlet property="updateButton" destination="OSL-U2-BKa" id="WbI-96-Nel"/>
|
||||
<outlet property="bannerView" destination="Nop-pL-Icx" id="GiX-K1-5oz"/>
|
||||
<outlet property="blurView" destination="1xN-9h-DFd" id="HBI-nT-xYh"/>
|
||||
<outlet property="versionDescriptionTextView" destination="rNs-2O-k3V" id="4TC-A3-oxb"/>
|
||||
<outlet property="versionDescriptionTitleLabel" destination="h1u-nj-qsP" id="dnz-Yv-BdY"/>
|
||||
</connections>
|
||||
@@ -132,6 +114,8 @@
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="BetaBadge" width="41" height="17"/>
|
||||
<namedColor name="BlurTint">
|
||||
<color red="1" green="1" blue="1" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -13,13 +13,16 @@ class NewsCollectionViewCell: UICollectionViewCell
|
||||
@IBOutlet var titleLabel: UILabel!
|
||||
@IBOutlet var captionLabel: UILabel!
|
||||
@IBOutlet var imageView: UIImageView!
|
||||
@IBOutlet var contentBackgroundView: UIView!
|
||||
|
||||
override func awakeFromNib()
|
||||
{
|
||||
super.awakeFromNib()
|
||||
|
||||
self.contentView.layer.cornerRadius = 30
|
||||
self.contentView.clipsToBounds = true
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
self.contentBackgroundView.layer.cornerRadius = 30
|
||||
self.contentBackgroundView.clipsToBounds = true
|
||||
|
||||
self.imageView.layer.cornerRadius = 30
|
||||
self.imageView.clipsToBounds = true
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -18,23 +16,26 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xba-Qs-SQo">
|
||||
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="azr-Ea-luN">
|
||||
<rect key="frame" x="16" y="0.0" width="303" height="299"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xba-Qs-SQo">
|
||||
<rect key="frame" x="16" y="0.0" width="303" height="299"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="tNk-9u-1tk">
|
||||
<rect key="frame" x="0.0" y="0.0" width="335" height="298.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="303" height="298.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="akF-Tr-G5M">
|
||||
<rect key="frame" x="0.0" y="0.0" width="335" height="98.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="303" height="117.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Delta" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AkN-BE-I1a">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Delta" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="AkN-BE-I1a">
|
||||
<rect key="frame" x="25" y="25" width="54.5" height="26.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" alpha="0.75" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="999" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SHB-kk-YhL">
|
||||
<rect key="frame" x="25" y="61.5" width="35.5" height="17"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" alpha="0.75" contentMode="left" horizontalHuggingPriority="251" verticalCompressionResistancePriority="999" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SHB-kk-YhL">
|
||||
<rect key="frame" x="25" y="61.5" width="35.5" height="36"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -43,9 +44,9 @@
|
||||
<edgeInsets key="layoutMargins" top="25" left="25" bottom="20" right="25"/>
|
||||
</stackView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="335" placeholderIntrinsicHeight="200" translatesAutoresizingMaskIntoConstraints="NO" id="l36-Bm-De0">
|
||||
<rect key="frame" x="0.0" y="98.5" width="335" height="200"/>
|
||||
<rect key="frame" x="0.0" y="117.5" width="303" height="181"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="l36-Bm-De0" secondAttribute="height" multiplier="67:40" id="QGD-YE-Hw2"/>
|
||||
<constraint firstAttribute="width" secondItem="l36-Bm-De0" secondAttribute="height" multiplier="67:40" priority="999" id="QGD-YE-Hw2"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
@@ -63,15 +64,21 @@
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Xba-Qs-SQo" firstAttribute="top" secondItem="wRF-2R-NUG" secondAttribute="top" id="0xe-Rt-MhF"/>
|
||||
<constraint firstItem="Xba-Qs-SQo" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leading" id="5MO-c0-5rG"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Xba-Qs-SQo" secondAttribute="trailing" id="DNL-Jj-3By"/>
|
||||
<constraint firstItem="Xba-Qs-SQo" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leadingMargin" id="5MO-c0-5rG"/>
|
||||
<constraint firstItem="azr-Ea-luN" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leadingMargin" id="8Ck-dI-nJy"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="Xba-Qs-SQo" secondAttribute="trailing" id="DNL-Jj-3By"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Xba-Qs-SQo" secondAttribute="bottom" id="Ecj-fN-hZv"/>
|
||||
<constraint firstAttribute="bottom" secondItem="azr-Ea-luN" secondAttribute="bottom" priority="999" id="e56-UD-DRT"/>
|
||||
<constraint firstItem="azr-Ea-luN" firstAttribute="top" secondItem="wRF-2R-NUG" secondAttribute="top" id="h2k-WE-Esg"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="azr-Ea-luN" secondAttribute="trailing" priority="999" id="hsS-zC-A58"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="captionLabel" destination="SHB-kk-YhL" id="zY3-qQ-9oY"/>
|
||||
<outlet property="contentBackgroundView" destination="azr-Ea-luN" id="2Pl-11-YvR"/>
|
||||
<outlet property="imageView" destination="l36-Bm-De0" id="3do-aQ-5r4"/>
|
||||
<outlet property="titleLabel" destination="AkN-BE-I1a" id="hA2-3O-q5J"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="138" y="153"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
</document>
|
||||
|
||||
@@ -22,8 +22,17 @@ private class AppBannerFooterView: UICollectionReusableView
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.bannerView, pinningEdgesWith: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20))
|
||||
self.addGestureRecognizer(self.tapGestureRecognizer)
|
||||
|
||||
self.bannerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.addSubview(self.bannerView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
self.bannerView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
self.bannerView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
self.bannerView.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor),
|
||||
self.bannerView.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
@@ -52,7 +61,6 @@ class NewsViewController: UICollectionViewController
|
||||
super.viewDidLoad()
|
||||
|
||||
self.prototypeCell = NewsCollectionViewCell.instantiate(with: NewsCollectionViewCell.nib!)
|
||||
self.prototypeCell.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
self.collectionView.dataSource = self.dataSource
|
||||
@@ -93,15 +101,18 @@ private extension NewsViewController
|
||||
let fetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: false)]
|
||||
|
||||
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.date), cacheName: nil)
|
||||
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.sortIndex), cacheName: nil)
|
||||
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>(fetchedResultsController: fetchedResultsController)
|
||||
dataSource.proxy = self
|
||||
dataSource.cellConfigurationHandler = { (cell, newsItem, indexPath) in
|
||||
let cell = cell as! NewsCollectionViewCell
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
cell.titleLabel.text = newsItem.title
|
||||
cell.captionLabel.text = newsItem.caption
|
||||
cell.contentView.backgroundColor = newsItem.tintColor
|
||||
cell.contentBackgroundView.backgroundColor = newsItem.tintColor
|
||||
|
||||
cell.imageView.image = nil
|
||||
|
||||
@@ -315,6 +326,9 @@ extension NewsViewController
|
||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner", for: indexPath) as! AppBannerFooterView
|
||||
guard let storeApp = item.storeApp else { return footerView }
|
||||
|
||||
footerView.layoutMargins.left = self.view.layoutMargins.left
|
||||
footerView.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
footerView.bannerView.titleLabel.text = storeApp.name
|
||||
footerView.bannerView.subtitleLabel.text = storeApp.developerName
|
||||
footerView.bannerView.tintColor = storeApp.tintColor
|
||||
@@ -330,7 +344,6 @@ extension NewsViewController
|
||||
|
||||
let progress = AppManager.shared.installationProgress(for: storeApp)
|
||||
footerView.bannerView.button.progress = progress
|
||||
footerView.bannerView.button.isInverted = false
|
||||
|
||||
if Date() < storeApp.versionDate
|
||||
{
|
||||
@@ -345,7 +358,6 @@ extension NewsViewController
|
||||
{
|
||||
footerView.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||
footerView.bannerView.button.progress = nil
|
||||
footerView.bannerView.button.isInverted = true
|
||||
footerView.bannerView.button.countdownDate = nil
|
||||
}
|
||||
|
||||
@@ -358,10 +370,7 @@ extension NewsViewController
|
||||
extension NewsViewController: UICollectionViewDelegateFlowLayout
|
||||
{
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||
{
|
||||
let padding = 40 as CGFloat
|
||||
let width = collectionView.bounds.width - padding
|
||||
|
||||
{
|
||||
let item = self.dataSource.item(at: indexPath)
|
||||
|
||||
if let previousSize = self.cachedCellSizes[item.identifier]
|
||||
@@ -369,7 +378,7 @@ extension NewsViewController: UICollectionViewDelegateFlowLayout
|
||||
return previousSize
|
||||
}
|
||||
|
||||
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: width)
|
||||
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
|
||||
NSLayoutConstraint.activate([widthConstraint])
|
||||
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
|
||||
|
||||
@@ -396,7 +405,7 @@ extension NewsViewController: UICollectionViewDelegateFlowLayout
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
|
||||
{
|
||||
var insets = UIEdgeInsets(top: 30, left: 20, bottom: 13, right: 20)
|
||||
var insets = UIEdgeInsets(top: 30, left: 0, bottom: 13, right: 0)
|
||||
|
||||
if section == 0
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ class AppOperationContext
|
||||
var app: ALTApplication?
|
||||
var resignedApp: ALTApplication?
|
||||
|
||||
var connection: NWConnection?
|
||||
var installationConnection: ServerConnection?
|
||||
|
||||
var installedApp: InstalledApp? {
|
||||
didSet {
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
import AltSign
|
||||
|
||||
enum AuthenticationError: LocalizedError
|
||||
@@ -30,13 +32,18 @@ enum AuthenticationError: LocalizedError
|
||||
}
|
||||
|
||||
@objc(AuthenticationOperation)
|
||||
class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
|
||||
{
|
||||
let group: OperationGroup
|
||||
|
||||
private weak var presentingViewController: UIViewController?
|
||||
|
||||
private lazy var navigationController: UINavigationController = {
|
||||
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
|
||||
navigationController.presentationController?.delegate = self
|
||||
if #available(iOS 13.0, *)
|
||||
{
|
||||
navigationController.isModalInPresentation = true
|
||||
}
|
||||
return navigationController
|
||||
}()
|
||||
|
||||
@@ -45,14 +52,18 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
private var appleIDPassword: String?
|
||||
private var shouldShowInstructions = false
|
||||
|
||||
private var signer: ALTSigner?
|
||||
private let operationQueue = OperationQueue()
|
||||
|
||||
init(presentingViewController: UIViewController?)
|
||||
private var submitCodeAction: UIAlertAction?
|
||||
|
||||
init(group: OperationGroup, presentingViewController: UIViewController?)
|
||||
{
|
||||
self.group = group
|
||||
self.presentingViewController = presentingViewController
|
||||
|
||||
super.init()
|
||||
|
||||
self.operationQueue.name = "com.altstore.AuthenticationOperation"
|
||||
self.progress.totalUnitCount = 3
|
||||
}
|
||||
|
||||
@@ -60,18 +71,24 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.group.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
// Sign In
|
||||
self.signIn { (result) in
|
||||
self.signIn() { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let account):
|
||||
case .success(let account, let session):
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Fetch Team
|
||||
self.fetchTeam(for: account) { (result) in
|
||||
self.fetchTeam(for: account, session: session) { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
@@ -81,7 +98,7 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Fetch Certificate
|
||||
self.fetchCertificate(for: team) { (result) in
|
||||
self.fetchCertificate(for: team, session: session) { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
@@ -89,13 +106,24 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let certificate):
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
self.signer = signer
|
||||
|
||||
self.showInstructionsIfNecessary() { (didShowInstructions) in
|
||||
self.finish(.success(signer))
|
||||
}
|
||||
|
||||
// Save account/team to disk.
|
||||
self.save(team) { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
|
||||
// Must cache App IDs _after_ saving account/team to disk.
|
||||
self.cacheAppIDs(signer: signer, session: session) { (result) in
|
||||
let result = result.map { _ in (signer, session) }
|
||||
self.finish(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,21 +132,65 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<ALTSigner, Error>)
|
||||
func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
do
|
||||
{
|
||||
let account: Account
|
||||
let team: Team
|
||||
|
||||
if let tempAccount = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context)
|
||||
{
|
||||
account = tempAccount
|
||||
}
|
||||
else
|
||||
{
|
||||
account = Account(altTeam.account, context: context)
|
||||
}
|
||||
|
||||
if let tempTeam = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
|
||||
{
|
||||
team = tempTeam
|
||||
}
|
||||
else
|
||||
{
|
||||
team = Team(altTeam, account: account, context: context)
|
||||
}
|
||||
|
||||
account.update(account: altTeam.account)
|
||||
team.update(team: altTeam)
|
||||
|
||||
try context.save()
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<(ALTSigner, ALTAppleAPISession), Error>)
|
||||
{
|
||||
guard !self.isFinished else { return }
|
||||
|
||||
print("Finished authenticating with result:", result)
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
context.perform {
|
||||
do
|
||||
{
|
||||
let signer = try result.get()
|
||||
let altAccount = signer.team.account
|
||||
let (signer, session) = try result.get()
|
||||
|
||||
guard
|
||||
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), signer.team.account.identifier), in: context),
|
||||
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), signer.team.identifier), in: context)
|
||||
else { throw AuthenticationError.noTeam }
|
||||
|
||||
// Account
|
||||
let account = Account(altAccount, context: context)
|
||||
account.isActiveAccount = true
|
||||
|
||||
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
|
||||
@@ -131,7 +203,6 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
}
|
||||
|
||||
// Team
|
||||
let team = Team(signer.team, account: account, context: context)
|
||||
team.isActiveTeam = true
|
||||
|
||||
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
|
||||
@@ -143,25 +214,41 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
team.isActiveTeam = false
|
||||
}
|
||||
|
||||
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.team == nil
|
||||
{
|
||||
// No team assigned to AltStore app yet, so assume this team was used to originally install it.
|
||||
altStoreApp.team = team
|
||||
}
|
||||
|
||||
// Save
|
||||
try context.save()
|
||||
|
||||
// Update keychain
|
||||
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
|
||||
Keychain.shared.appleIDEmailAddress = signer.team.account.appleID
|
||||
Keychain.shared.appleIDPassword = self.appleIDPassword
|
||||
|
||||
Keychain.shared.signingCertificateSerialNumber = signer.certificate.serialNumber
|
||||
Keychain.shared.signingCertificatePrivateKey = signer.certificate.privateKey
|
||||
Keychain.shared.signingCertificate = signer.certificate.p12Data()
|
||||
Keychain.shared.signingCertificatePassword = signer.certificate.machineIdentifier
|
||||
|
||||
super.finish(.success(signer))
|
||||
self.showInstructionsIfNecessary() { (didShowInstructions) in
|
||||
|
||||
// Refresh screen must go last since a successful refresh will cause the app to quit.
|
||||
self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in
|
||||
super.finish(result)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
super.finish(.failure(error))
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
super.finish(result)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,21 +281,26 @@ private extension AuthenticationOperation
|
||||
|
||||
private extension AuthenticationOperation
|
||||
{
|
||||
func signIn(completionHandler: @escaping (Result<ALTAccount, Swift.Error>) -> Void)
|
||||
func signIn(completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
|
||||
{
|
||||
func authenticate()
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
|
||||
authenticationViewController.authenticationHandler = { (result) in
|
||||
if let (account, password) = result
|
||||
authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
|
||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
authenticationViewController.completionHandler = { (result) in
|
||||
if let (account, session, password) = result
|
||||
{
|
||||
// We presented the Auth UI and the user signed in.
|
||||
// In this case, we'll assume we should show the instructions again.
|
||||
self.shouldShowInstructions = true
|
||||
|
||||
self.appleIDPassword = password
|
||||
completionHandler(.success(account))
|
||||
completionHandler(.success((account, session)))
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -225,24 +317,17 @@ private extension AuthenticationOperation
|
||||
|
||||
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
|
||||
{
|
||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in
|
||||
do
|
||||
self.authenticate(appleID: appleID, password: password) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let account, let session):
|
||||
self.appleIDPassword = password
|
||||
completionHandler(.success((account, session)))
|
||||
|
||||
let account = try Result(account, error).get()
|
||||
completionHandler(.success(account))
|
||||
}
|
||||
catch ALTAppleAPIError.incorrectCredentials
|
||||
{
|
||||
case .failure(ALTAppleAPIError.incorrectCredentials), .failure(ALTAppleAPIError.appSpecificPasswordRequired):
|
||||
authenticate()
|
||||
}
|
||||
catch ALTAppleAPIError.appSpecificPasswordRequired
|
||||
{
|
||||
authenticate()
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
@@ -253,7 +338,78 @@ private extension AuthenticationOperation
|
||||
}
|
||||
}
|
||||
|
||||
func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
|
||||
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
|
||||
{
|
||||
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(group: self.group)
|
||||
fetchAnisetteDataOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let anisetteData):
|
||||
let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
|
||||
|
||||
if let presentingViewController = self.presentingViewController
|
||||
{
|
||||
verificationHandler = { (completionHandler) in
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
|
||||
alertController.addTextField { (textField) in
|
||||
textField.autocorrectionType = .no
|
||||
textField.autocapitalizationType = .none
|
||||
textField.keyboardType = .numberPad
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
|
||||
}
|
||||
|
||||
let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
|
||||
let textField = alertController.textFields?.first
|
||||
|
||||
let code = textField?.text ?? ""
|
||||
completionHandler(code)
|
||||
}
|
||||
submitAction.isEnabled = false
|
||||
alertController.addAction(submitAction)
|
||||
self.submitCodeAction = submitAction
|
||||
|
||||
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
|
||||
completionHandler(nil)
|
||||
})
|
||||
|
||||
if self.navigationController.presentingViewController != nil
|
||||
{
|
||||
self.navigationController.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No view controller to present security code alert, so don't provide verificationHandler.
|
||||
verificationHandler = nil
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
|
||||
verificationHandler: verificationHandler) { (account, session, error) in
|
||||
if let account = account, let session = session
|
||||
{
|
||||
completionHandler(.success((account, session)))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(error ?? OperationError.unknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.operationQueue.addOperation(fetchAnisetteDataOperation)
|
||||
}
|
||||
|
||||
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
|
||||
{
|
||||
func selectTeam(from teams: [ALTTeam])
|
||||
{
|
||||
@@ -275,7 +431,7 @@ private extension AuthenticationOperation
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
|
||||
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
|
||||
switch Result(teams, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
@@ -294,18 +450,18 @@ private extension AuthenticationOperation
|
||||
}
|
||||
}
|
||||
|
||||
func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
|
||||
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
|
||||
{
|
||||
func requestCertificate()
|
||||
{
|
||||
let machineName = "AltStore - " + UIDevice.current.name
|
||||
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team) { (certificate, error) in
|
||||
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
|
||||
do
|
||||
{
|
||||
let certificate = try Result(certificate, error).get()
|
||||
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
@@ -334,7 +490,7 @@ private extension AuthenticationOperation
|
||||
{
|
||||
guard let certificate = certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
|
||||
|
||||
ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in
|
||||
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
@@ -346,25 +502,51 @@ private extension AuthenticationOperation
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
|
||||
do
|
||||
{
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
if
|
||||
let data = Keychain.shared.signingCertificate,
|
||||
let localCertificate = ALTCertificate(p12Data: data, password: nil),
|
||||
let certificate = certificates.first(where: { $0.serialNumber == localCertificate.serialNumber })
|
||||
{
|
||||
// We have a certificate stored in the keychain and it hasn't been revoked.
|
||||
localCertificate.machineIdentifier = certificate.machineIdentifier
|
||||
completionHandler(.success(localCertificate))
|
||||
}
|
||||
else if
|
||||
let serialNumber = Keychain.shared.signingCertificateSerialNumber,
|
||||
let privateKey = Keychain.shared.signingCertificatePrivateKey,
|
||||
let certificate = certificates.first(where: { $0.serialNumber == serialNumber })
|
||||
{
|
||||
// LEGACY
|
||||
// We have the private key for one of the certificates, so add it to certificate and use it.
|
||||
certificate.privateKey = privateKey
|
||||
completionHandler(.success(certificate))
|
||||
}
|
||||
else if
|
||||
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String,
|
||||
let certificate = certificates.first(where: { $0.serialNumber == serialNumber }),
|
||||
let machineIdentifier = certificate.machineIdentifier,
|
||||
FileManager.default.fileExists(atPath: Bundle.main.certificateURL.path),
|
||||
let data = try? Data(contentsOf: Bundle.main.certificateURL),
|
||||
let localCertificate = ALTCertificate(p12Data: data, password: machineIdentifier)
|
||||
{
|
||||
// We have an embedded certificate that hasn't been revoked.
|
||||
localCertificate.machineIdentifier = machineIdentifier
|
||||
completionHandler(.success(localCertificate))
|
||||
}
|
||||
else if certificates.isEmpty
|
||||
{
|
||||
// No certificates, so request a new one.
|
||||
requestCertificate()
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't have private keys for any of the certificates,
|
||||
// so we need to revoke one and create a new one.
|
||||
replaceCertificate(from: certificates)
|
||||
}
|
||||
}
|
||||
@@ -375,6 +557,30 @@ private extension AuthenticationOperation
|
||||
}
|
||||
}
|
||||
|
||||
func cacheAppIDs(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let group = OperationGroup()
|
||||
group.signer = signer
|
||||
group.session = session
|
||||
|
||||
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
|
||||
fetchAppIDsOperation.resultHandler = { (result) in
|
||||
do
|
||||
{
|
||||
let (_, context) = try result.get()
|
||||
try context.save()
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
self.operationQueue.addOperation(fetchAppIDsOperation)
|
||||
}
|
||||
|
||||
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
|
||||
{
|
||||
guard self.shouldShowInstructions else { return completionHandler(false) }
|
||||
@@ -392,19 +598,40 @@ private extension AuthenticationOperation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthenticationOperation: UIAdaptivePresentationControllerDelegate
|
||||
{
|
||||
func presentationControllerWillDismiss(_ presentationController: UIPresentationController)
|
||||
|
||||
func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
|
||||
{
|
||||
if let signer = self.signer
|
||||
{
|
||||
self.finish(.success(signer))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) }
|
||||
|
||||
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
|
||||
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
|
||||
|
||||
#if DEBUG
|
||||
completionHandler(false)
|
||||
#else
|
||||
DispatchQueue.main.async {
|
||||
let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController
|
||||
refreshViewController.signer = signer
|
||||
refreshViewController.session = session
|
||||
refreshViewController.completionHandler = { _ in
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
if !self.present(refreshViewController)
|
||||
{
|
||||
completionHandler(false)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthenticationOperation
|
||||
{
|
||||
@objc func textFieldTextDidChange(_ notification: Notification)
|
||||
{
|
||||
guard let textField = notification.object as? UITextField else { return }
|
||||
|
||||
self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6
|
||||
}
|
||||
}
|
||||
|
||||
73
AltStore/Operations/FetchAnisetteDataOperation.swift
Normal file
73
AltStore/Operations/FetchAnisetteDataOperation.swift
Normal 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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
AltStore/Operations/FetchAppIDsOperation.swift
Normal file
73
AltStore/Operations/FetchAppIDsOperation.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,16 @@ class FetchSourceOperation: ResultOperation<Source>
|
||||
decoder.managedObjectContext = context
|
||||
|
||||
let source = try decoder.decode(Source.self, from: data)
|
||||
|
||||
if let patreonAccessToken = source.userInfo?[.patreonAccessToken]
|
||||
{
|
||||
Keychain.shared.patreonCreatorAccessToken = patreonAccessToken
|
||||
}
|
||||
|
||||
#if STAGING
|
||||
source.sourceURL = self.sourceURL
|
||||
#endif
|
||||
|
||||
self.finish(.success(source))
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -7,13 +7,26 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltKit
|
||||
import Roxas
|
||||
|
||||
private extension Notification.Name
|
||||
{
|
||||
static let didReceiveWiredServerConnectionResponse = Notification.Name("io.altstore.didReceiveWiredServerConnectionResponse")
|
||||
}
|
||||
|
||||
private let ReceivedWiredServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
|
||||
{ (center, observer, name, object, userInfo) in
|
||||
NotificationCenter.default.post(name: .didReceiveWiredServerConnectionResponse, object: nil)
|
||||
}
|
||||
|
||||
@objc(FindServerOperation)
|
||||
class FindServerOperation: ResultOperation<Server>
|
||||
{
|
||||
let group: OperationGroup
|
||||
|
||||
private var isWiredServerConnectionAvailable = false
|
||||
|
||||
init(group: OperationGroup)
|
||||
{
|
||||
self.group = group
|
||||
@@ -31,21 +44,49 @@ class FindServerOperation: ResultOperation<Server>
|
||||
return
|
||||
}
|
||||
|
||||
if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred })
|
||||
{
|
||||
// Preferred server.
|
||||
self.finish(.success(server))
|
||||
}
|
||||
else if let server = ServerManager.shared.discoveredServers.first
|
||||
{
|
||||
// Any available server.
|
||||
self.finish(.success(server))
|
||||
}
|
||||
else
|
||||
{
|
||||
// No servers.
|
||||
self.finish(.failure(ConnectionError.serverNotFound))
|
||||
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
|
||||
// Prepare observers to receive callback from wired server (if connected).
|
||||
CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedWiredServerConnectionResponse, CFNotificationName.wiredServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(FindServerOperation.didReceiveWiredServerConnectionResponse(_:)), name: .didReceiveWiredServerConnectionResponse, object: nil)
|
||||
|
||||
// Post notification.
|
||||
CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionAvailableRequest, nil, nil, true)
|
||||
|
||||
// Wait for either callback or timeout.
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
|
||||
if self.isWiredServerConnectionAvailable
|
||||
{
|
||||
let server = Server(isWiredConnection: true)
|
||||
self.finish(.success(server))
|
||||
}
|
||||
else
|
||||
{
|
||||
if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred })
|
||||
{
|
||||
// Preferred server.
|
||||
self.finish(.success(server))
|
||||
}
|
||||
else if let server = ServerManager.shared.discoveredServers.first
|
||||
{
|
||||
// Any available server.
|
||||
self.finish(.success(server))
|
||||
}
|
||||
else
|
||||
{
|
||||
// No servers.
|
||||
self.finish(.failure(ConnectionError.serverNotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension FindServerOperation
|
||||
{
|
||||
@objc func didReceiveWiredServerConnectionResponse(_ notification: Notification)
|
||||
{
|
||||
self.isWiredServerConnectionAvailable = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
|
||||
private var didCleanUp = false
|
||||
|
||||
init(context: AppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
@@ -39,12 +41,13 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
guard
|
||||
let resignedApp = self.context.resignedApp,
|
||||
let connection = self.context.connection,
|
||||
let server = self.context.group.server
|
||||
let connection = self.context.installationConnection
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
backgroundContext.perform {
|
||||
|
||||
/* App */
|
||||
let installedApp: InstalledApp
|
||||
|
||||
// Fetch + update rather than insert + resolve merge conflicts to prevent potential context-level conflicts.
|
||||
@@ -57,24 +60,64 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, context: backgroundContext)
|
||||
}
|
||||
|
||||
installedApp.version = resignedApp.version
|
||||
|
||||
if let profile = resignedApp.provisioningProfile
|
||||
installedApp.update(resignedApp: resignedApp)
|
||||
|
||||
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
|
||||
{
|
||||
installedApp.refreshedDate = profile.creationDate
|
||||
installedApp.expirationDate = profile.expirationDate
|
||||
installedApp.team = team
|
||||
}
|
||||
|
||||
/* App Extensions */
|
||||
var installedExtensions = Set<InstalledExtension>()
|
||||
|
||||
if
|
||||
let bundle = Bundle(url: resignedApp.fileURL),
|
||||
let directory = bundle.builtInPlugInsURL,
|
||||
let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
||||
{
|
||||
for case let fileURL as URL in enumerator
|
||||
{
|
||||
guard let appExtensionBundle = Bundle(url: fileURL) else { continue }
|
||||
guard let appExtension = ALTApplication(fileURL: appExtensionBundle.bundleURL) else { continue }
|
||||
|
||||
let parentBundleID = self.context.bundleIdentifier
|
||||
let resignedParentBundleID = resignedApp.bundleIdentifier
|
||||
|
||||
let resignedBundleID = appExtension.bundleIdentifier
|
||||
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
|
||||
|
||||
let installedExtension: InstalledExtension
|
||||
|
||||
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
|
||||
{
|
||||
installedExtension = appExtension
|
||||
}
|
||||
else
|
||||
{
|
||||
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: backgroundContext)
|
||||
}
|
||||
|
||||
installedExtension.update(resignedAppExtension: appExtension)
|
||||
|
||||
installedExtensions.insert(installedExtension)
|
||||
}
|
||||
}
|
||||
|
||||
installedApp.appExtensions = installedExtensions
|
||||
|
||||
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
|
||||
self.cleanUp()
|
||||
|
||||
self.context.group.beginInstallationHandler?(installedApp)
|
||||
|
||||
let request = BeginInstallationRequest()
|
||||
server.send(request, via: connection) { (result) in
|
||||
connection.send(request) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
|
||||
self.receive(from: connection, server: server) { (result) in
|
||||
self.receive(from: connection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
@@ -92,27 +135,43 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
}
|
||||
}
|
||||
|
||||
func receive(from connection: NWConnection, server: Server, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
override func finish(_ result: Result<InstalledApp, Error>)
|
||||
{
|
||||
server.receive(ServerResponse.self, from: connection) { (result) in
|
||||
self.cleanUp()
|
||||
|
||||
super.finish(result)
|
||||
}
|
||||
}
|
||||
|
||||
private extension InstallAppOperation
|
||||
{
|
||||
func receive(from connection: ServerConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
connection.receiveResponse() { (result) in
|
||||
do
|
||||
{
|
||||
let response = try result.get()
|
||||
print(response)
|
||||
|
||||
if let error = response.error
|
||||
switch response
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else if response.progress == 1.0
|
||||
{
|
||||
self.progress.completedUnitCount = self.progress.totalUnitCount
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.progress.completedUnitCount = Int64(response.progress * 100)
|
||||
self.receive(from: connection, server: server, completionHandler: completionHandler)
|
||||
case .installationProgress(let response):
|
||||
if response.progress == 1.0
|
||||
{
|
||||
self.progress.completedUnitCount = self.progress.totalUnitCount
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.progress.completedUnitCount = Int64(response.progress * 100)
|
||||
self.receive(from: connection, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
case .error(let response):
|
||||
completionHandler(.failure(response.error))
|
||||
|
||||
default:
|
||||
completionHandler(.failure(ALTServerError(.unknownRequest)))
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -121,4 +180,25 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cleanUp()
|
||||
{
|
||||
guard !self.didCleanUp else { return }
|
||||
self.didCleanUp = true
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: self.context.temporaryDirectory)
|
||||
|
||||
if let app = self.context.app
|
||||
{
|
||||
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove temporary directory.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ enum OperationError: LocalizedError
|
||||
case invalidParameters
|
||||
|
||||
case iOSVersionNotSupported(ALTApplication)
|
||||
case sideloadingAppNotSupported(ALTApplication)
|
||||
case maximumAppIDLimitReached(application: ALTApplication, requiredAppIDs: Int, availableAppIDs: Int, nextExpirationDate: Date)
|
||||
|
||||
case noSources
|
||||
|
||||
@@ -49,6 +51,53 @@ enum OperationError: LocalizedError
|
||||
|
||||
let localizedDescription = String(format: NSLocalizedString("%@ requires %@.", comment: ""), name, version)
|
||||
return localizedDescription
|
||||
|
||||
case .sideloadingAppNotSupported(let app):
|
||||
let localizedDescription = String(format: NSLocalizedString("Sideloading “%@” Not Supported", comment: ""), app.name)
|
||||
return localizedDescription
|
||||
|
||||
case .maximumAppIDLimitReached: return NSLocalizedString("Cannot register more than 10 App IDs.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
switch self
|
||||
{
|
||||
case .maximumAppIDLimitReached(let application, let requiredAppIDs, let availableAppIDs, let date):
|
||||
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
|
||||
let message: String
|
||||
|
||||
if requiredAppIDs > 1
|
||||
{
|
||||
let availableText: String
|
||||
|
||||
switch availableAppIDs
|
||||
{
|
||||
case 0: availableText = NSLocalizedString("none are available", comment: "")
|
||||
case 1: availableText = NSLocalizedString("only 1 is available", comment: "")
|
||||
default: availableText = String(format: NSLocalizedString("only %@ are available", comment: ""), NSNumber(value: availableAppIDs))
|
||||
}
|
||||
|
||||
let prefixMessage = String(format: NSLocalizedString("%@ requires %@ App IDs, but %@.", comment: ""), application.name, NSNumber(value: requiredAppIDs), availableText)
|
||||
message = prefixMessage + " " + baseMessage
|
||||
}
|
||||
else
|
||||
{
|
||||
let dateComponents = Calendar.current.dateComponents([.day, .hour, .minute], from: Date(), to: date)
|
||||
|
||||
let dateComponentsFormatter = DateComponentsFormatter()
|
||||
dateComponentsFormatter.maximumUnitCount = 1
|
||||
dateComponentsFormatter.unitsStyle = .full
|
||||
|
||||
let remainingTime = dateComponentsFormatter.string(from: dateComponents)!
|
||||
|
||||
let remainingTimeMessage = String(format: NSLocalizedString("You can register another App ID in %@.", comment: ""), remainingTime)
|
||||
message = baseMessage + " " + remainingTimeMessage
|
||||
}
|
||||
|
||||
return message
|
||||
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ class OperationGroup
|
||||
var completionHandler: ((Result<[String: Result<InstalledApp, Error>], Error>) -> Void)?
|
||||
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
||||
|
||||
var session: ALTAppleAPISession?
|
||||
|
||||
var server: Server?
|
||||
var signer: ALTSigner?
|
||||
|
||||
@@ -73,7 +75,12 @@ class OperationGroup
|
||||
|
||||
func progress(for app: AppProtocol) -> Progress?
|
||||
{
|
||||
let progress = self.progressByBundleIdentifier[app.bundleIdentifier]
|
||||
return self.progress(forAppWithBundleIdentifier: app.bundleIdentifier)
|
||||
}
|
||||
|
||||
func progress(forAppWithBundleIdentifier bundleIdentifier: String) -> Progress?
|
||||
{
|
||||
let progress = self.progressByBundleIdentifier[bundleIdentifier]
|
||||
return progress
|
||||
}
|
||||
}
|
||||
|
||||
81
AltStore/Operations/PrepareDeveloperAccountOperation.swift
Normal file
81
AltStore/Operations/PrepareDeveloperAccountOperation.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,49 +37,45 @@ class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
|
||||
guard
|
||||
let app = self.context.app,
|
||||
let signer = self.context.group.signer
|
||||
let signer = self.context.group.signer,
|
||||
let session = self.context.group.session
|
||||
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
// Register Device
|
||||
self.registerCurrentDevice(for: signer.team) { (result) in
|
||||
guard let _ = self.process(result) else { return }
|
||||
// Prepare Provisioning Profiles
|
||||
self.prepareProvisioningProfiles(app.fileURL, team: signer.team, session: session) { (result) in
|
||||
guard let profiles = self.process(result) else { return }
|
||||
|
||||
// Prepare Provisioning Profiles
|
||||
self.prepareProvisioningProfiles(app.fileURL, team: signer.team) { (result) in
|
||||
guard let profiles = self.process(result) else { return }
|
||||
// Prepare app bundle
|
||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
||||
|
||||
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
|
||||
guard let appBundleURL = self.process(result) else { return }
|
||||
|
||||
// Prepare app bundle
|
||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
||||
print("Resigning App:", self.context.bundleIdentifier)
|
||||
|
||||
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
|
||||
guard let appBundleURL = self.process(result) else { return }
|
||||
// Resign app bundle
|
||||
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in
|
||||
guard let resignedURL = self.process(result) else { return }
|
||||
|
||||
print("Resigning App:", self.context.bundleIdentifier)
|
||||
|
||||
// Resign app bundle
|
||||
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in
|
||||
guard let resignedURL = self.process(result) else { return }
|
||||
// Finish
|
||||
do
|
||||
{
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
// Finish
|
||||
do
|
||||
{
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
self.finish(.success(resignedApplication))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
self.finish(.success(resignedApplication))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
|
||||
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,114 +100,113 @@ class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
|
||||
private extension ResignAppOperation
|
||||
{
|
||||
func registerCurrentDevice(for team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
|
||||
{
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
|
||||
return completionHandler(.failure(OperationError.unknownUDID))
|
||||
}
|
||||
guard let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
|
||||
|
||||
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
|
||||
do
|
||||
{
|
||||
let devices = try Result(devices, error).get()
|
||||
|
||||
if let device = devices.first(where: { $0.identifier == udid })
|
||||
{
|
||||
completionHandler(.success(device))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team) { (device, error) in
|
||||
completionHandler(Result(device, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
|
||||
{
|
||||
guard let bundle = Bundle(url: fileURL), let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
var profiles = [String: ALTProvisioningProfile]()
|
||||
var error: Error?
|
||||
|
||||
dispatchGroup.enter()
|
||||
|
||||
self.prepareProvisioningProfile(for: app, team: team) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let e): error = e
|
||||
case .success(let profile):
|
||||
profiles[app.bundleIdentifier] = profile
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
if let directory = bundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
||||
{
|
||||
for case let fileURL as URL in enumerator where fileURL.pathExtension.lowercased() == "appex"
|
||||
{
|
||||
guard let appExtension = ALTApplication(fileURL: fileURL) else { continue }
|
||||
|
||||
dispatchGroup.enter()
|
||||
|
||||
self.prepareProvisioningProfile(for: appExtension, team: team) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let e): error = e
|
||||
case .success(let profile):
|
||||
profiles[appExtension.bundleIdentifier] = profile
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
if let error = error
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(profiles))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepareProvisioningProfile(for app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
// Register
|
||||
self.register(app, team: team) { (result) in
|
||||
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
case .success(let profile):
|
||||
var profiles = [app.bundleIdentifier: profile]
|
||||
var error: Error?
|
||||
|
||||
// Update features
|
||||
self.updateFeatures(for: appID, app: app, team: team) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for appExtension in app.appExtensions
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let e): error = e
|
||||
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
|
||||
}
|
||||
|
||||
// Update app groups
|
||||
self.updateAppGroups(for: appID, app: app, team: team) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Fetch Provisioning Profile
|
||||
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
|
||||
completionHandler(result)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
if let error = error
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(profiles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
|
||||
let preferredBundleID: String
|
||||
|
||||
// Check if we have already installed this app with this team before.
|
||||
let predicate = NSPredicate(format: "%K == %@ AND %K == %@",
|
||||
#keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier,
|
||||
#keyPath(InstalledApp.team.identifier), team.identifier)
|
||||
if let installedApp = InstalledApp.first(satisfying: predicate, in: context)
|
||||
{
|
||||
// This app is already installed, so use the same resigned bundle identifier as before.
|
||||
// This way, if we change the identifier format (again), AltStore will continue to use
|
||||
// the old bundle identifier to prevent it from installing as a new app.
|
||||
preferredBundleID = installedApp.resignedBundleIdentifier
|
||||
}
|
||||
else
|
||||
{
|
||||
// This app isn't already installed, so create the resigned bundle identifier ourselves.
|
||||
// Or, if the app _is_ installed but with a different team, we need to create a new
|
||||
// bundle identifier anyway to prevent collisions with the previous team.
|
||||
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
|
||||
let updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
|
||||
|
||||
preferredBundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
|
||||
}
|
||||
|
||||
let preferredName: String
|
||||
|
||||
if let parentApp = parentApp
|
||||
{
|
||||
preferredName = "\(parentApp.name) - \(app.name)"
|
||||
}
|
||||
else
|
||||
{
|
||||
preferredName = app.name
|
||||
}
|
||||
|
||||
// Register
|
||||
self.registerAppID(for: app, name: preferredName, bundleIdentifier: preferredBundleID, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Update features
|
||||
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Update app groups
|
||||
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let appID):
|
||||
|
||||
// Fetch Provisioning Profile
|
||||
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,24 +216,63 @@ private extension ResignAppOperation
|
||||
}
|
||||
}
|
||||
|
||||
func register(_ app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let appName = app.name
|
||||
let bundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
|
||||
|
||||
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
|
||||
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
|
||||
do
|
||||
{
|
||||
let appIDs = try Result(appIDs, error).get()
|
||||
|
||||
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleID })
|
||||
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleIdentifier })
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
let requiredAppIDs = 1 + application.appExtensions.count
|
||||
let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count)
|
||||
|
||||
let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 })
|
||||
|
||||
if team.type == .free
|
||||
{
|
||||
if requiredAppIDs > availableAppIDs
|
||||
{
|
||||
if let expirationDate = sortedExpirationDates.first
|
||||
{
|
||||
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ALTAppleAPIError(.maximumAppIDLimitReached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
let appID = try Result(appID, error).get()
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
catch ALTAppleAPIError.maximumAppIDLimitReached
|
||||
{
|
||||
if let expirationDate = sortedExpirationDates.first
|
||||
{
|
||||
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ALTAppleAPIError(.maximumAppIDLimitReached)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,7 +283,7 @@ private extension ResignAppOperation
|
||||
}
|
||||
}
|
||||
|
||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
||||
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
|
||||
@@ -263,15 +297,41 @@ private extension ResignAppOperation
|
||||
features[.appGroups] = true
|
||||
}
|
||||
|
||||
let appID = appID.copy() as! ALTAppID
|
||||
appID.features = features
|
||||
var updateFeatures = false
|
||||
|
||||
ALTAppleAPI.shared.update(appID, team: team) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
// Determine whether the required features are already enabled for the AppID.
|
||||
for (feature, value) in features
|
||||
{
|
||||
if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue)
|
||||
{
|
||||
// AppID already has this feature enabled and the values are the same.
|
||||
continue
|
||||
}
|
||||
else
|
||||
{
|
||||
// AppID either doesn't have this feature enabled or the value has changed,
|
||||
// so we need to update it to reflect new values.
|
||||
updateFeatures = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if updateFeatures
|
||||
{
|
||||
let appID = appID.copy() as! ALTAppID
|
||||
appID.features = features
|
||||
|
||||
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
// TODO: Handle apps belonging to more than one app group.
|
||||
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
|
||||
@@ -287,7 +347,7 @@ private extension ResignAppOperation
|
||||
// Assign App Group
|
||||
// TODO: Determine whether app already belongs to app group.
|
||||
|
||||
ALTAppleAPI.shared.add(appID, to: group, team: team) { (success, error) in
|
||||
ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
|
||||
let result = result.map { _ in appID }
|
||||
completionHandler(result)
|
||||
}
|
||||
@@ -296,7 +356,7 @@ private extension ResignAppOperation
|
||||
|
||||
let adjustedGroupIdentifier = "group.\(team.identifier)." + groupIdentifier
|
||||
|
||||
ALTAppleAPI.shared.fetchAppGroups(for: team) { (groups, error) in
|
||||
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
|
||||
switch Result(groups, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
@@ -311,7 +371,7 @@ private extension ResignAppOperation
|
||||
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
|
||||
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
|
||||
|
||||
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team) { (group, error) in
|
||||
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
|
||||
finish(Result(group, error))
|
||||
}
|
||||
}
|
||||
@@ -319,23 +379,23 @@ private extension ResignAppOperation
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
|
||||
switch Result(profile, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let profile):
|
||||
|
||||
// Delete existing profile
|
||||
ALTAppleAPI.shared.delete(profile, for: team) { (success, error) in
|
||||
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success:
|
||||
|
||||
// Fetch new provisiong profile
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
}
|
||||
@@ -371,6 +431,17 @@ private extension ResignAppOperation
|
||||
infoDictionary[Bundle.Info.appGroups] = appGroups
|
||||
}
|
||||
|
||||
// Add app-specific exported UTI so we can check later if this app (extension) is installed or not.
|
||||
let installedAppUTI = ["UTTypeConformsTo": [],
|
||||
"UTTypeDescription": "AltStore Installed App",
|
||||
"UTTypeIconFiles": [],
|
||||
"UTTypeIdentifier": InstalledApp.installedAppUTI(forBundleIdentifier: profile.bundleIdentifier),
|
||||
"UTTypeTagSpecification": [:]] as [String : Any]
|
||||
|
||||
var exportedUTIs = infoDictionary[Bundle.Info.exportedUTIs] as? [[String: Any]] ?? []
|
||||
exportedUTIs.append(installedAppUTI)
|
||||
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
|
||||
|
||||
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
||||
}
|
||||
|
||||
@@ -400,6 +471,21 @@ private extension ResignAppOperation
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
|
||||
additionalValues[Bundle.Info.deviceID] = udid
|
||||
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
|
||||
|
||||
if
|
||||
let data = Keychain.shared.signingCertificate,
|
||||
let signingCertificate = ALTCertificate(p12Data: data, password: nil),
|
||||
let encryptingPassword = Keychain.shared.signingCertificatePassword
|
||||
{
|
||||
additionalValues[Bundle.Info.certificateID] = signingCertificate.serialNumber
|
||||
|
||||
let encryptedData = signingCertificate.encryptedP12Data(withPassword: encryptingPassword)
|
||||
try encryptedData?.write(to: appBundle.certificateURL, options: .atomic)
|
||||
}
|
||||
else
|
||||
{
|
||||
// The embedded certificate + certificate identifier are already in app bundle, no need to update them.
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare app
|
||||
|
||||
@@ -12,13 +12,13 @@ import Network
|
||||
import AltKit
|
||||
|
||||
@objc(SendAppOperation)
|
||||
class SendAppOperation: ResultOperation<NWConnection>
|
||||
class SendAppOperation: ResultOperation<ServerConnection>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "com.altstore.SendAppOperation")
|
||||
|
||||
private var connection: NWConnection?
|
||||
private var serverConnection: ServerConnection?
|
||||
|
||||
init(context: AppOperationContext)
|
||||
{
|
||||
@@ -45,21 +45,21 @@ class SendAppOperation: ResultOperation<NWConnection>
|
||||
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
|
||||
// Connect to server.
|
||||
self.connect(to: server) { (result) in
|
||||
ServerManager.shared.connect(to: server) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let connection):
|
||||
self.connection = connection
|
||||
case .success(let serverConnection):
|
||||
self.serverConnection = serverConnection
|
||||
|
||||
// Send app to server.
|
||||
self.sendApp(at: fileURL, via: connection, server: server) { (result) in
|
||||
self.sendApp(at: fileURL, via: serverConnection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
self.progress.completedUnitCount += 1
|
||||
self.finish(.success(connection))
|
||||
self.finish(.success(serverConnection))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,34 +69,7 @@ class SendAppOperation: ResultOperation<NWConnection>
|
||||
|
||||
private extension SendAppOperation
|
||||
{
|
||||
func connect(to server: Server, completionHandler: @escaping (Result<NWConnection, Error>) -> Void)
|
||||
{
|
||||
let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp)
|
||||
|
||||
connection.stateUpdateHandler = { [unowned connection] (state) in
|
||||
switch state
|
||||
{
|
||||
case .failed(let error):
|
||||
print("Failed to connect to service \(server.service.name).", error)
|
||||
completionHandler(.failure(ConnectionError.connectionFailed))
|
||||
|
||||
case .cancelled:
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
|
||||
case .ready:
|
||||
completionHandler(.success(connection))
|
||||
|
||||
case .waiting: break
|
||||
case .setup: break
|
||||
case .preparing: break
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: self.dispatchQueue)
|
||||
}
|
||||
|
||||
func sendApp(at fileURL: URL, via connection: NWConnection, server: Server, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
func sendApp(at fileURL: URL, via connection: ServerConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
@@ -106,14 +79,14 @@ private extension SendAppOperation
|
||||
let request = PrepareAppRequest(udid: udid, contentSize: appData.count)
|
||||
|
||||
print("Sending request \(request)")
|
||||
server.send(request, via: connection) { (result) in
|
||||
connection.send(request) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success:
|
||||
|
||||
print("Sending app data (\(appData.count) bytes)")
|
||||
server.send(appData, via: connection, prependSize: false) { (result) in
|
||||
connection.send(appData, prependSize: false) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
|
||||
@@ -11,7 +11,6 @@ import AuthenticationServices
|
||||
|
||||
private let clientID = "ZMx0EGUWe4TVWYXNZZwK_fbIK5jHFVWoUf1Qb-sqNXmT-YzAGwDPxxq7ak3_W5Q2"
|
||||
private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt"
|
||||
private let creatorAccessToken = "NSX1ts9Rf9IzKRCu8GjbwsZ6wll8bDtoJxNbPbp2eZo"
|
||||
|
||||
private let campaignID = "2863968"
|
||||
|
||||
@@ -71,7 +70,7 @@ extension PatreonAPI
|
||||
}
|
||||
}
|
||||
|
||||
class PatreonAPI
|
||||
class PatreonAPI: NSObject
|
||||
{
|
||||
static let shared = PatreonAPI()
|
||||
|
||||
@@ -84,8 +83,9 @@ class PatreonAPI
|
||||
private let session = URLSession(configuration: .ephemeral)
|
||||
private let baseURL = URL(string: "https://www.patreon.com/")!
|
||||
|
||||
private init()
|
||||
private override init()
|
||||
{
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,11 @@ extension PatreonAPI
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 13.0, *)
|
||||
{
|
||||
self.authenticationSession?.presentationContextProvider = self
|
||||
}
|
||||
|
||||
self.authenticationSession?.start()
|
||||
}
|
||||
|
||||
@@ -165,7 +170,8 @@ extension PatreonAPI
|
||||
var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(campaignID)/members")!
|
||||
components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"),
|
||||
URLQueryItem(name: "fields[tier]", value: "title"),
|
||||
URLQueryItem(name: "fields[member]", value: "full_name,patron_status")]
|
||||
URLQueryItem(name: "fields[member]", value: "full_name,patron_status"),
|
||||
URLQueryItem(name: "page[size]", value: "1000")]
|
||||
|
||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
||||
|
||||
@@ -342,7 +348,9 @@ private extension PatreonAPI
|
||||
{
|
||||
case .none: break
|
||||
case .creator:
|
||||
guard let creatorAccessToken = Keychain.shared.patreonCreatorAccessToken else { return completion(.failure(Error.invalidAccessToken)) }
|
||||
request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization")
|
||||
|
||||
case .user:
|
||||
guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(Error.notAuthenticated)) }
|
||||
request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
|
||||
@@ -384,3 +392,12 @@ private extension PatreonAPI
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension PatreonAPI: ASWebAuthenticationPresentationContextProviding
|
||||
{
|
||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor
|
||||
{
|
||||
return UIApplication.shared.keyWindow ?? UIWindow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,14 +7,14 @@
|
||||
"name": "AltStore",
|
||||
"bundleIdentifier": "com.rileytestut.AltStore",
|
||||
"developerName": "Riley Testut",
|
||||
"version": "1.0.1",
|
||||
"versionDate": "2019-09-28T03:00:00-07:00",
|
||||
"versionDescription": "Fixes Patreon bugs.",
|
||||
"version": "1.2",
|
||||
"versionDate": "2020-02-12T08:00:00-08:00",
|
||||
"versionDescription": "FEATURES\n• Install and refresh apps over USB, not just WiFi (requires updating to latest AltServer version)\n• Supports sideloading unc0ver (support for sideloading any app coming soon)\n\nBUG FIXES\n• Fixes \"Device Already Registered\" error\n• Fixes \"Session Expired\" error when installing apps on slow connection",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/altstore.ipa",
|
||||
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis initial release of AltStore allows you to install Delta, an all-in-one emulator for iOS, with support for installing 3rd party apps coming soon.",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
|
||||
"tintColor": "018084",
|
||||
"size": 3481256,
|
||||
"size": 2114068,
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/65605563-2f009d00-df5e-11e9-9b40-1f36135d5c80.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/65605569-30ca6080-df5e-11e9-8dfb-15ebb00e10cb.PNG",
|
||||
@@ -35,14 +35,15 @@
|
||||
"name": "AltStore",
|
||||
"bundleIdentifier": "com.rileytestut.AltStore.Beta",
|
||||
"developerName": "Riley Testut",
|
||||
"version": "1.0.1b",
|
||||
"versionDate": "2019-09-28T03:00:00-07:00",
|
||||
"versionDescription": "- Adds support for sideloading apps via \"Open In...\"\n- Fixes Patreon bugs",
|
||||
"subtitle": "An alternative App Store for iOS.",
|
||||
"version": "1.2b4",
|
||||
"versionDate": "2020-02-11T18:30:00-08:00",
|
||||
"versionDescription": "• View all registered App IDs for your account\n• Fixed inaccurate remaining App ID count\n\nThis is the final beta for AltStore 1.2 before the public release, which includes support for installing apps completely over USB. To test installing over USB, make sure to download the AltServer beta from https://altstore.io/altserver/beta",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/altstore-beta.ipa",
|
||||
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore allows you to install Delta as well as any app (.ipa) directly from the Files app.",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
|
||||
"tintColor": "018084",
|
||||
"size": 3481256,
|
||||
"size": 2114670,
|
||||
"beta": true,
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/65605563-2f009d00-df5e-11e9-9b40-1f36135d5c80.PNG",
|
||||
@@ -65,14 +66,14 @@
|
||||
"bundleIdentifier": "com.rileytestut.Delta",
|
||||
"developerName": "Riley Testut",
|
||||
"subtitle": "Classic games in your pocket.",
|
||||
"version": "1.0",
|
||||
"versionDate": "2019-09-28T12:00:00-07:00",
|
||||
"versionDescription": "Initial version.",
|
||||
"version": "1.1.2",
|
||||
"versionDate": "2020-02-04T15:30:00-08:00",
|
||||
"versionDescription": "• Fixes crash when running on iOS 13.3.1",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/delta.ipa",
|
||||
"localizedDescription": "Delta is an all-in-one emulator for iOS. Delta builds upon the strengths of its predecessor, GBA4iOS, while expanding to include support for more game systems such as NES, SNES, and N64.\n\nFEATURES\n\nSupported Game Systems\n• Nintendo Entertainment System\n• Super Nintendo Entertainment System\n• Nintendo 64\n• Game Boy (Color)\n• Game Boy Advance\n• And plenty more to come!\n\nController Support\n• Supports PS4, Xbox One S, and MFi game controllers.\n• Supports bluetooth (and wired) keyboards, as well as the Apple Smart Keyboard.\n• Completely customize button mappings on a per-system, per-controller basis.\n• Map buttons to special “Quick Save”, “Quick Load,” and “Fast Forward” actions.\n\nSave States\n• Save and load save states for any game from the pause menu.\n• Lock save states to prevent them from being accidentally overwritten.\n• Automatically makes backup save states to ensure you never lose your progress.\n• Support for “Quick Saves,” save states that can be quickly saved/loaded with a single button press (requires external controller).\n\nCheats\n• Supports various types of cheat codes for each supported system:\n• NES: Game Genie\n• SNES: Game Genie, Pro Action Replay\n• N64: GameShark\n• GBC: Game Genie, GameShark\n• GBA: Action Replay, Code Breaker, GameShark\n\nDelta Sync\n• Sync your games, game saves, save states, cheats, controller skins, and controller mappings between devices.\n• View version histories of everything you sync and optionally restore them to earlier versions.\n• Supports both Google Drive and Dropbox.\n\nCustom Controller Skins\n• Beautiful built-in controller skins for all systems.\n• Import controller skins made by others, or even make your own to share with the world!\n\nHold Button\n• Choose buttons for Delta to hold down on your behalf, freeing up your thumbs to press other buttons instead.\n• Perfect for games that typically require one button be held down constantly (ex: run button in Mario games, or the A button in Mario Kart).\n\nFast Forward\n• Speed through slower parts of games by running the game much faster than normal.\n• Easily enable or disable from the pause menu, or optionally with a mapped button on an external controller.\n\n3D/Haptic Touch\n• Use 3D or Haptic Touch to “peek” at games, save states, and cheat codes.\n• App icon shortcuts allow quick access to your most recently played games, or optionally customize the shortcuts to always include certain games.\n\nGame Artwork\n• Automatically displays appropriate box art for imported games.\n• Change a game’s artwork to anything you want, or select from the built-in game artwork database.\n\nMisc.\n• Gyroscope support for WarioWare: Twisted!\n• Support for delta:// URL scheme to jump directly into a specific game.\n\n**Delta and AltStore LLC are in no way affiliated with Nintendo. The name \"Nintendo\" and all associated game console names are registered trademarks of Nintendo Co., Ltd.**",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
|
||||
"tintColor": "8A28F7",
|
||||
"size": 23075523,
|
||||
"size": 17542718,
|
||||
"permissions": [
|
||||
{
|
||||
"type": "photos",
|
||||
@@ -91,14 +92,14 @@
|
||||
"bundleIdentifier": "com.rileytestut.Delta.Beta",
|
||||
"developerName": "Riley Testut",
|
||||
"subtitle": "Classic games in your pocket.",
|
||||
"version": "1.0b",
|
||||
"versionDate": "2019-09-28T12:00:00-07:00",
|
||||
"versionDescription": "Includes initial support for DS games.",
|
||||
"version": "1.2b",
|
||||
"versionDate": "2020-02-11T16:30:00-08:00",
|
||||
"versionDescription": "GENERAL\n• Replaces 3D Touch peek/pop with iOS 13 context menus\n\nCONTROLLER SKINS\n• Brand new Nintendo DS controller skin\n• New Quick Save, Quick Load, and Fast Forward inputs for controller skins\n• Apply video filters with controller skins\n• Assign controller skins to individual games",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/delta-beta.ipa",
|
||||
"localizedDescription": "The next console for Delta is coming: this beta version of Delta brings support for playing DS games!\n\nDS support currently includes:\n• Playing DS games\n• Save States\n• Hold Button\n\nFeatures I'm still working on:\n• Fast Forward\n• Cheats\n• Controller skin (using placeholder controller skin for now)\n\nPlease report any issues you find to support@altstore.io. Thanks!",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
|
||||
"tintColor": "8A28F7",
|
||||
"size": 23075071,
|
||||
"size": 17631726,
|
||||
"beta": true,
|
||||
"permissions": [
|
||||
{
|
||||
@@ -114,6 +115,31 @@
|
||||
"https://user-images.githubusercontent.com/705880/65601117-58b5c600-df56-11e9-9c19-9a5ba5da54cf.PNG"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Clip",
|
||||
"bundleIdentifier": "com.rileytestut.Clip.Beta",
|
||||
"subtitle": "Manage your clipboard history with ease.",
|
||||
"developerName": "Riley Testut",
|
||||
"version": "1.0b",
|
||||
"versionDate": "2019-01-24T18:20:00-08:00",
|
||||
"versionDescription": "• Disables notification vibration when sound is disabled for notifications.\n\nAssuming all goes well, this should be the final beta before the public launch! Please report any last minute bugs you find to support@altstore.io.",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/clip-beta.ipa",
|
||||
"localizedDescription": "Clip is a simple clipboard manager for iOS. \n\nUnlike other clipboard managers, Clip can continue monitoring your clipboard while in the background. No longer do you need to remember to manually open or share to an app to save your clipboard; just copy and paste as you would normally do, and Clip will have your back.\n\nIn addition to background monitoring, Clip also has these features:\n\n• Save text, URLs, and images copied to the clipboard.\n• Copy, delete, or share any clippings saved to Clip.\n• Customizable history limit.\n\nDownload Clip today, and never worry about losing your clipboard again!",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391981-5326f800-c37a-11e9-99d8-760fd06bb601.png",
|
||||
"tintColor": "EC008C",
|
||||
"size": 462771,
|
||||
"beta": true,
|
||||
"permissions": [
|
||||
{
|
||||
"type": "background-audio",
|
||||
"usageDescription": "Allows Clip to continuously monitor your clipboard in the background."
|
||||
}
|
||||
],
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png",
|
||||
"https://user-images.githubusercontent.com/705880/70830209-8e738980-1da4-11ea-8b3b-6e5fbc78adff.png"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Delta Lite",
|
||||
"bundleIdentifier": "com.rileytestut.Delta.Lite",
|
||||
@@ -194,12 +220,17 @@
|
||||
"notify": false
|
||||
},
|
||||
{
|
||||
"title": "Prevent AltStore Expiring",
|
||||
"identifier": "altstore-expiring-bug-fix",
|
||||
"caption": "Refresh AltStore at least once in “My Apps” to prevent it from expiring early. An update is coming soon to fix this bug.",
|
||||
"tintColor": "fd423a",
|
||||
"date": "2019-10-09",
|
||||
"title": "Coming Soon: Clip",
|
||||
"identifier": "clip-coming-soon",
|
||||
"caption": "A clipboard manager that can run in the background. Beta available now for all Patrons.",
|
||||
"tintColor": "EC008C",
|
||||
"url": "https://twitter.com/altstoreio/status/1205597959699582977",
|
||||
"imageURL": "https://user-images.githubusercontent.com/705880/65606598-04afdf00-df60-11e9-8f93-af6345d39557.png",
|
||||
"date": "2019-12-16",
|
||||
"notify": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"userInfo": {
|
||||
"patreonAccessToken": "JLh5bpOQsPg-HIRe6FHnRAktoeeU4JT5-Xk-y7njQrM"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,139 +44,22 @@ enum ConnectionError: LocalizedError
|
||||
|
||||
struct Server: Equatable
|
||||
{
|
||||
var identifier: String
|
||||
var service: NetService
|
||||
var identifier: String? = nil
|
||||
var service: NetService? = nil
|
||||
|
||||
var isPreferred = false
|
||||
|
||||
var isWiredConnection = false
|
||||
}
|
||||
|
||||
extension Server
|
||||
{
|
||||
// Defined in extension so we can still use the automatically synthesized initializer.
|
||||
init?(service: NetService, txtData: Data)
|
||||
{
|
||||
{
|
||||
let txtDictionary = NetService.dictionary(fromTXTRecord: txtData)
|
||||
guard let identifierData = txtDictionary["serverID"], let identifier = String(data: identifierData, encoding: .utf8) else { return nil }
|
||||
|
||||
self.identifier = identifier
|
||||
self.service = service
|
||||
}
|
||||
|
||||
func send<T: Encodable>(_ payload: T, via connection: NWConnection, prependSize: Bool = true, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
let data: Data
|
||||
|
||||
if let payload = payload as? Data
|
||||
{
|
||||
data = payload
|
||||
}
|
||||
else
|
||||
{
|
||||
data = try JSONEncoder().encode(payload)
|
||||
}
|
||||
|
||||
func process(_ error: Error?) -> Bool
|
||||
{
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(ConnectionError.connectionDropped))
|
||||
return false
|
||||
}
|
||||
else
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if prependSize
|
||||
{
|
||||
let requestSize = Int32(data.count)
|
||||
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
|
||||
|
||||
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
|
||||
guard process(error) else { return }
|
||||
|
||||
connection.send(content: data, completion: .contentProcessed { (error) in
|
||||
guard process(error) else { return }
|
||||
completionHandler(.success(()))
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
connection.send(content: data, completion: .contentProcessed { (error) in
|
||||
guard process(error) else { return }
|
||||
completionHandler(.success(()))
|
||||
})
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Invalid request.", error)
|
||||
completionHandler(.failure(ALTServerError(.invalidRequest)))
|
||||
}
|
||||
}
|
||||
|
||||
func receive<T: Decodable>(_ type: T.Type, from connection: NWConnection, completionHandler: @escaping (Result<T, Error>) -> Void)
|
||||
{
|
||||
let size = MemoryLayout<Int32>.size
|
||||
|
||||
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
|
||||
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
|
||||
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
|
||||
let response = try JSONDecoder().decode(T.self, from: data)
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Server
|
||||
{
|
||||
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
|
||||
{
|
||||
do
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let data = data else { throw error ?? ALTServerError(.unknown) }
|
||||
return data
|
||||
}
|
||||
catch let error as NWError
|
||||
{
|
||||
print("Error receiving data from connection \(connection)", error)
|
||||
|
||||
throw ALTServerError(.lostConnection)
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw error
|
||||
}
|
||||
}
|
||||
catch let error as ALTServerError
|
||||
{
|
||||
throw error
|
||||
}
|
||||
catch
|
||||
{
|
||||
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
|
||||
}
|
||||
self.identifier = identifier
|
||||
}
|
||||
}
|
||||
|
||||
146
AltStore/Server/ServerConnection.swift
Normal file
146
AltStore/Server/ServerConnection.swift
Normal 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
Reference in New Issue
Block a user