Compare commits

..

83 Commits
beta2 ... 1.0

Author SHA1 Message Date
Riley Testut
2d279775fe Updates apps.json for AltStore preview 2019-09-27 14:11:08 -07:00
Riley Testut
820b1fb718 Updates version to 1.0 2019-09-25 12:44:48 -07:00
Riley Testut
f6a797975f Updates icon attributions 2019-09-25 12:44:23 -07:00
Riley Testut
2977b79dcb [AltServer] Adds missing files to project 2019-09-25 12:44:00 -07:00
Riley Testut
0ce078a675 Rewords Patreon section in Settings 2019-09-25 12:43:32 -07:00
Riley Testut
de74aed83e Replaces Patreon photo of me with better photo of me 2019-09-25 12:41:53 -07:00
Riley Testut
01e2f635f8 [AltServer] Updates version to 1.0 2019-09-25 01:23:34 -07:00
Riley Testut
7b3f78082e [AltServer] Presents info notification on first launch 2019-09-25 01:23:23 -07:00
Riley Testut
046b36f4c4 Replaces tab bar icons 2019-09-25 01:22:16 -07:00
Riley Testut
1504a277d5 Re-enables checking if Patreon account is a patron 2019-09-25 00:53:36 -07:00
Riley Testut
865e3778b8 Adds reminder to use app-specific password on Login screen 2019-09-24 15:34:35 -07:00
Riley Testut
4c9480e6de [AltServer] Adds app-specific password info to Login alert 2019-09-24 15:33:20 -07:00
Riley Testut
14b2a10b4e Fixes parsing Patreon responses with null patron_status 2019-09-24 14:11:49 -07:00
Riley Testut
caac63c93b Updates apps.json 2019-09-22 00:23:39 -07:00
Riley Testut
32b4611c1e [Both] Updates AltStore + AltServer to 0.4 2019-09-21 22:58:05 -07:00
Riley Testut
993fa3eebb Revises “How it works” wording (again) 2019-09-21 22:58:05 -07:00
Riley Testut
3195a3f65d Presents notification when AltStore is about to expire 2019-09-21 22:31:10 -07:00
Riley Testut
b60d693056 Adds sound to News and Update alerts 2019-09-21 22:30:01 -07:00
Riley Testut
3faed8cf5c Updates wording in PatreonViewController 2019-09-21 21:27:47 -07:00
Riley Testut
6c91db1dcd Presents reminder to open AltStore after first background refresh 2019-09-21 21:27:20 -07:00
Riley Testut
f506988296 Updates cached AltStore bundle when app has been updated 2019-09-21 16:35:08 -07:00
Riley Testut
883e8cfbed Opens Twitter links in Twitter app if installed 2019-09-21 13:57:18 -07:00
Riley Testut
997376938a [AltServer] Updates AltStore download URL 2019-09-19 22:25:07 -07:00
Riley Testut
f51e41efab Hides Settings Debug section behind swipe gesture 2019-09-19 22:20:10 -07:00
Riley Testut
1117c05349 [AltServer] Adds app icon + updated menu bar icon 2019-09-19 22:12:06 -07:00
Riley Testut
26f799de72 Replaces personal email with AltStore email 2019-09-19 15:35:38 -07:00
Riley Testut
9ea584c1fb Adds placeholder view to NewsViewController and BrowseViewController 2019-09-19 15:18:21 -07:00
Riley Testut
73c44c5e29 Supports deep linking to Patreon settings 2019-09-19 14:43:26 -07:00
Riley Testut
00a7886941 Updates version to 0.31 2019-09-19 12:19:32 -07:00
Riley Testut
c5b0072443 Changes app icon + primary tint color 2019-09-19 11:38:38 -07:00
Riley Testut
94a22da471 Disables URL caching when fetching Source 2019-09-19 11:27:38 -07:00
Riley Testut
8bfa5c6ff3 Updates AltStore source to use new storage backend 2019-09-17 11:51:53 -07:00
Riley Testut
3a190afa3b Updates Apps.json 2019-09-15 19:39:01 -07:00
Riley Testut
d03d7eae42 Updates version to 0.3 2019-09-14 13:45:49 -07:00
Riley Testut
cb25e44636 [AltServer] Updates version to 0.3 2019-09-14 13:45:41 -07:00
Riley Testut
405e894768 [AltServer] Updates deployment target to macOS 10.14.4 2019-09-14 13:45:21 -07:00
Riley Testut
f03ae815d7 Temporarily enables Patreon benefits for all Patreon accounts 2019-09-14 13:41:58 -07:00
Riley Testut
9f9710c31d Updates + migrates Core Data model to v2 2019-09-14 13:22:38 -07:00
Riley Testut
ad69b9989c Updates Roxas 2019-09-14 12:32:00 -07:00
Riley Testut
e6fc491f6a [AltServer] Shows alert when installing AltStore onto second device 2019-09-14 11:29:34 -07:00
Riley Testut
f5d29cd2c1 Updates pods 2019-09-14 11:00:58 -07:00
Riley Testut
f47212000b Updates app icon 2019-09-14 11:00:17 -07:00
Riley Testut
5c3b129c7f Adds News tab bar image 2019-09-14 10:59:39 -07:00
Riley Testut
8110c12272 Adds support for Delta Lite 2019-09-14 10:58:41 -07:00
Riley Testut
7536b09c4a [AltServer] Deletes downloaded AltStore.ipa after installing to device 2019-09-13 14:48:12 -07:00
Riley Testut
deff48f9c3 [AltServer] Prefer free/individual teams over organization teams 2019-09-13 14:26:21 -07:00
Riley Testut
07746174d4 [AltServer] Improves error handling when installing apps 2019-09-13 14:25:26 -07:00
Riley Testut
e3cf7b203c [AltServer] Fixes missing LaunchAtLogin.framework 2019-09-12 15:21:19 -07:00
Riley Testut
ee20ac9a03 Presents app page when tapping updates 2019-09-12 13:51:03 -07:00
Riley Testut
ff5e805b81 Adds “Send Feedback” option in settings 2019-09-12 13:47:55 -07:00
Riley Testut
6214f1044b Improves handling of non-patron Patreon accounts 2019-09-12 13:08:38 -07:00
Riley Testut
502a5488b0 Adds support for installing AltStore beta from AltStore 2019-09-12 13:04:15 -07:00
Riley Testut
e3bf6d6239 Improves error message when multiple apps fail to refresh 2019-09-12 13:00:08 -07:00
Riley Testut
e510e9d992 Fixes crash when displaying new updates 2019-09-12 12:49:19 -07:00
Riley Testut
f01e4ec753 Rewords Patreon text in SettingsViewController 2019-09-10 12:33:14 -07:00
Riley Testut
225bbbe7af Fixes sideloaded apps disappearing after unlinking Patreon 2019-09-10 12:32:48 -07:00
Riley Testut
839b0b95fc Authenticates before checking for AltServers
This means Auth flow is presented even when AltServer is not nearby.
2019-09-10 12:19:46 -07:00
Riley Testut
f6768b2d72 [AltServer] Deletes received .ipa after installing 2019-09-10 12:17:26 -07:00
Riley Testut
6955f57063 Adds serverID to Info.plist when resigning AltStore 2019-09-09 17:40:05 -07:00
Riley Testut
5b59ccc6a0 Fixes non-legible toast view in AuthenticationViewController 2019-09-08 14:38:32 -07:00
Riley Testut
936474cd1c Fixes main thread freeze when installing/refreshing apps 2019-09-08 14:24:18 -07:00
Riley Testut
2192a756b2 Changes app tint color to Red (from Green) 2019-09-08 14:21:58 -07:00
Riley Testut
c8336d6199 Fixes inability to select .ipa files from document browser 2019-09-07 15:37:46 -07:00
Riley Testut
8881ebb0f2 Displays countdown for unreleased apps 2019-09-07 15:37:08 -07:00
Riley Testut
939d7c5f35 Handles Patreon de-authentication gracefully 2019-09-07 15:35:12 -07:00
Riley Testut
cf3977e7f3 Revises Settings + Patreon UI (again)
- Changes background color to red
- Improves Patreon screen
- Adds credits + software licenses
2019-09-07 15:34:07 -07:00
Riley Testut
ab8d51c000 Revises Auth flow UI 2019-09-07 15:29:19 -07:00
Riley Testut
f5ea5a140a Presents Local Notifications when new NewsItem is fetched in background 2019-09-06 14:41:30 -07:00
Riley Testut
e6bfdfdaee Revises Patreon UI 2019-09-05 15:37:58 -07:00
Riley Testut
6635565a1c Revises Settings UI 2019-09-05 11:59:10 -07:00
Riley Testut
859f8a255c Adds support for isBackgroundRefreshEnabled setting 2019-09-05 11:57:16 -07:00
Riley Testut
88ab3f0c37 Fixes crash when signing in with paid Developer account 2019-09-04 12:22:05 -07:00
Riley Testut
66c9f547c1 [AltServer] Uses NSAlerts for installation errors 2019-09-04 12:08:27 -07:00
Riley Testut
a37d02d5d1 [AltServer] Adds option to Launch at Login 2019-09-04 11:58:28 -07:00
Riley Testut
0c1f469dfa Prioritizes AltServer that originally installed AltStore over others 2019-09-04 10:45:24 -07:00
Riley Testut
d03f963d9b Improves some ALTServerError descriptions 2019-09-03 21:59:54 -07:00
Riley Testut
22fcb940f2 Improves provisioning profile logging when installing apps 2019-09-03 21:59:31 -07:00
Riley Testut
82b4d28698 Removes unused code from BrowseViewController 2019-09-03 21:58:44 -07:00
Riley Testut
c2a8b59e36 Adds News tab 2019-09-03 21:58:07 -07:00
Riley Testut
eb5b1a616a [AltStore] Adds basic Patreon integration
- Lists beta versions of apps when signed in to Patreon
- Lists names of Patrons with the Credits benefit
2019-08-28 11:13:22 -07:00
Riley Testut
8df4c97a74 [AltStore] Limits background app refreshing to once every 6 hours 2019-08-28 11:08:04 -07:00
Riley Testut
d45f052f16 [AltStore] Fixes potential endless loading of remote images 2019-08-27 16:00:59 -07:00
Riley Testut
7d48b831ed [AltStore] Loads images remotely rather than including them in app bundle 2019-08-20 19:06:03 -05:00
205 changed files with 13025 additions and 1643 deletions

View File

@@ -13,19 +13,20 @@ extern NSErrorDomain const AltServerInstallationErrorDomain;
typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError)
{
ALTServerErrorUnknown,
ALTServerErrorConnectionFailed,
ALTServerErrorLostConnection,
ALTServerErrorUnknown = 0,
ALTServerErrorConnectionFailed = 1,
ALTServerErrorLostConnection = 2,
ALTServerErrorDeviceNotFound,
ALTServerErrorDeviceWriteFailed,
ALTServerErrorDeviceNotFound = 3,
ALTServerErrorDeviceWriteFailed = 4,
ALTServerErrorInvalidRequest,
ALTServerErrorInvalidResponse,
ALTServerErrorInvalidRequest = 5,
ALTServerErrorInvalidResponse = 6,
ALTServerErrorInvalidApp,
ALTServerErrorInstallationFailed,
ALTServerErrorMaximumFreeAppLimitReached,
ALTServerErrorInvalidApp = 7,
ALTServerErrorInstallationFailed = 8,
ALTServerErrorMaximumFreeAppLimitReached = 9,
ALTServerErrorUnsupportediOSVersion = 10,
};
NS_ASSUME_NONNULL_BEGIN

View File

@@ -39,10 +39,10 @@ NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServ
return NSLocalizedString(@"Lost connection to AltServer.", @"");
case ALTServerErrorDeviceNotFound:
return NSLocalizedString(@"AltServer could not locate this device.", @"");
return NSLocalizedString(@"AltServer could not find this device.", @"");
case ALTServerErrorDeviceWriteFailed:
return NSLocalizedString(@"Failed to write app data to phone.", @"");
return NSLocalizedString(@"Failed to write app data to device.", @"");
case ALTServerErrorInvalidRequest:
return NSLocalizedString(@"AltServer received an invalid request.", @"");
@@ -58,6 +58,9 @@ NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServ
case ALTServerErrorMaximumFreeAppLimitReached:
return NSLocalizedString(@"You have reached the limit of 3 apps per device.", @"");
case ALTServerErrorUnsupportediOSVersion:
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
}
}

View File

@@ -11,6 +11,8 @@ import UserNotifications
import AltSign
import LaunchAtLogin
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
@@ -22,6 +24,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet private var appMenu: NSMenu!
@IBOutlet private var connectedDevicesMenu: NSMenu!
@IBOutlet private var launchAtLoginMenuItem: NSMenuItem!
private weak var authenticationAppleIDTextField: NSTextField?
private weak var authenticationPasswordTextField: NSSecureTextField?
@@ -43,6 +46,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.statusItem = item
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: "")
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
UserDefaults.standard.didPresentInitialNotification = true
}
}
func applicationWillTerminate(_ aNotification: Notification)
@@ -59,6 +74,9 @@ private extension AppDelegate
self.connectedDevices = ALTDeviceManager.shared.connectedDevices
self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:))
let x = button.frame.origin.x
let y = button.frame.origin.y - 5
@@ -78,7 +96,11 @@ 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.", 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: "")
let textFieldSize = NSSize(width: 300, height: 22)
@@ -121,22 +143,54 @@ private extension AppDelegate
let device = self.connectedDevices[index]
ALTDeviceManager.shared.installAltStore(to: device, appleID: username, password: password) { (result) in
let content = UNMutableNotificationContent()
switch result
{
case .success:
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("Installation Succeeded", comment: "")
content.body = String(format: NSLocalizedString("AltStore was successfully installed on %@.", comment: ""), device.name)
case .failure(let error):
content.title = NSLocalizedString("Installation Failed", comment: "")
content.body = error.localizedDescription
}
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
case .failure(InstallError.cancelled):
// Ignore
break
case .failure(let error as NSError):
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = NSLocalizedString("Installation Failed", comment: "")
if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? Error
{
alert.informativeText = underlyingError.localizedDescription
}
else
{
alert.informativeText = error.localizedDescription
}
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
alert.runModal()
}
}
}
@objc func toggleLaunchAtLogin(_ item: NSMenuItem)
{
if item.state == .on
{
item.state = .off
}
else
{
item.state = .on
}
LaunchAtLogin.isEnabled.toggle()
}
}

View File

