mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
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:
@@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 += `¤cy=${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);
|
||||||
|
|||||||
Reference in New Issue
Block a user