Update price display (#4736)

* Update price display

As proposed by @dstrukt in #4364.

* Update format

* Unify price display across the app

* Add DisplayFormatter

* Replace DisplayFormatCurrency method

* Use symbol currency format for invoice

* Unify currency formats on backend pages

* Revert recent changes

* Do not show exchange rate and fiat order amount for crypto denominations

* Fix test and add test cases
This commit is contained in:
d11n 2023-03-13 02:12:58 +01:00 committed by GitHub
parent f3d9e07c5e
commit ded0c8a3bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 269 additions and 152 deletions

View file

@ -5,7 +5,6 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using BTCPayServer.Rating;
using NBitcoin;
using Newtonsoft.Json;
@ -28,14 +27,6 @@ namespace BTCPayServer.Services.Rates
}
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
public string FormatCurrency(string price, string currency)
{
return FormatCurrency(decimal.Parse(price, CultureInfo.InvariantCulture), currency);
}
public string FormatCurrency(decimal price, string currency)
{
return price.ToString("C", GetCurrencyProvider(currency));
}
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
{
@ -56,6 +47,7 @@ namespace BTCPayServer.Services.Rates
currencyInfo.CurrencySymbol = currency;
return currencyInfo;
}
public NumberFormatInfo GetNumberFormatInfo(string currency)
{
var curr = GetCurrencyProvider(currency);
@ -65,6 +57,7 @@ namespace BTCPayServer.Services.Rates
return ni;
return null;
}
public IFormatProvider GetCurrencyProvider(string currency)
{
lock (_CurrencyProviders)
@ -104,30 +97,6 @@ namespace BTCPayServer.Services.Rates
currencyProviders.TryAdd(code, number);
}
/// <summary>
/// Format a currency like "0.004 $ (USD)", round to significant divisibility
/// </summary>
/// <param name="value">The value</param>
/// <param name="currency">Currency code</param>
/// <returns></returns>
public string DisplayFormatCurrency(decimal value, string currency)
{
var provider = GetNumberFormatInfo(currency, true);
var currencyData = GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
value = value.RoundToSignificant(ref divisibility);
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return value.ToString("C", provider);
else
return value.ToString("C", provider) + $" ({currency})";
}
readonly Dictionary<string, CurrencyData> _Currencies;
static CurrencyData[] LoadCurrency()

View file

@ -396,21 +396,21 @@ namespace BTCPayServer.Tests
}
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
Assert.Contains("$5,500.00", s.Driver.PageSource); // Should propose reimburse in fiat
Assert.Contains("1.10000000 ", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before
Assert.Contains("2.20000000 ", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate
Assert.Contains("5,500.00 USD", s.Driver.PageSource); // Should propose reimburse in fiat
Assert.Contains("1.10000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before
Assert.Contains("2.20000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate
s.Driver.WaitForAndClick(By.Id(rateSelection));
s.Driver.FindElement(By.Id("ok")).Click();
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));
Assert.Contains("pull-payments", s.Driver.Url);
if (rateSelection == "FiatOption")
Assert.Contains("$5,500.00", s.Driver.PageSource);
Assert.Contains("5,500.00 USD", s.Driver.PageSource);
if (rateSelection == "CurrentOption")
Assert.Contains("2.20000000 ", s.Driver.PageSource);
Assert.Contains("2.20000000 BTC", s.Driver.PageSource);
if (rateSelection == "RateThenOption")
Assert.Contains("1.10000000 ", s.Driver.PageSource);
Assert.Contains("1.10000000 BTC", s.Driver.PageSource);
s.GoToInvoice(invoice.Id);
s.Driver.FindElement(By.Id("IssueRefund")).Click();
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));

View file