@@ -1,53 +1,63 @@
{
"images" : [
{
"idiom" : "mac",
"size" : "16x16",
"idiom" : "mac",
"filename" : "Icon@16.png",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "16x16",
"idiom" : "mac",
"filename" : "Icon@32-1.png",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "32x32",
"idiom" : "mac",
"filename" : "Icon@32.png",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "32x32",
"idiom" : "mac",
"filename" : "Icon@64.png",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "128x128",
"idiom" : "mac",
"filename" : "Icon@128.png",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "128x128",
"idiom" : "mac",
"filename" : "Icon@256-1.png",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "256x256",
"idiom" : "mac",
"filename" : "Icon@256.png",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "256x256",
"idiom" : "mac",
"filename" : "Icon@512-1.png",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "512x512",
"idiom" : "mac",
"filename" : "Icon@512.png",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "512x512",
"idiom" : "mac",
"filename" : "Icon@1024.png",
"scale" : "2x"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -2,12 +2,12 @@
"images" : [
{
"idiom" : "universal",
"filename" : "MenuBarIcon.png",
"filename" : "MenuBar@19.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "MenuBarIcon@2x.png",
"filename" : "MenuBar@38.png",
"scale" : "2x"
},
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -61,6 +61,7 @@
<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="launchAtLoginMenuItem" destination="IyR-FQ-upe" id="Fxn-EP-hwH"/>
</connections>
</customObject>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
@@ -93,6 +94,10 @@
</connections>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="1ZZ-BB-xHy"/>
<menuItem title="Launch at Login" id="IyR-FQ-upe" userLabel="Launch At Login">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="mVM-Nm-Zi9"/>
<menuItem title="Quit AltServer" keyEquivalent="q" id="4sb-4s-VLi">
<connections>

View File

@@ -220,20 +220,35 @@ private extension ConnectionManager
func receiveApp(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
var temporaryURL: URL?
func finish(_ result: Result<Void, ALTServerError>)
{
if let temporaryURL = temporaryURL
{
do { try FileManager.default.removeItem(at: temporaryURL) }
catch { print("Failed to remove .ipa.", error) }
}
completionHandler(result)
}
self.receive(PrepareAppRequest.self, from: connection) { (result) in
print("Received request with result:", result)
switch result
{
case .failure(let error): completionHandler(.failure(error))
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)
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .failure(let error): finish(.failure(error))
case .success(let request, let fileURL):
temporaryURL = fileURL
print("Awaiting begin installation request...")
self.receive(BeginInstallationRequest.self, from: connection) { (result) in
@@ -241,7 +256,7 @@ private extension ConnectionManager
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .failure(let error): finish(.failure(error))
case .success:
print("Installing to device \(request.udid)...")
@@ -249,8 +264,8 @@ private extension ConnectionManager
print("Installed to device with result:", result)
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success: completionHandler(.success(()))
case .failure(let error): finish(.failure(error))
case .success: finish(.success(()))
}
}
}

View File

@@ -9,17 +9,17 @@
import Cocoa
import UserNotifications
enum InstallError: Error
enum InstallError: LocalizedError
{
case invalidCredentials
case cancelled
case noTeam
case missingPrivateKey
case missingCertificate
var localizedDescription: String {
var errorDescription: String? {
switch self
{
case .invalidCredentials: return "The provided Apple ID and password are incorrect."
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .noTeam: return "You are not a member of any developer teams."
case .missingPrivateKey: return "The developer certificate's private key could not be found."
case .missingCertificate: return "The developer certificate could not be found."
@@ -54,12 +54,6 @@ extension ALTDeviceManager
{
let account = try result.get()
let content = UNMutableNotificationContent()
content.title = String(format: NSLocalizedString("Installing AltStore to %@...", comment: ""), device.name)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
self.fetchTeam(for: account) { (result) in
do
{
@@ -75,6 +69,13 @@ extension ALTDeviceManager
{
let certificate = 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
do
{
@@ -84,6 +85,15 @@ extension ALTDeviceManager
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) }
self.registerAppID(name: "AltStore", identifier: "com.rileytestut.AltStore", team: team) { (result) in
@@ -157,7 +167,7 @@ extension ALTDeviceManager
func downloadApp(completionHandler: @escaping (Result<URL, Error>) -> Void)
{
let appURL = URL(string: "https://www.dropbox.com/s/w1gn9iztlqvltyp/AltStore.ipa?dl=1")!
let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")!
let downloadTask = URLSession.shared.downloadTask(with: appURL) { (fileURL, response, error) in
do
@@ -188,9 +198,23 @@ extension ALTDeviceManager
do
{
let teams = try Result(teams, error).get()
guard let team = teams.first else { throw InstallError.noTeam }
completionHandler(.success(team))
if let team = teams.first(where: { $0.type == .free })
{
return completionHandler(.success(team))
}
else if let team = teams.first(where: { $0.type == .individual })
{
return completionHandler(.success(team))
}
else if let team = teams.first
{
return completionHandler(.success(team))
}
else
{
throw InstallError.noTeam
}
}
catch
{
@@ -206,6 +230,34 @@ extension ALTDeviceManager
{
let certificates = try Result(certificates, error).get()
// Check if there is another AltStore certificate, which means AltStore has been installed with this Apple ID before.
if certificates.contains(where: { $0.machineName?.starts(with: "AltStore") == true })
{
var isCancelled = false
DispatchQueue.main.sync {
let alert = NSAlert()
alert.messageText = NSLocalizedString("AltStore already installed on another device.", comment: "")
alert.informativeText = NSLocalizedString("Apps installed with AltStore on your other devices will stop working. Are you sure you want to continue?", comment: "")
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let buttonIndex = alert.runModal()
if buttonIndex == NSApplication.ModalResponse.alertSecondButtonReturn
{
isCancelled = true
}
}
if isCancelled
{
return completionHandler(.failure(InstallError.cancelled))
}
}
if let certificate = certificates.first
{
ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in

View File

@@ -309,7 +309,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
if (![provisioningProfile.teamIdentifier isEqualToString:installationProvisioningProfile.teamIdentifier])
{
NSLog(@"Ignoring: %@", installationProvisioningProfile.teamIdentifier);
NSLog(@"Ignoring: %@ (Team: %@)", provisioningProfile.bundleIdentifier, provisioningProfile.teamIdentifier);
continue;
}
@@ -622,12 +622,12 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
uint64_t code = 0;
instproxy_status_get_error(status, &name, &description, &code);
if ((percent == -1 && progress.completedUnitCount > 0) || code != 0)
if ((percent == -1 && progress.completedUnitCount > 0) || code != 0 || name != NULL)
{
void (^completionHandler)(NSError *) = ALTDeviceManager.sharedManager.installationCompletionHandlers[UUID];
if (completionHandler != nil)
{
if (code != 0)
if (code != 0 || name != NULL)
{
NSLog(@"Error installing app. %@ (%@). %@", @(code), @(name), @(description));
@@ -638,11 +638,18 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorMaximumFreeAppLimitReached userInfo:nil];
}
else
{
NSString *errorName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
if ([errorName isEqualToString:@"DeviceOSVersionTooLow"])
{
error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnsupportediOSVersion userInfo:nil];
}
else
{
NSError *underlyingError = [NSError errorWithDomain:AltServerInstallationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: @(description)}];
error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorInstallationFailed userInfo:@{NSUnderlyingErrorKey: underlyingError}];
}
}
completionHandler(error);
}

View File

@@ -19,6 +19,15 @@ extension UserDefaults
}
}
var didPresentInitialNotification: Bool {
get {
return self.bool(forKey: "didPresentInitialNotification")
}
set {
self.set(newValue, forKey: "didPresentInitialNotification")
}
}
func registerDefaults()
{
if self.serverID == nil

View File

@@ -22,12 +22,12 @@
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019 Riley Testut. All rights reserved.</string>
<key>NSMainStoryboardFile</key>
<string>Main</string>
<key>LSUIElement</key>
<true/>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>

View File

@@ -17,6 +17,8 @@
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 */; };
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 */; };
BF1E312B229F474900370A3C /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3129229F474900370A3C /* ConnectionManager.swift */; };
BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; };
@@ -26,6 +28,8 @@
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 */; };
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 */; };
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648C22E79AC800E9056B /* ALTAppPermission.m */; };
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */; };
@@ -33,8 +37,12 @@
BF3D64A222E8031100E9056B /* MergePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D64A122E8031100E9056B /* MergePolicy.swift */; };
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D64AF22E8D4B800E9056B /* AppContentViewControllerCells.swift */; };
BF3F786422CAA41E008FBD20 /* ALTDeviceManager+Installation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3F786322CAA41E008FBD20 /* ALTDeviceManager+Installation.swift */; };
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF41B805233423AE00C593A3 /* TabBarController.swift */; };
BF41B808233433C100C593A3 /* LoadingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF41B807233433C100C593A3 /* LoadingState.swift */; };
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF43002D22A714AF0051E2BC /* Keychain.swift */; };
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */; };
BF44CC6C232AEB90004DA9C3 /* LaunchAtLogin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */; };
BF44CC6D232AEB90004DA9C3 /* LaunchAtLogin.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF45868F229872EA00BD7491 /* AppDelegate.swift */; };
BF458694229872EA00BD7491 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF458693229872EA00BD7491 /* Assets.xcassets */; };
BF458697229872EA00BD7491 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF458695229872EA00BD7491 /* Main.storyboard */; };
@@ -104,6 +112,7 @@
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, ); }; };
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, ); }; };
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; };
@@ -122,10 +131,15 @@
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4E22DD41A9008935CF /* UIColor+Hex.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 */; };
BFB1169D22932DB100BB457C /* Apps-Dev.json in Resources */ = {isa = PBXBuildFile; fileRef = BFB1169C22932DB100BB457C /* Apps-Dev.json */; };
BFB1169D22932DB100BB457C /* apps.json in Resources */ = {isa = PBXBuildFile; fileRef = BFB1169C22932DB100BB457C /* apps.json */; };
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; };
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; };
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 */; };
BFB6B22423187A3D0022A802 /* NewsCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB6B22323187A3D0022A802 /* NewsCollectionViewCell.xib */; };
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */; };
BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.swift */; };
BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* StoreApp.swift */; };
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; };
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */; };
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; };
@@ -169,10 +183,15 @@
BFD52C2022A1A9EC000B7ED1 /* node.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1D22A1A9EC000B7ED1 /* node.c */; };
BFD52C2122A1A9EC000B7ED1 /* node_list.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1E22A1A9EC000B7ED1 /* node_list.c */; };
BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; };
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6E7230CC961007955AB /* PatreonAPI.swift */; };
BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */; };
BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6ED230D8A86007955AB /* Patron.swift */; };
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F1230DD974007955AB /* Benefit.swift */; };
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 */; };
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */; };
BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */; };
BFDB69FD22A9A7B7007EA6D6 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB69FC22A9A7B7007EA6D6 /* SettingsViewController.swift */; };
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */; };
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */; };
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */; };
@@ -181,13 +200,22 @@
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 */; };
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 */; };
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6073F231AFD2A002B0E8E /* InsetGroupTableViewCell.swift */; };
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE60741231B07E6002B0E8E /* SettingsHeaderFooterView.swift */; };
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; };
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */; };
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */; };
BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; };
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */; };
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.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 */; };
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6932321CB85007A79E1 /* AuthenticationViewController.swift */; };
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 */
@@ -224,6 +252,7 @@
files = (
BF0201BB22C2EFA3000B93E4 /* AltSign.framework in Embed Frameworks */,
BF0201BE22C2EFBC000B93E4 /* openssl.framework in Embed Frameworks */,
BF44CC6D232AEB90004DA9C3 /* LaunchAtLogin.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -261,6 +290,9 @@
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>"; };
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>"; };
BF18B0F022E25DF9005C4CF5 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
BF1E3128229F474900370A3C /* ServerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerProtocol.swift; sourceTree = "<group>"; };
BF1E3129229F474900370A3C /* ConnectionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionManager.swift; sourceTree = "<group>"; };
@@ -271,6 +303,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>"; };
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>"; };
BF3D648B22E79AC800E9056B /* ALTAppPermission.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = "<group>"; };
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = "<group>"; };
@@ -279,8 +313,11 @@
BF3D64A122E8031100E9056B /* MergePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergePolicy.swift; sourceTree = "<group>"; };
BF3D64AF22E8D4B800E9056B /* AppContentViewControllerCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContentViewControllerCells.swift; sourceTree = "<group>"; };
BF3F786322CAA41E008FBD20 /* ALTDeviceManager+Installation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTDeviceManager+Installation.swift"; sourceTree = "<group>"; };
BF41B805233423AE00C593A3 /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = "<group>"; };
BF41B807233433C100C593A3 /* LoadingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingState.swift; sourceTree = "<group>"; };
BF43002D22A714AF0051E2BC /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+AltStore.swift"; sourceTree = "<group>"; };
BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LaunchAtLogin.framework; path = Carthage/Build/Mac/LaunchAtLogin.framework; sourceTree = "<group>"; };
BF45868D229872EA00BD7491 /* AltServer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltServer.app; sourceTree = BUILT_PRODUCTS_DIR; };
BF45868F229872EA00BD7491 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
BF458693229872EA00BD7491 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -356,6 +393,8 @@
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; };
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>"; };
BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
@@ -374,11 +413,16 @@
BF9B63C5229DD44D002F0A62 /* AltSign.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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-Dev.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "Apps-Dev.json"; sourceTree = "<group>"; };
BFB1169C22932DB100BB457C /* apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apps.json; sourceTree = "<group>"; };
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindServerOperation.swift; sourceTree = "<group>"; };
BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UpdateCollectionViewCell.xib; sourceTree = "<group>"; };
BFB6B21A23186D640022A802 /* NewsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsItem.swift; sourceTree = "<group>"; };
BFB6B21D231870160022A802 /* NewsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsViewController.swift; sourceTree = "<group>"; };
BFB6B21F231870B00022A802 /* NewsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsCollectionViewCell.swift; sourceTree = "<group>"; };
BFB6B22323187A3D0022A802 /* NewsCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NewsCollectionViewCell.xib; sourceTree = "<group>"; };
BFBAC8852295C90300587369 /* Result+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Conveniences.swift"; sourceTree = "<group>"; };
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AltStore.xcdatamodel; sourceTree = "<group>"; };
BFBBE2DE22931F73002097FA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
BFBBE2DE22931F73002097FA /* StoreApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp.swift; sourceTree = "<group>"; };
BFBBE2E022931F81002097FA /* InstalledApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledApp.swift; sourceTree = "<group>"; };
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAppOperation.swift; sourceTree = "<group>"; };
BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -424,10 +468,15 @@
BFD52C1D22A1A9EC000B7ED1 /* node.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node.c; path = Dependencies/libplist/libcnary/node.c; sourceTree = SOURCE_ROOT; };
BFD52C1E22A1A9EC000B7ED1 /* node_list.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node_list.c; path = Dependencies/libplist/libcnary/node_list.c; sourceTree = SOURCE_ROOT; };
BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; };
BFD5D6E7230CC961007955AB /* PatreonAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonAPI.swift; sourceTree = "<group>"; };
BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonAccount.swift; sourceTree = "<group>"; };
BFD5D6ED230D8A86007955AB /* Patron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patron.swift; sourceTree = "<group>"; };
BFD5D6F1230DD974007955AB /* Benefit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benefit.swift; sourceTree = "<group>"; };
BFD5D6F3230DDB0A007955AB /* Campaign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Campaign.swift; sourceTree = "<group>"; };
BFD5D6F5230DDB12007955AB /* Tier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tier.swift; sourceTree = "<group>"; };
BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsComponents.swift; sourceTree = "<group>"; };
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+RelativeDate.swift"; sourceTree = "<group>"; };
BFDB5B2522EFBBEA00F74113 /* BrowseCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BrowseCollectionViewCell.xib; sourceTree = "<group>"; };
BFDB69FC22A9A7B7007EA6D6 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = "<group>"; };
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResignAppOperation.swift; sourceTree = "<group>"; };
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; };
@@ -436,14 +485,24 @@
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>"; };
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>"; };
BFE6073F231AFD2A002B0E8E /* InsetGroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetGroupTableViewCell.swift; sourceTree = "<group>"; };
BFE60741231B07E6002B0E8E /* SettingsHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeaderFooterView.swift; sourceTree = "<group>"; };
BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = "<group>"; };
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = "<group>"; };
BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; };
BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = "<group>"; };
BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaceCertificateViewController.swift; sourceTree = "<group>"; };
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.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>"; };
BFF0B6932321CB85007A79E1 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = "<group>"; };
BFF0B695232242D3007A79E1 /* LicensesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicensesViewController.swift; sourceTree = "<group>"; };
BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionsViewController.swift; sourceTree = "<group>"; };
BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScreen+CompactHeight.swift"; sourceTree = "<group>"; };
EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = "<group>"; };
FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltServer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -467,6 +526,7 @@
files = (
BF1E315F22A0635900370A3C /* libAltKit.a in Frameworks */,
BF4588882298DD3F00BD7491 /* libxml2.tbd in Frameworks */,
BF44CC6C232AEB90004DA9C3 /* LaunchAtLogin.framework in Frameworks */,
BF4588472298D4B000BD7491 /* libimobiledevice.a in Frameworks */,
BF0201BD22C2EFBC000B93E4 /* openssl.framework in Frameworks */,
BF0201BA22C2EFA3000B93E4 /* AltSign.framework in Frameworks */,
@@ -497,6 +557,39 @@
path = Pods;
sourceTree = "<group>";
};
BF055B4A233B528B0086DEA9 /* Extensions */ = {
isa = PBXGroup;
children = (
BF0241A922F29CCD00129732 /* UserDefaults+AltServer.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
BF100C4E232D7C95006A8926 /* Migrations */ = {
isa = PBXGroup;
children = (
BF100C52232D7D9E006A8926 /* Policies */,
BF100C51232D7D91006A8926 /* Mapping Models */,
);
path = Migrations;
sourceTree = "<group>";
};
BF100C51232D7D91006A8926 /* Mapping Models */ = {
isa = PBXGroup;
children = (
BF100C4F232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel */,
);
path = "Mapping Models";
sourceTree = "<group>";
};
BF100C52232D7D9E006A8926 /* Policies */ = {
isa = PBXGroup;
children = (
BF100C53232D7DAE006A8926 /* StoreAppPolicy.swift */,
);
path = Policies;
sourceTree = "<group>";
};
BF1E315122A0616100370A3C /* AltKit */ = {
isa = PBXGroup;
children = (
@@ -515,6 +608,9 @@
children = (
BF3D648B22E79AC800E9056B /* ALTAppPermission.h */,
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */,
BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */,
BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */,
BF41B807233433C100C593A3 /* LoadingState.swift */,
);
path = Types;
sourceTree = "<group>";
@@ -537,6 +633,7 @@
BF458695229872EA00BD7491 /* Main.storyboard */,
BF703195229F36FF006E110F /* Devices */,
BFD52BDC22A0A659000B7ED1 /* Connections */,
BF055B4A233B528B0086DEA9 /* Extensions */,
BF703194229F36F6006E110F /* Resources */,
BF703196229F370F006E110F /* Supporting Files */,
);
@@ -721,6 +818,16 @@
path = Browse;
sourceTree = "<group>";
};
BFB6B21C2318700D0022A802 /* News */ = {
isa = PBXGroup;
children = (
BFB6B21D231870160022A802 /* NewsViewController.swift */,
BFB6B21F231870B00022A802 /* NewsCollectionViewCell.swift */,
BFB6B22323187A3D0022A802 /* NewsCollectionViewCell.xib */,
);
path = News;
sourceTree = "<group>";
};
BFBBE2E2229320A2002097FA /* My Apps */ = {
isa = PBXGroup;
children = (
@@ -770,13 +877,16 @@
children = (
BF219A7E22CAC431007676A6 /* AltStore.entitlements */,
BFD2476D2284B9A500981D42 /* AppDelegate.swift */,
BFE338E722F10E56002E24B9 /* LaunchViewController.swift */,
BFD247732284B9A500981D42 /* Main.storyboard */,
BFE338E722F10E56002E24B9 /* LaunchViewController.swift */,
BF41B805233423AE00C593A3 /* TabBarController.swift */,
BFE6325822A83BA800F30809 /* Authentication */,
BFB6B21C2318700D0022A802 /* News */,
BF9ABA4322DCFF33008935CF /* Browse */,
BF3D64A022E7FAD800E9056B /* App Detail */,
BFBBE2E2229320A2002097FA /* My Apps */,
BFDB69FB22A9A7A6007EA6D6 /* Settings */,
BFD5D6E6230CC94B007955AB /* Patreon */,
BFD2478A2284C49000981D42 /* Managing Apps */,
BFC51D7922972F1F00388324 /* Server */,
BFD247982284D7FC00981D42 /* Model */,
@@ -794,6 +904,7 @@
BFD247852284BB3300981D42 /* Frameworks */ = {
isa = PBXGroup;
children = (
BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */,
BF9B63C5229DD44D002F0A62 /* AltSign.framework */,
BF4588962298DE6E00BD7491 /* libzip.framework */,
BF4588872298DD3F00BD7491 /* libxml2.tbd */,
@@ -802,6 +913,7 @@
BF5AB3A72285FE6C00DC914B /* AltSign.framework */,
BF4713A422976CFC00784A2F /* openssl.framework */,
1039C07E517311FC499A0B64 /* Pods_AltStore.framework */,
FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -825,6 +937,8 @@
BF9ABA4C22DD16DE008935CF /* PillButton.swift */,
BF18B0F022E25DF9005C4CF5 /* ToastView.swift */,
BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */,
BF2901302318F7A800D88A45 /* AppBannerView.swift */,
BF29012E2318F6B100D88A45 /* AppBannerView.xib */,
);
path = Components;
sourceTree = "<group>";
@@ -832,7 +946,7 @@
BFD247962284D7C100981D42 /* Resources */ = {
isa = PBXGroup;
children = (
BFB1169C22932DB100BB457C /* Apps-Dev.json */,
BFB1169C22932DB100BB457C /* apps.json */,
BFD247762284B9A700981D42 /* Assets.xcassets */,
BF770E6822BD57DD002A40FE /* Silence.m4a */,
);
@@ -856,12 +970,15 @@
BFB11691229322E400BB457C /* DatabaseManager.swift */,
BF3D64A122E8031100E9056B /* MergePolicy.swift */,
BFE6326722A858F300F30809 /* Account.swift */,
BFBBE2DE22931F73002097FA /* App.swift */,
BF3D648722E79A3700E9056B /* AppPermission.swift */,
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
BFB6B21A23186D640022A802 /* NewsItem.swift */,
BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */,
BF02419322F2156E00129732 /* RefreshAttempt.swift */,
BFE338DC22F0E7F3002E24B9 /* Source.swift */,
BFBBE2DE22931F73002097FA /* StoreApp.swift */,
BFE6326522A857C100F30809 /* Team.swift */,
BF100C4E232D7C95006A8926 /* Migrations */,
);
path = Model;
sourceTree = "<group>";
@@ -874,6 +991,7 @@
BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */,
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */,
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */,
BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -882,16 +1000,35 @@
isa = PBXGroup;
children = (
BF1E3129229F474900370A3C /* ConnectionManager.swift */,
BF0241A922F29CCD00129732 /* UserDefaults+AltServer.swift */,
);
path = Connections;
sourceTree = "<group>";
};
BFD5D6E6230CC94B007955AB /* Patreon */ = {
isa = PBXGroup;
children = (
BFD5D6E7230CC961007955AB /* PatreonAPI.swift */,
BFD5D6ED230D8A86007955AB /* Patron.swift */,
BFD5D6F3230DDB0A007955AB /* Campaign.swift */,
BFD5D6F5230DDB12007955AB /* Tier.swift */,
BFD5D6F1230DD974007955AB /* Benefit.swift */,
);
path = Patreon;
sourceTree = "<group>";
};
BFDB69FB22A9A7A6007EA6D6 /* Settings */ = {
isa = PBXGroup;
children = (
BFDB69FC22A9A7B7007EA6D6 /* SettingsViewController.swift */,
BFE60737231ADF49002B0E8E /* Settings.storyboard */,
BFE60739231ADF82002B0E8E /* SettingsViewController.swift */,
BFE6073F231AFD2A002B0E8E /* InsetGroupTableViewCell.swift */,
BFE60741231B07E6002B0E8E /* SettingsHeaderFooterView.swift */,
BFE6073B231AE1E7002B0E8E /* SettingsHeaderFooterView.xib */,
BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */,
BFF0B68D23219520007A79E1 /* PatreonViewController.swift */,
BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */,
BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */,
BFF0B695232242D3007A79E1 /* LicensesViewController.swift */,
);
path = Settings;
sourceTree = "<group>";
@@ -913,6 +1050,7 @@
BF770E5722BC3D0F002A40FE /* OperationGroup.swift */,
BF770E5322BC044E002A40FE /* AppOperationContext.swift */,
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */,
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
@@ -926,9 +1064,8 @@
isa = PBXGroup;
children = (
BFE6325922A83BEB00F30809 /* Authentication.storyboard */,
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */,
BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */,
BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */,
BFF0B6932321CB85007A79E1 /* AuthenticationViewController.swift */,
BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */,
);
path = Authentication;
sourceTree = "<group>";
@@ -1009,6 +1146,7 @@
BF45868B229872EA00BD7491 /* Resources */,
BF4588462298D4AA00BD7491 /* Frameworks */,
BF0201BC22C2EFA3000B93E4 /* Embed Frameworks */,
BF7FDA2C23203B6B00B5D3A4 /* Copy Launcher App */,
);
buildRules = (
);
@@ -1136,13 +1274,18 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BFB1169D22932DB100BB457C /* Apps-Dev.json in Resources */,
BFB1169D22932DB100BB457C /* apps.json in Resources */,
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */,
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */,
BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */,
BFB6B22423187A3D0022A802 /* NewsCollectionViewCell.xib in Resources */,
BFD247752284B9A500981D42 /* Main.storyboard in Resources */,
BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */,
BFE6073C231AE1E7002B0E8E /* SettingsHeaderFooterView.xib in Resources */,
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */,
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1160,18 +1303,38 @@
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";
outputFileListPaths = (
);
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nuke.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
BF7FDA2C23203B6B00B5D3A4 /* Copy Launcher App */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Copy Launcher App";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PROJECT_DIR}/Carthage/Build/Mac/LaunchAtLogin.framework/Resources/copy-helper.sh\"\n";
};
FFB93342C7EB2021A1FFFB6A /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -1288,35 +1451,49 @@
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */,
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
BFDB69FD22A9A7B7007EA6D6 /* SettingsViewController.swift in Sources */,
BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */,
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */,
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */,
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
BFBBE2DF22931F73002097FA /* App.swift in Sources */,
BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */,
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */,
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */,
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */,
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */,
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */,
BFE6326822A858F300F30809 /* Account.swift in Sources */,
BFE6326622A857C200F30809 /* Team.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */,
BF100C54232D7DAE006A8926 /* StoreAppPolicy.swift in Sources */,
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */,
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */,
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */,
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */,
BF41B808233433C100C593A3 /* LoadingState.swift in Sources */,
BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */,
BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */,
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */,
@@ -1326,19 +1503,26 @@
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */,
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
BF3D64A222E8031100E9056B /* MergePolicy.swift in Sources */,
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */,
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */,
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */,
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */,
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
BF770E5622BC3C03002A40FE /* Server.swift in Sources */,
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */,
);
@@ -1434,6 +1618,10 @@
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 6XVY5G3U44;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Carthage/Build/Mac",
);
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
HAVE_OPENSSL,
@@ -1459,7 +1647,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.14.4;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
@@ -1479,6 +1667,10 @@
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 6XVY5G3U44;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Carthage/Build/Mac",
);
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
HAVE_OPENSSL,
@@ -1504,7 +1696,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.14.4;
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
@@ -1810,9 +2002,10 @@
BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */,
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */,
);
currentVersion = BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */;
currentVersion = BF100C46232D7828006A8926 /* AltStore 2.xcdatamodel */;
path = AltStore.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

