NFC improvements

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.
This commit is contained in:
Dennis Reimann
2023-03-27 18:28:53 +02:00
parent d3f5576570
commit 1055e61bb4
3 changed files with 89 additions and 59 deletions

View File

@@ -175,18 +175,18 @@ namespace BTCPayServer.Plugins.NFC
}
}
if (bolt11 is null)
if (string.IsNullOrEmpty(bolt11))
{
return BadRequest("Could not fetch BOLT11 invoice to pay to.");
}
var result = await info.SendRequest(bolt11, httpClient);
if (result.Status.Equals("ok", StringComparison.InvariantCultureIgnoreCase))
if (!string.IsNullOrEmpty(result.Status) && result.Status.Equals("ok", StringComparison.InvariantCultureIgnoreCase))
{
return Ok(result.Reason);
}
return BadRequest(result.Reason);
return BadRequest(result.Reason ?? "Unknown error");
}
}
}

View File

@@ -5,9 +5,11 @@
<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>
<button v-if="isV2" class="btn btn-secondary rounded-pill w-100" type="button"
:disabled="scanning || submitting" v-on:click="startScan" :id="btnId"
:class="{ 'loading': scanning || submitting, 'text-secondary': !supported }">{{btnText}}</button>
<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"
@@ -22,6 +24,7 @@
</template>
</template>
<script type="text/javascript">
// https://developer.chrome.com/articles/nfc/
Vue.component("lnurl-withdraw-checkout", {
template: "#lnurl-withdraw-template",
props: {
@@ -29,7 +32,7 @@ Vue.component("lnurl-withdraw-checkout", {
isV2: Boolean
},
computed: {
display: function () {
display () {
const {
onChainWithLnInvoiceFallback: isUnified,
paymentMethodId: activePaymentMethodId,
@@ -47,84 +50,113 @@ Vue.component("lnurl-withdraw-checkout", {
// Lightning with LNURL available
(activePaymentMethodId === 'BTC_LightningLike' && lnurlwAvailable))
},
btnId: function () {
btnId () {
return this.supported ? 'PayByNFC' : 'PayByLNURL'
},
btnText: function () {
btnText () {
if (this.supported) {
return this.isV2 ? this.$t('pay_by_nfc') : 'Pay by NFC'
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: function () {
data () {
return {
url: @Safe.Json(Context.Request.GetAbsoluteUri(Url.Action("SubmitLNURLWithdrawForInvoice", "NFC"))),
supported: ('NDEFReader' in window && window.self === window.top),
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: {
startScan: async function () {
try {
if (this.scanning || this.submitting) {
return;
}
async handleClick () {
if (this.supported) {
this.startScan()
} else {
if (this.model.isUnsetTopUp) {
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");
}
} else {
return;
}
}
const self = this;
self.submitting = false;
self.scanning = true;
if (!this.supported) {
const result = prompt("Enter LNURL-Withdraw");
if (result) {
await self.sendData.bind(self)(result);
this.handleUnsetTopUp()
if (!this.amount) {
return;
}
self.scanning = false;
}
ndef = new NDEFReader()
self.readerAbortController = new AbortController()
await ndef.scan({signal: self.readerAbortController.signal})
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.addEventListener('readingerror', () => {
self.scanning = false;
self.readerAbortController.abort()
})
ndef.onreadingerror = () => {
this.errorMessage = "Could not read NFC tag";
this.readerAbortController.abort()
}
ndef.addEventListener('reading', async ({message, serialNumber}) => {
//Decode NDEF data from tag
ndef.onreading = async ({ message, serialNumber }) => {
const record = message.records[0]
const textDecoder = new TextDecoder('utf-8')
const lnurl = textDecoder.decode(record.data)
//User feedback, show loader icon
self.scanning = false;
await self.sendData.bind(self)(lnurl);
})
} catch (e) {
self.scanning = false;
self.submitting = false;
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}`;
}
},
sendData: async function (lnurl) {
async sendData (lnurl) {
this.submitting = true;
this.successMessage = null;
this.errorMessage = null;
@@ -145,11 +177,7 @@ Vue.component("lnurl-withdraw-checkout", {
} catch (error) {
this.errorMessage = error;
}
this.scanning = false;
this.submitting = false;
if (this.readerAbortController) {
this.readerAbortController.abort()
}
}
}
});

View File

@@ -11,6 +11,8 @@
"pay_in_wallet": "Pay in wallet",
"pay_by_nfc": "Pay by NFC",
"pay_by_lnurl": "Pay by LNURL-Withdraw",
"scanning_nfc": "Scanning NFC …",
"submitting_nfc": "Submitting NFC …",
"invoice_id": "Invoice ID",
"order_id": "Order ID",
"total_price": "Total Price",