@ -73,6 +73,14 @@ namespace BTCPayServer.Tests
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
s.Driver.ElementDoesNotExist(By.Id("PayByLNURL"));
// Details should show exchange rate
s.Driver.ToggleCollapse("PaymentDetails");
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalPrice"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-AmountDue"));
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("sat/byte", s.Driver.FindElement(By.Id("PaymentDetails-RecommendedFee")).Text);
// Switch to LNURL
s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Click();
TestUtils.Eventually(() =>
@ -86,7 +94,7 @@ namespace BTCPayServer.Tests
// Default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
invoiceId = s.CreateInvoice(21000, "SATS", defaultPaymentMethod: "BTC_LightningLike");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
@ -112,6 +120,14 @@ namespace BTCPayServer.Tests
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
// Details should not show exchange rate
s.Driver.ToggleCollapse("PaymentDetails");
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-ExchangeRate"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-RecommendedFee"));
Assert.Contains("21 000 sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
Assert.Contains("21 000 sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
// Expire
var expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
@ -145,6 +161,10 @@ namespace BTCPayServer.Tests
Assert.Contains("Exchange Rate", details.Text);
Assert.Contains("Amount Due", details.Text);
Assert.Contains("Recommended Fee", details.Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
// Pay partial amount
await Task.Delay(200);
@ -218,6 +238,14 @@ namespace BTCPayServer.Tests
Assert.Contains("&lightning=LNBCRT", qrValue);
s.Driver.FindElement(By.Id("PayByLNURL"));
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
Assert.Contains("1 BTC = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
Assert.Contains("BTC", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
// Switch to amount displayed in sats
s.GoToHome();
s.GoToStore(StoreNavPages.CheckoutAppearance);
@ -227,7 +255,15 @@ namespace BTCPayServer.Tests
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
// BIP21 with LN as default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
@ -238,6 +274,14 @@ namespace BTCPayServer.Tests
Assert.StartsWith("bitcoin:", payUrl);
Assert.Contains("&lightning=lnbcrt", payUrl);
s.Driver.FindElement(By.Id("PayByLNURL"));
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-AmountDue")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.Id("PaymentDetails-TotalPrice")).Text);
// Ensure LNURL is enabled
s.GoToHome();
@ -263,6 +307,14 @@ namespace BTCPayServer.Tests
Assert.StartsWith("lnurl", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue);
s.Driver.FindElement(By.Id("PayByLNURL"));
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
Assert.Contains("1 sat = ", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
Assert.Contains("$", s.Driver.FindElement(By.Id("PaymentDetails-ExchangeRate")).Text);
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalFiat"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-AmountDue"));
s.Driver.ElementDoesNotExist(By.Id("PaymentDetails-TotalPrice"));
// Expiry message should not show amount for top-up invoice
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));

View file

@ -51,7 +51,6 @@ namespace BTCPayServer.Tests
{
public FastTests(ITestOutputHelper helper) : base(helper)
{
}
class DockerImage
{
@ -600,15 +599,16 @@ namespace BTCPayServer.Tests
[Fact]
public void RoundupCurrenciesCorrectly()
{
DisplayFormatter displayFormatter = new (CurrencyNameTable.Instance);
foreach (var test in new[]
{
(0.0005m, "$0.0005 (USD)", "USD"), (0.001m, "$0.001 (USD)", "USD"), (0.01m, "$0.01 (USD)", "USD"),
(0.1m, "$0.10 (USD)", "USD"), (0.1m, "0,10 € (EUR)", "EUR"), (1000m, "¥1,000 (JPY)", "JPY"),
(1000.0001m, "1,000.00 (INR)", "INR"),
(0.0m, "$0.00 (USD)", "USD")
(0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"),
(0.1m, "0.10 USD", "USD"), (0.1m, "0,10 EUR", "EUR"), (1000m, "1,000 JPY", "JPY"),
(1000.0001m, "1,000.00 INR", "INR"),
(0.0m, "0.00 USD", "USD")
})
{
var actual = CurrencyNameTable.Instance.DisplayFormatCurrency(test.Item1, test.Item3);
var actual = displayFormatter.Currency(test.Item1, test.Item3);
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
Assert.Equal(test.Item2, actual);
}

View file

@ -578,8 +578,7 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
});
Assert.Contains(s.Server.PayTester.GetService<CurrencyNameTable>().DisplayFormatCurrency(100, "USD"),
s.Driver.PageSource);
Assert.Contains("100.00 USD", s.Driver.PageSource);
Assert.Contains(i, s.Driver.PageSource);
s.GoToInvoices(s.StoreId);

View file

@ -1,6 +1,8 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client.Models
@using BTCPayServer.Services
@using BTCPayServer.Services.Invoices
@inject DisplayFormatter DisplayFormatter
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
@ -63,7 +65,7 @@
</span>
}
</td>
<td class="text-end">@invoice.AmountCurrency</td>
<td class="text-end">@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</td>
</tr>
}
</tbody>

View file

@ -7,7 +7,8 @@ public class StoreRecentInvoiceViewModel
{
public string InvoiceId { get; set; }
public string OrderId { get; set; }
public string AmountCurrency { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; }
public InvoiceState Status { get; set; }
public DateTimeOffset Date { get; set; }
public bool HasRefund { get; set; }

View file

@ -61,7 +61,8 @@ public class StoreRecentInvoices : ViewComponent
HasRefund = invoice.Refunds.Any(),
InvoiceId = invoice.Id,
OrderId = invoice.Metadata.OrderId ?? string.Empty,
AmountCurrency = _currencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
Amount = invoice.Price,
Currency = invoice.Currency
}).ToList();
return View(vm);

View file

@ -1,4 +1,6 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Services
@inject DisplayFormatter DisplayFormatter
@model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.Store.Id">
@ -49,11 +51,11 @@
</td>
@if (tx.Positive)
{
<td class="text-end text-success">@tx.Balance</td>
<td class="text-end text-success">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
}
else
{
<td class="text-end text-danger">@tx.Balance</td>
<td class="text-end text-danger">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
}
</tr>
}

View file

@ -5,6 +5,7 @@ namespace BTCPayServer.Components.StoreRecentTransactions;
public class StoreRecentTransactionViewModel
{
public string Id { get; set; }
public string Currency { get; set; }
public string Balance { get; set; }
public bool Positive { get; set; }
public bool IsConfirmed { get; set; }

View file

@ -58,6 +58,7 @@ public class StoreRecentTransactions : ViewComponent
Id = tx.TransactionId.ToString(),
Positive = tx.BalanceChange.GetValue(network) >= 0,
Balance = tx.BalanceChange.ShowMoney(network),
Currency = vm.CryptoCode,
IsConfirmed = tx.Confirmations != 0,
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, tx.TransactionId.ToString()),
Timestamp = tx.SeenAt

View file

