diff --git a/secant/Dependencies/NumberFormatterKey.swift b/secant/Dependencies/NumberFormatterKey.swift index 0e1a909f..52ec31c6 100644 --- a/secant/Dependencies/NumberFormatterKey.swift +++ b/secant/Dependencies/NumberFormatterKey.swift @@ -9,6 +9,7 @@ import ComposableArchitecture private enum NumberFormatterKey: DependencyKey { static let liveValue = WrappedNumberFormatter.live() + static let testValue = WrappedNumberFormatter.live() } extension DependencyValues { diff --git a/secant/Features/SendFlow/SendFlowStore.swift b/secant/Features/SendFlow/SendFlowStore.swift index e0890233..039e353f 100644 --- a/secant/Features/SendFlow/SendFlowStore.swift +++ b/secant/Features/SendFlow/SendFlowStore.swift @@ -18,6 +18,11 @@ typealias AnyTransactionAddressTextFieldReducer = AnyReducer< TransactionAddressTextFieldReducer.Action, SendFlowEnvironment > +typealias AnyTransactionAmountTextFieldReducer = AnyReducer< + TransactionAmountTextFieldReducer.State, + TransactionAmountTextFieldReducer.Action, + SendFlowEnvironment +> typealias AnyMultiLineTextFieldReducer = AnyReducer typealias AnyCheckCircleReducer = AnyReducer @@ -38,7 +43,7 @@ struct SendFlowState: Equatable { var route: Route? var shieldedBalance = WalletBalance.zero var transactionAddressInputState: TransactionAddressTextFieldReducer.State - var transactionAmountInputState: TransactionAmountTextFieldState + var transactionAmountInputState: TransactionAmountTextFieldReducer.State var address: String { get { transactionAddressInputState.textFieldState.text } @@ -92,7 +97,7 @@ enum SendFlowAction: Equatable { case sendTransactionResult(Result) case synchronizerStateChanged(WrappedSDKSynchronizerState) case transactionAddressInput(TransactionAddressTextFieldReducer.Action) - case transactionAmountInput(TransactionAmountTextFieldAction) + case transactionAmountInput(TransactionAmountTextFieldReducer.Action) case updateRoute(SendFlowState.Route?) } @@ -242,16 +247,15 @@ extension SendFlowReducer { environment: { $0 } ) - private static let transactionAmountInputReducer: SendFlowReducer = TransactionAmountTextFieldReducer.default.pullback( + private static let transactionAmountInputReducer: SendFlowReducer = AnyTransactionAmountTextFieldReducer { _ in + TransactionAmountTextFieldReducer() + } + .pullback( state: \SendFlowState.transactionAmountInputState, action: /SendFlowAction.transactionAmountInput, - environment: { environment in - TransactionAmountTextFieldEnvironment( - numberFormatter: environment.numberFormatter - ) - } + environment: { $0 } ) - + private static let memoReducer: SendFlowReducer = AnyMultiLineTextFieldReducer { _ in MultiLineTextFieldReducer() } diff --git a/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextField.swift b/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextField.swift index a3096a49..570afd08 100644 --- a/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextField.swift +++ b/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextField.swift @@ -19,7 +19,7 @@ struct TransactionAmountTextField: View { title: "How much ZEC would you like to send?", store: store.scope( state: \.textFieldState, - action: TransactionAmountTextFieldAction.textField + action: TransactionAmountTextFieldReducer.Action.textField ), titleAccessoryView: { Button( @@ -44,7 +44,7 @@ struct TransactionAmountTextField: View { CurrencySelectionView( store: store.scope( state: \.currencySelectionState, - action: TransactionAmountTextFieldAction.currencySelection + action: TransactionAmountTextFieldReducer.Action.currencySelection ) ) } @@ -65,10 +65,7 @@ struct TransactionAmountTextField_Previews: PreviewProvider { text: "" ) ), - reducer: .default, - environment: .init( - numberFormatter: .live() - ) + reducer: TransactionAmountTextFieldReducer() ) ) .preferredColorScheme(.dark) diff --git a/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextFieldStore.swift b/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextFieldStore.swift index 45177960..97dabe4e 100644 --- a/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextFieldStore.swift +++ b/secant/UI Components/TextFields/TransactionAmount/TransactionAmountTextFieldStore.swift @@ -9,130 +9,105 @@ import ComposableArchitecture import ZcashLightClientKit import Foundation -typealias TransactionAmountTextFieldReducer = Reducer< - TransactionAmountTextFieldState, - TransactionAmountTextFieldAction, - TransactionAmountTextFieldEnvironment -> +typealias TransactionAmountTextFieldStore = Store -typealias TransactionAmountTextFieldStore = Store +struct TransactionAmountTextFieldReducer: ReducerProtocol { + struct State: Equatable { + var amount: Int64 = 0 + var currencySelectionState: CurrencySelectionReducer.State + var maxValue: Int64 = 0 + var textFieldState: TCATextFieldReducer.State + // TODO [#311]: - Get the ZEC price from the SDK, https://github.com/zcash/secant-ios-wallet/issues/311 + var zecPrice = Decimal(140.0) -typealias AnyTCATextFieldReducerAmount = AnyReducer -typealias AnyCurrencySelectionReducer = AnyReducer - -struct TransactionAmountTextFieldState: Equatable { - var amount: Int64 = 0 - var currencySelectionState: CurrencySelectionReducer.State - var maxValue: Int64 = 0 - var textFieldState: TCATextFieldReducer.State - // TODO [#311]: - Get the ZEC price from the SDK, https://github.com/zcash/secant-ios-wallet/issues/311 - var zecPrice = Decimal(140.0) - - var isMax: Bool { - return amount == maxValue - } -} - -enum TransactionAmountTextFieldAction: Equatable { - case clearValue - case currencySelection(CurrencySelectionReducer.Action) - case setMax - case textField(TCATextFieldReducer.Action) - case updateAmount -} - -struct TransactionAmountTextFieldEnvironment { - let numberFormatter: WrappedNumberFormatter -} - -extension TransactionAmountTextFieldReducer { - static let `default` = TransactionAmountTextFieldReducer.combine( - [ - textFieldReducer, - currencySelectionReducer, - amountTextFieldReducer - ] - ) - - static let amountTextFieldReducer = TransactionAmountTextFieldReducer { state, action, environment in - switch action { - case .setMax: - let maxValueAsZec = Decimal(state.maxValue) / Decimal(Zatoshi.Constants.oneZecInZatoshi) - let currencyType = state.currencySelectionState.currencyType - let maxCurrencyConvertedValue: NSDecimalNumber = currencyType == .zec ? - NSDecimalNumber(decimal: maxValueAsZec).roundedZec : - NSDecimalNumber(decimal: maxValueAsZec * state.zecPrice).roundedZec - - let decimalString = environment.numberFormatter.string(maxCurrencyConvertedValue) ?? "" - - state.textFieldState.text = "\(decimalString)" - return Effect(value: .updateAmount) - - case .clearValue: - state.textFieldState.text = "" - return .none - - case .textField(.set(let amount)): - return Effect(value: .updateAmount) - - case .updateAmount: - guard var number = environment.numberFormatter.number(state.textFieldState.text) else { - state.amount = 0 - return .none - } - switch state.currencySelectionState.currencyType { - case .zec: - state.amount = NSDecimalNumber(decimal: number.decimalValue * Decimal(Zatoshi.Constants.oneZecInZatoshi)).roundedZec.int64Value - case .usd: - let decimal = (number.decimalValue / state.zecPrice) * Decimal(Zatoshi.Constants.oneZecInZatoshi) - state.amount = NSDecimalNumber(decimal: decimal).roundedZec.int64Value - } - return .none - - case .currencySelection: - guard let number = environment.numberFormatter.number(state.textFieldState.text) else { - state.amount = 0 - return .none - } - - let currencyType = state.currencySelectionState.currencyType - - let newValue = currencyType == .zec ? - number.decimalValue / state.zecPrice : - number.decimalValue * state.zecPrice - - let decimalString = environment.numberFormatter.string(NSDecimalNumber(decimal: newValue)) ?? "" - state.textFieldState.text = "\(decimalString)" - return Effect(value: .updateAmount) + var isMax: Bool { + return amount == maxValue } } - private static let textFieldReducer: TransactionAmountTextFieldReducer = AnyTCATextFieldReducerAmount { _ in - TCATextFieldReducer() + enum Action: Equatable { + case clearValue + case currencySelection(CurrencySelectionReducer.Action) + case setMax + case textField(TCATextFieldReducer.Action) + case updateAmount } - .pullback( - state: \TransactionAmountTextFieldState.textFieldState, - action: /TransactionAmountTextFieldAction.textField, - environment: { $0 } - ) - private static let currencySelectionReducer: TransactionAmountTextFieldReducer = AnyCurrencySelectionReducer { _ in - CurrencySelectionReducer() + @Dependency(\.numberFormatter) var numberFormatter + + var body: some ReducerProtocol { + Scope(state: \.currencySelectionState, action: /Action.currencySelection) { + CurrencySelectionReducer() + } + + Scope(state: \.textFieldState, action: /Action.textField) { + TCATextFieldReducer() + } + + Reduce { state, action in + switch action { + case .setMax: + let maxValueAsZec = Decimal(state.maxValue) / Decimal(Zatoshi.Constants.oneZecInZatoshi) + let currencyType = state.currencySelectionState.currencyType + let maxCurrencyConvertedValue: NSDecimalNumber = currencyType == .zec ? + NSDecimalNumber(decimal: maxValueAsZec).roundedZec : + NSDecimalNumber(decimal: maxValueAsZec * state.zecPrice).roundedZec + + let decimalString = numberFormatter.string(maxCurrencyConvertedValue) ?? "" + + state.textFieldState.text = "\(decimalString)" + return Effect(value: .updateAmount) + + case .clearValue: + state.textFieldState.text = "" + return .none + + case .textField(.set): + return Effect(value: .updateAmount) + + case .updateAmount: + guard let number = numberFormatter.number(state.textFieldState.text) else { + state.amount = 0 + return .none + } + switch state.currencySelectionState.currencyType { + case .zec: + state.amount = NSDecimalNumber(decimal: number.decimalValue * Decimal(Zatoshi.Constants.oneZecInZatoshi)).roundedZec.int64Value + case .usd: + let decimal = (number.decimalValue / state.zecPrice) * Decimal(Zatoshi.Constants.oneZecInZatoshi) + state.amount = NSDecimalNumber(decimal: decimal).roundedZec.int64Value + } + return .none + + case .currencySelection: + guard let number = numberFormatter.number(state.textFieldState.text) else { + state.amount = 0 + return .none + } + + let currencyType = state.currencySelectionState.currencyType + + let newValue = currencyType == .zec ? + number.decimalValue / state.zecPrice : + number.decimalValue * state.zecPrice + + let decimalString = numberFormatter.string(NSDecimalNumber(decimal: newValue)) ?? "" + state.textFieldState.text = "\(decimalString)" + return Effect(value: .updateAmount) + } + } } - .pullback( - state: \TransactionAmountTextFieldState.currencySelectionState, - action: /TransactionAmountTextFieldAction.currencySelection, - environment: { $0 } - ) } -extension TransactionAmountTextFieldState { - static let placeholder = TransactionAmountTextFieldState( +// MARK: - Placeholders + +extension TransactionAmountTextFieldReducer.State { + static let placeholder = TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), textFieldState: .placeholder ) - static let amount = TransactionAmountTextFieldState( + static let amount = TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), textFieldState: .amount ) @@ -141,10 +116,6 @@ extension TransactionAmountTextFieldState { extension TransactionAmountTextFieldStore { static let placeholder = TransactionAmountTextFieldStore( initialState: .placeholder, - reducer: .default, - environment: - TransactionAmountTextFieldEnvironment( - numberFormatter: .live() - ) + reducer: TransactionAmountTextFieldReducer() ) } diff --git a/secantTests/SendTests/SendTests.swift b/secantTests/SendTests/SendTests.swift index 0663af29..3add9f36 100644 --- a/secantTests/SendTests/SendTests.swift +++ b/secantTests/SendTests/SendTests.swift @@ -315,7 +315,7 @@ class SendTests: XCTestCase { memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), maxValue: 501_300, textFieldState: @@ -395,7 +395,7 @@ class SendTests: XCTestCase { route: nil, transactionAddressInputState: .placeholder, transactionAmountInputState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), textFieldState: TCATextFieldReducer.State( @@ -428,7 +428,7 @@ class SendTests: XCTestCase { memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( amount: 501_301, currencySelectionState: CurrencySelectionReducer.State(), maxValue: 501_302, @@ -477,7 +477,7 @@ class SendTests: XCTestCase { memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), maxValue: 501_300, textFieldState: @@ -525,7 +525,7 @@ class SendTests: XCTestCase { memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), maxValue: 501_302, textFieldState: @@ -573,7 +573,7 @@ class SendTests: XCTestCase { memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), maxValue: 501_302, textFieldState: @@ -630,7 +630,7 @@ class SendTests: XCTestCase { ) ), transactionAmountInputState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( amount: 100, currencySelectionState: CurrencySelectionReducer.State(), maxValue: 501_302, @@ -675,7 +675,7 @@ class SendTests: XCTestCase { memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), maxValue: 501_302, textFieldState: diff --git a/secantTests/SendTests/TransactionAmountInputTests.swift b/secantTests/SendTests/TransactionAmountInputTests.swift index bb7da7d6..de84cb8f 100644 --- a/secantTests/SendTests/TransactionAmountInputTests.swift +++ b/secantTests/SendTests/TransactionAmountInputTests.swift @@ -24,7 +24,7 @@ class TransactionAmountTextFieldTests: XCTestCase { func testMaxValue() throws { let store = TestStore( initialState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), maxValue: 501_301, textFieldState: @@ -33,11 +33,8 @@ class TransactionAmountTextFieldTests: XCTestCase { text: "0.002" ) ), - reducer: TransactionAmountTextFieldReducer.default, - environment: - TransactionAmountTextFieldEnvironment( - numberFormatter: .live(numberFormatter: usNumberFormatter) - ) + reducer: TransactionAmountTextFieldReducer() + .dependency(\.numberFormatter, .live(numberFormatter: usNumberFormatter)) ) store.send(.setMax) { state in @@ -52,7 +49,7 @@ class TransactionAmountTextFieldTests: XCTestCase { func testClearValue() throws { let store = TestStore( initialState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), maxValue: 501_301, textFieldState: @@ -61,11 +58,7 @@ class TransactionAmountTextFieldTests: XCTestCase { text: "0.002" ) ), - reducer: TransactionAmountTextFieldReducer.default, - environment: - TransactionAmountTextFieldEnvironment( - numberFormatter: .live() - ) + reducer: TransactionAmountTextFieldReducer() ) store.send(.clearValue) { state in @@ -77,7 +70,7 @@ class TransactionAmountTextFieldTests: XCTestCase { func testZec_to_UsdConvertedAmount() throws { let store = TestStore( initialState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State( currencyType: .zec @@ -89,11 +82,8 @@ class TransactionAmountTextFieldTests: XCTestCase { ), zecPrice: 1000.0 ), - reducer: TransactionAmountTextFieldReducer.default, - environment: - TransactionAmountTextFieldEnvironment( - numberFormatter: .live(numberFormatter: usNumberFormatter) - ) + reducer: TransactionAmountTextFieldReducer() + .dependency(\.numberFormatter, .live(numberFormatter: usNumberFormatter)) ) store.send(.currencySelection(.swapCurrencyType)) { state in @@ -109,7 +99,7 @@ class TransactionAmountTextFieldTests: XCTestCase { func testBigZecAmount_to_UsdConvertedAmount() throws { let store = TestStore( initialState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State( currencyType: .zec @@ -121,11 +111,8 @@ class TransactionAmountTextFieldTests: XCTestCase { ), zecPrice: 1000.0 ), - reducer: TransactionAmountTextFieldReducer.default, - environment: - TransactionAmountTextFieldEnvironment( - numberFormatter: .live(numberFormatter: usNumberFormatter) - ) + reducer: TransactionAmountTextFieldReducer() + .dependency(\.numberFormatter, .live(numberFormatter: usNumberFormatter)) ) store.send(.currencySelection(.swapCurrencyType)) { state in @@ -141,7 +128,7 @@ class TransactionAmountTextFieldTests: XCTestCase { func testUsd_to_ZecConvertedAmount() throws { let store = TestStore( initialState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State( currencyType: .usd @@ -153,11 +140,8 @@ class TransactionAmountTextFieldTests: XCTestCase { ), zecPrice: 1000.0 ), - reducer: TransactionAmountTextFieldReducer.default, - environment: - TransactionAmountTextFieldEnvironment( - numberFormatter: .live(numberFormatter: usNumberFormatter) - ) + reducer: TransactionAmountTextFieldReducer() + .dependency(\.numberFormatter, .live(numberFormatter: usNumberFormatter)) ) store.send(.currencySelection(.swapCurrencyType)) { state in @@ -173,7 +157,7 @@ class TransactionAmountTextFieldTests: XCTestCase { func testIfAmountIsMax() throws { let store = TestStore( initialState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State( currencyType: .usd @@ -186,11 +170,8 @@ class TransactionAmountTextFieldTests: XCTestCase { ), zecPrice: 1000.0 ), - reducer: TransactionAmountTextFieldReducer.default, - environment: - TransactionAmountTextFieldEnvironment( - numberFormatter: .live(numberFormatter: usNumberFormatter) - ) + reducer: TransactionAmountTextFieldReducer() + .dependency(\.numberFormatter, .live(numberFormatter: usNumberFormatter)) ) store.send(.textField(.set("1 000"))) { state in @@ -215,7 +196,7 @@ class TransactionAmountTextFieldTests: XCTestCase { func testMaxZecValue() throws { let store = TestStore( initialState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State( currencyType: .zec @@ -228,11 +209,8 @@ class TransactionAmountTextFieldTests: XCTestCase { ), zecPrice: 1000.0 ), - reducer: TransactionAmountTextFieldReducer.default, - environment: - TransactionAmountTextFieldEnvironment( - numberFormatter: .live(numberFormatter: usNumberFormatter) - ) + reducer: TransactionAmountTextFieldReducer() + .dependency(\.numberFormatter, .live(numberFormatter: usNumberFormatter)) ) store.send(.setMax) { state in @@ -247,7 +225,7 @@ class TransactionAmountTextFieldTests: XCTestCase { func testMaxUsdValue() throws { let store = TestStore( initialState: - TransactionAmountTextFieldState( + TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State( currencyType: .usd @@ -260,11 +238,8 @@ class TransactionAmountTextFieldTests: XCTestCase { ), zecPrice: 1000.0 ), - reducer: TransactionAmountTextFieldReducer.default, - environment: - TransactionAmountTextFieldEnvironment( - numberFormatter: .live(numberFormatter: usNumberFormatter) - ) + reducer: TransactionAmountTextFieldReducer() + .dependency(\.numberFormatter, .live(numberFormatter: usNumberFormatter)) ) store.send(.setMax) { state in diff --git a/secantTests/SnapshotTests/SendSnapshotTests/TransactionConfirmationSnapshotTests.swift b/secantTests/SnapshotTests/SendSnapshotTests/TransactionConfirmationSnapshotTests.swift index 0bbfd89d..c26eb548 100644 --- a/secantTests/SnapshotTests/SendSnapshotTests/TransactionConfirmationSnapshotTests.swift +++ b/secantTests/SnapshotTests/SendSnapshotTests/TransactionConfirmationSnapshotTests.swift @@ -31,7 +31,7 @@ class TransactionConfirmationSnapshotTests: XCTestCase { text: "ztestmockeddestinationaddress" ) ) - state.transactionAmountInputState = TransactionAmountTextFieldState( + state.transactionAmountInputState = TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), textFieldState: TCATextFieldReducer.State( validationType: nil, @@ -68,7 +68,7 @@ class TransactionConfirmationSnapshotTests: XCTestCase { text: "ztestmockeddestinationaddress" ) ) - state.transactionAmountInputState = TransactionAmountTextFieldState( + state.transactionAmountInputState = TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), textFieldState: TCATextFieldReducer.State( validationType: nil, diff --git a/secantTests/SnapshotTests/SendSnapshotTests/TransactionSendingTests.swift b/secantTests/SnapshotTests/SendSnapshotTests/TransactionSendingTests.swift index 7a4e7489..d5081217 100644 --- a/secantTests/SnapshotTests/SendSnapshotTests/TransactionSendingTests.swift +++ b/secantTests/SnapshotTests/SendSnapshotTests/TransactionSendingTests.swift @@ -31,7 +31,7 @@ class TransactionSendingTests: XCTestCase { text: "ztestmockeddestinationaddress" ) ) - state.transactionAmountInputState = TransactionAmountTextFieldState( + state.transactionAmountInputState = TransactionAmountTextFieldReducer.State( currencySelectionState: CurrencySelectionReducer.State(), textFieldState: TCATextFieldReducer.State( validationType: nil,