[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
This commit is contained in:
Lukas Korba 2022-08-17 09:12:15 +02:00 committed by GitHub
parent 4c36aa39a2
commit 558675aced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 97 additions and 124 deletions

View File

@ -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<Never, Never> {
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<Never, Never> {
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<Never, Never> {
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<Never, Never> {
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<Never, Never> {
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<Never, Never> {
var removals: [Effect<Never, Never>] = []
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: Value, forKey: String) -> Effect<Never, Never> {
let effect = userDefaults.setValue(value, forKey)
_ = userDefaults.synchronize()
return effect
func setValue<Value>(_ value: Value, forKey: String) async {
await userDefaults.setValue(value, forKey)
_ = await userDefaults.synchronize()
}
}

View File

@ -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<AppAction, Never> {
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)
}
}
}

View File

@ -25,14 +25,14 @@ enum RecoveryPhraseDisplayAction: Equatable {
case createPhrase
case copyToBufferPressed
case finishedPressed
case phraseResponse(Result<RecoveryPhrase, RecoveryPhraseError>)
case phraseResponse(RecoveryPhrase)
}
// MARK: - Environment
struct RecoveryPhraseDisplayEnvironment {
let scheduler: AnySchedulerOf<DispatchQueue>
let newPhrase: () -> Effect<RecoveryPhrase, RecoveryPhraseError>
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
}
}
}

View File

@ -112,7 +112,6 @@ enum RecoveryPhraseValidationFlowAction: Equatable {
struct RecoveryPhraseValidationFlowEnvironment {
let scheduler: AnySchedulerOf<DispatchQueue>
let newPhrase: () -> Effect<RecoveryPhrase, RecoveryPhraseError>
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

View File

@ -6,13 +6,12 @@
//
import Foundation
import ComposableArchitecture
struct WrappedUserDefaults {
let objectForKey: (String) -> Any?
let remove: (String) -> Effect<Never, Never>
let setValue: (Any?, String) -> Effect<Never, Never>
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 }
)
}

View File

@ -35,7 +35,7 @@ class RecoveryPhraseDisplayReducerTests: XCTestCase {
environment: .demo
)
store.send(.phraseResponse(.success(.placeholder))) {
store.send(.phraseResponse(.placeholder)) {
$0.phrase = .placeholder
$0.showCopyToBufferAlert = false
}

View File

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

View File

@ -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

View File

@ -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 {