@ -13,6 +13,7 @@ using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models.CustodianAccountViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Custodian.Client;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
@ -32,13 +33,13 @@ namespace BTCPayServer.Controllers
{
private readonly IEnumerable<ICustodian> _custodianRegistry;
private readonly CustodianAccountRepository _custodianAccountRepository;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly BTCPayServerClient _btcPayServerClient;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly LinkGenerator _linkGenerator;
public UICustodianAccountsController(
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
UserManager<ApplicationUser> userManager,
CustodianAccountRepository custodianAccountRepository,
IEnumerable<ICustodian> custodianRegistry,
@ -47,7 +48,7 @@ namespace BTCPayServer.Controllers
LinkGenerator linkGenerator
)
{
_currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_displayFormatter = displayFormatter;
_custodianAccountRepository = custodianAccountRepository;
_custodianRegistry = custodianRegistry;
_btcPayServerClient = btcPayServerClient;
@ -144,7 +145,7 @@ namespace BTCPayServer.Controllers
if (asset.Equals(defaultCurrency))
{
assetBalance.FormattedFiatValue =
_currencyNameTable.DisplayFormatCurrency(pair.Value.Qty, defaultCurrency);
_displayFormatter.Currency(pair.Value.Qty, defaultCurrency);
assetBalance.FiatValue = pair.Value.Qty;
}
else
@ -156,11 +157,11 @@ namespace BTCPayServer.Controllers
assetBalance.Bid = quote.Bid;
assetBalance.Ask = quote.Ask;
assetBalance.FormattedBid =
_currencyNameTable.DisplayFormatCurrency(quote.Bid, quote.FromAsset);
_displayFormatter.Currency(quote.Bid, quote.FromAsset);
assetBalance.FormattedAsk =
_currencyNameTable.DisplayFormatCurrency(quote.Ask, quote.FromAsset);
_displayFormatter.Currency(quote.Ask, quote.FromAsset);
assetBalance.FormattedFiatValue =
_currencyNameTable.DisplayFormatCurrency(pair.Value.Qty * quote.Bid,
_displayFormatter.Currency(pair.Value.Qty * quote.Bid,
defaultCurrency);
assetBalance.FiatValue = pair.Value.Qty * quote.Bid;
}

View file

@ -20,6 +20,7 @@ using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
@ -137,10 +138,10 @@ namespace BTCPayServer.Controllers
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
MonitoringDate = invoice.MonitoringExpiration,
Fiat = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
Fiat = _displayFormatter.Currency(invoice.Price, invoice.Currency),
TaxIncluded = invoice.Metadata.TaxIncluded is null
? null
: _CurrencyNameTable.DisplayFormatCurrency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency),
: _displayFormatter.Currency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency),
NotificationUrl = invoice.NotificationURL?.AbsoluteUri,
RedirectUrl = invoice.RedirectURL?.AbsoluteUri,
TypedMetadata = invoice.Metadata,
@ -229,8 +230,8 @@ namespace BTCPayServer.Controllers
Amount = amount,
Paid = paid,
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
PaidFormatted = _CurrencyNameTable.FormatCurrency(paid, i.Currency),
RateFormatted = _CurrencyNameTable.FormatCurrency(rate, i.Currency),
PaidFormatted = _displayFormatter.Currency(paid, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
RateFormatted = _displayFormatter.Currency(rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
PaymentMethod = paymentMethodId.ToPrettyString(),
Link = link,
Id = txId,
@ -354,8 +355,7 @@ namespace BTCPayServer.Controllers
var cryptoPaid = paymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
model.RateThenText =
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateResult = await _RateProvider.FetchRate(
new CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency), rules,
@ -369,13 +369,12 @@ namespace BTCPayServer.Controllers
}
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
model.CurrentRateText =
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
model.CurrentRateText = _displayFormatter.Currency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
model.FiatAmount = paidCurrency;
}
model.CustomAmount = model.FiatAmount;
model.CustomCurrency = invoice.Currency;
model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.Currency);
model.FiatText = _displayFormatter.Currency(model.FiatAmount, invoice.Currency);
return View("_RefundModal", model);
case RefundSteps.SelectRate:
@ -477,7 +476,6 @@ namespace BTCPayServer.Controllers
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
{
var overpaid = false;
var model = new InvoiceDetailsModel
{
@ -500,15 +498,11 @@ namespace BTCPayServer.Controllers
{
PaymentMethodId = paymentMethodId,
PaymentMethod = paymentMethodId.ToPrettyString(),
Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC),
paymentMethodId.CryptoCode),
Paid = _CurrencyNameTable.DisplayFormatCurrency(
accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
paymentMethodId.CryptoCode),
Overpaid = _CurrencyNameTable.DisplayFormatCurrency(
overpaidAmount, paymentMethodId.CryptoCode),
Due = _displayFormatter.Currency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
Paid = _displayFormatter.Currency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode),
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
Rate = ExchangeRate(data),
Rate = ExchangeRate(data.GetId().CryptoCode, data),
PaymentMethodRaw = data
};
}).ToList()
@ -794,10 +788,10 @@ namespace BTCPayServer.Controllers
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
BtcDue = accounting.Due.ShowMoney(divisibility),
BtcPaid = accounting.Paid.ShowMoney(divisibility),
InvoiceCurrency = invoice.Currency,
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
IsUnsetTopUp = invoice.IsUnsetTopUp(),
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice),
CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
@ -805,7 +799,7 @@ namespace BTCPayServer.Controllers
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.Metadata.ItemDesc,
Rate = ExchangeRate(paymentMethod),
Rate = ExchangeRate(network.CryptoCode, paymentMethod, DisplayFormatter.CurrencyFormat.Symbol),
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? receiptUrl ?? "/",
ReceiptLink = receiptUrl,
RedirectAutomatically = invoice.RedirectAutomatically,
@ -818,7 +812,6 @@ namespace BTCPayServer.Controllers
NetworkFeeMode.Never => 0,
_ => throw new NotImplementedException()
},
BtcPaid = accounting.Paid.ShowMoney(divisibility),
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete
@ -865,23 +858,33 @@ namespace BTCPayServer.Controllers
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
model.PaymentMethodId = paymentMethodId.ToString();
model.PaymentType = paymentMethodId.PaymentType.ToString();
model.OrderAmountFiat = OrderAmountFromInvoice(model.CryptoCode, invoice, DisplayFormatter.CurrencyFormat.Symbol);
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = expiration.PrettyPrint();
return model;
}
private string? OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity)
private string? OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity, DisplayFormatter.CurrencyFormat format = DisplayFormatter.CurrencyFormat.Code)
{
var currency = invoiceEntity.Currency;
var crypto = cryptoCode.ToUpperInvariant(); // uppercase to make comparison easier, might be "sats"
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
if (cryptoCode == invoiceEntity.Currency)
if (crypto == currency || (crypto == "SATS" && currency == "BTC") || (crypto == "BTC" && currency == "SATS"))
return null;
return _CurrencyNameTable.DisplayFormatCurrency(invoiceEntity.Price, invoiceEntity.Currency);
return _displayFormatter.Currency(invoiceEntity.Price, currency, format);
}
private string ExchangeRate(PaymentMethod paymentMethod)
private string? ExchangeRate(string cryptoCode, PaymentMethod paymentMethod, DisplayFormatter.CurrencyFormat format = DisplayFormatter.CurrencyFormat.Code)
{
string currency = paymentMethod.ParentEntity.Currency;
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
var currency = paymentMethod.ParentEntity.Currency;
var crypto = cryptoCode.ToUpperInvariant(); // uppercase to make comparison easier, might be "sats"
if (crypto == currency || (crypto == "SATS" && currency == "BTC") || (crypto == "BTC" && currency == "SATS"))
return null;
return _displayFormatter.Currency(paymentMethod.Rate, currency, format);
}
[HttpGet("i/{invoiceId}/status")]
@ -1004,7 +1007,8 @@ namespace BTCPayServer.Controllers
InvoiceId = invoice.Id,
OrderId = invoice.Metadata.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL?.AbsoluteUri ?? string.Empty,
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
Amount = invoice.Price,
Currency = invoice.Currency,
CanMarkInvalid = state.CanMarkInvalid(),
CanMarkSettled = state.CanMarkComplete(),
Details = InvoicePopulatePayments(invoice),

