[#452] Migrate Settings to ReducerProtocol (#459)

- Settings migrated to ReducerProtocol
- unit and snapshot tests fixed
This commit is contained in:
Lukas Korba 2022-11-05 08:16:10 +01:00 committed by GitHub
parent 410de3bfa2
commit df61f72459
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 116 additions and 194 deletions

View File

@ -7,6 +7,7 @@
import Foundation
import LocalAuthentication
import ComposableArchitecture
struct LocalAuthenticationHandler {
let authenticate: @Sendable () async -> Bool
@ -43,3 +44,15 @@ extension LocalAuthenticationHandler {
authenticate: { false }
)
}
private enum LocalAuthenticationHandlerKey: DependencyKey {
static let liveValue = LocalAuthenticationHandler.live
static let testValue = LocalAuthenticationHandler(authenticate: { true })
}
extension DependencyValues {
var localAuthenticationHandler: LocalAuthenticationHandler {
get { self[LocalAuthenticationHandlerKey.self] }
set { self[LocalAuthenticationHandlerKey.self] = newValue }
}
}

View File

@ -5,6 +5,8 @@ typealias ProfileReducer = Reducer<ProfileState, ProfileAction, ProfileEnvironme
typealias ProfileStore = Store<ProfileState, ProfileAction>
typealias ProfileViewStore = ViewStore<ProfileState, ProfileAction>
typealias AnySettingsReducer = AnyReducer<SettingsReducer.State, SettingsReducer.Action, ProfileEnvironment>
// MARK: - State
struct ProfileState: Equatable {
@ -19,7 +21,7 @@ struct ProfileState: Equatable {
var appVersion = ""
var route: Route?
var sdkVersion = ""
var settingsState: SettingsState
var settingsState: SettingsReducer.State
}
// MARK: - Action
@ -28,7 +30,7 @@ enum ProfileAction: Equatable {
case addressDetails(AddressDetailsAction)
case back
case onAppear
case settings(SettingsAction)
case settings(SettingsReducer.Action)
case updateRoute(ProfileState.Route?)
}
@ -105,18 +107,13 @@ extension ProfileReducer {
}
)
private static let settingsReducer: ProfileReducer = SettingsReducer.default.pullback(
private static let settingsReducer: ProfileReducer = AnySettingsReducer { _ in
SettingsReducer()
}
.pullback(
state: \ProfileState.settingsState,
action: /ProfileAction.settings,
environment: { environment in
SettingsEnvironment(
localAuthenticationHandler: .live,
mnemonic: environment.mnemonic,
SDKSynchronizer: environment.SDKSynchronizer,
userPreferencesStorage: .live,
walletStorage: environment.walletStorage
)
}
environment: { $0 }
)
}

View File

@ -1,120 +1,97 @@
import ComposableArchitecture
import SwiftUI
typealias SettingsReducer = Reducer<SettingsState, SettingsAction, SettingsEnvironment>
typealias AnySettingsReducer = AnyReducer<RecoveryPhraseDisplayReducer.State, RecoveryPhraseDisplayReducer.Action, SettingsEnvironment>
typealias SettingsStore = Store<SettingsState, SettingsAction>
typealias SettingsViewStore = ViewStore<SettingsState, SettingsAction>
typealias SettingsStore = Store<SettingsReducer.State, SettingsReducer.Action>
typealias SettingsViewStore = ViewStore<SettingsReducer.State, SettingsReducer.Action>
// MARK: - State
struct SettingsReducer: ReducerProtocol {
struct State: Equatable {
enum Route {
case backupPhrase
}
struct SettingsState: Equatable {
enum Route {
case backupPhrase
var phraseDisplayState: RecoveryPhraseDisplayReducer.State
var rescanDialog: ConfirmationDialogState<SettingsReducer.Action>?
var route: Route?
}
var phraseDisplayState: RecoveryPhraseDisplayReducer.State
var rescanDialog: ConfirmationDialogState<SettingsAction>?
var route: Route?
}
// MARK: - Action
enum SettingsAction: Equatable {
case backupWallet
case backupWalletAccessRequest
case cancelRescan
case fullRescan
case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
case quickRescan
case rescanBlockchain
case updateRoute(SettingsState.Route?)
}
// MARK: - Environment
struct SettingsEnvironment {
let localAuthenticationHandler: LocalAuthenticationHandler
let mnemonic: WrappedMnemonic
let SDKSynchronizer: WrappedSDKSynchronizer
let userPreferencesStorage: UserPreferencesStorage
let walletStorage: WrappedWalletStorage
}
// MARK: - Reducer
extension SettingsReducer {
static let `default` = SettingsReducer.combine(
[
settingsReducer,
backupPhraseReducer
]
)
enum Action: Equatable {
case backupWallet
case backupWalletAccessRequest
case cancelRescan
case fullRescan
case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
case quickRescan
case rescanBlockchain
case updateRoute(SettingsReducer.State.Route?)
}
private static let settingsReducer = SettingsReducer { state, action, environment in
switch action {
case .backupWalletAccessRequest:
return .run { send in
if await environment.localAuthenticationHandler.authenticate() {
await send(.backupWallet)
@Dependency(\.mnemonic) var mnemonic
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.walletStorage) var walletStorage
@Dependency(\.localAuthenticationHandler) var localAuthenticationHandler
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .backupWalletAccessRequest:
return .run { send in
if await localAuthenticationHandler.authenticate() {
await send(.backupWallet)
}
}
}
case .backupWallet:
do {
let storedWallet = try environment.walletStorage.exportWallet()
let phraseWords = try environment.mnemonic.asWords(storedWallet.seedPhrase)
let recoveryPhrase = RecoveryPhrase(words: phraseWords)
state.phraseDisplayState.phrase = recoveryPhrase
return Effect(value: .updateRoute(.backupPhrase))
} catch {
// TODO [#201]: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States
case .backupWallet:
do {
let storedWallet = try walletStorage.exportWallet()
let phraseWords = try mnemonic.asWords(storedWallet.seedPhrase)
let recoveryPhrase = RecoveryPhrase(words: phraseWords)
state.phraseDisplayState.phrase = recoveryPhrase
return Effect(value: .updateRoute(.backupPhrase))
} catch {
// TODO [#201]: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States
return .none
}
case .cancelRescan, .quickRescan, .fullRescan:
state.rescanDialog = nil
return .none
case .rescanBlockchain:
state.rescanDialog = .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
return .none
case .phraseDisplay:
state.route = nil
return .none
case .updateRoute(let route):
state.route = route
return .none
}
case .cancelRescan, .quickRescan, .fullRescan:
state.rescanDialog = nil
return .none
case .rescanBlockchain:
state.rescanDialog = .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
return .none
case .phraseDisplay:
state.route = nil
return .none
case .updateRoute(let route):
state.route = route
return .none
}
Scope(state: \.phraseDisplayState, action: /Action.phraseDisplay) {
RecoveryPhraseDisplayReducer()
}
}
private static let backupPhraseReducer: SettingsReducer = AnySettingsReducer { _ in
RecoveryPhraseDisplayReducer()
}
.pullback(
state: \SettingsState.phraseDisplayState,
action: /SettingsAction.phraseDisplay,
environment: { $0 }
)
}
// MARK: - ViewStore
extension SettingsViewStore {
var routeBinding: Binding<SettingsState.Route?> {
var routeBinding: Binding<SettingsReducer.State.Route?> {
self.binding(
get: \.route,
send: SettingsAction.updateRoute
send: SettingsReducer.Action.updateRoute
)
}
@ -132,15 +109,15 @@ extension SettingsStore {
func backupPhraseStore() -> RecoveryPhraseDisplayStore {
self.scope(
state: \.phraseDisplayState,
action: SettingsAction.phraseDisplay
action: SettingsReducer.Action.phraseDisplay
)
}
}
// MARK: Placeholders
extension SettingsState {
static let placeholder = SettingsState(
extension SettingsReducer.State {
static let placeholder = SettingsReducer.State(
phraseDisplayState: RecoveryPhraseDisplayReducer.State(
phrase: .placeholder
)
@ -150,13 +127,6 @@ extension SettingsState {
extension SettingsStore {
static let placeholder = SettingsStore(
initialState: .placeholder,
reducer: .default,
environment: SettingsEnvironment(
localAuthenticationHandler: .live,
mnemonic: .live,
SDKSynchronizer: MockWrappedSDKSynchronizer(),
userPreferencesStorage: .live,
walletStorage: .live()
)
reducer: SettingsReducer()
)
}

View File

@ -44,18 +44,10 @@ class SettingsTests: XCTestCase {
nukeWallet: { }
)
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: LocalAuthenticationHandler(authenticate: { true }),
mnemonic: .live,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
userPreferencesStorage: .mock,
walletStorage: mockedWalletStorage
)
let store = TestStore(
initialState: SettingsState(phraseDisplayState: RecoveryPhraseDisplayReducer.State(phrase: nil)),
reducer: SettingsReducer.default,
environment: testEnvironment
initialState: SettingsReducer.State(phraseDisplayState: RecoveryPhraseDisplayReducer.State(phrase: nil)),
reducer: SettingsReducer()
.dependency(\.walletStorage, mockedWalletStorage)
)
_ = await store.send(.backupWalletAccessRequest)
@ -69,18 +61,10 @@ class SettingsTests: XCTestCase {
}
func testBackupWalletAccessRequest_AuthenticateFailedPath() async throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
let store = TestStore(
initialState: .placeholder,
reducer: SettingsReducer.default,
environment: testEnvironment
reducer: SettingsReducer()
.dependency(\.localAuthenticationHandler, .unimplemented)
)
_ = await store.send(.backupWalletAccessRequest)
@ -89,18 +73,9 @@ class SettingsTests: XCTestCase {
}
func testRescanBlockchain() async throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
let store = TestStore(
initialState: .placeholder,
reducer: SettingsReducer.default,
environment: testEnvironment
reducer: SettingsReducer()
)
_ = await store.send(.rescanBlockchain) { state in
@ -117,16 +92,8 @@ class SettingsTests: XCTestCase {
}
func testRescanBlockchain_Cancelling() async throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
let store = TestStore(
initialState: SettingsState(
initialState: SettingsReducer.State(
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
@ -139,8 +106,7 @@ class SettingsTests: XCTestCase {
),
route: nil
),
reducer: SettingsReducer.default,
environment: testEnvironment
reducer: SettingsReducer()
)
_ = await store.send(.cancelRescan) { state in
@ -149,16 +115,8 @@ class SettingsTests: XCTestCase {
}
func testRescanBlockchain_QuickRescanClearance() async throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
let store = TestStore(
initialState: SettingsState(
initialState: SettingsReducer.State(
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
@ -171,8 +129,7 @@ class SettingsTests: XCTestCase {
),
route: nil
),
reducer: SettingsReducer.default,
environment: testEnvironment
reducer: SettingsReducer()
)
_ = await store.send(.quickRescan) { state in
@ -181,16 +138,8 @@ class SettingsTests: XCTestCase {
}
func testRescanBlockchain_FullRescanClearance() async throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
let store = TestStore(
initialState: SettingsState(
initialState: SettingsReducer.State(
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
@ -203,8 +152,7 @@ class SettingsTests: XCTestCase {
),
route: nil
),
reducer: SettingsReducer.default,
environment: testEnvironment
reducer: SettingsReducer()
)
_ = await store.send(.fullRescan) { state in

View File

@ -12,18 +12,12 @@ import SwiftUI
class SettingsSnapshotTests: XCTestCase {
func testSettingsSnapshot() throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
let store = Store(
initialState: .placeholder,
reducer: SettingsReducer.default,
environment: testEnvironment
reducer: SettingsReducer()
.dependency(\.localAuthenticationHandler, .unimplemented)
.dependency(\.sdkSynchronizer, TestWrappedSDKSynchronizer())
.dependency(\.walletStorage, .throwing)
)
addAttachments(SettingsView(store: store))