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