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

feature: set feelimit percentage in settings #315

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
20 changes: 10 additions & 10 deletions Library/Extensions/Localizable/ExpiryTime+Localizable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ extension ExpiryTime: Localizable {
public var localized: String {
switch self {
case .oneMinute:
return L10n.ExpiryTime.oneMinute
return L10n.ExpiryTime.oneMinute
case .tenMinutes:
return L10n.ExpiryTime.tenMinutes
return L10n.ExpiryTime.tenMinutes
case .thirtyMinutes:
return L10n.ExpiryTime.thirtyMinutes
return L10n.ExpiryTime.thirtyMinutes
case .oneHour:
return L10n.ExpiryTime.oneHour
return L10n.ExpiryTime.oneHour
case .sixHours:
return L10n.ExpiryTime.sixHours
return L10n.ExpiryTime.sixHours
case .oneDay:
return L10n.ExpiryTime.oneDay
return L10n.ExpiryTime.oneDay
case .oneWeek:
return L10n.ExpiryTime.oneWeek
return L10n.ExpiryTime.oneWeek
case .thirtyDays:
return L10n.ExpiryTime.thirtyDays
return L10n.ExpiryTime.thirtyDays
case .oneYear:
return L10n.ExpiryTime.oneYear
}
return L10n.ExpiryTime.oneYear
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Library
//
// Created by Christopher Pinski on 10/26/19.
// Copyright © 2019 Zap. All rights reserved.
//

import Foundation
import SwiftLnd

extension PaymentFeeLimitPercentage: Localizable {
public var localized: String {
switch self {
case .zero:
return L10n.PaymentFeeLimitPercentage.none
default:
return "\(self.rawValue)%"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the NumberFormatter() from the SendViewController here. Maybe put it in its own extension file.
There are many different ways to format the percent sign (https://en.wikipedia.org/wiki/Percent_sign#Correct_style) 😅

}
}
}
16 changes: 16 additions & 0 deletions Library/Extensions/UIAlertController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,20 @@ extension UIAlertController {

return alertController
}

static func feeLimitAlertController(message: String, sendAction: @escaping () -> Void) -> UIAlertController {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like my UIAlertController extension any more 😅

Imo it would make sense to remove it and put the Alerts in the ViewControllers where they are actually used.
(as long as both Alerts are only used in one place).
Otherwise the code just gets separated without much benefit. just makes it harder to reason about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My other PR also has an alert added to this controller. I don't mind where they live but I would rather open an issue to refactor the alerts out of this extension and address it after the PRs are merged rather than refactor both PRs.

let title: String = L10n.Scene.Send.FeeAlert.title
let confirmButtonTitle: String = L10n.Scene.Send.FeeAlert.ConfirmButton.title

let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let cancelAlertAction = UIAlertAction(title: L10n.Scene.Send.FeeAlert.CancelButton.title, style: .cancel, handler: nil)

let confirmAlertAction = UIAlertAction(title: confirmButtonTitle, style: .default) { _ in
sendAction()
}
alertController.addAction(cancelAlertAction)
alertController.addAction(confirmAlertAction)

return alertController
}
}
31 changes: 31 additions & 0 deletions Library/Generated/strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ internal enum L10n {
}
}
}

internal enum PaymentFeeLimitPercentage {
/// None
internal static let none = L10n.tr("Localizable", "payment_fee_limit_percentage.none")
}

