secant-ios-wallet/modules/Sources/Features/ZecKeyboard/ZecKeyboardStore.swift

315 lines
14 KiB
Swift

//
// ZecKeyboardStore.swift
// modules
//
// Created by Lukáš Korba on 20.09.2024.
//
import Foundation
import SwiftUI
import UIKit
import ComposableArchitecture
import ZcashLightClientKit
import Generated
import Utils
import Models
@Reducer
public struct ZecKeyboard {
public enum Constants {
static let initialValue = "0"
}
@ObservableState
public struct State: Equatable {
public var amount: Zatoshi = .zero
public var decimalSeparator = ""
public var convertedInput = Constants.initialValue
@Shared(.inMemory(.exchangeRate)) public var currencyConversion: CurrencyConversion? = nil
public var currencyValue: Double = 0
public var humanReadableConvertedInput = ""
public var humanReadableMainInput = ""
public var input = Constants.initialValue
public var isInputInZec = true
public var isCurrencySymbolPrefix = false
public var isValidInput = true
public var isZeroOutputAllowed = false
public var keys: [String] = []
public var localeCurrencySymbol = ""
public var isNextButtonDisabled: Bool {
amount.amount == 0 && !isZeroOutputAllowed
}
public init() { }
}
public enum Action: Equatable {
case longKeyTapped(Int)
case keyTapped(Int)
case nextTapped
case onAppear
case reportInvalidInput
case resolveHumanReadableStrings
case revertLastInput
case swapCurrenciesTapped
case validateInputs
}
public init() { }
public var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .onAppear:
if let decimalSeparator = Locale.current.decimalSeparator {
state.decimalSeparator = decimalSeparator
state.keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9", decimalSeparator, "0", "x"]
}
if state.input == Constants.initialValue {
state.isInputInZec = true
}
return .send(.validateInputs)
case .swapCurrenciesTapped:
state.isInputInZec.toggle()
if state.amount.amount > 0 {
state.input = state.convertedInput
return .send(.validateInputs)
}
return .send(.resolveHumanReadableStrings)
case .longKeyTapped(let index):
// backspace
if index == 11 {
state.input = Constants.initialValue
return .send(.validateInputs)
}
return .none
case .keyTapped(let index):
let inputSides = state.input.split(separator: Character(state.decimalSeparator))
if inputSides.count == 2, inputSides[1].count >= 8 && index != 11 {
return .none
}
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
impactFeedback.impactOccurred()
// backspace
if index == 11 {
let newValue = String(state.input.dropLast())
state.input = newValue.isEmpty ? Constants.initialValue : newValue
} else if index == 9 {
// decimal separator
if !state.input.contains(state.decimalSeparator) {
state.input += state.decimalSeparator
}
} else {
// some number
if state.input == Constants.initialValue {
state.input = state.keys[index]
} else {
if state.isInputInZec {
state.input += state.keys[index]
} else {
let split = state.input.split(separator: Character(state.decimalSeparator))
if split.count == 2 {
if split[1].count < 8 {
state.input += state.keys[index]
}
} else {
state.input += state.keys[index]
}
}
}
}
return .send(.validateInputs)
case .validateInputs:
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 8
numberFormatter.maximumIntegerDigits = 8
numberFormatter.numberStyle = .decimal
numberFormatter.usesGroupingSeparator = false
if let currencyConversion = state.currencyConversion {
// ZEC
if state.isInputInZec {
var amount = Zatoshi.zero
guard let inputToNumber = numberFormatter.number(from: state.input) else {
return .send(.reportInvalidInput)
}
amount = Zatoshi(
NSDecimalNumber(
decimal: inputToNumber.decimalValue * Decimal(Zatoshi.Constants.oneZecInZatoshi)
).roundedZec.int64Value)
// valid range
if amount.amount < Zatoshi.Constants.maxZatoshi {
state.amount = amount
state.currencyValue = currencyConversion.convert(amount)
state.convertedInput = Decimal(state.currencyValue).formatted(.number.grouping(.never))
state.isValidInput = true
} else {
// revert last input
return .send(.revertLastInput)
}
} else {
// USD
guard let inputToNumber = numberFormatter.number(from: state.input) else {
return .send(.reportInvalidInput)
}
guard let currencyValue = Double(exactly: inputToNumber) else {
return .send(.reportInvalidInput)
}
state.currencyValue = currencyValue
let valueInZec = currencyConversion.convert(currencyValue)
// valid range
if valueInZec.amount < Zatoshi.Constants.maxZatoshi {
if let stringValue = numberFormatter.string(from: valueInZec.decimalValue.roundedZec) {
state.convertedInput = stringValue
state.amount = valueInZec
} else {
state.isValidInput = false
}
} else {
// revert input
return .send(.revertLastInput)
}
state.isValidInput = true
}
} else {
// ZEC only input
var amount = Zatoshi.zero
if let number = numberFormatter.number(from: state.input) {
amount = Zatoshi(
NSDecimalNumber(
decimal: number.decimalValue * Decimal(Zatoshi.Constants.oneZecInZatoshi)
).roundedZec.int64Value)
// valid range
if amount.amount < Zatoshi.Constants.maxZatoshi {
state.amount = amount
state.isValidInput = true
} else {
// revert last input
return .send(.revertLastInput)
}
} else {
state.isValidInput = false
}
}
return .send(.resolveHumanReadableStrings)
case .reportInvalidInput:
state.isValidInput = false
return .send(.resolveHumanReadableStrings)
case .revertLastInput:
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 8
numberFormatter.maximumIntegerDigits = 8
numberFormatter.numberStyle = .decimal
numberFormatter.usesGroupingSeparator = false
// ZEC
if state.isInputInZec {
let newValue = String(state.input.dropLast())
state.input = newValue.isEmpty ? Constants.initialValue : newValue
if let number = numberFormatter.number(from: state.input) {
let amount = Zatoshi(
NSDecimalNumber(
decimal: number.decimalValue * Decimal(Zatoshi.Constants.oneZecInZatoshi)
).roundedZec.int64Value)
state.amount = amount
if let currencyConversion = state.currencyConversion {
state.currencyValue = currencyConversion.convert(amount)
state.convertedInput = Decimal(state.currencyValue).formatted(.number.grouping(.never))
}
state.isValidInput = true
}
} else {
guard let currencyConversion = state.currencyConversion else {
return .none
}
let newValue = String(state.input.dropLast())
state.input = newValue.isEmpty ? Constants.initialValue : newValue
if let number = numberFormatter.number(from: state.input) {
if let currencyValue = Double(exactly: number) {
state.currencyValue = currencyValue
let valueInZec = currencyConversion.convert(currencyValue)
if let stringValue = numberFormatter.string(from: valueInZec.decimalValue.roundedZec) {
state.convertedInput = stringValue
state.amount = valueInZec
} else {
state.isValidInput = false
}
}
}
}
return .send(.resolveHumanReadableStrings)
case .resolveHumanReadableStrings:
let formatter = NumberFormatter()
formatter.locale = Locale.current
formatter.numberStyle = .currency
if let currencyConversion = state.currencyConversion {
formatter.currencyCode = currencyConversion.iso4217.code
}
formatter.maximumFractionDigits = 8
let currencySymbol = formatter.currencySymbol ?? ""
state.localeCurrencySymbol = currencySymbol
if formatter.positivePrefix.contains(formatter.currencySymbol) {
state.isCurrencySymbolPrefix = true
} else {
state.isCurrencySymbolPrefix = false
}
if state.isInputInZec {
state.humanReadableMainInput = state.amount.decimalString()
let inputSides = state.input.split(separator: Character(state.decimalSeparator))
if inputSides.count == 2 {
if let humanReadableInput = state.humanReadableMainInput.split(separator: Character(state.decimalSeparator)).first {
state.humanReadableMainInput = "\(humanReadableInput)\(state.decimalSeparator)\(inputSides[1])"
}
} else {
if let lastChar = state.input.last, String(lastChar) == state.decimalSeparator {
state.humanReadableMainInput += state.decimalSeparator
}
}
state.humanReadableConvertedInput = Decimal(state.currencyValue).formatted(.number.precision(.fractionLength(2)))
} else {
state.humanReadableMainInput = Decimal(state.currencyValue).formatted(.number)
let inputSides = state.input.split(separator: Character(state.decimalSeparator))
if inputSides.count == 2 {
if let humanReadableInput = state.humanReadableMainInput.split(separator: Character(state.decimalSeparator)).first {
state.humanReadableMainInput = "\(humanReadableInput)\(state.decimalSeparator)\(inputSides[1])"
}
} else {
if let lastChar = state.input.last, String(lastChar) == state.decimalSeparator {
state.humanReadableMainInput += state.decimalSeparator
}
}
state.humanReadableConvertedInput = state.amount.decimalString()
}
return .none
case .nextTapped:
return .none
}
}
}
}