Checkout v2: Clicking QR code copies full payment URI (#5627)

* Checkout v2: Clicking QR code copies full payment URI

Before it copied only the destination value (Bitcoin address or Lightning BOLT11). This didn't include the BOLT11 in case of the unified QR code. Now it will copy the full payment URI, which is the same as the QR represents:

- Unified: `bitcoin:ADDRESS?amount=AMOUNT&lightning=BOLT11`
- Bitcoin: `bitcoin:ADDRESS?amount=AMOUNT`
- Lightning: `lightning:BOLT11`

Fixes #5625.

* Test fix
This commit is contained in:
d11n 2024-01-16 08:54:59 +01:00 committed by GitHub
parent 5e25ee2996
commit 89d294524a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 29 additions and 26 deletions

View file

@ -60,13 +60,13 @@ namespace BTCPayServer.Tests
Assert.Contains("Bitcoin", s.Driver.FindElement(By.CssSelector(".payment-method.active")).Text); Assert.Contains("Bitcoin", s.Driver.FindElement(By.CssSelector(".payment-method.active")).Text);
Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text); Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text);
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); var clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddress = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text; var address = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
Assert.Equal($"bitcoin:{address}", payUrl);
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text); Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text);
Assert.DoesNotContain("lightning=", payUrl); Assert.DoesNotContain("lightning=", payUrl);
Assert.Equal(address, copyAddress); Assert.Equal($"bitcoin:{address}", payUrl);
Assert.Equal($"bitcoin:{address}", clipboard);
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue); Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC")); s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
@ -97,11 +97,11 @@ namespace BTCPayServer.Tests
Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text); Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text);
Assert.Contains("Bitcoin", s.Driver.WaitForElement(By.CssSelector(".payment-method")).Text); Assert.Contains("Bitcoin", s.Driver.WaitForElement(By.CssSelector(".payment-method")).Text);
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddress = s.Driver.FindElement(By.CssSelector("#Lightning_BTC_LightningLike .truncate-center-start")).Text; address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC_LightningLike .truncate-center-start")).Text;
Assert.Equal($"lightning:{address}", payUrl); Assert.Equal($"lightning:{address}", payUrl);
Assert.Equal(address, copyAddress); Assert.Equal($"lightning:{address}", clipboard);
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue); Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC")); s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
@ -153,7 +153,7 @@ namespace BTCPayServer.Tests
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
await Task.Delay(200); await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); address = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
var amountFraction = "0.00001"; var amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest), await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction)); Money.Parse(amountFraction));
@ -202,15 +202,14 @@ namespace BTCPayServer.Tests
// Pay partial amount // Pay partial amount
await Task.Delay(200); await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); s.Driver.FindElement(By.Id("test-payment-amount")).Clear();
amountFraction = "0.00001"; s.Driver.FindElement(By.Id("test-payment-amount")).SendKeys("0.00001");
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
await s.Server.ExplorerNode.GenerateAsync(1);
// Fake Pay // Fake Pay
TestUtils.Eventually(() => TestUtils.Eventually(() =>
{ {
s.Driver.FindElement(By.Id("FakePayment")).Click();
s.Driver.FindElement(By.Id("mine-block")).Click();
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo")); paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("The invoice hasn't been paid in full", paymentInfo.Text); Assert.Contains("The invoice hasn't been paid in full", paymentInfo.Text);
Assert.Contains("Please send", paymentInfo.Text); Assert.Contains("Please send", paymentInfo.Text);
@ -265,18 +264,19 @@ namespace BTCPayServer.Tests
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method"))); Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text); Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text; var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text; var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{address}?amount=", payUrl); Assert.StartsWith($"bitcoin:{copyAddressOnchain}?amount=", payUrl);
Assert.Contains("?amount=", payUrl); Assert.Contains("?amount=", payUrl);
Assert.Contains("&lightning=", payUrl); Assert.Contains("&lightning=", payUrl);
Assert.StartsWith("bcrt", copyAddressOnchain); Assert.StartsWith("bcrt", copyAddressOnchain);
Assert.Equal(address, copyAddressOnchain);
Assert.StartsWith("lnbcrt", copyAddressLightning); Assert.StartsWith("lnbcrt", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?amount=", qrValue); Assert.StartsWith($"bitcoin:{copyAddressOnchain.ToUpperInvariant()}?amount=", qrValue);
Assert.Contains("&lightning=LNBCRT", qrValue); Assert.Contains("&lightning=LNBCRT", qrValue);
Assert.Contains("&lightning=lnbcrt", clipboard);
Assert.Equal(clipboard, payUrl);
// Check details // Check details
s.Driver.ToggleCollapse("PaymentDetails"); s.Driver.ToggleCollapse("PaymentDetails");
@ -333,17 +333,18 @@ namespace BTCPayServer.Tests
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method"))); Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value"); qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard"); clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href"); payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text; copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text; copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{address}", payUrl); Assert.StartsWith($"bitcoin:{copyAddressOnchain}", payUrl);
Assert.Contains("?lightning=lnurl", payUrl); Assert.Contains("?lightning=lnurl", payUrl);
Assert.DoesNotContain("amount=", payUrl); Assert.DoesNotContain("amount=", payUrl);
Assert.StartsWith("bcrt", copyAddressOnchain); Assert.StartsWith("bcrt", copyAddressOnchain);
Assert.Equal(address, copyAddressOnchain);
Assert.StartsWith("lnurl", copyAddressLightning); Assert.StartsWith("lnurl", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue); Assert.StartsWith($"bitcoin:{copyAddressOnchain.ToUpperInvariant()}?lightning=LNURL", qrValue);
Assert.Contains($"bitcoin:{copyAddressOnchain}?lightning=lnurl", clipboard);
Assert.Equal(clipboard, payUrl);
// Check details // Check details
s.Driver.ToggleCollapse("PaymentDetails"); s.Driver.ToggleCollapse("PaymentDetails");

View file

@ -6,7 +6,7 @@
<template id="bitcoin-method-checkout-template"> <template id="bitcoin-method-checkout-template">
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-bitcoin-pre-content", model = Model}) @await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-bitcoin-pre-content", model = Model})
<div class="payment-box"> <div class="payment-box">
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.btcAddress"> <div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.invoiceBitcoinUrl" data-clipboard-confirm-element="#Address_@Model.PaymentMethodId [data-clipboard]">
<div> <div>
<qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" /> <qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" />
</div> </div>

