From 558675aced4ac490a0e2996b4810b045dad089fa Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Wed, 17 Aug 2022 09:12:15 +0200 Subject: [PATCH] [408] Reduce dependency on TCA in the dependencies (#413) Following dependencies refactored to await/async - RecoveryPhraseValidationFlowReducer - RecoveryPhraseDisplayStore - AppStore - UserPreferencesStorage - WrappedUserDefaults which allowed to reduce imports of Combine/TCA --- .../Dependencies/UserPreferencesStorage.swift | 39 +++++----- secant/Features/App/AppStore.swift | 22 +++--- .../RecoveryPhraseDisplayStore.swift | 24 +++--- .../RecoveryPhraseValidationFlowStore.swift | 3 - secant/Wrappers/WrappedUserDefaults.swift | 29 +++---- .../RecoveryPhraseDisplayReducerTests.swift | 2 +- secantTests/DeeplinkTests/DeeplinkTests.swift | 26 ++++--- .../RecoveryPhraseValidationTests.swift | 1 - .../UserPreferencesStorageTests.swift | 75 +++++++------------ 9 files changed, 97 insertions(+), 124 deletions(-) diff --git a/secant/Dependencies/UserPreferencesStorage.swift b/secant/Dependencies/UserPreferencesStorage.swift index d3d21988..6bdaaa40 100644 --- a/secant/Dependencies/UserPreferencesStorage.swift +++ b/secant/Dependencies/UserPreferencesStorage.swift @@ -6,7 +6,6 @@ // import Foundation -import ComposableArchitecture /// Live implementation of the `UserPreferences` using User Defaults /// according to https://developer.apple.com/documentation/foundation/userdefaults @@ -50,8 +49,8 @@ struct UserPreferencesStorage { getValue(forKey: Constants.zcashActiveAppSessionFrom.rawValue, default: appSessionFrom) } - func setActiveAppSessionFrom(_ timeInterval: TimeInterval) -> Effect { - setValue(timeInterval, forKey: Constants.zcashActiveAppSessionFrom.rawValue) + func setActiveAppSessionFrom(_ timeInterval: TimeInterval) async { + await setValue(timeInterval, forKey: Constants.zcashActiveAppSessionFrom.rawValue) } /// What is the set up currency @@ -59,8 +58,8 @@ struct UserPreferencesStorage { getValue(forKey: Constants.zcashCurrency.rawValue, default: convertedCurrency) } - func setCurrency(_ string: String) -> Effect { - setValue(string, forKey: Constants.zcashCurrency.rawValue) + func setCurrency(_ string: String) async { + await setValue(string, forKey: Constants.zcashCurrency.rawValue) } /// Whether the fiat conversion is on/off @@ -68,8 +67,8 @@ struct UserPreferencesStorage { getValue(forKey: Constants.zcashFiatConverted.rawValue, default: fiatConvertion) } - func setIsFiatConverted(_ bool: Bool) -> Effect { - setValue(bool, forKey: Constants.zcashFiatConverted.rawValue) + func setIsFiatConverted(_ bool: Bool) async { + await setValue(bool, forKey: Constants.zcashFiatConverted.rawValue) } /// Whether user finished recovery phrase backup test @@ -77,8 +76,8 @@ struct UserPreferencesStorage { getValue(forKey: Constants.zcashRecoveryPhraseTestCompleted.rawValue, default: recoveryPhraseTestCompleted) } - func setIsRecoveryPhraseTestCompleted(_ bool: Bool) -> Effect { - setValue(bool, forKey: Constants.zcashRecoveryPhraseTestCompleted.rawValue) + func setIsRecoveryPhraseTestCompleted(_ bool: Bool) async { + await setValue(bool, forKey: Constants.zcashRecoveryPhraseTestCompleted.rawValue) } /// Whether the user has been autoshielded in the running session @@ -86,17 +85,15 @@ struct UserPreferencesStorage { getValue(forKey: Constants.zcashSessionAutoshielded.rawValue, default: sessionAutoshielded) } - func setIsSessionAutoshielded(_ bool: Bool) -> Effect { - setValue(bool, forKey: Constants.zcashSessionAutoshielded.rawValue) + func setIsSessionAutoshielded(_ bool: Bool) async { + await setValue(bool, forKey: Constants.zcashSessionAutoshielded.rawValue) } /// Use carefully: Deletes all user preferences from the User Defaults - func removeAll() -> Effect { - var removals: [Effect] = [] - - Constants.allCases.forEach { removals.append(userDefaults.remove($0.rawValue)) } - - return Effect.concatenate(removals) + func removeAll() async { + for key in Constants.allCases { + await userDefaults.remove(key.rawValue) + } } } @@ -105,11 +102,9 @@ private extension UserPreferencesStorage { userDefaults.objectForKey(forKey) as? Value ?? defaultIfNil } - func setValue(_ value: Value, forKey: String) -> Effect { - let effect = userDefaults.setValue(value, forKey) - _ = userDefaults.synchronize() - - return effect + func setValue(_ value: Value, forKey: String) async { + await userDefaults.setValue(value, forKey) + _ = await userDefaults.synchronize() } } diff --git a/secant/Features/App/AppStore.swift b/secant/Features/App/AppStore.swift index 18c4f054..be2d9cbc 100644 --- a/secant/Features/App/AppStore.swift +++ b/secant/Features/App/AppStore.swift @@ -322,11 +322,12 @@ extension AppReducer { // (https://github.com/zcash/secant-ios-wallet/issues/370) return .none } - do { - return try process(url: url, with: environment) - } catch { - // TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221) - return .none + return .run { send in + do { + await send(try await process(url: url, with: environment)) + } catch { + // TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221) + } } case .deeplinkHome: @@ -391,7 +392,6 @@ extension AppReducer { environment: { environment in RecoveryPhraseValidationFlowEnvironment( scheduler: environment.scheduler, - newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) }, pasteboard: .test, feedbackGenerator: .silent, recoveryPhraseRandomizer: environment.recoveryPhraseRandomizer @@ -405,7 +405,7 @@ extension AppReducer { environment: { environment in RecoveryPhraseDisplayEnvironment( scheduler: environment.scheduler, - newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) }, + newPhrase: { .init(words: RecoveryPhrase.placeholder.words) }, pasteboard: .live, feedbackGenerator: environment.feedbackGenerator ) @@ -493,15 +493,15 @@ extension AppReducer { throw SDKInitializationError.failed } } - - static func process(url: URL, with environment: AppEnvironment) throws -> Effect { + + static func process(url: URL, with environment: AppEnvironment) async throws -> AppAction { let deeplink = try environment.deeplinkHandler.resolveDeeplinkURL(url, environment.derivationTool) switch deeplink { case .home: - return Effect(value: .deeplinkHome) + return .deeplinkHome case let .send(amount, address, memo): - return Effect(value: .deeplinkSend(Zatoshi(amount), address, memo)) + return .deeplinkSend(Zatoshi(amount), address, memo) } } } diff --git a/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayStore.swift b/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayStore.swift index 11064f7f..8daeff94 100644 --- a/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayStore.swift +++ b/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayStore.swift @@ -25,14 +25,14 @@ enum RecoveryPhraseDisplayAction: Equatable { case createPhrase case copyToBufferPressed case finishedPressed - case phraseResponse(Result) + case phraseResponse(RecoveryPhrase) } // MARK: - Environment struct RecoveryPhraseDisplayEnvironment { let scheduler: AnySchedulerOf - let newPhrase: () -> Effect + let newPhrase: () async throws -> RecoveryPhrase let pasteboard: WrappedPasteboard let feedbackGenerator: WrappedFeedbackGenerator } @@ -40,7 +40,7 @@ struct RecoveryPhraseDisplayEnvironment { extension RecoveryPhraseDisplayEnvironment { static let demo = Self( scheduler: DispatchQueue.main.eraseToAnyScheduler(), - newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) }, + newPhrase: { .init(words: RecoveryPhrase.placeholder.words) }, pasteboard: .test, feedbackGenerator: .silent ) @@ -52,23 +52,27 @@ extension RecoveryPhraseDisplayReducer { static let `default` = RecoveryPhraseDisplayReducer { state, action, environment in switch action { case .createPhrase: - return environment.newPhrase() - .receive(on: environment.scheduler) - .catchToEffect(RecoveryPhraseDisplayAction.phraseResponse) + return .run { send in + do { + await send(.phraseResponse(try await environment.newPhrase())) + } catch { + // TODO: remove this when feature is implemented in https://github.com/zcash/secant-ios-wallet/issues/129 + } + } + case .copyToBufferPressed: guard let phrase = state.phrase?.toString() else { return .none } environment.pasteboard.setString(phrase) state.showCopyToBufferAlert = true return .none + case .finishedPressed: // TODO: remove this when feature is implemented in https://github.com/zcash/secant-ios-wallet/issues/47 return .none - case let .phraseResponse(.success(phrase)): + + case let .phraseResponse(phrase): state.phrase = phrase return .none - case .phraseResponse(.failure): - // TODO: remove this when feature is implemented in https://github.com/zcash/secant-ios-wallet/issues/129 - return .none } } } diff --git a/secant/Features/RecoveryPhraseValidationFlow/RecoveryPhraseValidationFlowStore.swift b/secant/Features/RecoveryPhraseValidationFlow/RecoveryPhraseValidationFlowStore.swift index cbdd5cfb..74affbfe 100644 --- a/secant/Features/RecoveryPhraseValidationFlow/RecoveryPhraseValidationFlowStore.swift +++ b/secant/Features/RecoveryPhraseValidationFlow/RecoveryPhraseValidationFlowStore.swift @@ -112,7 +112,6 @@ enum RecoveryPhraseValidationFlowAction: Equatable { struct RecoveryPhraseValidationFlowEnvironment { let scheduler: AnySchedulerOf - let newPhrase: () -> Effect let pasteboard: WrappedPasteboard let feedbackGenerator: WrappedFeedbackGenerator let recoveryPhraseRandomizer: WrappedRecoveryPhraseRandomizer @@ -121,7 +120,6 @@ struct RecoveryPhraseValidationFlowEnvironment { extension RecoveryPhraseValidationFlowEnvironment { static let demo = Self( scheduler: DispatchQueue.main.eraseToAnyScheduler(), - newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) }, pasteboard: .test, feedbackGenerator: .silent, recoveryPhraseRandomizer: .live @@ -129,7 +127,6 @@ extension RecoveryPhraseValidationFlowEnvironment { static let live = Self( scheduler: DispatchQueue.main.eraseToAnyScheduler(), - newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) }, pasteboard: .live, feedbackGenerator: .haptic, recoveryPhraseRandomizer: .live diff --git a/secant/Wrappers/WrappedUserDefaults.swift b/secant/Wrappers/WrappedUserDefaults.swift index 76103b4e..e66b8e32 100644 --- a/secant/Wrappers/WrappedUserDefaults.swift +++ b/secant/Wrappers/WrappedUserDefaults.swift @@ -6,13 +6,12 @@ // import Foundation -import ComposableArchitecture struct WrappedUserDefaults { - let objectForKey: (String) -> Any? - let remove: (String) -> Effect - let setValue: (Any?, String) -> Effect - let synchronize: () -> Bool + let objectForKey: @Sendable (String) -> Any? + let remove: @Sendable (String) async -> Void + let setValue: @Sendable (Any?, String) async -> Void + let synchronize: @Sendable () async -> Bool } extension WrappedUserDefaults { @@ -20,25 +19,17 @@ extension WrappedUserDefaults { userDefaults: UserDefaults = .standard ) -> Self { Self( - objectForKey: userDefaults.object(forKey:), - remove: { key in - .fireAndForget { - userDefaults.removeObject(forKey: key) - } - }, - setValue: { value, key in - .fireAndForget { - userDefaults.set(value, forKey: key) - } - }, - synchronize: userDefaults.synchronize + objectForKey: { userDefaults.object(forKey: $0) }, + remove: { userDefaults.removeObject(forKey: $0) }, + setValue: { userDefaults.set($0, forKey: $1) }, + synchronize: { userDefaults.synchronize() } ) } static let mock = WrappedUserDefaults( objectForKey: { _ in }, - remove: { _ in .none }, - setValue: { _, _ in .none }, + remove: { _ in }, + setValue: { _, _ in }, synchronize: { true } ) } diff --git a/secantTests/BackupFlowTests/RecoveryPhraseDisplayReducerTests.swift b/secantTests/BackupFlowTests/RecoveryPhraseDisplayReducerTests.swift index b53a0e69..3f306d5a 100644 --- a/secantTests/BackupFlowTests/RecoveryPhraseDisplayReducerTests.swift +++ b/secantTests/BackupFlowTests/RecoveryPhraseDisplayReducerTests.swift @@ -35,7 +35,7 @@ class RecoveryPhraseDisplayReducerTests: XCTestCase { environment: .demo ) - store.send(.phraseResponse(.success(.placeholder))) { + store.send(.phraseResponse(.placeholder)) { $0.phrase = .placeholder $0.showCopyToBufferAlert = false } diff --git a/secantTests/DeeplinkTests/DeeplinkTests.swift b/secantTests/DeeplinkTests/DeeplinkTests.swift index 02ce93b6..9ef242ce 100644 --- a/secantTests/DeeplinkTests/DeeplinkTests.swift +++ b/secantTests/DeeplinkTests/DeeplinkTests.swift @@ -9,8 +9,8 @@ import XCTest @testable import secant_testnet import ComposableArchitecture import ZcashLightClientKit -import Combine +@MainActor class DeeplinkTests: XCTestCase { func testActionDeeplinkHome_SameRouteLevel() throws { let testEnvironment = AppEnvironment.mock @@ -73,7 +73,7 @@ class DeeplinkTests: XCTestCase { } } - func testDeeplinkRequest_homeURL() throws { + func testDeeplinkRequest_homeURL() async throws { let synchronizer = TestWrappedSDKSynchronizer() synchronizer.updateStateChanged(.synced) @@ -107,14 +107,16 @@ class DeeplinkTests: XCTestCase { return XCTFail("Deeplink: 'testDeeplinkRequest_homeURL' URL is expected to be valid.") } - store.send(.deeplink(url)) + await store.send(.deeplink(url)) - store.receive(.deeplinkHome) { state in + await store.receive(.deeplinkHome) { state in state.route = .home } + + await store.finish() } - func testDeeplinkRequest_sendURL_amount() throws { + func testDeeplinkRequest_sendURL_amount() async throws { let synchronizer = TestWrappedSDKSynchronizer() synchronizer.updateStateChanged(.synced) @@ -148,22 +150,24 @@ class DeeplinkTests: XCTestCase { return XCTFail("Deeplink: 'testDeeplinkRequest_sendURL_amount' URL is expected to be valid.") } - store.send(.deeplink(url)) + await store.send(.deeplink(url)) let amount = Zatoshi(123_000_000) let address = "" let memo = "" - store.receive(.deeplinkSend(amount, address, memo)) { state in + await store.receive(.deeplinkSend(amount, address, memo)) { state in state.route = .home state.homeState.route = .send state.homeState.sendState.amount = amount state.homeState.sendState.address = address state.homeState.sendState.memoState.text = memo } + + await store.finish() } - func testDeeplinkRequest_sendURL_allFields() throws { + func testDeeplinkRequest_sendURL_allFields() async throws { let synchronizer = TestWrappedSDKSynchronizer() synchronizer.updateStateChanged(.synced) @@ -197,18 +201,20 @@ class DeeplinkTests: XCTestCase { return XCTFail("Deeplink: 'testDeeplinkRequest_sendURL_amount' URL is expected to be valid.") } - store.send(.deeplink(url)) + await store.send(.deeplink(url)) let amount = Zatoshi(123_000_000) let address = "address" let memo = "some text" - store.receive(.deeplinkSend(amount, address, memo)) { state in + await store.receive(.deeplinkSend(amount, address, memo)) { state in state.route = .home state.homeState.route = .send state.homeState.sendState.amount = amount state.homeState.sendState.address = address state.homeState.sendState.memoState.text = memo } + + await store.finish() } } diff --git a/secantTests/RecoveryPhraseValidationTests/RecoveryPhraseValidationTests.swift b/secantTests/RecoveryPhraseValidationTests/RecoveryPhraseValidationTests.swift index 02d7a7c3..cb711442 100644 --- a/secantTests/RecoveryPhraseValidationTests/RecoveryPhraseValidationTests.swift +++ b/secantTests/RecoveryPhraseValidationTests/RecoveryPhraseValidationTests.swift @@ -14,7 +14,6 @@ class RecoveryPhraseValidationTests: XCTestCase { let testEnvironment = RecoveryPhraseValidationFlowEnvironment( scheduler: testScheduler.eraseToAnyScheduler(), - newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) }, pasteboard: .test, feedbackGenerator: .silent, recoveryPhraseRandomizer: .live diff --git a/secantTests/UtilTests/UserPreferencesStorageTests.swift b/secantTests/UtilTests/UserPreferencesStorageTests.swift index 6bc67e71..3d60c857 100644 --- a/secantTests/UtilTests/UserPreferencesStorageTests.swift +++ b/secantTests/UtilTests/UserPreferencesStorageTests.swift @@ -7,16 +7,13 @@ import XCTest @testable import secant_testnet -import Combine class UserPreferencesStorageTests: XCTestCase { - private var cancellables: [AnyCancellable] = [] - // swiftlint:disable:next implicitly_unwrapped_optional var storage: UserPreferencesStorage! - override func setUp() { - super.setUp() + override func setUp() async throws { + try await super.setUp() guard let userDefaults = UserDefaults.init(suiteName: "test") else { XCTFail("UserPreferencesStorageTests: UserDefaults.init(suiteName: \"test\") failed to initialize") @@ -31,16 +28,12 @@ class UserPreferencesStorageTests: XCTestCase { sessionAutoshielded: false, userDefaults: .live(userDefaults: userDefaults) ) - storage.removeAll() - .sink(receiveValue: { _ in }) - .store(in: &cancellables) + await storage.removeAll() } - override func tearDown() { - super.tearDown() - storage.removeAll() - .sink(receiveValue: { _ in }) - .store(in: &cancellables) + override func tearDown() async throws { + try await super.tearDown() + await storage.removeAll() storage = nil } @@ -68,42 +61,32 @@ class UserPreferencesStorageTests: XCTestCase { // MARK: - Set new values in the live UserDefaults environment - func testAppSessionFrom_setNewValue() throws { - storage.setActiveAppSessionFrom(87654321.0) - .sink(receiveValue: { _ in }) - .store(in: &cancellables) + func testAppSessionFrom_setNewValue() async throws { + await storage.setActiveAppSessionFrom(87654321.0) XCTAssertEqual(87654321.0, storage.activeAppSessionFrom, "User Preferences: `activeAppSessionFrom` default doesn't match.") } - func testConvertedCurrency_setNewValue() throws { - storage.setCurrency("CZK") - .sink(receiveValue: { _ in }) - .store(in: &cancellables) + func testConvertedCurrency_setNewValue() async throws { + await storage.setCurrency("CZK") XCTAssertEqual("CZK", storage.currency, "User Preferences: `currency` default doesn't match.") } - func testFiatConvertion_setNewValue() throws { - storage.setIsFiatConverted(false) - .sink(receiveValue: { _ in }) - .store(in: &cancellables) + func testFiatConvertion_setNewValue() async throws { + await storage.setIsFiatConverted(false) XCTAssertEqual(false, storage.isFiatConverted, "User Preferences: `isFiatConverted` default doesn't match.") } - func testRecoveryPhraseTestCompleted_setNewValue() throws { - storage.setIsRecoveryPhraseTestCompleted(false) - .sink(receiveValue: { _ in }) - .store(in: &cancellables) + func testRecoveryPhraseTestCompleted_setNewValue() async throws { + await storage.setIsRecoveryPhraseTestCompleted(false) XCTAssertEqual(false, storage.isRecoveryPhraseTestCompleted, "User Preferences: `isRecoveryPhraseTestCompleted` default doesn't match.") } - func testSessionAutoshielded_setNewValue() throws { - storage.setIsSessionAutoshielded(true) - .sink(receiveValue: { _ in }) - .store(in: &cancellables) + func testSessionAutoshielded_setNewValue() async throws { + await storage.setIsSessionAutoshielded(true) XCTAssertEqual(true, storage.isSessionAutoshielded, "User Preferences: `isSessionAutoshielded` default doesn't match.") } @@ -113,8 +96,8 @@ class UserPreferencesStorageTests: XCTestCase { func testAppSessionFrom_mocked() throws { let mockedUD = WrappedUserDefaults( objectForKey: { _ in 87654321.0 }, - remove: { _ in .none }, - setValue: { _, _ in .none }, + remove: { _ in }, + setValue: { _, _ in }, synchronize: { true } ) @@ -133,8 +116,8 @@ class UserPreferencesStorageTests: XCTestCase { func testConvertedCurrency_mocked() throws { let mockedUD = WrappedUserDefaults( objectForKey: { _ in "CZK" }, - remove: { _ in .none }, - setValue: { _, _ in .none }, + remove: { _ in }, + setValue: { _, _ in }, synchronize: { true } ) @@ -153,8 +136,8 @@ class UserPreferencesStorageTests: XCTestCase { func testFiatConvertion_mocked() throws { let mockedUD = WrappedUserDefaults( objectForKey: { _ in false }, - remove: { _ in .none }, - setValue: { _, _ in .none }, + remove: { _ in }, + setValue: { _, _ in }, synchronize: { true } ) @@ -173,8 +156,8 @@ class UserPreferencesStorageTests: XCTestCase { func testRecoveryPhraseTestCompleted_mocked() throws { let mockedUD = WrappedUserDefaults( objectForKey: { _ in false }, - remove: { _ in .none }, - setValue: { _, _ in .none }, + remove: { _ in }, + setValue: { _, _ in }, synchronize: { true } ) @@ -193,8 +176,8 @@ class UserPreferencesStorageTests: XCTestCase { func testSessionAutoshielded_mocked() throws { let mockedUD = WrappedUserDefaults( objectForKey: { _ in true }, - remove: { _ in .none }, - setValue: { _, _ in .none }, + remove: { _ in }, + setValue: { _, _ in }, synchronize: { true } ) @@ -212,7 +195,7 @@ class UserPreferencesStorageTests: XCTestCase { // MARK: - Remove all keys from the live UD environment - func testRemoveAll() throws { + func testRemoveAll() async throws { guard let userDefaults = UserDefaults.init(suiteName: "test") else { XCTFail("User Preferences: UserDefaults.init(suiteName: \"test\") failed to initialize") return @@ -224,9 +207,7 @@ class UserPreferencesStorageTests: XCTestCase { } // remove it - storage?.removeAll() - .sink(receiveValue: { _ in }) - .store(in: &cancellables) + await storage?.removeAll() // check the presence UserPreferencesStorage.Constants.allCases.forEach {