View File

@@ -4,3 +4,4 @@
#import "NSError+ALTServerError.h"
#import "ALTAppPermission.h"
#import "ALTPatreonBenefitType.h"

View File

@@ -10,6 +10,8 @@ import UIKit
import Roxas
import Nuke
extension AppContentViewController
{
private enum Row: Int, CaseIterable
@@ -52,11 +54,9 @@ class AppContentViewController: UITableViewController
@IBOutlet private var permissionsCollectionView: UICollectionView!
var preferredScreenshotSize: CGSize? {
guard let image = self.screenshotsDataSource.items.first else { return nil }
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let aspectRatio = image.size.height / image.size.width
let aspectRatio: CGFloat = 16.0 / 9.0 // Hardcoded for now.
let width = self.screenshotsCollectionView.bounds.width - (layout.minimumInteritemSpacing * 2)
@@ -125,14 +125,39 @@ class AppContentViewController: UITableViewController
private extension AppContentViewController
{
func makeScreenshotsDataSource() -> RSTArrayCollectionViewDataSource<UIImage>
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
{
let screenshots = self.app.screenshotNames.compactMap(UIImage.init(named:))
let dataSource = RSTArrayCollectionViewDataSource(items: screenshots)
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: self.app.screenshotURLs as [NSURL])
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.image = screenshot
cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true
}
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: imageURL as URL, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image
{
completionHandler(image, nil)
}
else
{
completionHandler(nil, error)
}
})
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.imageView.image = image
if let error = error
{
print("Error loading image:", error)
}
}
return dataSource

View File

@@ -10,6 +10,8 @@ import UIKit
import Roxas
import Nuke
class AppViewController: UIViewController
{
var app: StoreApp!
@@ -35,6 +37,7 @@ class AppViewController: UIViewController
@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!
@@ -83,17 +86,16 @@ class AppViewController: UIViewController
self.nameLabel.text = self.app.name
self.developerLabel.text = self.app.developerName
self.developerLabel.textColor = self.app.tintColor
self.appIconImageView.image = UIImage(named: self.app.iconName)
self.appIconImageView.image = nil
self.appIconImageView.tintColor = self.app.tintColor
self.downloadButton.tintColor = self.app.tintColor
self.backgroundAppIconImageView.image = UIImage(named: self.app.iconName)
self.betaBadgeView.isHidden = !self.app.isBeta
self.backButtonContainerView.tintColor = self.app.tintColor
self.navigationController?.navigationBar.tintColor = self.app.tintColor
self.navigationBarDownloadButton.tintColor = self.app.tintColor
self.navigationBarAppNameLabel.text = self.app.name
self.navigationBarAppIconImageView.image = UIImage(named: self.app.iconName)
self.navigationBarAppIconImageView.tintColor = self.app.tintColor
self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { [weak self] (tableView, change) in
@@ -108,6 +110,19 @@ class AppViewController: UIViewController
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
// Load Images
for imageView in [self.appIconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
{
imageView.isIndicatingActivity = true
Nuke.loadImage(with: self.app.iconURL, options: .shared, into: imageView, progress: nil) { [weak imageView] (response, error) in
if response?.image != nil
{
imageView?.isIndicatingActivity = false
}
}
}
}
override func viewWillAppear(_ animated: Bool)
@@ -355,6 +370,17 @@ private extension AppViewController
button.progress = progress
}
if Date() < self.app.versionDate
{
self.downloadButton.countdownDate = self.app.versionDate
self.navigationBarDownloadButton.countdownDate = self.app.versionDate
}
else
{
self.downloadButton.countdownDate = nil
self.navigationBarDownloadButton.countdownDate = nil
}
let barButtonItem = self.navigationItem.rightBarButtonItem
self.navigationItem.rightBarButtonItem = nil
self.navigationItem.rightBarButtonItem = barButtonItem
@@ -366,7 +392,7 @@ private extension AppViewController
navigationController?.navigationBar.barStyle = .default
navigationController?.navigationBar.alpha = 1.0
navigationController?.navigationBar.barTintColor = .white
navigationController?.navigationBar.tintColor = .altGreen
navigationController?.navigationBar.tintColor = .altPrimary
}
func hideNavigationBar(for navigationController: UINavigationController? = nil)

View File

