mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-11 15:53:30 +01:00
Merge remote-tracking branch 'origin/develop' into fabianthdev/feature/SwiftUI
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
<key>ALTPairingFile</key>
|
||||
<string><insert pairing file here></string>
|
||||
<key>ALTAnisetteURL</key>
|
||||
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
|
||||
<string>https://ani.sidestore.io</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
|
||||
@@ -15,6 +15,8 @@ import minimuxer
|
||||
import AltStoreCore
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
let pairingFileName = "ALTPairingFile.mobiledevicepairing"
|
||||
|
||||
final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate
|
||||
{
|
||||
private var didFinishLaunching = false
|
||||
@@ -83,7 +85,7 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
} else {
|
||||
// Show an alert explaining the pairing file
|
||||
// Create new Alert
|
||||
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://youtu.be/dQw4w9WgXcQ", preferredStyle: .alert)
|
||||
let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file for your device. For more information, go to https://wiki.sidestore.io/guides/install#pairing-process", preferredStyle: .alert)
|
||||
|
||||
// Create OK button with action handler
|
||||
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
|
||||
@@ -129,14 +131,11 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
}
|
||||
|
||||
// Save to a file for next launch
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
let fm = FileManager.default
|
||||
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
|
||||
try pairing_string?.write(to: documentsPath, atomically: true, encoding: String.Encoding.utf8)
|
||||
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")
|
||||
}
|
||||
@@ -152,22 +151,15 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
|
||||
}
|
||||
|
||||
func start_minimuxer_threads(_ pairing_file: String) {
|
||||
set_usbmuxd_socket()
|
||||
#if false // Retries
|
||||
var res = start_minimuxer(pairing_file: pairing_file)
|
||||
var attempts = 10
|
||||
while (attempts != 0 && res != 0) {
|
||||
print("start_minimuxer `res` != 0, retry #\(attempts)")
|
||||
res = start_minimuxer(pairing_file: pairing_file)
|
||||
attempts -= 1
|
||||
target_minimuxer_address()
|
||||
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
||||
do {
|
||||
try start(pairing_file, documentsDirectory)
|
||||
} catch {
|
||||
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
|
||||
displayError("minimuxer failed to start, please restart SideStore. \(minimuxerToOperationError(error).failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
|
||||
}
|
||||
#else
|
||||
let res = start_minimuxer(pairing_file: pairing_file)
|
||||
#endif
|
||||
if res != 0 {
|
||||
displayError("minimuxer failed to start. Incorrect arguments were passed.")
|
||||
}
|
||||
auto_mount_dev_image()
|
||||
start_auto_mounter(documentsDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -392,7 +392,8 @@ extension AppManager
|
||||
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
|
||||
{
|
||||
let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in
|
||||
print("Authenticated for fetching App IDs with result:", result)
|
||||
// result contains name, email, auth token, OTP and other possibly personal/account specific info. we don't want this logged
|
||||
//print("Authenticated for fetching App IDs with result:", result)
|
||||
}
|
||||
|
||||
let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context)
|
||||
@@ -873,7 +874,11 @@ private extension AppManager
|
||||
// Check if backup app is installed in place of real app.
|
||||
let uti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
||||
|
||||
if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
|
||||
// for some reason, `app.certificateSerialNumber != group.context.certificate?.serialNumber` is true on first SideStore refresh
|
||||
// in most cases, the first refresh gets stuck since it is a full reinstall, and to fix it you must exit to home screen
|
||||
// which finishes it but removes all app data
|
||||
// so we want to ensure we don't reinstall for SideStore if it's true (it will still reinstall if needsResign is true)
|
||||
if (app.certificateSerialNumber != group.context.certificate?.serialNumber && app.bundleIdentifier != StoreApp.altstoreAppID) ||
|
||||
uti != nil ||
|
||||
app.needsResign
|
||||
{
|
||||
|
||||
@@ -44,14 +44,9 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
for profile in allIdentifiers {
|
||||
do {
|
||||
let res = try remove_provisioning_profile(id: profile)
|
||||
if case Uhoh.Bad(let code) = res {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
try remove_provisioning_profile(profile)
|
||||
} catch {
|
||||
self.finish(.failure(ALTServerError(.unknownResponse)))
|
||||
return self.finish(.failure(minimuxerToOperationError(error)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,23 +45,13 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
|
||||
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
let v = minimuxer_to_operation(code: 1)
|
||||
|
||||
do {
|
||||
var x = try debug_app(app_id: installedApp.resignedBundleIdentifier)
|
||||
switch x {
|
||||
case .Good:
|
||||
self.finish(.success(()))
|
||||
case .Bad(let code):
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
try debug_app(installedApp.resignedBundleIdentifier)
|
||||
} catch {
|
||||
self.finish(.failure(OperationError.unknown))
|
||||
return self.finish(.failure(minimuxerToOperationError(error)))
|
||||
}
|
||||
|
||||
|
||||
self.finish(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,6 +384,16 @@ extension FetchProvisioningProfilesOperation
|
||||
|
||||
if app.isAltStoreApp
|
||||
{
|
||||
print("Application groups before modifying for SideStore: \(applicationGroups)")
|
||||
|
||||
// Remove app groups that contain AltStore since they can be problematic (cause SideStore to expire early)
|
||||
for (index, group) in applicationGroups.enumerated() {
|
||||
if group.contains("AltStore") {
|
||||
print("Removing application group: \(group)")
|
||||
applicationGroups.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
// Potentially updating app groups for this specific AltStore.
|
||||
// Find the (unique) AltStore app group, then replace it
|
||||
// with the correct "base" app group ID.
|
||||
@@ -397,6 +407,7 @@ extension FetchProvisioningProfilesOperation
|
||||
applicationGroups.append(Bundle.baseAltStoreAppGroupID)
|
||||
}
|
||||
}
|
||||
print("Application groups: \(applicationGroups)")
|
||||
|
||||
// Dispatch onto global queue to prevent appGroupsLock deadlock.
|
||||
DispatchQueue.global().async {
|
||||
@@ -478,10 +489,13 @@ extension FetchProvisioningProfilesOperation
|
||||
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
|
||||
switch Result(success, error)
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success:
|
||||
case .failure:
|
||||
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
|
||||
// So instead, we just return the fetched profile from above.
|
||||
completionHandler(.success(profile))
|
||||
|
||||
// Fetch new provisiong profile
|
||||
case .success:
|
||||
// Fetch new provisioning profile
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import Network
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
import minimuxer
|
||||
|
||||
@objc(InstallAppOperation)
|
||||
final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
@@ -148,17 +149,14 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
})
|
||||
}
|
||||
|
||||
let ns_bundle = NSString(string: installedApp.bundleIdentifier)
|
||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
||||
|
||||
let res = minimuxer_install_ipa(ns_bundle_ptr)
|
||||
if res == 0 {
|
||||
installedApp.refreshedDate = Date()
|
||||
self.finish(.success(installedApp))
|
||||
|
||||
} else {
|
||||
self.finish(.failure(minimuxer_to_operation(code: res)))
|
||||
do {
|
||||
try install_ipa(installedApp.bundleIdentifier)
|
||||
} catch {
|
||||
return self.finish(.failure(minimuxerToOperationError(error)))
|
||||
}
|
||||
|
||||
installedApp.refreshedDate = Date()
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,10 +172,11 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
print("Removed refreshed IPA")
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove refreshed .ipa:", error)
|
||||
print("Failed to remove refreshed .ipa: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import AltSign
|
||||
import minimuxer
|
||||
|
||||
enum OperationError: LocalizedError
|
||||
{
|
||||
@@ -42,9 +43,11 @@ enum OperationError: LocalizedError
|
||||
case uninstall
|
||||
case lookupApps
|
||||
case detach
|
||||
case attach
|
||||
case functionArguments
|
||||
case profileInstall
|
||||
case profileManage
|
||||
case noConnection
|
||||
case invalidPairingFile
|
||||
|
||||
var failureReason: String? {
|
||||
switch self {
|
||||
@@ -70,9 +73,11 @@ enum OperationError: LocalizedError
|
||||
case .uninstall: return NSLocalizedString("Unable to uninstall the app", comment: "")
|
||||
case .lookupApps: return NSLocalizedString("Unable to fetch apps from the device", comment: "")
|
||||
case .detach: return NSLocalizedString("Unable to detach from the app's process", comment: "")
|
||||
case .attach: return NSLocalizedString("Unable to attach to the app's process", comment: "")
|
||||
case .functionArguments: return NSLocalizedString("A function was passed invalid arguments", comment: "")
|
||||
case .profileInstall: return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
case .profileManage: return NSLocalizedString("Unable to manage profiles on the device", comment: "")
|
||||
case .noConnection: return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi", comment: "")
|
||||
case .invalidPairingFile: return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,49 +123,50 @@ enum OperationError: LocalizedError
|
||||
}
|
||||
}
|
||||
|
||||
func minimuxer_to_operation(code: Int32) -> OperationError {
|
||||
switch code {
|
||||
case -1:
|
||||
/// crashes if error is not a MinimuxerError
|
||||
func minimuxerToOperationError(_ error: Error) -> OperationError {
|
||||
switch error as! MinimuxerError {
|
||||
case .NoDevice:
|
||||
return OperationError.noDevice
|
||||
case -2:
|
||||
return OperationError.createService(name: "debug")
|
||||
case -3:
|
||||
return OperationError.createService(name: "instproxy")
|
||||
case -4:
|
||||
return OperationError.getFromDevice(name: "installed apps")
|
||||
case -5:
|
||||
return OperationError.getFromDevice(name: "path to the app")
|
||||
case -6:
|
||||
return OperationError.getFromDevice(name: "bundle path")
|
||||
case -7:
|
||||
return OperationError.setArgument(name: "max packet")
|
||||
case -8:
|
||||
return OperationError.setArgument(name: "working directory")
|
||||
case -9:
|
||||
return OperationError.setArgument(name: "argv")
|
||||
case -10:
|
||||
return OperationError.getFromDevice(name: "launch success")
|
||||
case -11:
|
||||
return OperationError.detach
|
||||
case -12:
|
||||
return OperationError.functionArguments
|
||||
case -13:
|
||||
return OperationError.createService(name: "AFC")
|
||||
case -14:
|
||||
return OperationError.afc
|
||||
case -15:
|
||||
return OperationError.install
|
||||
case -16:
|
||||
return OperationError.uninstall
|
||||
case -17:
|
||||
return OperationError.createService(name: "misagent")
|
||||
case -18:
|
||||
return OperationError.profileInstall
|
||||
case -19:
|
||||
return OperationError.profileInstall
|
||||
case -20:
|
||||
case .NoConnection:
|
||||
return OperationError.noConnection
|
||||
default:
|
||||
return OperationError.unknown
|
||||
case .PairingFile:
|
||||
return OperationError.invalidPairingFile
|
||||
case .CreateDebug:
|
||||
return OperationError.createService(name: "debug")
|
||||
case .CreateInstproxy:
|
||||
return OperationError.createService(name: "instproxy")
|
||||
case .LookupApps:
|
||||
return OperationError.getFromDevice(name: "installed apps")
|
||||
case .FindApp:
|
||||
return OperationError.getFromDevice(name: "path to the app")
|
||||
case .BundlePath:
|
||||
return OperationError.getFromDevice(name: "bundle path")
|
||||
case .MaxPacket:
|
||||
return OperationError.setArgument(name: "max packet")
|
||||
case .WorkingDirectory:
|
||||
return OperationError.setArgument(name: "working directory")
|
||||
case .Argv:
|
||||
return OperationError.setArgument(name: "argv")
|
||||
case .LaunchSuccess:
|
||||
return OperationError.getFromDevice(name: "launch success")
|
||||
case .Detach:
|
||||
return OperationError.detach
|
||||
case .Attach:
|
||||
return OperationError.attach
|
||||
case .CreateAfc:
|
||||
return OperationError.createService(name: "AFC")
|
||||
case .RwAfc:
|
||||
return OperationError.afc
|
||||
case .InstallApp:
|
||||
return OperationError.install
|
||||
case .UninstallApp:
|
||||
return OperationError.uninstall
|
||||
case .CreateMisagent:
|
||||
return OperationError.createService(name: "misagent")
|
||||
case .ProfileInstall:
|
||||
return OperationError.profileManage
|
||||
case .ProfileRemove:
|
||||
return OperationError.profileManage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,15 +49,12 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
for p in profiles {
|
||||
do {
|
||||
let x = try install_provisioning_profile(plist: p.value.data)
|
||||
if case .Bad(let code) = x {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
let bytes = p.value.data.toRustByteSlice()
|
||||
try install_provisioning_profile(bytes.forRust())
|
||||
} catch {
|
||||
self.finish(.failure(OperationError.unknown))
|
||||
return self.finish(.failure(minimuxerToOperationError(error)))
|
||||
}
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
|
||||
|
||||
@@ -39,15 +39,11 @@ final class RemoveAppOperation: ResultOperation<InstalledApp>
|
||||
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
|
||||
|
||||
do {
|
||||
let res = try remove_app(app_id: resignedBundleIdentifier)
|
||||
if case Uhoh.Bad(let code) = res {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
}
|
||||
} catch Uhoh.Bad(let code) {
|
||||
self.finish(.failure(minimuxer_to_operation(code: code)))
|
||||
try remove_app(resignedBundleIdentifier)
|
||||
} catch {
|
||||
self.finish(.failure(ALTServerError(.appDeletionFailed)))
|
||||
return self.finish(.failure(minimuxerToOperationError(error)))
|
||||
}
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
||||
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
||||
print("Successfully resigned app to \(destinationURL.absoluteString)")
|
||||
|
||||
// Use appBundleURL since we need an app bundle, not .ipa.
|
||||
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||
|
||||
@@ -9,6 +9,7 @@ import Foundation
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
import minimuxer
|
||||
|
||||
@objc(SendAppOperation)
|
||||
final class SendAppOperation: ResultOperation<()>
|
||||
@@ -44,24 +45,18 @@ final class SendAppOperation: ResultOperation<()>
|
||||
|
||||
print("AFC App `fileURL`: \(fileURL.absoluteString)")
|
||||
|
||||
let ns_bundle = NSString(string: app.bundleIdentifier)
|
||||
let ns_bundle_ptr = UnsafeMutablePointer<CChar>(mutating: ns_bundle.utf8String)
|
||||
|
||||
if let data = NSData(contentsOf: fileURL) {
|
||||
let pls = UnsafeMutablePointer<UInt8>.allocate(capacity: data.length)
|
||||
for (index, data) in data.enumerated() {
|
||||
pls[index] = data
|
||||
}
|
||||
let res = minimuxer_yeet_app_afc(ns_bundle_ptr, pls, UInt(data.length))
|
||||
if res == 0 {
|
||||
print("minimuxer_yeet_app_afc `res` == \(res)")
|
||||
self.progress.completedUnitCount += 1
|
||||
self.finish(.success(()))
|
||||
} else {
|
||||
self.finish(.failure(minimuxer_to_operation(code: res)))
|
||||
do {
|
||||
let bytes = Data(data).toRustByteSlice()
|
||||
try yeet_app_afc(app.bundleIdentifier, bytes.forRust())
|
||||
} catch {
|
||||
return self.finish(.failure(minimuxerToOperationError(error)))
|
||||
}
|
||||
|
||||
self.progress.completedUnitCount += 1
|
||||
self.finish(.success(()))
|
||||
} else {
|
||||
print("IPA doesn't exist????")
|
||||
self.finish(.failure(ALTServerError(.underlyingError)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>application-identifier</key>
|
||||
<string>A72ZC8AJ5X.com.SideStore.AltStore</string>
|
||||
<string>A72ZC8AJ5X.com.SideStore.SideStore</string>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.siri</key>
|
||||
@@ -12,9 +12,9 @@
|
||||
<string>A72ZC8AJ5X</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.SideStore.AltStore</string>
|
||||
<string>group.com.SideStore.SideStore</string>
|
||||
</array>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
||||
@@ -16,15 +16,13 @@
|
||||
<key>Key</key>
|
||||
<string>customAnisetteURL</string>
|
||||
<key>DefaultValue</key>
|
||||
<string>http://ani.sidestore.io</string>
|
||||
<string>https://ani.sidestore.io</string>
|
||||
<key>Titles</key>
|
||||
<array>
|
||||
<string>SideStore</string>
|
||||
<string>Macley (US)</string>
|
||||
<string>Macley (DE)</string>
|
||||
<string>DrPudding</string>
|
||||
<string>jkcoxson (AltServer)</string>
|
||||
<string>jkcoxson (Provision)</string>
|
||||
<string>Sideloadly</string>
|
||||
<string>Nick</string>
|
||||
<string>Jawshoeadan</string>
|
||||
@@ -32,12 +30,10 @@
|
||||
</array>
|
||||
<key>Values</key>
|
||||
<array>
|
||||
<string>http://ani.sidestore.io</string>
|
||||
<string>https://ani.sidestore.io</string>
|
||||
<string>http://us1.sternserv.tech</string>
|
||||
<string>http://de1.sternserv.tech</string>
|
||||
<string>https://sign.rheaa.xyz</string>
|
||||
<string>http://jkcoxson.com:2095</string>
|
||||
<string>http://jkcoxson.com:2052</string>
|
||||
<string>https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx</string>
|
||||
<string>http://45.33.29.114</string>
|
||||
<string>https://anisette.jawshoeadan.me</string>
|
||||
|
||||
@@ -381,13 +381,12 @@ private extension SourcesViewController
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if let error = fetchError
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
let sources = featuredSourceURLs.compactMap { sourcesByURL[$0] }
|
||||
finish(.success(sources))
|
||||
print(error)
|
||||
// 1 error doesn't mean all trusted sources failed to load! Riley, why did you do this???????
|
||||
// finish(.failure(error))
|
||||
}
|
||||
let sources = featuredSourceURLs.compactMap { sourcesByURL[$0] }
|
||||
finish(.success(sources))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user