Skip to content
This repository has been archived by the owner on Nov 17, 2023. It is now read-only.

Commit

Permalink
feature: Error messaging for send amount too small (#297)
Browse files Browse the repository at this point in the history
commit 7dace9e2532da36faa84ee2aeb3ca1e8eda42d00
Merge: 305bce9 a04e48d
Author: Otto Suess <[email protected]>
Date:   Thu Nov 14 10:34:26 2019 +0100

    Merge branch 'dustEstimateError' of github.com:chitowncrispy/zap-iOS into chitowncrispy-dustEstimateError

    # Conflicts:
    #	SwiftLnd/Api/Helper/LndApiError.swift
    #	SwiftLnd/Api/Helper/RpcZapHelper.swift

commit a04e48d
Author: Christopher Pinski <[email protected]>
Date:   Sat Nov 9 17:49:19 2019 -0600

    Created a LoadingError which translates any error from the app to a specific UI state

commit 369f379
Author: Christopher Pinski <[email protected]>
Date:   Thu Oct 31 13:06:08 2019 -0500

    Using a result type for the Loadable enum element case

commit 31c8d9d
Author: Christopher Pinski <[email protected]>
Date:   Thu Oct 31 09:47:33 2019 -0500

    Setting initial value for subtitleText back to nil since we aren't showing an error initially

commit 3dabcde
Author: Christopher Pinski <[email protected]>
Date:   Wed Oct 30 23:02:00 2019 -0500

    Removing error messaging from fee label

commit a5b7658
Author: Christopher Pinski <[email protected]>
Date:   Fri Oct 25 08:52:26 2019 -0500

    Cleaning up code based on PR comments

commit 4f3094b
Author: Christopher Pinski <[email protected]>
Date:   Thu Oct 24 20:19:53 2019 -0500

    Setting up the primary currency listener only once

commit 2b3f895
Author: Christopher Pinski <[email protected]>
Date:   Thu Oct 24 13:01:42 2019 -0500

    Updating the view to show the on chain balance when the user hasn't input any amount

commit cfe8917
Author: Christopher Pinski <[email protected]>
Date:   Wed Oct 23 20:47:51 2019 -0500

    Not allowing the user to send if the transaction is dust

commit 54986bd
Author: Christopher Pinski <[email protected]>
Date:   Wed Oct 23 20:00:08 2019 -0500

    Rebasing PR

Co-authored-by: Christopher Pinski <[email protected]>
  • Loading branch information
chitowncrispy authored and ottosuess committed Nov 14, 2019
1 parent 305bce9 commit 8f12a41
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 28 deletions.
2 changes: 2 additions & 0 deletions Library/Generated/strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,8 @@ internal enum L10n {
internal static let maximumFee = L10n.tr("Localizable", "scene.send.maximum_fee")
/// Memo:
internal static let memoHeadline = L10n.tr("Localizable", "scene.send.memo_headline")
/// Send amount is too small.
internal static let sendAmountTooSmall = L10n.tr("Localizable", "scene.send.send_amount_too_small")
/// Send
internal static let sendButton = L10n.tr("Localizable", "scene.send.send_button")
/// Sending...
Expand Down
21 changes: 13 additions & 8 deletions Library/Scenes/ModalDetail/Send/LoadingAmountView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ import Bond
import Foundation
import ReactiveKit
import SwiftBTC
import SwiftLnd

enum Loadable<E> {
case loading
case element(E)
}

enum LoadingError: Error {
case invalidAmount
case lndApiError(LndApiError)
}

final class LoadingAmountView: UIView {
let amountLabel: UILabel
let activityIndicator: UIActivityIndicatorView
Expand All @@ -29,7 +35,7 @@ final class LoadingAmountView: UIView {
}
}

init(loadable: Observable<Loadable<Satoshi?>>) {
init(loadable: Observable<Loadable<Result<Satoshi, LoadingError>>>) {
amountLabel = UILabel(frame: CGRect.zero)
activityIndicator = UIActivityIndicatorView(style: .white)

Expand Down Expand Up @@ -63,25 +69,24 @@ final class LoadingAmountView: UIView {
fatalError("init(coder:) has not been implemented")
}

private func updateLoadable(_ loadable: Loadable<Satoshi?>) {
private func updateLoadable(_ loadable: Loadable<Result<Satoshi, LoadingError>>) {
switch loadable {
case .loading:
activityIndicator.isHidden = false
amountLabel.isHidden = true
case .element(let amount):
case .element(let result):
disposable?.dispose()
amountLabel.isHidden = false
activityIndicator.isHidden = true

if let amount = amount {

switch result {
case .success(let amount):
disposable = amount
.bind(to: amountLabel, currency: Settings.shared.primaryCurrency)
disposable?.dispose(in: reactive.bag)
} else {
case .failure:
amountLabel.text = "-"
}

}

}
}
63 changes: 50 additions & 13 deletions Library/Scenes/ModalDetail/Send/SendViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ final class SendViewModel: NSObject {
}
}

let fee = Observable<Loadable<Satoshi?>>(.loading)
let fee = Observable<Loadable<Result<Satoshi, LoadingError>>>(.loading)

let method: SendMethod

Expand All @@ -59,6 +59,7 @@ final class SendViewModel: NSObject {
didSet {
guard oldValue != amount else { return }
updateFee()
updateSubtitle()
updateIsUIEnabled()
}
}
Expand All @@ -68,6 +69,13 @@ final class SendViewModel: NSObject {
updateFee()
}
}

var isTransactionDust = false {
didSet {
updateIsUIEnabled()
updateSubtitle()
}
}

var isSending = false {
didSet {
Expand Down Expand Up @@ -118,10 +126,10 @@ final class SendViewModel: NSObject {

updateFee()
updateIsUIEnabled()
updateSubtitle()
setupPrimaryCurrencyListener()
}

private func updateSubtitle() {
private func setupPrimaryCurrencyListener() {
Settings.shared.primaryCurrency
.compactMap { [method, maxPaymentAmount] in
guard let amount = $0.format(satoshis: maxPaymentAmount) else { return nil }
Expand All @@ -137,11 +145,28 @@ final class SendViewModel: NSObject {
}
.dispose(in: reactive.bag)
}

private func updateSubtitle() {
if isTransactionDust && amount ?? 0 > 0 {
self.subtitleText.value = L10n.Scene.Send.sendAmountTooSmall
} else {
guard let amount = Settings.shared.primaryCurrency.value.format(satoshis: maxPaymentAmount) else {
return
}

switch method {
case .lightning:
self.subtitleText.value = L10n.Scene.Send.Subtitle.lightningCanSendBalance(amount)
case .onChain:
self.subtitleText.value = L10n.Scene.Send.Subtitle.onChainBalance(amount)
}
}
}

private func updateIsUIEnabled() {
isSendButtonEnabled.value = isAmountValid && !isSending
isSendButtonEnabled.value = isAmountValid && !isSending && !isTransactionDust
isInputViewEnabled.value = !isSending
isSubtitleTextWarning.value = amount ?? 0 > maxPaymentAmount
isSubtitleTextWarning.value = amount ?? 0 > maxPaymentAmount || (isTransactionDust && amount ?? 0 > 0)
}

private var isAmountValid: Bool {
Expand All @@ -152,24 +177,36 @@ final class SendViewModel: NSObject {
private func updateFee() {
if isAmountValid {
fee.value = .loading
updateIsUIEnabled()
debounceFetchFee()
} else {
fee.value = .element(nil)
fee.value = .element(.failure(.invalidAmount))
}
}

private func fetchFee() {
guard let amount = amount else { return }

let feeCompletion = { [weak self] (result: (amount: Satoshi, fee: Satoshi?)) -> Void in
let feeCompletion = { [weak self] (result: Result<(amount: Satoshi, fee: Satoshi?), LndApiError>) -> Void in
guard
let self = self,
result.amount == self.amount
let self = self
else { return }

self.fee.value = .element(result.fee)
self.updateIsUIEnabled()
switch result {
case .success(let result):
guard
result.amount == self.amount
else { return }

self.isTransactionDust = false
if let fee = result.fee {
self.fee.value = .element(.success(fee))
} else {
self.fee.value = .element(.failure(.invalidAmount))
}
case .failure(let lndApiError):
self.isTransactionDust = lndApiError == .transactionOutputIsDust
self.fee.value = amount > 0 ? .element(Result.failure(.lndApiError(lndApiError))) : .element(.failure(.invalidAmount))
self.updateIsUIEnabled()
}
}

switch method {
Expand Down
5 changes: 3 additions & 2 deletions Library/Views/OnChainFeeView/OnChainFeeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Bond
import Foundation
import SwiftBTC
import SwiftLnd

protocol OnChainFeeViewDelegate: class {
func confirmationTargetChanged(to confirmationTarget: Int)
Expand Down Expand Up @@ -35,7 +36,7 @@ final class OnChainFeeView: UIView {

weak var delegate: OnChainFeeViewDelegate?

init(loadable: Observable<Loadable<Satoshi?>>) {
init(loadable: Observable<Loadable<Result<Satoshi, LoadingError>>>) {
super.init(frame: .zero)
setup(loadable: loadable)
}
Expand All @@ -44,7 +45,7 @@ final class OnChainFeeView: UIView {
fatalError("init(coder:) has not been implemented")
}

private func setup(loadable: Observable<Loadable<Satoshi?>>) {
private func setup(loadable: Observable<Loadable<Result<Satoshi, LoadingError>>>) {
Bundle.library.loadNibNamed("OnChainFeeView", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds
Expand Down
1 change: 1 addition & 0 deletions Library/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"scene.send.address_headline" = "To:";
"scene.send.memo_headline" = "Memo:";
"scene.send.sending" = "Sending...";
"scene.send.send_amount_too_small" = "Send amount is too small.";
"scene.send.lightning.title" = "Send Lightning Payment";
"scene.send.on_chain.title" = "Send On-Chain";
"scene.send.on_chain.fee" = "Fee:";
Expand Down
14 changes: 9 additions & 5 deletions Lightning/Services/TransactionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,23 @@ public final class TransactionService {
/// Fee methods can be called multiple times at once. So the completion
/// includes amount to match on the caller side that the returned result is
/// for the correct amount.
public typealias FeeApiCompletion = ((amount: Satoshi, fee: Satoshi?)) -> Void
public typealias FeeApiCompletion = (Result<(amount: Satoshi, fee: Satoshi?), LndApiError>) -> Void

public func lightningFees(for paymentRequest: PaymentRequest, amount: Satoshi, completion: @escaping FeeApiCompletion) {
api.route(destination: paymentRequest.destination, amount: amount) { result in
let totalFees = (try? result.get())?.totalFees
completion((amount: amount, fee: totalFees))
completion(.success((amount: amount, fee: totalFees)))
}
}

public func onChainFees(address: BitcoinAddress, amount: Satoshi, confirmationTarget: Int, completion: @escaping FeeApiCompletion) {
api.estimateFees(address: address, amount: amount, confirmationTarget: confirmationTarget) {
let fees = (try? $0.get())?.total
completion((amount: amount, fee: fees))
api.estimateFees(address: address, amount: amount, confirmationTarget: confirmationTarget) { result in
switch result {
case .success(let feeEstimate):
completion(.success((amount: amount, fee: feeEstimate.total)))
case .failure(let lndApiError):
completion(.failure(lndApiError))
}
}
}

Expand Down

0 comments on commit 8f12a41

Please sign in to comment.