Support LNURL in pay button (#5107)

* Support LNURL in pay button

* UI updates

* Cleanups

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri
2023-07-06 10:12:31 +02:00
committed by GitHub
parent 0b082138c8
commit fbe31ce64f
4 changed files with 103 additions and 21 deletions

View File

@@ -443,28 +443,37 @@ namespace BTCPayServer
} }
[HttpGet("pay")] [HttpGet("{storeId}/pay")]
[EnableCors(CorsPolicies.All)] [EnableCors(CorsPolicies.All)]
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]
public async Task<IActionResult> GetLNUrlForStore( public async Task<IActionResult> GetLNUrlForStore(
string cryptoCode, string cryptoCode,
string storeId, string storeId,
string currencyCode = null) string currency = null,
string orderId = null,
decimal? amount = null)
{ {
var store = this.HttpContext.GetStoreData(); var store = await _storeRepository.FindStore(storeId);
if (store is null) if (store is null)
return NotFound(); return NotFound();
var blob = store.GetStoreBlob(); var blob = store.GetStoreBlob();
if (!blob.AnyoneCanInvoice) if (!blob.AnyoneCanInvoice)
return NotFound("'Anyone can invoice' is turned off"); return NotFound("'Anyone can invoice' is turned off");
var metadata = new InvoiceMetadata();
if (!string.IsNullOrEmpty(orderId))
{
metadata.OrderId = orderId;
}
return await GetLNURLRequest( return await GetLNURLRequest(
cryptoCode, cryptoCode,
store, store,
blob, blob,
new CreateInvoiceRequest new CreateInvoiceRequest
{ {
Currency = currencyCode Amount = amount,
Metadata = metadata.ToJObject(),
Currency = currency
}); });
} }

View File

@@ -1,5 +1,6 @@
@using BTCPayServer.Views.Stores @using BTCPayServer.Views.Stores
@inject BTCPayServer.Security.ContentSecurityPolicies Csp @inject Security.ContentSecurityPolicies Csp
@inject BTCPayNetworkProvider NetworkProvider
@model BTCPayServer.Plugins.PayButton.Models.PayButtonViewModel @model BTCPayServer.Plugins.PayButton.Models.PayButtonViewModel
@{ @{
ViewData.SetActivePage(StoreNavPages.PayButton, "Pay Button", Context.GetStoreData().Id); ViewData.SetActivePage(StoreNavPages.PayButton, "Pay Button", Context.GetStoreData().Id);
@@ -14,6 +15,7 @@
<script src="~/vendor/highlightjs/highlight.min.js" asp-append-version="true"></script> <script src="~/vendor/highlightjs/highlight.min.js" asp-append-version="true"></script>
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script> <script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
<script src="~/vendor/vuejs-vee-validate/vee-validate.js" asp-append-version="true"></script> <script src="~/vendor/vuejs-vee-validate/vee-validate.js" asp-append-version="true"></script>
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
<script src="~/paybutton/paybutton.js" asp-append-version="true"></script> <script src="~/paybutton/paybutton.js" asp-append-version="true"></script>
<template id="template-modal" csp-allow> <template id="template-modal" csp-allow>
if (!window.btcpay) { if (!window.btcpay) {
@@ -116,13 +118,34 @@
</template> </template>
<script> <script>
window.lnurlEndpoint = @Safe.Json(Url.Action("GetLNUrlForStore", "UILNURL", new
{
storeId = Model.StoreId,
cryptoCode = NetworkProvider.DefaultNetwork.CryptoCode
}, "lnurlp", Context.Request.Host.ToString()));
const srvModel = @Safe.Json(Model); const srvModel = @Safe.Json(Model);
const payButtonCtrl = new Vue({ const payButtonCtrl = new Vue({
el: '#payButtonCtrl', el: '#payButtonCtrl',
components: {
qrcode: VueQrcode
},
data: { data: {
srvModel: srvModel, srvModel: srvModel,
originalButtonImageUrl: srvModel.payButtonImageUrl, originalButtonImageUrl: srvModel.payButtonImageUrl,
buttonInlineTextMode: false buttonInlineTextMode: false,
previewLink: "",
lnurlLink: "",
alternativeMode: 'link',
qrOptions: {
width: 256,
height: 256,
margin: 1,
color: {
dark: '#000',
light: '#f5f5f7'
}
}
}, },
computed: { computed: {
imageUrlRequired() { imageUrlRequired() {
@@ -131,7 +154,7 @@
}, },
methods: { methods: {
inputChanges(event, buttonSize) { inputChanges(event, buttonSize) {
inputChanges(event, buttonSize); inputChanges(payButtonCtrl, event, buttonSize, );
} }
}, },
watch: { watch: {
@@ -145,8 +168,9 @@
} }
this.inputChanges(); this.inputChanges();
} }
} },
}); });
inputChanges(payButtonCtrl);
</script> </script>
} }
@@ -311,10 +335,6 @@
<div class="col-xl-4 mt-4 mt-xl-0"> <div class="col-xl-4 mt-4 mt-xl-0">
<h5 class="mb-3">Preview</h5> <h5 class="mb-3">Preview</h5>
<div id="preview"></div> <div id="preview"></div>
<div v-show="!srvModel.appIdEndpoint">
<h5 class="mt-4 mb-3">Link</h5>
<span>Alternatively, you can share <a id="preview-link" href="#">this link</a> or encode it in a QR code.</span>
</div>
</div> </div>
</div> </div>
@@ -388,7 +408,6 @@
</div> </div>
<h4 class="mt-5 mb-3">Generated Code</h4> <h4 class="mt-5 mb-3">Generated Code</h4>
<div class="row" v-show="!errors.any()"> <div class="row" v-show="!errors.any()">
<div class="col-xxl-8"> <div class="col-xxl-8">
<pre><code id="mainCode" class="html"></code></pre> <pre><code id="mainCode" class="html"></code></pre>
@@ -402,6 +421,49 @@
Please fix errors shown in order for code generation to successfully execute. Please fix errors shown in order for code generation to successfully execute.
</div> </div>
</div> </div>
<div v-if="!srvModel.appIdEndpoint && (previewLink || lnurlLink)">
<h4 class="mt-4 mb-3">Alternatives</h4>
<p>You can also share the link/LNURL or encode it in a QR code.</p>
<div class="align-items-center" style="width:256px">
<ul class="nav my-3 btcpay-pills align-items-center gap-2">
<li class="nav-item" v-if="previewLink">
<a class="btcpay-pill" :class="{ active: alternativeMode === 'link' }" data-bs-toggle="tab" data-bs-target="#Alternative-Link" role="tab" href="#Alternative-Link">
Link
</a>
</li>
<li class="nav-item" v-if="previewLink">
<a class="btcpay-pill" :class="{ active: alternativeMode === 'lnurl' }" data-bs-toggle="tab" data-bs-target="#Alternative-LNURL" role="tab" href="#Alternative-LNURL">
LNURL
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane" :class="{ active: alternativeMode === 'link' }" id="Alternative-Link" role="tabpanel">
<a class="qr-container d-inline-block" :class="{ active: true }" :href="previewLink">
<qrcode :value="previewLink" :options="qrOptions" tag="img"></qrcode>
</a>
<div class="input-group mt-3">
<div class="form-floating">
<vc:truncate-center text="previewLink" is-vue="true" padding="15" elastic="true" classes="form-control-plaintext" />
<label>Link URL</label>
</div>
</div>
</div>
<div class="tab-pane" :class="{ active: alternativeMode === 'lnurl' }" id="Alternative-LNURL" role="tabpanel">
<a class="qr-container d-inline-block" :href="lnurlLink">
<qrcode :value="lnurlLink" :options="qrOptions" tag="img"></qrcode>
</a>
<div class="input-group mt-3">
<div class="form-floating">
<vc:truncate-center text="lnurlLink" is-vue="true" padding="15" elastic="true" classes="form-control-plaintext" />
<label>LNURL</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<script id="template-paybutton-styles" type="text/template"> <script id="template-paybutton-styles" type="text/template">

