diff --git a/ios/Approach.xcodeproj/project.pbxproj b/ios/Approach.xcodeproj/project.pbxproj index fe7845c3e..4687b4fd1 100644 --- a/ios/Approach.xcodeproj/project.pbxproj +++ b/ios/Approach.xcodeproj/project.pbxproj @@ -61,7 +61,6 @@ CAD15BBB29FDF8E40015E4CB /* GamesRepository in Frameworks */ = {isa = PBXBuildFile; productRef = CAD15BBA29FDF8E40015E4CB /* GamesRepository */; }; CAD15BC129FDFF890015E4CB /* TestDatabaseUtilitiesLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = CAD15BC029FDFF890015E4CB /* TestDatabaseUtilitiesLibrary */; }; CAECBAEB2ABD7323007713A8 /* LanesRepository in Frameworks */ = {isa = PBXBuildFile; productRef = CAECBAEA2ABD7323007713A8 /* LanesRepository */; }; - CAF80290295D67FC00B19459 /* FeatureFlagsService in Frameworks */ = {isa = PBXBuildFile; productRef = CAF8028F295D67FC00B19459 /* FeatureFlagsService */; }; CAFF2FCB2AB19B3000AE31BF /* AvatarService in Frameworks */ = {isa = PBXBuildFile; productRef = CAFF2FCA2AB19B3000AE31BF /* AvatarService */; }; /* End PBXBuildFile section */ @@ -144,7 +143,6 @@ CA626E2F2A842032002BCC13 /* TipsService in Frameworks */, CA5952BB2A1FF51D0002494C /* StatisticsRepository in Frameworks */, CAA3D74D29CFB48D00876EDC /* BowlersRepository in Frameworks */, - CAF80290295D67FC00B19459 /* FeatureFlagsService in Frameworks */, CA318B402999E1F500BD4BEC /* AnalyticsService in Frameworks */, CAC56853291F54A100192D12 /* RecentlyUsedService in Frameworks */, ); @@ -304,7 +302,6 @@ CA2890DD28F87FF2003C1EE9 /* AppFeature */, CAC56850291F54A100192D12 /* PreferenceService */, CAC56852291F54A100192D12 /* RecentlyUsedService */, - CAF8028F295D67FC00B19459 /* FeatureFlagsService */, CA318B3F2999E1F500BD4BEC /* AnalyticsService */, CA681DED29B94BE900535750 /* AvatarService */, CABEC0CE29CBF9CD003CF669 /* AddressLookupService */, @@ -717,10 +714,6 @@ isa = XCSwiftPackageProductDependency; productName = LanesRepository; }; - CAF8028F295D67FC00B19459 /* FeatureFlagsService */ = { - isa = XCSwiftPackageProductDependency; - productName = FeatureFlagsService; - }; CAFF2FCA2AB19B3000AE31BF /* AvatarService */ = { isa = XCSwiftPackageProductDependency; productName = AvatarService; diff --git a/ios/Approach.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Approach.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 011a744c0..8486c6a61 100644 --- a/ios/Approach.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Approach.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -204,7 +204,7 @@ "location" : "https://github.com/autoreleasefool/swift-utilities.git", "state" : { "branch" : "main", - "revision" : "8b406f005b27805742dfb2eee653f11e76a7d499" + "revision" : "da6794300b2f41ec925e3982def20b425626d41d" } }, { diff --git a/ios/Approach/Package.swift b/ios/Approach/Package.swift index e1a4f49ad..01cf17805 100644 --- a/ios/Approach/Package.swift +++ b/ios/Approach/Package.swift @@ -92,8 +92,6 @@ let package = Package( .library(name: "DatabaseServiceInterface", targets: ["DatabaseServiceInterface"]), .library(name: "EmailService", targets: ["EmailService"]), .library(name: "EmailServiceInterface", targets: ["EmailServiceInterface"]), - .library(name: "FeatureFlagsService", targets: ["FeatureFlagsService"]), - .library(name: "FeatureFlagsServiceInterface", targets: ["FeatureFlagsServiceInterface"]), .library(name: "ImportExportService", targets: ["ImportExportService"]), .library(name: "ImportExportServiceInterface", targets: ["ImportExportServiceInterface"]), .library(name: "LaunchService", targets: ["LaunchService"]), @@ -223,7 +221,7 @@ let package = Package( name: "AlleysListFeature", dependencies: [ "AlleyEditorFeature", - "FeatureFlagsServiceInterface", + "FeatureFlagsLibrary", "ResourceListLibrary", ] ), @@ -256,6 +254,7 @@ let package = Package( .product(name: "AnalyticsPackageService", package: "swift-utilities"), .product(name: "AppInfoPackageService", package: "swift-utilities"), .product(name: "BundlePackageService", package: "swift-utilities"), + .product(name: "FeatureFlagsPackageService", package: "swift-utilities"), .product(name: "FileManagerPackageService", package: "swift-utilities"), .product(name: "PasteboardPackageService", package: "swift-utilities"), .product(name: "SentryErrorReportingPackageService", package: "swift-utilities"), @@ -372,7 +371,7 @@ let package = Package( dependencies: [ "AnalyticsServiceInterface", "FeatureActionLibrary", - "FeatureFlagsServiceInterface", + "FeatureFlagsLibrary", "LoggingServiceInterface", "StringsLibrary", ] @@ -402,7 +401,7 @@ let package = Package( dependencies: [ .product(name: "StoreReviewPackageServiceInterface", package: "swift-utilities"), "AvatarServiceInterface", - "FeatureFlagsServiceInterface", + "FeatureFlagsLibrary", "FramesRepositoryInterface", "GearRepositoryInterface", "LanesRepositoryInterface", @@ -572,7 +571,7 @@ let package = Package( .target( name: "OpponentsListFeature", dependencies: [ - "FeatureFlagsServiceInterface", + "FeatureFlagsLibrary", "ModelsViewsLibrary", "OpponentDetailsFeature", "RecentlyUsedServiceInterface", @@ -605,7 +604,7 @@ let package = Package( dependencies: [ "AlleysRepositoryInterface", "DateTimeLibrary", - "FeatureFlagsServiceInterface", + "FeatureFlagsLibrary", "FormFeature", "ModelsViewsLibrary", "PickableModelsLibrary", @@ -1053,7 +1052,7 @@ let package = Package( dependencies: [ .product(name: "UserDefaultsPackageServiceInterface", package: "swift-utilities"), "DatabaseServiceInterface", - "FeatureFlagsServiceInterface", + "FeatureFlagsLibrary", "PreferenceServiceInterface", "RepositoryLibrary", "StatisticsModelsLibrary", @@ -1260,27 +1259,6 @@ let package = Package( "EmailService", ] ), - .target( - name: "FeatureFlagsService", - dependencies: [ - .product(name: "UserDefaultsPackageServiceInterface", package: "swift-utilities"), - "FeatureFlagsServiceInterface", - ] - ), - .target( - name: "FeatureFlagsServiceInterface", - dependencies: [ - .product(name: "Dependencies", package: "swift-dependencies"), - "FeatureFlagsLibrary", - ] - ), - .testTarget( - name: "FeatureFlagsServiceTests", - dependencies: [ - .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), - "FeatureFlagsService", - ] - ), .target( name: "ImportExportService", dependencies: [ @@ -1310,7 +1288,7 @@ let package = Package( .product(name: "StoreReviewPackageServiceInterface", package: "swift-utilities"), .product(name: "UserDefaultsPackageServiceInterface", package: "swift-utilities"), "AnalyticsServiceInterface", - "FeatureFlagsServiceInterface", + "FeatureFlagsLibrary", "LaunchServiceInterface", "PreferenceServiceInterface", "ProductsServiceInterface", @@ -1561,13 +1539,9 @@ let package = Package( ), .target( name: "FeatureFlagsLibrary", - dependencies: [] - ), - .testTarget( - name: "FeatureFlagsLibraryTests", dependencies: [ - .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), - "FeatureFlagsLibrary", + .product(name: "FeatureFlagsPackageLibrary", package: "swift-utilities"), + .product(name: "FeatureFlagsPackageServiceInterface", package: "swift-utilities"), ] ), .target( diff --git a/ios/Approach/Package.swift.toml b/ios/Approach/Package.swift.toml index 51202370f..d241ebf8e 100644 --- a/ios/Approach/Package.swift.toml +++ b/ios/Approach/Package.swift.toml @@ -12,7 +12,7 @@ supported = [ "\"17.0\"" ] [features.App] features = [ "AccessoriesOverview", "BowlersList", "Onboarding", "Settings", "StatisticsOverview" ] services = [ "Launch" ] -dependencies = [ "AnalyticsPackageService", "AppInfoPackageService", "BundlePackageService", "FileManagerPackageService", "PasteboardPackageService", "SentryErrorReportingPackageService", "StoreReviewPackageService", "TelemetryDeckAnalyticsPackageService", "UserDefaultsPackageService" ] +dependencies = [ "AnalyticsPackageService", "AppInfoPackageService", "BundlePackageService", "FeatureFlagsPackageService", "FileManagerPackageService", "PasteboardPackageService", "SentryErrorReportingPackageService", "StoreReviewPackageService", "TelemetryDeckAnalyticsPackageService", "UserDefaultsPackageService" ] [features.AccessoriesOverview] features = [ "AlleysList", "GearList" ] @@ -31,8 +31,7 @@ libraries = [ "ModelsViews" ] [features.AlleysList] features = [ "AlleyEditor" ] -services = [ "FeatureFlags" ] -libraries = [ "ResourceList" ] +libraries = [ "FeatureFlags", "ResourceList" ] [features.Announcements] services = [ "Announcements" ] @@ -61,8 +60,7 @@ libraries = [ "Constants", "Toast", "Views" ] dependencies = [ "AppInfoPackageServiceInterface", "EquatablePackageLibrary", "FileManagerPackageServiceInterface", "PasteboardPackageServiceInterface" ] [features.FeatureFlagsList] -services = [ "FeatureFlags" ] -libraries = [ "Strings" ] +libraries = [ "FeatureFlags", "Strings" ] [features.Form] features = [ "Errors" ] @@ -70,7 +68,8 @@ features = [ "Errors" ] [features.GamesEditor] features = [ "Sharing", "StatisticsDetails" ] repositories = [ "Frames", "Gear", "Lanes" ] -services = [ "Avatar", "FeatureFlags", "RecentlyUsed" ] +services = [ "Avatar", "RecentlyUsed" ] +libraries = [ "FeatureFlags" ] dependencies = [ "StoreReviewPackageServiceInterface" ] [features.GamesList] @@ -111,8 +110,8 @@ libraries = [ "ResourceList" ] [features.OpponentsList] features = [ "OpponentDetails" ] -services = [ "FeatureFlags", "RecentlyUsed" ] -libraries = [ "ModelsViews", "SortOrder" ] +services = [ "RecentlyUsed" ] +libraries = [ "FeatureFlags", "ModelsViews", "SortOrder" ] [features.Onboarding] repositories = [ "Bowlers" ] @@ -126,8 +125,7 @@ services = [ "Products" ] [features.SeriesEditor] features = [ "Form" ] repositories = [ "Alleys", "Series" ] -services = [ "FeatureFlags" ] -libraries = [ "DateTime", "ModelsViews", "PickableModels" ] +libraries = [ "DateTime", "FeatureFlags", "ModelsViews", "PickableModels" ] [features.SeriesList] features = [ "GamesList", "LeagueEditor" ] @@ -206,8 +204,8 @@ repositories = [ "Frames", "Games" ] [repositories.Series.interface] [repositories.Statistics] -services = [ "Preference", "FeatureFlags" ] -libraries = [ "StatisticsModels" ] +services = [ "Preference" ] +libraries = [ "FeatureFlags", "StatisticsModels" ] dependencies = [ "UserDefaultsPackageServiceInterface" ] [repositories.Statistics.interface] libraries = [ "StatisticsWidgets" ] @@ -258,18 +256,14 @@ skip_tests = true [services.Email] -[services.FeatureFlags] -dependencies = [ "UserDefaultsPackageServiceInterface" ] -[services.FeatureFlags.interface] -libraries = [ "FeatureFlags" ] - [services.ImportExport] services = [ "Database" ] libraries = [ "DateTime" ] dependencies = [ "FileManagerPackageServiceInterface" ] [services.Launch] -services = [ "Analytics", "FeatureFlags", "Preference", "Products" ] +services = [ "Analytics", "Preference", "Products" ] +libraries = [ "FeatureFlags" ] dependencies = [ "AppInfoPackageServiceInterface", "StoreReviewPackageServiceInterface", "UserDefaultsPackageServiceInterface" ] [services.Logging] @@ -341,6 +335,8 @@ skip_tests = true dependencies = [ "ComposableArchitecture" ] [libraries.FeatureFlags] +skip_tests = true +dependencies = [ "FeatureFlagsPackageLibrary", "FeatureFlagsPackageServiceInterface" ] [libraries.ListContent] libraries = [ "Views" ] @@ -536,6 +532,15 @@ sharedRef = "SwiftUtilities" [dependencies.ExtensionsPackageLibrary] sharedRef = "SwiftUtilities" +[dependencies.FeatureFlagsPackageLibrary] +sharedRef = "SwiftUtilities" + +[dependencies.FeatureFlagsPackageService] +sharedRef = "SwiftUtilities" + +[dependencies.FeatureFlagsPackageServiceInterface] +sharedRef = "SwiftUtilities" + [dependencies.FileManagerPackageService] sharedRef = "SwiftUtilities" diff --git a/ios/Approach/Sources/AlleysListFeature/AlleysList.swift b/ios/Approach/Sources/AlleysListFeature/AlleysList.swift index f2ba0d0fb..a00104ef2 100644 --- a/ios/Approach/Sources/AlleysListFeature/AlleysList.swift +++ b/ios/Approach/Sources/AlleysListFeature/AlleysList.swift @@ -5,7 +5,7 @@ import AssetsLibrary import ComposableArchitecture import ErrorsFeature import FeatureActionLibrary -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary import ModelsLibrary import ResourceListLibrary import StringsLibrary @@ -48,8 +48,8 @@ public struct AlleysList: Reducer { ) ) - @Dependency(FeatureFlagsService.self) var featureFlags - self.isAlleyAndGearAveragesEnabled = featureFlags.isEnabled(.alleyAndGearAverages) + @Dependency(\.featureFlags) var featureFlags + self.isAlleyAndGearAveragesEnabled = try featureFlags.isFlagEnabled(.alleyAndGearAverages) } } @@ -89,7 +89,6 @@ public struct AlleysList: Reducer { public init() {} @Dependency(AlleysRepository.self) var alleys - @Dependency(FeatureFlagsService.self) var featureFlags @Dependency(\.uuid) var uuid public var body: some ReducerOf { diff --git a/ios/Approach/Sources/AppFeature/App.swift b/ios/Approach/Sources/AppFeature/App.swift index 431717928..2a6fb79d5 100644 --- a/ios/Approach/Sources/AppFeature/App.swift +++ b/ios/Approach/Sources/AppFeature/App.swift @@ -9,6 +9,7 @@ import StatisticsRepositoryInterface import AnalyticsPackageService import AppInfoPackageService import BundlePackageService +import FeatureFlagsPackageService import FileManagerPackageService import PasteboardPackageService import SentryErrorReportingPackageService diff --git a/ios/Approach/Sources/AppFeature/TabbedContent.swift b/ios/Approach/Sources/AppFeature/TabbedContent.swift index c601cc30a..5f0b84439 100644 --- a/ios/Approach/Sources/AppFeature/TabbedContent.swift +++ b/ios/Approach/Sources/AppFeature/TabbedContent.swift @@ -3,8 +3,6 @@ import AnalyticsServiceInterface import BowlersListFeature import ComposableArchitecture import FeatureActionLibrary -import FeatureFlagsLibrary -import FeatureFlagsServiceInterface import SettingsFeature import StatisticsOverviewFeature @@ -50,8 +48,6 @@ public struct TabbedContent: Reducer { public init() {} - @Dependency(FeatureFlagsService.self) var featureFlags - public var body: some ReducerOf { BindingReducer() diff --git a/ios/Approach/Sources/FeatureFlagsLibrary/FeatureFlag.swift b/ios/Approach/Sources/FeatureFlagsLibrary/FeatureFlag.swift deleted file mode 100644 index aa5dede59..000000000 --- a/ios/Approach/Sources/FeatureFlagsLibrary/FeatureFlag.swift +++ /dev/null @@ -1,36 +0,0 @@ -public struct FeatureFlag: Identifiable, Hashable { - public let name: String - public let introduced: String - public let stage: RolloutStage - public let isOverridable: Bool - - public var id: String { name } - - init(name: String, introduced: String, stage: RolloutStage, isOverridable: Bool = true) { - self.name = name - self.introduced = introduced - self.stage = stage - self.isOverridable = isOverridable - } -} - -extension FeatureFlag { - public enum RolloutStage: Comparable, CaseIterable { - case disabled - case development - case test - case release - - public static func < (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case (.disabled, .disabled): return false - case (.disabled, _): return true - case (.development, .disabled), (.development, .development): return false - case (.development, _): return true - case (.test, .disabled), (.test, .development), (.test, .test): return false - case (.test, _): return true - case (.release, _): return false - } - } - } -} diff --git a/ios/Approach/Sources/FeatureFlagsLibrary/FeatureFlags.swift b/ios/Approach/Sources/FeatureFlagsLibrary/FeatureFlags.swift index b261d3294..aefb8e4e3 100644 --- a/ios/Approach/Sources/FeatureFlagsLibrary/FeatureFlags.swift +++ b/ios/Approach/Sources/FeatureFlagsLibrary/FeatureFlags.swift @@ -1,4 +1,6 @@ // swiftlint:disable line_length +@_exported import FeatureFlagsPackageLibrary +@_exported import FeatureFlagsPackageServiceInterface extension FeatureFlag { public static let developerOptions = Self(name: "developerOptions", introduced: "2022-11-10", stage: .development, isOverridable: false) diff --git a/ios/Approach/Sources/FeatureFlagsLibrary/FeatureFlagsService+Extensions.swift b/ios/Approach/Sources/FeatureFlagsLibrary/FeatureFlagsService+Extensions.swift new file mode 100644 index 000000000..7400fdf5f --- /dev/null +++ b/ios/Approach/Sources/FeatureFlagsLibrary/FeatureFlagsService+Extensions.swift @@ -0,0 +1,12 @@ +import FeatureFlagsPackageLibrary +import FeatureFlagsPackageServiceInterface + +extension FeatureFlagsService { + public func isFlagEnabled(_ flag: FeatureFlag) -> Bool { + (try? self.isEnabled(flag)) ?? false + } + + public func allFlagsEnabled(_ flags: [FeatureFlag]) -> Bool { + (try? self.allEnabled(flags)) ?? false + } +} diff --git a/ios/Approach/Sources/FeatureFlagsListFeature/FeatureFlagList.swift b/ios/Approach/Sources/FeatureFlagsListFeature/FeatureFlagList.swift index 0e9f00150..152cdd706 100644 --- a/ios/Approach/Sources/FeatureFlagsListFeature/FeatureFlagList.swift +++ b/ios/Approach/Sources/FeatureFlagsListFeature/FeatureFlagList.swift @@ -1,7 +1,6 @@ import ComposableArchitecture import FeatureActionLibrary import FeatureFlagsLibrary -import FeatureFlagsServiceInterface public struct FeatureFlagItem: Equatable { let flag: FeatureFlag @@ -38,7 +37,7 @@ public struct FeatureFlagsList: Reducer { public init() {} - @Dependency(FeatureFlagsService.self) var featureFlagService + @Dependency(\.featureFlags) var featureFlagService public var body: some ReducerOf { Reduce { state, action in @@ -48,7 +47,7 @@ public struct FeatureFlagsList: Reducer { case .didStartObservingFlags: return .run { send in let observedFlags = FeatureFlag.allFlags - for await flags in featureFlagService.observeAll(observedFlags) { + for await flags in try featureFlagService.observeAll(observedFlags) { await send(.internal(.didLoadFlags(observedFlags.map { FeatureFlagItem(flag: $0, enabled: flags[$0] ?? false) }))) @@ -93,7 +92,7 @@ public struct FeatureFlagsList: Reducer { case let .featureFlagToggle(.element(id, .binding(\.flag))): guard let flag = FeatureFlag.find(byId: id) else { return .none } return .run { _ in - let isEnabled = featureFlagService.isEnabled(flag) + let isEnabled = featureFlagService.isFlagEnabled(flag) featureFlagService.setEnabled(flag, !isEnabled) } diff --git a/ios/Approach/Sources/FeatureFlagsService/FeatureFlag+IsEnabled.swift b/ios/Approach/Sources/FeatureFlagsService/FeatureFlag+IsEnabled.swift deleted file mode 100644 index 0c007550b..000000000 --- a/ios/Approach/Sources/FeatureFlagsService/FeatureFlag+IsEnabled.swift +++ /dev/null @@ -1,17 +0,0 @@ -import FeatureFlagsLibrary -import Foundation - -extension FeatureFlag { - public var isEnabled: Bool { - #if DEBUG - stage >= .development - #else - let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" - if isTestFlight { - return stage >= .test - } else { - return stage >= .release - } - #endif - } -} diff --git a/ios/Approach/Sources/FeatureFlagsService/FeatureFlagService+Live.swift b/ios/Approach/Sources/FeatureFlagsService/FeatureFlagService+Live.swift deleted file mode 100644 index b028a4998..000000000 --- a/ios/Approach/Sources/FeatureFlagsService/FeatureFlagService+Live.swift +++ /dev/null @@ -1,157 +0,0 @@ -import Combine -import Dependencies -import FeatureFlagsLibrary -import FeatureFlagsServiceInterface -import Foundation -import UserDefaultsPackageServiceInterface - -extension NSNotification.Name { - enum FeatureFlag { - static let didChange = NSNotification.Name("FeatureFlag.didChange") - } -} - -extension FeatureFlagsService: DependencyKey { - public static var liveValue: Self { - @Dependency(\.featureFlagsQueue) var queue - - let flagManager = FeatureFlagOverrides(queue: queue) - - @Sendable func isFlagEnabled(flag: FeatureFlag) -> Bool { - #if DEBUG - return flagManager.getOverride(flag: flag) ?? flag.isEnabled - #else - return flag.isEnabled - #endif - } - - @Sendable func areFlagsEnabled(flags: [FeatureFlag]) -> [FeatureFlag: Bool] { - #if DEBUG - let overrides = flagManager.getOverrides(flags: flags) - return zip(flags, overrides).reduce(into: [:]) { acc, override in - acc[override.0] = override.1 ?? override.0.isEnabled - } - #else - return flags.reduce(into: [:]) { acc, flag in acc[flag] = flag.isEnabled } - #endif - } - - return Self( - isEnabled: isFlagEnabled(flag:), - allEnabled: { flags in areFlagsEnabled(flags: flags).allSatisfy { $0.value } }, - observe: { flag in - .init { continuation in - continuation.yield(isFlagEnabled(flag: flag)) - - let cancellable = NotificationCenter.default - .publisher(for: .FeatureFlag.didChange) - .filter { - guard let objectFlag = $0.object as? FeatureFlag else { return false } - return flag == objectFlag - } - .sink { _ in - continuation.yield(isFlagEnabled(flag: flag)) - } - - continuation.onTermination = { _ in cancellable.cancel() } - } - }, - observeAll: { flags in - .init { continuation in - continuation.yield(areFlagsEnabled(flags: flags)) - - let cancellable = NotificationCenter.default - .publisher(for: .FeatureFlag.didChange) - .filter { - guard let objectFlag = $0.object as? FeatureFlag else { return false } - return flags.contains(objectFlag) - } - .sink { _ in - continuation.yield(areFlagsEnabled(flags: flags)) - } - - continuation.onTermination = { _ in cancellable.cancel() } - } - }, - setEnabled: { flag, enabled in - flagManager.setOverride(forFlag: flag, enabled: enabled) - NotificationCenter.default.post(name: .FeatureFlag.didChange, object: flag) - }, - resetOverrides: { - for flag in flagManager.resetOverrides() { - NotificationCenter.default.post(name: .FeatureFlag.didChange, object: flag) - } - } - ) - } -} - -class FeatureFlagOverrides { - private let queue: DispatchQueue - private var queue_overrides: [FeatureFlag: Bool] = [:] - @Dependency(\.userDefaults) var userDefaults - - init(queue: DispatchQueue) { - self.queue = queue - queue.sync { - for flag in FeatureFlag.allFlags { - queue_overrides[flag] = userDefaults.bool(forKey: flag.overrideKey) - } - } - } - - func resetOverrides() -> [FeatureFlag] { - queue.sync { - let overridden = Array(self.queue_overrides.keys) - self.queue_overrides.removeAll() - for flag in FeatureFlag.allFlags { - userDefaults.remove(key: flag.overrideKey) - } - return overridden - } - } - - func setOverride(forFlag flag: FeatureFlag, enabled: Bool?) { - queue.async { - guard flag.isOverridable else { return } - self.queue_overrides[flag] = enabled - if let enabled { - self.userDefaults.setBool(forKey: flag.overrideKey, to: enabled) - } else { - self.userDefaults.remove(key: flag.overrideKey) - } - } - } - - func getOverride(flag: FeatureFlag) -> Bool? { - guard flag.isOverridable else { return nil } - return queue.sync(flags: .barrier) { queue_overrides[flag] } - } - - func getOverrides(flags: [FeatureFlag]) -> [Bool?] { - queue.sync(flags: .barrier) { flags.map { $0.isOverridable ? queue_overrides[$0] : nil } } - } -} - -extension FeatureFlag { - var overrideKey: String { - "FeatureFlag.Override.\(name)" - } -} - -extension DependencyValues { - var featureFlagsQueue: DispatchQueue { - get { self[FeatureFlagsQueueKey.self] } - set { self[FeatureFlagsQueueKey.self] = newValue } - } - - enum FeatureFlagsQueueKey: DependencyKey { - static var liveValue: DispatchQueue { - DispatchQueue(label: "FeatureFlagsService.FeatureFlagManager", attributes: .concurrent) - } - - static var testValue: DispatchQueue { - DispatchQueue(label: "FeatureFlagsService.FeatureFlagManager", attributes: .concurrent) - } - } -} diff --git a/ios/Approach/Sources/FeatureFlagsServiceInterface/FeatureFlagService+Interface.swift b/ios/Approach/Sources/FeatureFlagsServiceInterface/FeatureFlagService+Interface.swift deleted file mode 100644 index 9ed4bf7ca..000000000 --- a/ios/Approach/Sources/FeatureFlagsServiceInterface/FeatureFlagService+Interface.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Dependencies -import FeatureFlagsLibrary - -public struct FeatureFlagsService: Sendable { - public var isEnabled: @Sendable (FeatureFlag) -> Bool - public var allEnabled: @Sendable ([FeatureFlag]) -> Bool - public var observe: @Sendable (FeatureFlag) -> AsyncStream - public var observeAll: @Sendable ([FeatureFlag]) -> AsyncStream<[FeatureFlag: Bool]> - public var setEnabled: @Sendable (FeatureFlag, Bool?) -> Void - public var resetOverrides: @Sendable () -> Void - - public init( - isEnabled: @escaping @Sendable (FeatureFlag) -> Bool, - allEnabled: @escaping @Sendable ([FeatureFlag]) -> Bool, - observe: @escaping @Sendable (FeatureFlag) -> AsyncStream, - observeAll: @escaping @Sendable ([FeatureFlag]) -> AsyncStream<[FeatureFlag: Bool]>, - setEnabled: @escaping @Sendable (FeatureFlag, Bool?) -> Void, - resetOverrides: @escaping @Sendable () -> Void - ) { - self.isEnabled = isEnabled - self.allEnabled = allEnabled - self.observe = observe - self.observeAll = observeAll - self.setEnabled = setEnabled - self.resetOverrides = resetOverrides - } -} - -extension FeatureFlagsService: TestDependencyKey { - public static var testValue = Self( - isEnabled: { _ in unimplemented("\(Self.self).isEnabled") }, - allEnabled: { _ in unimplemented("\(Self.self).allEnabled") }, - observe: { _ in unimplemented("\(Self.self).observe") }, - observeAll: { _ in unimplemented("\(Self.self).observeAll") }, - setEnabled: { _, _ in unimplemented("\(Self.self).setEnabled") }, - resetOverrides: { unimplemented("\(Self.self).resetOverrides") } - ) -} diff --git a/ios/Approach/Sources/GamesEditorFeature/Manage/GamesHeader.swift b/ios/Approach/Sources/GamesEditorFeature/Manage/GamesHeader.swift index cab0f4d25..8d35dc097 100644 --- a/ios/Approach/Sources/GamesEditorFeature/Manage/GamesHeader.swift +++ b/ios/Approach/Sources/GamesEditorFeature/Manage/GamesHeader.swift @@ -1,7 +1,7 @@ import AssetsLibrary import ComposableArchitecture import FeatureActionLibrary -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary import ModelsLibrary import PreferenceServiceInterface import StringsLibrary @@ -19,8 +19,8 @@ public struct GamesHeader: Reducer { public var isFlashEditorChangesEnabled: Bool init() { - @Dependency(FeatureFlagsService.self) var featureFlags - self.isSharingGameEnabled = featureFlags.isEnabled(.sharingGame) + @Dependency(\.featureFlags) var featureFlags + self.isSharingGameEnabled = featureFlags.isFlagEnabled(.sharingGame) @Dependency(\.preferences) var preferences self.isFlashEditorChangesEnabled = preferences.bool(forKey: .gameShouldNotifyEditorChanges) ?? true @@ -159,22 +159,3 @@ public struct GamesHeaderView: View { } } } - -#if DEBUG -struct GamesHeaderPreview: PreviewProvider { - static var previews: some View { - GamesHeaderView(store: .init( - initialState: GamesHeader.State(), - reducer: { - GamesHeader() - .dependency({ () -> FeatureFlagsService in - var service = FeatureFlagsService.testValue - service.isEnabled = { @Sendable _ in true } - return service - }()) - } - )) - .background(.black) - } -} -#endif diff --git a/ios/Approach/Sources/GamesEditorFeature/Manage/GamesSettings.swift b/ios/Approach/Sources/GamesEditorFeature/Manage/GamesSettings.swift index c5dd2de72..480923619 100644 --- a/ios/Approach/Sources/GamesEditorFeature/Manage/GamesSettings.swift +++ b/ios/Approach/Sources/GamesEditorFeature/Manage/GamesSettings.swift @@ -2,7 +2,6 @@ import AnalyticsServiceInterface import ComposableArchitecture import FeatureActionLibrary import FeatureFlagsLibrary -import FeatureFlagsServiceInterface import GamesRepositoryInterface import ModelsLibrary import PreferenceServiceInterface @@ -28,8 +27,8 @@ public struct GamesSettings: Reducer { self.numberOfGames = numberOfGames self.gameIndex = gameIndex - @Dependency(FeatureFlagsService.self) var featureFlags - self.isTeamsEnabled = featureFlags.isEnabled(.teams) + @Dependency(\.featureFlags) var featureFlags + self.isTeamsEnabled = featureFlags.isFlagEnabled(.teams) @Dependency(\.preferences) var preferences self.isFlashEditorChangesEnabled = preferences.bool(forKey: .gameShouldNotifyEditorChanges) ?? true diff --git a/ios/Approach/Sources/GamesEditorFeature/Roll/RollEditor.swift b/ios/Approach/Sources/GamesEditorFeature/Roll/RollEditor.swift index 3001723d8..24b97d884 100644 --- a/ios/Approach/Sources/GamesEditorFeature/Roll/RollEditor.swift +++ b/ios/Approach/Sources/GamesEditorFeature/Roll/RollEditor.swift @@ -5,7 +5,7 @@ import ComposableArchitecture import EquatablePackageLibrary import ExtensionsPackageLibrary import FeatureActionLibrary -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary import GearRepositoryInterface import ModelsLibrary import RecentlyUsedServiceInterface @@ -182,55 +182,6 @@ public struct RollEditorView: View { } } -#if DEBUG -struct RollEditorPreview: PreviewProvider { - static var previews: some View { - RollEditorView(store: .init( - initialState: .init( - ballRolled: .init( - id: UUID(0), - name: "Yellow", - kind: .bowlingBall, - ownerName: nil, - avatar: .init(id: UUID(0), value: .text("", .default)) - ), - didFoul: false - ), - reducer: RollEditor.init - ) { - $0[GearRepository.self].mostRecentlyUsed = { @Sendable _, _ in - let (stream, continuation) = AsyncThrowingStream<[Gear.Summary], Error>.makeStream() - let task = Task { - while !Task.isCancelled { - try await Task.sleep(for: .seconds(1)) - continuation.yield([ - .init( - id: UUID(0), - name: "Yellow", - kind: .bowlingBall, - ownerName: "Joseph", - avatar: .init(id: UUID(0), value: .text("", .default)) - ), - .init( - id: UUID(1), - name: "Blue", - kind: .bowlingBall, - ownerName: "Sarah", - avatar: .init(id: UUID(1), value: .text("", .default)) - ), - ]) - } - } - continuation.onTermination = { _ in task.cancel() } - return stream - } - $0[FeatureFlagsService.self].isEnabled = { @Sendable _ in true } - }) - .background(.black) - } -} -#endif - extension Color { var rgb: Avatar.Background.RGB { let (red, green, blue, _) = UIColor(self).rgba diff --git a/ios/Approach/Sources/GamesListFeature/GamesList.swift b/ios/Approach/Sources/GamesListFeature/GamesList.swift index b048a9314..4d8336df4 100644 --- a/ios/Approach/Sources/GamesListFeature/GamesList.swift +++ b/ios/Approach/Sources/GamesListFeature/GamesList.swift @@ -5,7 +5,6 @@ import EquatablePackageLibrary import ErrorsFeature import FeatureActionLibrary import FeatureFlagsLibrary -import FeatureFlagsServiceInterface import Foundation import GamesEditorFeature import GamesRepositoryInterface @@ -53,8 +52,8 @@ public struct GamesList: Reducer { ) ) - @Dependency(FeatureFlagsService.self) var featureFlags - self.isSeriesSharingEnabled = featureFlags.isEnabled(.sharingSeries) + @Dependency(\.featureFlags) var featureFlags + self.isSeriesSharingEnabled = featureFlags.isFlagEnabled(.sharingSeries) @Dependency(TipsService.self) var tips self.isShowingArchiveTip = tips.shouldShow(tipFor: .gameArchiveTip) diff --git a/ios/Approach/Sources/LaunchService/LaunchService+Live.swift b/ios/Approach/Sources/LaunchService/LaunchService+Live.swift index 421a08077..bef43847a 100644 --- a/ios/Approach/Sources/LaunchService/LaunchService+Live.swift +++ b/ios/Approach/Sources/LaunchService/LaunchService+Live.swift @@ -1,7 +1,7 @@ import AnalyticsServiceInterface import AppInfoPackageServiceInterface import Dependencies -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary import LaunchServiceInterface import PreferenceServiceInterface import ProductsServiceInterface @@ -22,8 +22,10 @@ extension LaunchService: DependencyKey { }, didLaunch: { // For async initializers that can wait until task - @Dependency(FeatureFlagsService.self) var features - let isProductsEnabled = features.isEnabled(.purchases) + @Dependency(\.featureFlags) var featureFlags + featureFlags.initialize(registeringFeatureFlags: FeatureFlag.allFlags) + + let isProductsEnabled = featureFlags.isFlagEnabled(.purchases) if isProductsEnabled { @Dependency(ProductsService.self) var products products.initialize() diff --git a/ios/Approach/Sources/LeaguesListFeature/LeaguesList.swift b/ios/Approach/Sources/LeaguesListFeature/LeaguesList.swift index 2fedfbb23..cca9942d7 100644 --- a/ios/Approach/Sources/LeaguesListFeature/LeaguesList.swift +++ b/ios/Approach/Sources/LeaguesListFeature/LeaguesList.swift @@ -3,7 +3,6 @@ import AssetsLibrary import ComposableArchitecture import ErrorsFeature import FeatureActionLibrary -import FeatureFlagsServiceInterface import GamesListFeature import GearRepositoryInterface import LeagueEditorFeature @@ -131,7 +130,6 @@ public struct LeaguesList: Reducer { public init() {} @Dependency(\.continuousClock) var clock - @Dependency(FeatureFlagsService.self) var featureFlags @Dependency(GearRepository.self) var gear @Dependency(LeaguesRepository.self) var leagues @Dependency(\.preferences) var preferences diff --git a/ios/Approach/Sources/OpponentsListFeature/OpponentsList.swift b/ios/Approach/Sources/OpponentsListFeature/OpponentsList.swift index 4e8273194..4fec2bb94 100644 --- a/ios/Approach/Sources/OpponentsListFeature/OpponentsList.swift +++ b/ios/Approach/Sources/OpponentsListFeature/OpponentsList.swift @@ -6,7 +6,7 @@ import ComposableArchitecture import ErrorsFeature import FeatureActionLibrary import FeatureFlagsLibrary -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary import ModelsLibrary import OpponentDetailsFeature import RecentlyUsedServiceInterface @@ -55,8 +55,8 @@ public struct OpponentsList: Reducer { ) ) - @Dependency(FeatureFlagsService.self) var features - self.isOpponentDetailsEnabled = features.isEnabled(.opponentDetails) + @Dependency(\.featureFlags) var featureFlags + self.isOpponentDetailsEnabled = featureFlags.isFlagEnabled(.opponentDetails) } } diff --git a/ios/Approach/Sources/SeriesEditorFeature/SeriesEditor.swift b/ios/Approach/Sources/SeriesEditorFeature/SeriesEditor.swift index 4e0d18799..177ed1169 100644 --- a/ios/Approach/Sources/SeriesEditorFeature/SeriesEditor.swift +++ b/ios/Approach/Sources/SeriesEditorFeature/SeriesEditor.swift @@ -4,7 +4,7 @@ import ComposableArchitecture import DateTimeLibrary import EquatablePackageLibrary import FeatureActionLibrary -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary import FormFeature import Foundation import LanesRepositoryInterface @@ -84,9 +84,9 @@ public struct SeriesEditor: Reducer { } self.form = .init(initialValue: self.initialValue) - @Dependency(FeatureFlagsService.self) var featureFlags - self.isPreBowlFormEnabled = featureFlags.isEnabled(.preBowlForm) - self.isManualSeriesEnabled = featureFlags.isEnabled(.manualSeries) + @Dependency(\.featureFlags) var featureFlags + self.isPreBowlFormEnabled = featureFlags.isFlagEnabled(.preBowlForm) + self.isManualSeriesEnabled = featureFlags.isFlagEnabled(.manualSeries) } mutating func syncFormSharedState() { diff --git a/ios/Approach/Sources/SeriesListFeature/SeriesList.swift b/ios/Approach/Sources/SeriesListFeature/SeriesList.swift index 8f2a028f1..4489f12ba 100644 --- a/ios/Approach/Sources/SeriesListFeature/SeriesList.swift +++ b/ios/Approach/Sources/SeriesListFeature/SeriesList.swift @@ -4,7 +4,7 @@ import ComposableArchitecture import EquatablePackageLibrary import ErrorsFeature import FeatureActionLibrary -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary import Foundation import GamesListFeature import LeagueEditorFeature @@ -74,8 +74,8 @@ public struct SeriesList: Reducer { ) ) - @Dependency(FeatureFlagsService.self) var featureFlags - self.isPreBowlFormEnabled = featureFlags.isEnabled(.preBowlForm) + @Dependency(\.featureFlags) var featureFlags + self.isPreBowlFormEnabled = featureFlags.isFlagEnabled(.preBowlForm) } } @@ -132,7 +132,6 @@ public struct SeriesList: Reducer { @Dependency(\.calendar) var calendar @Dependency(\.date) var date @Dependency(\.dismiss) var dismiss - @Dependency(FeatureFlagsService.self) var featureFlags @Dependency(LeaguesRepository.self) var leagues @Dependency(SeriesRepository.self) var series @Dependency(\.uuid) var uuid diff --git a/ios/Approach/Sources/SettingsFeature/AppIcon/AppIconList.swift b/ios/Approach/Sources/SettingsFeature/AppIcon/AppIconList.swift index 4cac8dc50..3c1714581 100644 --- a/ios/Approach/Sources/SettingsFeature/AppIcon/AppIconList.swift +++ b/ios/Approach/Sources/SettingsFeature/AppIcon/AppIconList.swift @@ -3,7 +3,7 @@ import AppIconServiceInterface import AssetsLibrary import ComposableArchitecture import FeatureActionLibrary -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary import ProductsServiceInterface import StringsLibrary import SwiftUI @@ -21,8 +21,8 @@ public struct AppIconList: Reducer { public var isProEnabled: Bool init() { - @Dependency(FeatureFlagsService.self) var features - self.isPurchasesEnabled = features.isEnabled(.purchases) + @Dependency(\.featureFlags) var featureFlags + self.isPurchasesEnabled = featureFlags.isFlagEnabled(.purchases) @Dependency(ProductsService.self) var products self.isProEnabled = products.peekIsAvailable(.proSubscription) diff --git a/ios/Approach/Sources/SettingsFeature/Settings.swift b/ios/Approach/Sources/SettingsFeature/Settings.swift index 90e33417c..c5a35aee6 100644 --- a/ios/Approach/Sources/SettingsFeature/Settings.swift +++ b/ios/Approach/Sources/SettingsFeature/Settings.swift @@ -11,7 +11,6 @@ import EmailServiceInterface import FeatureActionLibrary import FeatureFlagsLibrary import FeatureFlagsListFeature -import FeatureFlagsServiceInterface import Foundation import ImportExportFeature import ImportExportServiceInterface @@ -41,10 +40,10 @@ public struct Settings: Reducer { public let isDeveloperOptionsEnabled: Bool public init() { - @Dependency(FeatureFlagsService.self) var featureFlags - self.isShowingDeveloperOptions = featureFlags.isEnabled(.developerOptions) - self.isImportEnabled = featureFlags.isEnabled(.dataImport) - self.isDeveloperOptionsEnabled = featureFlags.isEnabled(.developerOptions) + @Dependency(\.featureFlags) var featureFlags + self.isShowingDeveloperOptions = featureFlags.isFlagEnabled(.developerOptions) + self.isImportEnabled = featureFlags.isFlagEnabled(.dataImport) + self.isDeveloperOptionsEnabled = featureFlags.isFlagEnabled(.developerOptions) @Dependency(\.appInfo) var appInfo self.appVersion = appInfo.getFullAppVersion() diff --git a/ios/Approach/Sources/StatisticsRepository/StatisticsRepository+Live.swift b/ios/Approach/Sources/StatisticsRepository/StatisticsRepository+Live.swift index c7f203476..b4675c4c3 100644 --- a/ios/Approach/Sources/StatisticsRepository/StatisticsRepository+Live.swift +++ b/ios/Approach/Sources/StatisticsRepository/StatisticsRepository+Live.swift @@ -2,7 +2,7 @@ import DatabaseModelsLibrary import DatabaseServiceInterface import Dependencies -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary import Foundation import GRDB import ModelsLibrary diff --git a/ios/Approach/Tests/FeatureFlagsLibraryTests/FeatureFlagTests.swift b/ios/Approach/Tests/FeatureFlagsLibraryTests/FeatureFlagTests.swift deleted file mode 100644 index e46821458..000000000 --- a/ios/Approach/Tests/FeatureFlagsLibraryTests/FeatureFlagTests.swift +++ /dev/null @@ -1,64 +0,0 @@ -@testable import FeatureFlagsLibrary -import XCTest - -final class FeatureFlagTests: XCTestCase { - func testDisabledRolloutStage() { - let lesserThan: [FeatureFlag.RolloutStage] = [] - let greaterThan: [FeatureFlag.RolloutStage] = [.development, .release, .test] - - for stage in FeatureFlag.RolloutStage.allCases { - if lesserThan.contains(stage) { - XCTAssertTrue(stage < .disabled) - } else if greaterThan.contains(stage) { - XCTAssertTrue(stage > .disabled) - } else { - XCTAssertEqual(stage, .disabled) - } - } - } - - func testDevelopmentRolloutStage() { - let lesserThan: [FeatureFlag.RolloutStage] = [.disabled] - let greaterThan: [FeatureFlag.RolloutStage] = [.release, .test] - - for stage in FeatureFlag.RolloutStage.allCases { - if lesserThan.contains(stage) { - XCTAssertTrue(stage < .development) - } else if greaterThan.contains(stage) { - XCTAssertTrue(stage > .development) - } else { - XCTAssertEqual(stage, .development) - } - } - } - - func testTestRolloutStage() { - let lesserThan: [FeatureFlag.RolloutStage] = [.disabled, .development] - let greaterThan: [FeatureFlag.RolloutStage] = [.release] - - for stage in FeatureFlag.RolloutStage.allCases { - if lesserThan.contains(stage) { - XCTAssertTrue(stage < .test) - } else if greaterThan.contains(stage) { - XCTAssertTrue(stage > .test) - } else { - XCTAssertEqual(stage, .test) - } - } - } - - func testReleaseRolloutStage() { - let lesserThan: [FeatureFlag.RolloutStage] = [.disabled, .development, .test] - let greaterThan: [FeatureFlag.RolloutStage] = [] - - for stage in FeatureFlag.RolloutStage.allCases { - if lesserThan.contains(stage) { - XCTAssertTrue(stage < .release) - } else if greaterThan.contains(stage) { - XCTAssertTrue(stage > .release) - } else { - XCTAssertEqual(stage, .release) - } - } - } -} diff --git a/ios/Approach/Tests/FeatureFlagsServiceTests/FeatureFlagsServiceTests.swift b/ios/Approach/Tests/FeatureFlagsServiceTests/FeatureFlagsServiceTests.swift deleted file mode 100644 index e8b920add..000000000 --- a/ios/Approach/Tests/FeatureFlagsServiceTests/FeatureFlagsServiceTests.swift +++ /dev/null @@ -1,239 +0,0 @@ -import Dependencies -@testable import FeatureFlagsLibrary -@testable import FeatureFlagsService -@testable import FeatureFlagsServiceInterface -import PreferenceServiceInterface -import XCTest - -final class FeatureFlagsServiceTests: XCTestCase { - let testQueue = DispatchQueue(label: "TestQueue") - - override func invokeTest() { - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - $0[PreferenceService.self].setBool = { @Sendable _, _ in } - $0[PreferenceService.self].remove = { @Sendable _ in } - $0.featureFlagsQueue = testQueue - } operation: { - super.invokeTest() - } - } - - override func tearDown() { - testQueue.sync { } - } - - func testIsFlagEnabledWhenEnabled() { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - } operation: { - featureFlags = .liveValue - } - - let enabledFlag = FeatureFlag(name: "Test", introduced: "", stage: .release) - XCTAssertTrue(featureFlags.isEnabled(enabledFlag)) - } - - func testIsFlagEnabledWhenDisabled() { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - } operation: { - featureFlags = .liveValue - } - - let disabledFlag = FeatureFlag(name: "Test", introduced: "", stage: .disabled) - XCTAssertFalse(featureFlags.isEnabled(disabledFlag)) - } - - func testAllEnabledWhenAllEnabled() { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - } operation: { - featureFlags = .liveValue - } - - let flags: [FeatureFlag] = [ - .init(name: "Test1", introduced: "", stage: .release), - .init(name: "Test2", introduced: "", stage: .release), - .init(name: "Test3", introduced: "", stage: .release), - ] - - XCTAssertTrue(featureFlags.allEnabled(flags)) - } - - func testAllEnabledWhenSomeEnabled() { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - } operation: { - featureFlags = .liveValue - } - - let flags: [FeatureFlag] = [ - .init(name: "Test1", introduced: "", stage: .release), - .init(name: "Test2", introduced: "", stage: .disabled), - .init(name: "Test3", introduced: "", stage: .release), - ] - - XCTAssertFalse(featureFlags.allEnabled(flags)) - } - - func testAllEnabledWhenNoneEnabled() { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - } operation: { - featureFlags = .liveValue - } - - let flags: [FeatureFlag] = [ - .init(name: "Test1", introduced: "", stage: .disabled), - .init(name: "Test2", introduced: "", stage: .disabled), - .init(name: "Test3", introduced: "", stage: .disabled), - ] - - XCTAssertFalse(featureFlags.allEnabled(flags)) - } - - func testObserveFlagReceivesChanges() async { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - $0[PreferenceService.self].setBool = { @Sendable _, _ in } - } operation: { - featureFlags = .liveValue - } - - let flag = FeatureFlag(name: "Test", introduced: "", stage: .release) - - var observations = featureFlags.observe(flag).makeAsyncIterator() - - let firstObservation = await observations.next() - XCTAssertTrue(firstObservation == true) - - featureFlags.setEnabled(flag, false) - - let secondObservation = await observations.next() - XCTAssertTrue(secondObservation == false) - } - - func testObserveFlagDoesNotReceiveUnrelatedChanges() async { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - $0[PreferenceService.self].setBool = { @Sendable _, _ in } - } operation: { - featureFlags = .liveValue - } - - let flag = FeatureFlag(name: "Test", introduced: "", stage: .release) - let flag2 = FeatureFlag(name: "Test2", introduced: "", stage: .release) - - var observations = featureFlags.observe(flag).makeAsyncIterator() - - let firstObservation = await observations.next() - XCTAssertTrue(firstObservation == true) - - featureFlags.setEnabled(flag2, false) - featureFlags.setEnabled(flag, true) - - let secondObservation = await observations.next() - XCTAssertTrue(secondObservation == true) - } - - func testObserveAllFlagsReceivesAllChanges() async { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - $0[PreferenceService.self].setBool = { @Sendable _, _ in } - } operation: { - featureFlags = .liveValue - } - - let flags: [FeatureFlag] = [ - .init(name: "Test1", introduced: "", stage: .release), - .init(name: "Test2", introduced: "", stage: .release), - .init(name: "Test3", introduced: "", stage: .release), - ] - - var observations = featureFlags.observeAll(flags).makeAsyncIterator() - - let firstObservation = await observations.next() - XCTAssertEqual(firstObservation, [flags[0]: true, flags[1]: true, flags[2]: true]) - - featureFlags.setEnabled(flags[0], false) - featureFlags.setEnabled(flags[2], false) - - let secondObservation = await observations.next() - XCTAssertEqual(secondObservation, [flags[0]: false, flags[1]: true, flags[2]: true]) - - let thirdObservation = await observations.next() - XCTAssertEqual(thirdObservation, [flags[0]: false, flags[1]: true, flags[2]: false]) - } - - func testOverridingFlagPublishesNotification() { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - $0[PreferenceService.self].setBool = { @Sendable _, _ in } - } operation: { - featureFlags = .liveValue - } - - let flag = FeatureFlag(name: "Test", introduced: "", stage: .release) - - let expectation = self.expectation(description: "notification published") - let cancellable = NotificationCenter.default - .publisher(for: .FeatureFlag.didChange) - .sink { notification in - XCTAssertEqual(notification.object as? FeatureFlag, flag) - expectation.fulfill() - } - - featureFlags.setEnabled(flag, true) - - waitForExpectations(timeout: 1) - cancellable.cancel() - } - - func testSetOverride() { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - $0[PreferenceService.self].setBool = { @Sendable _, _ in } - } operation: { - featureFlags = .liveValue - } - - let flag = FeatureFlag(name: "Test", introduced: "", stage: .release) - - XCTAssertTrue(featureFlags.isEnabled(flag)) - featureFlags.setEnabled(flag, false) - XCTAssertFalse(featureFlags.isEnabled(flag)) - featureFlags.setEnabled(flag, true) - XCTAssertTrue(featureFlags.isEnabled(flag)) - featureFlags.setEnabled(flag, nil) - XCTAssertTrue(featureFlags.isEnabled(flag)) - } - - func testResetOverrides() { - var featureFlags: FeatureFlagsService! - withDependencies { - $0[PreferenceService.self].getBool = { @Sendable _ in false } - $0[PreferenceService.self].setBool = { @Sendable _, _ in } - } operation: { - featureFlags = .liveValue - } - - let flag = FeatureFlag(name: "Test", introduced: "", stage: .release) - - XCTAssertTrue(featureFlags.isEnabled(flag)) - featureFlags.setEnabled(flag, false) - XCTAssertFalse(featureFlags.isEnabled(flag)) - featureFlags.resetOverrides() - XCTAssertTrue(featureFlags.isEnabled(flag)) - } -} diff --git a/ios/Approach/Tests/StatisticsRepositoryTests/StatisticsRepository+TrackableFilterSourceTests.swift b/ios/Approach/Tests/StatisticsRepositoryTests/StatisticsRepository+TrackableFilterSourceTests.swift index 42a59d4cd..ca6bc739f 100644 --- a/ios/Approach/Tests/StatisticsRepositoryTests/StatisticsRepository+TrackableFilterSourceTests.swift +++ b/ios/Approach/Tests/StatisticsRepositoryTests/StatisticsRepository+TrackableFilterSourceTests.swift @@ -1,6 +1,6 @@ import DatabaseServiceInterface import Dependencies -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary @testable import ModelsLibrary import PreferenceServiceInterface @testable import StatisticsLibrary diff --git a/ios/ApproachIOS/Source/ContentView.swift b/ios/ApproachIOS/Source/ContentView.swift index fdfbbd41b..5e69cd61d 100644 --- a/ios/ApproachIOS/Source/ContentView.swift +++ b/ios/ApproachIOS/Source/ContentView.swift @@ -1,6 +1,6 @@ import AppFeature import ComposableArchitecture -import FeatureFlagsServiceInterface +import FeatureFlagsLibrary import SwiftUI #if DEBUG