344 lines
12 KiB
Swift
344 lines
12 KiB
Swift
//
|
|
// SendFlowStore.swift
|
|
// secant-testnet
|
|
//
|
|
// Created by Lukáš Korba on 04/25/2022.
|
|
//
|
|
|
|
import SwiftUI
|
|
import ComposableArchitecture
|
|
import ZcashLightClientKit
|
|
|
|
typealias SendFlowStore = Store<SendFlowReducer.State, SendFlowReducer.Action>
|
|
typealias SendFlowViewStore = ViewStore<SendFlowReducer.State, SendFlowReducer.Action>
|
|
|
|
struct SendFlowReducer: ReducerProtocol {
|
|
private enum SyncStatusUpdatesID {}
|
|
|
|
struct State: Equatable {
|
|
enum Destination: Equatable {
|
|
case inProgress
|
|
case scanQR
|
|
case success
|
|
case failure
|
|
case done
|
|
}
|
|
|
|
var addMemoState: Bool
|
|
var destination: Destination?
|
|
var isSendingTransaction = false
|
|
var memoState: MultiLineTextFieldReducer.State
|
|
var scanState: ScanReducer.State
|
|
var shieldedBalance = Balance.zero
|
|
var transactionAddressInputState: TransactionAddressTextFieldReducer.State
|
|
var transactionAmountInputState: TransactionAmountTextFieldReducer.State
|
|
|
|
var address: String {
|
|
get { transactionAddressInputState.textFieldState.text.data }
|
|
set { transactionAddressInputState.textFieldState.text = newValue.redacted }
|
|
}
|
|
|
|
var amount: Zatoshi {
|
|
get { Zatoshi(transactionAmountInputState.amount.data) }
|
|
set {
|
|
transactionAmountInputState.amount = newValue.amount.redacted
|
|
transactionAmountInputState.textFieldState.text = newValue.amount == 0 ?
|
|
"".redacted :
|
|
newValue.decimalString().redacted
|
|
}
|
|
}
|
|
|
|
var isInvalidAddressFormat: Bool {
|
|
!transactionAddressInputState.isValidAddress
|
|
&& !transactionAddressInputState.textFieldState.text.data.isEmpty
|
|
}
|
|
|
|
var isInvalidAmountFormat: Bool {
|
|
!transactionAmountInputState.textFieldState.valid
|
|
&& !transactionAmountInputState.textFieldState.text.data.isEmpty
|
|
}
|
|
|
|
var isValidForm: Bool {
|
|
transactionAmountInputState.amount.data > 0
|
|
&& transactionAddressInputState.isValidAddress
|
|
&& !isInsufficientFunds
|
|
&& memoState.isValid
|
|
}
|
|
|
|
var isInsufficientFunds: Bool {
|
|
transactionAmountInputState.amount.data > transactionAmountInputState.maxValue.data
|
|
}
|
|
|
|
var totalCurrencyBalance: Zatoshi {
|
|
Zatoshi.from(decimal: shieldedBalance.data.total.decimalValue.decimalValue * transactionAmountInputState.zecPrice)
|
|
}
|
|
}
|
|
|
|
enum Action: Equatable {
|
|
case addMemo(CheckCircleReducer.Action)
|
|
case memo(MultiLineTextFieldReducer.Action)
|
|
case onAppear
|
|
case onDisappear
|
|
case scan(ScanReducer.Action)
|
|
case sendPressed
|
|
case sendTransactionResult(Result<TransactionState, NSError>)
|
|
case synchronizerStateChanged(SDKSynchronizerState)
|
|
case transactionAddressInput(TransactionAddressTextFieldReducer.Action)
|
|
case transactionAmountInput(TransactionAmountTextFieldReducer.Action)
|
|
case updateDestination(SendFlowReducer.State.Destination?)
|
|
}
|
|
|
|
@Dependency(\.audioServices) var audioServices
|
|
@Dependency(\.derivationTool) var derivationTool
|
|
@Dependency(\.mainQueue) var mainQueue
|
|
@Dependency(\.mnemonic) var mnemonic
|
|
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
|
|
@Dependency(\.walletStorage) var walletStorage
|
|
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
|
|
|
|
var body: some ReducerProtocol<State, Action> {
|
|
Scope(state: \.addMemoState, action: /Action.addMemo) {
|
|
CheckCircleReducer()
|
|
}
|
|
|
|
Scope(state: \.memoState, action: /Action.memo) {
|
|
MultiLineTextFieldReducer()
|
|
}
|
|
|
|
Scope(state: \.transactionAddressInputState, action: /Action.transactionAddressInput) {
|
|
TransactionAddressTextFieldReducer()
|
|
}
|
|
|
|
Scope(state: \.transactionAmountInputState, action: /Action.transactionAmountInput) {
|
|
TransactionAmountTextFieldReducer()
|
|
}
|
|
|
|
Scope(state: \.scanState, action: /Action.scan) {
|
|
ScanReducer()
|
|
}
|
|
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .addMemo:
|
|
return .none
|
|
|
|
case .updateDestination(.done):
|
|
state.destination = nil
|
|
state.memoState.text = "".redacted
|
|
state.transactionAmountInputState.textFieldState.text = "".redacted
|
|
state.transactionAmountInputState.amount = Int64(0).redacted
|
|
state.transactionAddressInputState.textFieldState.text = "".redacted
|
|
return .none
|
|
|
|
case .updateDestination(.failure):
|
|
state.destination = .failure
|
|
state.isSendingTransaction = false
|
|
return .none
|
|
|
|
case let .updateDestination(destination):
|
|
state.destination = destination
|
|
return .none
|
|
|
|
case .sendPressed:
|
|
guard !state.isSendingTransaction else {
|
|
return .none
|
|
}
|
|
state.amount = Zatoshi(state.transactionAmountInputState.amount.data)
|
|
state.address = state.transactionAddressInputState.textFieldState.text.data
|
|
|
|
do {
|
|
let storedWallet = try walletStorage.exportWallet()
|
|
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value())
|
|
let spendingKey = try derivationTool.deriveSpendingKey(seedBytes, 0)
|
|
|
|
state.isSendingTransaction = true
|
|
|
|
let memo: Memo?
|
|
if let memoText = state.addMemoState ? state.memoState.text : nil {
|
|
memo = try Memo(string: memoText.data)
|
|
} else {
|
|
memo = nil
|
|
}
|
|
|
|
let recipient = try Recipient(state.address, network: zcashSDKEnvironment.network.networkType)
|
|
let sendTransActionEffect = sdkSynchronizer.sendTransaction(
|
|
with: spendingKey,
|
|
zatoshi: state.amount,
|
|
to: recipient,
|
|
memo: memo
|
|
)
|
|
.receive(on: mainQueue)
|
|
.map(SendFlowReducer.Action.sendTransactionResult)
|
|
.eraseToEffect()
|
|
|
|
return .concatenate(
|
|
EffectTask(value: .updateDestination(.inProgress)),
|
|
sendTransActionEffect
|
|
)
|
|
} catch {
|
|
return EffectTask(value: .updateDestination(.failure))
|
|
}
|
|
|
|
case .sendTransactionResult(let result):
|
|
state.isSendingTransaction = false
|
|
do {
|
|
_ = try result.get()
|
|
return EffectTask(value: .updateDestination(.success))
|
|
} catch {
|
|
return EffectTask(value: .updateDestination(.failure))
|
|
}
|
|
|
|
case .transactionAmountInput:
|
|
return .none
|
|
|
|
case .transactionAddressInput(.scanQR):
|
|
return EffectTask(value: .updateDestination(.scanQR))
|
|
|
|
case .transactionAddressInput:
|
|
return .none
|
|
|
|
case .onAppear:
|
|
state.memoState.charLimit = zcashSDKEnvironment.memoCharLimit
|
|
return sdkSynchronizer.stateChanged
|
|
.map(SendFlowReducer.Action.synchronizerStateChanged)
|
|
.eraseToEffect()
|
|
.cancellable(id: SyncStatusUpdatesID.self, cancelInFlight: true)
|
|
|
|
case .onDisappear:
|
|
return .cancel(id: SyncStatusUpdatesID.self)
|
|
|
|
case .synchronizerStateChanged(.synced):
|
|
if let shieldedBalance = sdkSynchronizer.latestScannedSynchronizerState?.shieldedBalance {
|
|
state.shieldedBalance = shieldedBalance.redacted
|
|
state.transactionAmountInputState.maxValue = shieldedBalance.total.amount.redacted
|
|
}
|
|
return .none
|
|
|
|
case .synchronizerStateChanged:
|
|
return .none
|
|
|
|
case .memo:
|
|
return .none
|
|
|
|
case .scan(.found(let address)):
|
|
state.transactionAddressInputState.textFieldState.text = address
|
|
// The is valid Zcash address check is already covered in the scan feature
|
|
// so we can be sure it's valid and thus `true` value here.
|
|
state.transactionAddressInputState.isValidAddress = true
|
|
audioServices.systemSoundVibrate()
|
|
return EffectTask(value: .updateDestination(nil))
|
|
|
|
case .scan:
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Store
|
|
|
|
extension SendFlowStore {
|
|
func addMemoStore() -> CheckCircleStore {
|
|
self.scope(
|
|
state: \.addMemoState,
|
|
action: SendFlowReducer.Action.addMemo
|
|
)
|
|
}
|
|
|
|
func memoStore() -> MultiLineTextFieldStore {
|
|
self.scope(
|
|
state: \.memoState,
|
|
action: SendFlowReducer.Action.memo
|
|
)
|
|
}
|
|
|
|
func scanStore() -> ScanStore {
|
|
self.scope(
|
|
state: \.scanState,
|
|
action: SendFlowReducer.Action.scan
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - ViewStore
|
|
|
|
extension SendFlowViewStore {
|
|
var destinationBinding: Binding<SendFlowReducer.State.Destination?> {
|
|
self.binding(
|
|
get: \.destination,
|
|
send: SendFlowReducer.Action.updateDestination
|
|
)
|
|
}
|
|
|
|
var bindingForInProgress: Binding<Bool> {
|
|
self.destinationBinding.map(
|
|
extract: {
|
|
$0 == .inProgress ||
|
|
$0 == .success ||
|
|
$0 == .failure
|
|
},
|
|
embed: { $0 ? SendFlowReducer.State.Destination.inProgress : nil }
|
|
)
|
|
}
|
|
|
|
var bindingForSuccess: Binding<Bool> {
|
|
self.destinationBinding.map(
|
|
extract: { $0 == .success },
|
|
embed: { _ in SendFlowReducer.State.Destination.success }
|
|
)
|
|
}
|
|
|
|
var bindingForFailure: Binding<Bool> {
|
|
self.destinationBinding.map(
|
|
extract: { $0 == .failure },
|
|
embed: { _ in SendFlowReducer.State.Destination.failure }
|
|
)
|
|
}
|
|
|
|
var bindingForScanQR: Binding<Bool> {
|
|
self.destinationBinding.map(
|
|
extract: {
|
|
$0 == .scanQR
|
|
},
|
|
embed: { $0 ? SendFlowReducer.State.Destination.scanQR : nil }
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: Placeholders
|
|
|
|
extension SendFlowReducer.State {
|
|
static var placeholder: Self {
|
|
.init(
|
|
addMemoState: true,
|
|
destination: nil,
|
|
memoState: .placeholder,
|
|
scanState: .placeholder,
|
|
transactionAddressInputState: .placeholder,
|
|
transactionAmountInputState: .amount
|
|
)
|
|
}
|
|
|
|
static var emptyPlaceholder: Self {
|
|
.init(
|
|
addMemoState: true,
|
|
destination: nil,
|
|
memoState: .placeholder,
|
|
scanState: .placeholder,
|
|
transactionAddressInputState: .placeholder,
|
|
transactionAmountInputState: .placeholder
|
|
)
|
|
}
|
|
}
|
|
|
|
// #if DEBUG // FIX: Issue #306 - Release build is broken
|
|
extension SendFlowStore {
|
|
static var placeholder: SendFlowStore {
|
|
return SendFlowStore(
|
|
initialState: .emptyPlaceholder,
|
|
reducer: SendFlowReducer()
|
|
)
|
|
}
|
|
}
|
|
// #endif
|