- Settings migrated to ReducerProtocol - unit and snapshot tests fixed
This commit is contained in:
parent
410de3bfa2
commit
df61f72459
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue