Added toggle button to switch to store default currency (#3752)

* Added toggle button to switch to store default currency

* Replaced ["usd"] with the currency variable

* Fix indentation and improve JS part

* Update script and display

* Improve chart display

* Improve rate display

* Address code review comments

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Saksham Solanki
2022-06-15 13:03:24 +05:30
committed by GitHub
parent 0da97c5da3
commit 346821f0d6
3 changed files with 128 additions and 28 deletions

View File

@@ -1,17 +1,42 @@
@using BTCPayServer.Services.Wallets @using BTCPayServer.Services.Wallets
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel @model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
<style>
#DefaultCurrencyToggle .btn {
background-color: var(--btcpay-bg-tile);
border-color: var(--btcpay-body-border-light);
}
#DefaultCurrencyToggle input:not(:checked) + .btn {
color: var(--btcpay-body-text-muted);
}
</style>
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance"> <div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
<h6 class="mb-2">Wallet Balance</h6> <div class="d-flex gap-3 align-items-center justify-content-between mb-2">
<h6>Wallet Balance</h6>
@if (Model.CryptoCode != Model.DefaultCurrency)
{
<div class="btn-group btn-group-sm gap-0" role="group" id="DefaultCurrencyToggle">
<input type="radio" class="btn-check" name="currency" id="currency_@Model.CryptoCode" value="@Model.CryptoCode" autocomplete="off" checked>
<label class="btn btn-outline-secondary px-2 py-1" for="currency_@Model.CryptoCode">@Model.CryptoCode</label>
<input type="radio" class="btn-check" name="currency" id="currency_@Model.DefaultCurrency" value="@Model.DefaultCurrency" autocomplete="off">
<label class="btn btn-outline-secondary px-2 py-1" for="currency_@Model.DefaultCurrency">@Model.DefaultCurrency</label>
</div>
}
</div>
<header class="mb-3"> <header class="mb-3">
@if (Model.Balance is not null) @if (Model.Balance is not null)
{ {
<div class="balance"> <div class="balance" id="Balance-@Model.CryptoCode">
<h3 class="d-inline-block me-1">@Model.Balance</h3> <h3 class="d-inline-block me-1">@Model.Balance</h3>
<span class="text-secondary fw-semibold">@Model.CryptoCode</span> <span class="text-secondary fw-semibold">@Model.CryptoCode</span>
</div> </div>
} <div class="balance" id="Balance-@Model.DefaultCurrency">
<div class="btn-group mt-1" role="group" aria-label="Filter"> <h3 class="d-inline-block" id="DefaultCurrencyBalance"></h3>
<span class="text-secondary fw-semibold">@Model.DefaultCurrency</span>
</div>
}
<div class="btn-group mt-1" role="group" aria-label="Filter">
<input type="radio" class="btn-check" name="filter" id="filter-week" value="week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")> <input type="radio" class="btn-check" name="filter" id="filter-week" value="week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
<label class="btn btn-link" for="filter-week">1W</label> <label class="btn btn-link" for="filter-week">1W</label>
<input type="radio" class="btn-check" name="filter" id="filter-month" value="month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")> <input type="radio" class="btn-check" name="filter" id="filter-month" value="month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
@@ -20,50 +45,112 @@
<label class="btn btn-link" for="filter-year">1Y</label> <label class="btn btn-link" for="filter-year">1Y</label>
</div> </div>
</header> </header>
<div class="ct-chart ct-major-eleventh"></div> <div class="ct-chart"></div>
<script> <script>
(function () { (function () {
const id = 'StoreWalletBalance-@Model.Store.Id'; const balance = @Safe.Json(Model.Balance);
const storeId = @Safe.Json(Model.Store.Id);
const cryptoCode = @Safe.Json(Model.CryptoCode);
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
const pathBase = @Safe.Json(Context.Request.PathBase);
let data = { series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) };
let rate = null;
const $cryptoBalance = document.getElementById(`Balance-${cryptoCode}`);
const $defaultBalance = document.getElementById(`Balance-${defaultCurrency}`);
const $defaultCurrencyBalance = document.getElementById('DefaultCurrencyBalance');
$defaultBalance.style.display = 'none';
const id = `StoreWalletBalance-${storeId}`;
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week })); const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
const chartOpts = {
fullWidth: true,
showArea: true,
axisY: {
labelInterpolationFnc: value => rate
? displayDefaultCurrency(value, defaultCurrency).toString()
: value
}
};
const render = data => { const render = data => {
const { series, labels, balance } = data; let { series, labels, balance } = data;
if (rate)
series = data.series.map(i => toDefaultCurrency(i, rate));
if (balance) if (balance)
document.querySelector(`#${id} h3`).innerText = balance; document.querySelector(`#${id} h3`).innerText = balance;
const min = Math.min(...series); const min = Math.min(...series);
const max = Math.max(...series); const max = Math.max(...series);
const low = Math.max(min - ((max - min) / 5), 0); const low = Math.max(min - ((max - min) / 5), 0);
const renderOpts = Object.assign({}, chartOpts, { low });
const chart = new Chartist.Line(`#${id} .ct-chart`, { const chart = new Chartist.Line(`#${id} .ct-chart`, {
labels, labels,
series: [series] series: [series]
}, { }, renderOpts);
low,
fullWidth: true,
showArea: true
});
// prevent y-axis labels from getting cut off // prevent y-axis labels from getting cut off
window.setTimeout(() => { window.setTimeout(() => {
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')]; const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
if (yLabels) { if (yLabels) {
const width = Math.max(...(yLabels.map(l => l.innerText.length * 8))) const factor = rate ? 6 : 8;
chart.update(null, { axisY: { offset: width } }) const width = Math.max(...(yLabels.map(l => l.innerText.length * factor)));
const opts = Object.assign({}, renderOpts, {
axisY: Object.assign({}, renderOpts.axisY, { offset: width })
});
chart.update(null, opts);
} }
}, 0) }, 0)
}; };
const update = async type => { const update = async type => {
const url = baseUrl.replace(/\/week$/gi, `/${type}`); const url = baseUrl.replace(/\/week$/gi, `/${type}`);
const response = await fetch(url); const response = await fetch(url);
if (response.ok) { if (response.ok) {
const json = await response.json(); data = await response.json();
render(json); render(data);
} }
}; };
render({ series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) });
const toDefaultCurrency = (value, rate) => {
return Math.round((value * rate) * 100) / 100;
};
const displayDefaultCurrency = (value, currency) => {
const locale = currency === "USD" ? 'en-US' : navigator.language;
const opts = { currency, style: 'decimal', minimumFractionDigits: divisibility };
return new Intl.NumberFormat(locale, opts).format(value);
};
render(data);
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
delegate('change', `#${id} [name="filter"]`, async e => { delegate('change', `#${id} [name="filter"]`, async e => {
const type = e.target.value; const type = e.target.value;
await update(type); await update(type);
}) })
}) delegate('change', '#DefaultCurrencyToggle input', async e => {
const { target } = e;
if (target.value === defaultCurrency) {
const currencyPair = `${cryptoCode}_${defaultCurrency}`;
const response = await fetch(`${pathBase}/api/rates?storeId=${storeId}&currencyPairs=${currencyPair}`);
const json = await response.json();
rate = json[0] && json[0].rate;
if (rate) {
const value = toDefaultCurrency(balance, rate);
$defaultCurrencyBalance.innerText = displayDefaultCurrency(value, defaultCurrency);
render(data);
} else {
console.warn(`Fetching rate for ${currencyPair} failed.`);
}
} else {
rate = null;
render(data);
}
$cryptoBalance.style.display = rate ? 'none' : 'block';
$defaultBalance.style.display = rate ? 'block' : 'none';
});
});
})(); })();
</script> </script>
</div> </div>

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using Dapper; using Dapper;
@@ -18,28 +19,37 @@ namespace BTCPayServer.Components.StoreWalletBalance;
public class StoreWalletBalance : ViewComponent public class StoreWalletBalance : ViewComponent
{ {
private string CryptoCode; private string _cryptoCode;
private const WalletHistogramType DefaultType = WalletHistogramType.Week; private const WalletHistogramType DefaultType = WalletHistogramType.Week;
private readonly StoreRepository _storeRepo; private readonly StoreRepository _storeRepo;
private readonly CurrencyNameTable _currencies;
private readonly WalletHistogramService _walletHistogramService; private readonly WalletHistogramService _walletHistogramService;
public StoreWalletBalance(StoreRepository storeRepo, WalletHistogramService walletHistogramService, BTCPayNetworkProvider networkProvider) public StoreWalletBalance(
StoreRepository storeRepo,
CurrencyNameTable currencies,
WalletHistogramService walletHistogramService,
BTCPayNetworkProvider networkProvider)
{ {
_storeRepo = storeRepo; _storeRepo = storeRepo;
_currencies = currencies;
_walletHistogramService = walletHistogramService; _walletHistogramService = walletHistogramService;
CryptoCode = networkProvider.DefaultNetwork.CryptoCode; _cryptoCode = networkProvider.DefaultNetwork.CryptoCode;
} }
public async Task<IViewComponentResult> InvokeAsync(StoreData store) public async Task<IViewComponentResult> InvokeAsync(StoreData store)
{ {
var walletId = new WalletId(store.Id, CryptoCode); var walletId = new WalletId(store.Id, _cryptoCode);
var data = await _walletHistogramService.GetHistogram(store, walletId, DefaultType); var data = await _walletHistogramService.GetHistogram(store, walletId, DefaultType);
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
var vm = new StoreWalletBalanceViewModel var vm = new StoreWalletBalanceViewModel
{ {
Store = store, Store = store,
CryptoCode = CryptoCode, CryptoCode = _cryptoCode,
CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
DefaultCurrency = defaultCurrency,
WalletId = walletId, WalletId = walletId,
Series = data?.Series, Series = data?.Series,
Labels = data?.Labels, Labels = data?.Labels,

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
namespace BTCPayServer.Components.StoreWalletBalance; namespace BTCPayServer.Components.StoreWalletBalance;
@@ -8,6 +9,8 @@ public class StoreWalletBalanceViewModel
{ {
public decimal? Balance { get; set; } public decimal? Balance { get; set; }
public string CryptoCode { get; set; } public string CryptoCode { get; set; }
public string DefaultCurrency { get; set; }
public CurrencyData CurrencyData { get; set; }
public StoreData Store { get; set; } public StoreData Store { get; set; }
public WalletId WalletId { get; set; } public WalletId WalletId { get; set; }
public WalletHistogramType Type { get; set; } public WalletHistogramType Type { get; set; }