Path: blob/main/Signal/src/ViewControllers/Payments/SendPaymentHelper.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
public struct SendPaymentInfo {
let recipient: SendPaymentRecipient
let paymentAmount: TSPaymentAmount
let estimatedFeeAmount: TSPaymentAmount
let currencyConversion: CurrencyConversionInfo?
let memoMessage: String?
let isOutgoingTransfer: Bool
}
// MARK: -
// TODO: Add support for requests.
public struct SendRequestInfo {
let recipientAddress: SignalServiceAddress
let paymentAmount: TSPaymentAmount
let estimatedFeeAmount: TSPaymentAmount
let currencyConversion: CurrencyConversionInfo?
let memoMessage: String?
}
// MARK: -
protocol SendPaymentHelperDelegate: AnyObject {
func balanceDidChange()
func currencyConversionDidChange()
}
// MARK: -
class SendPaymentHelper {
private weak var delegate: SendPaymentHelperDelegate?
private var _currentCurrencyConversion: CurrencyConversionInfo?
var currentCurrencyConversion: CurrencyConversionInfo? {
get {
AssertIsOnMainThread()
return _currentCurrencyConversion
}
set {
AssertIsOnMainThread()
_currentCurrencyConversion = newValue
}
}
private var maximumPaymentAmount: TSPaymentAmount?
init(delegate: SendPaymentHelperDelegate) {
self.delegate = delegate
addObservers()
updateCurrentCurrencyConversion()
updateMaximumPaymentAmount()
}
private func addObservers() {
NotificationCenter.default.addObserver(
self,
selector: #selector(currentPaymentBalanceDidChange),
name: PaymentsImpl.currentPaymentBalanceDidChange,
object: nil,
)
NotificationCenter.default.addObserver(
self,
selector: #selector(paymentConversionRatesDidChange),
name: PaymentsCurrenciesImpl.paymentConversionRatesDidChange,
object: nil,
)
}
@MainActor
func refreshObservedValues() {
updateCurrentCurrencyConversion()
SUIEnvironment.shared.paymentsSwiftRef.updateCurrentPaymentBalance()
SSKEnvironment.shared.paymentsCurrenciesRef.updateConversionRates()
}
static let minTopVSpacing: CGFloat = 16
static let vSpacingAboveBalance: CGFloat = 20
static func buildBottomButton(
title: String,
target: Any,
selector: Selector,
) -> UIView {
let button = OWSFlatButton.insetButton(
title: title,
font: bottomButtonFont,
titleColor: .white,
backgroundColor: .ows_accentBlue,
target: target,
selector: selector,
)
button.autoSetHeightUsingFont(extraVerticalInsets: 6)
return button
}
static func buildBottomButtonStack(_ subviews: [UIView]) -> UIView {
let buttonStack = UIStackView(arrangedSubviews: subviews)
buttonStack.axis = .horizontal
buttonStack.spacing = 20
buttonStack.distribution = .fillEqually
buttonStack.alignment = .center
buttonStack.autoSetDimension(.height, toSize: bottomControlHeight)
return buttonStack
}
static let progressIndicatorSize: CGFloat = 48
// To avoid layout jitter, all of the "bottom controls"
// (buttons, progress indicator, error indicator) occupy
// the same exact height.
static var bottomControlHeight: CGFloat {
max(
progressIndicatorSize,
OWSFlatButton.heightForFont(bottomButtonFont) + 2.0,
)
}
static var bottomButtonFont: UIFont {
UIFont.dynamicTypeHeadline
}
static func buildBottomLabel() -> UILabel {
let label = UILabel()
label.font = .dynamicTypeSubheadlineClamped
label.textColor = Theme.secondaryTextAndIconColor
label.textAlignment = .center
return label
}
func updateBalanceLabel(_ balanceLabel: UILabel) {
AssertIsOnMainThread()
guard let maximumPaymentAmount = self.maximumPaymentAmount else {
// Use whitespace to ensure that the height of the label
// is constant, avoiding layout jitter.
balanceLabel.text = " "
return
}
let format = OWSLocalizedString(
"PAYMENTS_NEW_PAYMENT_BALANCE_FORMAT",
comment: "Format for the 'balance' indicator. Embeds {{ the current payments balance }}.",
)
balanceLabel.text = String.nonPluralLocalizedStringWithFormat(
format,
Self.formatMobileCoinAmount(maximumPaymentAmount),
)
}
private func updateMaximumPaymentAmount() {
Task { @MainActor [weak self] in
do {
let maximumPaymentAmount = try await SUIEnvironment.shared.paymentsSwiftRef.maximumPaymentAmount()
self?.maximumPaymentAmount = maximumPaymentAmount
self?.delegate?.balanceDidChange()
} catch PaymentsError.insufficientFunds {
self?.maximumPaymentAmount = TSPaymentAmount(currency: .mobileCoin, picoMob: 0)
self?.delegate?.balanceDidChange()
} catch {
owsFailDebugUnlessMCNetworkFailure(error)
}
}
delegate?.balanceDidChange()
}
@objc
private func currentPaymentBalanceDidChange() {
delegate?.balanceDidChange()
updateMaximumPaymentAmount()
}
@objc
private func paymentConversionRatesDidChange() {
updateCurrentCurrencyConversion()
}
private func updateCurrentCurrencyConversion() {
let localCurrencyCode = SSKEnvironment.shared.paymentsCurrenciesRef.currentCurrencyCode
let currentCurrencyConversion = SSKEnvironment.shared.paymentsCurrenciesRef.conversionInfo(forCurrencyCode: localCurrencyCode)
guard
!CurrencyConversionInfo.areEqual(
currentCurrencyConversion,
self.currentCurrencyConversion,
)
else {
// Did not change.
return
}
self.currentCurrencyConversion = currentCurrencyConversion
delegate?.currencyConversionDidChange()
}
static func formatMobileCoinAmount(_ paymentAmount: TSPaymentAmount) -> String {
owsAssertDebug(paymentAmount.isValidAmount(canBeEmpty: true))
owsAssertDebug(paymentAmount.currency == .mobileCoin)
owsAssertDebug(paymentAmount.picoMob >= 0)
let formattedAmount = PaymentsFormat.format(
paymentAmount: paymentAmount,
isShortForm: false,
)
let format = OWSLocalizedString(
"PAYMENTS_NEW_PAYMENT_CURRENCY_FORMAT",
comment: "Format for currency amounts in the 'send payment' UI. Embeds {{ %1$@ the current payments balance, %2$@ the currency indicator }}.",
)
return String.nonPluralLocalizedStringWithFormat(
format,
formattedAmount,
PaymentsConstants.mobileCoinCurrencyIdentifier,
)
}
}
extension SendPaymentHelperDelegate {
var minTopVSpacing: CGFloat { SendPaymentHelper.minTopVSpacing }
var vSpacingAboveBalance: CGFloat { SendPaymentHelper.vSpacingAboveBalance }
var bottomControlHeight: CGFloat { SendPaymentHelper.bottomControlHeight }
func buildBottomButtonStack(_ subviews: [UIView]) -> UIView {
SendPaymentHelper.buildBottomButtonStack(subviews)
}
func buildBottomButton(title: String, target: Any, selector: Selector) -> UIView {
SendPaymentHelper.buildBottomButton(title: title, target: target, selector: selector)
}
func buildBottomLabel() -> UILabel {
SendPaymentHelper.buildBottomLabel()
}
func formatMobileCoinAmount(_ paymentAmount: TSPaymentAmount) -> String {
SendPaymentHelper.formatMobileCoinAmount(paymentAmount)
}
}