@@ -11,6 +11,7 @@ import UserNotifications
import AVFoundation
import AltSign
import AltKit
import Roxas
private enum RefreshError: LocalizedError
@@ -49,6 +50,11 @@ private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, Uns
appDelegate.receivedApplicationState(notification: name)
}
extension AppDelegate
{
static let openPatreonSettingsDeepLinkNotification = Notification.Name("openPatreonSettingsDeepLinkNotification")
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -62,12 +68,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
ServerManager.shared.startDiscovering()
UserDefaults.standard.registerDefaults()
if UserDefaults.standard.firstLaunch == nil
{
Keychain.shared.reset()
UserDefaults.standard.firstLaunch = Date()
}
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
#if DEBUG || BETA
UserDefaults.standard.isDebugModeEnabled = true
#endif
self.prepareForBackgroundFetch()
return true
@@ -82,6 +96,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
{
AppManager.shared.update()
ServerManager.shared.startDiscovering()
PatreonAPI.shared.refreshPatreonAccount()
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
{
return self.open(url)
}
}
@@ -89,7 +110,19 @@ private extension AppDelegate
{
func setTintColor()
{
self.window?.tintColor = .altGreen
self.window?.tintColor = .altPrimary
}
func open(_ url: URL) -> Bool
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let host = components.host, host.lowercased() == "patreon" else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
}
return true
}
}
@@ -124,9 +157,27 @@ extension AppDelegate
}
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
if UserDefaults.standard.isBackgroundRefreshEnabled
{
ServerManager.shared.startDiscovering()
if !UserDefaults.standard.presentedLaunchReminderNotification
{
let threeHours: TimeInterval = 3 * 60 * 60
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("App Refresh Tip", comment: "")
content.body = NSLocalizedString("The more you open AltStore, the more chances it's given to refresh apps in the background.", comment: "")
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
UserDefaults.standard.presentedLaunchReminderNotification = true
}
}
let refreshIdentifier = UUID().uuidString
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
@@ -135,9 +186,11 @@ extension AppDelegate
{
// If finish is actually called, that means an error occured during installation.
if UserDefaults.standard.isBackgroundRefreshEnabled
{
ServerManager.shared.stopDiscovering()
self.scheduleFinishedRefreshingNotification(for: result, identifier: refreshIdentifier, delay: 0)
}
taskCompletionHandler()
}
@@ -178,12 +231,106 @@ private extension AppDelegate
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
{
var fetchSourceResult: Result<Source, Error>?
var serversResult: Result<Void, Error>?
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
AppManager.shared.fetchSource() { (result) in
fetchSourceResult = result
do
{
let source = try result.get()
guard let context = source.managedObjectContext else { return }
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
previousUpdatesFetchRequest.includesPendingChanges = false
previousUpdatesFetchRequest.resultType = .dictionaryResultType
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
previousNewsItemsFetchRequest.includesPendingChanges = false
previousNewsItemsFetchRequest.resultType = .dictionaryResultType
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
try context.save()
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
let updates = try context.fetch(updatesFetchRequest)
let newsItems = try context.fetch(newsItemsFetchRequest)
for update in updates
{
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
guard let storeApp = update.storeApp else { continue }
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "")
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version)
content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
for newsItem in newsItems
{
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
guard !newsItem.isSilent else { continue }
let content = UNMutableNotificationContent()
if let app = newsItem.storeApp
{
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
}
else
{
content.title = NSLocalizedString("AltStore News", comment: "")
}
content.body = newsItem.title
content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = updates.count
}
}
catch
{
print("Error fetching apps:", error)
fetchSourceResult = .failure(error)
}
dispatchGroup.leave()
}
if UserDefaults.standard.isBackgroundRefreshEnabled
{
dispatchGroup.enter()
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
guard !installedApps.isEmpty else {
backgroundFetchCompletionHandler(.noData)
serversResult = .success(())
dispatchGroup.leave()
completionHandler(.failure(RefreshError.noInstalledApps))
return
}
@@ -205,74 +352,6 @@ private extension AppDelegate
}
}
var fetchSourceResult: Result<Source, Error>?
var serversResult: Result<Void, Error>?
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
dispatchGroup.enter()
AppManager.shared.fetchSource() { (result) in
fetchSourceResult = result
dispatchGroup.leave()
do
{
let source = try result.get()
guard let context = source.managedObjectContext else { return }
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
updatesFetchRequest.includesPendingChanges = true
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest()
previousUpdatesFetchRequest.includesPendingChanges = false
let previousUpdates = try context.fetch(previousUpdatesFetchRequest)
try context.save()
let updates = try context.fetch(updatesFetchRequest)
for update in updates
{
guard !previousUpdates.contains(where: { $0.bundleIdentifier == update.bundleIdentifier }) else { continue }
guard let storeApp = update.storeApp else { continue }
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "")
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = updates.count
}
}
catch
{
print("Error fetching apps:", error)
}
}
dispatchGroup.notify(queue: .main) {
guard let fetchSourceResult = fetchSourceResult, let serversResult = serversResult else {
backgroundFetchCompletionHandler(.failed)
return
}
// Call completionHandler early to improve chances of refreshing in the background again.
switch (fetchSourceResult, serversResult)
{
case (.success, .success): backgroundFetchCompletionHandler(.newData)
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)
case (.failure, _), (_, .failure): backgroundFetchCompletionHandler(.failed)
}
}
// Wait for three seconds to:
// a) give us time to discover AltServers
// b) give other processes a chance to respond to requestAppState notification
@@ -323,6 +402,40 @@ private extension AppDelegate
}
}
dispatchGroup.notify(queue: .main) {
if !UserDefaults.standard.isBackgroundRefreshEnabled
{
guard let fetchSourceResult = fetchSourceResult else {
backgroundFetchCompletionHandler(.failed)
return
}
switch fetchSourceResult
{
case .failure: backgroundFetchCompletionHandler(.failed)
case .success: backgroundFetchCompletionHandler(.newData)
}
completionHandler(.success([:]))
}
else
{
guard let fetchSourceResult = fetchSourceResult, let serversResult = serversResult else {
backgroundFetchCompletionHandler(.failed)
return
}
// Call completionHandler early to improve chances of refreshing in the background again.
switch (fetchSourceResult, serversResult)
{
case (.success, .success): backgroundFetchCompletionHandler(.newData)
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)
case (.failure, _), (_, .failure): backgroundFetchCompletionHandler(.failed)
}
}
}
}
func receivedApplicationState(notification: CFNotificationName)
{
let baseName = String(CFNotificationName.appIsRunning.rawValue)

View File

@@ -4,239 +4,452 @@
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<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>
<scenes>
<!--Apple ID-->
<scene sceneID="3cc-cd-zDK">
<!--Navigation Controller-->
<scene sceneID="lNR-II-WoW">
<objects>
<tableViewController storyboardIdentifier="authenticationViewController" id="nRn-xt-2XS" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" estimatedRowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="r38-H3-S3C">
<navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="Primary"/>
<textAttributes key="titleTextAttributes">
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textAttributes>
<textAttributes key="largeTitleTextAttributes">
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textAttributes>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="automaticallyAdjustsItemPositions" value="NO"/>
</userDefinedRuntimeAttributes>
</navigationBar>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="9J6-jc-46k" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-164" y="735"/>
</scene>
<!--Authentication View Controller-->
<scene sceneID="OCd-xc-Ms7">
<objects>
<viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<sections>
<tableViewSection id="uDm-cx-LdY">
<string key="footerTitle">Your email address and password are used only to sign in with Apple and is never stored.
If you have two-factor authentication enabled, make sure to use an app-specific password.</string>
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ER5-4r-tld">
<rect key="frame" x="0.0" y="35" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ER5-4r-tld" id="BnC-HI-d8z">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="70T-cn-6XF">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
</view>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apple ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="09n-b4-DRC">
<rect key="frame" x="0.0" y="0.0" width="74" height="43.5"/>
<constraints>
<constraint firstAttribute="width" constant="74" id="Y87-hZ-IsD"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
<rect key="frame" x="0.0" y="0.0" width="375" height="603"/>
<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"/>
<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"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to AltStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
<rect key="frame" x="0.0" y="0.0" width="333.5" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Email Address" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="V6B-NM-wpL">
<rect key="frame" x="90" y="0.0" width="253" height="43.5"/>
<nil key="textColor"/>
<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"/>
<textInputTraits key="textInputTraits" returnKeyType="next" enablesReturnKeyAutomatically="YES" textContentType="email"/>
<color key="textColor" white="1" alpha="1" 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"/>
<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"/>
<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"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q">
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="APPLE ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="59N-O1-6bM">
<rect key="frame" x="14" y="0.0" width="329" height="17"/>
<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="0.0"/>
</stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1">
<rect key="frame" x="0.0" y="21" width="343" height="51"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="name@email.com" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="DBu-vt-hlo">
<rect key="frame" x="14" y="0.0" width="315" height="51"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
<textInputTraits key="textInputTraits" returnKeyType="next" textContentType="email"/>
<connections>
<outlet property="delegate" destination="nRn-xt-2XS" id="5Us-OB-B4F"/>
<outlet property="delegate" destination="yO1-iT-7NP" id="G13-jV-DLX"/>
</connections>
</textField>
</subviews>
</stackView>
<color key="backgroundColor" white="1" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="DBu-vt-hlo" secondAttribute="trailing" id="0Lf-vH-juh"/>
<constraint firstItem="DBu-vt-hlo" firstAttribute="centerY" secondItem="gNe-dC-oI1" secondAttribute="centerY" id="kgs-hg-ECM"/>
<constraint firstItem="DBu-vt-hlo" firstAttribute="height" secondItem="gNe-dC-oI1" secondAttribute="height" id="n7y-Xg-8MP"/>
<constraint firstItem="DBu-vt-hlo" firstAttribute="leading" secondItem="gNe-dC-oI1" secondAttribute="leadingMargin" id="sat-rb-OIu"/>
<constraint firstAttribute="height" constant="51" id="tuP-Uo-6qp"/>
</constraints>
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="14"/>
</view>
</subviews>
<constraints>
<constraint firstItem="70T-cn-6XF" firstAttribute="top" secondItem="BnC-HI-d8z" secondAttribute="top" id="Zyt-OB-o6T"/>
<constraint firstAttribute="trailingMargin" secondItem="70T-cn-6XF" secondAttribute="trailing" id="lYn-uy-vRk"/>
<constraint firstAttribute="bottom" secondItem="70T-cn-6XF" secondAttribute="bottom" id="urj-EQ-5WK"/>
<constraint firstItem="70T-cn-6XF" firstAttribute="leading" secondItem="BnC-HI-d8z" secondAttribute="leadingMargin" id="yqr-Kr-I93"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="E9B-Cb-M5e">
<rect key="frame" x="0.0" y="79" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="E9B-Cb-M5e" id="S4n-4w-12m">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
</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"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="pON-cO-VYR">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<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"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Password" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vqv-cC-kya">
<rect key="frame" x="0.0" y="0.0" width="74" height="43.5"/>
<constraints>
<constraint firstAttribute="width" constant="74" id="Egk-ba-Kh3"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PASSWORD" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ava-XY-7vs">
<rect key="frame" x="14" y="0.0" width="329" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="z98-Sm-yDv">
<rect key="frame" x="90" y="0.0" width="253" height="43.5"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
</subviews>
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
</stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5">
<rect key="frame" x="0.0" y="21" width="343" height="51"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="••••••••" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="R77-TQ-lVT">
<rect key="frame" x="14" y="0.0" width="315" height="51"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
<textInputTraits key="textInputTraits" returnKeyType="go" enablesReturnKeyAutomatically="YES" secureTextEntry="YES" textContentType="password"/>
<connections>
<outlet property="delegate" destination="nRn-xt-2XS" id="7pH-Sf-Wmb"/>
<outlet property="delegate" destination="yO1-iT-7NP" id="Wpg-DV-BNL"/>
</connections>
</textField>
</subviews>
<color key="backgroundColor" white="1" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="R77-TQ-lVT" firstAttribute="leading" secondItem="cLc-iA-yq5" secondAttribute="leadingMargin" id="130-RD-MwU"/>
<constraint firstAttribute="height" constant="51" id="9Jw-2V-fgf"/>
<constraint firstItem="R77-TQ-lVT" firstAttribute="height" secondItem="cLc-iA-yq5" secondAttribute="height" id="FFf-Bp-LPT"/>
<constraint firstItem="R77-TQ-lVT" firstAttribute="centerY" secondItem="cLc-iA-yq5" secondAttribute="centerY" id="agB-KM-ba3"/>
<constraint firstAttribute="trailingMargin" secondItem="R77-TQ-lVT" secondAttribute="trailing" id="jB5-Ye-cJB"/>
</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="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" 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"/>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="pON-cO-VYR" secondAttribute="trailing" id="IPH-Og-2ch"/>
<constraint firstAttribute="bottom" secondItem="pON-cO-VYR" secondAttribute="bottom" id="j7H-Ds-pJg"/>
<constraint firstItem="pON-cO-VYR" firstAttribute="leading" secondItem="S4n-4w-12m" secondAttribute="leadingMargin" id="uAc-4j-0pB"/>
<constraint firstItem="pON-cO-VYR" firstAttribute="top" secondItem="S4n-4w-12m" secondAttribute="top" id="xZe-CS-STZ"/>
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
<state key="normal" title="Sign in">
<color key="titleColor" name="Pink"/>
</state>
<connections>
<outlet property="dataSource" destination="nRn-xt-2XS" id="VWO-oe-ykv"/>
<outlet property="delegate" destination="nRn-xt-2XS" id="CL1-Go-uiO"/>
<action selector="authenticate" destination="yO1-iT-7NP" eventType="primaryActionTriggered" id="LER-a2-CbC"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Apple ID" id="viw-66-ZJ7">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="KXh-qW-MIA">
<connections>
<action selector="cancel" destination="nRn-xt-2XS" id="l1X-bA-xsz"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Sign In" style="done" id="mkE-Q8-CxO">
<connections>
<action selector="authenticate" destination="nRn-xt-2XS" id="q60-9N-xVb"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="emailAddressTextField" destination="V6B-NM-wpL" id="N3F-eI-yhE"/>
<outlet property="passwordTextField" destination="z98-Sm-yDv" id="WDu-6c-oBa"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="v2u-D2-stc" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="605.60000000000002" y="19.340329835082461"/>
</scene>
<!--Select Team-->
<scene sceneID="0Hb-4t-vQ3">
<objects>
<tableViewController storyboardIdentifier="selectTeamViewController" id="R11-Yh-Wb1" customClass="SelectTeamViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="g2d-7w-OVl">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="iCV-rW-IhB" detailTextLabel="2hi-el-KvN" style="IBUITableViewCellStyleSubtitle" id="pPa-pY-koy">
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pPa-pY-koy" id="DjO-Wt-6j2">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
</button>
</subviews>
</stackView>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
<rect key="frame" x="16" y="498.5" width="343" height="96.5"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="iCV-rW-IhB">
<rect key="frame" x="16" y="5" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2hi-el-KvN">
<rect key="frame" x="16" y="25.5" width="33" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY">
<rect key="frame" x="0.0" y="24.5" width="343" height="72"/>
<string key="text">Your Apple ID is used to configure apps so they can be installed on this device. Your credentials will be stored securely in this device's Keychain and sent only to Apple for authentication.</string>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
</stackView>
</subviews>
</view>
</subviews>
<constraints>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="leading" secondItem="WXx-hX-AXv" secondAttribute="leading" id="13j-ii-X7W"/>
<constraint firstAttribute="bottom" secondItem="2wp-qG-f0Z" secondAttribute="bottom" id="Ggl-es-C4C"/>
<constraint firstAttribute="trailing" secondItem="2wp-qG-f0Z" secondAttribute="trailing" id="nl1-88-5mM"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="top" secondItem="WXx-hX-AXv" secondAttribute="top" id="wiH-lv-L9P"/>
</constraints>
</scrollView>
</subviews>
<color key="backgroundColor" name="Primary"/>
<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"/>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
<constraint firstItem="oyW-Fd-ojD" firstAttribute="top" secondItem="zMn-DV-fpy" secondAttribute="top" id="730-db-ukB"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="oyW-Fd-ojD" secondAttribute="trailing" id="KGE-CN-SWf"/>
<constraint firstItem="WXx-hX-AXv" firstAttribute="top" secondItem="mjy-4S-hyH" secondAttribute="top" id="LPQ-bF-ic0"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="WXx-hX-AXv" secondAttribute="trailing" id="MG7-A6-pKp"/>
<constraint firstAttribute="trailingMargin" secondItem="YmX-7v-pxh" secondAttribute="trailing" id="O4T-nu-o3e"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="bottom" secondItem="oyW-Fd-ojD" secondAttribute="bottom" id="PuX-ab-cEq"/>
<constraint firstItem="oyW-Fd-ojD" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="SzC-gC-Nvi"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
<constraint firstItem="WXx-hX-AXv" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="d08-zF-5X6"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="height" secondItem="oyW-Fd-ojD" secondAttribute="height" id="dFN-pw-TWt"/>
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/>
</constraints>
<viewLayoutGuide key="safeArea" id="zMn-DV-fpy"/>
</view>
<toolbarItems/>
<navigationItem key="navigationItem" largeTitleDisplayMode="never" id="jCf-N4-xVD">
<barButtonItem key="leftBarButtonItem" title="Close" id="nDc-Zs-wnK">
<connections>
<outlet property="dataSource" destination="R11-Yh-Wb1" id="zkX-xW-GvZ"/>
<outlet property="delegate" destination="R11-Yh-Wb1" id="vP7-NA-Y0n"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Select Team" id="ALr-U3-Ucl">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="HUE-P1-xa1">
<connections>
<action selector="cancel" destination="R11-Yh-Wb1" id="Ckg-bQ-0nv"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Next" style="done" id="7Ou-hQ-Cr3">
<connections>
<action selector="chooseTeam:" destination="R11-Yh-Wb1" id="nin-nM-lxU"/>
<action selector="cancel:" destination="yO1-iT-7NP" id="xls-in-Pre"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HxT-dJ-1Ry" userLabel="First Responder" sceneMemberID="firstResponder"/>
<nil key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="appleIDBackgroundView" destination="gNe-dC-oI1" id="lab-WG-pyJ"/>
<outlet property="appleIDTextField" destination="DBu-vt-hlo" id="ZMK-9K-phY"/>
<outlet property="contentStackView" destination="YmX-7v-pxh" id="ZX5-Af-cEB"/>
<outlet property="passwordBackgroundView" destination="cLc-iA-yq5" id="2JD-nS-Gf7"/>
<outlet property="passwordTextField" destination="R77-TQ-lVT" id="cLQ-Wn-MsE"/>
<outlet property="scrollView" destination="WXx-hX-AXv" id="hOb-gl-0OP"/>
<outlet property="signInButton" destination="2N5-zd-fUj" id="ul1-bh-4l4"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="U7A-Cx-Bo9" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1354" y="20"/>
<point key="canvasLocation" x="605.60000000000002" y="736.28185907046486"/>
</scene>
<!--Replace Certificate-->
<scene sceneID="fW2-QW-a2Z">
<!--How it works-->
<scene sceneID="dMt-EA-SGy">
<objects>
<tableViewController storyboardIdentifier="replaceCertificateViewController" id="LAG-dk-a0f" customClass="ReplaceCertificateViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="enT-LI-CNI">
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="luH-7x-QoO" style="IBUITableViewCellStyleDefault" id="i0O-XG-rRJ">
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i0O-XG-rRJ" id="GCT-3I-GCy">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="luH-7x-QoO">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
<rect key="frame" x="0.0" y="64" width="375" height="544"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
<rect key="frame" x="16" y="35" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i9V-3h-B8f">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints>
<constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="black" pointSize="80"/>
<color key="textColor" white="1" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0">
<rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch AltServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave AltServer running in the background on your computer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH">
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
</stackView>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
<rect key="frame" x="16" y="161" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints>
<constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="black" pointSize="80"/>
<color key="textColor" white="1" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
<rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to WiFi" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable iTunes WiFi Sync and connect to the same WiFi as AltServer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
<rect key="frame" x="16" y="287.5" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints>
<constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="black" pointSize="80"/>
<color key="textColor" white="1" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
<rect key="frame" x="79" y="15.5" width="264" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from AltStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt">
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
<rect key="frame" x="16" y="413.5" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints>
<constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="black" pointSize="80"/>
<color key="textColor" white="1" alpha="0.5" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
<rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background when on same WiFi as AltServer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
</stackView>
</subviews>
<edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/>
</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"/>
<constraints>
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
<state key="normal" title="Got it">
<color key="titleColor" name="Pink"/>
</state>
<connections>
<outlet property="dataSource" destination="LAG-dk-a0f" id="kOS-KX-Duz"/>
<outlet property="delegate" destination="LAG-dk-a0f" id="plW-kJ-BmR"/>
<action selector="dismiss" destination="aFi-fb-W0B" eventType="primaryActionTriggered" id="sBq-zj-Mln"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Replace Certificate" id="BM2-Vg-AJk">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="lPC-Dj-3Ik">
<connections>
<action selector="cancel" destination="LAG-dk-a0f" id="5C2-Hg-Les"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Next" style="done" id="ndJ-l9-HeM">
<connections>
<action selector="replaceCertificate:" destination="LAG-dk-a0f" id="vl2-E6-qi4"/>
</connections>
</barButtonItem>
</navigationItem>
</button>
</subviews>
<color key="backgroundColor" name="Primary"/>
<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"/>
<constraint firstAttribute="trailingMargin" secondItem="qZ9-AR-2zK" secondAttribute="trailing" id="8b4-iU-U7R"/>
<constraint firstItem="bp6-55-IG2" firstAttribute="leading" secondItem="Zek-aC-HOO" secondAttribute="leading" id="K1R-1r-FP3"/>
<constraint firstItem="Zek-aC-HOO" firstAttribute="trailing" secondItem="bp6-55-IG2" secondAttribute="trailing" id="aKV-sS-alh"/>
<constraint firstAttribute="bottomMargin" secondItem="qZ9-AR-2zK" secondAttribute="bottom" id="e8e-9l-Mkt"/>
<constraint firstItem="qZ9-AR-2zK" firstAttribute="leading" secondItem="Otz-hn-WGS" secondAttribute="leadingMargin" id="t2b-3e-6ld"/>
</constraints>
<viewLayoutGuide key="safeArea" id="Zek-aC-HOO"/>
</view>
<navigationItem key="navigationItem" title="How it works" largeTitleDisplayMode="always" id="bCq-Jq-gf1"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="yxU-EG-3sE" userLabel="First Responder" sceneMemberID="firstResponder"/>
<connections>
<outlet property="contentStackView" destination="bp6-55-IG2" id="k0Q-yS-Dxp"/>
<outlet property="dismissButton" destination="qZ9-AR-2zK" id="w5c-v6-TcC"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="3Q4-ya-qhc" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2135" y="19"/>
<point key="canvasLocation" x="1353" y="736"/>
</scene>
</scenes>
<color key="tintColor" name="Purple"/>
<resources>
<namedColor name="Pink">
<color red="0.92549019607843142" green="0.25490196078431371" blue="0.69803921568627447" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Primary">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
<color key="tintColor" name="Primary"/>
</document>

View File

@@ -2,48 +2,57 @@
// AuthenticationViewController.swift
// AltStore
//
// Created by Riley Testut on 6/5/19.
// Created by Riley Testut on 9/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltSign
import Roxas
class AuthenticationViewController: UITableViewController
class AuthenticationViewController: UIViewController
{
var authenticationHandler: (((ALTAccount, String)?) -> Void)?
private var _didLayoutSubviews = false
private weak var toastView: ToastView?
@IBOutlet private var emailAddressTextField: UITextField!
@IBOutlet private var appleIDTextField: UITextField!
@IBOutlet private var passwordTextField: UITextField!
@IBOutlet private var signInButton: UIButton!
@IBOutlet private var appleIDBackgroundView: UIView!
@IBOutlet private var passwordBackgroundView: UIView!
@IBOutlet private var scrollView: UIScrollView!
@IBOutlet private var contentStackView: UIStackView!
override func viewDidLoad()
{
super.viewDidLoad()
for view in [self.appleIDBackgroundView!, self.passwordBackgroundView!, self.signInButton!]
{
view.clipsToBounds = true
view.layer.cornerRadius = 16
}
if UIScreen.main.isExtraCompactHeight
{
self.contentStackView.spacing = 20
}
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationViewController.textFieldDidChangeText(_:)), name: UITextField.textDidChangeNotification, object: self.appleIDTextField)
NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationViewController.textFieldDidChangeText(_:)), name: UITextField.textDidChangeNotification, object: self.passwordTextField)
self.update()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
if !_didLayoutSubviews
{
self.emailAddressTextField.becomeFirstResponder()
}
_didLayoutSubviews = true
}
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
self.signInButton.isIndicatingActivity = false
self.toastView?.dismiss()
}
}
@@ -53,39 +62,25 @@ private extension AuthenticationViewController
{
if let _ = self.validate()
{
self.navigationItem.rightBarButtonItem?.isEnabled = true
self.signInButton.isEnabled = true
self.signInButton.alpha = 1.0
}
else
{
self.navigationItem.rightBarButtonItem?.isEnabled = false
self.signInButton.isEnabled = false
self.signInButton.alpha = 0.6
}
}
func validate() -> (String, String)?
{
guard
let emailAddress = self.emailAddressTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !emailAddress.isEmpty,
let emailAddress = self.appleIDTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !emailAddress.isEmpty,
let password = self.passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !password.isEmpty
else { return nil }
return (emailAddress, password)
}
func authenticate(emailAddress: String, password: String, completionHandler: @escaping (Result<(ALTAccount, [ALTTeam]), Error>) -> Void)
{
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in
switch Result(account, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let account):
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
let result = Result(teams, error).map { (account, $0) }
completionHandler(result)
}
}
}
}
}
private extension AuthenticationViewController
@@ -94,10 +89,10 @@ private extension AuthenticationViewController
{
guard let (emailAddress, password) = self.validate() else { return }
self.emailAddressTextField.resignFirstResponder()
self.appleIDTextField.resignFirstResponder()
self.passwordTextField.resignFirstResponder()
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = true
self.signInButton.isIndicatingActivity = true
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in
do
@@ -108,17 +103,23 @@ private extension AuthenticationViewController
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
let toastView = ToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription)
toastView.textLabel.textColor = .altPink
toastView.detailTextLabel.textColor = .altPink
toastView.show(in: self.navigationController?.view ?? self.view)
self.toastView = toastView
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
self.signInButton.isIndicatingActivity = false
}
}
DispatchQueue.main.async {
self.scrollView.setContentOffset(CGPoint(x: 0, y: -self.view.safeAreaInsets.top), animated: true)
}
}
}
@IBAction func cancel()
@IBAction func cancel(_ sender: UIBarButtonItem)
{
self.authenticationHandler?(nil)
}
@@ -130,7 +131,7 @@ extension AuthenticationViewController: UITextFieldDelegate
{
switch textField
{
case self.emailAddressTextField: self.passwordTextField.becomeFirstResponder()
case self.appleIDTextField: self.passwordTextField.becomeFirstResponder()
case self.passwordTextField: self.authenticate()
default: break
}
@@ -140,12 +141,21 @@ extension AuthenticationViewController: UITextFieldDelegate
return false
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
func textFieldDidBeginEditing(_ textField: UITextField)
{
DispatchQueue.main.async {
self.update()
guard UIScreen.main.isExtraCompactHeight else { return }
// Position all the controls within visible frame.
var contentOffset = self.scrollView.contentOffset
contentOffset.y = 44
self.scrollView.setContentOffset(contentOffset, animated: true)
}
}
return true
extension AuthenticationViewController
{
@objc func textFieldDidChangeText(_ notification: Notification)
{
self.update()
}
}

View File

@@ -0,0 +1,50 @@
//
// InstructionsViewController.swift
// AltStore
//
// Created by Riley Testut on 9/6/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
class InstructionsViewController: UIViewController
{
var completionHandler: (() -> Void)?
var showsBottomButton: Bool = false
@IBOutlet private var contentStackView: UIStackView!
@IBOutlet private var dismissButton: UIButton!
override func viewDidLoad()
{
super.viewDidLoad()
if UIScreen.main.isExtraCompactHeight
{
self.contentStackView.layoutMargins.top = 0
self.contentStackView.layoutMargins.bottom = self.contentStackView.layoutMargins.left
}
self.dismissButton.clipsToBounds = true
self.dismissButton.layer.cornerRadius = 16
if self.showsBottomButton
{
self.navigationItem.hidesBackButton = true
}
else
{
self.dismissButton.isHidden = true
}
}
}
private extension InstructionsViewController
{
@IBAction func dismiss()
{
self.completionHandler?()
}
}

View File

@@ -1,156 +0,0 @@
//
// ReplaceCertificateViewController.swift
// AltStore
//
// Created by Riley Testut on 6/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltSign
import Roxas
extension ReplaceCertificateViewController
{
private enum Error: LocalizedError
{
case missingPrivateKey
case missingCertificate
var errorDescription: String? {
switch self
{
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
}
}
}
}
class ReplaceCertificateViewController: UITableViewController
{
var replacementHandler: ((ALTCertificate?) -> Void)?
var team: ALTTeam!
var certificates: [ALTCertificate] {
get {
return self.dataSource.items
}
set {
self.dataSource.items = newValue
}
}
private var selectedCertificate: ALTCertificate? {
didSet {
self.update()
}
}
private lazy var dataSource = self.makeDataSource()
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
self.update()
}
}
private extension ReplaceCertificateViewController
{
func makeDataSource() -> RSTArrayTableViewDataSource<ALTCertificate>
{
let dataSource = RSTArrayTableViewDataSource<ALTCertificate>(items: [])
dataSource.proxy = self
dataSource.cellConfigurationHandler = { [weak self] (cell, certificate, indexPath) in
cell.textLabel?.text = certificate.name
cell.accessoryType = (self?.selectedCertificate == certificate) ? .checkmark : .none
}
let placeholderView = RSTPlaceholderView(frame: .zero)
placeholderView.textLabel.text = NSLocalizedString("No Certificates", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("There are no certificates associated with this team.", comment: "")
dataSource.placeholderView = placeholderView
return dataSource
}
func update()
{
self.navigationItem.rightBarButtonItem?.isEnabled = (self.selectedCertificate != nil)
if self.isViewLoaded
{
self.tableView.reloadData()
}
}
}
private extension ReplaceCertificateViewController
{
@IBAction func replaceCertificate(_ sender: UIBarButtonItem)
{
guard let certificate = self.selectedCertificate else { return }
func replace()
{
sender.isIndicatingActivity = true
ALTAppleAPI.shared.revoke(certificate, for: self.team) { (success, error) in
let result = Result(success, error).map { certificate }
do
{
let certificate = try result.get()
self.replacementHandler?(certificate)
}
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: NSLocalizedString("Error Replacing Certificate", comment: ""), detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
sender.isIndicatingActivity = false
}
}
}
}
let localizedTitle = String(format: NSLocalizedString("Are you sure you want to replace %@?", comment: ""), certificate.name)
let localizedMessage = NSLocalizedString("Any AltStore apps currently installed with this certificate will need to be refreshed.", comment: "")
let localizedReplaceActionTitle = String(format: NSLocalizedString("Replace %@", comment: ""), certificate.name)
let alertController = UIAlertController(title: localizedTitle, message: localizedMessage, preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: localizedReplaceActionTitle, style: .destructive) { (action) in
replace()
})
alertController.addAction(.cancel)
self.present(alertController, animated: true, completion: nil)
}
@IBAction func cancel()
{
self.replacementHandler?(nil)
}
}
extension ReplaceCertificateViewController
{
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
{
return NSLocalizedString("You have reached the maximum number of development certificates. Please select a certificate to replace.", comment: "")
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let certificate = self.dataSource.item(at: indexPath)
self.selectedCertificate = certificate
}
}

View File

@@ -1,141 +0,0 @@
//
// SelectTeamViewController.swift
// AltStore
//
// Created by Riley Testut on 6/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltSign
import Roxas
class SelectTeamViewController: UITableViewController
{
var selectionHandler: ((ALTTeam?) -> Void)?
var teams: [ALTTeam] {
get {
return self.dataSource.items
}
set {
self.dataSource.items = newValue
}
}
private var selectedTeam: ALTTeam? {
didSet {
self.update()
}
}
private lazy var dataSource = self.makeDataSource()
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
self.update()
}
override func viewWillDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
}
}
private extension SelectTeamViewController
{
func makeDataSource() -> RSTArrayTableViewDataSource<ALTTeam>
{
let dataSource = RSTArrayTableViewDataSource<ALTTeam>(items: [])
dataSource.proxy = self
dataSource.cellConfigurationHandler = { [weak self] (cell, team, indexPath) in
cell.textLabel?.text = team.name
cell.detailTextLabel?.text = team.type.localizedDescription
cell.accessoryType = (self?.selectedTeam == team) ? .checkmark : .none
}
let placeholderView = RSTPlaceholderView(frame: .zero)
placeholderView.textLabel.text = NSLocalizedString("No Teams", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("You are not a member of any development teams.", comment: "")
dataSource.placeholderView = placeholderView
return dataSource
}
func update()
{
self.navigationItem.rightBarButtonItem?.isEnabled = (self.selectedTeam != nil)
if self.isViewLoaded
{
self.tableView.reloadData()
}
}
func fetchCertificates(for team: ALTTeam, completionHandler: @escaping (Result<[ALTCertificate], Error>) -> Void)
{
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificate, error) in
let result = Result(certificate, error)
completionHandler(result)
}
}
}
private extension SelectTeamViewController
{
@IBAction func chooseTeam(_ sender: UIBarButtonItem)
{
guard let team = self.selectedTeam else { return }
func choose()
{
sender.isIndicatingActivity = true
self.selectionHandler?(team)
}
if team.type == .organization
{
let localizedActionTitle = String(format: NSLocalizedString("Use %@?", comment: ""), team.name)
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to use an Organization team?", comment: ""),
message: NSLocalizedString("Doing so may affect other members of this team.", comment: ""), preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: localizedActionTitle, style: .destructive, handler: { (action) in
choose()
}))
alertController.addAction(.cancel)
self.present(alertController, animated: true, completion: nil)
}
else
{
choose()
}
}
@IBAction func cancel()
{
self.selectionHandler?(nil)
}
}
extension SelectTeamViewController
{
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
{
return NSLocalizedString("Select the team you would like to use to install apps.", comment: "")
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let team = self.dataSource.item(at: indexPath)
self.selectedTeam = team
}
}

View File

