Introduce QR Code View component (#2125)

* Introduce QR Code View component

* more cleanup

* fix js clipboard

* Fix clipboard confirmation width calculation

* fix tests

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri
2020-12-22 03:18:51 +01:00
committed by GitHub
parent 26b04e70b1
commit 5ca4e71c34
9 changed files with 87 additions and 163 deletions

View File

@@ -329,7 +329,7 @@ namespace BTCPayServer.Tests
walletId ??= WalletId;
GoToWallet(walletId, WalletsNavPages.Receive);
Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = Driver.FindElement(By.Id("vue-address")).GetProperty("value");
var addressStr = Driver.FindElement(By.Id("address")).GetProperty("value");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
for (int i = 0; i < coins; i++)
{

View File

@@ -560,7 +560,7 @@ namespace BTCPayServer.Tests
var walletId = new WalletId(storeId, "BTC");
s.GoToWallet(walletId, WalletsNavPages.Receive);
s.Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = s.Driver.FindElement(By.Id("vue-address")).GetProperty("value");
var addressStr = s.Driver.FindElement(By.Id("address")).GetProperty("value");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
await s.Server.ExplorerNode.GenerateAsync(1);
for (int i = 0; i < 6; i++)
@@ -761,13 +761,13 @@ namespace BTCPayServer.Tests
//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);
var receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value");
var receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value");
//unreserve
s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click();
//generate it again, should be the same one as before as nothign got used in the meantime
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value"));
//send money to addr and ensure it changed
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
@@ -779,8 +779,8 @@ namespace BTCPayServer.Tests
await Task.Delay(200);
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value");
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value"));
receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value");
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
s.GoToStore(storeId.storeId);
@@ -789,7 +789,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletReceive")).Click();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value"));
var invoiceId = s.CreateInvoice(storeId.storeName);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);

View File

@@ -57,6 +57,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="QRCoder" Version="1.4.1" />
<PackageReference Include="System.IO.Pipelines" Version="4.7.2" />
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
<PackageReference Include="DBriize" Version="1.0.1.3" />

View File

@@ -0,0 +1,22 @@
using System.Drawing;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using QRCoder;
namespace BTCPayServer.Components.QRCode
{
public class QRCode : ViewComponent
{
private static QRCodeGenerator qrGenerator = new QRCodeGenerator();
public IViewComponentResult Invoke(string data)
{
QRCodeData qrCodeData = qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
SvgQRCode qrCode = new SvgQRCode(qrCodeData);
return new HtmlContentViewComponentResult(new HtmlString(qrCode.GetGraphic(new Size(256,256), "#000", "#f5f5f7", true, SvgQRCode.SizingMode.ViewBoxAttribute)));
}
}
}

View File

