Merge branch 'SideStore:develop' into csdev

This commit is contained in:
CelloSerenity
2025-11-08 19:54:10 -07:00
committed by GitHub
17 changed files with 586 additions and 429 deletions

View File

@@ -73,8 +73,8 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
outputs:
@@ -426,8 +426,8 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
steps:
@@ -443,7 +443,7 @@ jobs:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.2'
xcode-version: '26.0'
# - name: (Tests-Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
@@ -610,8 +610,8 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
steps:
@@ -628,7 +628,7 @@ jobs:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.2'
xcode-version: '26.0'
# - name: (Tests-Run) Cache Build
# uses: irgaly/xcode-cache@v1.8.1

View File

@@ -35,8 +35,8 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
outputs:
version: ${{ steps.version.outputs.version }}

View File

@@ -19,8 +19,8 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
steps:
@@ -37,7 +37,7 @@ jobs:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.2'
xcode-version: '26.0'
# - name: (Tests-Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1

View File

@@ -19,8 +19,8 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
steps:
@@ -38,7 +38,7 @@ jobs:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.2'
xcode-version: '26.0'
# - name: (Tests-Run) Cache Build
# uses: irgaly/xcode-cache@v1.8.1

View File

@@ -12,8 +12,8 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
- os: 'macos-26'
version: '26.0'
runs-on: ${{ matrix.os }}
steps:

View File

@@ -3472,7 +3472,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
@@ -3500,7 +3500,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
@@ -3527,7 +3527,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
@@ -3554,7 +3554,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";

View File

@@ -532,6 +532,7 @@
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" name="Primary"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
@@ -561,6 +562,7 @@
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="CzO-Kt-BlZ" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
@@ -913,6 +915,7 @@
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="9sB-f3-Fnk">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>

View File

