mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 22:46:49 +01:00
Two changes which fix #4807: - Once permissions are granted we start scanning immediately, no need to ask for permissions or have the user click the button again - We don't abort the scan, which gets rid of the cases in which the OS took over after the scan, because the user left the card on the device Also adds feedback for the NFC states scanning and submitting.
184 lines
7.2 KiB
Text
184 lines
7.2 KiB
Text
@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"
|
|
:disabled="scanning || submitting" v-on:click="handleClick" :id="btnId"
|
|
:class="{ 'text-secondary': !supported }">{{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="startScan" :id="btnId"
|
|
:class="{ 'loading': scanning || submitting, 'action-button': supported, 'btn btn-text w-100': !supported }">
|
|
<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>
|
|
<script type="text/javascript">
|
|
// https://developer.chrome.com/articles/nfc/
|
|
Vue.component("lnurl-withdraw-checkout", {
|
|
template: "#lnurl-withdraw-template",
|
|
props: {
|
|
model: Object,
|
|
isV2: Boolean
|
|
},
|
|
computed: {
|
|
display () {
|
|
const {
|
|
onChainWithLnInvoiceFallback: isUnified,
|
|
paymentMethodId: activePaymentMethodId,
|
|
availableCryptos: availablePaymentMethods,
|
|
invoiceBitcoinUrl: paymentUrl
|
|
} = this.model
|
|
const lnurlwAvailable =
|
|
// Either we have LN or LNURL available directly
|
|
!!availablePaymentMethods.find(pm => ['BTC_LNURLPAY', 'BTC_LightningLike'].includes(pm.paymentMethodId)) ||
|
|
// Or the BIP21 payment URL flags Lightning support
|
|
!!paymentUrl.match(/lightning=ln/i)
|
|
return activePaymentMethodId === 'BTC_LNURLPAY' || (
|
|
// Unified QR/BIP21 case
|
|
(activePaymentMethodId === 'BTC' && isUnified && lnurlwAvailable) ||
|
|
// Lightning with LNURL available
|
|
(activePaymentMethodId === 'BTC_LightningLike' && lnurlwAvailable))
|
|
},
|
|
btnId () {
|
|
return this.supported ? 'PayByNFC' : 'PayByLNURL'
|
|
},
|
|
btnText () {
|
|
if (this.supported) {
|
|
if (this.submitting) {
|
|
return this.isV2 ? this.$t('submitting_nfc') : 'Submitting NFC …'
|
|
} else if (this.scanning) {
|
|
return this.isV2 ? this.$t('scanning_nfc') : 'Scanning NFC …'
|
|
} else {
|
|
return this.isV2 ? this.$t('pay_by_nfc') : 'Pay by NFC'
|
|
}
|
|
} else {
|
|
return this.isV2 ? this.$t('pay_by_lnurl') : 'Pay by LNURL-Withdraw'
|
|
}
|
|
}
|
|
},
|
|
data () {
|
|
return {
|
|
url: @Safe.Json(Context.Request.GetAbsoluteUri(Url.Action("SubmitLNURLWithdrawForInvoice", "NFC"))),
|
|
supported: 'NDEFReader' in window && window.self === window.top,
|
|
scanning: false,
|
|
submitting: false,
|
|
permissionGranted: false,
|
|
readerAbortController: null,
|
|
amount: 0,
|
|
successMessage: null,
|
|
errorMessage: null
|
|
}
|
|
},
|
|
async mounted () {
|
|
try {
|
|
this.permissionGranted = navigator.permissions &&
|
|
(await navigator.permissions.query({ name: 'nfc' })).state === 'granted'
|
|
} catch (e) {}
|
|
if (this.permissionGranted) {
|
|
this.startScan()
|
|
}
|
|
},
|
|
methods: {
|
|
async handleClick () {
|
|
if (this.supported) {
|
|
this.startScan()
|
|
} else {
|
|
if (this.model.isUnsetTopUp) {
|
|
this.handleUnsetTopUp()
|
|
if (!this.amount) {
|
|
return;
|
|
}
|
|
}
|
|
const lnurl = prompt("Enter LNURL-Withdraw")
|
|
if (lnurl) {
|
|
await this.sendData(lnurl)
|
|
}
|
|
}
|
|
},
|
|
handleUnsetTopUp () {
|
|
const amountStr = prompt("How many sats do you want to pay?")
|
|
if (amountStr) {
|
|
try {
|
|
this.amount = parseInt(amountStr)
|
|
} catch {
|
|
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 ndef = new NDEFReader()
|
|
this.readerAbortController = new AbortController()
|
|
this.readerAbortController.signal.onabort = () => {
|
|
this.scanning = false;
|
|
};
|
|
|
|
await ndef.scan({ signal: this.readerAbortController.signal })
|
|
|
|
ndef.onreadingerror = () => {
|
|
this.errorMessage = "Could not read NFC tag";
|
|
this.readerAbortController.abort()
|
|
}
|
|
|
|
ndef.onreading = async ({ message, serialNumber }) => {
|
|
const record = message.records[0]
|
|
const textDecoder = new TextDecoder('utf-8')
|
|
const lnurl = textDecoder.decode(record.data)
|
|
await this.sendData(lnurl)
|
|
}
|
|
|
|
// we came here, so the user must have allowed NFC access
|
|
this.permissionGranted = true;
|
|
} catch (error) {
|
|
this.errorMessage = `NFC scan failed: ${error}`;
|
|
}
|
|
},
|
|
async sendData (lnurl) {
|
|
this.submitting = true;
|
|
this.successMessage = null;
|
|
this.errorMessage = null;
|
|
|
|
// Post LNURL-Withdraw data to server
|
|
const body = JSON.stringify({ lnurl, 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.errorMessage = result;
|
|
}
|
|
} catch (error) {
|
|
this.errorMessage = error;
|
|
}
|
|
this.submitting = false;
|
|
}
|
|
}
|
|
});
|
|
</script>
|