From a1104163f33f9e097161e63f42e4a90761c5e27c Mon Sep 17 00:00:00 2001 From: Michal Fousek Date: Fri, 24 Feb 2023 17:33:46 +0100 Subject: [PATCH] [#554] Add WalletConfigProvider (#574) - Added `WalletConfigProvider` object which handles loading and caching of feature flags. - Added `WalletConfig` which represents one configuration of all feature flags. --- .swiftlint.yml | 1 + secant.xcodeproj/project.pbxproj | 96 ++++++++++--- .../UserDefaultsWalletConfigStorage.swift | 75 ++++++++++ .../WalletConfigProvider.swift | 78 +++++++++++ .../WalletConfigProviderInterface.swift | 20 +++ .../WalletConfigProviderLiveKey.swift | 24 ++++ .../WalletConfigProviderTestKey.swift | 22 +++ secant/Models/WalletConfig.swift | 34 +++++ .../WalletConfigProviderTests.swift | 129 ++++++++++++++++++ 9 files changed, 459 insertions(+), 20 deletions(-) create mode 100644 secant/Dependencies/FeatureFlagsManager/UserDefaultsWalletConfigStorage.swift create mode 100644 secant/Dependencies/FeatureFlagsManager/WalletConfigProvider.swift create mode 100644 secant/Dependencies/FeatureFlagsManager/WalletConfigProviderInterface.swift create mode 100644 secant/Dependencies/FeatureFlagsManager/WalletConfigProviderLiveKey.swift create mode 100644 secant/Dependencies/FeatureFlagsManager/WalletConfigProviderTestKey.swift create mode 100644 secant/Models/WalletConfig.swift create mode 100644 secantTests/WalletConfigProvider/WalletConfigProviderTests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 83dfacb..c7412ac 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -13,6 +13,7 @@ disabled_rules: - nesting # allow for types to be nested, common pattern in Swift - multiple_closures_with_trailing_closure - generic_type_name # allow for arbitrarily long generic type names + - empty_parentheses_with_trailing_closure opt_in_rules: - mark diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index b4c6c54..955a996 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -330,6 +330,19 @@ 34E0AF0F28DEE4C70034CF37 /* HoldToSendButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E0AF0E28DEE4C70034CF37 /* HoldToSendButton.swift */; }; 34E0AF1128DEE5220034CF37 /* Wedge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E0AF1028DEE5220034CF37 /* Wedge.swift */; }; 34E5F2F328E46DB700C17E5F /* DiskSpaceChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E5F2F228E46DB700C17E5F /* DiskSpaceChecker.swift */; }; + 34F682E529A75EB60022C079 /* WalletConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682E429A75EB60022C079 /* WalletConfig.swift */; }; + 34F682E629A75EB60022C079 /* WalletConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682E429A75EB60022C079 /* WalletConfig.swift */; }; + 34F682EC29A763FD0022C079 /* WalletConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682EB29A763FD0022C079 /* WalletConfigProvider.swift */; }; + 34F682ED29A763FD0022C079 /* WalletConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682EB29A763FD0022C079 /* WalletConfigProvider.swift */; }; + 34F682EF29A7640A0022C079 /* WalletConfigProviderInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682EE29A7640A0022C079 /* WalletConfigProviderInterface.swift */; }; + 34F682F029A7640A0022C079 /* WalletConfigProviderInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682EE29A7640A0022C079 /* WalletConfigProviderInterface.swift */; }; + 34F682F229A764120022C079 /* WalletConfigProviderLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682F129A764120022C079 /* WalletConfigProviderLiveKey.swift */; }; + 34F682F329A764120022C079 /* WalletConfigProviderLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682F129A764120022C079 /* WalletConfigProviderLiveKey.swift */; }; + 34F682F529A7641B0022C079 /* WalletConfigProviderTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682F429A7641B0022C079 /* WalletConfigProviderTestKey.swift */; }; + 34F682F629A7641B0022C079 /* WalletConfigProviderTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682F429A7641B0022C079 /* WalletConfigProviderTestKey.swift */; }; + 34F682F829A775C10022C079 /* UserDefaultsWalletConfigStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682F729A775C10022C079 /* UserDefaultsWalletConfigStorage.swift */; }; + 34F682F929A775C10022C079 /* UserDefaultsWalletConfigStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682F729A775C10022C079 /* UserDefaultsWalletConfigStorage.swift */; }; + 34F682FC29A784660022C079 /* WalletConfigProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F682FB29A784660022C079 /* WalletConfigProviderTests.swift */; }; 660558E9270C7A54009D6954 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 660558E8270C7A54009D6954 /* Colors.xcassets */; }; 660558F7270C862F009D6954 /* Fonts+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660558F5270C862F009D6954 /* Fonts+Generated.swift */; }; 660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660558F6270C862F009D6954 /* XCAssets+Generated.swift */; }; @@ -644,6 +657,13 @@ 34E0AF0E28DEE4C70034CF37 /* HoldToSendButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldToSendButton.swift; sourceTree = ""; }; 34E0AF1028DEE5220034CF37 /* Wedge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wedge.swift; sourceTree = ""; }; 34E5F2F228E46DB700C17E5F /* DiskSpaceChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskSpaceChecker.swift; sourceTree = ""; }; + 34F682E429A75EB60022C079 /* WalletConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConfig.swift; sourceTree = ""; }; + 34F682EB29A763FD0022C079 /* WalletConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConfigProvider.swift; sourceTree = ""; }; + 34F682EE29A7640A0022C079 /* WalletConfigProviderInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConfigProviderInterface.swift; sourceTree = ""; }; + 34F682F129A764120022C079 /* WalletConfigProviderLiveKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConfigProviderLiveKey.swift; sourceTree = ""; }; + 34F682F429A7641B0022C079 /* WalletConfigProviderTestKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConfigProviderTestKey.swift; sourceTree = ""; }; + 34F682F729A775C10022C079 /* UserDefaultsWalletConfigStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsWalletConfigStorage.swift; sourceTree = ""; }; + 34F682FB29A784660022C079 /* WalletConfigProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConfigProviderTests.swift; sourceTree = ""; }; 660558E8270C7A54009D6954 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; 660558F5270C862F009D6954 /* Fonts+Generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fonts+Generated.swift"; sourceTree = ""; }; 660558F6270C862F009D6954 /* XCAssets+Generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCAssets+Generated.swift"; sourceTree = ""; }; @@ -983,26 +1003,27 @@ 0D4E7A1926B364180058B01E /* secantTests */ = { isa = PBXGroup; children = ( - 9E391162284E3ECF0073DD9A /* SnapshotTests */, + 0D4E7A1C26B364180058B01E /* Info.plist */, + 0D4E7A1A26B364180058B01E /* secantTests.swift */, 9E207C372966EF6E003E2C9B /* AddressDetailsTests */, + 0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */, 9E94C61E28AA7DD5008256E9 /* BalanceBreakdownTests */, - 9E6713EF2897F80A00A6796F /* MultiLineTextFieldTests */, - 9E7CB6222874245400A02233 /* ProfileTests */, 9EAB4674285B5C68002904A0 /* DeeplinkTests */, + 34F682FA29A784580022C079 /* WalletConfigProvider */, 9E3911372848AD3A0073DD9A /* HomeTests */, 9E391122283E4C970073DD9A /* ImportWalletTests */, - 9E612C7729913F2300D09B09 /* SensitiveDataTests */, - 9E66129C2889388C00C75B70 /* SettingsTests */, + 9E6713EF2897F80A00A6796F /* MultiLineTextFieldTests */, + 6654C7422715A48E00901167 /* OnboardingTests */, + 9E7CB6222874245400A02233 /* ProfileTests */, + 0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */, + 9EAFEB802805791400199FC9 /* RootTests */, 9E01F8262833CD84000EFC57 /* ScanTests */, 9E5BF642281FEC8700BA3F17 /* SendTests */, - 9E5BF63D281953F900BA3F17 /* WalletEventsTests */, - 9EAFEB802805791400199FC9 /* RootTests */, + 9E612C7729913F2300D09B09 /* SensitiveDataTests */, + 9E66129C2889388C00C75B70 /* SettingsTests */, + 9E391162284E3ECF0073DD9A /* SnapshotTests */, 9EF8135927ECC25E0075AF48 /* UtilTests */, - 0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */, - 0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */, - 6654C7422715A48E00901167 /* OnboardingTests */, - 0D4E7A1A26B364180058B01E /* secantTests.swift */, - 0D4E7A1C26B364180058B01E /* Info.plist */, + 9E5BF63D281953F900BA3F17 /* WalletEventsTests */, ); path = secantTests; sourceTree = ""; @@ -1060,15 +1081,16 @@ isa = PBXGroup; children = ( 9EF8135F27F043CC0075AF48 /* AppDelegate.swift */, - 9EF8139B27F47AED0075AF48 /* InitializationState.swift */, 0D6D628A276A528D002FB4CC /* DropDelegate.swift */, - 9E5BF63B2818305D00BA3F17 /* TransactionState.swift */, + 34F682E429A75EB60022C079 /* WalletConfig.swift */, + 9EF8139B27F47AED0075AF48 /* InitializationState.swift */, 9E7FE0D6282D286500C374E8 /* RecoveryPhrase.swift */, - 9E7FE0DC282D298900C374E8 /* ValidationWord.swift */, 9E612C7C2991476F00D09B09 /* SensitiveData.swift */, 9E7FE0E5282E7B1100C374E8 /* StoredWallet.swift */, - 9EAB46772860A1D2002904A0 /* WalletEvent.swift */, 9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */, + 9E5BF63B2818305D00BA3F17 /* TransactionState.swift */, + 9E7FE0DC282D298900C374E8 /* ValidationWord.swift */, + 9EAB46772860A1D2002904A0 /* WalletEvent.swift */, ); path = Models; sourceTree = ""; @@ -1167,6 +1189,26 @@ path = CheckCircle; sourceTree = ""; }; + 34F682EA29A763F00022C079 /* FeatureFlagsManager */ = { + isa = PBXGroup; + children = ( + 34F682EB29A763FD0022C079 /* WalletConfigProvider.swift */, + 34F682F729A775C10022C079 /* UserDefaultsWalletConfigStorage.swift */, + 34F682EE29A7640A0022C079 /* WalletConfigProviderInterface.swift */, + 34F682F129A764120022C079 /* WalletConfigProviderLiveKey.swift */, + 34F682F429A7641B0022C079 /* WalletConfigProviderTestKey.swift */, + ); + path = FeatureFlagsManager; + sourceTree = ""; + }; + 34F682FA29A784580022C079 /* WalletConfigProvider */ = { + isa = PBXGroup; + children = ( + 34F682FB29A784660022C079 /* WalletConfigProviderTests.swift */, + ); + path = WalletConfigProvider; + sourceTree = ""; + }; 663FAB9A271D873300E495F8 /* Buttons */ = { isa = PBXGroup; children = ( @@ -1602,6 +1644,7 @@ 9EBDF959291E654F000A1A05 /* Deeplink */, 9EBDF971291F79C9000A1A05 /* DerivationTool */, 9EBDF945291D759B000A1A05 /* DiskSpaceChecker */, + 34F682EA29A763F00022C079 /* FeatureFlagsManager */, 9EB863882922CC0E003D0F8B /* FeedbackGenerator */, 9EB863B52923C4ED003D0F8B /* FileManager */, 9EBDF981291F91B1000A1A05 /* LocalAuthentication */, @@ -1990,13 +2033,13 @@ 9EF8135927ECC25E0075AF48 /* UtilTests */ = { isa = PBXGroup; children = ( - 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */, + 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */, + 9E0F574A2980260D005304FA /* LoggerTests.swift */, 9EAFEB852805A23100199FC9 /* SecItemClientTests.swift */, 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */, - 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */, - 9E39112D283F91600073DD9A /* ZatoshiTests.swift */, 0DB4E0B02881F2DB00947B78 /* WalletBalance+testing.swift */, - 9E0F574A2980260D005304FA /* LoggerTests.swift */, + 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */, + 9E39112D283F91600073DD9A /* ZatoshiTests.swift */, ); path = UtilTests; sourceTree = ""; @@ -2510,6 +2553,7 @@ files = ( 0D26AE9B299E8196005260EE /* OnboardingFlowView.swift in Sources */, 0D26AE9C299E8196005260EE /* ZcashBadge.swift in Sources */, + 34F682F329A764120022C079 /* WalletConfigProviderLiveKey.swift in Sources */, 0D26AE9D299E8196005260EE /* CrashReporterTestKey.swift in Sources */, 0D26AE9E299E8196005260EE /* DerivationToolInterface.swift in Sources */, 0D26AE9F299E8196005260EE /* XCAssets+Generated.swift in Sources */, @@ -2526,6 +2570,7 @@ 0D26AEAA299E8196005260EE /* CrashReporterLiveKey.swift in Sources */, 0D26AEAB299E8196005260EE /* RecoveryPhraseRandomizerLiveKey.swift in Sources */, 0D26AEAC299E8196005260EE /* TCATextField.swift in Sources */, + 34F682F029A7640A0022C079 /* WalletConfigProviderInterface.swift in Sources */, 0D26AEAD299E8196005260EE /* DeeplinkInterface.swift in Sources */, 0D26AEAE299E8196005260EE /* DerivationToolTestKey.swift in Sources */, 0D26AEAF299E8196005260EE /* TransactionAmountTextFieldStore.swift in Sources */, @@ -2533,6 +2578,7 @@ 0D26AEB1299E8196005260EE /* AppDelegate.swift in Sources */, 0D26AEB2299E8196005260EE /* LogsHandlerInterface.swift in Sources */, 0D26AEB3299E8196005260EE /* DeeplinkTestKey.swift in Sources */, + 34F682F929A775C10022C079 /* UserDefaultsWalletConfigStorage.swift in Sources */, 0D26AEB4299E8196005260EE /* Wedge.swift in Sources */, 0D26AEB5299E8196005260EE /* TransactionDetailView.swift in Sources */, 0D26AEB6299E8196005260EE /* NumberFormatterLiveKey.swift in Sources */, @@ -2613,6 +2659,7 @@ 0D26AF01299E8196005260EE /* RecoveryPhraseDisplayView.swift in Sources */, 0D26AF02299E8196005260EE /* URIParser.swift in Sources */, 0D26AF03299E8196005260EE /* URIParserLive.swift in Sources */, + 34F682ED29A763FD0022C079 /* WalletConfigProvider.swift in Sources */, 0D26AF04299E8196005260EE /* LocalAuthenticationTestKey.swift in Sources */, 0D26AF05299E8196005260EE /* ScanView.swift in Sources */, 0D26AF06299E8196005260EE /* DatabaseFiles.swift in Sources */, @@ -2636,6 +2683,7 @@ 0D26AF18299E8196005260EE /* WalletEventsFlowStore.swift in Sources */, 0D26AF19299E8196005260EE /* StoredWallet.swift in Sources */, 0D26AF1A299E8196005260EE /* UserDefaultsInterface.swift in Sources */, + 34F682E629A75EB60022C079 /* WalletConfig.swift in Sources */, 0D26AF1B299E8196005260EE /* HomeStore.swift in Sources */, 0D26AF1C299E8196005260EE /* AppVersionMocks.swift in Sources */, 0D26AF1D299E8196005260EE /* RequestView.swift in Sources */, @@ -2664,6 +2712,7 @@ 0D26AF34299E8196005260EE /* SecItemInterface.swift in Sources */, 0D26AF35299E8196005260EE /* WalletInfoStore.swift in Sources */, 0D26AF36299E8196005260EE /* DatabaseFilesTestKey.swift in Sources */, + 34F682F629A7641B0022C079 /* WalletConfigProviderTestKey.swift in Sources */, 0D26AF37299E8196005260EE /* ScanUIView.swift in Sources */, 0D26AF38299E8196005260EE /* AppVersionLiveKey.swift in Sources */, 0D26AF39299E8196005260EE /* ColoredChip.swift in Sources */, @@ -2730,6 +2779,7 @@ files = ( 2EB660E02747EAB900A06A07 /* OnboardingFlowView.swift in Sources */, 9E7FE0DF282D2DD600C374E8 /* ZcashBadge.swift in Sources */, + 34F682F229A764120022C079 /* WalletConfigProviderLiveKey.swift in Sources */, 0D261040298C406F00CC9DE9 /* CrashReporterTestKey.swift in Sources */, 9EBDF975291F79F9000A1A05 /* DerivationToolInterface.swift in Sources */, 660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */, @@ -2746,6 +2796,7 @@ 0D26103E298C3FA600CC9DE9 /* CrashReporterLiveKey.swift in Sources */, 9EB863A829239DCB003D0F8B /* RecoveryPhraseRandomizerLiveKey.swift in Sources */, 2EDA07A027EDE18C00D6F09B /* TCATextField.swift in Sources */, + 34F682EF29A7640A0022C079 /* WalletConfigProviderInterface.swift in Sources */, 9EBDF961291E657B000A1A05 /* DeeplinkInterface.swift in Sources */, 9EBDF977291F79F9000A1A05 /* DerivationToolTestKey.swift in Sources */, 2EB7758727FC67FD00269373 /* TransactionAmountTextFieldStore.swift in Sources */, @@ -2753,6 +2804,7 @@ 9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */, 9E612C7229880E9200D09B09 /* LogsHandlerInterface.swift in Sources */, 9EBDF960291E657B000A1A05 /* DeeplinkTestKey.swift in Sources */, + 34F682F829A775C10022C079 /* UserDefaultsWalletConfigStorage.swift in Sources */, 34E0AF1128DEE5220034CF37 /* Wedge.swift in Sources */, F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */, 9EB863942922D036003D0F8B /* NumberFormatterLiveKey.swift in Sources */, @@ -2833,6 +2885,7 @@ 0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */, 9EB863A2292398A8003D0F8B /* URIParser.swift in Sources */, 9EB863C12923C779003D0F8B /* URIParserLive.swift in Sources */, + 34F682EC29A763FD0022C079 /* WalletConfigProvider.swift in Sources */, 9EBDF987291F91EF000A1A05 /* LocalAuthenticationTestKey.swift in Sources */, F9971A5F27680DF600A2DB75 /* ScanView.swift in Sources */, 9E39114C2848EEB90073DD9A /* DatabaseFiles.swift in Sources */, @@ -2856,6 +2909,7 @@ F96B41E7273B501F0021B49A /* WalletEventsFlowStore.swift in Sources */, 9E7FE0E6282E7B1100C374E8 /* StoredWallet.swift in Sources */, 9E153A7629216EFB00112F41 /* UserDefaultsInterface.swift in Sources */, + 34F682E529A75EB60022C079 /* WalletConfig.swift in Sources */, 9EAFEB9128081E9400199FC9 /* HomeStore.swift in Sources */, 9EBDF980291F8261000A1A05 /* AppVersionMocks.swift in Sources */, F9971A5A27680DDE00A2DB75 /* RequestView.swift in Sources */, @@ -2884,6 +2938,7 @@ 9EAFEB84280597B700199FC9 /* SecItemInterface.swift in Sources */, F9971A6B27680E1000A2DB75 /* WalletInfoStore.swift in Sources */, 9EBDF953291E5E86000A1A05 /* DatabaseFilesTestKey.swift in Sources */, + 34F682F529A7641B0022C079 /* WalletConfigProviderTestKey.swift in Sources */, 9E7FE0F628327F6F00C374E8 /* ScanUIView.swift in Sources */, 9EBDF97D291F7EB0000A1A05 /* AppVersionLiveKey.swift in Sources */, 0D185819272723FF0046B928 /* ColoredChip.swift in Sources */, @@ -2991,6 +3046,7 @@ 9E02B56C27FED475005B809B /* DatabaseFilesTests.swift in Sources */, 9E612C7929913F3600D09B09 /* SensitiveDataTests.swift in Sources */, 9EF8135D27ECC25E0075AF48 /* UserPreferencesStorageTests.swift in Sources */, + 34F682FC29A784660022C079 /* WalletConfigProviderTests.swift in Sources */, 9E94C62028AA7DEE008256E9 /* BalanceBreakdownTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/secant/Dependencies/FeatureFlagsManager/UserDefaultsWalletConfigStorage.swift b/secant/Dependencies/FeatureFlagsManager/UserDefaultsWalletConfigStorage.swift new file mode 100644 index 0000000..6018e7e --- /dev/null +++ b/secant/Dependencies/FeatureFlagsManager/UserDefaultsWalletConfigStorage.swift @@ -0,0 +1,75 @@ +// +// UserDefaultsWalletConfigStorage.swift +// secant +// +// Created by Michal Fousek on 23.02.2023. +// + +import Foundation + +typealias UserDefaultsWalletConfigProvider = UserDefaultsWalletConfigStorage +typealias UserDefaultsWalletConfigProviderCache = UserDefaultsWalletConfigStorage + +struct UserDefaultsWalletConfigStorage { + private let userDefaults = UserDefaults.standard + + enum InternalError: Error { + case noValueStored + case unableToDeserializeData + } + + enum Constants { + static let providerKey = "feature_flags_ud_config_provider" + static let cacheKey = "feature_flags_ud_config_cache" + } + + private func load(key: String) async throws -> WalletConfig { + guard let data = userDefaults.data(forKey: key) else { throw InternalError.noValueStored } + do { + let rawFlags = try PropertyListDecoder().decode(WalletConfig.RawFlags.self, from: data) + return WalletConfig(flags: rawFlags) + } catch { + LoggerProxy.debug("Error when deocding feature flags from user defaults: \(error)") + throw InternalError.unableToDeserializeData + } + } + + private func store(flags: WalletConfig.RawFlags, key: String) async { + do { + let data = try PropertyListEncoder().encode(flags) + userDefaults.set(data, forKey: key) + } catch { + LoggerProxy.debug("Can't store/encode feature flags when updating user defaults: \(error)") + } + } + + // This is used only in debug menu to change configuration for specific flag + func store(featureFlag: FeatureFlag, isEnabled: Bool) async { + let currentConfig = (try? await load(key: Constants.providerKey)) ?? WalletConfig.default + var rawFlags = currentConfig.flags + rawFlags[featureFlag] = isEnabled + + await store(flags: rawFlags, key: Constants.providerKey) + } +} + +extension UserDefaultsWalletConfigStorage: WalletConfigSourceProvider { + func load() async throws -> WalletConfig { + return try await load(key: Constants.providerKey) + } +} + +extension UserDefaultsWalletConfigStorage: WalletConfigProviderCache { + func load() async -> WalletConfig? { + do { + return try await load(key: Constants.cacheKey) + } catch { + LoggerProxy.debug("Can't load feature flags from cache: \(error)") + return nil + } + } + + func store(_ configuration: WalletConfig) async { + await store(flags: configuration.flags, key: Constants.cacheKey) + } +} diff --git a/secant/Dependencies/FeatureFlagsManager/WalletConfigProvider.swift b/secant/Dependencies/FeatureFlagsManager/WalletConfigProvider.swift new file mode 100644 index 0000000..765af0d --- /dev/null +++ b/secant/Dependencies/FeatureFlagsManager/WalletConfigProvider.swift @@ -0,0 +1,78 @@ +// +// WalletConfigProvider.swift +// secant +// +// Created by Michal Fousek on 23.02.2023. +// + +import Foundation + +struct WalletConfigProvider { + /// Objects that fetches flags configuration from some source. It can be fetched from user defaults or some backend API for example. It depends + /// on implementation. + private let configSourceProvider: WalletConfigSourceProvider + /// Object that caches provided flags configuration. + private let cache: WalletConfigProviderCache + + init(configSourceProvider: WalletConfigSourceProvider, cache: WalletConfigProviderCache) { + self.configSourceProvider = configSourceProvider + self.cache = cache + } + + /// Loads flags configuration. + /// + /// First `configurationProvider` is used to fetch flags configuration. If that fails then `cache` is used to load flags configuration. And if + /// that fails `WalletConfig.default` is used. + /// + /// Loaded configuration is merged with with `WalletConfig.default` to be sure that all recognized flags are always returned in + /// configuration. + /// + /// Merged configuration is stored in cache. + func load() async -> WalletConfig { + let configuration: WalletConfig + do { + configuration = try await configSourceProvider.load() + } catch { + LoggerProxy.debug("Error when loading feature flags from configuration provider: \(error)") + if let cachedConfiguration = await cache.load() { + configuration = cachedConfiguration + } else { + configuration = WalletConfig.default + } + } + + let finalConfiguration = merge(configuration: configuration, withDefaultConfiguration: WalletConfig.default) + + await cache.store(finalConfiguration) + + return finalConfiguration + } + + // This is used only in debug menu to change configuration for specific flag + func update(featureFlag: FeatureFlag, isEnabled: Bool) async { + guard let provider = configSourceProvider as? UserDefaultsWalletConfigStorage else { + LoggerProxy.debug("This is now only support with UserDefaultsWalletConfigStorage as configurationProvider.") + return + } + + await provider.store(featureFlag: featureFlag, isEnabled: isEnabled) + } + + private func merge( + configuration: WalletConfig, + withDefaultConfiguration defaultConfiguration: WalletConfig + ) -> WalletConfig { + var rawDefaultFlags = defaultConfiguration.flags + rawDefaultFlags.merge(configuration.flags, uniquingKeysWith: { $1 }) + return WalletConfig(flags: rawDefaultFlags) + } +} + +protocol WalletConfigSourceProvider { + func load() async throws -> WalletConfig +} + +protocol WalletConfigProviderCache { + func load() async -> WalletConfig? + func store(_ configuration: WalletConfig) async +} diff --git a/secant/Dependencies/FeatureFlagsManager/WalletConfigProviderInterface.swift b/secant/Dependencies/FeatureFlagsManager/WalletConfigProviderInterface.swift new file mode 100644 index 0000000..e3f02d2 --- /dev/null +++ b/secant/Dependencies/FeatureFlagsManager/WalletConfigProviderInterface.swift @@ -0,0 +1,20 @@ +// +// WalletConfigProviderInterface.swift +// secant +// +// Created by Michal Fousek on 23.02.2023. +// + +import ComposableArchitecture +import Foundation + +extension DependencyValues { + var walletConfigProvider: WalletConfigProviderClient { + get { self[WalletConfigProviderClient.self] } + set { self[WalletConfigProviderClient.self] = newValue } + } +} + +struct WalletConfigProviderClient { + let load: () async -> WalletConfig +} diff --git a/secant/Dependencies/FeatureFlagsManager/WalletConfigProviderLiveKey.swift b/secant/Dependencies/FeatureFlagsManager/WalletConfigProviderLiveKey.swift new file mode 100644 index 0000000..36fce6f --- /dev/null +++ b/secant/Dependencies/FeatureFlagsManager/WalletConfigProviderLiveKey.swift @@ -0,0 +1,24 @@ +// +// WalletConfigProviderLiveKey.swift +// secant +// +// Created by Michal Fousek on 23.02.2023. +// + +import ComposableArchitecture +import Foundation + +extension WalletConfigProviderClient: DependencyKey { + static let liveValue = WalletConfigProviderClient.live() + + private static var defaultWalletConfigProvider: WalletConfigProvider { + WalletConfigProvider( + configSourceProvider: UserDefaultsWalletConfigProvider(), + cache: UserDefaultsWalletConfigProviderCache() + ) + } + + static func live(walletConfigProvider: WalletConfigProvider = WalletConfigProviderClient.defaultWalletConfigProvider) -> Self { + Self(load: { return await walletConfigProvider.load() }) + } +} diff --git a/secant/Dependencies/FeatureFlagsManager/WalletConfigProviderTestKey.swift b/secant/Dependencies/FeatureFlagsManager/WalletConfigProviderTestKey.swift new file mode 100644 index 0000000..4079034 --- /dev/null +++ b/secant/Dependencies/FeatureFlagsManager/WalletConfigProviderTestKey.swift @@ -0,0 +1,22 @@ +// +// WalletConfigProviderTestKey.swift +// secant +// +// Created by Michal Fousek on 23.02.2023. +// + +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay + +extension WalletConfigProviderClient: TestDependencyKey { + static let testValue = Self( + load: XCTUnimplemented("\(Self.self).load", placeholder: WalletConfig.default) + ) +} + +extension WalletConfigProviderClient { + static let `default` = Self( + load: { WalletConfig.default } + ) +} diff --git a/secant/Models/WalletConfig.swift b/secant/Models/WalletConfig.swift new file mode 100644 index 0000000..bda3fce --- /dev/null +++ b/secant/Models/WalletConfig.swift @@ -0,0 +1,34 @@ +// +// WalletConfig.swift +// secant +// +// Created by Michal Fousek on 23.02.2023. +// + +enum FeatureFlag: String, CaseIterable, Codable { + // These two flags should stay here because those are used in tests. It's not super nice but there is probably no other way. + case testFlag1 + case testFlag2 + + var enabledByDefault: Bool { + switch self { + case .testFlag1, .testFlag2: + return false + } + } +} + +struct WalletConfig: Equatable { + typealias RawFlags = [FeatureFlag: Bool] + + let flags: RawFlags + + func isEnabled(_ featureFlag: FeatureFlag) -> Bool { + return flags[featureFlag, default: false] + } + + static var `default`: WalletConfig = { + let defaultSettings = FeatureFlag.allCases.map { ($0, $0.enabledByDefault) } + return WalletConfig(flags: Dictionary(uniqueKeysWithValues: defaultSettings)) + }() +} diff --git a/secantTests/WalletConfigProvider/WalletConfigProviderTests.swift b/secantTests/WalletConfigProvider/WalletConfigProviderTests.swift new file mode 100644 index 0000000..447b1bc --- /dev/null +++ b/secantTests/WalletConfigProvider/WalletConfigProviderTests.swift @@ -0,0 +1,129 @@ +// +// WalletConfigProviderTests.swift +// secantTests +// +// Created by Michal Fousek on 23.02.2023. +// + +import XCTest +@testable import secant_testnet + +class WalletConfigProviderTests: XCTestCase { + override func setUp() { + super.setUp() + + UserDefaultsWalletConfigStorage().clearAll() + } + + func testLoadFlagsFromProvider() async { + XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag1)) + XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag2)) + + let provider = WalletConfigSourceProviderMock() { + return WalletConfig(flags: [.testFlag1: true, .testFlag2: false]) + } + + let manager = WalletConfigProvider(configSourceProvider: provider, cache: UserDefaultsWalletConfigStorage()) + let configuration = await manager.load() + + XCTAssertTrue(configuration.isEnabled(.testFlag1)) + XCTAssertFalse(configuration.isEnabled(.testFlag2)) + } + + 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]) + + let manager = WalletConfigProvider(configSourceProvider: provider, cache: cache) + let configuration = await manager.load() + + XCTAssertFalse(configuration.isEnabled(.testFlag1)) + XCTAssertTrue(configuration.isEnabled(.testFlag2)) + } + + 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)) + } + + func testAllTheFlagsAreAlwaysReturned() async { + XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag1)) + XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag2)) + + let provider = WalletConfigSourceProviderMock() { + return WalletConfig(flags: [.testFlag1: true]) + } + + let manager = WalletConfigProvider(configSourceProvider: provider, cache: UserDefaultsWalletConfigStorage()) + let configuration = await manager.load() + + XCTAssertTrue(configuration.isEnabled(.testFlag1)) + XCTAssertFalse(configuration.isEnabled(.testFlag2)) + } + + func testProvidedFlagsAreCached() async { + XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag1)) + XCTAssertFalse(WalletConfig.default.isEnabled(.testFlag2)) + + let provider = WalletConfigSourceProviderMock() { + return WalletConfig(flags: [.testFlag1: true, .testFlag2: false]) + } + let cache: WalletConfigProviderCache = UserDefaultsWalletConfigProviderCache() + + let manager = WalletConfigProvider(configSourceProvider: provider, cache: cache) + _ = await manager.load() + + guard let cachedConfiguration = await cache.load() else { + return XCTFail("No cached configuration.") + } + + XCTAssertTrue(cachedConfiguration.isEnabled(.testFlag1)) + XCTAssertFalse(cachedConfiguration.isEnabled(.testFlag2)) + } +} + +struct WalletConfigSourceProviderMock: WalletConfigSourceProvider { + let provider: () async throws -> WalletConfig + + func load() async throws -> WalletConfig { + return try await provider() + } +} + +class WalletConfigProviderCacheMock: WalletConfigProviderCache { + var cachedFlags: WalletConfig.RawFlags = [:] + + init(cachedFlags: WalletConfig.RawFlags) { + self.cachedFlags = cachedFlags + } + + func load() async -> WalletConfig? { + guard !cachedFlags.isEmpty else { return nil } + return WalletConfig(flags: cachedFlags) + } + + func store(_ configuration: WalletConfig) async { + cachedFlags = configuration.flags + } +} + +extension UserDefaultsWalletConfigStorage { + func clearAll() { + let userdef = UserDefaults.standard + userdef.removeObject(forKey: Constants.cacheKey) + userdef.removeObject(forKey: Constants.providerKey) + userdef.synchronize() + } +}