@@ -11,83 +11,69 @@ import Roxas
import minimuxer
import WidgetKit
import AltSign
import AltStoreCore
import UniformTypeIdentifiers
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
{
final class LaunchViewController: UIViewController, UIDocumentPickerDelegate {
private var didFinishLaunching = false
private var destinationViewController: TabBarController!
override var launchConditions: [RSTLaunchCondition] {
let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in
DatabaseManager.shared.start(completionHandler: completionHandler)
}
private var retries = 0
private var maxRetries = 3
private var splashView: SplashView!
private var destinationViewController: TabBarController?
private var startTime: Date!
return [isDatabaseStarted]
}
override var childForStatusBarStyle: UIViewController? {
return self.children.first
}
override var childForStatusBarHidden: UIViewController? {
return self.children.first
}
override func viewDidLoad()
{
defer {
// Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
}
override func viewDidLoad() {
super.viewDidLoad()
splashView = SplashView(frame: view.bounds, appName: "SideStore")
destinationViewController = storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController
view.addSubview(splashView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if #available(iOS 17, *), !UserDefaults.standard.sidejitenable {
DispatchQueue.global().async {
self.isSideJITServerDetected() { result in
DispatchQueue.main.async {
switch result {
case .success():
let dialogMessage = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
// Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
UserDefaults.standard.sidejitenable = true
})
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
//Add OK button to a dialog message
dialogMessage.addAction(ok)
dialogMessage.addAction(cancel)
// Present Alert to
self.present(dialogMessage, animated: true, completion: nil)
case .failure(_):
print("Cannot find sideJITServer")
super.viewDidAppear(animated)
guard !didFinishLaunching else { return }
Task {
startTime = Date()
await runLaunchSequence()
doPostLaunch()
}
}
private func runLaunchSequence() async {
guard retries < maxRetries else { return }
retries += 1
await Task.detached {
if !DatabaseManager.shared.isStarted {
await withCheckedContinuation { continuation in
DatabaseManager.shared.start { error in
if let error {
Task { await self.handleLaunchError(error, retryCallback: self.runLaunchSequence) }
} else {
Task { await self.finishLaunching() }
}
continuation.resume(returning: ())
}
}
} else {
await self.finishLaunching()
}
}
}.value
}
private func doPostLaunch() {
SideJITManager.shared.checkAndPromptIfNeeded(presentingVC: self)
if #available(iOS 17, *), UserDefaults.standard.sidejitenable {
DispatchQueue.global().async {
self.askfornetwork()
}
DispatchQueue.global().async { SideJITManager.shared.askForNetwork() }
print("SideJITServer Enabled")
}
#if !targetEnvironment(simulator)
detectAndImportAccountFile()
guard let pf = fetchPairingFile() else {
displayError("Device pairing file not found.")
return
@@ -95,216 +81,128 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
start_minimuxer_threads(pf)
#endif
}
func askfornetwork() {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
var SJSURL = address
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
// Create a network operation at launch to Refresh SideJITServer
let url = URL(string: "\(SJSURL)/re/")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
print(data)
}
task.resume()
}
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
var SJSURL = address
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
// Create a network operation at launch to Refresh SideJITServer
let url = URL(string: SJSURL)!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print("No SideJITServer on Network")
completion(.failure(error))
return
}
completion(.success(()))
}
task.resume()
return
}
func fetchPairingFile() -> String? {
let filename = "ALTPairingFile.mobiledevicepairing"
let fm = FileManager.default
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
print("Loaded ALTPairingFile from \(documentsPath.path)")
return contents
} else if
let appResourcePath = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"),
fm.fileExists(atPath: appResourcePath.path),
let data = fm.contents(atPath: appResourcePath.path),
let contents = String(data: data, encoding: .utf8),
!contents.isEmpty,
!UserDefaults.standard.isPairingReset {
print("Loaded ALTPairingFile from \(appResourcePath.path)")
return contents
} else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{
print("Loaded ALTPairingFile from Info.plist")
return plistString
} else {
// Show an alert explaining the pairing file
// Create new Alert
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert)
// Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
// Try to load it from a file picker
var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil)
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data))
types.append(.xml)
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types)
documentPickerController.shouldShowFileExtensions = true
documentPickerController.delegate = self
self.present(documentPickerController, animated: true, completion: nil)
UserDefaults.standard.isPairingReset = false
})
//Add "help" button to take user to wiki
let wikiOption = UIAlertAction(title: "Help", style: .default) { (action) in
let wikiURL: String = "https://docs.sidestore.io/docs/installation/pairing-file"
if let url = URL(string: wikiURL) {
UIApplication.shared.open(url)
}
sleep(2)
exit(0)
}
//Add buttons to dialog message
dialogMessage.addAction(wikiOption)
dialogMessage.addAction(ok)
// Present Alert to
self.present(dialogMessage, animated: true, completion: nil)
let dialogMessage2 = UIAlertController(title: "Analytics", message: "This app contains anonymous analytics for research and project development. By continuing to use this app, you are consenting to this data collection", preferredStyle: .alert)
let ok2 = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in})
dialogMessage2.addAction(ok2)
self.present(dialogMessage2, animated: true, completion: nil)
return nil
}
}
func displayError(_ msg: String) {
print(msg)
// Create a new alert
let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
// Present alert to user
self.present(dialogMessage, animated: true, completion: nil)
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
let url = urls[0]
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
do {
// Read to a string
let data1 = try Data(contentsOf: urls[0])
let pairing_string = String(bytes: data1, encoding: .utf8)
if pairing_string == nil {
displayError("Unable to read pairing file")
}
// Save to a file for next launch
let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")
try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8)
// Start minimuxer now that we have a file
start_minimuxer_threads(pairing_string!)
} catch {
displayError("Unable to read pairing file")
}
if (isSecuredURL) {
url.stopAccessingSecurityScopedResource()
}
controller.dismiss(animated: true, completion: nil)
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
}
func start_minimuxer_threads(_ pairing_file: String) {
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {
// enable minimuxer console logging only if enabled in settings
let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled)
let loggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
try minimuxer.startWithLogger(pairing_file, documentsDirectory, loggingEnabled)
} catch {
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName))
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR")")
}
start_auto_mounter(documentsDirectory)
// Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController
}
}
extension LaunchViewController
{
override func handleLaunchError(_ error: Error)
{
do
{
throw error
func fetchPairingFile() -> String? { PairingFileManager.shared.fetchPairingFile(presentingVC: self) }
func displayError(_ msg: String) {
print(msg)
let alert = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert)
self.present(alert, animated: true)
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
let url = urls[0]
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
defer {
if (isSecuredURL) {
url.stopAccessingSecurityScopedResource()
}
}
catch let error as NSError
{
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
let errorDescription: String
if #available(iOS 14.5, *)
{
let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }
errorDescription = errorMessages.joined(separator: "\n\n")
do {
let data = try Data(contentsOf: url)
guard let pairingString = String(data: data, encoding: .utf8) else {
displayError("Unable to read pairing file")
return
}
else
{
errorDescription = error.debugDescription
}
let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
self.handleLaunchConditions()
}))
self.present(alertController, animated: true, completion: nil)
try pairingString.write(to: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName), atomically: true, encoding: .utf8)
start_minimuxer_threads(pairingString)
} catch {
displayError("Unable to read pairing file")
}
controller.dismiss(animated: true, completion: nil)
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.")
}
func importAccountAtFile(_ file: URL, remove: Bool = false) {
_ = file.startAccessingSecurityScopedResource()
defer { file.stopAccessingSecurityScopedResource() }
guard let accountD = try? Data(contentsOf: file) else {
let toastView = ToastView(text: NSLocalizedString("Could not read data from file!", comment: ""), detailText: "\(file)")
return toastView.show(in: self)
}
guard let account = try? Foundation.JSONDecoder().decode(ImportedAccount.self, from: accountD) else {
let toastView = ToastView(text: NSLocalizedString("Could not parse data from file!", comment: ""), detailText: "\(file)")
return toastView.show(in: self)
}
print("We want to import this account probably: \(account)")
if remove {
try? FileManager.default.removeItem(at: file)
}
Keychain.shared.appleIDEmailAddress = account.email
Keychain.shared.appleIDPassword = account.password
Keychain.shared.adiPb = account.adiPB
Keychain.shared.identifier = account.local_user
if let altCert = ALTCertificate(p12Data: account.cert, password: account.certpass) {
Keychain.shared.signingCertificate = altCert.encryptedP12Data(withPassword: "")!
Keychain.shared.signingCertificatePassword = account.certpass
let toastView = ToastView(text: NSLocalizedString("Successfully imported '\(account.email)'!", comment: ""), detailText: "SideStore should be fully operational!")
return toastView.show(in: self)
} else {
let toastView = ToastView(text: NSLocalizedString("Failed to import account certificate!", comment: ""), detailText: "Failed to create ALTCertificate. Check if the password is correct. Still imported account/adi.pb details!")
return toastView.show(in: self)
}
}
override func finishLaunching()
{
super.finishLaunching()
guard !self.didFinishLaunching else { return }
func detectAndImportAccountFile() {
let accountFileURL = FileManager.default.documentsDirectory.appendingPathComponent("Account.sideconf")
#if !DEBUG
importAccountAtFile(accountFileURL, remove: true)
#else
importAccountAtFile(accountFileURL)
#endif
}
}
extension LaunchViewController {
@MainActor
func handleLaunchError(_ error: Error, retryCallback: (() async -> Void)? = nil) {
do { throw error } catch let error as NSError {
let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "")
let desc: String
if #available(iOS 14.5, *) {
desc = ([error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }).joined(separator: "\n\n")
} else {
desc = error.debugDescription
}
let alert = UIAlertController(title: title, message: desc, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default) { _ in
Task { await retryCallback?() }
})
present(alert, animated: true)
}
}
@MainActor
func finishLaunching() async {
guard !didFinishLaunching else { return }
didFinishLaunching = true
AppManager.shared.update()
AppManager.shared.updatePatronsIfNeeded()
PatreonAPI.shared.refreshPatreonAccount()
AppManager.shared.updateAllSources { result in
guard case .failure(let error) = result else { return }
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)")
let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError)
print("Failed to update sources on launch. \(errorDesc)")
@@ -312,63 +210,64 @@ extension LaunchViewController
if String(describing: error).contains("The Internet connection appears to be offline"){
mode = .localizedDescription // dont make noise!
}
let toastView = ToastView(error: error, mode: mode)
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController)
toastView.show(in: self.destinationViewController!.selectedViewController ?? self.destinationViewController!)
}
self.updateKnownSources()
// Ask widgets to be refreshed
updateKnownSources()
WidgetCenter.shared.reloadAllTimelines()
didFinishLaunching = true
// Add view controller as child (rather than presenting modally)
// so tint adjustment + card presentations works correctly.
self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
self.destinationViewController.view.alpha = 0.0
self.addChild(self.destinationViewController)
self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero)
self.destinationViewController.didMove(toParent: self)
let destinationVC = destinationViewController!
UIView.animate(withDuration: 0.2) {
self.destinationViewController.view.alpha = 1.0
}
let elapsed = abs(startTime.timeIntervalSinceNow)
let remaining = elapsed >= 1 ? 0 : 1 - elapsed
try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
self.didFinishLaunching = true
}
}
destinationVC.loadViewIfNeeded()
addChild(destinationVC)
destinationVC.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(destinationVC.view)
destinationVC.didMove(toParent: self)
// Pin edges BEFORE animation
NSLayoutConstraint.activate([
destinationVC.view.topAnchor.constraint(equalTo: view.topAnchor),
destinationVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
destinationVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
destinationVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
private extension LaunchViewController
{
func updateKnownSources()
{
// Set initial alpha for fade-in
destinationVC.view.alpha = 0
UIView.transition(with: view, duration: 0.3, options: .transitionCrossDissolve) { [self] in
self.splashView.alpha = 0
destinationVC.view.alpha = 1
} completion: { _ in
self.splashView.removeFromSuperview()
self.destinationViewController = destinationVC
}
}
func updateKnownSources() {
AppManager.shared.updateKnownSources { result in
switch result
{
switch result {
case .failure(let error): print("[ALTLog] Failed to update known sources:", error)
case .success((_, let blockedSources)):
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
let blockedSourceIDs = Set(blockedSources.lazy.map { $0.identifier })
let blockedSourceURLs = Set(blockedSources.lazy.compactMap { $0.sourceURL })
let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@",
#keyPath(Source.identifier), blockedSourceIDs,
#keyPath(Source.sourceURL), blockedSourceURLs)
let sourceErrors = Source.all(satisfying: predicate, in: context).map { (source) in
let blockedSource = blockedSources.first { $0.identifier == source.identifier }
return SourceError.blocked(source, bundleIDs: blockedSource?.bundleIDs, existingSource: source)
let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@", #keyPath(Source.identifier), blockedSourceIDs, #keyPath(Source.sourceURL), blockedSourceURLs)
let sourceErrors = Source.all(satisfying: predicate, in: context).map { source in
let blocked = blockedSources.first { $0.identifier == source.identifier }
return SourceError.blocked(source, bundleIDs: blocked?.bundleIDs, existingSource: source)
}
guard !sourceErrors.isEmpty else { return }
Task {
for error in sourceErrors
{
for error in sourceErrors {
let title = String(format: NSLocalizedString("“%@” Blocked", comment: ""), error.$source.name)
let message = [error.localizedDescription, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
await self.presentAlert(title: title, message: message)
}
}
@@ -377,3 +276,142 @@ private extension LaunchViewController
}
}
}
// MARK: - SplashView
final class SplashView: UIView {
let iconView = UIImageView()
let titleLabel = UILabel()
init(frame: CGRect, appName: String) {
super.init(frame: frame)
backgroundColor = .systemBackground
setupIcon()
setupTitle(appName: appName)
}
required init?(coder: NSCoder) { fatalError() }
private func setupIcon() {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
container.layer.shadowColor = UIColor.black.cgColor
container.layer.shadowOpacity = 0.25
container.layer.shadowOffset = CGSize(width: 0, height: 4)
container.layer.shadowRadius = 8
addSubview(container)
iconView.image = UIImage(named: "AppIcon") ?? UIImage(named: "AppIcon60x60") ?? UIImage(systemName: "app.fill")
iconView.contentMode = .scaleAspectFit
iconView.translatesAutoresizingMaskIntoConstraints = false
iconView.layer.cornerRadius = 24
iconView.clipsToBounds = true
container.addSubview(iconView)
NSLayoutConstraint.activate([
container.centerXAnchor.constraint(equalTo: centerXAnchor),
container.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20),
container.widthAnchor.constraint(equalToConstant: 120),
container.heightAnchor.constraint(equalToConstant: 120),
iconView.topAnchor.constraint(equalTo: container.topAnchor),
iconView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
iconView.trailingAnchor.constraint(equalTo: container.trailingAnchor)
])
}
private func setupTitle(appName: String) {
titleLabel.text = appName
titleLabel.font = .systemFont(ofSize: 24, weight: .bold)
titleLabel.textColor = .label
titleLabel.textAlignment = .center
titleLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 12),
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor)
])
}
}
// MARK: - PairingFileManager
final class PairingFileManager {
static let shared = PairingFileManager()
func fetchPairingFile(presentingVC: UIViewController) -> String? {
let fm = FileManager.default
let filename = pairingFileName
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
if fm.fileExists(atPath: documentsPath.path),
let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
return contents
}
if let url = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"),
fm.fileExists(atPath: url.path),
let data = fm.contents(atPath: url.path),
let contents = String(data: data, encoding: .utf8),
!contents.isEmpty, !UserDefaults.standard.isPairingReset { return contents }
if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String,
!plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset { return plistString }
presentPairingFileAlert(on: presentingVC)
return nil
}
private func presentPairingFileAlert(on vc: UIViewController) {
let alert = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Help", style: .default) { _ in
if let url = URL(string: "https://docs.sidestore.io/docs/installation/pairing-file") { UIApplication.shared.open(url) }
sleep(2); exit(0)
})
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
var types = UTType.types(tag: "plist", tagClass: .filenameExtension, conformingTo: nil)
types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: .filenameExtension, conformingTo: .data))
types.append(.xml)
let picker = UIDocumentPickerViewController(forOpeningContentTypes: types)
picker.delegate = vc as? UIDocumentPickerDelegate
picker.shouldShowFileExtensions = true
vc.present(picker, animated: true)
UserDefaults.standard.isPairingReset = false
})
vc.present(alert, animated: true)
}
}
// MARK: - SideJITManager
final class SideJITManager {
static let shared = SideJITManager()
func checkAndPromptIfNeeded(presentingVC: UIViewController) {
guard #available(iOS 17, *), !UserDefaults.standard.sidejitenable else { return }
DispatchQueue.global().async {
self.isSideJITServerDetected { result in
DispatchQueue.main.async {
switch result {
case .success():
let alert = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in UserDefaults.standard.sidejitenable = true })
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
presentingVC.present(alert, animated: true)
case .failure(_): print("Cannot find sideJITServer")
}
}
}
}
}
func askForNetwork() {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address
URLSession.shared.dataTask(with: URL(string: "\(SJSURL)/re/")!) { data, resp, err in
print("data: \(String(describing: data)), response: \(String(describing: resp)), error: \(String(describing: err))")
}.resume()
}
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
let address = UserDefaults.standard.textInputSideJITServerurl ?? ""
let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address
guard let url = URL(string: SJSURL) else { return }
URLSession.shared.dataTask(with: url) { _, _, error in
if let error = error { completion(.failure(error)); return }
completion(.success(()))
}.resume()
}
}