View file

@ -3,7 +3,7 @@
<template id="lightning-method-checkout-template"> <template id="lightning-method-checkout-template">
<div class="payment-box"> <div class="payment-box">
@await Component.InvokeAsync("UiExtensionPoint" , new { location="checkout-v2-lightning-pre-content", model = Model}) @await Component.InvokeAsync("UiExtensionPoint" , new { location="checkout-v2-lightning-pre-content", model = Model})
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.btcAddress"> <div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.invoiceBitcoinUrl" data-clipboard-confirm-element="#Lightning_@Model.PaymentMethodId [data-clipboard]">
<div> <div>
<qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" /> <qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" />
</div> </div>

View file

@ -4,8 +4,10 @@ function confirmCopy(el, message) {
if (hasIcon) { if (hasIcon) {
el.innerHTML = el.innerHTML.replace('#copy', '#checkmark'); el.innerHTML = el.innerHTML.replace('#copy', '#checkmark');
} else { } else {
const { width, height } = el.getBoundingClientRect();
el.dataset.clipboardInitial = el.innerHTML; el.dataset.clipboardInitial = el.innerHTML;
el.style.minWidth = el.getBoundingClientRect().width + 'px'; el.style.minWidth = width + 'px';
el.style.minHeight = height + 'px';
el.innerHTML = confirmHTML; el.innerHTML = confirmHTML;
} }
el.dataset.clipboardConfirming = true; el.dataset.clipboardConfirming = true;
@ -28,7 +30,7 @@ window.copyToClipboard = async function (e, data) {
e.preventDefault(); e.preventDefault();
const item = e.target.closest('[data-clipboard]') || e.target.closest('[data-clipboard-target]') || e.target; const item = e.target.closest('[data-clipboard]') || e.target.closest('[data-clipboard-target]') || e.target;
const confirm = item.dataset.clipboardConfirmElement const confirm = item.dataset.clipboardConfirmElement
? document.getElementById(item.dataset.clipboardConfirmElement) || item ? document.querySelector(item.dataset.clipboardConfirmElement) || item
: item.querySelector('[data-clipboard-confirm]') || item; : item.querySelector('[data-clipboard-confirm]') || item;
const message = confirm.getAttribute('data-clipboard-confirm') || 'Copied'; const message = confirm.getAttribute('data-clipboard-confirm') || 'Copied';
// Check compatibility and permissions: // Check compatibility and permissions: