[#554] Add ability to update feature flags from debug screen (#583)

This commit is contained in:
Michal Fousek 2023-02-27 17:26:24 +01:00 committed by GitHub
parent 56e1364659
commit 148ca941f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 110 additions and 42 deletions

View File

@ -17,4 +17,5 @@ extension DependencyValues {
struct WalletConfigProviderClient {
let load: () async -> WalletConfig
let update: (FeatureFlag, Bool) async -> Void
}

View File

@ -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)
}
)
}
}

View File

@ -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 }
)
}

View File

@ -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
}

View File

@ -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<RootReducer.State, RootReducer.Action> {
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
}
}

View File

@ -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

View File

@ -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)
}
)
}
}
}
}
}

View File

@ -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))
}()
}

View File

@ -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])
}