@@ -32,16 +32,17 @@
<!--Tab Bar Controller-->
<scene sceneID="yl2-sM-qoP">
<objects>
<tabBarController id="49e-Tb-3d3" sceneMemberID="viewController">
<tabBarController 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"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
</tabBar>
<connections>
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="kCE-KJ-sWv"/>
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="F8I-ea-yTZ"/>
<segue destination="MGm-Zy-ffn" kind="relationship" relationship="viewControllers" id="9m0-Rb-vjU"/>
<segue destination="kjR-gi-fgT" kind="relationship" relationship="viewControllers" id="eWy-uk-nwG"/>
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="dXz-Tu-hW8"/>
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="zii-dF-qEt"/>
<segue destination="p3d-dP-Swg" kind="relationship" relationship="viewControllers" id="4Nf-rL-P4c"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
@@ -72,7 +73,7 @@
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1517.5999999999999" y="-1013.3433283358322"/>
<point key="canvasLocation" x="1730" y="-17"/>
</scene>
<!--App View Controller-->
<scene sceneID="TgT-LO-3Er">
@@ -133,7 +134,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="LZw-eU-5SO" userLabel="App Info">
<rect key="frame" x="0.0" y="0.0" width="273" height="93"/>
<rect key="frame" x="0.0" y="0.0" width="320" height="93"/>
<autoresizingMask key="autoresizingMask" widthSizable="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">
@@ -143,17 +144,25 @@
<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" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="bR7-SO-m8f">
<rect key="frame" x="90" y="26.5" width="88" height="40.5"/>
<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="90" y="26.5" width="135" height="40.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dNE-IO-y3o">
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="9z7-I4-q6g">
<rect key="frame" x="0.0" y="0.0" width="135" 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="88" 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="94" 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="88" height="17"/>
<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"/>
@@ -161,10 +170,10 @@
</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="189" y="31" width="72" height="31"/>
<rect key="frame" x="236" 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" constant="72" id="j44-T1-0dc"/>
<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"/>
@@ -230,7 +239,7 @@
<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="S2r-fI-PQB"/>
<constraint firstAttribute="width" constant="72" id="Xtq-UG-h3b"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="72" id="Xtq-UG-h3b"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<state key="normal" title="FREE"/>
@@ -246,6 +255,7 @@
<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="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"/>
@@ -261,7 +271,7 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="C9o-C3-sMK" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2312.8000000000002" y="-1013.3433283358322"/>
<point key="canvasLocation" x="2526" y="-17"/>
</scene>
<!--App-->
<scene sceneID="CgX-7h-sRI">
@@ -538,7 +548,7 @@ World</string>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dhh-ZN-LoG" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3088.8000000000002" y="-1014.2428785607198"/>
<point key="canvasLocation" x="3302" y="-18"/>
</scene>
<!--Permission Popover View Controller-->
<scene sceneID="24j-EJ-G4e">
@@ -585,241 +595,43 @@ World</string>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="7Tu-x9-xBb" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3908" y="-1484"/>
<point key="canvasLocation" x="4257" y="-412"/>
</scene>
<!--Settings-->
<scene sceneID="GaO-Ug-BdZ">
<scene sceneID="KlD-j0-ROn">
<objects>
<navigationController id="MGm-Zy-ffn" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Settings" image="Settings" id="8Ic-ki-txH"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="rzJ-pZ-611">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="VBC-qD-V1a" kind="relationship" relationship="rootViewController" id="tgI-RK-57z"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cM4-hZ-uHG" userLabel="First Responder" sceneMemberID="firstResponder"/>
<viewControllerPlaceholder storyboardName="Settings" id="p3d-dP-Swg" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Settings" image="Settings" id="OZm-Le-7oJ"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="HgE-PD-dC2" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="750" y="515"/>
<point key="canvasLocation" x="962" y="1197"/>
</scene>
<!--Settings-->
<scene sceneID="Xdi-2V-rwM">
<!--News-->
<scene sceneID="bqw-wB-hyB">
<objects>
<tableViewController id="VBC-qD-V1a" customClass="SettingsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="dOC-Gz-Ieu">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<sections>
<tableViewSection headerTitle="Account" id="nOs-a4-lBS">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="4Qf-b3-Kd1" detailTextLabel="zvb-TJ-uGW" style="IBUITableViewCellStyleValue1" id="HgQ-vv-9nH">
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="HgQ-vv-9nH" id="SSv-nz-f4V">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="4Qf-b3-Kd1">
<rect key="frame" x="16" y="12" width="45" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Riley Testut (iOS Developer)" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="zvb-TJ-uGW">
<rect key="frame" x="144.5" y="12" width="214.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="1UL-2f-ayi" detailTextLabel="2YH-D5-AnU" style="IBUITableViewCellStyleValue1" id="7cR-Qb-5GU">
<rect key="frame" x="0.0" y="99.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="7cR-Qb-5GU" id="iPP-TB-jnD">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Email" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="1UL-2f-ayi">
<rect key="frame" x="16" y="12" width="41" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="riley@rileytestut.com" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2YH-D5-AnU">
<rect key="frame" x="198" y="12" width="161" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Team" id="xqO-qN-967">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="ktC-F0-e9P" detailTextLabel="GtD-Jo-ONK" style="IBUITableViewCellStyleSubtitle" id="itp-Ya-UBR">
<rect key="frame" x="0.0" y="199.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="itp-Ya-UBR" id="w1A-z5-P4W">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="ktC-F0-e9P">
<rect key="frame" x="16" y="5" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="GtD-Jo-ONK">
<rect key="frame" x="16" y="25.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Debug" id="K7R-6x-gHl">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="nQj-qq-PmI" style="IBUITableViewCellStyleDefault" id="8M3-mu-gRd">
<rect key="frame" x="0.0" y="299.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="8M3-mu-gRd" id="6TQ-nF-Rkl">
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Background Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="nQj-qq-PmI">
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="RUB-gO-oQ6" kind="show" id="tkT-F7-PA4"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection footerTitle="" id="Yg2-vc-vLQ">
<cells/>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="VBC-qD-V1a" id="1Xd-SN-tww"/>
<outlet property="delegate" destination="VBC-qD-V1a" id="KEk-wr-hab"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Settings" id="Mtw-26-mVI">
<barButtonItem key="rightBarButtonItem" title="Sign Out" id="0wM-zj-gVA">
<color key="tintColor" red="1" green="0.14901960780000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<action selector="signOut:" destination="VBC-qD-V1a" id="X7d-Qp-VWw"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="accountEmailLabel" destination="2YH-D5-AnU" id="fyD-e7-ygs"/>
<outlet property="accountNameLabel" destination="zvb-TJ-uGW" id="mCh-p8-qCs"/>
<outlet property="teamNameLabel" destination="ktC-F0-e9P" id="2Zg-sh-2mY"/>
<outlet property="teamTypeLabel" destination="GtD-Jo-ONK" id="Jzp-2K-Bjk"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="BE4-68-0PU" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1518" y="515"/>
</scene>
<!--Refresh Attempts-->
<scene sceneID="tCt-AY-k3Z">
<objects>
<tableViewController id="RUB-gO-oQ6" customClass="RefreshAttemptsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="g2y-h0-0xu">
<collectionViewController id="3sa-FZ-PTg" customClass="NewsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<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"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="bBs-eV-Rlj" customClass="RefreshAttemptTableViewCell">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="bBs-eV-Rlj" id="YfI-8z-wCv">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="SFD-t6-Qk3">
<rect key="frame" x="16" y="11" width="343" height="22"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="gQk-PG-cjg">
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Success" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CZj-FM-9Qv">
<rect key="frame" x="0.0" y="0.0" width="67.5" height="17"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Date" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0Md-jd-XXe">
<rect key="frame" x="312.5" y="0.0" width="30.5" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Could not connect to AltServer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pwo-OM-Gm3">
<rect key="frame" x="0.0" y="21" width="343" height="1"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="SFD-t6-Qk3" firstAttribute="leading" secondItem="YfI-8z-wCv" secondAttribute="leadingMargin" id="ehs-Sf-kyZ"/>
<constraint firstAttribute="trailingMargin" secondItem="SFD-t6-Qk3" secondAttribute="trailing" id="g4i-l8-aln"/>
<constraint firstAttribute="bottomMargin" secondItem="SFD-t6-Qk3" secondAttribute="bottom" id="h38-79-xRT"/>
<constraint firstItem="SFD-t6-Qk3" firstAttribute="top" secondItem="YfI-8z-wCv" secondAttribute="topMargin" id="mCE-eA-UKd"/>
</constraints>
</tableViewCellContentView>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="40" minimumInteritemSpacing="40" id="63d-78-Y24">
<size key="itemSize" width="335" 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"/>
</collectionViewFlowLayout>
<cells/>
<connections>
<outlet property="dateLabel" destination="0Md-jd-XXe" id="znM-W1-cZi"/>
<outlet property="errorDescriptionLabel" destination="Pwo-OM-Gm3" id="3zi-gX-I5t"/>
<outlet property="successLabel" destination="CZj-FM-9Qv" id="vtL-iu-aHR"/>
<outlet property="dataSource" destination="3sa-FZ-PTg" id="80N-Sr-Foq"/>
<outlet property="delegate" destination="3sa-FZ-PTg" id="9fB-sR-8Xt"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="RUB-gO-oQ6" id="vqw-LK-wuY"/>
<outlet property="delegate" destination="RUB-gO-oQ6" id="9uY-h3-7qR"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Refresh Attempts" largeTitleDisplayMode="never" id="jO4-kG-Alq">
<barButtonItem key="rightBarButtonItem" title="Sign Out" id="9Pj-GZ-Rra">
<color key="tintColor" red="1" green="0.14901960780000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<action selector="signOut:" destination="VBC-qD-V1a" id="m55-18-kgT"/>
</connections>
</barButtonItem>
</navigationItem>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Wk8-xA-fxp" userLabel="First Responder" sceneMemberID="firstResponder"/>
</collectionView>
<navigationItem key="navigationItem" title="News" id="ZxL-Ws-lJO"/>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="YS7-2X-joz" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2313" y="515"/>
<point key="canvasLocation" x="1730" y="-752"/>
</scene>
<!--Browse-->
<scene sceneID="VHa-uP-bFU">
@@ -830,7 +642,7 @@ World</string>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" name="Green"/>
<color key="tintColor" name="Primary"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
@@ -839,14 +651,14 @@ World</string>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="OkH-49-O0J" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="750" y="-1013"/>
<point key="canvasLocation" x="962" y="-17"/>
</scene>
<!--My Apps-->
<scene sceneID="nhh-BJ-XiT">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="3Ew-ox-i4n" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y">
<color key="badgeColor" name="Green"/>
<color key="badgeColor" name="Primary"/>
</tabBarItem>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
@@ -860,7 +672,7 @@ World</string>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="9Nj-f6-CAf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="749.60000000000002" y="-279.31034482758622"/>
<point key="canvasLocation" x="962" y="717"/>
</scene>
<!--My Apps-->
<scene sceneID="EC8-Sf-AF9">
@@ -894,17 +706,25 @@ World</string>
<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" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="7iy-Zp-LEj">
<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>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" 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="203" height="18"/>
<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="203" height="16"/>
<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"/>
@@ -936,6 +756,7 @@ World</string>
</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"/>
@@ -955,7 +776,7 @@ World</string>
<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="Green"/>
<color key="textColor" name="Primary"/>
<nil key="highlightedColor"/>
</label>
</subviews>
@@ -1007,23 +828,48 @@ World</string>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<segue destination="0V6-N4-hTO" kind="show" identifier="showUpdate" id="dzt-2e-VM9"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kiO-UO-esV" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1517.5999999999999" y="-279.31034482758622"/>
<point key="canvasLocation" x="1730" y="717"/>
</scene>
<!--News-->
<scene sceneID="BV8-6J-nIv">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" 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">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="3sa-FZ-PTg" kind="relationship" relationship="rootViewController" id="Dcj-St-vt5"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iUr-Sd-9ER" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="962" y="-752"/>
</scene>
</scenes>
<resources>
<image name="Back" width="18" height="18"/>
<image name="BetaBadge" width="41" height="17"/>
<image name="Browse" width="19.5" height="20.5"/>
<image name="MyApps" width="28" height="24"/>
<image name="News" width="17" height="21"/>
<image name="Settings" width="21" height="21"/>
<namedColor name="Green">
<color red="0.22352941176470589" green="0.49411764705882355" blue="0.396078431372549" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<namedColor name="Primary">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
<inferredMetricsTieBreakers>
<segue reference="cnd-KK-o60"/>
<segue reference="dzt-2e-VM9"/>
</inferredMetricsTieBreakers>
<color key="tintColor" name="Green"/>
<color key="tintColor" name="Primary"/>
</document>

View File

@@ -10,11 +10,13 @@ import UIKit
import Roxas
import Nuke
@objc class BrowseCollectionViewCell: UICollectionViewCell
{
var imageNames: [String] = [] {
var imageURLs: [URL] = [] {
didSet {
self.dataSource.items = self.imageNames.map { $0 as NSString }
self.dataSource.items = self.imageURLs as [NSURL]
}
}
private lazy var dataSource = self.makeDataSource()
@@ -26,6 +28,7 @@ import Roxas
@IBOutlet var subtitleLabel: UILabel!
@IBOutlet var screenshotsCollectionView: UICollectionView!
@IBOutlet var betaBadgeView: UIImageView!
@IBOutlet private var screenshotsContentView: UIView!
@@ -56,24 +59,39 @@ import Roxas
private extension BrowseCollectionViewCell
{
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSString, UIImage>
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
{
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSString, UIImage>(items: [])
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: [])
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true
}
dataSource.prefetchHandler = { (imageName, indexPath, completion) in
return BlockOperation {
let image = UIImage(named: imageName as String)
completion(image, nil)
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: imageURL as URL, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image
{
completionHandler(image, nil)
}
else
{
completionHandler(nil, error)
}
})
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.imageView.image = image
if let error = error
{
print("Error loading image:", error)
}
}
return dataSource

View File

@@ -29,17 +29,25 @@
<constraint firstAttribute="height" constant="65" id="ufl-3d-nkT"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="zkp-KH-OyV">
<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>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xni-8I-ewW">
<rect key="frame" x="0.0" y="0.0" width="176" height="20.5"/>
<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="176" height="14.5"/>
<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"/>
@@ -50,7 +58,7 @@
<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" constant="72" id="X7D-DN-WnD"/>
<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"/>
@@ -108,6 +116,7 @@
<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="screenshotsCollectionView" destination="RFs-qp-Ca4" id="xfi-AN-l17"/>
@@ -116,4 +125,7 @@
</connections>
</collectionViewCell>
</objects>
<resources>
<image name="BetaBadge" width="41" height="17"/>
</resources>
</document>

View File

@@ -10,12 +10,21 @@ import UIKit
import Roxas
import Nuke
class BrowseViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
private let prototypeCell = BrowseCollectionViewCell.instantiate(with: BrowseCollectionViewCell.nib!)!
private var loadingState: LoadingState = .loading {
didSet {
self.update()
}
}
private var cachedItemSizes = [String: CGSize]()
override func viewDidLoad()
@@ -30,6 +39,8 @@ class BrowseViewController: UICollectionViewController
self.collectionView.prefetchDataSource = self.dataSource
self.registerForPreviewing(with: self, sourceView: self.collectionView)
self.update()
}
override func viewWillAppear(_ animated: Bool)
@@ -37,18 +48,7 @@ class BrowseViewController: UICollectionViewController
super.viewWillAppear(animated)
self.fetchSource()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard segue.identifier == "showApp" else { return }
guard let cell = sender as? UICollectionViewCell, let indexPath = self.collectionView.indexPath(for: cell) else { return }
let app = self.dataSource.item(at: indexPath)
let appViewController = segue.destination as! AppViewController
appViewController.app = app
self.updateDataSource()
}
}
@@ -75,8 +75,10 @@ private extension BrowseViewController
cell.nameLabel.text = app.name
cell.developerLabel.text = app.developerName
cell.subtitleLabel.text = app.subtitle
cell.imageNames = Array(app.screenshotNames.prefix(3))
cell.appIconImageView.image = UIImage(named: app.iconName)
cell.imageURLs = Array(app.screenshotURLs.prefix(2))
cell.appIconImageView.image = nil
cell.appIconImageView.isIndicatingActivity = true
cell.betaBadgeView.isHidden = !app.isBeta
cell.actionButton.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
cell.actionButton.activityIndicatorView.style = .white
@@ -85,7 +87,7 @@ private extension BrowseViewController
// Otherwise, cell reuse can mess up some cached values.
cell.actionButton.isIndicatingActivity = false
let tintColor = app.tintColor ?? .altGreen
let tintColor = app.tintColor ?? .altPrimary
cell.tintColor = tintColor
if app.installedApp == nil
@@ -95,37 +97,129 @@ private extension BrowseViewController
let progress = AppManager.shared.installationProgress(for: app)
cell.actionButton.progress = progress
cell.actionButton.isInverted = false
if Date() < app.versionDate
{
cell.actionButton.countdownDate = app.versionDate
}
else
{
cell.actionButton.countdownDate = nil
}
}
else
{
cell.actionButton.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
cell.actionButton.progress = nil
cell.actionButton.isInverted = true
cell.actionButton.countdownDate = nil
}
}
dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
let iconURL = storeApp.iconURL
return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image
{
completionHandler(image, nil)
}
else
{
completionHandler(nil, error)
}
})
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! BrowseCollectionViewCell
cell.appIconImageView.isIndicatingActivity = false
cell.appIconImageView.image = image
if let error = error
{
print("Error loading image:", error)
}
}
dataSource.placeholderView = self.placeholderView
return dataSource
}
func updateDataSource()
{
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
self.dataSource.predicate = nil
}
else
{
self.dataSource.predicate = NSPredicate(format: "%K == NO", #keyPath(StoreApp.isBeta))
}
}
func fetchSource()
{
self.loadingState = .loading
AppManager.shared.fetchSource() { (result) in
do
{
let source = try result.get()
try source.managedObjectContext?.save()
DispatchQueue.main.async {
self.loadingState = .finished(.success(()))
}
}
catch
{
DispatchQueue.main.async {
if self.dataSource.itemCount > 0
{
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
}
self.loadingState = .finished(.failure(error))
}
}
}
}
func update()
{
switch self.loadingState
{
case .loading:
self.placeholderView.textLabel.isHidden = true
self.placeholderView.detailTextLabel.isHidden = false
self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
self.placeholderView.activityIndicatorView.startAnimating()
case .finished(.failure(let error)):
self.placeholderView.textLabel.isHidden = false
self.placeholderView.detailTextLabel.isHidden = false
self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "")
self.placeholderView.detailTextLabel.text = error.localizedDescription
self.placeholderView.activityIndicatorView.stopAnimating()
case .finished(.success):
self.placeholderView.textLabel.isHidden = true
self.placeholderView.detailTextLabel.isHidden = true
self.placeholderView.activityIndicatorView.stopAnimating()
}
}
}
private extension BrowseViewController
{
@IBAction func performAppAction(_ sender: PillButton)

View File

@@ -0,0 +1,40 @@
//
// AppBannerView.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class AppBannerView: RSTNibView
{
@IBOutlet var titleLabel: UILabel!
@IBOutlet var subtitleLabel: UILabel!
@IBOutlet var iconImageView: AppIconImageView!
@IBOutlet var button: PillButton!
@IBOutlet var betaBadgeView: UIView!
override func tintColorDidChange()
{
super.tintColorDidChange()
self.update()
}
}
private extension AppBannerView
{
func update()
{
self.clipsToBounds = true
self.layer.cornerRadius = 22
self.subtitleLabel.textColor = self.tintColor
self.button.tintColor = self.tintColor
self.backgroundColor = self.tintColor.withAlphaComponent(0.1)
}
}

View File

@@ -0,0 +1,87 @@
<?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>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<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="betaBadgeView" destination="qQl-Ez-zC5" id="6O1-Cx-7qz"/>
<outlet property="button" destination="tVx-3G-dcu" id="joa-AH-syX"/>
<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"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<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>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="14" y="14" width="60" height="60"/>
<constraints>
<constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/>
<constraint firstAttribute="width" secondItem="avS-dx-4iy" secondAttribute="height" multiplier="1:1" id="AYT-3g-wcV"/>
</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"/>
<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"/>
<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"/>
<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"/>
</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>
</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"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
</view>
</objects>
<resources>
<image name="BetaBadge" width="41" height="17"/>
</resources>
</document>

View File

@@ -58,30 +58,33 @@ class CollapsingTextView: UITextView
let buttonFont = UIFont.systemFont(ofSize: font.pointSize, weight: .medium)
self.moreButton.titleLabel?.font = buttonFont
let buttonY = (font.lineHeight + self.lineSpacing) * CGFloat(self.maximumNumberOfLines - 1)
let size = self.moreButton.sizeThatFits(CGSize(width: 1000, height: 1000))
let moreButtonFrame = CGRect(x: self.bounds.width - self.moreButton.bounds.width,
y: self.bounds.height - self.moreButton.bounds.height - self.lineSpacing,
y: buttonY,
width: size.width,
height: font.lineHeight)
self.moreButton.frame = moreButtonFrame
if self.isCollapsed
{
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
let maximumCollapsedHeight = font.lineHeight * CGFloat(self.maximumNumberOfLines)
if self.intrinsicContentSize.height > maximumCollapsedHeight
{
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
let maximumCollapsedHeight = font.lineHeight * CGFloat(self.maximumNumberOfLines)
if self.bounds.height > maximumCollapsedHeight
{
self.moreButton.isHidden = false
}
else
{
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
}

View File

@@ -71,4 +71,24 @@ extension Keychain
self.keychain["signingCertificateSerialNumber"] = newValue
}
}
var patreonAccessToken: String? {
get {
let accessToken = try? self.keychain.get("patreonAccessToken")
return accessToken
}
set {
self.keychain["patreonAccessToken"] = newValue
}
}
var patreonRefreshToken: String? {
get {
let refreshToken = try? self.keychain.get("patreonRefreshToken")
return refreshToken
}
set {
self.keychain["patreonRefreshToken"] = newValue
}
}
}