View File

@@ -1450,6 +1450,8 @@
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
<textAttributes key="titleTextAttributes">
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textAttributes>
@@ -1923,6 +1925,7 @@ Settings by i cons from the Noun Project</string>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="dI0-sh-yGf">
<rect key="frame" x="0.0" y="16" width="402" height="54"/>
<autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>

View File

@@ -202,6 +202,16 @@ final class SettingsViewController: UITableViewController
{
super.viewDidLoad()
// --- iOS 26 fix ---
if #available(iOS 26.0, *) {
let appearance = UINavigationBarAppearance()
// appearance.configureWithOpaqueBackground() // or .defaultBackground if you want blur
// appearance.backgroundColor = UIColor(named: "SettingsBackground")
appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
navigationController?.navigationBar.standardAppearance = appearance
navigationController?.navigationBar.scrollEdgeAppearance = appearance // required for iOS 26, maybe enforce it in storyboard?
}
let nib = UINib(nibName: "SettingsHeaderFooterView", bundle: nil)
self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView
@@ -251,28 +261,34 @@ final class SettingsViewController: UITableViewController
}
func importAccountAtFile(_ file: URL, remove: Bool = false) {
if let accountData = try? Data(contentsOf: file),
let account = try? Foundation.JSONDecoder().decode(ImportedAccount.self, from: accountData) {
print("We want to import this account probably: \(account)")
if remove {
try? FileManager.default.removeItem(at: file)
}
Keychain.shared.appleIDEmailAddress = account.email
Keychain.shared.appleIDPassword = account.password
Keychain.shared.adiPb = account.adiPB
Keychain.shared.adiSerial = account.serial
Keychain.shared.identifier = account.local_user
signIn()
update()
if let altCert = ALTCertificate(p12Data: account.cert, password: account.certpass) {
Keychain.shared.signingCertificate = altCert.encryptedP12Data(withPassword: "")!
Keychain.shared.signingCertificatePassword = account.certpass
let toastView = ToastView(text: NSLocalizedString("Successfully imported '\(account.email)'!", comment: ""), detailText: "SideStore should be fully operational!")
return toastView.show(in: self)
} else {
let toastView = ToastView(text: NSLocalizedString("Failed to import account certificate!", comment: ""), detailText: "Failed to create ALTCertificate. Check if the password is correct. Still imported account/adi.pb details!")
return toastView.show(in: self)
}
_ = file.startAccessingSecurityScopedResource()
defer { file.stopAccessingSecurityScopedResource() }
guard let accountD = try? Data(contentsOf: file) else {
let toastView = ToastView(text: NSLocalizedString("Could not read data from file!", comment: ""), detailText: "\(file)")
return toastView.show(in: self)
}
guard let account = try? Foundation.JSONDecoder().decode(ImportedAccount.self, from: accountD) else {
let toastView = ToastView(text: NSLocalizedString("Could not parse data from file!", comment: ""), detailText: "\(file)")
return toastView.show(in: self)
}
print("We want to import this account probably: \(account)")
if remove {
try? FileManager.default.removeItem(at: file)
}
Keychain.shared.appleIDEmailAddress = account.email
Keychain.shared.appleIDPassword = account.password
Keychain.shared.adiPb = account.adiPB
Keychain.shared.identifier = account.local_user
signIn()
update()
if let altCert = ALTCertificate(p12Data: account.cert, password: account.certpass) {
Keychain.shared.signingCertificate = altCert.encryptedP12Data(withPassword: "")!
Keychain.shared.signingCertificatePassword = account.certpass
let toastView = ToastView(text: NSLocalizedString("Successfully imported '\(account.email)'!", comment: ""), detailText: "SideStore should be fully operational!")
return toastView.show(in: self)
} else {
let toastView = ToastView(text: NSLocalizedString("Failed to import account certificate!", comment: ""), detailText: "Failed to create ALTCertificate. Check if the password is correct. Still imported account/adi.pb details!")
return toastView.show(in: self)
}
}
@@ -290,7 +306,6 @@ final class SettingsViewController: UITableViewController
let password = Keychain.shared.appleIDPassword,
let cert = Keychain.shared.signingCertificate,
let identifier = Keychain.shared.identifier,
let adiSerial = Keychain.shared.adiSerial,
let adiPB = Keychain.shared.adiPb else {
#if DEBUG
print(Keychain.shared.appleIDEmailAddress ?? "Empty email")
@@ -298,17 +313,16 @@ final class SettingsViewController: UITableViewController
print(Keychain.shared.signingCertificate ?? "Empty cert")
print(Keychain.shared.identifier ?? "Empty identifier")
print(Keychain.shared.adiPb ?? "Empty adiPb")
print(Keychain.shared.adiSerial ?? "Empty adiSerial")
#endif
return nil
}
return ImportedAccount(email: email, password: password, cert: cert, certpass: certpass, local_user: identifier, serial: adiSerial, adiPB: adiPB)
return ImportedAccount(email: email, password: password, cert: cert, certpass: certpass, local_user: identifier, adiPB: adiPB)
}
func showExportAccount() {
Task {
let password = await withUnsafeContinuation { (c: UnsafeContinuation<String?,Never>) in
guard let password = await withUnsafeContinuation({ (c: UnsafeContinuation<String?,Never>) in
let alertController = UIAlertController(title: NSLocalizedString("Please enter the password for the certificate.", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
@@ -328,19 +342,16 @@ final class SettingsViewController: UITableViewController
})
self.present(alertController, animated: true)
}
guard let password else {
}) else {
return
}
guard let account = exportAccount(password) else {
let toastView = ToastView(text: NSLocalizedString("Failed to export account!", comment: ""), detailText: "Account not found.")
toastView.show(in: self)
return
return toastView.show(in: self)
}
guard let accountData = try? Foundation.JSONEncoder().encode(account).base64EncodedData() else {
guard let accountData = try? Foundation.JSONEncoder().encode(account) else {
let toastView = ToastView(text: NSLocalizedString("Failed to export account data!", comment: ""), detailText: "Account malformed.")
toastView.show(in: self)
return
@@ -1354,15 +1365,25 @@ extension SettingsViewController
guard let confUrl else {
return
}
_ = confUrl.startAccessingSecurityScopedResource()
defer { confUrl.stopAccessingSecurityScopedResource() }
importAccountAtFile(confUrl)
}
case .importCert:
let importVc = UIDocumentPickerViewController(forOpeningContentTypes: [UTType(filenameExtension: "p12")!], asCopy: false)
ImportExport.documentPickerHandler = DocumentPickerHandler { url in
guard let url else {
return
}
importVc.delegate = ImportExport.documentPickerHandler
self.present(importVc, animated: true)
_ = url.startAccessingSecurityScopedResource()
defer { url.stopAccessingSecurityScopedResource() }
}
Task {
let certUrl = await withUnsafeContinuation { c in
let importVc = UIDocumentPickerViewController(forOpeningContentTypes: [UTType(filenameExtension: "p12")!], asCopy: false)
ImportExport.documentPickerHandler = DocumentPickerHandler { url in
_ = url?.startAccessingSecurityScopedResource()
defer { url?.stopAccessingSecurityScopedResource() }
c.resume(returning: url)
}
importVc.delegate = ImportExport.documentPickerHandler

View File

@@ -20,6 +20,7 @@
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="xWh-1U-u0q" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="59" width="393" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
@@ -248,6 +249,7 @@
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="HLe-3g-P8I" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="393" height="108"/>
<autoresizingMask key="autoresizingMask"/>
<edgeInsets key="layoutMargins" top="8" left="20" bottom="8" right="8"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>

View File

@@ -46,6 +46,14 @@ final class SourcesViewController: UICollectionViewController
{
super.viewDidLoad()
// Ensure large titles
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .automatic
// Set title
navigationItem.title = "Sources"
navigationController?.navigationBar.layoutMargins.left = 20
let layout = self.makeLayout()
self.collectionView.collectionViewLayout = layout

View File

@@ -14,6 +14,5 @@ struct ImportedAccount: Codable {
let cert: Data
let certpass: String
let local_user: String
let serial: String
let adiPB: String
}

View File

@@ -80,9 +80,6 @@ public class Keychain
@KeychainItem(key: "identifier")
public var identifier: String?
@KeychainItem(key: "adiSerial")
public var adiSerial: String?
@KeychainItem(key: "adiPb")
public var adiPb: String?

View File

@@ -201,7 +201,7 @@ build-and-test:
@echo ""
@echo "Performing a build and running tests..."
@xcodebuild test \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \
-resultBundlePath build/tests/test-results.xcresult \
-enableCodeCoverage YES \
$(COMMON_BUILD_SETTINGS)
@@ -213,7 +213,7 @@ build-tests:
@echo "Performing a build-for-testing..."
@xcodebuild build-for-testing \
-enableCodeCoverage YES \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \
$(COMMON_BUILD_SETTINGS)
run-tests:
@@ -224,22 +224,22 @@ run-tests:
@xcodebuild test-without-building \
-enableCodeCoverage YES \
-resultBundlePath build/tests/test-results.xcresult \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \
$(COMMON_BUILD_SETTINGS)
boot-sim-async:
@if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \
echo "Simulator 'iPhone 16 Pro' is already booted."; \
@if xcrun simctl list devices "iPhone 17 Pro" | grep -q "Booted"; then \
echo "Simulator 'iPhone 17 Pro' is already booted."; \
else \
echo "Booting simulator 'iPhone 16 Pro' asynchronously..."; \
xcrun simctl boot "iPhone 16 Pro" & \
echo "Booting simulator 'iPhone 17 Pro' asynchronously..."; \
xcrun simctl boot "iPhone 17 Pro" & \
echo "Simulator boot command dispatched."; \
fi
sim-boot-check:
@echo "Checking simulator boot status..."
@if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \
echo "Simulator 'iPhone 16 Pro' is booted."; \
@if xcrun simctl list devices "iPhone 17 Pro" | grep -q "Booted"; then \
echo "Simulator 'iPhone 17 Pro' is booted."; \
else \
echo "Simulator bootup failed or is not booted yet."; \
exit 1; \

View File

@@ -19,7 +19,41 @@ final class UITests: XCTestCase {
private static let APP_NAME = "SideStore"
func printAllMethods(of className: String) {
guard let cls: AnyClass = objc_getClass(className) as? AnyClass else {
print("Class \(className) not found")
return
}
var methodCount: UInt32 = 0
if let methodList = class_copyMethodList(cls, &methodCount) {
for i in 0..<Int(methodCount) {
let method = methodList[i]
let sel = method_getName(method)
print(String(describing: sel))
}
free(methodList)
}
}
override func setUpWithError() throws {
// ensure the swizzle only happens once
if !Self.mockIdlingPrivateApiToNoOp {
let original = class_getInstanceMethod(
objc_getClass("XCUIApplicationProcess") as? AnyClass,
// this is the new method signature obtained via reflection
Selector(("waitForQuiescenceIncludingAnimationsIdle:isPreEvent:"))
)
let replaced = class_getInstanceMethod(type(of: self), #selector(Self.replace))
if let original, let replaced{
method_exchangeImplementations(original, replaced)
}
Self.mockIdlingPrivateApiToNoOp = true
}
/* UNCOMMENT below to enable the printing of private members of XCUIApplicationProcess */
// printAllMethods(of: "XCUIApplicationProcess")
// Put setup code here. This method is called before the invocation of each test method in the class.
// Self.dismissSpotlight()
// Self.deleteMyApp()
@@ -47,7 +81,9 @@ final class UITests: XCTestCase {
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
let allowButton = systemAlert.scrollViews.otherElements.buttons["Allow"]
_ = allowButton.exists || allowButton.waitForExistence(timeout: 0.5)
allowButton.tap()
// Do the actual validation
try performBulkAddingRecommendedSources(for: app)
@@ -55,6 +91,7 @@ final class UITests: XCTestCase {
func testBulkAddInputSources() throws {
// let app = XCUIApplication()
let app = XCUIApplication()
app.launch()
@@ -62,8 +99,10 @@ final class UITests: XCTestCase {
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
let allowButton = systemAlert.scrollViews.otherElements.buttons["Allow"]
_ = allowButton.exists || allowButton.waitForExistence(timeout: 0.5)
allowButton.tap()
// Do the actual validation
try performBulkAddingInputSources(for: app)
}
@@ -77,8 +116,10 @@ final class UITests: XCTestCase {
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
let allowButton = systemAlert.scrollViews.otherElements.buttons["Allow"]
_ = allowButton.exists || allowButton.waitForExistence(timeout: 0.5)
allowButton.tap()
// Do the actual validation
try performRepeatabilityForStagingInputSources(for: app)
}
@@ -92,8 +133,10 @@ final class UITests: XCTestCase {
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
let allowButton = systemAlert.scrollViews.otherElements.buttons["Allow"]
_ = allowButton.exists || allowButton.waitForExistence(timeout: 0.5)
allowButton.tap()
// Do the actual validation
try performRepeatabilityForStagingRecommendedSources(for: app)
}
@@ -157,6 +200,13 @@ private extension UITests {
springboard_app.tap()
}
@objc func replace() {
return
}
static var mockIdlingPrivateApiToNoOp = false
class func deleteMyApp2() {
XCUIApplication().terminate()
dismissSpringboardAlerts()
@@ -171,16 +221,19 @@ private extension UITests {
let button = springboard_app.buttons["Remove App"]
_ = button.exists || button.waitForExistence(timeout: 5)
button.tap()
_ = springboard_app.waitForExistence(timeout: 0.5)
}
do {
let button = springboard_app.buttons["Delete App"]
_ = button.waitForExistence(timeout: 0.3)
_ = button.waitForExistence(timeout: 0.5)
button.tap()
_ = springboard_app.waitForExistence(timeout: 0.5)
}
do {
let button = springboard_app.buttons["Delete"]
_ = button.waitForExistence(timeout: 0.3)
_ = button.waitForExistence(timeout: 0.5)
button.tap()
_ = springboard_app.waitForExistence(timeout: 0.5)
}
// // Press home once to make the icons stop wiggling
@@ -230,20 +283,50 @@ private extension UITests {
try tapAddForThesePickedSources(app: app, sourceMappings: sourceMappings, cellsQuery: cellsQuery)
// Commit the changes by tapping "Done".
app.navigationBars["Add Source"].buttons["Done"].tap()
let doneButton = app.navigationBars["Add Source"].buttons["Done"]
_ = doneButton.waitForExistence(timeout: 0.5)
doneButton.tap()
// Accept each source addition via alert.
for source in sourceMappings {
let alertIdentifier = "Would you like to add the source “\(source.alertTitle)”?"
let addSourceButton = app.alerts[alertIdentifier]
.scrollViews.otherElements.buttons["Add Source"]
_ = addSourceButton.exists || addSourceButton.waitForExistence(timeout: 0.3)
_ = addSourceButton.waitForExistence(timeout: 0.5)
addSourceButton.tap()
}
}
private func performBulkAddingRecommendedSources(for app: XCUIApplication) throws {
// Navigate to the Sources screen and open the Add Source view.
let srcTab = app.tabBars["Tab Bar"].buttons["Sources"]
_ = srcTab.waitForExistence(timeout: 0.5)
srcTab.tap()
let srcAdd = app.navigationBars["Sources"].buttons["Add"]
_ = srcAdd.waitForExistence(timeout: 0.5)
srcAdd.tap()
let cellsQuery = app.collectionViews.cells
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false),
("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false),
("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false),
("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", true),
("UTM Repository\nVirtual machines for iOS", "UTM Repository", false),
("Flyinghead\nflyinghead.github.io/flycast-builds/altstore.json", "Flyinghead", false),
// ("PojavLauncher Repository\nalt.crystall1ne.dev", "PojavLauncher Repository", false), // not a stable source, sometimes becomes unreachable, so disabled
("PokeMMO\npokemmo.eu/altstore/", "PokeMMO", true),
("Odyssey\ntheodyssey.dev/altstore/odysseysource.json", "Odyssey", false),
("Yattee\nrepos.yattee.stream/alt/apps.json", "Yattee", false),
("ThatStella7922 Source\nThe home for all apps ThatStella7922", "ThatStella7922 Source", false)
]
try performBulkAdd(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery)
}
private func performBulkAddingInputSources(for app: XCUIApplication) throws {
// set content into clipboard (for bulk add (paste))
@@ -260,17 +343,23 @@ private extension UITests {
https://bit.ly/Quantumsource
""".trimmedIndentation
let app = XCUIApplication()
app.tabBars["Tab Bar"].buttons["Sources"].tap()
app.navigationBars["Sources"].buttons["Add"].tap()
let srcTab = app.tabBars["Tab Bar"].buttons["Sources"]
_ = srcTab.waitForExistence(timeout: 0.5)
srcTab.tap()
let srcAdd = app.navigationBars["Sources"].buttons["Add"]
_ = srcAdd.waitForExistence(timeout: 0.5)
srcAdd.tap()
let collectionViewsQuery = app.collectionViews
let appsSidestoreIoTextField = collectionViewsQuery.textFields["apps.sidestore.io"]
_ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5)
_ = appsSidestoreIoTextField.waitForExistence(timeout: 0.5)
appsSidestoreIoTextField.tap()
appsSidestoreIoTextField.tap()
collectionViewsQuery.staticTexts["Paste"].tap()
_ = appsSidestoreIoTextField.waitForExistence(timeout: 0.5)
let pasteButton = collectionViewsQuery.staticTexts["Paste"]
_ = pasteButton.waitForExistence(timeout: 0.5)
pasteButton.tap()
// if app.keyboards.buttons["Return"].exists {
// app.keyboards.buttons["Return"].tap()
// } else if app.keyboards.buttons["Done"].exists {
@@ -282,6 +371,7 @@ private extension UITests {
if app.keyboards.count > 0 {
appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed
_ = app.exists || app.waitForExistence(timeout: 0.5)
}
let cellsQuery = collectionViewsQuery.cells
@@ -302,7 +392,32 @@ private extension UITests {
try performBulkAdd(app: app, sourceMappings: textInputSources, cellsQuery: cellsQuery)
}
private func performRepeatabilityForStagingRecommendedSources(for app: XCUIApplication) throws {
// Navigate to the Sources screen and open the Add Source view.
let srcTab = app.tabBars["Tab Bar"].buttons["Sources"]
srcTab.tap()
_ = srcTab.waitForExistence(timeout: 0.5)
let srcAdd = app.navigationBars["Sources"].buttons["Add"]
srcAdd.tap()
_ = srcAdd.waitForExistence(timeout: 0.5)
let cellsQuery = app.collectionViews.cells
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false),
("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false),
("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false),
("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", false),
("UTM Repository\nVirtual machines for iOS", "UTM Repository", false),
]
let repeatCount = 3 // number of times to run the entire sequence
let timeSeed = UInt64(Date().timeIntervalSince1970) // time is unique (upto microseconds) - uncomment this to use non-deterministic seed based RNG (random number generator)
try repeatabilityTest(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery, repeatCount: repeatCount, seed: timeSeed)
}
private func performRepeatabilityForStagingInputSources(for app: XCUIApplication) throws {
// set content into clipboard (for bulk add (paste))
@@ -315,20 +430,28 @@ private extension UITests {
https://bit.ly/40Isul6
""".trimmedIndentation
let app = XCUIApplication()
app.tabBars["Tab Bar"].buttons["Sources"].tap()
app.navigationBars["Sources"].buttons["Add"].tap()
let srcTab = app.tabBars["Tab Bar"].buttons["Sources"]
_ = srcTab.waitForExistence(timeout: 0.5)
srcTab.tap()
let srcAdd = app.navigationBars["Sources"].buttons["Add"]
_ = srcAdd.waitForExistence(timeout: 0.5)
srcAdd.tap()
let collectionViewsQuery = app.collectionViews
let appsSidestoreIoTextField = collectionViewsQuery.textFields["apps.sidestore.io"]
_ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5)
_ = appsSidestoreIoTextField.waitForExistence(timeout: 0.5)
appsSidestoreIoTextField.tap()
appsSidestoreIoTextField.tap()
_ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5)
collectionViewsQuery.staticTexts["Paste"].tap()
_ = appsSidestoreIoTextField.waitForExistence(timeout: 0.5)
let pasteButton = collectionViewsQuery.staticTexts["Paste"]
_ = pasteButton.waitForExistence(timeout: 0.5)
pasteButton.tap()
if app.keyboards.count > 0 {
appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed
_ = app.exists || app.waitForExistence(timeout: 0.5)
}
let cellsQuery = collectionViewsQuery.cells
@@ -380,64 +503,27 @@ private extension UITests {
.containing(.button, identifier: source.identifier)
.children(matching: .button)[source.identifier]
XCTAssert(sourceButton.exists || sourceButton.waitForExistence(timeout: 10), "Source preview for id: '\(source.alertTitle)' not found in the view")
_ = sourceButton.exists || sourceButton.waitForExistence(timeout: 0.5)
// let addButton = sourceButton.children(matching: .button).firstMatch
// let addButton = sourceButton.descendants(matching: .button)["add"]
// XCTAssert(addButton.exists || addButton.waitForExistence(timeout: 0.5), " `+` button for id: '\(source.alertTitle)' not found in the preview container")
// addButton.tap()
let addButton = sourceButton.children(matching: .button)["add"]
XCTAssert(addButton.exists || addButton.waitForExistence(timeout: 0.3), " `+` button for id: '\(source.alertTitle)' not found in the preview container")
addButton.tap()
XCTAssert(addButton.waitForExistence(timeout: 1)) //TODO: fine tune down the value to make tests faster (but validate tests still works)
// addButton.tap()
let coord = addButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
coord.tap()
if source.requiresSwipe {
sourceButton.swipeUp(velocity: .slow) // Swipe up if needed.
_ = sourceButton.waitForExistence(timeout: 0.5)
}
}
}
private func performBulkAddingRecommendedSources(for app: XCUIApplication) throws {
// Navigate to the Sources screen and open the Add Source view.
app.tabBars["Tab Bar"].buttons["Sources"].tap()
app.navigationBars["Sources"].buttons["Add"].tap()
let cellsQuery = app.collectionViews.cells
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false),
("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false),
("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false),
("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", true),
("UTM Repository\nVirtual machines for iOS", "UTM Repository", false),
("Flyinghead\nflyinghead.github.io/flycast-builds/altstore.json", "Flyinghead", false),
// ("PojavLauncher Repository\nalt.crystall1ne.dev", "PojavLauncher Repository", false), // not a stable source, sometimes becomes unreachable, so disabled
("PokeMMO\npokemmo.eu/altstore/", "PokeMMO", true),
("Odyssey\ntheodyssey.dev/altstore/odysseysource.json", "Odyssey", false),
("Yattee\nrepos.yattee.stream/alt/apps.json", "Yattee", false),
("ThatStella7922 Source\nThe home for all apps ThatStella7922", "ThatStella7922 Source", false)
]
try performBulkAdd(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery)
}
private func performRepeatabilityForStagingRecommendedSources(for app: XCUIApplication) throws {
// Navigate to the Sources screen and open the Add Source view.
app.tabBars["Tab Bar"].buttons["Sources"].tap()
app.navigationBars["Sources"].buttons["Add"].tap()
let cellsQuery = app.collectionViews.cells
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false),
("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false),
("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false),
("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", false),
("UTM Repository\nVirtual machines for iOS", "UTM Repository", false),
]
let repeatCount = 3 // number of times to run the entire sequence
let timeSeed = UInt64(Date().timeIntervalSince1970) // time is unique (upto microseconds) - uncomment this to use non-deterministic seed based RNG (random number generator)
try repeatabilityTest(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery, repeatCount: repeatCount, seed: timeSeed)
}
}

View File

@@ -29,7 +29,7 @@
"sourceURL": "https://flyinghead.github.io/flycast-builds/altstore.json"
},
{
"identifier": "dev.crystall1ne.repos.PojavLauncher",
"identifier": "dev.crystall1ne.alt",
"sourceURL": "https://alt.crystall1ne.dev"
},
{
@@ -82,7 +82,7 @@
"sourceURL": "https://flyinghead.github.io/flycast-builds/altstore.json"
},
{
"identifier": "dev.crystall1ne.repos.PojavLauncher",
"identifier": "dev.crystall1ne.alt",
"sourceURL": "https://alt.crystall1ne.dev"
},
{