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
This commit is contained in:
Lukas Korba 2022-03-18 14:09:10 +01:00
parent 7bcb9c5596
commit 1635ee81ce
3 changed files with 216 additions and 11 deletions

View File

@ -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 = "<group>"; };
9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeumorphicDesignModifier.swift; sourceTree = "<group>"; };
9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantButtonStyles.swift; sourceTree = "<group>"; };
9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = "<group>"; };
9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseTestPreambleView.swift; sourceTree = "<group>"; };
9EF8135A27ECC25E0075AF48 /* RecoveryPhraseStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseStorageTests.swift; sourceTree = "<group>"; };
9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorageTests.swift; sourceTree = "<group>"; };
F9322DBF273B555C00C105B5 /* NavigationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationLinks.swift; sourceTree = "<group>"; };
F93673D52742CB840099C6AF /* Previews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Previews.swift; sourceTree = "<group>"; };
F93874ED273C4DE200F0E875 /* HomeStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = "<group>"; };
@ -363,6 +368,7 @@
0D4E7A1926B364180058B01E /* secantTests */,
0D4E7A2426B364180058B01E /* secantUITests */,
0D4E7A0626B364170058B01E /* Products */,
9EF8135627ECAFF50075AF48 /* Recovered References */,
);
sourceTree = "<group>";
};
@ -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 = "<group>";
@ -657,14 +664,6 @@
path = CircularFrame;
sourceTree = "<group>";
};
9E2AC10427DA34450042AA47 /* Util */ = {
isa = PBXGroup;
children = (
9E2AC10527DA34610042AA47 /* RecoveryPhraseStorageTests.swift */,
);
path = Util;
sourceTree = "<group>";
};
9E2DF99727CF704D00649636 /* ImportWallet */ = {
isa = PBXGroup;
children = (
@ -691,6 +690,23 @@
path = Preamble;
sourceTree = "<group>";
};
9EF8135627ECAFF50075AF48 /* Recovered References */ = {
isa = PBXGroup;
children = (
9E2AC10527DA34610042AA47 /* RecoveryPhraseStorageTests.swift */,
);
name = "Recovered References";
sourceTree = "<group>";
};
9EF8135927ECC25E0075AF48 /* Util */ = {
isa = PBXGroup;
children = (
9EF8135A27ECC25E0075AF48 /* RecoveryPhraseStorageTests.swift */,
9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */,
);
path = Util;
sourceTree = "<group>";
};
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;
};

View File

@ -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<Value>(forKey: String, default defaultIfNil: Value) -> Value {
userDefaults.object(forKey: forKey) as? Value ?? defaultIfNil
}
func setValue<Value>(_ value: Value, forKey: String) {
userDefaults.set(value, forKey: forKey)
userDefaults.synchronize()
}
}

View File

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