View File

@@ -12,6 +12,10 @@ import Roxas
class NavigationBar: UINavigationBar
{
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
private let backgroundColorView = UIView()
override init(frame: CGRect)
{
super.init(frame: frame)
@@ -28,14 +32,33 @@ class NavigationBar: UINavigationBar
private func initialize()
{
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
}
}
override func layoutSubviews()
{
super.layoutSubviews()
if self.backgroundColorView.superview != nil
{
self.insertSubview(self.backgroundColorView, at: 1)
}
if self.automaticallyAdjustsItemPositions
{
// We can't easily shift just the back button up, so we shift the entire content view slightly.
for contentView in self.subviews
{
@@ -44,3 +67,4 @@ class NavigationBar: UINavigationBar
}
}
}
}

View File

@@ -36,8 +36,35 @@ class PillButton: UIButton
}
}
var countdownDate: Date? {
didSet {
self.isEnabled = (self.countdownDate == nil)
self.displayLink.isPaused = (self.countdownDate == nil)
if self.countdownDate == nil
{
self.setTitle(nil, for: .disabled)
}
}
}
private let progressView = UIProgressView(progressViewStyle: .default)
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(PillButton.updateCountdown))
displayLink.preferredFramesPerSecond = 15
displayLink.isPaused = true
displayLink.add(to: .main, forMode: .common)
return displayLink
}()
private let dateComponentsFormatter: DateComponentsFormatter = {
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.zeroFormattingBehavior = [.pad]
dateComponentsFormatter.collapsesLargestUnit = false
return dateComponentsFormatter
}()
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width += 26
@@ -45,6 +72,11 @@ class PillButton: UIButton
return size
}
deinit
{
self.displayLink.remove(from: .main, forMode: RunLoop.Mode.default)
}
override func awakeFromNib()
{
super.awakeFromNib()
@@ -101,4 +133,55 @@ private extension PillButton
self.progressView.progressTintColor = self.tintColor
}
}
@objc func updateCountdown()
{
guard let endDate = self.countdownDate else { return }
let startDate = Date()
let interval = endDate.timeIntervalSince(startDate)
guard interval > 0 else {
self.isEnabled = true
return
}
let text: String?
if interval < (1 * 60 * 60)
{
self.dateComponentsFormatter.unitsStyle = .positional
self.dateComponentsFormatter.allowedUnits = [.minute, .second]
text = self.dateComponentsFormatter.string(from: startDate, to: endDate)
}
else if interval < (2 * 24 * 60 * 60)
{
self.dateComponentsFormatter.unitsStyle = .positional
self.dateComponentsFormatter.allowedUnits = [.hour, .minute, .second]
text = self.dateComponentsFormatter.string(from: startDate, to: endDate)
}
else
{
self.dateComponentsFormatter.unitsStyle = .full
self.dateComponentsFormatter.allowedUnits = [.day]
let numberOfDays = endDate.numberOfCalendarDays(since: startDate)
text = String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays))
}
if let text = text
{
UIView.performWithoutAnimation {
self.isEnabled = false
self.setTitle(text, for: .disabled)
self.layoutIfNeeded()
}
}
else
{
self.isEnabled = true
}
}
}

View File

@@ -10,8 +10,9 @@ import UIKit
extension UIColor
{
static let altPurple = UIColor(named: "Purple")!
static let altGreen = UIColor(named: "Green")!
static let altPrimary = UIColor(named: "Primary")!
static let altPink = UIColor(named: "Pink")!
static let refreshRed = UIColor(named: "RefreshRed")!
static let refreshOrange = UIColor(named: "RefreshOrange")!

View File

@@ -0,0 +1,16 @@
//
// UIScreen+CompactHeight.swift
// AltStore
//
// Created by Riley Testut on 9/6/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
extension UIScreen
{
var isExtraCompactHeight: Bool {
return self.fixedCoordinateSpace.bounds.height < 600
}
}

View File

@@ -8,7 +8,20 @@
import Foundation
import Roxas
extension UserDefaults
{
@NSManaged var firstLaunch: Date?
@NSManaged var preferredServerID: String?
@NSManaged var isBackgroundRefreshEnabled: Bool
@NSManaged var isDebugModeEnabled: Bool
@NSManaged var presentedLaunchReminderNotification: Bool
func registerDefaults()
{
self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true])
}
}

View File

@@ -4,6 +4,8 @@
<dict>
<key>ALTDeviceID</key>
<string>1c3416b7b0ab68773e6e7eb7f0d110f7c9353acc</string>
<key>ALTServerID</key>
<string>1AAAB6FD-E8CE-4B70-8F26-4073215C03B0</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
@@ -17,17 +19,17 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2</string>
<string>1.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltStore</string>
<string>AltStore General</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altstore-com.rileytestut.altstore</string>
<string>altstore</string>
</array>
</dict>
</array>
@@ -36,8 +38,13 @@
<key>LSApplicationQueriesSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
<string>altstore-com.rileytestut.AltStore.Beta</string>
<string>altstore-com.rileytestut.Delta</string>
<string>altstore-com.rileytestut.Delta.Beta</string>
<string>altstore-com.rileytestut.Delta.Lite</string>
<string>altstore-com.rileytestut.Delta.Lite.Beta</string>
<string>altstore-com.rileytestut.Clip</string>
<string>altstore-com.rileytestut.Clip.Beta</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
@@ -76,5 +83,25 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>iOS App</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.apple.itunes.ipa</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<string>ipa</string>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -45,6 +45,7 @@ extension LaunchViewController
super.finishLaunching()
AppManager.shared.update()
PatreonAPI.shared.refreshPatreonAccount()
self.performSegue(withIdentifier: "finishLaunching", sender: nil)
}

View File

@@ -8,6 +8,7 @@
import Foundation
import UIKit
import UserNotifications
import AltSign
import AltKit
@@ -17,6 +18,8 @@ import Roxas
extension AppManager
{
static let didFetchSourceNotification = Notification.Name("com.altstore.AppManager.didFetchSource")
static let expirationWarningNotificationID = "altstore-expiration-warning"
}
class AppManager
@@ -54,15 +57,18 @@ extension AppManager
let installedApps = try context.fetch(fetchRequest)
for app in installedApps where app.storeApp != nil
{
if UIApplication.shared.canOpenURL(app.openAppURL)
if app.bundleIdentifier == StoreApp.altstoreAppID
{
// App is still installed, good!
self.scheduleExpirationWarningLocalNotification(for: app)
}
else
{
if !UIApplication.shared.canOpenURL(app.openAppURL)
{
context.delete(app)
}
}
}
try context.save()
}
@@ -175,17 +181,6 @@ private extension AppManager
{
// Authenticate -> Download (if necessary) -> Resign -> Send -> Install.
let group = group ?? OperationGroup()
guard let server = ServerManager.shared.discoveredServers.first else {
DispatchQueue.main.async {
group.completionHandler?(.failure(ConnectionError.serverNotFound))
}
return group
}
group.server = server
var operations = [Operation]()
@@ -200,6 +195,18 @@ private extension AppManager
}
operations.append(authenticationOperation)
/* Find Server */
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
}
}
findServerOperation.addDependency(authenticationOperation)
operations.append(findServerOperation)
for app in apps
{
@@ -213,7 +220,7 @@ private extension AppManager
guard let resignedApp = self.process(result, context: context) else { return }
context.resignedApp = resignedApp
}
resignAppOperation.addDependency(authenticationOperation)
resignAppOperation.addDependency(findServerOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
operations.append(resignAppOperation)
@@ -246,12 +253,13 @@ private extension AppManager
{
// App is not yet installed (or we're forcing it to download a new version), so download it before resigning it.
let downloadOperation = DownloadAppOperation(app: app)
let downloadOperation = DownloadAppOperation(app: app, context: context)
downloadOperation.resultHandler = { (result) in
guard let app = self.process(result, context: context) else { return }
context.app = app
}
progress.addChild(downloadOperation.progress, withPendingUnitCount: 40)
downloadOperation.addDependency(findServerOperation)
resignAppOperation.addDependency(downloadOperation)
operations.append(downloadOperation)
}
@@ -267,6 +275,16 @@ private extension AppManager
operations.append(sendAppOperation)
let beginInstallationHandler = group.beginInstallationHandler
group.beginInstallationHandler = { (installedApp) in
if installedApp.bundleIdentifier == StoreApp.altstoreAppID
{
self.scheduleExpirationWarningLocalNotification(for: installedApp)
}
beginInstallationHandler?(installedApp)
}
/* Install */
let installOperation = InstallAppOperation(context: context)
installOperation.resultHandler = { (result) in
@@ -330,8 +348,25 @@ private extension AppManager
if let error = context.error
{
switch error
{
case let error as ALTServerError where error.code == .deviceNotFound || error.code == .lostConnection:
if let server = context.group.server, server.isPreferred
{
// Preferred server, so report errors normally.
context.group.results[context.bundleIdentifier] = .failure(error)
}
else
{
// Not preferred server, so ignore these specific errors and throw serverNotFound instead.
context.group.results[context.bundleIdentifier] = .failure(ConnectionError.serverNotFound)
}
case let error:
context.group.results[context.bundleIdentifier] = .failure(error)
}
}
else if let installedApp = context.installedApp
{
context.group.results[context.bundleIdentifier] = .success(installedApp)
@@ -351,7 +386,34 @@ private extension AppManager
if context.group.results.count == context.group.progress.totalUnitCount
{
context.group.completionHandler?(.success(context.group.results))
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
backgroundContext.performAndWait {
guard let altstore = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: backgroundContext) else { return }
self.scheduleExpirationWarningLocalNotification(for: altstore)
}
}
}
}
func scheduleExpirationWarningLocalNotification(for app: InstalledApp)
{
let notificationDate = app.expirationDate.addingTimeInterval(-1 * 60 * 60 * 24) // 24 hours before expiration.
let timeIntervalUntilNotification = notificationDate.timeIntervalSinceNow
guard timeIntervalUntilNotification > 0 else {
// Crashes if we pass negative value to UNTimeIntervalNotificationTrigger initializer.
return
}
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("AltStore Expiring Soon", comment: "")
content.body = NSLocalizedString("AltStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
content.sound = .default
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>AltStore 2.xcdatamodel</string>
</dict>
</plist>

View File

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

View File

@@ -115,6 +115,12 @@ extension DatabaseManager
let activeTeam = Team.first(satisfying: predicate, in: context)
return activeTeam
}
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount?
{
let patronAccount = PatreonAccount.first(in: context)
return patronAccount
}
}
private extension DatabaseManager
@@ -125,6 +131,20 @@ private extension DatabaseManager
context.performAndWait {
guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return }
let altStoreSource: Source
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
{
altStoreSource = source
}
else
{
altStoreSource = Source.makeAltStoreSource(in: context)
}
// Make sure to always update source URL to be current.
altStoreSource.sourceURL = Source.altStoreSourceURL
let storeApp: StoreApp
if let app = StoreApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: context)
@@ -133,11 +153,9 @@ private extension DatabaseManager
}
else
{
let source = Source.makeAltStoreSource(in: context)
storeApp = StoreApp.makeAltStoreApp(in: context)
storeApp.version = localApp.version
storeApp.source = source
storeApp.source = altStoreSource
}
let installedApp: InstalledApp
@@ -152,30 +170,31 @@ private extension DatabaseManager
installedApp.storeApp = storeApp
}
installedApp.version = localApp.version
let fileURL = installedApp.fileURL
if !FileManager.default.fileExists(atPath: fileURL.path)
if !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version
{
FileManager.default.prepareTemporaryURL() { (temporaryFileURL) in
do
{
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: fileURL)
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: temporaryFileURL)
let infoPlistURL = fileURL.appendingPathComponent("Info.plist")
let infoPlistURL = temporaryFileURL.appendingPathComponent("Info.plist")
// TODO: Copy to temporary location, modify it, _then_ copy to final destination.
guard var infoDictionary = Bundle.main.infoDictionary else { throw ALTError(.missingInfoPlist) }
infoDictionary[kCFBundleIdentifierKey as String] = StoreApp.altstoreAppID
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
try FileManager.default.copyItem(at: temporaryFileURL, to: fileURL, shouldReplace: true)
}
catch
{
print("Failed to copy AltStore app bundle to its proper location.", error)
}
}
}
try? FileManager.default.removeItem(at: fileURL)
}
}
// 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
{

View File

@@ -82,7 +82,17 @@ extension InstalledApp
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
{
let predicate = NSPredicate(format: "%K != %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
var predicate = NSPredicate(format: "%K != %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
// No additional predicate
}
else
{
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
}
var installedApps = InstalledApp.all(satisfying: predicate,
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
@@ -99,12 +109,23 @@ extension InstalledApp
class func fetchAppsForBackgroundRefresh(in context: NSManagedObjectContext) -> [InstalledApp]
{
let date = Date().addingTimeInterval(-120)
// Date 6 hours before now.
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
let predicate = NSPredicate(format: "(%K < %@) AND (%K != %@)",
var predicate = NSPredicate(format: "(%K < %@) AND (%K != %@)",
#keyPath(InstalledApp.refreshedDate), date as NSDate,
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
// No additional predicate
}
else
{
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
}
var installedApps = InstalledApp.all(satisfying: predicate,
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
in: context)

View File

@@ -34,6 +34,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
let newsItemIdentifiers = Set(conflictedObject.newsItems.map { $0.identifier })
for app in databaseObject.apps
{
@@ -44,6 +45,15 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
}
}
for newsItem in databaseObject.newsItems
{
if !newsItemIdentifiers.contains(newsItem.identifier)
{
// No longer listed in Source, so remove it from database.
newsItem.managedObjectContext?.delete(newsItem)
}
}
default: break
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
//
// StoreAppPolicy.swift
// AltStore
//
// Created by Riley Testut on 9/14/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
@objc(StoreAppToStoreAppMigrationPolicy)
class StoreAppToStoreAppMigrationPolicy: NSEntityMigrationPolicy
{
@objc(migrateIconURL)
func migrateIconURL() -> URL
{
return URL(string: "https://via.placeholder.com/150")!
}
@objc(migrateScreenshotURLs)
func migrateScreenshotURLs() -> NSCopying
{
return [] as NSArray
}
}

View File

@@ -0,0 +1,90 @@
//
// NewsItem.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import CoreData
@objc(NewsItem)
class NewsItem: NSManagedObject, Decodable, Fetchable
{
/* Properties */
@NSManaged var identifier: String
@NSManaged var date: Date
@NSManaged var title: String
@NSManaged var caption: String
@NSManaged var tintColor: UIColor
@NSManaged var sortIndex: Int32
@NSManaged var isSilent: Bool
@NSManaged var imageURL: URL?
@NSManaged var externalURL: URL?
@NSManaged var appID: String?
/* Relationships */
@NSManaged var storeApp: StoreApp?
@NSManaged var source: Source?
private enum CodingKeys: String, CodingKey
{
case identifier
case date
case title
case caption
case tintColor
case imageURL
case externalURL = "url"
case appID
case notify
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
required init(from decoder: Decoder) throws
{
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
super.init(entity: NewsItem.entity(), insertInto: context)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.identifier = try container.decode(String.self, forKey: .identifier)
self.date = try container.decode(Date.self, forKey: .date)
self.title = try container.decode(String.self, forKey: .title)
self.caption = try container.decode(String.self, forKey: .caption)
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
{
guard let tintColor = UIColor(hexString: tintColorHex) else {
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
}
self.tintColor = tintColor
}
self.imageURL = try container.decodeIfPresent(URL.self, forKey: .imageURL)
self.externalURL = try container.decodeIfPresent(URL.self, forKey: .externalURL)
self.appID = try container.decodeIfPresent(String.self, forKey: .appID)
let notify = try container.decodeIfPresent(Bool.self, forKey: .notify) ?? false
self.isSilent = !notify
}
}
extension NewsItem
{
@nonobjc class func fetchRequest() -> NSFetchRequest<NewsItem>
{
return NSFetchRequest<NewsItem>(entityName: "NewsItem")
}
}

View File

@@ -0,0 +1,74 @@
//
// PatreonAccount.swift
// AltStore
//
// Created by Riley Testut on 8/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
extension PatreonAPI
{
struct AccountResponse: Decodable
{
struct Data: Decodable
{
struct Attributes: Decodable
{
var first_name: String?
var full_name: String
}
var id: String
var attributes: Attributes
}
var data: Data
var included: [PatronResponse]?
}
}
@objc(PatreonAccount)
class PatreonAccount: NSManagedObject, Fetchable
{
@NSManaged var identifier: String
@NSManaged var name: String
@NSManaged var firstName: String?
@NSManaged var isPatron: Bool
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
init(response: PatreonAPI.AccountResponse, context: NSManagedObjectContext)
{
super.init(entity: PatreonAccount.entity(), insertInto: context)
self.identifier = response.data.id
self.name = response.data.attributes.full_name
self.firstName = response.data.attributes.first_name
if let patronResponse = response.included?.first
{
let patron = Patron(response: patronResponse)
self.isPatron = (patron.status == .active)
}
else
{
self.isPatron = false
}
}
}
extension PatreonAccount
{
@nonobjc class func fetchRequest() -> NSFetchRequest<PatreonAccount>
{
return NSFetchRequest<PatreonAccount>(entityName: "PatreonAccount")
}
}

View File

@@ -11,6 +11,7 @@ import CoreData
extension Source
{
static let altStoreIdentifier = "com.rileytestut.AltStore"
static let altStoreSourceURL = URL(string: "https://cdn.altstore.io/file/altstore/apps.json")!
}
@objc(Source)
@@ -23,6 +24,7 @@ class Source: NSManagedObject, Fetchable, Decodable
/* Relationships */
@objc(apps) @NSManaged private(set) var _apps: NSOrderedSet
@objc(newsItems) @NSManaged private(set) var _newsItems: NSOrderedSet
@nonobjc var apps: [StoreApp] {
get {
@@ -33,12 +35,22 @@ class Source: NSManagedObject, Fetchable, Decodable
}
}
@nonobjc var newsItems: [NewsItem] {
get {
return self._newsItems.array as! [NewsItem]
}
set {
self._newsItems = NSOrderedSet(array: newValue)
}
}
private enum CodingKeys: String, CodingKey
{
case name
case identifier
case sourceURL
case apps
case news
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
@@ -63,10 +75,31 @@ class Source: NSManagedObject, Fetchable, Decodable
app.sortIndex = Int32(index)
}
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
for (index, item) in newsItems.enumerated()
{
item.sortIndex = Int32(index)
}
context.insert(self)
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { (a, b) in return a })
for newsItem in newsItems
{
newsItem.source = self
guard let appID = newsItem.appID else { continue }
if let storeApp = appsByID[appID]
{
newsItem.storeApp = storeApp
}
}
// Must assign after we're inserted into context.
self._apps = NSMutableOrderedSet(array: apps)
self._newsItems = NSMutableOrderedSet(array: newsItems)
print("Downloaded Order:", self.apps.map { $0.bundleIdentifier })
}
@@ -79,7 +112,7 @@ extension Source
let source = Source(context: context)
source.name = "AltStore"
source.identifier = Source.altStoreIdentifier
source.sourceURL = URL(string: "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1")!
source.sourceURL = Source.altStoreSourceURL
return source
}

View File

@@ -14,7 +14,13 @@ import AltSign
extension StoreApp
{
#if BETA
static let altstoreAppID = "com.rileytestut.AltStore.Beta"
static let alternativeAltStoreAppID = "com.rileytestut.AltStore"
#else
static let altstoreAppID = "com.rileytestut.AltStore"
static let alternativeAltStoreAppID = "com.rileytestut.AltStore.Beta"
#endif
}
@objc(StoreApp)
@@ -29,8 +35,8 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
@NSManaged private(set) var localizedDescription: String
@NSManaged private(set) var size: Int32
@NSManaged private(set) var iconName: String
@NSManaged private(set) var screenshotNames: [String]
@NSManaged private(set) var iconURL: URL
@NSManaged private(set) var screenshotURLs: [URL]
@NSManaged var version: String
@NSManaged private(set) var versionDate: Date
@@ -38,6 +44,7 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
@NSManaged private(set) var downloadURL: URL
@NSManaged private(set) var tintColor: UIColor?
@NSManaged private(set) var isBeta: Bool
@NSManaged var sortIndex: Int32
@@ -64,13 +71,14 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
case version
case versionDescription
case versionDate
case iconName
case screenshotNames
case iconURL
case screenshotURLs
case downloadURL
case tintColor
case subtitle
case permissions
case size
case isBeta = "beta"
}
required init(from decoder: Decoder) throws
@@ -91,8 +99,8 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
self.versionDate = try container.decode(Date.self, forKey: .versionDate)
self.versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
self.iconName = try container.decode(String.self, forKey: .iconName)
self.screenshotNames = try container.decodeIfPresent([String].self, forKey: .screenshotNames) ?? []
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
@@ -106,6 +114,7 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
}
self.size = try container.decode(Int32.self, forKey: .size)
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
@@ -130,12 +139,16 @@ extension StoreApp
app.bundleIdentifier = StoreApp.altstoreAppID
app.developerName = "Riley Testut"
app.localizedDescription = "AltStore is an alternative App Store."
app.iconName = ""
app.screenshotNames = []
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
app.screenshotURLs = []
app.version = "1.0"
app.versionDate = Date()
app.downloadURL = URL(string: "http://rileytestut.com")!
#if BETA
app.isBeta = true
#endif
return app
}
}