View file

@ -45,6 +45,7 @@ namespace BTCPayServer.Controllers
readonly StoreRepository _StoreRepository;
readonly UserManager<ApplicationUser> _UserManager;
private readonly CurrencyNameTable _CurrencyNameTable;
private readonly DisplayFormatter _displayFormatter;
readonly EventAggregator _EventAggregator;
readonly BTCPayNetworkProvider _NetworkProvider;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
@ -61,6 +62,7 @@ namespace BTCPayServer.Controllers
public UIInvoiceController(
InvoiceRepository invoiceRepository,
WalletRepository walletRepository,
DisplayFormatter displayFormatter,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
RateFetcher rateProvider,
@ -78,6 +80,7 @@ namespace BTCPayServer.Controllers
InvoiceActivator invoiceActivator,
LinkGenerator linkGenerator)
{
_displayFormatter = displayFormatter;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));

View file

@ -14,6 +14,7 @@ using BTCPayServer.Forms.Models;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
@ -37,6 +38,7 @@ namespace BTCPayServer.Controllers
private readonly PaymentRequestService _PaymentRequestService;
private readonly EventAggregator _EventAggregator;
private readonly CurrencyNameTable _Currencies;
private readonly DisplayFormatter _displayFormatter;
private readonly InvoiceRepository _InvoiceRepository;
private readonly StoreRepository _storeRepository;
@ -50,6 +52,7 @@ namespace BTCPayServer.Controllers
PaymentRequestService paymentRequestService,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
DisplayFormatter displayFormatter,
StoreRepository storeRepository,
InvoiceRepository invoiceRepository,
FormComponentProviders formProviders,
@ -61,6 +64,7 @@ namespace BTCPayServer.Controllers
_PaymentRequestService = paymentRequestService;
_EventAggregator = eventAggregator;
_Currencies = currencies;
_displayFormatter = displayFormatter;
_storeRepository = storeRepository;
_InvoiceRepository = invoiceRepository;
FormProviders = formProviders;
@ -89,7 +93,7 @@ namespace BTCPayServer.Controllers
var blob = data.GetBlob();
return new ViewPaymentRequestViewModel(data)
{
AmountFormatted = _Currencies.DisplayFormatCurrency(blob.Amount, blob.Currency)
AmountFormatted = _displayFormatter.Currency(blob.Amount, blob.Currency)
};
}).ToList();

View file

@ -27,6 +27,7 @@ namespace BTCPayServer.Controllers
{
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
@ -34,6 +35,7 @@ namespace BTCPayServer.Controllers
public UIPullPaymentController(ApplicationDbContextFactory dbContextFactory,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings serializerSettings,
IEnumerable<IPayoutHandler> payoutHandlers,
@ -41,6 +43,7 @@ namespace BTCPayServer.Controllers
{
_dbContextFactory = dbContextFactory;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
_pullPaymentHostedService = pullPaymentHostedService;
_serializerSettings = serializerSettings;
_payoutHandlers = payoutHandlers;
@ -79,12 +82,9 @@ namespace BTCPayServer.Controllers
{
BrandColor = storeBlob.BrandColor,
CssFileId = storeBlob.CssFileId,
AmountFormatted = _currencyNameTable.FormatCurrency(blob.Limit, blob.Currency),
AmountCollected = totalPaid,
AmountCollectedFormatted = _currencyNameTable.FormatCurrency(totalPaid, blob.Currency),
AmountDue = amountDue,
ClaimedAmount = amountDue,
AmountDueFormatted = _currencyNameTable.FormatCurrency(amountDue, blob.Currency),
CurrencyData = cd,
StartDate = pp.StartDate,
LastRefreshed = DateTime.UtcNow,
@ -93,7 +93,6 @@ namespace BTCPayServer.Controllers
{
Id = entity.Entity.Id,
Amount = entity.Blob.Amount,
AmountFormatted = _currencyNameTable.FormatCurrency(entity.Blob.Amount, blob.Currency),
Currency = blob.Currency,
Status = entity.Entity.State,
Destination = entity.Blob.Destination,
@ -200,8 +199,8 @@ namespace BTCPayServer.Controllers
var amount = ppBlob.Currency == "SATS" ? new Money(vm.ClaimedAmount, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) : vm.ClaimedAmount;
if (destination.destination.Amount != null && amount != destination.destination.Amount)
{
var implied = _currencyNameTable.DisplayFormatCurrency(destination.destination.Amount.Value, paymentMethodId.CryptoCode);
var provided = _currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency);
var implied = _displayFormatter.Currency(destination.destination.Amount.Value, paymentMethodId.CryptoCode, DisplayFormatter.CurrencyFormat.Symbol);
var provided = _displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol);
ModelState.AddModelError(nameof(vm.ClaimedAmount),
$"Amount implied in destination ({implied}) does not match the payout amount provided ({provided}).");
}
@ -235,7 +234,7 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = $"Your claim request of {_currencyNameTable.DisplayFormatCurrency(vm.ClaimedAmount, ppBlob.Currency)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.",
Message = $"Your claim request of {_displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? "approval" : "payment")}.",
Severity = StatusMessageModel.StatusSeverity.Success
});

View file

@ -34,6 +34,7 @@ namespace BTCPayServer.Controllers
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly PullPaymentHostedService _pullPaymentService;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
@ -49,6 +50,7 @@ namespace BTCPayServer.Controllers
public UIStorePullPaymentsController(BTCPayNetworkProvider btcPayNetworkProvider,
IEnumerable<IPayoutHandler> payoutHandlers,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
PullPaymentHostedService pullPaymentHostedService,
ApplicationDbContextFactory dbContextFactory,
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings)
@ -56,6 +58,7 @@ namespace BTCPayServer.Controllers
_btcPayNetworkProvider = btcPayNetworkProvider;
_payoutHandlers = payoutHandlers;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
_pullPaymentService = pullPaymentHostedService;
_dbContextFactory = dbContextFactory;
_jsonSerializerSettings = jsonSerializerSettings;
@ -532,7 +535,7 @@ namespace BTCPayServer.Controllers
PullPaymentName = ppBlob?.Name ?? item.PullPayment?.Id,
Date = item.Payout.Date,
PayoutId = item.Payout.Id,
Amount = _currencyNameTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode),
Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode),
Destination = payoutBlob.Destination
};
var handler = _payoutHandlers