View File

@@ -11,7 +11,7 @@
<div class="modal-body text-center"> <div class="modal-body text-center">
<div class="text-center my-2" :style="{height: `${qrOptions.height}px`}"> <div class="text-center my-2" :style="{height: `${qrOptions.height}px`}">
<component v-if="currentFragment" :is="currentMode.href ? 'a': 'div'" class="qr-container d-inline-block" :href="currentMode.href"> <component v-if="currentFragment" :is="currentMode.href ? 'a': 'div'" class="qr-container d-inline-block" :href="currentMode.href">
<qrcode :value="currentFragment" :options="qrOptions"></qrcode> <qrcode :value="currentFragment" :options="qrOptions"></qrcode>
</component> </component>
</div> </div>
<ul class="nav btcpay-pills justify-content-center mt-4 mb-3" v-if="modes && Object.keys(modes).length > 1"> <ul class="nav btcpay-pills justify-content-center mt-4 mb-3" v-if="modes && Object.keys(modes).length > 1">

View File

@@ -1,6 +1,3 @@
$(function () {
inputChanges();
});
function esc(input) { function esc(input) {
return ('' + input) /* Forces the conversion to string. */ return ('' + input) /* Forces the conversion to string. */
@@ -61,7 +58,7 @@ function getScripts(srvModel) {
return scripts return scripts
} }
function inputChanges(event, buttonSize) { function inputChanges(vueApp, event, buttonSize) {
if (buttonSize !== null && buttonSize !== undefined) { if (buttonSize !== null && buttonSize !== undefined) {
srvModel.buttonSize = buttonSize; srvModel.buttonSize = buttonSize;
} }
@@ -187,8 +184,22 @@ function inputChanges(event, buttonSize) {
} }
}); });
url = url.href; url = url.href;
vueApp.previewLink = url;
$("#preview-link").attr('href', url); if (window.lnurlEndpoint){
let lnurlResult = lnurlEndpoint + "?";
if (srvModel.currency){
lnurlResult += `&currency=${srvModel.currency}`;
}
if (srvModel.price){
lnurlResult += `&amount=${srvModel.price}`;
}
if (srvModel.orderId){
lnurlResult += `&orderId=${srvModel.orderId}`;
}
lnurlResult= lnurlResult.replace("?&", "?");
vueApp.lnurlLink = lnurlResult;
}
$('pre code').each(function (i, block) { $('pre code').each(function (i, block) {
hljs.highlightBlock(block); hljs.highlightBlock(block);