From 148ca941f1c02048d3619d7ec910154bb17b30d8 Mon Sep 17 00:00:00 2001 From: Michal Fousek Date: Mon, 27 Feb 2023 17:26:24 +0100 Subject: [PATCH] [#554] Add ability to update feature flags from debug screen (#583) --- .../WalletConfigProviderInterface.swift | 1 + .../WalletConfigProviderLiveKey.swift | 7 +- .../WalletConfigProviderTestKey.swift | 6 +- secant/Features/Root/RootDestination.swift | 2 +- secant/Features/Root/RootInitialization.swift | 24 +++++- secant/Features/Root/RootStore.swift | 1 + secant/Features/Root/RootView.swift | 85 ++++++++++++++----- secant/Models/WalletConfig.swift | 5 +- .../WalletConfigProviderTests.swift | 21 ++--- 9 files changed, 110 insertions(+), 42 deletions(-) diff --git a/secant/Dependencies/WalletConfigProvider/WalletConfigProviderInterface.swift b/secant/Dependencies/WalletConfigProvider/WalletConfigProviderInterface.swift index e3f02d2..260c94c 100644 --- a/secant/Dependencies/WalletConfigProvider/WalletConfigProviderInterface.swift +++ b/secant/Dependencies/WalletConfigProvider/WalletConfigProviderInterface.swift @@ -17,4 +17,5 @@ extension DependencyValues { struct WalletConfigProviderClient { let load: () async -> WalletConfig + let update: (FeatureFlag, Bool) async -> Void } diff --git a/secant/Dependencies/WalletConfigProvider/WalletConfigProviderLiveKey.swift b/secant/Dependencies/WalletConfigProvider/WalletConfigProviderLiveKey.swift index 36fce6f..f083954 100644 --- a/secant/Dependencies/WalletConfigProvider/WalletConfigProviderLiveKey.swift +++ b/secant/Dependencies/WalletConfigProvider/WalletConfigProviderLiveKey.swift @@ -19,6 +19,11 @@ extension WalletConfigProviderClient: DependencyKey { } static func live(walletConfigProvider: WalletConfigProvider = WalletConfigProviderClient.defaultWalletConfigProvider) -> Self { - Self(load: { return await walletConfigProvider.load() }) + Self( + load: { return await walletConfigProvider.load() }, + update: { flag, isEnabled in + await walletConfigProvider.update(featureFlag: flag, isEnabled: isEnabled) + } + ) } } diff --git a/secant/Dependencies/WalletConfigProvider/WalletConfigProviderTestKey.swift b/secant/Dependencies/WalletConfigProvider/WalletConfigProviderTestKey.swift index 1727e83..dcc18aa 100644 --- a/secant/Dependencies/WalletConfigProvider/WalletConfigProviderTestKey.swift +++ b/secant/Dependencies/WalletConfigProvider/WalletConfigProviderTestKey.swift @@ -10,12 +10,14 @@ import XCTestDynamicOverlay extension WalletConfigProviderClient: TestDependencyKey { static let testValue = Self( - load: XCTUnimplemented("\(Self.self).load", placeholder: WalletConfig.default) + load: XCTUnimplemented("\(Self.self).load", placeholder: WalletConfig.default), + update: XCTUnimplemented("\(Self.self).update") ) } extension WalletConfigProviderClient { static let noOp = Self( - load: { WalletConfig.default } + load: { WalletConfig.default }, + update: { _, _ in } ) } diff --git a/secant/Features/Root/RootDestination.swift b/secant/Features/Root/RootDestination.swift index 326fd50..e191eb7 100644 --- a/secant/Features/Root/RootDestination.swift +++ b/secant/Features/Root/RootDestination.swift @@ -112,7 +112,7 @@ extension RootReducer { } return EffectTask(value: .destination(.deeplink(url))) - case .home, .initialization, .onboarding, .phraseDisplay, .phraseValidation, .sandbox, .welcome: + case .home, .initialization, .onboarding, .phraseDisplay, .phraseValidation, .sandbox, .welcome, .debug: return .none } diff --git a/secant/Features/Root/RootInitialization.swift b/secant/Features/Root/RootInitialization.swift index 7ee3a4c..24ebc28 100644 --- a/secant/Features/Root/RootInitialization.swift +++ b/secant/Features/Root/RootInitialization.swift @@ -25,6 +25,11 @@ extension RootReducer { case walletConfigChanged(WalletConfig) } + enum DebugAction: Equatable { + case updateFlag(FeatureFlag, Bool) + case flagUpdated(WalletConfig) + } + // swiftlint:disable:next cyclomatic_complexity function_body_length func initializationReduce() -> Reduce { Reduce { state, action in @@ -45,8 +50,7 @@ extension RootReducer { } case .initialization(.walletConfigChanged(let walletConfig)): - state.walletConfig = walletConfig - state.onboardingState.walletConfig = walletConfig + updateStateAfterConfigUpdate(state: &state, config: walletConfig) return EffectTask(value: .initialization(.initialSetups)) case .initialization(.initialSetups): @@ -227,7 +231,23 @@ extension RootReducer { !userStoredPreferences.isUserOptedOutOfCrashReporting() ) return .none + + case let .debug(.updateFlag(flag, isEnabled)): + return .run { send in + await walletConfigProvider.update(flag, !isEnabled) + let walletConfig = await walletConfigProvider.load() + await send(.debug(.flagUpdated(walletConfig))) + } + + case let .debug(.flagUpdated(walletConfig)): + updateStateAfterConfigUpdate(state: &state, config: walletConfig) + return .none } } } + + private func updateStateAfterConfigUpdate(state: inout RootReducer.State, config: WalletConfig) { + state.walletConfig = config + state.onboardingState.walletConfig = config + } } diff --git a/secant/Features/Root/RootStore.swift b/secant/Features/Root/RootStore.swift index 8a7570b..68403a9 100644 --- a/secant/Features/Root/RootStore.swift +++ b/secant/Features/Root/RootStore.swift @@ -29,6 +29,7 @@ struct RootReducer: ReducerProtocol { case phraseValidation(RecoveryPhraseValidationFlowReducer.Action) case sandbox(SandboxReducer.Action) case welcome(WelcomeReducer.Action) + case debug(DebugAction) } @Dependency(\.crashReporter) var crashReporter diff --git a/secant/Features/Root/RootView.swift b/secant/Features/Root/RootView.swift index eae646d..be2bbe8 100644 --- a/secant/Features/Root/RootView.swift +++ b/secant/Features/Root/RootView.swift @@ -83,28 +83,75 @@ struct RootView: View { } } +private struct FeatureFlagWrapper: Identifiable, Equatable, Comparable { + let name: FeatureFlag + let isEnabled: Bool + var id: String { name.rawValue } + + static func < (lhs: FeatureFlagWrapper, rhs: FeatureFlagWrapper) -> Bool { + lhs.name.rawValue < rhs.name.rawValue + } + + static func == (lhs: FeatureFlagWrapper, rhs: FeatureFlagWrapper) -> Bool { + lhs.name.rawValue == rhs.name.rawValue + } +} + private extension RootView { @ViewBuilder func debugView(_ viewStore: RootViewStore) -> some View { - List { - Section(header: Text("Navigation Stack Destinations")) { - Button("Go To Sandbox (navigation proof)") { - viewStore.goToDestination(.sandbox) + VStack(alignment: .leading) { + Button("Back") { + viewStore.goToDestination(.home) + } + .navigationButtonStyle + .frame(width: 75, height: 40, alignment: .leading) + .padding() + + List { + Section(header: Text("Navigation Stack Destinations")) { + Button("Go To Sandbox (navigation proof)") { + viewStore.goToDestination(.sandbox) + } + + Button("Go To Onboarding") { + viewStore.goToDestination(.onboarding) + } + + Button("Go To Phrase Validation Demo") { + viewStore.goToDestination(.phraseValidation) + } + + Button("Restart the app") { + viewStore.goToDestination(.welcome) + } + + Button("[Be careful] Nuke Wallet") { + viewStore.send(.initialization(.nukeWallet)) + } } - - Button("Go To Onboarding") { - viewStore.goToDestination(.onboarding) - } - - Button("Go To Phrase Validation Demo") { - viewStore.goToDestination(.phraseValidation) - } - - Button("Restart the app") { - viewStore.goToDestination(.welcome) - } - - Button("[Be careful] Nuke Wallet") { - viewStore.send(.initialization(.nukeWallet)) + + Section(header: Text("Feature flags")) { + let flags = viewStore.state.walletConfig.flags + .map { FeatureFlagWrapper(name: $0.key, isEnabled: $0.value) } + .sorted() + + ForEach(flags) { flag in + HStack { + Toggle( + isOn: Binding( + get: { flag.isEnabled }, + set: { _ in + viewStore.send(.debug(.updateFlag(flag.name, flag.isEnabled))) + } + ), + label: { + Text(flag.name.rawValue) + .foregroundColor(flag.isEnabled ? .green : .red) + .frame(maxWidth: .infinity, alignment: .leading) + } + ) + } + } } } } diff --git a/secant/Models/WalletConfig.swift b/secant/Models/WalletConfig.swift index 7856ab2..fc2b8bf 100644 --- a/secant/Models/WalletConfig.swift +++ b/secant/Models/WalletConfig.swift @@ -29,7 +29,10 @@ struct WalletConfig: Equatable { } static var `default`: WalletConfig = { - let defaultSettings = FeatureFlag.allCases.map { ($0, $0.enabledByDefault) } + let defaultSettings = FeatureFlag.allCases + .filter { $0 != .testFlag1 && $0 != .testFlag2 } + .map { ($0, $0.enabledByDefault) } + return WalletConfig(flags: Dictionary(uniqueKeysWithValues: defaultSettings)) }() } diff --git a/secantTests/WalletConfigProviderTests/WalletConfigProviderTests.swift b/secantTests/WalletConfigProviderTests/WalletConfigProviderTests.swift index 447b1bc..cac68ed 100644 --- a/secantTests/WalletConfigProviderTests/WalletConfigProviderTests.swift +++ b/secantTests/WalletConfigProviderTests/WalletConfigProviderTests.swift @@ -11,14 +11,15 @@ import XCTest class WalletConfigProviderTests: XCTestCase { override func setUp() { super.setUp() - UserDefaultsWalletConfigStorage().clearAll() } - func testLoadFlagsFromProvider() async { + func testTestFlagsAreDisabledByDefault() { XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag1)) XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag2)) + } + func testLoadFlagsFromProvider() async { let provider = WalletConfigSourceProviderMock() { return WalletConfig(flags: [.testFlag1: true, .testFlag2: false]) } @@ -31,9 +32,6 @@ class WalletConfigProviderTests: XCTestCase { } func testLoadFlagsFromCache() async { - XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag1)) - XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag2)) - let provider = WalletConfigSourceProviderMock() { throw NSError(domain: "whatever", code: 21) } let cache = WalletConfigProviderCacheMock(cachedFlags: [.testFlag1: false, .testFlag2: true]) @@ -45,23 +43,17 @@ class WalletConfigProviderTests: XCTestCase { } func testLoadDefaultFlags() async { - XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag1)) - XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag2)) - let provider = WalletConfigSourceProviderMock() { throw NSError(domain: "whatever", code: 21) } let cache = WalletConfigProviderCacheMock(cachedFlags: [:]) let manager = WalletConfigProvider(configSourceProvider: provider, cache: cache) let configuration = await manager.load() - XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag1)) - XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag2)) + XCTAssertFalse(configuration.isEnabled(.testFlag1)) + XCTAssertFalse(configuration.isEnabled(.testFlag2)) } func testAllTheFlagsAreAlwaysReturned() async { - XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag1)) - XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag2)) - let provider = WalletConfigSourceProviderMock() { return WalletConfig(flags: [.testFlag1: true]) } @@ -74,9 +66,6 @@ class WalletConfigProviderTests: XCTestCase { } func testProvidedFlagsAreCached() async { - XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag1)) - XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag2)) - let provider = WalletConfigSourceProviderMock() { return WalletConfig(flags: [.testFlag1: true, .testFlag2: false]) }