View file

@ -251,6 +251,7 @@ namespace BTCPayServer.HostedServices
IEnumerable<IPayoutHandler> payoutHandlers,
ILogger<PullPaymentHostedService> logger,
Logs logs,
DisplayFormatter displayFormatter,
CurrencyNameTable currencyNameTable) : base(logs)
{
_dbContextFactory = dbContextFactory;
@ -262,6 +263,7 @@ namespace BTCPayServer.HostedServices
_payoutHandlers = payoutHandlers;
_logger = logger;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
}
Channel<object> _Channel;
@ -274,6 +276,7 @@ namespace BTCPayServer.HostedServices
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly ILogger<PullPaymentHostedService> _logger;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly CompositeDisposable _subscriptions = new CompositeDisposable();
internal override Task[] InitializeTasks()
@ -754,7 +757,7 @@ namespace BTCPayServer.HostedServices
Completed = totalCompleted,
CompletedFormatted = totalCompleted.ToString("C", nfi),
Limit = ppBlob.Limit.RoundToSignificant(currencyData.Divisibility),
LimitFormatted = _currencyNameTable.DisplayFormatCurrency(ppBlob.Limit, ppBlob.Currency),
LimitFormatted = _displayFormatter.Currency(ppBlob.Limit, ppBlob.Currency),
ResetIn = period?.End is { } nr ? ZeroIfNegative(nr - now).TimeString() : null,
EndIn = pp.EndDate is { } end ? ZeroIfNegative(end - now).TimeString() : null,
};

View file

