Last active
February 27, 2018 22:10
-
-
Save zackshapiro/5d946a308a2252e1fe9d5a3037a77b17 to your computer and use it in GitHub Desktop.
SendVC.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
final class SendTextField: UITextField { | |
init() { | |
super.init(frame: .zero) | |
backgroundColor = .clear | |
textColor = .white | |
tintColor = .white | |
textAlignment = .center | |
placeholder = "0.00" | |
inputView = UIView() // Don't show a keyboard | |
font = Styleguide.Fonts.nunitoLight.font(ofSize: (isiPhoneSE() ? 17 : 20)) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
func setSmallerFontSize() { | |
alpha = 0.5 | |
font = Styleguide.Fonts.nunitoLight.font(ofSize: (isiPhoneSE() ? 14 : 16)) | |
} | |
func setLargerFontSize() { | |
alpha = 1.0 | |
font = Styleguide.Fonts.nunitoLight.font(ofSize: (isiPhoneSE() ? 17 : 20)) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SendViewController.swift | |
// Nano | |
// | |
// Created by Zack Shapiro on 12/7/17. | |
// Copyright © 2017 Nano. All rights reserved. | |
// | |
import AVFoundation | |
import UIKit | |
import LocalAuthentication | |
import MobileCoreServices | |
import Cartography | |
import Crashlytics | |
import ReactiveSwift | |
import Result | |
import SwiftWebSocket | |
protocol SendViewControllerDelegate: class { | |
func didFinishWithViewController() | |
} | |
final class SendViewController: UIViewController { | |
private let (lifetime, token) = Lifetime.make() | |
private weak var addressTextView: SendAddressTextView? | |
private let sendAddressIsValid = MutableProperty<Bool>(false) | |
private let sendableAmountIsValid = MutableProperty<Bool>(false) | |
private let viewModel: SendViewModel | |
private weak var nanoTextField: SendTextField? | |
private weak var localCurrencyTextField: SendTextField? | |
private weak var activeTextField: UITextField? | |
private weak var sendButton: NanoButton? | |
private var isScanningAmount: Bool = false | |
weak var delegate: SendViewControllerDelegate? | |
let (nanoSignal, nanoObserver) = Signal<KeyboardButton, NoError>.pipe() | |
let (localCurrencySignal, localCurrencyObserver) = Signal<KeyboardButton, NoError>.pipe() | |
let nanoProducer: SignalProducer<KeyboardButton, NoError> | |
let localCurrencyProducer: SignalProducer<KeyboardButton, NoError> | |
init(viewModel: SendViewModel) { | |
self.viewModel = viewModel | |
self.nanoProducer = SignalProducer(nanoSignal) | |
self.localCurrencyProducer = SignalProducer(localCurrencySignal) | |
super.init(nibName: nil, bundle: nil) | |
Answers.logCustomEvent(withName: "Send VC Viewed") | |
SignalProducer.combineLatest(sendAddressIsValid.producer, sendableAmountIsValid.producer) | |
.producer | |
.take(during: lifetime) | |
.observe(on: UIScheduler()) | |
.startWithValues { | |
self.sendButton?.isEnabled = ($0 && $1) | |
} | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
view.backgroundColor = .white | |
self.navigationController?.navigationBar.barTintColor = Styleguide.Colors.darkBlue.color | |
self.navigationController?.navigationBar.tintColor = Styleguide.Colors.lightBlue.color | |
self.navigationController?.navigationBar.titleTextAttributes = [ | |
.foregroundColor: Styleguide.Colors.lightBlue.color, | |
.font: Styleguide.Fonts.sofiaRegular.font(ofSize: 16), | |
.kern: 5.0 | |
] | |
self.navigationItem.titleView = SendReceiveHeaderView(withType: .send) | |
self.navigationItem.title = "Send To".uppercased() | |
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "camera"), style: .plain, target: self, action: #selector(openCamera)) | |
let addressTextView = SendAddressTextView() | |
addressTextView.delegate = self | |
addressTextView.inputAccessoryView = keyboardAccessoryView() | |
addressTextView.placeholder = "Enter a Nano Address" | |
view.addSubview(addressTextView) | |
constrain(addressTextView) { | |
$0.top == $0.superview!.top | |
$0.left == $0.superview!.left | |
$0.right == $0.superview!.right | |
$0.height == CGFloat(88) | |
} | |
self.addressTextView = addressTextView | |
// MARK: - Price Section | |
let priceSection = UIView() | |
priceSection.backgroundColor = Styleguide.Colors.lightBlue.color | |
view.addSubview(priceSection) | |
constrain(priceSection, addressTextView) { | |
$0.width == $0.superview!.width | |
$0.top == $1.bottom | |
$0.height == (isiPhoneSE() ? CGFloat(100) : CGFloat(140)) | |
} | |
let nanoTextField = SendTextField() | |
nanoTextField.delegate = self | |
nanoTextField.textAlignment = .center | |
priceSection.addSubview(nanoTextField) | |
constrain(nanoTextField) { | |
$0.top == $0.superview!.top | |
$0.height == $0.superview!.height * CGFloat(0.5) | |
$0.width == $0.superview!.width | |
} | |
self.nanoTextField = nanoTextField | |
let maxButton = UIButton() | |
maxButton.addTarget(self, action: #selector(fillOutWithMaxBalance), for: .touchUpInside) | |
view.addSubview(maxButton) | |
maxButton.layer.cornerRadius = 3 | |
maxButton.clipsToBounds = true | |
maxButton.setBackgroundColor(color: UIColor.white.withAlphaComponent(0.2), forState: .normal) | |
maxButton.setBackgroundColor(color: UIColor.white.withAlphaComponent(0.3), forState: .highlighted) | |
let title = NSMutableAttributedString(string: "MAX") | |
title.addAttribute(.kern, value: 3.0, range: NSMakeRange(0, title.length)) | |
title.addAttribute(.foregroundColor, value: UIColor.white, range: NSMakeRange(0, title.length)) | |
maxButton.titleLabel?.font = Styleguide.Fonts.sofiaRegular.font(ofSize: 14) | |
maxButton.setAttributedTitle(title, for: .normal) | |
maxButton.titleLabel?.textAlignment = .center | |
maxButton.titleLabel?.numberOfLines = 1 | |
maxButton.titleLabel?.baselineAdjustment = .alignCenters | |
constrain(maxButton, nanoTextField) { | |
$0.centerY == $1.centerY | |
$0.left == $0.superview!.left + CGFloat(20) | |
$0.width == CGFloat(58) | |
$0.height == CGFloat(29) | |
} | |
let border = UIView() | |
border.backgroundColor = UIColor.white.withAlphaComponent(0.25) | |
priceSection.addSubview(border) | |
constrain(border) { | |
$0.height == CGFloat(1) | |
$0.centerY == $0.superview!.centerY | |
$0.width == $0.superview!.width | |
} | |
let upDownArrow = UIImageView(image: UIImage(named: "upDownArrow")) | |
priceSection.addSubview(upDownArrow) | |
constrain(upDownArrow, border) { | |
$0.center == $1.center | |
} | |
let localCurrencyTextField = SendTextField() | |
localCurrencyTextField.delegate = self | |
localCurrencyTextField.setSmallerFontSize() | |
priceSection.addSubview(localCurrencyTextField) | |
constrain(localCurrencyTextField, nanoTextField) { | |
$0.top == $1.bottom | |
$0.bottom == $0.superview!.bottom | |
$0.width == $0.superview!.width | |
$0.height == $0.superview!.height * CGFloat(0.5) | |
} | |
self.localCurrencyTextField = localCurrencyTextField | |
// MARK: - Bottom Section | |
let bottomSection = UIView() | |
bottomSection.backgroundColor = Styleguide.Colors.darkBlue.color | |
view.addSubview(bottomSection) | |
constrain(bottomSection, priceSection) { | |
$0.width == $1.width | |
$0.centerX == $0.superview!.centerX | |
$0.top == $1.bottom | |
$0.bottom == $0.superview!.bottom | |
} | |
let sendButton = NanoButton(withType: .lightBlueSend) | |
sendButton.setAttributedTitle("SEND") | |
sendButton.addTarget(self, action: #selector(sendNano), for: .touchUpInside) | |
sendButton.isEnabled = false | |
bottomSection.addSubview(sendButton) | |
constrain(sendButton) { | |
$0.width == $0.superview!.width * CGFloat(0.8) | |
$0.centerX == $0.superview!.centerX | |
$0.height == CGFloat(55) | |
$0.bottom == $0.superview!.bottom - CGFloat((isiPhoneX() ? 34 : 20)) | |
} | |
self.sendButton = sendButton | |
let keyboard = SendKeyboard() | |
keyboard.delegate = self | |
bottomSection.addSubview(keyboard) | |
constrain(keyboard, priceSection, sendButton) { | |
$0.top == $1.bottom + (isiPhoneSE() ? CGFloat(9) : CGFloat(36)) | |
$0.bottom == $2.top - (isiPhoneSE() ? CGFloat(9) : CGFloat(36)) | |
$0.width == $0.superview!.width * CGFloat(0.8) | |
$0.centerX == $0.superview!.centerX | |
} | |
// Refactor all of this to be more functional | |
nanoProducer.producer.startWithValues { button in | |
guard | |
let textField = self.nanoTextField, | |
let text = textField.text | |
else { return } | |
switch button { | |
case .backspace: | |
self.viewModel.maxAmountInUse = false | |
// TODO: add case: if second to last char is decimalSeparator, deleteBackwardsx2 (see also in other producer below) | |
if textField.text! == "0\(self.viewModel.decimalSeparator)" { | |
textField.deleteBackward() | |
textField.deleteBackward() | |
} else { | |
textField.deleteBackward() | |
} | |
case .number: | |
if textField.text! == "", button.valueIsDecimalIndicator { | |
textField.text!.insert("0", at: String.Index(encodedOffset: 0)) | |
textField.text!.insert(Character(self.viewModel.decimalSeparator), at: String.Index(encodedOffset: 1)) | |
} else { | |
textField.text!.insert(button.characterValue, at: String.Index(encodedOffset: text.count)) | |
} | |
} | |
let value = NSDecimalNumber(string: (textField.text == "" ? "0" : self.formatForMath(textField.text!))) | |
// If the value you typed is equal to your total balance | |
if value.asRawValue.compare(self.viewModel.sendableNanoBalance) == .orderedSame { | |
self.sendableAmountIsValid.value = true | |
return self.viewModel.nanoAmount.value = value | |
} | |
self.viewModel.maxAmountInUse = false | |
// If the value you typed is too large | |
if value.asRawValue.compare(self.viewModel.sendableNanoBalance) == .orderedDescending { | |
return self.fillOutWithMaxBalance(showAlert: true) | |
} | |
self.sendableAmountIsValid.value = textField.text != "" | |
self.viewModel.nanoAmount.value = value | |
} | |
localCurrencyProducer.producer.startWithValues { button in | |
guard | |
let textField = self.localCurrencyTextField, | |
let text = textField.text | |
else { return } | |
switch button { | |
case .backspace: | |
self.viewModel.maxAmountInUse = false | |
if textField.text! == "0\(self.viewModel.decimalSeparator)" { | |
textField.deleteBackward() | |
textField.deleteBackward() | |
} else { | |
textField.deleteBackward() | |
} | |
case .number: | |
if textField.text! == "\(self.viewModel.localCurrency.mark)", button.valueIsDecimalIndicator { | |
textField.text!.insert("0", at: String.Index(encodedOffset: textField.text!.count)) | |
textField.text!.insert(Character(self.viewModel.decimalSeparator), at: String.Index(encodedOffset: textField.text!.count)) | |
} else { | |
textField.text!.insert(button.characterValue, at: String.Index(encodedOffset: text.count)) | |
} | |
} | |
guard let updatedText = textField.text else { | |
return self.viewModel.localCurrencyAmount.value = 0.0 | |
} | |
var string = updatedText | |
// Remove currency mark | |
for _ in 0..<self.viewModel.localCurrency.mark.count { | |
string.remove(at: string.startIndex) | |
} | |
string = self.formatForMath(string) | |
self.sendableAmountIsValid.value = textField.text != "\(self.viewModel.localCurrency.mark)" | |
self.viewModel.localCurrencyAmount.value = NSDecimalNumber(string: string == "" ? "0" : string) | |
} | |
viewModel.nanoAmount.producer.startWithValues { amount in | |
// TODO: fix case where local currency amount is selected, you scan a code with an amount and the translated amount isn't present | |
if self.activeTextField == self.nanoTextField || self.activeTextField == nil { | |
self.localCurrencyTextField?.text = self.convertNanoToLocalCurrency(value: amount) ?? "0\(self.viewModel.decimalSeparator)0" | |
} | |
} | |
viewModel.localCurrencyAmount.producer.startWithValues { amount in | |
if self.activeTextField == self.localCurrencyTextField { | |
let lastTradePrice = NSDecimalNumber(value: self.viewModel.priceService.lastNanoLocalCurrencyPrice.value) | |
// If there is an error with the PriceService | |
guard lastTradePrice.compare(0) == .orderedDescending else { | |
self.nanoTextField?.text = "Error Getting Nano Price" | |
self.sendableAmountIsValid.value = false | |
// TODO: make this more apparent to the user with online/offline UI states | |
return | |
} | |
let dividedAmount = amount.dividing(by: lastTradePrice) | |
let raw = dividedAmount.asRawValue | |
if raw.compare(self.viewModel.sendableNanoBalance) == .orderedDescending { | |
self.fillOutWithMaxBalance(showAlert: true) | |
} else { | |
let numberHandler = NSDecimalNumberHandler(roundingMode: .plain, scale: 2, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false) | |
let roundedAmount = dividedAmount.rounding(accordingToBehavior: numberHandler) | |
self.viewModel.nanoAmount.value = roundedAmount | |
self.nanoTextField?.text = roundedAmount .stringValue | |
} | |
} | |
} | |
} | |
func formatForMath(_ string: String) -> String { | |
guard let separator = viewModel.localCurrency.locale.decimalSeparator, separator != "." else { return string } | |
return string.replacingOccurrences(of: separator, with: ".") | |
} | |
func formatForView(_ string: String) -> String { | |
guard let separator = viewModel.localCurrency.locale.decimalSeparator else { return string } | |
return string.replacingOccurrences(of: ".", with: separator) | |
} | |
private func convertNanoToLocalCurrency(value: NSDecimalNumber) -> String? { | |
var val = value | |
// FIXME: implement a better fix for this | |
if value.stringValue.contains("0000000") || value.stringValue.count >= 20 { | |
val = NSDecimalNumber(string: value.rawAsUsableString) | |
} | |
let numberFormatter = NumberFormatter() | |
numberFormatter.numberStyle = .currency | |
numberFormatter.locale = viewModel.localCurrency.locale | |
let amount = val.doubleValue * viewModel.priceService.lastNanoLocalCurrencyPrice.value | |
return numberFormatter.string(from: NSNumber(floatLiteral: amount)) | |
} | |
@objc private func fillOutWithMaxBalance(showAlert: Bool = false) { | |
nanoTextField?.becomeFirstResponder() | |
activeTextField = nanoTextField | |
nanoTextField?.text = viewModel.sendableNanoBalance.rawAsUsableString | |
localCurrencyTextField?.text = self.convertNanoToLocalCurrency(value: viewModel.sendableNanoBalance) ?? "0\(self.viewModel.decimalSeparator)0" | |
viewModel.nanoAmount.value = viewModel.sendableNanoBalance | |
viewModel.maxAmountInUse = true | |
Answers.logCustomEvent(withName: "Send: Max Amount Used") | |
if activeTextField == nil { | |
self.addressTextView?.resignFirstResponder() | |
localCurrencyTextField?.setSmallerFontSize() | |
} | |
self.sendableAmountIsValid.value = true | |
// Crash happens when this hits | |
if showAlert { | |
self.viewModel.maxAmountInUse = true | |
// let ac = UIAlertController(title: "Amount Too Large", message: "The amount you entered was larger than your Nano balance.\n\nWe've filled the form with your full balance.", preferredStyle: .alert) | |
// ac.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil)) | |
// self.present(ac, animated: true, completion: nil) | |
} | |
} | |
@objc func sendNano() { | |
Answers.logCustomEvent(withName: "Send Nano Began") | |
// TODO: make address show error | |
guard | |
let textView = addressTextView, | |
let address = Address(textView.attributedText.string), | |
let work = viewModel.work | |
else { return } | |
let subtractor: NSDecimalNumber | |
let remainingBalance: NSDecimalNumber | |
if viewModel.maxAmountInUse { | |
subtractor = viewModel.sendableNanoBalance | |
remainingBalance = 0 | |
} else { | |
subtractor = viewModel.nanoAmount.value.asRawValue | |
remainingBalance = viewModel.sendableNanoBalance.subtracting(subtractor) | |
} | |
let endpoint = Endpoint.createSendBlock( | |
destination: address, | |
balanceHex: viewModel.hexify(balance: remainingBalance), | |
previous: viewModel.previousFrontierHash, | |
work: work, | |
privateKey: viewModel.privateKeyData | |
) | |
authenticateAndSend(endpoint: endpoint, amountYoullSend: subtractor) | |
} | |
private func authenticateAndSend(endpoint: Endpoint, amountYoullSend: NSDecimalNumber) { | |
guard let amount = amountYoullSend.rawAsLongerUsableString else { | |
self.showError(title: "Something went wrong.", message: "There was a problem sending Nano. Please try again.") | |
Crashlytics.sharedInstance().recordError(NanoWalletError.longUsableStringCastFailed) | |
return | |
} | |
let appDelegate = UIApplication.shared.delegate as! AppDelegate | |
appDelegate.appBackgroundingForSeedOrSend = true | |
let context = LAContext() | |
var error: NSError? | |
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) { | |
let reason = "Send \(amount) Nano?" | |
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { [unowned self] success, error in | |
DispatchQueue.main.async { | |
guard success else { | |
guard let error = error else { | |
Answers.logCustomEvent(withName: "Error with Send Authentication", customAttributes: ["description": "Generic error"]) | |
return self.showError(title: "There was a problem", message: "Please try again.") | |
} | |
Answers.logCustomEvent(withName: "Error with Send Authentication", customAttributes: ["description": error.localizedDescription]) | |
switch error { | |
case LAError.userCancel: return | |
case LAError.biometryLockout: | |
return self.showError(title: "Too Many Tries", message: "There were too many failed attempts. Please try your passcode.") | |
case LAError.biometryNotAvailable: | |
return self.showError(title: "Uh oh", message: "FaceID/TouchID are not available on your device.") | |
case LAError.biometryNotEnrolled: | |
return self.showError(title: "There was a problem", message: "Please add your face for FaceID or a fingerprint for TouchID.") | |
case LAError.authenticationFailed: | |
return self.showError(title: "There was a problem", message: "Please try again.") | |
case LAError.passcodeNotSet: | |
return self.showError(title: "Passcode Not Set", message: "Please set a passcode for your phone to send Nano (and for security reasons, in general).") | |
default: | |
return self.showError(title: "There was a problem", message: "Please try again.") | |
} | |
} | |
self.viewModel.socket.send(endpoint: endpoint) | |
self.viewModel.maxAmountInUse = false | |
Answers.logCustomEvent(withName: "Send Nano Finished") | |
appDelegate.appBackgroundingForSeedOrSend = false | |
self.delegate?.didFinishWithViewController() | |
} | |
} | |
} else { | |
showError(title: "Authentication Not Available", message: "Please set your iOS device up with a password, TouchID, or FaceID.") | |
} | |
} | |
private func showError(title: String, message: String, buttonText text: String = "Okay") { | |
let appDelegate = UIApplication.shared.delegate as! AppDelegate | |
appDelegate.appBackgroundingForSeedOrSend = false | |
let ac = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) | |
ac.addAction(UIAlertAction(title: text, style: .default)) | |
present(ac, animated: true) | |
} | |
} | |
extension SendViewController: UITextViewDelegate { | |
func textViewDidChange(_ textView: UITextView) { | |
if let address = Address(textView.text) { | |
textView.text = "" | |
textView.attributedText = addAttributes(forAttributedText: address.longAddressWithColor) | |
self.sendAddressIsValid.value = true | |
// TODO: make sure address is not mine too | |
} else { | |
self.sendAddressIsValid.value = false | |
textView.attributedText = handleRegularTextEntry(forAttributedText: textView.text) | |
} | |
} | |
// Allow any A-Z,0-9 character through for the seed as well as backspaces, prevent everything else | |
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { | |
let isBackspace = strcmp(text.cString(using: .utf8)!, "\\b") == -92 | |
// Only allow backspaces on already valid address | |
if sendAddressIsValid.value { | |
if text == "\n" { | |
textView.resignFirstResponder() | |
return true | |
} | |
return isBackspace | |
} | |
// if you paste in an address | |
if text.count > 60 { | |
self.addressTextView?.togglePlaceholder(show: false) | |
if let _ = Address(text) { | |
self.addressTextView?.resignFirstResponder() | |
} | |
return true | |
} | |
// Done key | |
guard text != "\n" else { | |
textView.resignFirstResponder() | |
return true | |
} | |
guard !isBackspace else { | |
if textView.text.count == 1 { | |
self.addressTextView?.togglePlaceholder(show: true) | |
} | |
return true | |
} | |
let validCharacters = ["a","b","c","d","e","f","g","h","i","j","k","m","n","o","p","q","r","s","t","u","w","x","y","z","1","3","4","5","6","7","8","9", "_"] | |
let isValidCharacter = validCharacters.contains(text.lowercased()) | |
if self.addressTextView!.text.count >= 0, isValidCharacter { | |
self.addressTextView?.togglePlaceholder(show: false) | |
} else { | |
return false | |
} | |
return isValidCharacter | |
} | |
} | |
/// Used for the Nano and local currency price text fields | |
extension SendViewController: UITextFieldDelegate { | |
func textFieldDidBeginEditing(_ textField: UITextField) { | |
guard let textField = textField as? SendTextField else { return } | |
self.activeTextField = textField | |
// This guard makes sure that fields aren't zeroed out after the alert shows when you enter over your max balance | |
guard !viewModel.maxAmountInUse else { | |
return viewModel.maxAmountInUse = false | |
} | |
if !self.isScanningAmount { | |
nanoTextField?.text = nil | |
localCurrencyTextField?.text = viewModel.localCurrency.mark | |
viewModel.nanoAmount.value = 0 | |
viewModel.localCurrencyAmount.value = 0 | |
} | |
if activeTextField == nanoTextField { | |
nanoTextField?.setLargerFontSize() | |
localCurrencyTextField?.setSmallerFontSize() | |
} else { | |
localCurrencyTextField?.setLargerFontSize() | |
nanoTextField?.setSmallerFontSize() | |
} | |
self.isScanningAmount = false | |
} | |
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { | |
guard let text = textField.text else { return false } | |
guard string.count == 1 else { return false } | |
if text == viewModel.localCurrency.mark && range.length == 1 && string == "<" { return false } | |
// handle backspace, NOTE: this needs to come after line above to prevent the user from removing the $ | |
if string == "<" { return true } | |
let separator = viewModel.decimalSeparator | |
if text.contains(separator), string == separator { return false } | |
if text == "0" && string != separator { return false } // Don't allow 000 | |
return true | |
} | |
} | |
extension SendViewController: SendKeyboardDelegate { | |
func valueWasSent(button: KeyboardButton) { | |
// If the user types without a field explicitly selected | |
if activeTextField == nil { | |
activeTextField = nanoTextField | |
nanoTextField?.becomeFirstResponder() | |
} | |
guard | |
let textField = activeTextField, | |
let selectedTextRange = textField.selectedTextRange, | |
let delegate = textField.delegate | |
else { return } | |
let range = NSMakeRange(selectedTextRange.end.hash, 1) | |
if delegate.textField!(textField, shouldChangeCharactersIn: range, replacementString: button.stringValue) { | |
if self.activeTextField == nanoTextField { | |
self.nanoObserver.send(value: button) | |
} else { | |
self.localCurrencyObserver.send(value: button) | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import ReactiveSwift | |
import SwiftWebSocket | |
final class SendViewModel { | |
let privateKeyData: Data | |
let sendableNanoBalance: NSDecimalNumber | |
let previousFrontierHash: String | |
let socket: WebSocket | |
let localCurrency: Currency | |
let groupingSeparator: String | |
let decimalSeparator: String | |
let nanoAmount = MutableProperty<NSDecimalNumber>(0) | |
let localCurrencyAmount = MutableProperty<NSDecimalNumber>(0) | |
var maxAmountInUse: Bool = false | |
let priceService = PriceService() | |
private (set) var work: String? | |
init(sendableNanoBalance: NSDecimalNumber, privateKeyData: Data, previousFrontierHash: String, socket: WebSocket, localCurrency: Currency) { | |
self.sendableNanoBalance = sendableNanoBalance | |
self.privateKeyData = privateKeyData | |
self.previousFrontierHash = previousFrontierHash | |
self.socket = socket | |
self.localCurrency = localCurrency | |
self.groupingSeparator = localCurrency.locale.groupingSeparator ?? "," | |
self.decimalSeparator = localCurrency.locale.decimalSeparator ?? "." | |
// Default values are USD grouping/separator values | |
// Create work for the transaction | |
DispatchQueue.global(qos: .background).async { | |
RaiCore().createWork(previousHash: previousFrontierHash) { self.work = $0 } | |
} | |
priceService.fetchLatestBTCLocalCurrencyPrice() | |
priceService.fetchLatestNanoLocalCurrencyPrice() | |
} | |
/// Turn the remaining balance into a hex | |
/// `balance` sent in should be raw value | |
/// e.g. 9993120000000000000000000000000 | |
func hexify(balance num: NSDecimalNumber) -> String { | |
var result = num | |
var hex = "" | |
let index: String.Index = hex.startIndex | |
// result > 0 | |
while result.compare(0) == .orderedDescending { | |
let radix: NSDecimalNumber = 16 | |
let handler = NSDecimalNumberHandler(roundingMode: .down, scale: 0, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false) | |
let quotient = result.dividing(by: radix, withBehavior: handler) | |
let subtractAmount = quotient.multiplying(by: radix) | |
let remainder = result.subtracting(subtractAmount).intValue | |
switch remainder { | |
case 0...9: hex.insert(String(remainder).first!, at: index) | |
case 10: hex.insert("A", at: index) | |
case 11: hex.insert("B", at: index) | |
case 12: hex.insert("C", at: index) | |
case 13: hex.insert("D", at: index) | |
case 14: hex.insert("E", at: index) | |
case 15: hex.insert("F", at: index) | |
default: | |
fatalError("Hexing problem") | |
} | |
result = quotient | |
} | |
return hex | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
SendTextField Producer 1 https://gist.github.com/zackshapiro/5d946a308a2252e1fe9d5a3037a77b17#file-sendviewcontroller-swift-L211
SendTextField Producer 2 https://gist.github.com/zackshapiro/5d946a308a2252e1fe9d5a3037a77b17#file-sendviewcontroller-swift-L257
NSDecimalProducer 1 (Nano amount) https://gist.github.com/zackshapiro/5d946a308a2252e1fe9d5a3037a77b17#file-sendviewcontroller-swift-L300
NSDecimalProducer 2 (Local currency amount) https://gist.github.com/zackshapiro/5d946a308a2252e1fe9d5a3037a77b17#file-sendviewcontroller-swift-L307
Code area where crash occurs https://gist.github.com/zackshapiro/5d946a308a2252e1fe9d5a3037a77b17#file-sendviewcontroller-swift-L383