From 1635ee81ce047b117b33dc8bf962648dd85b4fbb Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Fri, 18 Mar 2022 14:09:10 +0100 Subject: [PATCH] draft of the storage Initial user preferences storage refactor of one comment handling defaults singleton removed support for the defaults alphabetical sort standard user defaults set by default unit tests added for-each to cover all cases project fix --- secant.xcodeproj/project.pbxproj | 40 ++++-- secant/Util/UserPreferencesStorage.swift | 115 ++++++++++++++++++ .../Util/UserPreferencesStorageTests.swift | 72 +++++++++++ 3 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 secant/Util/UserPreferencesStorage.swift create mode 100644 secantTests/Util/UserPreferencesStorageTests.swift diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 6c1da61..301857e 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -82,14 +82,16 @@ 9E2AC0FF27D8EC120042AA47 /* MnemonicSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9E2AC0FE27D8EC120042AA47 /* MnemonicSwift */; }; 9E2AC10127D8EF0B0042AA47 /* MnemonicSeedPhraseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2AC10027D8EF0B0042AA47 /* MnemonicSeedPhraseProvider.swift */; }; 9E2AC10327DA28200042AA47 /* RecoveryPhraseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2AC10227DA28200042AA47 /* RecoveryPhraseStorage.swift */; }; - 9E2AC10627DA34610042AA47 /* RecoveryPhraseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2AC10527DA34610042AA47 /* RecoveryPhraseStorageTests.swift */; }; 9E2DF99C27CF704D00649636 /* ImportWalletStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99827CF704D00649636 /* ImportWalletStore.swift */; }; 9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */; }; 9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99B27CF704D00649636 /* ImportWalletView.swift */; }; 9E37A2B827C8F59F00AE57B3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9E37A2B727C8F59F00AE57B3 /* Localizable.strings */; }; 9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */; }; 9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */; }; + 9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; }; 9EBEF87A27CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */; }; + 9EF8135C27ECC25E0075AF48 /* RecoveryPhraseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135A27ECC25E0075AF48 /* RecoveryPhraseStorageTests.swift */; }; + 9EF8135D27ECC25E0075AF48 /* UserPreferencesStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */; }; F9322DC0273B555C00C105B5 /* NavigationLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9322DBF273B555C00C105B5 /* NavigationLinks.swift */; }; F93673D62742CB840099C6AF /* Previews.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93673D52742CB840099C6AF /* Previews.swift */; }; F93874F0273C4DE200F0E875 /* HomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93874ED273C4DE200F0E875 /* HomeStore.swift */; }; @@ -223,7 +225,10 @@ 9E37A2B727C8F59F00AE57B3 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeumorphicDesignModifier.swift; sourceTree = ""; }; 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantButtonStyles.swift; sourceTree = ""; }; + 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = ""; }; 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseTestPreambleView.swift; sourceTree = ""; }; + 9EF8135A27ECC25E0075AF48 /* RecoveryPhraseStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseStorageTests.swift; sourceTree = ""; }; + 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorageTests.swift; sourceTree = ""; }; F9322DBF273B555C00C105B5 /* NavigationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationLinks.swift; sourceTree = ""; }; F93673D52742CB840099C6AF /* Previews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Previews.swift; sourceTree = ""; }; F93874ED273C4DE200F0E875 /* HomeStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = ""; }; @@ -363,6 +368,7 @@ 0D4E7A1926B364180058B01E /* secantTests */, 0D4E7A2426B364180058B01E /* secantUITests */, 0D4E7A0626B364170058B01E /* Products */, + 9EF8135627ECAFF50075AF48 /* Recovered References */, ); sourceTree = ""; }; @@ -411,7 +417,7 @@ 0D4E7A1926B364180058B01E /* secantTests */ = { isa = PBXGroup; children = ( - 9E2AC10427DA34450042AA47 /* Util */, + 9EF8135927ECC25E0075AF48 /* Util */, 0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */, 0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */, 6654C7422715A48E00901167 /* OnboardingTests */, @@ -504,6 +510,7 @@ 0D35CC45277A36E00074316A /* ScrollableWhenScaled.swift */, 9E2AC10027D8EF0B0042AA47 /* MnemonicSeedPhraseProvider.swift */, 9E2AC10227DA28200042AA47 /* RecoveryPhraseStorage.swift */, + 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */, ); path = Util; sourceTree = ""; @@ -657,14 +664,6 @@ path = CircularFrame; sourceTree = ""; }; - 9E2AC10427DA34450042AA47 /* Util */ = { - isa = PBXGroup; - children = ( - 9E2AC10527DA34610042AA47 /* RecoveryPhraseStorageTests.swift */, - ); - path = Util; - sourceTree = ""; - }; 9E2DF99727CF704D00649636 /* ImportWallet */ = { isa = PBXGroup; children = ( @@ -691,6 +690,23 @@ path = Preamble; sourceTree = ""; }; + 9EF8135627ECAFF50075AF48 /* Recovered References */ = { + isa = PBXGroup; + children = ( + 9E2AC10527DA34610042AA47 /* RecoveryPhraseStorageTests.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; + 9EF8135927ECC25E0075AF48 /* Util */ = { + isa = PBXGroup; + children = ( + 9EF8135A27ECC25E0075AF48 /* RecoveryPhraseStorageTests.swift */, + 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */, + ); + path = Util; + sourceTree = ""; + }; F93874EC273C4DE200F0E875 /* Home */ = { isa = PBXGroup; children = ( @@ -1053,6 +1069,7 @@ 0D35CC46277A36E00074316A /* ScrollableWhenScaled.swift in Sources */, F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */, 669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */, + 9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */, F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */, 663FABA2271D876C00E495F8 /* SecondaryButton.swift in Sources */, 0DC487C32772574C00BE6A63 /* ValidationSucceededView.swift in Sources */, @@ -1140,9 +1157,10 @@ 0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */, 6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */, 0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */, - 9E2AC10627DA34610042AA47 /* RecoveryPhraseStorageTests.swift in Sources */, 0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */, 0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */, + 9EF8135C27ECC25E0075AF48 /* RecoveryPhraseStorageTests.swift in Sources */, + 9EF8135D27ECC25E0075AF48 /* UserPreferencesStorageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/secant/Util/UserPreferencesStorage.swift b/secant/Util/UserPreferencesStorage.swift new file mode 100644 index 0000000..480ef70 --- /dev/null +++ b/secant/Util/UserPreferencesStorage.swift @@ -0,0 +1,115 @@ +// +// UserPreferencesStorage.swift +// secant-testnet +// +// Created by Lukáš Korba on 03/18/2022. +// + +import Foundation + +/// Representation of the user preferences stored in the local persistent storage (non-encrypted, no security needed) +protocol UserPreferences { + /// From when the app is on and uninterrupted + var activeAppSessionFrom: TimeInterval { get set } + /// What is the set up currency + var currency: String { get set } + /// Whether the fiat conversion is on/off + var isFiatConverted: Bool { get set } + /// Whether user finished recovery phrase backup test + var isRecoveryPhraseTestCompleted: Bool { get set } + /// Whether the user has been autoshielded in the running session + var isSessionAutoshielded: Bool { get set } +} + +/// Live implementation of the `UserPreferences` using User Defaults +/// according to https://developer.apple.com/documentation/foundation/userdefaults +/// the UserDefaults class is thread-safe. +struct UserPreferencesStorage: UserPreferences { + enum Constants: String, CaseIterable { + case zcashActiveAppSessionFrom + case zcashCurrency + case zcashFiatConverted + case zcashRecoveryPhraseTestCompleted + case zcashSessionAutoshielded + } + + static let `default` = UserPreferencesStorage( + appSessionFrom: Date().timeIntervalSince1970, + convertedCurrency: "USD", + fiatConvertion: true, + recoveryPhraseTestCompleted: false, + sessionAutoshielded: true, + userDefaults: UserDefaults.standard + ) + + /// Default values for all preferences in case there is no value stored (counterparts to `Constants`) + private let appSessionFrom: TimeInterval + private let convertedCurrency: String + private let fiatConvertion: Bool + private let recoveryPhraseTestCompleted: Bool + private let sessionAutoshielded: Bool + + private let userDefaults: UserDefaults + + init( + appSessionFrom: TimeInterval, + convertedCurrency: String, + fiatConvertion: Bool, + recoveryPhraseTestCompleted: Bool, + sessionAutoshielded: Bool, + userDefaults: UserDefaults + ) { + self.appSessionFrom = appSessionFrom + self.convertedCurrency = convertedCurrency + self.fiatConvertion = fiatConvertion + self.recoveryPhraseTestCompleted = recoveryPhraseTestCompleted + self.sessionAutoshielded = sessionAutoshielded + self.userDefaults = userDefaults + } + + /// From when the app is on and uninterrupted + var activeAppSessionFrom: TimeInterval { + get { getValue(forKey: Constants.zcashActiveAppSessionFrom.rawValue, default: appSessionFrom) } + set { setValue(newValue, forKey: Constants.zcashActiveAppSessionFrom.rawValue) } + } + + /// What is the set up currency + var currency: String { + get { getValue(forKey: Constants.zcashCurrency.rawValue, default: convertedCurrency) } + set { setValue(newValue, forKey: Constants.zcashCurrency.rawValue) } + } + + /// Whether the fiat conversion is on/off + var isFiatConverted: Bool { + get { getValue(forKey: Constants.zcashFiatConverted.rawValue, default: fiatConvertion) } + set { setValue(newValue, forKey: Constants.zcashFiatConverted.rawValue) } + } + + /// Whether user finished recovery phrase backup test + var isRecoveryPhraseTestCompleted: Bool { + get { getValue(forKey: Constants.zcashRecoveryPhraseTestCompleted.rawValue, default: recoveryPhraseTestCompleted) } + set { setValue(newValue, forKey: Constants.zcashRecoveryPhraseTestCompleted.rawValue) } + } + + /// Whether the user has been autoshielded in the running session + var isSessionAutoshielded: Bool { + get { getValue(forKey: Constants.zcashSessionAutoshielded.rawValue, default: sessionAutoshielded) } + set { setValue(newValue, forKey: Constants.zcashSessionAutoshielded.rawValue) } + } + + /// Use carefully: Deletes all user preferences from the User Defaults + func removeAll() { + Constants.allCases.forEach { userDefaults.removeObject(forKey: $0.rawValue) } + } +} + +private extension UserPreferencesStorage { + func getValue(forKey: String, default defaultIfNil: Value) -> Value { + userDefaults.object(forKey: forKey) as? Value ?? defaultIfNil + } + + func setValue(_ value: Value, forKey: String) { + userDefaults.set(value, forKey: forKey) + userDefaults.synchronize() + } +} diff --git a/secantTests/Util/UserPreferencesStorageTests.swift b/secantTests/Util/UserPreferencesStorageTests.swift new file mode 100644 index 0000000..e874d6e --- /dev/null +++ b/secantTests/Util/UserPreferencesStorageTests.swift @@ -0,0 +1,72 @@ +// +// UserPreferencesStorageTests.swift +// secantTests +// +// Created by Lukáš Korba on 22.03.2022. +// + +import XCTest +@testable import secant_testnet + +class UserPreferencesStorageTests: XCTestCase { + // swiftlint:disable:next implicitly_unwrapped_optional + var storage: UserPreferencesStorage! + + override func setUp() { + super.setUp() + storage = UserPreferencesStorage( + appSessionFrom: 12345678.0, + convertedCurrency: "USD", + fiatConvertion: true, + recoveryPhraseTestCompleted: true, + sessionAutoshielded: false, + userDefaults: .standard + ) + storage.removeAll() + } + + override func tearDown() { + super.tearDown() + storage = nil + } + + func testAppSessionFrom_defaultValue() throws { + XCTAssertEqual(12345678.0, storage.activeAppSessionFrom, "User Preferences: `activeAppSessionFrom` default doesn't match.") + } + + func testConvertedCurrency_defaultValue() throws { + XCTAssertEqual("USD", storage.currency, "User Preferences: `currency` default doesn't match.") + } + + func testFiatConvertion_defaultValue() throws { + XCTAssertEqual(true, storage.isFiatConverted, "User Preferences: `isFiatConverted` default doesn't match.") + } + + func testRecoveryPhraseTestCompleted_defaultValue() throws { + XCTAssertEqual(true, storage.isRecoveryPhraseTestCompleted, "User Preferences: `isRecoveryPhraseTestCompleted` default doesn't match.") + } + + func testSessionAutoshielded_defaultValue() throws { + XCTAssertEqual(false, storage.isSessionAutoshielded, "User Preferences: `isSessionAutoshielded` default doesn't match.") + } + + func testRemoveAll() throws { + let userDefaults = UserDefaults.standard + + // fill in the data + UserPreferencesStorage.Constants.allCases.forEach { + userDefaults.set("anyValue", forKey: $0.rawValue) + } + + // remove it + storage?.removeAll() + + // check the presence + UserPreferencesStorage.Constants.allCases.forEach { + XCTAssertNil( + userDefaults.object(forKey: $0.rawValue), + "User Preferences: key \($0.rawValue) should be removed but it's still present in User Defaults" + ) + } + } +}