@ -280,6 +280,7 @@ namespace BTCPayServer.Hosting
services.AddTransient<PluginService>();
services.AddSingleton<IPluginHookService, PluginHookService>();
services.TryAddTransient<Safe>();
services.TryAddTransient<DisplayFormatter>();
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
{

View file

@ -28,7 +28,8 @@ namespace BTCPayServer.Models.InvoicingModels
public bool CanMarkStatus => CanMarkSettled || CanMarkInvalid;
public bool ShowCheckout { get; set; }
public string ExceptionStatus { get; set; }
public string AmountCurrency { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; }
public InvoiceDetailsModel Details { get; set; }
public bool HasRefund { get; set; }

View file

@ -82,7 +82,6 @@ namespace BTCPayServer.Models
public decimal ClaimedAmount { get; set; }
public decimal MinimumClaim { get; set; }
public string Destination { get; set; }
public string AmountDueFormatted { get; set; }
public decimal Amount { get; set; }
public string Id { get; set; }
public string Currency { get; set; }
@ -97,8 +96,6 @@ namespace BTCPayServer.Models
public DateTimeOffset StartDate { get; set; }
public DateTime LastRefreshed { get; set; }
public CurrencyData CurrencyData { get; set; }
public string AmountCollectedFormatted { get; set; }
public string AmountFormatted { get; set; }
public bool Archived { get; set; }
public bool AutoApprove { get; set; }

View file

@ -5,6 +5,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
@ -20,17 +21,20 @@ namespace BTCPayServer.PaymentRequest
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
private readonly AppService _AppService;
private readonly CurrencyNameTable _currencies;
private readonly DisplayFormatter _displayFormatter;
public PaymentRequestService(
PaymentRequestRepository paymentRequestRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
AppService appService,
DisplayFormatter displayFormatter,
CurrencyNameTable currencies)
{
_PaymentRequestRepository = paymentRequestRepository;
_BtcPayNetworkProvider = btcPayNetworkProvider;
_AppService = appService;
_currencies = currencies;
_displayFormatter = displayFormatter;
}
public async Task UpdatePaymentRequestStateIfNeeded(string id)
@ -90,11 +94,11 @@ namespace BTCPayServer.PaymentRequest
return new ViewPaymentRequestViewModel(pr)
{
Archived = pr.Archived,
AmountFormatted = _currencies.FormatCurrency(blob.Amount, blob.Currency),
AmountFormatted = _displayFormatter.Currency(blob.Amount, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
AmountCollected = paymentStats.TotalCurrency,
AmountCollectedFormatted = _currencies.FormatCurrency(paymentStats.TotalCurrency, blob.Currency),
AmountCollectedFormatted = _displayFormatter.Currency(paymentStats.TotalCurrency, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
AmountDue = amountDue,
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
AmountDueFormatted = _displayFormatter.Currency(amountDue, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
LastUpdated = DateTime.UtcNow,
FormId = blob.FormId,
@ -128,8 +132,8 @@ namespace BTCPayServer.PaymentRequest
Amount = amount,
Paid = paid,
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
PaidFormatted = _currencies.FormatCurrency(paid, blob.Currency),
RateFormatted = _currencies.FormatCurrency(rate, blob.Currency),
PaidFormatted = _displayFormatter.Currency(paid, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
RateFormatted = _displayFormatter.Currency(rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
PaymentMethod = paymentMethodId.ToPrettyString(),
Link = link,
Id = txId,
@ -147,7 +151,7 @@ namespace BTCPayServer.PaymentRequest
{
Id = entity.Id,
Amount = entity.Price,
AmountFormatted = _currencies.FormatCurrency(entity.Price, blob.Currency),
AmountFormatted = _displayFormatter.Currency(entity.Price, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
Currency = entity.Currency,
ExpiryDate = entity.ExpirationTime.DateTime,
State = state,

View file

@ -10,7 +10,6 @@ using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.Models;
@ -24,14 +23,14 @@ namespace BTCPayServer.Payments.Bitcoin
private readonly BTCPayNetworkProvider _networkProvider;
private readonly IFeeProviderFactory _FeeRateProviderFactory;
private readonly NBXplorerDashboard _dashboard;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly Services.Wallets.BTCPayWalletProvider _WalletProvider;
private readonly Dictionary<string, string> _bech32Prefix;
public BitcoinLikePaymentHandler(ExplorerClientProvider provider,
BTCPayNetworkProvider networkProvider,
IFeeProviderFactory feeRateProviderFactory,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
NBXplorerDashboard dashboard,
Services.Wallets.BTCPayWalletProvider walletProvider)
{
@ -40,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin
_FeeRateProviderFactory = feeRateProviderFactory;
_dashboard = dashboard;
_WalletProvider = walletProvider;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
_bech32Prefix = networkProvider.GetAll().OfType<BTCPayNetwork>()
.Where(network => network.NBitcoinNetwork?.Consensus?.SupportSegwit is true).ToDictionary(network => network.CryptoCode,
@ -144,7 +143,7 @@ namespace BTCPayServer.Payments.Bitcoin
if (model.Activated && amountInSats)
{
base.PreparePaymentModelForAmountInSats(model, paymentMethod, _currencyNameTable);
base.PreparePaymentModelForAmountInSats(model, paymentMethod, _displayFormatter);
}
}

View file

@ -6,6 +6,7 @@ using BTCPayServer.Data;
using BTCPayServer.Logging;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
@ -100,7 +101,7 @@ namespace BTCPayServer.Payments
return null;
}
public virtual void PreparePaymentModelForAmountInSats(PaymentModel model, IPaymentMethod paymentMethod, CurrencyNameTable currencyNameTable)
public virtual void PreparePaymentModelForAmountInSats(PaymentModel model, IPaymentMethod paymentMethod, DisplayFormatter displayFormatter)
{
var satoshiCulture = new CultureInfo(CultureInfo.InvariantCulture.Name)
{
@ -111,7 +112,9 @@ namespace BTCPayServer.Payments
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi);
model.Rate = currencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, model.InvoiceCurrency);
model.Rate = model.InvoiceCurrency is "BTC" or "SATS"
? null
: displayFormatter.Currency(paymentMethod.Rate / 100_000_000, model.InvoiceCurrency, DisplayFormatter.CurrencyFormat.Symbol);
}
public Task<IPaymentMethodDetails> CreatePaymentMethodDetails(InvoiceLogs logs,

View file

@ -9,8 +9,8 @@ using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Payments.Lightning
@ -18,17 +18,17 @@ namespace BTCPayServer.Payments.Lightning
public class LNURLPayPaymentHandler : PaymentMethodHandlerBase<LNURLPaySupportedPaymentMethod, BTCPayNetwork>
{
private readonly BTCPayNetworkProvider _networkProvider;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler;
public LNURLPayPaymentHandler(
BTCPayNetworkProvider networkProvider,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
IOptions<LightningNetworkOptions> options,
LightningLikePaymentHandler lightningLikePaymentHandler)
{
_networkProvider = networkProvider;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
_lightningLikePaymentHandler = lightningLikePaymentHandler;
Options = options;
}
@ -115,7 +115,7 @@ namespace BTCPayServer.Payments.Lightning
if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC")
{
base.PreparePaymentModelForAmountInSats(model, paymentMethod, _currencyNameTable);
base.PreparePaymentModelForAmountInSats(model, paymentMethod, _displayFormatter);
}
}

View file

@ -14,7 +14,6 @@ using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Options;
using NBitcoin;
@ -27,21 +26,21 @@ namespace BTCPayServer.Payments.Lightning
private readonly LightningClientFactoryService _lightningClientFactory;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly SocketFactory _socketFactory;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
public LightningLikePaymentHandler(
NBXplorerDashboard dashboard,
LightningClientFactoryService lightningClientFactory,
BTCPayNetworkProvider networkProvider,
SocketFactory socketFactory,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
IOptions<LightningNetworkOptions> options)
{
_Dashboard = dashboard;
_lightningClientFactory = lightningClientFactory;
_networkProvider = networkProvider;
_socketFactory = socketFactory;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
Options = options;
}
@ -221,7 +220,7 @@ namespace BTCPayServer.Payments.Lightning
if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC")
{
base.PreparePaymentModelForAmountInSats(model, paymentMethod, _currencyNameTable);
base.PreparePaymentModelForAmountInSats(model, paymentMethod, _displayFormatter);
}
}
public override string GetCryptoImage(PaymentMethodId paymentMethodId)

View file

@ -35,12 +35,14 @@ namespace BTCPayServer.Services.Apps
readonly ApplicationDbContextFactory _ContextFactory;
private readonly InvoiceRepository _InvoiceRepository;
readonly CurrencyNameTable _Currencies;
private readonly DisplayFormatter _displayFormatter;
private readonly StoreRepository _storeRepository;
private readonly HtmlSanitizer _HtmlSanitizer;
public CurrencyNameTable Currencies => _Currencies;
public AppService(ApplicationDbContextFactory contextFactory,
InvoiceRepository invoiceRepository,
CurrencyNameTable currencies,
DisplayFormatter displayFormatter,
StoreRepository storeRepository,
HtmlSanitizer htmlSanitizer)
{
@ -49,6 +51,7 @@ namespace BTCPayServer.Services.Apps
_Currencies = currencies;
_storeRepository = storeRepository;
_HtmlSanitizer = htmlSanitizer;
_displayFormatter = displayFormatter;
}
public async Task<object> GetAppInfo(string appId)
@ -599,7 +602,7 @@ namespace BTCPayServer.Services.Apps
if (pValue != null)
{
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
price.Formatted = Currencies.FormatCurrency(pValue.Value.Value, currency);
price.Formatted = _displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
}
break;
case "fixed":
@ -607,7 +610,7 @@ namespace BTCPayServer.Services.Apps
case null:
price.Type = ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed;
price.Value = decimal.Parse(pValue.Value.Value, CultureInfo.InvariantCulture);
price.Formatted = Currencies.FormatCurrency(pValue.Value.Value, currency);
price.Formatted = _displayFormatter.Currency(price.Value.Value, currency, DisplayFormatter.CurrencyFormat.Symbol);
break;
}

View file

@ -0,0 +1,57 @@
using System;
using System.Globalization;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Services;
public class DisplayFormatter
{
private readonly CurrencyNameTable _currencyNameTable;
public DisplayFormatter(CurrencyNameTable currencyNameTable)
{
_currencyNameTable = currencyNameTable;
}
public enum CurrencyFormat
{
Code,
Symbol,
CodeAndSymbol
}
/// <summary>
/// Format a currency, rounded to significant divisibility
/// </summary>
/// <param name="value">The value</param>
/// <param name="currency">Currency code</param>
/// <param name="format">The format, defaults to amount + code, e.g. 1.234,56 USD</param>
/// <returns>Formatted amount and currency string</returns>
public string Currency(decimal value, string currency, CurrencyFormat format = CurrencyFormat.Code)
{
var provider = _currencyNameTable.GetNumberFormatInfo(currency, true);
var currencyData = _currencyNameTable.GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
value = value.RoundToSignificant(ref divisibility);
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
var formatted = value.ToString("C", provider);
return format switch
{
CurrencyFormat.Code => $"{formatted.Replace(provider.CurrencySymbol, "").Trim()} {currency}",
CurrencyFormat.Symbol => formatted,
CurrencyFormat.CodeAndSymbol => $"{formatted} ({currency})",
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null)
};
}
public string Currency(string value, string currency, CurrencyFormat format = CurrencyFormat.Code)
{
return Currency(decimal.Parse(value, CultureInfo.InvariantCulture), currency, format);
}
}

View file

@ -1,6 +1,8 @@
@using System.Globalization
@using BTCPayServer.Payments
@using BTCPayServer.Payments.Bitcoin
@using BTCPayServer.Services
@inject DisplayFormatter DisplayFormatter
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
@{
@ -78,7 +80,7 @@
<td>@(payment.CryptoPaymentData.KeyPath?.ToString()?? "Unknown")</td>
<td style="max-width:300px;" data-bs-toggle="tooltip" class="text-truncate" title="@payment.DepositAddress">@payment.DepositAddress</td>
<td class="payment-value">
@payment.CryptoPaymentData.GetValue()
@DisplayFormatter.Currency(payment.CryptoPaymentData.GetValue(), payment.Crypto)
@if (!string.IsNullOrEmpty(payment.AdditionalInformation))
{
<div>(@payment.AdditionalInformation)</div>

View file

@ -183,37 +183,37 @@
</noscript>
<script type="text/x-template" id="payment-details">
<dl>
<div v-if="orderAmount > 0">
<div v-if="orderAmount > 0" id="PaymentDetails-TotalPrice">
<dt v-t="'total_price'"></dt>
<dd :data-clipboard="srvModel.orderAmount" :data-clipboard-confirm="$t('copy_confirm')">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</dd>
</div>
<div v-if="orderAmount > 0 && srvModel.orderAmountFiat">
<div v-if="orderAmount > 0 && srvModel.orderAmountFiat" id="PaymentDetails-TotalFiat">
<dt v-t="'total_fiat'"></dt>
<dd :data-clipboard="srvModel.orderAmountFiat" :data-clipboard-confirm="$t('copy_confirm')">{{srvModel.orderAmountFiat}}</dd>
</div>
<div v-if="srvModel.rate && srvModel.cryptoCode">
<div v-if="srvModel.rate && srvModel.cryptoCode" id="PaymentDetails-ExchangeRate">
<dt v-t="'exchange_rate'"></dt>
<dd :data-clipboard="srvModel.rate" :data-clipboard-confirm="$t('copy_confirm')">
<template v-if="srvModel.cryptoCode === 'sats'">1 sat = {{ srvModel.rate }}</template>
<template v-else>1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}</template>
</dd>
</div>
<div v-if="srvModel.networkFee">
<div v-if="srvModel.networkFee" id="PaymentDetails-NetworkCost">
<dt v-t="'network_cost'"></dt>
<dd :data-clipboard="srvModel.networkFee" :data-clipboard-confirm="$t('copy_confirm')">
<div v-if="srvModel.txCountForFee > 0" v-t="{ path: 'tx_count', args: { count: srvModel.txCount } }"></div>
<div v-text="`${srvModel.networkFee} ${srvModel.cryptoCode}`"></div>
</dd>
</div>
<div v-if="btcPaid > 0">
<div v-if="btcPaid > 0" id="PaymentDetails-AmountPaid">
<dt v-t="'amount_paid'"></dt>
<dd :data-clipboard="srvModel.btcPaid" :data-clipboard-confirm="$t('copy_confirm')" v-text="`${srvModel.btcPaid} ${srvModel.cryptoCode}`"></dd>
</div>
<div v-if="btcDue > 0">
<div v-if="btcDue > 0" id="PaymentDetails-AmountDue">
<dt v-t="'amount_due'"></dt>
<dd :data-clipboard="srvModel.btcDue" :data-clipboard-confirm="$t('copy_confirm')" v-text="`${srvModel.btcDue} ${srvModel.cryptoCode}`"></dd>
</div>
<div v-if="showRecommendedFee">
<div v-if="showRecommendedFee" id="PaymentDetails-RecommendedFee">
<dt v-t="'recommended_fee'"></dt>
<dd :data-clipboard="srvModel.feeRate" :data-clipboard-confirm="$t('copy_confirm')" v-t="{ path: 'fee_rate', args: { feeRate: srvModel.feeRate } }"></dd>
</div>

View file

@ -1,10 +1,12 @@
@model BTCPayServer.Models.InvoicingModels.InvoiceReceiptViewModel
@using BTCPayServer.Client
@using BTCPayServer.Client.Models
@using BTCPayServer.Services.Rates
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
@inject BTCPayServer.Services.ThemeSettings Theme
@inject CurrencyNameTable CurrencyNameTable
@using BTCPayServer.Components.QRCode
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.TagHelpers
@inject BTCPayServerEnvironment Env
@inject DisplayFormatter DisplayFormatter
@{
Layout = null;
ViewData["Title"] = $"Receipt from {Model.StoreName}";
@ -66,7 +68,7 @@
</button>
<dd class="text-muted mb-0 fw-semibold">Amount Paid</dd>
</div>
<dt class="fs-2 mb-0 text-nowrap fw-semibold">@CurrencyNameTable.DisplayFormatCurrency(Model.Amount, Model.Currency)</dt>
<dt class="fs-2 mb-0 text-nowrap fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</dt>
</div>
<div class="d-flex flex-column">
<dd class="text-muted mb-0 fw-semibold">Date</dd>

View file

@ -1,5 +1,7 @@
@using BTCPayServer.Client
@using BTCPayServer.Client.Models
@using BTCPayServer.Services
@inject DisplayFormatter DisplayFormatter
@model InvoicesModel
@{
ViewData.SetActivePage(InvoiceNavPages.Index, "Invoices");
@ -371,7 +373,7 @@
<span class="badge bg-warning">Refund</span>
}
</td>
<td class="text-end text-nowrap">@invoice.AmountCurrency</td>
<td class="text-end text-nowrap">@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</td>
<td class="text-end text-nowrap">
@if (invoice.ShowCheckout)
{

View file

@ -1,11 +1,13 @@
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
@inject BTCPayNetworkProvider BtcPayNetworkProvider
@using BTCPayServer.Client
@using BTCPayServer.Components.ThemeSwitch
@using BTCPayServer.Payments
@model BTCPayServer.Models.ViewPullPaymentModel
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.TagHelpers
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
@inject BTCPayServerEnvironment Env
@inject BTCPayNetworkProvider BtcPayNetworkProvider
@inject DisplayFormatter DisplayFormatter
@model BTCPayServer.Models.ViewPullPaymentModel
@{
ViewData["Title"] = Model.Title;
Csp.UnsafeEval();
@ -141,18 +143,18 @@
<h2 class="h4 mb-3">Payment Details</h2>
<dl class="mb-0 mt-md-4">
<div class="d-flex d-print-inline-block flex-column mb-4 me-5">
<dt class="h4 fw-semibold text-nowrap text-primary text-print-default order-2 order-sm-1 mb-1">@Model.AmountDueFormatted</dt>
<dt class="h4 fw-semibold text-nowrap text-primary text-print-default order-2 order-sm-1 mb-1">@DisplayFormatter.Currency(Model.AmountDue, Model.Currency)</dt>
<dd class="order-1 order-sm-2 mb-1">Available claim</dd>
</div>
<div class="progress bg-light d-none d-sm-flex mb-sm-4 d-print-none" style="height:5px">
<div class="progress-bar bg-primary" role="progressbar" style="width:@((Model.AmountCollected / Model.Amount) * 100)%"></div>
</div>
<div class="d-flex d-print-inline-block flex-column mb-4 me-5 d-sm-inline-flex mb-sm-0">
<dt class="h4 fw-semibold text-nowrap order-2 order-sm-1 mb-1">@Model.AmountCollectedFormatted</dt>
<dt class="h4 fw-semibold text-nowrap order-2 order-sm-1 mb-1">@DisplayFormatter.Currency(Model.AmountCollected, Model.Currency)</dt>
<dd class="order-1 order-sm-2 mb-1">Already claimed</dd>
</div>
<div class="d-flex d-print-inline-block flex-column mb-0 d-sm-inline-flex float-sm-end">
<dt class="h4 text-sm-end fw-semibold text-nowrap order-2 order-sm-1 mb-1">@Model.AmountFormatted</dt>
<dt class="h4 text-sm-end fw-semibold text-nowrap order-2 order-sm-1 mb-1">@DisplayFormatter.Currency(Model.Amount, Model.Currency)</dt>
<dd class="text-sm-end order-1 order-sm-2 mb-1">Claim limit</dd>
</div>
</dl>