diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 721362b6..a831d74c 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */; }; 4879A95F2861046500FC1BBD /* AltSign in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A95E2861046500FC1BBD /* AltSign */; }; 4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; }; + 990D2AE22A1910CD0055D93C /* UnstableFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 990D2AE12A1910CD0055D93C /* UnstableFeatures.swift */; }; 9922FFEC29B501C50020F868 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 9922FFEB29B501C50020F868 /* Starscream */; }; 99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 99C4EF4C2979132100CB538D /* SemanticVersion */; }; 99F87D0529D8B4E200B40039 /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9961EC2D29BE9F2E00AF2C6F /* minimuxer-helpers.swift */; }; @@ -511,6 +512,7 @@ 191E5FD1290A651D001A3B7C /* jsmn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = jsmn.h; path = Dependencies/libplist/src/jsmn.h; sourceTree = SOURCE_ROOT; }; 1920B04E2924AC8300744F60 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = ""; }; + 990D2AE12A1910CD0055D93C /* UnstableFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnstableFeatures.swift; sourceTree = ""; }; 9961EC2D29BE9F2E00AF2C6F /* minimuxer-helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "minimuxer-helpers.swift"; path = "Dependencies/minimuxer/minimuxer-helpers.swift"; sourceTree = SOURCE_ROOT; }; 99F87D1629D8E4C900B40039 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftBridgeCore.swift; path = Dependencies/minimuxer/SwiftBridgeCore.swift; sourceTree = SOURCE_ROOT; }; 99F87D1729D8E4C900B40039 /* minimuxer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = minimuxer.swift; path = Dependencies/minimuxer/minimuxer.swift; sourceTree = SOURCE_ROOT; }; @@ -971,6 +973,14 @@ path = "libimobiledevice-glue/src"; sourceTree = ""; }; + 990D2AE02A1910920055D93C /* Unstable Features */ = { + isa = PBXGroup; + children = ( + 990D2AE12A1910CD0055D93C /* UnstableFeatures.swift */, + ); + path = "Unstable Features"; + sourceTree = ""; + }; 99F87D1429D8E3F100B40039 /* Generated */ = { isa = PBXGroup; children = ( @@ -1545,6 +1555,7 @@ BFD2476C2284B9A500981D42 /* AltStore */ = { isa = PBXGroup; children = ( + 990D2AE02A1910920055D93C /* Unstable Features */, B39F16112918D7B5002E9404 /* Consts */, BF219A7E22CAC431007676A6 /* AltStore.entitlements */, BFD2476D2284B9A500981D42 /* AppDelegate.swift */, @@ -2462,6 +2473,7 @@ D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */, BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */, + 990D2AE22A1910CD0055D93C /* UnstableFeatures.swift in Sources */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */, D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */, diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index 34e1b073..8ece8b2a 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -61,6 +61,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // Register default settings before doing anything else. UserDefaults.registerDefaults() + UnstableFeatures.load() + DatabaseManager.shared.start { (error) in if let error = error { diff --git a/AltStore/Unstable Features/UnstableFeatures.swift b/AltStore/Unstable Features/UnstableFeatures.swift new file mode 100644 index 00000000..975b3449 --- /dev/null +++ b/AltStore/Unstable Features/UnstableFeatures.swift @@ -0,0 +1,95 @@ +// +// UnstableFeatures.swift +// SideStore +// +// Created by naturecodevoid on 5/20/23. +// Copyright © 2023 SideStore. All rights reserved. +// + +// I prefixed it with Available to make UnstableFeatures come up first in autocomplete, feel free to rename it if you know a better name +enum AvailableUnstableFeature: String, CaseIterable { + // The value will be the GitHub Issue number. For example, "123" would correspond to https://github.com/SideStore/SideStore/issues/123 + // + // Unstable features must have a GitHub Issue for tracking progress, PRs and feedback/commenting. + + /// Dummy variant to ensure there is always at least one variant. DO NOT USE! + case dummy = "dummy" + + func availableOutsideDevMode() -> Bool { + switch self { + // If your unstable feature is stable enough to be used by nightly users who are not alpha testers or developers, + // you may want to have it available in the "Unstable Features" menu in Settings (outside of dev mode). To do so, add this: + //case .yourFeature: return true + + default: return false + } + } +} + +class UnstableFeatures { + #if UNSTABLE + private static var features: [AvailableUnstableFeature: Bool] = [:] + #endif + + static func load() { + #if UNSTABLE + + if features.count > 0 { return print("It seems unstable features have already been loaded, skipping") } + + if let rawFeatures = UserDefaults.shared.unstableFeatures, + var rawFeatures = try? JSONDecoder().decode([String: Bool].self, from: rawFeatures) { + for rawFeature in rawFeatures { + if let feature = AvailableUnstableFeature.allCases.first(where: { feature in String(describing: feature) == rawFeature.key }) { + features[feature] = rawFeature.value + } else { + print("Unknown unstable feature: \(rawFeature.key) = \(rawFeature.value)") + } + } + save(load: true) + } else { + print("Setting all unstable features to false since we couldn't load them from UserDefaults (either they were never saved or there was an error decoding JSON)") + for feature in AvailableUnstableFeature.allCases { + features[feature] = false + } + save() + } + + #else + print("Unstable features are not available on this build") + #endif + } + + private static func save(load: Bool = false) { + #if UNSTABLE + + var rawFeatures: [String: Bool] = [:] + for feature in features { + rawFeatures[String(describing: feature.key)] = feature.value + } + UserDefaults.shared.unstableFeatures = try! JSONEncoder().encode(rawFeatures) + print("\(load ? "Loaded" : "Saved") unstable features: \(String(describing: rawFeatures))") + + #endif + } + + static func set(_ feature: AvailableUnstableFeature, enabled: Bool) { + #if UNSTABLE + + features[feature] = enabled + save() + + #else + // we want this to crash, this function should never be triggered on non-unstable builds + fatalError("Tried to set unstable feature \(String(describing: feature)) to \(enabled) on non-unstable build!") + #endif + } + + @inline(__always) // hopefully this will help the compiler realize that if statements that use this function should be removed on non-unstable builds + static func enabled(_ feature: AvailableUnstableFeature) -> Bool { + #if UNSTABLE + features[feature] ?? false + #else + false + #endif + } +} diff --git a/AltStoreCore/Extensions/UserDefaults+AltStore.swift b/AltStoreCore/Extensions/UserDefaults+AltStore.swift index e72d48dc..7a057a55 100644 --- a/AltStoreCore/Extensions/UserDefaults+AltStore.swift +++ b/AltStoreCore/Extensions/UserDefaults+AltStore.swift @@ -44,6 +44,8 @@ public extension UserDefaults @NSManaged var trustedSourceIDs: [String]? @NSManaged var trustedServerURL: String? + @NSManaged var unstableFeatures: Data? + var activeAppsLimit: Int? { get { return self._activeAppsLimit?.intValue diff --git a/Build.xcconfig b/Build.xcconfig index b0686dea..c3cbb3a5 100644 --- a/Build.xcconfig +++ b/Build.xcconfig @@ -4,6 +4,8 @@ MARKETING_VERSION = 0.3.2 CURRENT_PROJECT_VERSION = 3050 +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug] = $(inherited) UNSTABLE + // Vars to be overwritten by `CodeSigning.xcconfig` if exists DEVELOPMENT_TEAM = S32Z3HMYVQ ORG_IDENTIFIER = com.SideStore