internal enum RpcConnectQrcodeError {
/// Could not read BTCPay configurations file.
Expand Down Expand Up @@ -588,15 +593,39 @@ internal enum L10n {
internal static let successLabel = L10n.tr("Localizable", "scene.send.success_label")
/// Send
internal static let title = L10n.tr("Localizable", "scene.send.title")
/// The fee for this payment (%@) exceeds the limit specified in the settings (%@).
internal static func feeExceedsUserLimit(_ p1: String, _ p2: String) -> String {
return L10n.tr("Localizable", "scene.send.fee_exceeds_user_limit", p1, p2)
}
/// The fee for this payment (%@ sats) will be higher than the payment amount (%@ sats).
internal static func feeExceedsPayment(_ p1: String, _ p2: String) -> String {
return L10n.tr("Localizable", "scene.send.fee_exceeds_payment_amount", p1, p2)
}
internal enum FeeAlert {
/// Fee Limit Alert
internal static let title = L10n.tr("Localizable", "scene.send.fee_alert.title")
internal enum CancelButton {
/// No
internal static let title = L10n.tr("Localizable", "scene.send.fee_alert.cancel_button.title")
}
internal enum ConfirmButton {
/// Yes
internal static let title = L10n.tr("Localizable", "scene.send.fee_alert.confirm_button.title")
}
}
internal enum Lightning {
/// Send Lightning Payment
internal static let title = L10n.tr("Localizable", "scene.send.lightning.title")
/// Do you really want to pay this invoice?
internal static let paymentConfirmation = L10n.tr("Localizable", "scene.send.lightning.payment_confirmation")
}
internal enum OnChain {
/// Fee:
internal static let fee = L10n.tr("Localizable", "scene.send.on_chain.fee")
/// Send On-Chain
internal static let title = L10n.tr("Localizable", "scene.send.on_chain.title")
/// Do you really want to send this payment?
internal static let paymentConfirmation = L10n.tr("Localizable", "scene.send.on_chain.payment_confirmation")
internal enum Fee {
/// Estimated Delivery: %@
internal static func estimatedDelivery(_ p1: String) -> String {
Expand Down Expand Up @@ -649,6 +678,8 @@ internal enum L10n {
internal static let currency = L10n.tr("Localizable", "scene.settings.item.currency")
/// Need Help?
internal static let help = L10n.tr("Localizable", "scene.settings.item.help")
/// Lightning Payment Fee Limit
internal static let lightningPaymentFeeLimit = L10n.tr("Localizable", "scene.settings.item.lightning_payment_fee_limit")
/// Lightning Request Expiry
internal static let lightningRequestExpiry = L10n.tr("Localizable", "scene.settings.item.lightning_request_expiry")
/// Show lnd Log
Expand Down
115 changes: 86 additions & 29 deletions Library/Scenes/ModalDetail/Send/SendViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@ import SwiftBTC
import SwiftLnd
import UIKit

extension Formatter {
static let asPercentage: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .percent
formatter.minimumSignificantDigits = 1
formatter.maximumSignificantDigits = 3
formatter.multiplier = 1
return formatter
}()
}

extension Int {
var formattedAsPercentage: String {
return Formatter.asPercentage.string(for: self) ?? ""
}
}

extension Decimal {
var formattedAsPercentage: String {
return Formatter.asPercentage.string(for: self) ?? ""
}
}

final class SendViewController: ModalDetailViewController {
private let viewModel: SendViewModel
private let authenticationViewModel: AuthenticationViewModel
Expand Down Expand Up @@ -87,6 +110,19 @@ final class SendViewController: ModalDetailViewController {
self?.amountInputView?.subtitleTextColor = $0
}
.dispose(in: reactive.bag)

viewModel.sendStatus.observeNext { [weak self] feeLimitPercent in
self?.triggerSend(feeLimitPercent: feeLimitPercent)
}.dispose(in: reactive.bag)

viewModel.sendStatus.observeFailed { [weak self] error in
switch error {
case .feeGreaterThanPayment(let feeInfo):
self?.showFeeLimitAlert(feePercentage: feeInfo.feePercentage, userFeeLimitPercent: feeInfo.userFeeLimitPercentage, sendFeeLimitPercent: feeInfo.sendFeeLimitPercentage)
case .feePercentageGreaterThanUserLimit(let feeInfo):
self?.showFeeLimitAlert(feePercentage: feeInfo.feePercentage, userFeeLimitPercent: feeInfo.userFeeLimitPercentage, sendFeeLimitPercent: feeInfo.sendFeeLimitPercentage)
}
}.dispose(in: reactive.bag)
}

private func addAmountInputView() {
Expand Down Expand Up @@ -160,14 +196,29 @@ final class SendViewController: ModalDetailViewController {
}

private func sendButtonTapped() {
authenticate { [weak self] result in
switch result {
case .success:
self?.send()
case .failure:
Toast.presentError(L10n.Scene.Send.authenticationFailed)
}
viewModel.determineSendStatus()
}

private func showFeeLimitAlert(feePercentage: Decimal, userFeeLimitPercent: Int, sendFeeLimitPercent: Int?) {
let message: String
switch viewModel.method {
case .onChain:
message = """
\(L10n.Scene.Send.feeExceedsUserLimit(feePercentage.formattedAsPercentage, userFeeLimitPercent.formattedAsPercentage))

\(L10n.Scene.Send.OnChain.paymentConfirmation)
"""
case .lightning:
message = """
\(L10n.Scene.Send.feeExceedsUserLimit(feePercentage.formattedAsPercentage, userFeeLimitPercent.formattedAsPercentage))

\(L10n.Scene.Send.Lightning.paymentConfirmation)
"""
}
let controller = UIAlertController.feeLimitAlertController(message: message) { [weak self] in
self?.triggerSend(feeLimitPercent: sendFeeLimitPercent)
}
self.present(controller, animated: true)
}

private func presentLoading() {
Expand Down Expand Up @@ -252,29 +303,35 @@ final class SendViewController: ModalDetailViewController {
self?.dismissParent()
}
}

private func send() {
let sendStartTime = Date()

presentLoading()

viewModel.send { [weak self] result in
let minimumLoadingTime: TimeInterval = 1
let sendingTime = Date().timeIntervalSince(sendStartTime)
let delay = max(0, minimumLoadingTime - sendingTime)

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delay) {
switch result {
case .success:

self?.presentSuccess()
case .failure(let error):
UINotificationFeedbackGenerator().notificationOccurred(.error)

Toast.presentError(error.localizedDescription)
self?.amountInputView?.isEnabled = true
self?.recoverFromLoadingState()

private func triggerSend(feeLimitPercent: Int?) {
authenticate { [weak self] result in
switch result {
case .success:
let sendStartTime = Date()

self?.presentLoading()
self?.viewModel.send(feeLimitPercent: feeLimitPercent) { [weak self] result in
let minimumLoadingTime: TimeInterval = 1
let sendingTime = Date().timeIntervalSince(sendStartTime)
let delay = max(0, minimumLoadingTime - sendingTime)

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delay) {
switch result {
case .success:

self?.presentSuccess()
case .failure(let error):
UINotificationFeedbackGenerator().notificationOccurred(.error)

Toast.presentError(error.localizedDescription)
self?.amountInputView?.isEnabled = true
self?.recoverFromLoadingState()
}
}
}
case .failure:
Toast.presentError(L10n.Scene.Send.authenticationFailed)
}
}
}
Expand Down
Loading