View File

@@ -14,6 +14,7 @@ class InstalledAppCollectionViewCell: UICollectionViewCell
@IBOutlet var nameLabel: UILabel!
@IBOutlet var developerLabel: UILabel!
@IBOutlet var refreshButton: PillButton!
@IBOutlet var betaBadgeView: UIImageView!
}
class InstalledAppsCollectionHeaderView: UICollectionReusableView

View File

@@ -13,6 +13,8 @@ import Roxas
import AltSign
import Nuke
private let maximumCollapsedUpdatesCount = 2
extension MyAppsViewController
@@ -79,9 +81,13 @@ class MyAppsViewController: UICollectionViewController
self.sideloadingProgressView = UIProgressView(progressViewStyle: .bar)
self.sideloadingProgressView.translatesAutoresizingMaskIntoConstraints = false
self.sideloadingProgressView.progressTintColor = .altGreen
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)
@@ -93,18 +99,33 @@ class MyAppsViewController: UICollectionViewController
// Gestures
self.longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(MyAppsViewController.handleLongPressGesture(_:)))
self.collectionView.addGestureRecognizer(self.longPressGestureRecognizer)
self.registerForPreviewing(with: self, sourceView: self.collectionView)
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.updateDataSource()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard segue.identifier == "showApp" else { return }
guard let identifier = segue.identifier else { return }
switch identifier
{
case "showApp", "showUpdate":
guard let cell = sender as? UICollectionViewCell, let indexPath = self.collectionView.indexPath(for: cell) else { return }
let installedApp = self.dataSource.item(at: indexPath)
let appViewController = segue.destination as! AppViewController
appViewController.app = installedApp.storeApp
default: break
}
}
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool
@@ -136,7 +157,7 @@ private extension MyAppsViewController
dynamicDataSource.cellConfigurationHandler = { (cell, _, indexPath) in
cell.layer.cornerRadius = 20
cell.layer.masksToBounds = true
cell.contentView.backgroundColor = UIColor.altGreen.withAlphaComponent(0.15)
cell.contentView.backgroundColor = UIColor.altPrimary.withAlphaComponent(0.15)
}
return dynamicDataSource
@@ -152,14 +173,17 @@ private extension MyAppsViewController
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.liveFetchLimit = maximumCollapsedUpdatesCount
dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
guard let self = self else { return }
guard let app = installedApp.storeApp else { return }
let cell = cell as! UpdateCollectionViewCell
cell.tintColor = app.tintColor ?? .altGreen
cell.tintColor = app.tintColor ?? .altPrimary
cell.nameLabel.text = app.name
cell.versionDescriptionTextView.text = app.versionDescription
cell.appIconImageView.image = UIImage(named: app.iconName)
cell.appIconImageView.image = nil
cell.appIconImageView.isIndicatingActivity = true
cell.betaBadgeView.isHidden = !app.isBeta
cell.updateButton.isIndicatingActivity = false
cell.updateButton.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
@@ -182,6 +206,34 @@ private extension MyAppsViewController
cell.setNeedsLayout()
}
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
guard let iconURL = installedApp.storeApp?.iconURL else { return nil }
return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image
{
completionHandler(image, nil)
}
else
{
completionHandler(nil, error)
}
})
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! UpdateCollectionViewCell
cell.appIconImageView.isIndicatingActivity = false
cell.appIconImageView.image = image
if let error = error
{
print("Error loading image:", error)
}
}
return dataSource
}
@@ -198,11 +250,12 @@ private extension MyAppsViewController
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellIdentifierHandler = { _ in "AppCell" }
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
let tintColor = installedApp.storeApp?.tintColor ?? .altGreen
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
let cell = cell as! InstalledAppCollectionViewCell
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)
@@ -264,6 +317,21 @@ private extension MyAppsViewController
return dataSource
}
func updateDataSource()
{
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
self.dataSource.predicate = nil
}
else
{
self.dataSource.predicate = NSPredicate(format: "%K == nil OR %K == NO OR %K == %@",
#keyPath(InstalledApp.storeApp),
#keyPath(InstalledApp.storeApp.isBeta),
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
}
}
}
private extension MyAppsViewController
@@ -281,10 +349,13 @@ private extension MyAppsViewController
UIApplication.shared.applicationIconBadgeNumber = 0
}
if self.isViewLoaded
{
UIView.performWithoutAnimation {
self.collectionView.reloadSections(IndexSet(integer: Section.updates.rawValue))
}
}
}
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping (Result<[String : Result<InstalledApp, Error>], Error>) -> Void)
{
@@ -313,17 +384,20 @@ private extension MyAppsViewController
guard !failures.isEmpty else { break }
let localizedText: String
let detailText: String?
if let failure = failures.first, failures.count == 1
{
localizedText = failure.value.localizedDescription
detailText = nil
}
else
{
localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count))
detailText = failures.first?.value.localizedDescription
}
let toastView = ToastView(text: localizedText, detailText: nil)
toastView.tintColor = .refreshRed
let toastView = ToastView(text: localizedText, detailText: detailText)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
}
@@ -512,7 +586,7 @@ private extension MyAppsViewController
self.present(documentPickerViewController, animated: true, completion: nil)
}
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 riley@rileytestut.com.", comment: ""), preferredStyle: .alert)
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)
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .default, handler: { (action) in
sideloadApp()
}))
@@ -584,10 +658,10 @@ extension MyAppsViewController
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader", for: indexPath) as! UpdatesCollectionHeaderView
UIView.performWithoutAnimation {
headerView.button.backgroundColor = UIColor.altGreen.withAlphaComponent(0.15)
headerView.button.backgroundColor = UIColor.altPrimary.withAlphaComponent(0.15)
headerView.button.setTitle("", for: .normal)
headerView.button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 28)
headerView.button.setTitleColor(.altGreen, for: .normal)
headerView.button.setTitleColor(.altPrimary, for: .normal)
headerView.button.addTarget(self, action: #selector(MyAppsViewController.toggleAppUpdates), for: .primaryActionTriggered)
if self.isUpdateSectionCollapsed
@@ -613,7 +687,7 @@ extension MyAppsViewController
headerView.textLabel.text = NSLocalizedString("Installed", comment: "")
headerView.button.isIndicatingActivity = false
headerView.button.activityIndicatorView.color = .altGreen
headerView.button.activityIndicatorView.color = .altPrimary
headerView.button.setTitle(NSLocalizedString("Refresh All", comment: ""), for: .normal)
headerView.button.addTarget(self, action: #selector(MyAppsViewController.refreshAllApps(_:)), for: .primaryActionTriggered)
headerView.button.isIndicatingActivity = self.isRefreshingAllApps
@@ -624,6 +698,19 @@ extension MyAppsViewController
return headerView
}
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let section = Section.allCases[indexPath.section]
switch section
{
case .updates:
guard let cell = collectionView.cellForItem(at: indexPath) else { break }
self.performSegue(withIdentifier: "showUpdate", sender: cell)
default: break
}
}
}
extension MyAppsViewController: UICollectionViewDelegateFlowLayout
@@ -795,3 +882,37 @@ extension MyAppsViewController: UIDocumentPickerDelegate
}
}
}
extension MyAppsViewController: UIViewControllerPreviewingDelegate
{
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
{
guard
let indexPath = self.collectionView.indexPathForItem(at: location),
let cell = self.collectionView.cellForItem(at: indexPath)
else { return nil }
let section = Section.allCases[indexPath.section]
switch section
{
case .updates:
previewingContext.sourceRect = cell.frame
let app = self.dataSource.item(at: indexPath)
guard let storeApp = app.storeApp else { return nil}
let appViewController = AppViewController.makeAppViewController(app: storeApp)
return appViewController
default: return nil
}
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
{
let point = CGPoint(x: previewingContext.sourceRect.midX, y: previewingContext.sourceRect.midY)
guard let indexPath = self.collectionView.indexPathForItem(at: point), let cell = self.collectionView.cellForItem(at: indexPath) else { return }
self.performSegue(withIdentifier: "showUpdate", sender: cell)
}
}

View File

@@ -31,6 +31,7 @@ extension UpdateCollectionViewCell
@IBOutlet var updateButton: PillButton!
@IBOutlet var versionDescriptionTitleLabel: UILabel!
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
@IBOutlet var betaBadgeView: UIImageView!
override func awakeFromNib()
{
@@ -57,6 +58,22 @@ extension UpdateCollectionViewCell
}
animator.startAnimation()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
let view = super.hitTest(point, with: event)
if view == self.versionDescriptionTextView
{
// Forward touches on the text view (but not on the nested "more" button)
// so cell selection works as expected.
return self
}
else
{
return view
}
}
}
private extension UpdateCollectionViewCell

View File

@@ -35,17 +35,25 @@
<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" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="2Ii-Hu-4ru">
<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>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" 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="172" height="20.5"/>
<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="172" height="14.5"/>
<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"/>
@@ -76,7 +84,7 @@
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
<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"/>
@@ -113,6 +121,7 @@
<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"/>
@@ -122,4 +131,7 @@
<point key="canvasLocation" x="618.39999999999998" y="96.251874062968525"/>
</collectionViewCell>
</objects>
<resources>
<image name="BetaBadge" width="41" height="17"/>
</resources>
</document>

View File

@@ -0,0 +1,27 @@
//
// NewsCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
class NewsCollectionViewCell: UICollectionViewCell
{
@IBOutlet var titleLabel: UILabel!
@IBOutlet var captionLabel: UILabel!
@IBOutlet var imageView: UIImageView!
override func awakeFromNib()
{
super.awakeFromNib()
self.contentView.layer.cornerRadius = 30
self.contentView.clipsToBounds = true
self.imageView.layer.cornerRadius = 30
self.imageView.clipsToBounds = true
}
}

View File

@@ -0,0 +1,77 @@
<?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>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<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" insetsLayoutMarginsFromSafeArea="NO" id="wRF-2R-NUG" customClass="NewsCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
<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="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"/>
<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"/>
<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"/>
<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">
<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"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<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"/>
<constraints>
<constraint firstAttribute="width" secondItem="l36-Bm-De0" secondAttribute="height" multiplier="67:40" id="QGD-YE-Hw2"/>
</constraints>
</imageView>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="tNk-9u-1tk" firstAttribute="top" secondItem="Xba-Qs-SQo" secondAttribute="top" id="Dw8-lF-Fzl"/>
<constraint firstAttribute="trailing" secondItem="tNk-9u-1tk" secondAttribute="trailing" id="Zt8-Wa-oB9"/>
<constraint firstItem="tNk-9u-1tk" firstAttribute="leading" secondItem="Xba-Qs-SQo" secondAttribute="leading" id="m6p-Ee-dTh"/>
<constraint firstAttribute="bottom" secondItem="tNk-9u-1tk" secondAttribute="bottom" constant="0.5" id="v9g-yC-db9"/>
</constraints>
</view>
</subviews>
</view>
<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 firstAttribute="bottom" secondItem="Xba-Qs-SQo" secondAttribute="bottom" id="Ecj-fN-hZv"/>
</constraints>
<connections>
<outlet property="captionLabel" destination="SHB-kk-YhL" id="zY3-qQ-9oY"/>
<outlet property="imageView" destination="l36-Bm-De0" id="3do-aQ-5r4"/>
<outlet property="titleLabel" destination="AkN-BE-I1a" id="hA2-3O-q5J"/>
</connections>
</collectionViewCell>
</objects>
</document>

View File

@@ -0,0 +1,459 @@
//
// NewsViewController.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import SafariServices
import Roxas
import Nuke
private class AppBannerFooterView: UICollectionReusableView
{
let bannerView = AppBannerView(frame: .zero)
let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
override init(frame: CGRect)
{
super.init(frame: frame)
self.addSubview(self.bannerView, pinningEdgesWith: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20))
self.addGestureRecognizer(self.tapGestureRecognizer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class NewsViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
private var prototypeCell: NewsCollectionViewCell!
private var loadingState: LoadingState = .loading {
didSet {
self.update()
}
}
// Cache
private var cachedCellSizes = [String: CGSize]()
override func viewDidLoad()
{
super.viewDidLoad()
self.prototypeCell = NewsCollectionViewCell.instantiate(with: NewsCollectionViewCell.nib!)
self.prototypeCell.translatesAutoresizingMaskIntoConstraints = false
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.contentInset.bottom = 20
self.collectionView.dataSource = self.dataSource
self.collectionView.prefetchDataSource = self.dataSource
self.collectionView.register(NewsCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.collectionView.register(AppBannerFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner")
self.registerForPreviewing(with: self, sourceView: self.collectionView)
self.update()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.fetchSource()
}
}
private extension NewsViewController
{
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>
{
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 dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>(fetchedResultsController: fetchedResultsController)
dataSource.proxy = self
dataSource.cellConfigurationHandler = { (cell, newsItem, indexPath) in
let cell = cell as! NewsCollectionViewCell
cell.titleLabel.text = newsItem.title
cell.captionLabel.text = newsItem.caption
cell.contentView.backgroundColor = newsItem.tintColor
cell.imageView.image = nil
if newsItem.imageURL != nil
{
cell.imageView.isIndicatingActivity = true
cell.imageView.isHidden = false
}
else
{
cell.imageView.isIndicatingActivity = false
cell.imageView.isHidden = true
}
}
dataSource.prefetchHandler = { (newsItem, indexPath, completionHandler) in
guard let imageURL = newsItem.imageURL else { return nil }
return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: imageURL, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image
{
completionHandler(image, nil)
}
else
{
completionHandler(nil, error)
}
})
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! NewsCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.imageView.image = image
if let error = error
{
print("Error loading image:", error)
}
}
dataSource.placeholderView = self.placeholderView
return dataSource
}
func fetchSource()
{
self.loadingState = .loading
AppManager.shared.fetchSource() { (result) in
do
{
let source = try result.get()
try source.managedObjectContext?.save()
DispatchQueue.main.async {
self.loadingState = .finished(.success(()))
}
}
catch
{
DispatchQueue.main.async {
if self.dataSource.itemCount > 0
{
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
}
self.loadingState = .finished(.failure(error))
}
}
}
}
func update()
{
switch self.loadingState
{
case .loading:
self.placeholderView.textLabel.isHidden = true
self.placeholderView.detailTextLabel.isHidden = false
self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
self.placeholderView.activityIndicatorView.startAnimating()
case .finished(.failure(let error)):
self.placeholderView.textLabel.isHidden = false
self.placeholderView.detailTextLabel.isHidden = false
self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch News", comment: "")
self.placeholderView.detailTextLabel.text = error.localizedDescription
self.placeholderView.activityIndicatorView.stopAnimating()
case .finished(.success):
self.placeholderView.textLabel.isHidden = true
self.placeholderView.detailTextLabel.isHidden = true
self.placeholderView.activityIndicatorView.stopAnimating()
}
}
}
private extension NewsViewController
{
@objc func handleTapGesture(_ gestureRecognizer: UITapGestureRecognizer)
{
guard let footerView = gestureRecognizer.view as? UICollectionReusableView else { return }
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
let supplementaryView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath)
return supplementaryView == footerView
}) else { return }
let item = self.dataSource.item(at: indexPath)
guard let storeApp = item.storeApp else { return }
let appViewController = AppViewController.makeAppViewController(app: storeApp)
self.navigationController?.pushViewController(appViewController, animated: true)
}
@objc func performAppAction(_ sender: PillButton)
{
let point = self.collectionView.convert(sender.center, from: sender.superview)
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
let supplementaryView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath)
return supplementaryView?.frame.contains(point) ?? false
}) else { return }
let app = self.dataSource.item(at: indexPath)
guard let storeApp = app.storeApp else { return }
if let installedApp = app.storeApp?.installedApp
{
self.open(installedApp)
}
else
{
self.install(storeApp, at: indexPath)
}
}
@objc func install(_ storeApp: StoreApp, at indexPath: IndexPath)
{
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
guard previousProgress == nil else {
previousProgress?.cancel()
return
}
_ = AppManager.shared.install(storeApp, presentingViewController: self) { (result) in
DispatchQueue.main.async {
switch result
{
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)
case .success: print("Installed app:", storeApp.bundleIdentifier)
}
UIView.performWithoutAnimation {
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
}
}
}
UIView.performWithoutAnimation {
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
}
}
func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL)
}
}
extension NewsViewController
{
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let newsItem = self.dataSource.item(at: indexPath)
if let externalURL = newsItem.externalURL
{
let safariViewController = SFSafariViewController(url: externalURL)
safariViewController.preferredControlTintColor = newsItem.tintColor
self.present(safariViewController, animated: true, completion: nil)
}
else if let storeApp = newsItem.storeApp
{
let appViewController = AppViewController.makeAppViewController(app: storeApp)
self.navigationController?.pushViewController(appViewController, animated: true)
}
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
let item = self.dataSource.item(at: indexPath)
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner", for: indexPath) as! AppBannerFooterView
guard let storeApp = item.storeApp else { return footerView }
footerView.bannerView.titleLabel.text = storeApp.name
footerView.bannerView.subtitleLabel.text = storeApp.developerName
footerView.bannerView.tintColor = storeApp.tintColor
footerView.bannerView.betaBadgeView.isHidden = !storeApp.isBeta
footerView.bannerView.button.addTarget(self, action: #selector(NewsViewController.performAppAction(_:)), for: .primaryActionTriggered)
footerView.tapGestureRecognizer.addTarget(self, action: #selector(NewsViewController.handleTapGesture(_:)))
footerView.bannerView.button.isIndicatingActivity = false
if storeApp.installedApp == nil
{
footerView.bannerView.button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress
footerView.bannerView.button.isInverted = false
if Date() < storeApp.versionDate
{
footerView.bannerView.button.countdownDate = storeApp.versionDate
}
else
{
footerView.bannerView.button.countdownDate = nil
}
}
else
{
footerView.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
footerView.bannerView.button.progress = nil
footerView.bannerView.button.isInverted = true
footerView.bannerView.button.countdownDate = nil
}
Nuke.loadImage(with: storeApp.iconURL, into: footerView.bannerView.iconImageView)
return footerView
}
}
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]
{
return previousSize
}
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: width)
NSLayoutConstraint.activate([widthConstraint])
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
self.cachedCellSizes[item.identifier] = size
return size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
{
let item = self.dataSource.item(at: IndexPath(row: 0, section: section))
if item.storeApp != nil
{
return CGSize(width: 88, height: 88)
}
else
{
return .zero
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
{
var insets = UIEdgeInsets(top: 30, left: 20, bottom: 13, right: 20)
if section == 0
{
insets.top = 10
}
return insets
}
}
extension NewsViewController: UIViewControllerPreviewingDelegate
{
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
{
if let indexPath = self.collectionView.indexPathForItem(at: location), let cell = self.collectionView.cellForItem(at: indexPath)
{
// Previewing news item.
previewingContext.sourceRect = cell.frame
let newsItem = self.dataSource.item(at: indexPath)
if let externalURL = newsItem.externalURL
{
let safariViewController = SFSafariViewController(url: externalURL)
safariViewController.preferredControlTintColor = newsItem.tintColor
return safariViewController
}
else if let storeApp = newsItem.storeApp
{
let appViewController = AppViewController.makeAppViewController(app: storeApp)
return appViewController
}
return nil
}
else
{
// Previewing app banner (or nothing).
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
let layoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath)
return layoutAttributes?.frame.contains(location) ?? false
}) else { return nil }
guard let layoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath) else { return nil }
previewingContext.sourceRect = layoutAttributes.frame
let item = self.dataSource.item(at: indexPath)
guard let storeApp = item.storeApp else { return nil }
let appViewController = AppViewController.makeAppViewController(app: storeApp)
return appViewController
}
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
{
if let safariViewController = viewControllerToCommit as? SFSafariViewController
{
self.present(safariViewController, animated: true, completion: nil)
}
else
{
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
}
}
}

