Compare commits
10 Commits
v1.13.2
...
v1.11.7-af
Author | SHA1 | Date | |
---|---|---|---|
c3ffcae89d | |||
6662cb7ea9 | |||
c8123f932f | |||
8c5648108a | |||
5793f92c8a | |||
5df5ce0037 | |||
25c30512ec | |||
52df7a5b89 | |||
1a85da27db | |||
10326a822e |
@ -137,6 +137,7 @@
|
||||
<ItemGroup>
|
||||
<Watch Include="Views\**\*.*"></Watch>
|
||||
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
||||
<Watch Remove="Views\UIPullPayment\ViewPullPaymentPrint.cshtml" />
|
||||
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
|
||||
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
|
@ -11,13 +11,14 @@ namespace BTCPayServer.Components.QRCode
|
||||
{
|
||||
private static QRCodeGenerator _qrGenerator = new();
|
||||
|
||||
public IViewComponentResult Invoke(string data)
|
||||
public IViewComponentResult Invoke(string data, int size=256)
|
||||
{
|
||||
var qrCodeData = _qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
|
||||
var qrCode = new PngByteQRCode(qrCodeData);
|
||||
var bytes = qrCode.GetGraphic(5, new byte[] { 0, 0, 0, 255 }, new byte[] { 0xf5, 0xf5, 0xf7, 255 });
|
||||
var b64 = Convert.ToBase64String(bytes);
|
||||
return new HtmlContentViewComponentResult(new HtmlString($"<img style=\"image-rendering:pixelated;image-rendering:-moz-crisp-edges;min-width:256px;min-height:256px\" src=\"data:image/png;base64,{b64}\" class=\"qr-code\" />"));
|
||||
return new HtmlContentViewComponentResult(new HtmlString(
|
||||
$"<img style=\"image-rendering:pixelated;image-rendering:-moz-crisp-edges;min-width:{size}px;min-height:{size}px\" src=\"data:image/png;base64,{b64}\" class=\"qr-code\" />"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -254,7 +254,7 @@ namespace BTCPayServer.Controllers
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.InvoicePaidAmount.Net,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
AmountFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Gross, paymentEntity.PaidAmount.Currency, DisplayFormatter.CurrencyFormat.None),
|
||||
AmountFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Gross, paymentEntity.PaidAmount.Currency),
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
|
@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("pull-payments/{pullPaymentId}")]
|
||||
public async Task<IActionResult> ViewPullPayment(string pullPaymentId)
|
||||
public async Task<IActionResult> ViewPullPayment(string pullPaymentId, [FromQuery] bool print = false)
|
||||
{
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
|
||||
@ -111,8 +111,9 @@ namespace BTCPayServer.Controllers
|
||||
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
||||
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
|
||||
}
|
||||
|
||||
return View(nameof(ViewPullPayment), vm);
|
||||
|
||||
|
||||
return View(print ? "ViewPullPaymentPrint" : "ViewPullPayment", vm);
|
||||
}
|
||||
|
||||
[HttpGet("stores/{storeId}/pull-payments/edit/{pullPaymentId}")]
|
||||
|
@ -135,7 +135,7 @@ namespace BTCPayServer.PaymentRequest
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.InvoicePaidAmount.Net,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
AmountFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Gross, paymentEntity.PaidAmount.Currency, DisplayFormatter.CurrencyFormat.None),
|
||||
AmountFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Gross, paymentEntity.PaidAmount.Currency),
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
|
@ -32,7 +32,7 @@
|
||||
|
||||
<script>
|
||||
Vue.component('BitcoinLikeMethodCheckout', {
|
||||
props: ["model"],
|
||||
props: ['model', 'nfcSupported', 'nfcScanning'],
|
||||
template: "#bitcoin-method-checkout-template",
|
||||
components: {
|
||||
qrcode: VueQrcode
|
||||
|
@ -79,7 +79,7 @@
|
||||
<script type="text/javascript">
|
||||
Vue.component('BitcoinLikeMethodCheckout',
|
||||
{
|
||||
props: ["srvModel"],
|
||||
props: ['srvModel', 'nfcSupported', 'nfcScanning'],
|
||||
template: "#bitcoin-method-checkout-template",
|
||||
components: {
|
||||
qrcode: VueQrcode
|
||||
@ -107,7 +107,7 @@
|
||||
});
|
||||
|
||||
Vue.component('BitcoinLikeMethodCheckoutHeader', {
|
||||
props: ["srvModel"],
|
||||
props: ['srvModel', 'nfcSupported', 'nfcScanning'],
|
||||
template: "#bitcoin-method-checkout-header-template",
|
||||
data: function() {
|
||||
return {
|
||||
|
@ -24,7 +24,7 @@
|
||||
|
||||
<script>
|
||||
Vue.component('LightningLikeMethodCheckout', {
|
||||
props: ["model"],
|
||||
props: ['model', 'nfcSupported', 'nfcScanning'],
|
||||
template: "#lightning-method-checkout-template",
|
||||
components: {
|
||||
qrcode: VueQrcode
|
||||
|
@ -80,7 +80,7 @@
|
||||
<script type="text/javascript">
|
||||
Vue.component('LightningLikeMethodCheckout',
|
||||
{
|
||||
props: ["srvModel"],
|
||||
props: ['srvModel', 'nfcSupported', 'nfcScanning'],
|
||||
template: "#lightning-method-checkout-template",
|
||||
components: {
|
||||
qrcode: VueQrcode
|
||||
|
@ -1,48 +1,30 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
<template id="lnurl-withdraw-template">
|
||||
<template v-if="display">
|
||||
<div class="mt-4">
|
||||
<p id="CheatSuccessMessage" class="alert alert-success text-break" v-if="successMessage" v-text="successMessage"></p>
|
||||
<p id="CheatErrorMessage" class="alert alert-danger text-break" v-if="errorMessage" v-text="errorMessage"></p>
|
||||
<template v-if="isV2">
|
||||
<button class="btn btn-secondary rounded-pill w-100" type="button" id="PayByNFC"
|
||||
:disabled="scanning || submitting" v-on:click="handleClick">{{btnText}}</button>
|
||||
</template>
|
||||
<bp-loading-button v-else>
|
||||
<button class="action-button" style="margin: 0 45px;width:calc(100% - 90px) !important" :disabled="scanning || submitting" v-on:click="handleClick" id="PayByNFC"
|
||||
:class="{ 'loading': scanning || submitting, 'action-button': supported, 'btn btn-text w-100': !supported }">
|
||||
<div v-if="display" class="mt-4">
|
||||
<template v-if="isV2">
|
||||
<button class="btn btn-secondary rounded-pill w-100" type="button" id="PayByNFC"
|
||||
:disabled="nfcScanning || submitting" v-on:click="handleClick">{{btnText}}</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<bp-loading-button>
|
||||
<button class="action-button" style="margin: 0 45px;width:calc(100% - 90px) !important" :disabled="nfcScanning || submitting" v-on:click="handleClick" id="PayByNFC"
|
||||
:class="{ 'action-button': nfcSupported, 'btn btn-text w-100': !nfcSupported }">
|
||||
<span class="button-text">{{btnText}}</span>
|
||||
<div class="loader-wrapper">
|
||||
@await Html.PartialAsync("~/Views/UIInvoice/Checkout-Spinner.cshtml")
|
||||
</div>
|
||||
</button>
|
||||
</bp-loading-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
class NDEFReaderWrapper {
|
||||
constructor() {
|
||||
this.onreading = null;
|
||||
this.onreadingerror = null;
|
||||
}
|
||||
|
||||
async scan(opts) {
|
||||
if (opts && opts.signal){
|
||||
opts.signal.addEventListener('abort', () => {
|
||||
window.parent.postMessage('nfc:abort', '*');
|
||||
});
|
||||
}
|
||||
window.parent.postMessage('nfc:startScan', '*');
|
||||
}
|
||||
}
|
||||
|
||||
Vue.component("lnurl-withdraw-checkout", {
|
||||
template: "#lnurl-withdraw-template",
|
||||
props: {
|
||||
model: Object,
|
||||
isV2: Boolean
|
||||
isV2: Boolean,
|
||||
nfcSupported: Boolean,
|
||||
nfcScanning: Boolean
|
||||
},
|
||||
computed: {
|
||||
display () {
|
||||
@ -62,16 +44,16 @@ Vue.component("lnurl-withdraw-checkout", {
|
||||
(activePaymentMethodId === 'BTC' && isUnified && lnurlwAvailable) ||
|
||||
// Lightning with LNURL available
|
||||
(activePaymentMethodId === 'BTC_LightningLike' && lnurlwAvailable))
|
||||
return isAvailable && (this.supported || this.testFallback)
|
||||
return isAvailable && (this.nfcSupported || this.testFallback)
|
||||
},
|
||||
testFallback () {
|
||||
return !this.supported && window.location.search.match('lnurlwtest=(1|true)')
|
||||
return !this.nfcSupported && window.location.search.match('lnurlwtest=(1|true)')
|
||||
},
|
||||
btnText () {
|
||||
if (this.supported) {
|
||||
if (this.nfcSupported) {
|
||||
if (this.submitting) {
|
||||
return this.isV2 ? this.$t('submitting_nfc') : 'Submitting NFC …'
|
||||
} else if (this.scanning) {
|
||||
} else if (this.nfcScanning) {
|
||||
return this.isV2 ? this.$t('scanning_nfc') : 'Scanning NFC …'
|
||||
} else {
|
||||
return this.isV2 ? this.$t('pay_by_nfc') : 'Pay by NFC'
|
||||
@ -84,35 +66,20 @@ Vue.component("lnurl-withdraw-checkout", {
|
||||
data () {
|
||||
return {
|
||||
url: @Safe.Json(Context.Request.GetAbsoluteUri(Url.Action("SubmitLNURLWithdrawForInvoice", "NFC"))),
|
||||
supported: 'NDEFReader' in window,
|
||||
scanning: false,
|
||||
submitting: false,
|
||||
permissionGranted: false,
|
||||
readerAbortController: null,
|
||||
amount: 0,
|
||||
successMessage: null,
|
||||
errorMessage: null
|
||||
submitting: false
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
if (!this.supported) return;
|
||||
try {
|
||||
this.permissionGranted = navigator.permissions &&
|
||||
(await navigator.permissions.query({ name: 'nfc' })).state === 'granted'
|
||||
} catch (e) {}
|
||||
if (this.permissionGranted) {
|
||||
this.startScan()
|
||||
}
|
||||
beforeMount () {
|
||||
this.$root.$on('read-nfc-data', this.sendData)
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.readerAbortController) {
|
||||
this.readerAbortController.abort()
|
||||
}
|
||||
this.$root.$off('read-nfc-data')
|
||||
},
|
||||
methods: {
|
||||
async handleClick () {
|
||||
if (this.supported) {
|
||||
this.startScan()
|
||||
if (this.nfcSupported) {
|
||||
this.$emit('start-nfc-scan')
|
||||
} else {
|
||||
if (this.model.isUnsetTopUp) {
|
||||
this.handleUnsetTopUp()
|
||||
@ -132,94 +99,29 @@ Vue.component("lnurl-withdraw-checkout", {
|
||||
try {
|
||||
this.amount = parseInt(amountStr)
|
||||
} catch {
|
||||
alert("Please provide a valid number amount in sats");
|
||||
alert("Please provide a valid number amount in sats")
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
async startScan () {
|
||||
if (this.scanning || this.submitting) {
|
||||
return;
|
||||
}
|
||||
if (this.model.isUnsetTopUp) {
|
||||
this.handleUnsetTopUp()
|
||||
if (!this.amount) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.submitting = false;
|
||||
this.scanning = true;
|
||||
try {
|
||||
const inModal = window.self !== window.top;
|
||||
const ndef = inModal ? new NDEFReaderWrapper() : new NDEFReader();
|
||||
this.readerAbortController = new AbortController()
|
||||
this.readerAbortController.signal.onabort = () => {
|
||||
this.scanning = false;
|
||||
};
|
||||
|
||||
await ndef.scan({ signal: this.readerAbortController.signal })
|
||||
|
||||
ndef.onreadingerror = () => this.reportNfcError('Could not read NFC tag')
|
||||
|
||||
ndef.onreading = async ({ message }) => {
|
||||
const record = message.records[0]
|
||||
const textDecoder = new TextDecoder('utf-8')
|
||||
const lnurl = textDecoder.decode(record.data)
|
||||
await this.sendData(lnurl)
|
||||
}
|
||||
|
||||
if (inModal) {
|
||||
// receive messages from iframe
|
||||
window.addEventListener('message', async event => {
|
||||
// deny messages from other origins
|
||||
if (event.origin !== window.location.origin) return
|
||||
|
||||
const { action, data } = event.data
|
||||
switch (action) {
|
||||
case 'nfc:data':
|
||||
await this.sendData(data)
|
||||
break;
|
||||
case 'nfc:error':
|
||||
this.reportNfcError('Could not read NFC tag')
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// we came here, so the user must have allowed NFC access
|
||||
this.permissionGranted = true;
|
||||
} catch (error) {
|
||||
this.reportNfcError(`NFC scan failed: ${error}`);
|
||||
}
|
||||
},
|
||||
async sendData (lnurl) {
|
||||
this.submitting = true;
|
||||
this.successMessage = null;
|
||||
this.errorMessage = null;
|
||||
|
||||
if (this.isV2) this.$root.playSound('nfcRead');
|
||||
async sendData (data) {
|
||||
this.submitting = true
|
||||
this.$emit('handle-nfc-data')
|
||||
|
||||
// Post LNURL-Withdraw data to server
|
||||
const body = JSON.stringify({ lnurl, invoiceId: this.model.invoiceId, amount: this.amount })
|
||||
const body = JSON.stringify({ lnurl: data, invoiceId: this.model.invoiceId, amount: this.amount })
|
||||
const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body }
|
||||
const response = await fetch(this.url, opts)
|
||||
|
||||
// Handle response
|
||||
try {
|
||||
const result = await response.text()
|
||||
if (response.ok) {
|
||||
this.successMessage = result;
|
||||
} else {
|
||||
this.reportNfcError(result);
|
||||
}
|
||||
const action = response.ok ? 'handle-nfc-result' : 'handle-nfc-error'
|
||||
this.$emit(action, result)
|
||||
} catch (error) {
|
||||
this.reportNfcError(error);
|
||||
this.$emit('handle-nfc-error', error)
|
||||
}
|
||||
this.submitting = false;
|
||||
},
|
||||
reportNfcError(message) {
|
||||
this.errorMessage = message;
|
||||
if (this.isV2) this.$root.playSound('error');
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1 +1 @@
|
||||
<lnurl-withdraw-checkout :model="model" :is-v2="true" />
|
||||
<lnurl-withdraw-checkout :model="model" :is-v2="true" :nfc-supported="nfcSupported" :nfc-scanning="nfcScanning" v-on="$listeners" />
|
||||
|
@ -1 +1 @@
|
||||
<lnurl-withdraw-checkout :model="srvModel" />
|
||||
<lnurl-withdraw-checkout :model="srvModel" :nfc-supported="nfcSupported" :nfc-scanning="nfcScanning" v-on="$listeners" />
|
||||
|
@ -1,2 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 84" role="img" alt="BTCPay Server" class="d-print-none"><path d="M5.206 83.433a4.86 4.86 0 01-4.859-4.861V5.431a4.86 4.86 0 119.719 0v73.141a4.861 4.861 0 01-4.86 4.861" fill="currentColor" class="logo-brand-light"/><path d="M5.209 83.433a4.862 4.862 0 01-2.086-9.253L32.43 60.274 2.323 38.093a4.861 4.861 0 015.766-7.826l36.647 26.999a4.864 4.864 0 01-.799 8.306L7.289 82.964a4.866 4.866 0 01-2.08.469" fill="currentColor" class="logo-brand-medium"/><path d="M5.211 54.684a4.86 4.86 0 01-2.887-8.774L32.43 23.73 3.123 9.821a4.861 4.861 0 014.166-8.784l36.648 17.394a4.86 4.86 0 01.799 8.305l-36.647 27a4.844 4.844 0 01-2.878.948" fill="currentColor" class="logo-brand-light"/><path d="M10.066 31.725v20.553L24.01 42.006z" fill="currentColor" class="logo-brand-dark"/><path d="M10.066 5.431A4.861 4.861 0 005.206.57 4.86 4.86 0 00.347 5.431v61.165h9.72V5.431h-.001z" fill="currentColor" class="logo-brand-light"/><path d="M74.355 41.412c3.114.884 4.84 3.704 4.84 7.238 0 5.513-3.368 8.082-7.955 8.082H60.761V27.271h9.259c4.504 0 7.997 2.146 7.997 7.743 0 2.821-1.179 5.43-3.662 6.398m-4.293-.716c3.324 0 6.018-1.179 6.018-5.724 0-4.586-2.776-5.808-6.145-5.808h-7.197v11.531h7.324v.001zm1.052 14.099c3.366 0 6.06-1.768 6.06-6.145 0-4.713-3.072-6.144-6.901-6.144h-7.534v12.288h8.375v.001zM98.893 27.271v1.81h-8.122v27.651h-1.979V29.081h-8.123v-1.81zM112.738 26.85c5.01 0 9.554 2.524 10.987 8.543h-1.895c-1.348-4.923-5.303-6.732-9.134-6.732-6.944 0-10.605 5.681-10.605 13.341 0 8.08 3.661 13.256 10.646 13.256 4.125 0 7.828-1.85 9.26-7.279h1.895c-1.264 6.271-6.229 9.174-11.154 9.174-7.87 0-12.583-5.808-12.583-15.15 0-8.966 4.969-15.153 12.583-15.153M138.709 27.271c5.091 0 8.795 3.326 8.795 9.764 0 6.06-3.704 9.722-8.795 9.722h-7.746v9.976h-1.935V27.271h9.681zm0 17.549c3.745 0 6.816-2.397 6.816-7.827 0-5.429-2.947-7.869-6.816-7.869h-7.746V44.82h7.746zM147.841 56.732v-.255l11.741-29.29h.885l11.615 29.29v.255h-2.062l-3.322-8.501H153.27l-3.324 8.501h-2.105zm12.164-26.052l-6.059 15.697h12.078l-6.019-15.697zM189.551 27.271h2.104v.293l-9.176 16.92v12.248h-2.02V44.484l-9.216-16.961v-.252h2.147l3.997 7.492 4.043 7.786h.04l4.081-7.786z" fill="currentColor" class="logo-brand-text"/></svg>
|
||||
<span class="d-none d-print-inline">BTCPay Server</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 84" role="img" alt="BTCPay Server"><path d="M5.206 83.433a4.86 4.86 0 01-4.859-4.861V5.431a4.86 4.86 0 119.719 0v73.141a4.861 4.861 0 01-4.86 4.861" fill="currentColor" class="logo-brand-light"/><path d="M5.209 83.433a4.862 4.862 0 01-2.086-9.253L32.43 60.274 2.323 38.093a4.861 4.861 0 015.766-7.826l36.647 26.999a4.864 4.864 0 01-.799 8.306L7.289 82.964a4.866 4.866 0 01-2.08.469" fill="currentColor" class="logo-brand-medium"/><path d="M5.211 54.684a4.86 4.86 0 01-2.887-8.774L32.43 23.73 3.123 9.821a4.861 4.861 0 014.166-8.784l36.648 17.394a4.86 4.86 0 01.799 8.305l-36.647 27a4.844 4.844 0 01-2.878.948" fill="currentColor" class="logo-brand-light"/><path d="M10.066 31.725v20.553L24.01 42.006z" fill="currentColor" class="logo-brand-dark"/><path d="M10.066 5.431A4.861 4.861 0 005.206.57 4.86 4.86 0 00.347 5.431v61.165h9.72V5.431h-.001z" fill="currentColor" class="logo-brand-light"/><path d="M74.355 41.412c3.114.884 4.84 3.704 4.84 7.238 0 5.513-3.368 8.082-7.955 8.082H60.761V27.271h9.259c4.504 0 7.997 2.146 7.997 7.743 0 2.821-1.179 5.43-3.662 6.398m-4.293-.716c3.324 0 6.018-1.179 6.018-5.724 0-4.586-2.776-5.808-6.145-5.808h-7.197v11.531h7.324v.001zm1.052 14.099c3.366 0 6.06-1.768 6.06-6.145 0-4.713-3.072-6.144-6.901-6.144h-7.534v12.288h8.375v.001zM98.893 27.271v1.81h-8.122v27.651h-1.979V29.081h-8.123v-1.81zM112.738 26.85c5.01 0 9.554 2.524 10.987 8.543h-1.895c-1.348-4.923-5.303-6.732-9.134-6.732-6.944 0-10.605 5.681-10.605 13.341 0 8.08 3.661 13.256 10.646 13.256 4.125 0 7.828-1.85 9.26-7.279h1.895c-1.264 6.271-6.229 9.174-11.154 9.174-7.87 0-12.583-5.808-12.583-15.15 0-8.966 4.969-15.153 12.583-15.153M138.709 27.271c5.091 0 8.795 3.326 8.795 9.764 0 6.06-3.704 9.722-8.795 9.722h-7.746v9.976h-1.935V27.271h9.681zm0 17.549c3.745 0 6.816-2.397 6.816-7.827 0-5.429-2.947-7.869-6.816-7.869h-7.746V44.82h7.746zM147.841 56.732v-.255l11.741-29.29h.885l11.615 29.29v.255h-2.062l-3.322-8.501H153.27l-3.324 8.501h-2.105zm12.164-26.052l-6.059 15.697h12.078l-6.019-15.697zM189.551 27.271h2.104v.293l-9.176 16.92v12.248h-2.02V44.484l-9.216-16.961v-.252h2.147l3.997 7.492 4.043 7.786h.04l4.081-7.786z" fill="currentColor" class="logo-brand-text"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@ -183,12 +183,15 @@
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="showPaymentUI">
|
||||
<div v-if="(nfc.successMessage || nfc.errorMessage)" class="alert mx-1" :class="{'alert-success': !!nfc.successMessage, 'alert-danger': !!nfc.errorMessage }" v-text="nfc.successMessage || nfc.errorMessage"></div>
|
||||
<component v-if="srvModel.uiSettings && srvModel.uiSettings.checkoutBodyVueComponentName && srvModel.activated"
|
||||
v-bind:srv-model="srvModel"
|
||||
v-bind:is="srvModel.uiSettings.checkoutBodyVueComponentName">
|
||||
</component>
|
||||
:is="srvModel.uiSettings.checkoutBodyVueComponentName" :srv-model="srvModel"
|
||||
:nfc-supported="nfc.supported" :nfc-scanning="nfc.scanning"
|
||||
v-on:start-nfc-scan="startNFCScan"
|
||||
v-on:handle-nfc-data="handleNFCData"
|
||||
v-on:handle-nfc-error="handleNFCError"
|
||||
v-on:handle-nfc-result="handleNFCResult" />
|
||||
</div>
|
||||
|
||||
<div class="bp-view" id="paid" v-bind:class="{ 'active': invoicePaid && !showEmailForm}">
|
||||
<div class="status-block">
|
||||
<div class="success-block">
|
||||
|
@ -206,7 +206,16 @@
|
||||
lineItemsExpanded: false,
|
||||
changingCurrencies: false,
|
||||
loading: true,
|
||||
isModal: initialSrvModel.isModal
|
||||
isModal: initialSrvModel.isModal,
|
||||
nfc: {
|
||||
supported: 'NDEFReader' in window,
|
||||
scanning: false,
|
||||
submitting: false,
|
||||
permissionGranted: false,
|
||||
readerAbortController: null,
|
||||
successMessage: null,
|
||||
errorMessage: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
expiringSoon: function(){
|
||||
@ -233,7 +242,7 @@
|
||||
: null;
|
||||
}
|
||||
},
|
||||
mounted: function(){
|
||||
mounted: async function(){
|
||||
this.startProgressTimer();
|
||||
this.listenIn();
|
||||
this.onDataCallback(this.srvModel);
|
||||
@ -244,6 +253,14 @@
|
||||
jQuery("invoice").fadeOut(0).fadeIn(300);
|
||||
window.closePaymentMethodDialog = this.closePaymentMethodDialog.bind(this);
|
||||
this.loading = false;
|
||||
if (this.nfc.supported) {
|
||||
this.setupNFC();
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.nfc.readerAbortController) {
|
||||
this.nfc.readerAbortController.abort()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onlyExpandLineItems: function() {
|
||||
@ -403,6 +420,71 @@
|
||||
} else {
|
||||
this.emailAddressInputInvalid = true;
|
||||
}
|
||||
},
|
||||
async setupNFC () {
|
||||
try {
|
||||
this.$set(this.nfc, 'permissionGranted', navigator.permissions && (await navigator.permissions.query({ name: 'nfc' })).state === 'granted');
|
||||
} catch (e) {}
|
||||
if (this.nfc.permissionGranted) {
|
||||
await this.startNFCScan();
|
||||
}
|
||||
},
|
||||
async startNFCScan () {
|
||||
if (this.nfc.scanning) return;
|
||||
this.$set(this.nfc, 'scanning', true);
|
||||
try {
|
||||
const inModal = window.self !== window.top;
|
||||
const ndef = inModal ? new NDEFReaderWrapper() : new NDEFReader();
|
||||
this.nfc.readerAbortController = new AbortController()
|
||||
this.nfc.readerAbortController.signal.onabort = () => {
|
||||
this.$set(this.nfc, 'scanning', false);
|
||||
};
|
||||
|
||||
await ndef.scan({ signal: this.nfc.readerAbortController.signal })
|
||||
ndef.onreadingerror = () => this.reportNfcError('Could not read NFC tag')
|
||||
ndef.onreading = async ({ message }) => {
|
||||
const record = message.records[0]
|
||||
const textDecoder = new TextDecoder('utf-8')
|
||||
const decoded = textDecoder.decode(record.data)
|
||||
this.$emit('read-nfc-data', decoded)
|
||||
}
|
||||
|
||||
if (inModal) {
|
||||
// receive messages from iframe
|
||||
window.addEventListener('message', async event => {
|
||||
// deny messages from other origins
|
||||
if (event.origin !== window.location.origin) return
|
||||
|
||||
const { action, data } = event.data
|
||||
switch (action) {
|
||||
case 'nfc:data':
|
||||
this.$emit('read-nfc-data', data)
|
||||
break;
|
||||
case 'nfc:error':
|
||||
this.handleNFCError('Could not read NFC tag')
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// we came here, so the user must have allowed NFC access
|
||||
this.$set(this.nfc, 'permissionGranted', true);
|
||||
} catch (error) {
|
||||
this.handleNFCError(`NFC scan failed: ${error}`);
|
||||
}
|
||||
},
|
||||
handleNFCData() { // child component reports it is handling the data
|
||||
this.$set(this.nfc, 'errorMessage', null);
|
||||
this.$set(this.nfc, 'successMessage', null);
|
||||
this.$set(this.nfc, 'submitting', true);
|
||||
},
|
||||
handleNFCResult(message) { // child component reports result for handling the data
|
||||
this.$set(this.nfc, 'submitting', false);
|
||||
this.$set(this.nfc, 'successMessage', message);
|
||||
},
|
||||
handleNFCError(message) { // internal or via child component reporting failure of handling the data
|
||||
this.$set(this.nfc, 'submitting', false);
|
||||
this.$set(this.nfc, 'errorMessage', message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -109,7 +109,12 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<component v-if="paymentMethodComponent" :is="paymentMethodComponent" :model="srvModel" />
|
||||
<component v-if="paymentMethodComponent" :is="paymentMethodComponent"
|
||||
:model="srvModel" :nfc-supported="nfc.supported" :nfc-scanning="nfc.scanning"
|
||||
v-on:start-nfc-scan="startNFCScan"
|
||||
v-on:handle-nfc-data="handleNFCData"
|
||||
v-on:handle-nfc-error="handleNFCError"
|
||||
v-on:handle-nfc-result="handleNFCResult" />
|
||||
</section>
|
||||
<section id="result" v-else>
|
||||
<div v-if="isProcessing" id="processing" key="processing">
|
||||
|
@ -31,6 +31,7 @@
|
||||
</script>
|
||||
}
|
||||
<style>
|
||||
#InvoiceReceipt { --wrap-max-width: 768px; }
|
||||
#InvoiceSummary { gap: var(--btcpay-space-l); }
|
||||
#PaymentDetails table tbody tr:first-child td { padding-top: 1rem; }
|
||||
#PaymentDetails table tbody:not(:last-child) tr:last-child > th,td { padding-bottom: 1rem; }
|
||||
@ -39,38 +40,34 @@
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-vh-100">
|
||||
<div class="public-page-wrap">
|
||||
<div id="InvoiceReceipt" class="public-page-wrap">
|
||||
<main class="flex-grow-1">
|
||||
<div class="container" style="max-width:720px;">
|
||||
<div class="d-flex flex-column justify-content-center gap-4">
|
||||
<partial name="_StoreHeader" model="(Model.StoreName, Model.LogoFileId)" />
|
||||
<partial name="_StatusMessage" model="@(new ViewDataDictionary(ViewData) { { "Margin", "mb-4" } })"/>
|
||||
|
||||
<div class="d-flex flex-column justify-content-center gap-4">
|
||||
<partial name="_StoreHeader" model="(Model.StoreName, Model.LogoFileId)" />
|
||||
<div id="InvoiceSummary" class="bg-tile p-3 p-sm-4 rounded d-flex flex-wrap align-items-center justify-content-center">
|
||||
@if (isProcessing)
|
||||
<div id="InvoiceSummary" class="tile d-flex flex-wrap align-items-center justify-content-center">
|
||||
@if (isProcessing)
|
||||
{
|
||||
<div class="lead text-center p-4 fw-semibold" id="invoice-processing">
|
||||
The invoice has detected a payment but is still waiting to be settled.
|
||||
</div>
|
||||
}
|
||||
else if (!isSettled)
|
||||
{
|
||||
<div class="lead text-center p-4 fw-semibold" id="invoice-unsettled">
|
||||
The invoice is not settled.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Model.ReceiptOptions.ShowQR is true)
|
||||
{
|
||||
<div class="lead text-center p-4 fw-semibold" id="invoice-processing">
|
||||
The invoice has detected a payment but is still waiting to be settled.
|
||||
</div>
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()"></vc:qr-code>
|
||||
}
|
||||
else if (!isSettled)
|
||||
{
|
||||
<div class="lead text-center p-4 fw-semibold" id="invoice-unsettled">
|
||||
The invoice is not settled.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Model.ReceiptOptions.ShowQR is true)
|
||||
{
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()"></vc:qr-code>
|
||||
}
|
||||
<div class="d-flex gap-4 mb-0 flex-fill">
|
||||
<dl class="d-flex flex-column gap-4 mb-0 flex-fill">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<a href="?print=true" class="btn btn-link p-0 d-print-none fw-semibold order-1" target="_blank">Print</a>
|
||||
<dd class="text-muted mb-0 fw-semibold">Amount Paid</dd>
|
||||
</div>
|
||||
<dd class="text-muted mb-0 fw-semibold">Amount Paid</dd>
|
||||
<dt class="fs-2 mb-0 text-nowrap fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</dt>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
@ -85,91 +82,92 @@
|
||||
</div>
|
||||
}
|
||||
</dl>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (isProcessing)
|
||||
{
|
||||
<small class="d-block text-muted text-center px-4">This page will refresh periodically until the invoice is settled.</small>
|
||||
}
|
||||
else if (isSettled)
|
||||
{
|
||||
if (Model.AdditionalData?.Any() is true)
|
||||
{
|
||||
<div id="AdditionalData" class="bg-tile p-3 p-sm-4 rounded">
|
||||
<h2 class="h4 mb-3">Additional Data</h2>
|
||||
<div class="table-responsive my-0">
|
||||
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if (Model.Payments?.Any() is true)
|
||||
{
|
||||
<div id="PaymentDetails" class="bg-tile p-3 p-sm-4 rounded">
|
||||
<h2 class="h4 mb-3">Payment Details</h2>
|
||||
<div class="table-responsive my-0 d-print-none">
|
||||
<table class="invoice table table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="fw-normal text-secondary date-col w-125px">Date</th>
|
||||
<th class="fw-normal text-secondary amount-col">Paid</th>
|
||||
<th class="fw-normal text-secondary amount-col w-225px">Payment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in Model.Payments)
|
||||
{
|
||||
<tr>
|
||||
<td class="date-col">@payment.ReceivedDate.ToBrowserDate()</td>
|
||||
<td class="amount-col">@payment.PaidFormatted</td>
|
||||
<td class="amount-col">@payment.AmountFormatted @payment.PaymentMethod</td>
|
||||
</tr>
|
||||
@if (!string.IsNullOrEmpty(payment.Destination))
|
||||
{
|
||||
<tr>
|
||||
<th class="fw-normal text-nowrap text-secondary">
|
||||
Destination
|
||||
</th>
|
||||
<td class="fw-normal" colspan="2">
|
||||
<vc:truncate-center text="@payment.Destination" classes="truncate-center-id" />
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(payment.PaymentProof))
|
||||
{
|
||||
<tr>
|
||||
<th class="fw-normal text-nowrap text-secondary">
|
||||
Payment Proof
|
||||
</th>
|
||||
<td class="fw-normal" colspan="2">
|
||||
<vc:truncate-center text="@payment.PaymentProof" link="@payment.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-none d-print-block">
|
||||
@foreach (var payment in Model.Payments)
|
||||
{
|
||||
<div class="mb-4">
|
||||
<strong>@payment.PaidFormatted</strong> = @payment.AmountFormatted @payment.PaymentMethod, Rate: @payment.RateFormatted
|
||||
@if (!string.IsNullOrEmpty(payment.PaymentProof))
|
||||
{
|
||||
<div>Proof: @payment.PaymentProof</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.OrderUrl))
|
||||
{
|
||||
<a href="@Model.OrderUrl" class="btn btn-secondary rounded-pill mx-auto mt-3" rel="noreferrer noopener" target="_blank">Return to @(string.IsNullOrEmpty(Model.StoreName) ? "store" : Model.StoreName)</a>
|
||||
<a href="?print=true" class="flex-grow-0 align-self-start btn btn-secondary d-print-none fs-4" target="_blank">Print</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (isProcessing)
|
||||
{
|
||||
<small class="d-block text-muted text-center px-4">This page will refresh periodically until the invoice is settled.</small>
|
||||
}
|
||||
else if (isSettled)
|
||||
{
|
||||
if (Model.AdditionalData?.Any() is true)
|
||||
{
|
||||
<div id="AdditionalData" class="tile">
|
||||
<h2 class="h4 mb-3">Additional Data</h2>
|
||||
<div class="table-responsive my-0">
|
||||
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if (Model.Payments?.Any() is true)
|
||||
{
|
||||
<div id="PaymentDetails" class="tile">
|
||||
<h2 class="h4 mb-3">Payment Details</h2>
|
||||
<div class="table-responsive my-0 d-print-none">
|
||||
<table class="invoice table table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="fw-normal text-secondary date-col w-125px">Date</th>
|
||||
<th class="fw-normal text-secondary amount-col">Paid</th>
|
||||
<th class="fw-normal text-secondary amount-col w-225px">Payment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@foreach (var payment in Model.Payments)
|
||||
{
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="date-col">@payment.ReceivedDate.ToBrowserDate()</td>
|
||||
<td class="amount-col">@payment.PaidFormatted</td>
|
||||
<td class="amount-col">@payment.AmountFormatted</td>
|
||||
</tr>
|
||||
@if (!string.IsNullOrEmpty(payment.Destination))
|
||||
{
|
||||
<tr>
|
||||
<th class="fw-normal text-nowrap text-secondary">
|
||||
Destination
|
||||
</th>
|
||||
<td class="fw-normal" colspan="2">
|
||||
<vc:truncate-center text="@payment.Destination" classes="truncate-center-id" />
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(payment.PaymentProof))
|
||||
{
|
||||
<tr>
|
||||
<th class="fw-normal text-nowrap text-secondary">
|
||||
Payment Proof
|
||||
</th>
|
||||
<td class="fw-normal" colspan="2">
|
||||
<vc:truncate-center text="@payment.PaymentProof" link="@payment.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-none d-print-block">
|
||||
@foreach (var payment in Model.Payments)
|
||||
{
|
||||
<div class="mb-4">
|
||||
<strong>@payment.PaidFormatted</strong> = @payment.Amount @payment.PaymentMethod, Rate: @payment.RateFormatted
|
||||
@if (!string.IsNullOrEmpty(payment.PaymentProof))
|
||||
{
|
||||
<div>Proof: @payment.PaymentProof</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.OrderUrl))
|
||||
{
|
||||
<a href="@Model.OrderUrl" class="btn btn-secondary rounded-pill mx-auto mt-3" rel="noreferrer noopener" target="_blank">Return to @(string.IsNullOrEmpty(Model.StoreName) ? "store" : Model.StoreName)</a>
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
<footer class="store-footer">
|
||||
|
@ -2,71 +2,213 @@
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Components.QRCode
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@{
|
||||
Layout = null;
|
||||
ViewData["Title"] = $"Receipt from {Model.StoreName}";
|
||||
var isProcessing = Model.Status == InvoiceStatus.Processing;
|
||||
var isFreeInvoice = (Model.Status == InvoiceStatus.New && Model.Amount == 0);
|
||||
var isSettled = Model.Status == InvoiceStatus.Settled;
|
||||
}
|
||||
|
||||
<link href="~/main/bootstrap/bootstrap.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/site.css" asp-append-version="true" rel="stylesheet" />
|
||||
|
||||
|
||||
<p class="text-center">@Model.StoreName</p>
|
||||
<p class="text-center">@Model.Timestamp.ToBrowserDate()</p>
|
||||
<p> </p>
|
||||
|
||||
@if (isProcessing)
|
||||
{
|
||||
<div class="lead text-center p-4 fw-semibold" id="invoice-processing">
|
||||
The invoice has detected a payment but is still waiting to be settled.
|
||||
</div>
|
||||
}
|
||||
else if (!isSettled)
|
||||
{
|
||||
<div class="lead text-center p-4 fw-semibold" id="invoice-unsettled">
|
||||
The invoice is not settled.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h3 class="text-center">
|
||||
<strong>@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</strong>
|
||||
</h3>
|
||||
|
||||
@if (Model.Payments?.Any() is true)
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="~/favicon.ico" type="image/x-icon">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>@ViewData["Title"]</title>
|
||||
@* CSS *@
|
||||
<link href="~/main/bootstrap/bootstrap.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/fonts/OpenSans.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/layout.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/site.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/themes/default.css" asp-append-version="true" rel="stylesheet" />
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
@if (isProcessing)
|
||||
{
|
||||
<p> </p>
|
||||
<p class="text-center"><strong>Payments</strong></p>
|
||||
@foreach (var payment in Model.Payments)
|
||||
{
|
||||
<p> </p>
|
||||
<p class="text-center">@payment.AmountFormatted <span class="text-nowrap">@payment.PaymentMethod</span></p>
|
||||
<p class="text-center">Rate: @payment.RateFormatted</p>
|
||||
<p class="text-center">= @payment.PaidFormatted</p>
|
||||
<script type="text/javascript">
|
||||
setTimeout(() => { window.location.reload(); }, 10000);
|
||||
</script>
|
||||
}
|
||||
else if (isFreeInvoice)
|
||||
{
|
||||
<script type="text/javascript">
|
||||
setTimeout(() => { window.location.reload(); }, 2000);
|
||||
</script>
|
||||
}
|
||||
<style>
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
if (Model.AdditionalData?.Any() is true)
|
||||
{
|
||||
<p> </p>
|
||||
<p class="text-center"><strong>Additional Data</strong></p>
|
||||
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.OrderId))
|
||||
{
|
||||
<p> </p>
|
||||
<p class="text-break">Order ID: @Model.OrderId</p>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.ReceiptOptions.ShowQR is true)
|
||||
{
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()"></vc:qr-code>
|
||||
}
|
||||
.qr-code {
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
<script>window.print();</script>
|
||||
/* change height as you like */
|
||||
@@media print {
|
||||
body {
|
||||
width: 58mm;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 1mm !important;
|
||||
}
|
||||
|
||||
.m-1 {
|
||||
margin: 1mm !important;
|
||||
}
|
||||
}
|
||||
/* this line is needed for fixing Chrome's bug */
|
||||
@@page {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="m-0 p-0 bg-white">
|
||||
<center>
|
||||
<partial name="_StoreHeader" model="(Model.StoreName, Model.LogoFileId)" />
|
||||
<div id="InvoiceSummary" style="max-width:600px">
|
||||
@if (isProcessing)
|
||||
{
|
||||
<div class="lead text-center fw-semibold" id="invoice-processing">
|
||||
The invoice has detected a payment but is still waiting to be settled.
|
||||
</div>
|
||||
}
|
||||
else if (!isSettled)
|
||||
{
|
||||
<div class="lead text-center fw-semibold" id="invoice-unsettled">
|
||||
The invoice is not settled.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="PaymentDetails">
|
||||
<div class="my-2 text-center small">
|
||||
@if (!string.IsNullOrEmpty(Model.OrderId))
|
||||
{
|
||||
<div>Order ID: @Model.OrderId</div>
|
||||
}
|
||||
@Model.Timestamp.ToBrowserDate()
|
||||
</div>
|
||||
<table class="table table-borderless table-sm small my-0">
|
||||
<tr>
|
||||
<td class="text-nowrap text-secondary">Total</td>
|
||||
<td class="text-end fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
@if (Model.AdditionalData?.Any() is true &&
|
||||
(Model.AdditionalData.ContainsKey("Cart") || Model.AdditionalData.ContainsKey("Discount") || Model.AdditionalData.ContainsKey("Tip")))
|
||||
{
|
||||
@if (Model.AdditionalData.ContainsKey("Cart"))
|
||||
{
|
||||
@foreach (var (key, value) in (Dictionary<string, object>)Model.AdditionalData["Cart"])
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">@key</td>
|
||||
<td class="text-end">@value</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
@if (Model.AdditionalData.ContainsKey("Subtotal"))
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">Subtotal</td>
|
||||
<td class="text-end">@Model.AdditionalData["Subtotal"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.AdditionalData.ContainsKey("Discount"))
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">Discount</td>
|
||||
<td class="text-end">@Model.AdditionalData["Discount"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.AdditionalData.ContainsKey("Tip"))
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">Tip</td>
|
||||
<td class="text-end">@Model.AdditionalData["Tip"]</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Payments?.Any() is true)
|
||||
{
|
||||
@for (var i = 0; i < Model.Payments.Count; i++)
|
||||
{
|
||||
var payment = Model.Payments[i];
|
||||
@if (Model.Payments.Count > 1)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="2" class="text-nowrap text-secondary">Payment @(i + 1)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-nowrap">Received</td>
|
||||
<td>@payment.ReceivedDate.ToBrowserDate()</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td class="text-nowrap text-secondary">@(Model.Payments.Count == 1 ? "Paid" : "")</td>
|
||||
<td class="text-end">@payment.AmountFormatted</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="text-end">@payment.PaidFormatted</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-nowrap text-secondary">Rate</td>
|
||||
<td class="text-end">@payment.RateFormatted</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
@if (!string.IsNullOrEmpty(payment.Destination))
|
||||
{
|
||||
<tr>
|
||||
<td class="text-break" colspan="2">
|
||||
<span class="text-secondary">Destination:</span> @payment.Destination
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(payment.PaymentProof))
|
||||
{
|
||||
<tr>
|
||||
<td class="text-break" colspan="2">
|
||||
<span class="text-secondary">Pay Proof:</span> @payment.PaymentProof
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
if (Model.ReceiptOptions.ShowQR is true)
|
||||
{
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()" size="128" />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="store-footer p-3">
|
||||
<a class="store-powered-by" style="color:#000;">Powered by <partial name="_StoreFooterLogo" /></a>
|
||||
</div>
|
||||
<hr class="w-100 my-0 bg-none"/>
|
||||
</center>
|
||||
</body>
|
||||
<script>
|
||||
window.print();
|
||||
</script>
|
||||
</html>
|
||||
|
@ -299,7 +299,7 @@
|
||||
<tr>
|
||||
<td class="text-break"><vc:truncate-center text="@payment.Id" link="@payment.Link" padding="7" classes="truncate-center-id" /></td>
|
||||
<td class="amount-col">@payment.PaidFormatted</td>
|
||||
<td class="text-end text-nowrap">@payment.AmountFormatted @payment.PaymentMethod</td>
|
||||
<td class="text-end text-nowrap">@payment.AmountFormatted</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +92,11 @@
|
||||
<div class="row">
|
||||
<div class="col col-12 col-lg-6 mb-4">
|
||||
<div class="bg-tile h-100 m-0 p-3 p-sm-5 rounded">
|
||||
@if (Model.LnurlEndpoint != null)
|
||||
{
|
||||
<a href="?print=true" class="float-end btn btn-secondary d-print-none fs-4" target="_blank">Print</a>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Title))
|
||||
{
|
||||
<h2 class="h4 mb-3">@Model.Title</h2>
|
||||
|
138
BTCPayServer/Views/UIPullPayment/ViewPullPaymentPrint.cshtml
Normal file
138
BTCPayServer/Views/UIPullPayment/ViewPullPaymentPrint.cshtml
Normal file
@ -0,0 +1,138 @@
|
||||
@model BTCPayServer.Models.ViewPullPaymentModel
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@using BTCPayServer.Components.QRCode
|
||||
@{
|
||||
Layout = null;
|
||||
ViewData["Title"] = Model.Title;
|
||||
ViewData.SetBlazorAllowed(false);
|
||||
|
||||
string lnurl = null;
|
||||
if (Model.LnurlEndpoint != null)
|
||||
{
|
||||
lnurl = LNURL.LNURL.EncodeBech32(new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = "BTC", pullPaymentId = Model.Id }, Context.Request.Scheme, Context.Request.Host.ToString())));
|
||||
}
|
||||
|
||||
var fullView = Url.Action("ViewPullPayment", "UIPullPayment", new { pullPaymentId = Model.Id }, Context.Request.Scheme, Context.Request.Host.ToString());
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="~/favicon.ico" type="image/x-icon">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>@ViewData["Title"]</title>
|
||||
@* CSS *@
|
||||
<link href="~/main/bootstrap/bootstrap.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/fonts/OpenSans.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/layout.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/site.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/themes/default.css" asp-append-version="true" rel="stylesheet" />
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<style>
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
/* change height as you like */
|
||||
@@media print {
|
||||
body {
|
||||
width: 58mm;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 1mm !important;
|
||||
}
|
||||
|
||||
.m-1 {
|
||||
margin: 1mm !important;
|
||||
}
|
||||
}
|
||||
/* this line is needed for fixing Chrome's bug */
|
||||
@@page {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="m-0 p-0 bg-white">
|
||||
<center>
|
||||
<partial name="_StoreHeader" model="(Model.Title, String.Empty)" />
|
||||
|
||||
<div id="InvoiceSummary" style="max-width:600px">
|
||||
@if (!@Model.IsPending || Model.SelectedPaymentMethod != "BTC")
|
||||
{
|
||||
<div class="lead text-center fw-semibold" id="invoice-processing">
|
||||
This Pull Payment is not Printable
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="PaymentDetails">
|
||||
|
||||
<hr class="w-100 my-0 bg-none" />
|
||||
|
||||
<table class="table table-borderless table-sm small my-0">
|
||||
<tr>
|
||||
<td class="text-nowrap text-secondary">Amount</td>
|
||||
<td class="text-end fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
@if (lnurl != null)
|
||||
{
|
||||
<div class="m-2 text-center">
|
||||
<vc:qr-code data="@lnurl" size="178"></vc:qr-code>
|
||||
<div>
|
||||
@if (!string.IsNullOrEmpty(Model.Description))
|
||||
{
|
||||
<span>@Model.Description</span><br />
|
||||
}
|
||||
Scan with LNURLw compatible Bitcoin Wallet to redeem
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<hr class="w-100 my-0 p-1 bg-none" />
|
||||
|
||||
<div class="my-2 text-center small">
|
||||
<a href="@fullView">
|
||||
<vc:qr-code data="@fullView" size="128"></vc:qr-code>
|
||||
</a>
|
||||
<div>Scan to open this link in browser for the full redemption option list</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<hr class="w-100 my-0 p-1 bg-none" />
|
||||
|
||||
<div class="store-footer p-2">
|
||||
<a class="store-powered-by" style="color:#000;">Powered by <partial name="_StoreFooterLogo" /></a>
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
<script>
|
||||
window.print();
|
||||
</script>
|
||||
</html>
|
@ -82,6 +82,18 @@ section dl > div dd {
|
||||
word-break: break-word;
|
||||
max-width: 62.5%;
|
||||
}
|
||||
.toast-container {
|
||||
transform: translateX(-50%);
|
||||
width: calc(var(--wrap-max-width) - 3rem);
|
||||
min-width: 256px;
|
||||
margin-top: var(--wrap-padding-vertical);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.toast-container .toast {
|
||||
--btcpay-toast-max-width: 100%;
|
||||
--btcpay-bg-opacity: .95;
|
||||
text-align: center;
|
||||
}
|
||||
.info {
|
||||
color: var(--btcpay-neutral-700);
|
||||
background-color: var(--btcpay-body-bg);
|
||||
|
@ -5,6 +5,22 @@ const STATUS_SETTLED = ['complete', 'confirmed'];
|
||||
const STATUS_INVALID = ['expired', 'invalid'];
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
class NDEFReaderWrapper {
|
||||
constructor() {
|
||||
this.onreading = null;
|
||||
this.onreadingerror = null;
|
||||
}
|
||||
|
||||
async scan(opts) {
|
||||
if (opts && opts.signal){
|
||||
opts.signal.addEventListener('abort', () => {
|
||||
window.parent.postMessage('nfc:abort', '*');
|
||||
});
|
||||
}
|
||||
window.parent.postMessage('nfc:startScan', '*');
|
||||
}
|
||||
}
|
||||
|
||||
function computeStartingLanguage() {
|
||||
const lang = urlParams.get('lang')
|
||||
if (lang && isLanguageAvailable(lang)) return lang;
|
||||
@ -45,7 +61,6 @@ Vue.use(VueI18next);
|
||||
const fallbackLanguage = 'en';
|
||||
const startingLanguage = computeStartingLanguage();
|
||||
const i18n = new VueI18next(i18next);
|
||||
const eventBus = new Vue();
|
||||
|
||||
const PaymentDetails = {
|
||||
template: '#payment-details',
|
||||
@ -82,6 +97,15 @@ function initApp() {
|
||||
paymentSound: null,
|
||||
nfcReadSound: null,
|
||||
errorSound: null,
|
||||
nfc: {
|
||||
supported: 'NDEFReader' in window,
|
||||
scanning: false,
|
||||
submitting: false,
|
||||
permissionGranted: false,
|
||||
readerAbortController: null,
|
||||
successMessage: null,
|
||||
errorMessage: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -192,7 +216,7 @@ function initApp() {
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
async mounted () {
|
||||
this.updateData(this.srvModel);
|
||||
this.updateTimer();
|
||||
if (this.isActive || this.isProcessing) {
|
||||
@ -206,9 +230,18 @@ function initApp() {
|
||||
this.prepareSound(this.srvModel.nfcReadSoundUrl).then(sound => this.nfcReadSound = sound);
|
||||
this.prepareSound(this.srvModel.errorSoundUrl).then(sound => this.errorSound = sound);
|
||||
}
|
||||
if (this.nfc.supported) {
|
||||
await this.setupNFC();
|
||||
}
|
||||
updateLanguageSelect();
|
||||
|
||||
window.parent.postMessage('loaded', '*');
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.nfc.readerAbortController) {
|
||||
this.nfc.readerAbortController.abort()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changePaymentMethod (id) { // payment method or plugin id
|
||||
if (this.pmId !== id) {
|
||||
@ -303,7 +336,6 @@ function initApp() {
|
||||
|
||||
// updating ui
|
||||
this.srvModel = data;
|
||||
eventBus.$emit('data-fetched', this.srvModel);
|
||||
},
|
||||
replaceNewlines (value) {
|
||||
return value ? value.replace(/\n/ig, '<br>') : '';
|
||||
@ -345,6 +377,73 @@ function initApp() {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
return { audioContext, audioBuffer, playing: false };
|
||||
},
|
||||
async setupNFC () {
|
||||
try {
|
||||
this.$set(this.nfc, 'permissionGranted', navigator.permissions && (await navigator.permissions.query({ name: 'nfc' })).state === 'granted');
|
||||
} catch (e) {}
|
||||
if (this.nfc.permissionGranted) {
|
||||
await this.startNFCScan();
|
||||
}
|
||||
},
|
||||
async startNFCScan () {
|
||||
if (this.nfc.scanning) return;
|
||||
this.$set(this.nfc, 'scanning', true);
|
||||
try {
|
||||
const inModal = window.self !== window.top;
|
||||
const ndef = inModal ? new NDEFReaderWrapper() : new NDEFReader();
|
||||
this.nfc.readerAbortController = new AbortController()
|
||||
this.nfc.readerAbortController.signal.onabort = () => {
|
||||
this.$set(this.nfc, 'scanning', false);
|
||||
};
|
||||
|
||||
await ndef.scan({ signal: this.nfc.readerAbortController.signal })
|
||||
ndef.onreadingerror = () => this.reportNfcError('Could not read NFC tag')
|
||||
ndef.onreading = async ({ message }) => {
|
||||
const record = message.records[0]
|
||||
const textDecoder = new TextDecoder('utf-8')
|
||||
const decoded = textDecoder.decode(record.data)
|
||||
this.$emit('read-nfc-data', decoded)
|
||||
}
|
||||
|
||||
if (inModal) {
|
||||
// receive messages from iframe
|
||||
window.addEventListener('message', async event => {
|
||||
// deny messages from other origins
|
||||
if (event.origin !== window.location.origin) return
|
||||
|
||||
const { action, data } = event.data
|
||||
switch (action) {
|
||||
case 'nfc:data':
|
||||
this.$emit('read-nfc-data', data)
|
||||
break;
|
||||
case 'nfc:error':
|
||||
this.handleNFCError('Could not read NFC tag')
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// we came here, so the user must have allowed NFC access
|
||||
this.$set(this.nfc, 'permissionGranted', true);
|
||||
} catch (error) {
|
||||
this.handleNFCError(`NFC scan failed: ${error}`);
|
||||
}
|
||||
},
|
||||
handleNFCData() { // child component reports it is handling the data
|
||||
this.playSound('nfcRead');
|
||||
this.$set(this.nfc, 'errorMessage', null);
|
||||
this.$set(this.nfc, 'successMessage', null);
|
||||
this.$set(this.nfc, 'submitting', true);
|
||||
},
|
||||
handleNFCResult(message) { // child component reports result for handling the data
|
||||
this.$set(this.nfc, 'submitting', false);
|
||||
this.$set(this.nfc, 'successMessage', message);
|
||||
},
|
||||
handleNFCError(message) { // internal or via child component reporting failure of handling the data
|
||||
this.playSound('error');
|
||||
this.$set(this.nfc, 'submitting', false);
|
||||
this.$set(this.nfc, 'errorMessage', message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,21 @@
|
||||
class NDEFReaderWrapper {
|
||||
constructor() {
|
||||
this.onreading = null;
|
||||
this.onreadingerror = null;
|
||||
}
|
||||
|
||||
async scan(opts) {
|
||||
if (opts && opts.signal){
|
||||
opts.signal.addEventListener('abort', () => {
|
||||
window.parent.postMessage('nfc:abort', '*');
|
||||
});
|
||||
}
|
||||
window.parent.postMessage('nfc:startScan', '*');
|
||||
}
|
||||
}
|
||||
|
||||
delegate('click', '.payment-method', e => {
|
||||
const el = e.target.closest('.payment-method')
|
||||
closePaymentMethodDialog(el.dataset.paymentMethod);
|
||||
return false;
|
||||
})
|
||||
})
|
||||
|
Reference in New Issue
Block a user