From 5c8ca15ee2fe27764a0a75fac7bba50c1610d016 Mon Sep 17 00:00:00 2001 From: d11n Date: Thu, 17 Feb 2022 10:07:41 +0100 Subject: [PATCH] Redesign Wallet UI (#3441) * Update wallet navigation * Find matching text color for label bg color * Cleanup * Extract WalletNav component * Move PSBT link to Send and Rescan link to Settings * Update transactions view * Test fixes * Adapt invoices list actions * Show invoice actions only if there are any invoices * Link wallet name and balance to tranactions list * Move wallet related actions from list to settings * Fix main menu z-index Needs a value between fixed and the offcanvas backdrop, see https://getbootstrap.com/docs/5.1/layout/z-index/ * Update receive and send views --- BTCPayServer.Tests/SeleniumTester.cs | 6 +- BTCPayServer.Tests/SeleniumTests.cs | 17 +- BTCPayServer/Components/MainNav/MainNav.cs | 1 - .../Components/WalletNav/Default.cshtml | 29 ++ .../Components/WalletNav/WalletNav.cs | 56 +++ .../WalletNav/WalletNavViewModel.cs | 10 + .../Controllers/UIWalletsController.cs | 2 +- BTCPayServer/Services/Labels/ColoredLabel.cs | 1 + BTCPayServer/Services/Labels/LabelFactory.cs | 17 +- .../Views/UIInvoice/ListInvoices.cshtml | 85 ++--- .../Views/UIStores/WalletSettings.cshtml | 78 +++-- .../Views/UIWallets/WalletReceive.cshtml | 17 +- .../Views/UIWallets/WalletSend.cshtml | 8 +- .../Views/UIWallets/WalletTransactions.cshtml | 323 +++++++++--------- BTCPayServer/Views/UIWallets/_Nav.cshtml | 30 +- BTCPayServer/wwwroot/img/icon-sprite.svg | 1 + BTCPayServer/wwwroot/js/copy-to-clipboard.js | 6 +- BTCPayServer/wwwroot/main/layout.css | 2 +- BTCPayServer/wwwroot/main/qrcode.css | 2 +- BTCPayServer/wwwroot/main/site.css | 5 + 20 files changed, 400 insertions(+), 296 deletions(-) create mode 100644 BTCPayServer/Components/WalletNav/Default.cshtml create mode 100644 BTCPayServer/Components/WalletNav/WalletNav.cs create mode 100644 BTCPayServer/Components/WalletNav/WalletNavViewModel.cs diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 3cd12046a..2c4f82e50 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -403,9 +403,9 @@ namespace BTCPayServer.Tests public void GoToWalletSettings(string cryptoCode = "BTC") { Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - if (Driver.PageSource.Contains("id=\"SectionNav-Settings\"")) + if (Driver.PageSource.Contains("id=\"WalletNav-Settings\"")) { - Driver.FindElement(By.Id("SectionNav-Settings")).Click(); + Driver.FindElement(By.Id("WalletNav-Settings")).Click(); } } @@ -565,7 +565,7 @@ namespace BTCPayServer.Tests Driver.Navigate().GoToUrl(new Uri(ServerUri, $"wallets/{walletId}")); if (navPages != WalletsNavPages.Transactions) { - Driver.FindElement(By.Id($"SectionNav-{navPages}")).Click(); + Driver.FindElement(By.Id($"WalletNav-{navPages}")).Click(); } } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index f58b12e9e..9b3d5ec93 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -998,13 +998,13 @@ namespace BTCPayServer.Tests //let's test quickly the receive wallet page s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.Driver.FindElement(By.Id("SectionNav-Send")).Click(); + s.Driver.FindElement(By.Id("WalletNav-Send")).Click(); s.Driver.FindElement(By.Id("SignTransaction")).Click(); //you cannot use the Sign with NBX option without saving private keys when generating the wallet. Assert.DoesNotContain("nbx-seed", s.Driver.PageSource); - s.Driver.FindElement(By.Id("SectionNav-Receive")).Click(); + s.Driver.FindElement(By.Id("WalletNav-Receive")).Click(); //generate a receiving address s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed); @@ -1071,20 +1071,19 @@ namespace BTCPayServer.Tests Assert.Contains("m/84'/1'/0'", s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value")); - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - // Make sure we can rescan, because we are admin! - s.Driver.FindElement(By.Id("SectionNav-Rescan")).Click(); + s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click(); + s.Driver.FindElement(By.Id("Rescan")).Click(); Assert.Contains("The batch size make sure", s.Driver.PageSource); // Check the tx sent earlier arrived - s.Driver.FindElement(By.Id("SectionNav-Transactions")).Click(); + s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); var walletTransactionLink = s.Driver.Url; Assert.Contains(tx.ToString(), s.Driver.PageSource); // Send to bob - s.Driver.FindElement(By.Id("SectionNav-Send")).Click(); + s.Driver.FindElement(By.Id("WalletNav-Send")).Click(); var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); SetTransactionOutput(s, 0, bob, 1); s.Driver.FindElement(By.Id("SignTransaction")).Click(); @@ -1096,7 +1095,7 @@ namespace BTCPayServer.Tests Assert.Equal(walletTransactionLink, s.Driver.Url); s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.Driver.FindElement(By.Id("SectionNav-Send")).Click(); + s.Driver.FindElement(By.Id("WalletNav-Send")).Click(); var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); SetTransactionOutput(s, 0, jack, 0.01m); @@ -1113,7 +1112,7 @@ namespace BTCPayServer.Tests bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!"; var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest); s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.Driver.FindElement(By.Id("SectionNav-Send")).Click(); + s.Driver.FindElement(By.Id("WalletNav-Send")).Click(); s.Driver.FindElement(By.Id("bip21parse")).Click(); s.Driver.SwitchTo().Alert().SendKeys(bip21); s.Driver.SwitchTo().Alert().Accept(); diff --git a/BTCPayServer/Components/MainNav/MainNav.cs b/BTCPayServer/Components/MainNav/MainNav.cs index b0e09c120..b315dfb93 100644 --- a/BTCPayServer/Components/MainNav/MainNav.cs +++ b/BTCPayServer/Components/MainNav/MainNav.cs @@ -19,7 +19,6 @@ namespace BTCPayServer.Components.MainNav { public class MainNav : ViewComponent { - private const string RootName = "Global"; private readonly AppService _appService; private readonly StoreRepository _storeRepo; private readonly UIStoresController _storesController; diff --git a/BTCPayServer/Components/WalletNav/Default.cshtml b/BTCPayServer/Components/WalletNav/Default.cshtml new file mode 100644 index 000000000..29ebfc770 --- /dev/null +++ b/BTCPayServer/Components/WalletNav/Default.cshtml @@ -0,0 +1,29 @@ +@using BTCPayServer.Views.Stores +@using BTCPayServer.Client +@using BTCPayServer.Views.Wallets +@using BTCPayServer.Abstractions.Extensions +@inject BTCPayNetworkProvider _btcPayNetworkProvider + +@model BTCPayServer.Components.WalletNav.WalletNavViewModel +@addTagHelper *, BundlerMinifier.TagHelpers + +
+ +

@Model.Label

+
+ @Model.Balance @Model.Network.CryptoCode +
+
+
+ @if (!Model.Network.ReadonlyWallet) + { + Send + } + Receive + + + +
+
+ + diff --git a/BTCPayServer/Components/WalletNav/WalletNav.cs b/BTCPayServer/Components/WalletNav/WalletNav.cs new file mode 100644 index 000000000..d31d4332e --- /dev/null +++ b/BTCPayServer/Components/WalletNav/WalletNav.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Controllers; +using BTCPayServer.Data; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments; +using BTCPayServer.Payments.Lightning; +using BTCPayServer.Services.Apps; +using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Stores; +using BTCPayServer.Services.Wallets; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using NBitcoin; +using NBitcoin.Secp256k1; + +namespace BTCPayServer.Components.WalletNav +{ + public class WalletNav : ViewComponent + { + private readonly BTCPayWalletProvider _walletProvider; + private readonly UIWalletsController _walletsController; + private readonly BTCPayNetworkProvider _networkProvider; + + public WalletNav( + BTCPayWalletProvider walletProvider, + BTCPayNetworkProvider networkProvider, + UIWalletsController walletsController) + { + _walletProvider = walletProvider; + _networkProvider = networkProvider; + _walletsController = walletsController; + } + + public async Task InvokeAsync(WalletId walletId) + { + var store = ViewContext.HttpContext.GetStoreData(); + var network = _networkProvider.GetNetwork(walletId.CryptoCode); + var wallet = _walletProvider.GetWallet(network); + var derivation = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode); + var balance = await _walletsController.GetBalanceString(wallet, derivation.AccountDerivation); + + var vm = new WalletNavViewModel + { + WalletId = walletId, + Network = network, + Balance = balance, + Label = derivation.Label ?? $"{store.StoreName} {walletId.CryptoCode} Wallet" + }; + + return View(vm); + } + } +} diff --git a/BTCPayServer/Components/WalletNav/WalletNavViewModel.cs b/BTCPayServer/Components/WalletNav/WalletNavViewModel.cs new file mode 100644 index 000000000..0238972bb --- /dev/null +++ b/BTCPayServer/Components/WalletNav/WalletNavViewModel.cs @@ -0,0 +1,10 @@ +namespace BTCPayServer.Components.WalletNav +{ + public class WalletNavViewModel + { + public WalletId WalletId { get; set; } + public BTCPayNetwork Network { get; set; } + public string Label { get; set; } + public string Balance { get; set; } + } +} diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs index ba76449ba..3bc536d20 100644 --- a/BTCPayServer/Controllers/UIWalletsController.cs +++ b/BTCPayServer/Controllers/UIWalletsController.cs @@ -1057,7 +1057,7 @@ namespace BTCPayServer.Controllers } } - private static async Task GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy) + internal async Task GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy) { try { diff --git a/BTCPayServer/Services/Labels/ColoredLabel.cs b/BTCPayServer/Services/Labels/ColoredLabel.cs index 9ff06c498..c4936a866 100644 --- a/BTCPayServer/Services/Labels/ColoredLabel.cs +++ b/BTCPayServer/Services/Labels/ColoredLabel.cs @@ -10,6 +10,7 @@ namespace BTCPayServer.Services.Labels public string Text { get; internal set; } public string Color { get; internal set; } + public string TextColor { get; internal set; } public string Link { get; internal set; } public string Tooltip { get; internal set; } diff --git a/BTCPayServer/Services/Labels/LabelFactory.cs b/BTCPayServer/Services/Labels/LabelFactory.cs index 77f83f4ca..23665ffdc 100644 --- a/BTCPayServer/Services/Labels/LabelFactory.cs +++ b/BTCPayServer/Services/Labels/LabelFactory.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Drawing; using Amazon.Util.Internal.PlatformServices; using BTCPayServer.Client.Models; using BTCPayServer.Data; @@ -41,12 +42,13 @@ namespace BTCPayServer.Services.Labels private ColoredLabel CreateLabel(LabelData uncoloredLabel, string color, HttpRequest request) { ArgumentNullException.ThrowIfNull(uncoloredLabel); - color = color ?? DefaultColor; + color ??= DefaultColor; - ColoredLabel coloredLabel = new ColoredLabel() + ColoredLabel coloredLabel = new ColoredLabel { Text = uncoloredLabel.Text, - Color = color + Color = color, + TextColor = TextColor(color) }; if (uncoloredLabel is ReferenceLabel refLabel) { @@ -90,5 +92,14 @@ namespace BTCPayServer.Services.Labels } return coloredLabel; } + + private string TextColor(string bgColor) + { + int nThreshold = 105; + var bg = ColorTranslator.FromHtml(bgColor); + int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114)); + Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White; + return ColorTranslator.ToHtml(color); + } } } diff --git a/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml b/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml index be680ee0c..626612b6b 100644 --- a/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml @@ -66,7 +66,7 @@ } /* pull mass action form up, so that it is besides the search form */ - @@media (min-width: 992px) { + @@media (min-width: 1200px) { #MassAction { margin-top: -4rem; } @@ -267,7 +267,7 @@ -
+
@@ -302,38 +302,38 @@ -
-
- - - - - - - - - - - - -
-
- @if (Model.Total > 0) - { +@if (Model.Total > 0) +{ + +
+ + + + + + + + + + + + +
+
@@ -452,11 +452,12 @@ - } - else - { -

- There are no invoices matching your criteria. -

- } - + + +} +else +{ +

+ There are no invoices matching your criteria. +

+} diff --git a/BTCPayServer/Views/UIStores/WalletSettings.cshtml b/BTCPayServer/Views/UIStores/WalletSettings.cshtml index ccdd842ff..4a15b37f2 100644 --- a/BTCPayServer/Views/UIStores/WalletSettings.cshtml +++ b/BTCPayServer/Views/UIStores/WalletSettings.cshtml @@ -19,48 +19,56 @@

@ViewData["Title"]

@(Model.IsHotWallet ? "Hot wallet" : "Watch-only wallet") - -
-
-
+
diff --git a/BTCPayServer/Views/UIWallets/WalletReceive.cshtml b/BTCPayServer/Views/UIWallets/WalletReceive.cshtml index 2e258febc..b5cf8f2e5 100644 --- a/BTCPayServer/Views/UIWallets/WalletReceive.cshtml +++ b/BTCPayServer/Views/UIWallets/WalletReceive.cshtml @@ -13,18 +13,17 @@
- + @if (string.IsNullOrEmpty(Model.Address)) { -

@ViewData["Title"]

- + } else {

@Model.CryptoCode Address

- - - - - - - - - - - - @foreach (var transaction in Model.Transactions) - { + + +
+
+
- - - Date - - - - LabelTransaction IdAmount
+ - - - + + + + + + + + + @foreach (var transaction in Model.Transactions) + { + + + + + + @if (transaction.Positive) + { + + } + else + { + + } + - - @if (transaction.Positive) - { - - } - else - { - - } - - - } - -
- - - - @transaction.Timestamp.ToBrowserDate() - - - @foreach (var label in transaction.Labels) - { -
-
+ + + Date + + + + LabelTransaction IdAmount
+ + + + @transaction.Timestamp.ToBrowserDate() + + + @foreach (var label in transaction.Labels) + { +
+
+ @label.Text -
+ + + +
+
+ @if (!string.IsNullOrEmpty(label.Link)) + { + + + Transaction details + + + } +
+ } +
+ + @transaction.Id + + @transaction.Balance@transaction.Balance +
+ + - @if (!string.IsNullOrEmpty(label.Link)) +
+
+ @if (string.IsNullOrEmpty(transaction.Comment)) { - - - Transaction details - - + } -
- } -
- - @transaction.Id - - @transaction.Balance@transaction.Balance -
- - -
- @if (string.IsNullOrEmpty(transaction.Comment)) - { - - } - else - { - - } - -
-
+ + + } + + +
} @@ -266,3 +253,9 @@ else There are no transactions yet.

} + +

+ If BTCPay Server shows you an invalid balance, rescan your wallet. +
+ If some transactions appear in BTCPay Server, but are missing in another wallet, follow these instructions. +

diff --git a/BTCPayServer/Views/UIWallets/_Nav.cshtml b/BTCPayServer/Views/UIWallets/_Nav.cshtml index b5d907a2e..b94724b55 100644 --- a/BTCPayServer/Views/UIWallets/_Nav.cshtml +++ b/BTCPayServer/Views/UIWallets/_Nav.cshtml @@ -1,27 +1,15 @@ -@using BTCPayServer.Views.Stores -@using BTCPayServer.Client -@inject BTCPayNetworkProvider _btcPayNetworkProvider @{ var walletId = Context.GetRouteValue("walletId")?.ToString(); var storeId = Context.GetRouteValue("storeId")?.ToString(); var cryptoCode = Context.GetRouteValue("cryptoCode")?.ToString(); var wallet = walletId != null ? WalletId.Parse(walletId) : new WalletId(storeId, cryptoCode); - var network = _btcPayNetworkProvider.GetNetwork(wallet.CryptoCode); } - + +
+ + diff --git a/BTCPayServer/wwwroot/img/icon-sprite.svg b/BTCPayServer/wwwroot/img/icon-sprite.svg index 5ec403603..a5caf1406 100644 --- a/BTCPayServer/wwwroot/img/icon-sprite.svg +++ b/BTCPayServer/wwwroot/img/icon-sprite.svg @@ -3,6 +3,7 @@ + diff --git a/BTCPayServer/wwwroot/js/copy-to-clipboard.js b/BTCPayServer/wwwroot/js/copy-to-clipboard.js index ce2628d20..a42cb1417 100644 --- a/BTCPayServer/wwwroot/js/copy-to-clipboard.js +++ b/BTCPayServer/wwwroot/js/copy-to-clipboard.js @@ -1,7 +1,7 @@ const confirmCopy = (el, message) => { el.innerText = message; setTimeout(function () { - el.innerText = el.dataset.clipboardInitialText; + el.innerHTML = el.dataset.clipboardInitial; }, 2500); } @@ -10,8 +10,8 @@ window.copyToClipboard = function (e, data) { const item = e.target.closest('[data-clipboard]'); const confirm = item.querySelector('[data-clipboard-confirm]') || item; const message = confirm.getAttribute('data-clipboard-confirm') || 'Copied ✔'; - if (!confirm.dataset.clipboardInitialText) { - confirm.dataset.clipboardInitialText = confirm.innerText; + if (!confirm.dataset.clipboardInitial) { + confirm.dataset.clipboardInitial = confirm.innerHTML; confirm.style.minWidth = confirm.getBoundingClientRect().width + 'px'; } if (navigator.clipboard) { diff --git a/BTCPayServer/wwwroot/main/layout.css b/BTCPayServer/wwwroot/main/layout.css index fefe24b46..9d5ccfee9 100644 --- a/BTCPayServer/wwwroot/main/layout.css +++ b/BTCPayServer/wwwroot/main/layout.css @@ -19,7 +19,7 @@ --icon-size: 1.5rem; height: var(--header-height); - z-index: 9999999; + z-index: 1031; /* needs a value between fixed and the offcanvas backdrop, see https://getbootstrap.com/docs/5.1/layout/z-index/ */ } #mainMenuHead { diff --git a/BTCPayServer/wwwroot/main/qrcode.css b/BTCPayServer/wwwroot/main/qrcode.css index 3c15c6efb..70f179ec5 100644 --- a/BTCPayServer/wwwroot/main/qrcode.css +++ b/BTCPayServer/wwwroot/main/qrcode.css @@ -11,7 +11,7 @@ .qr-container { position: relative; - text-align: center; + display: inline-block; } .qr-container svg { diff --git a/BTCPayServer/wwwroot/main/site.css b/BTCPayServer/wwwroot/main/site.css index fa6c7f7ff..12bad809e 100644 --- a/BTCPayServer/wwwroot/main/site.css +++ b/BTCPayServer/wwwroot/main/site.css @@ -65,6 +65,11 @@ hr.primary { } } +a.unobtrusive-link { + color: inherit; + text-decoration: inherit; +} + /* Info icons in main headline */ h2 small .fa-question-circle-o { position: relative;