View File

@@ -34,10 +34,15 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
{
private weak var presentingViewController: UIViewController?
private lazy var navigationController = UINavigationController()
private lazy var navigationController: UINavigationController = {
let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
return navigationController
}()
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
private var appleIDPassword: String?
private var shouldShowInstructions = false
init(presentingViewController: UIViewController?)
{
@@ -82,6 +87,7 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
case .success(let certificate):
self.progress.completedUnitCount += 1
self.showInstructionsIfNecessary() { (didShowInstructions) in
let signer = ALTSigner(team: team, certificate: certificate)
self.finish(.success(signer))
}
@@ -91,6 +97,7 @@ class AuthenticationOperation: ResultOperation<ALTSigner>
}
}
}
}
override func finish(_ result: Result<ALTSigner, Error>)
{
@@ -161,7 +168,7 @@ private extension AuthenticationOperation
{
guard let presentingViewController = self.presentingViewController else { return false }
self.navigationController.view.tintColor = .altPurple
self.navigationController.view.tintColor = .white
if self.navigationController.viewControllers.isEmpty
{
@@ -191,8 +198,11 @@ private extension AuthenticationOperation
authenticationViewController.authenticationHandler = { (result) in
if let (account, password) = result
{
self.appleIDPassword = password
// 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))
}
else
@@ -242,29 +252,21 @@ private extension AuthenticationOperation
{
func selectTeam(from teams: [ALTTeam])
{
if let team = teams.first, teams.count == 1
if let team = teams.first(where: { $0.type == .free })
{
return completionHandler(.success(team))
}
DispatchQueue.main.async {
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
selectTeamViewController.teams = teams
selectTeamViewController.selectionHandler = { (team) in
if let team = team
else if let team = teams.first(where: { $0.type == .individual })
{
completionHandler(.success(team))
return completionHandler(.success(team))
}
else if let team = teams.first
{
return completionHandler(.success(team))
}
else
{
completionHandler(.failure(OperationError.cancelled))
}
}
if !self.present(selectTeamViewController)
{
completionHandler(.failure(AuthenticationError.noTeam))
}
return completionHandler(.failure(AuthenticationError.noTeam))
}
}
@@ -273,7 +275,6 @@ private extension AuthenticationOperation
{
case .failure(let error): completionHandler(.failure(error))
case .success(let teams):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier })
{
@@ -326,8 +327,8 @@ private extension AuthenticationOperation
func replaceCertificate(from certificates: [ALTCertificate])
{
if let certificate = certificates.first, certificates.count == 1
{
guard let certificate = certificates.first else { return completionHandler(.failure(AuthenticationError.noCertificate)) }
ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in
if let error = error, !success
{
@@ -338,30 +339,6 @@ private extension AuthenticationOperation
requestCertificate()
}
}
return
}
DispatchQueue.main.async {
let replaceCertificateViewController = self.storyboard.instantiateViewController(withIdentifier: "replaceCertificateViewController") as! ReplaceCertificateViewController
replaceCertificateViewController.team = team
replaceCertificateViewController.certificates = certificates
replaceCertificateViewController.replacementHandler = { (certificate) in
if certificate != nil
{
requestCertificate()
}
else
{
completionHandler(.failure(OperationError.cancelled))
}
}
if !self.present(replaceCertificateViewController)
{
completionHandler(.failure(AuthenticationError.noCertificate))
}
}
}
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
@@ -393,4 +370,21 @@ private extension AuthenticationOperation
}
}
func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
{
guard self.shouldShowInstructions else { return completionHandler(false) }
DispatchQueue.main.async {
let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
instructionsViewController.showsBottomButton = true
instructionsViewController.completionHandler = {
completionHandler(true)
}
if !self.present(instructionsViewController)
{
completionHandler(false)
}
}
}
}

View File

@@ -15,6 +15,7 @@ import AltSign
class DownloadAppOperation: ResultOperation<ALTApplication>
{
let app: AppProtocol
let context: AppOperationContext
private let bundleIdentifier: String
private let sourceURL: URL
@@ -22,9 +23,11 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
private let session = URLSession(configuration: .default)
init(app: AppProtocol)
init(app: AppProtocol, context: AppOperationContext)
{
self.app = app
self.context = context
self.bundleIdentifier = app.bundleIdentifier
self.sourceURL = app.url
self.destinationURL = InstalledApp.fileURL(for: app)
@@ -38,6 +41,12 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
print("Downloading App:", self.bundleIdentifier)
func finishOperation(_ result: Result<URL, Error>)

View File

@@ -14,17 +14,22 @@ class FetchSourceOperation: ResultOperation<Source>
{
let sourceURL: URL
private let session = URLSession(configuration: .default)
private let session: URLSession
private lazy var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
private lazy var dateFormatter: ISO8601DateFormatter = {
let dateFormatter = ISO8601DateFormatter()
return dateFormatter
}()
init(sourceURL: URL)
{
self.sourceURL = sourceURL
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
configuration.urlCache = nil
self.session = URLSession(configuration: configuration)
}
override func main()
@@ -38,7 +43,27 @@ class FetchSourceOperation: ResultOperation<Source>
let (data, _) = try Result((data, response), error).get()
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(self.dateFormatter)
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let text = try container.decode(String.self)
// Full ISO8601 Format.
self.dateFormatter.formatOptions = [.withFullDate, .withFullTime, .withTimeZone]
if let date = self.dateFormatter.date(from: text)
{
return date
}
// Just date portion of ISO8601.
self.dateFormatter.formatOptions = [.withFullDate]
if let date = self.dateFormatter.date(from: text)
{
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Date is in invalid format.")
})
decoder.managedObjectContext = context
let source = try decoder.decode(Source.self, from: data)

View File

@@ -0,0 +1,51 @@
//
// FindServerOperation.swift
// AltStore
//
// Created by Riley Testut on 9/8/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
@objc(FindServerOperation)
class FindServerOperation: ResultOperation<Server>
{
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
}
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))
}
}
}

View File

@@ -395,10 +395,11 @@ private extension ResignAppOperation
var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes]
if self.context.bundleIdentifier == StoreApp.altstoreAppID
if self.context.bundleIdentifier == StoreApp.altstoreAppID || self.context.bundleIdentifier == StoreApp.alternativeAltStoreAppID
{
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
}
// Prepare app

View File

@@ -0,0 +1,27 @@
//
// Benefit.swift
// AltStore
//
// Created by Riley Testut on 8/21/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension PatreonAPI
{
struct BenefitResponse: Decodable
{
var id: String
}
}
struct Benefit: Hashable
{
var type: ALTPatreonBenefitType
init(response: PatreonAPI.BenefitResponse)
{
self.type = ALTPatreonBenefitType(response.id)
}
}

View File

@@ -0,0 +1,27 @@
//
// Campaign.swift
// AltStore
//
// Created by Riley Testut on 8/21/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension PatreonAPI
{
struct CampaignResponse: Decodable
{
var id: String
}
}
struct Campaign
{
var identifier: String
init(response: PatreonAPI.CampaignResponse)
{
self.identifier = response.id
}
}

View File

@@ -0,0 +1,385 @@
//
// PatreonAPI.swift
// AltStore
//
// Created by Riley Testut on 8/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import AuthenticationServices
private let clientID = "ZMx0EGUWe4TVWYXNZZwK_fbIK5jHFVWoUf1Qb-sqNXmT-YzAGwDPxxq7ak3_W5Q2"
private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt"
private let creatorAccessToken = "mBh0yyK40Ibjzwb_cYeKIuzq8nNFBdEIlNPfgAQlhcU"
private let campaignID = "2863968"
extension PatreonAPI
{
enum Error: LocalizedError
{
case unknown
case notAuthenticated
var errorDescription: String? {
switch self
{
case .unknown: return NSLocalizedString("An unknown error occurred.", comment: "")
case .notAuthenticated: return NSLocalizedString("No connected Patreon account.", comment: "")
}
}
}
enum AuthorizationType
{
case none
case user
case creator
}
enum AnyResponse: Decodable
{
case tier(TierResponse)
case benefit(BenefitResponse)
enum CodingKeys: String, CodingKey
{
case type
}
init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type
{
case "tier":
let tier = try TierResponse(from: decoder)
self = .tier(tier)
case "benefit":
let benefit = try BenefitResponse(from: decoder)
self = .benefit(benefit)
default: throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unrecognized Patreon response type.")
}
}
}
}
class PatreonAPI
{
static let shared = PatreonAPI()
var isAuthenticated: Bool {
return Keychain.shared.patreonAccessToken != nil
}
private var authenticationSession: ASWebAuthenticationSession?
private let session = URLSession(configuration: .ephemeral)
private let baseURL = URL(string: "https://www.patreon.com/")!
private init()
{
}
}
extension PatreonAPI
{
func authenticate(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
{
var components = URLComponents(string: "/oauth2/authorize")!
components.queryItems = [URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "client_id", value: clientID),
URLQueryItem(name: "redirect_uri", value: "https://rileytestut.com/patreon/altstore")]
let requestURL = components.url(relativeTo: self.baseURL)!
self.authenticationSession = ASWebAuthenticationSession(url: requestURL, callbackURLScheme: "altstore") { (callbackURL, error) in
do
{
let callbackURL = try Result(callbackURL, error).get()
guard
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }),
let code = codeQueryItem.value
else { throw Error.unknown }
self.fetchAccessToken(oauthCode: code) { (result) in
switch result
{
case .failure(let error): completion(.failure(error))
case .success(let accessToken, let refreshToken):
Keychain.shared.patreonAccessToken = accessToken
Keychain.shared.patreonRefreshToken = refreshToken
self.fetchAccount(completion: completion)
}
}
}
catch
{
completion(.failure(error))
}
}
self.authenticationSession?.start()
}
func fetchAccount(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
{
var components = URLComponents(string: "/api/oauth2/v2/identity")!
components.queryItems = [URLQueryItem(name: "include", value: "memberships"),
URLQueryItem(name: "fields[user]", value: "first_name,full_name"),
URLQueryItem(name: "fields[member]", value: "full_name,patron_status")]
let requestURL = components.url(relativeTo: self.baseURL)!
let request = URLRequest(url: requestURL)
self.send(request, authorizationType: .user) { (result: Result<AccountResponse, Swift.Error>) in
switch result
{
case .failure(Error.notAuthenticated):
self.signOut() { (result) in
completion(.failure(Error.notAuthenticated))
}
case .failure(let error): completion(.failure(error))
case .success(let response):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let account = PatreonAccount(response: response, context: context)
completion(.success(account))
}
}
}
}
func fetchPatrons(completion: @escaping (Result<[Patron], Swift.Error>) -> Void)
{
var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(campaignID)/members")!
components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"),
URLQueryItem(name: "fields[tier]", value: "title"),
URLQueryItem(name: "fields[member]", value: "full_name,patron_status")]
let requestURL = components.url(relativeTo: self.baseURL)!
struct Response: Decodable
{
var data: [PatronResponse]
var included: [AnyResponse]
var links: [String: URL]?
}
var allPatrons = [Patron]()
func fetchPatrons(url: URL)
{
let request = URLRequest(url: url)
self.send(request, authorizationType: .creator) { (result: Result<Response, Swift.Error>) in
switch result
{
case .failure(let error): completion(.failure(error))
case .success(let response):
let tiers = response.included.compactMap { (response) -> Tier? in
switch response
{
case .tier(let tierResponse): return Tier(response: tierResponse)
case .benefit: return nil
}
}
let tiersByIdentifier = Dictionary(tiers.map { ($0.identifier, $0) }, uniquingKeysWith: { (a, b) in return a })
let patrons = response.data.map { (response) -> Patron in
let patron = Patron(response: response)
for tierID in response.relationships?.currently_entitled_tiers.data ?? []
{
guard let tier = tiersByIdentifier[tierID.id] else { continue }
patron.benefits.formUnion(tier.benefits)
}
return patron
}.filter { $0.benefits.contains(where: { $0.type == .credits }) }
allPatrons.append(contentsOf: patrons)
if let nextURL = response.links?["next"]
{
fetchPatrons(url: nextURL)
}
else
{
completion(.success(allPatrons))
}
}
}
}
fetchPatrons(url: requestURL)
}
func signOut(completion: @escaping (Result<Void, Swift.Error>) -> Void)
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let accounts = PatreonAccount.all(in: context)
accounts.forEach(context.delete(_:))
do
{
try context.save()
Keychain.shared.patreonAccessToken = nil
Keychain.shared.patreonRefreshToken = nil
completion(.success(()))
}
catch
{
completion(.failure(error))
}
}
}
}
extension PatreonAPI
{
func refreshPatreonAccount()
{
guard PatreonAPI.shared.isAuthenticated else { return }
PatreonAPI.shared.fetchAccount { (result: Result<PatreonAccount, Swift.Error>) in
do
{
let account = try result.get()
try account.managedObjectContext?.save()
}
catch
{
print("Failed to fetch Patreon account.", error)
}
}
}
}
private extension PatreonAPI
{
func fetchAccessToken(oauthCode: String, completion: @escaping (Result<(String, String), Swift.Error>) -> Void)
{
let encodedRedirectURI = ("https://rileytestut.com/patreon/altstore" as NSString).addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
let encodedOauthCode = (oauthCode as NSString).addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
let body = "code=\(encodedOauthCode)&grant_type=authorization_code&client_id=\(clientID)&client_secret=\(clientSecret)&redirect_uri=\(encodedRedirectURI)"
let requestURL = URL(string: "/api/oauth2/token", relativeTo: self.baseURL)!
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
request.httpBody = body.data(using: .utf8)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
struct Response: Decodable
{
var access_token: String
var refresh_token: String
}
self.send(request, authorizationType: .none) { (result: Result<Response, Swift.Error>) in
switch result
{
case .failure(let error): completion(.failure(error))
case .success(let response): completion(.success((response.access_token, response.refresh_token)))
}
}
}
func refreshAccessToken(completion: @escaping (Result<Void, Swift.Error>) -> Void)
{
guard let refreshToken = Keychain.shared.patreonRefreshToken else { return }
var components = URLComponents(string: "/api/oauth2/token")!
components.queryItems = [URLQueryItem(name: "grant_type", value: "refresh_token"),
URLQueryItem(name: "refresh_token", value: refreshToken),
URLQueryItem(name: "client_id", value: clientID),
URLQueryItem(name: "client_secret", value: clientSecret)]
let requestURL = components.url(relativeTo: self.baseURL)!
var request = URLRequest(url: requestURL)
request.httpMethod = "POST"
struct Response: Decodable
{
var access_token: String
var refresh_token: String
}
self.send(request, authorizationType: .none) { (result: Result<Response, Swift.Error>) in
switch result
{
case .failure(let error): completion(.failure(error))
case .success(let response):
Keychain.shared.patreonAccessToken = response.access_token
Keychain.shared.patreonRefreshToken = response.refresh_token
completion(.success(()))
}
}
}
func send<ResponseType: Decodable>(_ request: URLRequest, authorizationType: AuthorizationType, completion: @escaping (Result<ResponseType, Swift.Error>) -> Void)
{
var request = request
switch authorizationType
{
case .none: break
case .creator:
request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization")
case .user:
guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(Error.notAuthenticated)) }
request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
}
let task = self.session.dataTask(with: request) { (data, response, error) in
do
{
let data = try Result(data, error).get()
if let response = response as? HTTPURLResponse, response.statusCode == 401
{
if authorizationType == .user
{
self.refreshAccessToken() { (result) in
switch result
{
case .failure(let error): completion(.failure(error))
case .success: self.send(request, authorizationType: authorizationType, completion: completion)
}
}
}
else
{
completion(.failure(Error.notAuthenticated))
}
return
}
let response = try JSONDecoder().decode(ResponseType.self, from: data)
completion(.success(response))
}
catch let error
{
completion(.failure(error))
}
}
task.resume()
}
}

View File

@@ -0,0 +1,78 @@
//
// Patron.swift
// AltStore
//
// Created by Riley Testut on 8/21/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension PatreonAPI
{
struct PatronResponse: Decodable
{
struct Attributes: Decodable
{
var full_name: String
var patron_status: String?
}
struct Relationships: Decodable
{
struct Tiers: Decodable
{
struct TierID: Decodable
{
var id: String
var type: String
}
var data: [TierID]
}
var currently_entitled_tiers: Tiers
}
var id: String
var attributes: Attributes
var relationships: Relationships?
}
}
extension Patron
{
enum Status: String, Decodable
{
case active = "active_patron"
case declined = "declined_patron"
case former = "former_patron"
case unknown = "unknown"
}
}
class Patron
{
var name: String
var identifier: String
var status: Status
var benefits: Set<Benefit> = []
init(response: PatreonAPI.PatronResponse)
{
self.name = response.attributes.full_name
self.identifier = response.id
if let status = response.attributes.patron_status
{
self.status = Status(rawValue: status) ?? .unknown
}
else
{
self.status = .unknown
}
}
}

View File

@@ -0,0 +1,50 @@
//
// Tier.swift
// AltStore
//
// Created by Riley Testut on 8/21/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension PatreonAPI
{
struct TierResponse: Decodable
{
struct Attributes: Decodable
{
var title: String
}
struct Relationships: Decodable
{
struct Benefits: Decodable
{
var data: [BenefitResponse]
}
var benefits: Benefits
}
var id: String
var attributes: Attributes
var relationships: Relationships
}
}
struct Tier
{
var name: String
var identifier: String
var benefits: [Benefit] = []
init(response: PatreonAPI.TierResponse)
{
self.name = response.attributes.title
self.identifier = response.id
self.benefits = response.relationships.benefits.data.map(Benefit.init(response:))
}
}

View File

@@ -1,78 +0,0 @@
{
"name": "AltStore",
"identifier": "com.rileytestut.AltStore",
"sourceURL": "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1",
"apps": [
{
"name": "AltStore",
"bundleIdentifier": "com.rileytestut.AltStore",
"developerName": "Riley Testut",
"version": "0.2",
"versionDate": "2019-07-31",
"versionDescription": "AltStore has been updated with bug fixes and improvements and other nice goodies for you to enjoy.",
"downloadURL": "https://www.dropbox.com/s/w1gn9iztlqvltyp/AltStore.ipa?dl=1",
"localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.",
"iconName": "AppIcon",
"size": 10010524,
"permissions": [
{
"type": "background-fetch",
"usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring."
},
{
"type": "background-audio",
"usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background."
}
]
},
{
"name": "Delta",
"bundleIdentifier": "com.rileytestut.Delta",
"developerName": "Riley Testut",
"subtitle": "Classic games in your pocket.",
"version": "0.82",
"versionDate": "2019-07-31",
"versionDescription": "Finally, after almost 5 years of waiting, Delta is out of beta and ready for everyone to enjoy!\n\nCurrently supports NES, SNES, N64, GB(C), and GBA games, with more to come in the future.",
"downloadURL": "https://www.dropbox.com/s/31i4hcqnorucrxi/Delta.ipa?dl=1",
"localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.",
"iconName": "DeltaIcon",
"tintColor": "8A28F7",
"size": 26908804,
"permissions": [
{
"type": "photos",
"usageDescription": "Allows Delta to use images from your Photo Library as game artwork."
}
],
"screenshotNames": [
"Delta1",
"Delta2",
"Delta3"
]
},
{
"name": "Clip",
"bundleIdentifier": "com.rileytestut.Clip",
"subtitle": "Manage your clipboard history with ease.",
"developerName": "Riley Testut",
"version": "0.2",
"versionDate": "2019-07-31",
"versionDescription": "Bug fixes and improvements.",
"downloadURL": "https://www.dropbox.com/s/x11b4m8jvmz6tpl/Clip.ipa?dl=1",
"localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.",
"iconName": "ClipboardIcon",
"tintColor": "EC008C",
"size": 438855,
"permissions": [
{
"type": "background-audio",
"usageDescription": "Allows Clip to continuously monitor your clipboard in the background."
}
],
"screenshotNames": [
"Clip1",
"Clip2"
]
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -1,6 +0,0 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "DeltaIcon.png"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "IMG_4222.png"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "IMG_4221.png"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -1,6 +0,0 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -33,13 +33,13 @@
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Asset 1@120.png",
"filename" : "Group 23_120.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Asset 1@180.png",
"filename" : "Group 23_180.png",
"scale" : "3x"
},
{
@@ -90,7 +90,7 @@
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Asset 1@1024.png",
"filename" : "Group 23.png",
"scale" : "1x"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

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