@@ -18,65 +18,13 @@
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" asp-append-version="true" />
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.ThemeUri)" rel="stylesheet" asp-append-version="true" />
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" asp-append-version="true" />
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js" asp-append-version="true" />
<script type="text/javascript">
var srvModel = @Safe.Json(Model);
window.onload = function() {
Vue.use(Toasted);
new Vue({
el: '#app',
components: {
qrcode: VueQrcode
},
data: {
srvModel: srvModel
},
mounted: function() {
this.$nextTick(function() {
var copyInput = new Clipboard('.copy');
copyInput.on("success", function() {
Vue.toasted.show('Copied', {
iconPack: "fontawesome",
icon: "copy",
duration: 5000
});
});
});
}
});
}
<link href="~/main/qrcode.css" rel="stylesheet" asp-append-version="true" />
<script src="~/js/copy-to-clipboard.js"></script>
<script>
window.onload = function (){
document.querySelectorAll('[data-clipboard]').forEach(value => value.addEventListener('click', window.copyToClipboard));
}
</script>
<style>
[v-cloak] {
display: none;
}
.qr-icon {
height: 64px;
width: 64px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
.qr-container {
position: relative;
text-align: center;
}
.qr-container svg {
width: 256px;
height: 256px;
}
.copy {
cursor: copy;
}
</style>
</head>
<body>
<div id="app" class="container">
@@ -84,29 +32,32 @@
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto">
<div class="card border-0">
<div class="card-body p-4">
<h1 class="card-title text-center" v-text="srvModel.storeName">@Model.StoreName</h1>
<h1 class="card-title text-center">@Model.StoreName</h1>
<h2 class="card-subtitle text-center text-secondary mb-2">
<span v-text="srvModel.cryptoCode">@Model.CryptoCode</span>
<span>@Model.CryptoCode</span>
Lightning Node
</h2>
<h3 class="card-title text-center">
<span v-text="srvModel.available ? 'Online' : 'Unavailable'">
<span>
@(Model.Available ? "Online" : "Unavailable")
</span>
<small class="text-@(Model.Available ? "success" : "danger")" :class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
<small class="text-@(Model.Available ? "success" : "danger")" >
<span class="fa fa-circle"></span>
</small>
</h3>
<div class="qr-container my-3" v-cloak v-if="srvModel.available">
<img src="" alt="@Model.CryptoCode" class="qr-icon" :src="srvModel.cryptoImage" v-bind:alt="srvModel.cryptoCode" />
<qrcode v-bind:value="srvModel.nodeInfo" :options="{ width: 256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"></qrcode>
</div>
<div data-clipboard-target="#peer-info" class="input-group copy d-@(Model.Available ? "flex" : "none")" :class="{ 'd-flex': srvModel.available, 'd-none': !srvModel.available }">
<input type="text" class="form-control" readonly="readonly" asp-for="NodeInfo" id="peer-info" :value="srvModel.nodeInfo" />
<div class="input-group-append">
<span class="input-group-text fa fa-copy py-2"></span>
@if (Model.Available)
{
<div class="qr-container my-3">
<img alt="@Model.CryptoCode" class="qr-icon" src="@Model.CryptoImage" />
<vc:qr-code data="@Model.NodeInfo"> </vc:qr-code>
</div>
</div>
<div class="input-group d-flex" data-clipboard="@Model.NodeInfo">
<input type="text" class="form-control" style="cursor: copy" readonly="readonly" value="@Model.NodeInfo" id="peer-info"/>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" data-clipboard-confirm>Copy node info</button>
</div>
</div>
}
</div>
</div>
</div>

View File

@@ -1,5 +1,4 @@

@addTagHelper *, BundlerMinifier.TagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@model BTCPayServer.Controllers.WalletReceiveViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
@@ -10,7 +9,7 @@
{
<div class="row">
<div class="col-md-12 text-center">
<partial name="_StatusMessage" />
<partial name="_StatusMessage"/>
</div>
</div>
}
@@ -20,7 +19,6 @@
<div class="card-body">
@if (string.IsNullOrEmpty(Model.Address))
{
<h3 class="card-title mb-3">Receive @Model.CryptoCode</h3>
<button id="generateButton" class="btn btn-lg btn-primary" type="submit" name="command" value="generate-new-address">Generate @Model.CryptoCode address</button>
}
@@ -30,7 +28,7 @@
<noscript>
<div class="card-body m-sm-0 p-sm-0">
<div class="input-group">
<input type="text" class="form-control " readonly="readonly" asp-for="Address" id="address" />
<input type="text" class="form-control " readonly="readonly" asp-for="Address" id="address"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</div>
@@ -47,14 +45,13 @@
</noscript>
<div class="only-for-js card-body m-sm-0 p-sm-0" id="app">
<div class="qr-container mb-4">
<img v-bind:src="srvModel.cryptoImage" class="qr-icon" />
<qrcode v-bind:value="srvModel.address" :options="{ width: 256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg">
</qrcode>
<img src="@Model.CryptoImage" class="qr-icon"/>
<vc:qr-code data="@Model.Address"/>
</div>
<div class="input-group copy" data-clipboard-target="#vue-address">
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.address" id="vue-address" />
<div class="input-group" data-clipboard="@Model.Address">
<input type="text" class="form-control" style="cursor: copy" readonly="readonly" value="@Model.Address" id="address"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
<button type="button" class="btn btn-outline-secondary" data-clipboard-confirm>Copy address</button>
</div>
</div>
<div class="row mt-4">
@@ -73,69 +70,12 @@
</div>
@section HeadScripts
{
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js" asp-append-version="true" />
<script type="text/javascript">
var srvModel = @Safe.Json(Model);
window.onload = function() {
if($("#app").length <1){
return;
}
Vue.use(Toasted);
var app = new Vue({
el: '#app',
components: {
qrcode: VueQrcode
},
data: {
srvModel: srvModel
},
mounted: function() {
this.$nextTick(function() {
var copyInput = new Clipboard('.copy');
copyInput.on("success",
function(e) {
Vue.toasted.show('Copied',
{
iconPack: "fontawesome",
icon: "copy",
duration: 5000
});
});
});
}
});
<link href="~/main/qrcode.css" rel="stylesheet" asp-append-version="true"/>
<script src="~/js/copy-to-clipboard.js"></script>
<script>
window.onload = function (){
document.querySelectorAll('[data-clipboard]').forEach(value => value.addEventListener('click', window.copyToClipboard));
}
</script>
<style>
.qr-icon {
height: 64px;
width: 64px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
.qr-container {
position: relative;
text-align: center;
}
.qr-container svg {
width: 256px;
height: 256px;
}
.copy {
cursor: copy;
}
</style>
</script>
}

View File

@@ -58,16 +58,6 @@
"wwwroot/checkout/**/*.js"
]
},
{
"outputFileName": "wwwroot/bundles/lightning-node-info-bundle.min.js",
"inputFiles": [
"wwwroot/vendor/clipboard.js/clipboard.js",
"wwwroot/vendor/jquery/jquery.js",
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/vue-qrcode/vue-qrcode.min.js",
"wwwroot/vendor/vue-toasted/vue-toasted.min.js"
]
},
{
"outputFileName": "wwwroot/bundles/cart-bundle.min.js",
"inputFiles": [

View File

@@ -7,7 +7,7 @@ window.copyToClipboard = function (e, text) {
var message = confirm.getAttribute('data-clipboard-confirm') || 'Copied ✔';
if (!confirm.dataset.clipboardInitialText) {
confirm.dataset.clipboardInitialText = confirm.innerText;
confirm.style.minWidth = confirm.clientWidth + 'px';
confirm.style.minWidth = confirm.getBoundingClientRect().width + 'px';
}
navigator.clipboard.writeText(data).then(function () {
confirm.innerText = message;

View File

@@ -0,0 +1,20 @@
.qr-icon {
height: 64px;
width: 64px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
.qr-container {
position: relative;
text-align: center;
}
.qr-container svg {
width: 256px;
height: 256px;
}