feat: move qrcode scanner into reuseable component (#3567)

Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
This commit is contained in:
dni ⚡ 2025-11-28 17:58:50 +01:00 committed by GitHub
parent 4da651b74a
commit 6449276003
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 111 additions and 65 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,55 @@
window.app.component('lnbits-qrcode-scanner', {
template: '#lnbits-qrcode-scanner',
mixins: [window.windowMixin],
watch: {
'g.showScanner'(newVal) {
if (newVal === true) {
if (this.g.hasCamera === false) {
Quasar.Notify.create({
message: 'No camera found on this device.',
type: 'negative'
})
this.g.showScanner = false
}
}
}
},
methods: {
detect(val) {
const rawValue = val[0].rawValue
console.log('Detected QR code value:', rawValue)
this.$emit('detect', rawValue)
this.g.showScanner = false
},
async onInitQR(promise) {
try {
await promise
} catch (error) {
const mapping = {
NotAllowedError: 'ERROR: you need to grant camera access permission',
NotFoundError: 'ERROR: no camera on this device',
NotSupportedError:
'ERROR: secure context required (HTTPS, localhost)',
NotReadableError: 'ERROR: is the camera already in use?',
OverconstrainedError: 'ERROR: installed cameras are not suitable',
StreamApiNotSupportedError:
'ERROR: Stream API is not supported in this browser',
InsecureContextError:
'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
}
const valid_error = Object.keys(mapping).filter(key => {
return error.name === key
})
const camera_error = valid_error
? mapping[valid_error]
: `ERROR: Camera error (${error.name})`
Quasar.Notify.create({
message: camera_error,
type: 'negative'
})
this.g.hasCamera = false
this.showScanner = false
}
}
}
})

View file

@ -11,6 +11,7 @@ const localStore = (key, defaultValue) => {
window.g = Vue.reactive({ window.g = Vue.reactive({
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'), isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
offline: !navigator.onLine, offline: !navigator.onLine,
hasCamera: false,
visibleDrawer: false, visibleDrawer: false,
extensions: WINDOW_SETTINGS.EXTENSIONS, extensions: WINDOW_SETTINGS.EXTENSIONS,
user: null, user: null,
@ -18,6 +19,7 @@ window.g = Vue.reactive({
fiatBalance: 0, fiatBalance: 0,
exchangeRate: 0, exchangeRate: 0,
fiatTracking: false, fiatTracking: false,
showScanner: false,
payments: [], payments: [],
walletEventListeners: [], walletEventListeners: [],
showNewWalletDialog: false, showNewWalletDialog: false,
@ -52,7 +54,8 @@ window.g = Vue.reactive({
), ),
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')), ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION, denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION,
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats' isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats',
scanDetectCallback: null
}) })
window.dateFormat = 'YYYY-MM-DD HH:mm' window.dateFormat = 'YYYY-MM-DD HH:mm'
@ -78,3 +81,9 @@ if (navigator.serviceWorker != null) {
console.log('Registered events at scope: ', registration.scope) console.log('Registered events at scope: ', registration.scope)
}) })
} }
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
navigator.mediaDevices.enumerateDevices().then(devices => {
window.g.hasCamera = devices.some(device => device.kind === 'videoinput')
})
}

View file

@ -97,12 +97,6 @@ window.PageWallet = {
msatoshiFormat(value) { msatoshiFormat(value) {
return LNbits.utils.formatSat(value / 1000) return LNbits.utils.formatSat(value / 1000)
}, },
closeCamera() {
this.parse.camera.show = false
},
showCamera() {
this.parse.camera.show = true
},
showReceiveDialog() { showReceiveDialog() {
this.receive.show = true this.receive.show = true
this.receive.status = 'pending' this.receive.status = 'pending'
@ -205,35 +199,6 @@ window.PageWallet = {
this.receive.status = 'pending' this.receive.status = 'pending'
}) })
}, },
async onInitQR(promise) {
try {
await promise
} catch (error) {
const mapping = {
NotAllowedError: 'ERROR: you need to grant camera access permission',
NotFoundError: 'ERROR: no camera on this device',
NotSupportedError:
'ERROR: secure context required (HTTPS, localhost)',
NotReadableError: 'ERROR: is the camera already in use?',
OverconstrainedError: 'ERROR: installed cameras are not suitable',
StreamApiNotSupportedError:
'ERROR: Stream API is not supported in this browser',
InsecureContextError:
'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
}
const valid_error = Object.keys(mapping).filter(key => {
return error.name === key
})
const camera_error = valid_error
? mapping[valid_error]
: `ERROR: Camera error (${error.name})`
this.parse.camera.show = false
Quasar.Notify.create({
message: camera_error,
type: 'negative'
})
}
},
lnurlScan() { lnurlScan() {
LNbits.api LNbits.api
.request('POST', '/api/v1/lnurlscan', this.g.wallet.adminkey, { .request('POST', '/api/v1/lnurlscan', this.g.wallet.adminkey, {
@ -281,8 +246,8 @@ window.PageWallet = {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
}) })
}, },
decodeQR(res) { decodeQR(val) {
this.parse.data.request = res[0].rawValue this.parse.data.request = val
this.decodeRequest() this.decodeRequest()
this.parse.camera.show = false this.parse.camera.show = false
}, },

View file

@ -1,5 +1,15 @@
window.windowMixin = { window.windowMixin = {
methods: { methods: {
handleScan(val) {
if (this.g.scanDetectCallback) {
this.g.scanDetectCallback(val)
this.g.scanDetectCallback = null
}
},
openScanDialog(scanDetectCallback) {
this.g.showScanner = true
this.g.scanDetectCallback = scanDetectCallback
},
openNewWalletDialog(walletType = 'lightning') { openNewWalletDialog(walletType = 'lightning') {
this.g.newWalletType = walletType this.g.newWalletType = walletType
this.g.showNewWalletDialog = true this.g.showNewWalletDialog = true

View file

@ -83,6 +83,7 @@
"js/components/lnbits-header-wallets.js", "js/components/lnbits-header-wallets.js",
"js/components/lnbits-drawer.js", "js/components/lnbits-drawer.js",
"js/components/lnbits-theme.js", "js/components/lnbits-theme.js",
"js/components/lnbits-qrcode-scanner.js",
"js/components/lnbits-manage-extension-list.js", "js/components/lnbits-manage-extension-list.js",
"js/components/lnbits-manage-wallet-list.js", "js/components/lnbits-manage-wallet-list.js",
"js/components/lnbits-language-dropdown.js", "js/components/lnbits-language-dropdown.js",

View file

@ -34,6 +34,7 @@
<div id="vue"> <div id="vue">
<q-layout view="hHh lpR lfr" v-cloak> <q-layout view="hHh lpR lfr" v-cloak>
<lnbits-disclaimer></lnbits-disclaimer> <lnbits-disclaimer></lnbits-disclaimer>
<lnbits-qrcode-scanner @detect="handleScan"></lnbits-qrcode-scanner>
<lnbits-theme></lnbits-theme> <lnbits-theme></lnbits-theme>
<lnbits-header></lnbits-header> <lnbits-header></lnbits-header>
{% block drawer %} {% block drawer %}

View file

@ -10,6 +10,7 @@ include('components/admin/extensions.vue') %} {%
include('components/admin/assets-config.vue') %} {% include('components/admin/assets-config.vue') %} {%
include('components/admin/notifications.vue') %} {% include('components/admin/notifications.vue') %} {%
include('components/admin/server.vue') %} {% include('components/admin/server.vue') %} {%
include('components/lnbits-qrcode-scanner.vue') %} {%
include('components/lnbits-disclaimer.vue') %} {% include('components/lnbits-disclaimer.vue') %} {%
include('components/lnbits-footer.vue') %} {% include('components/lnbits-footer.vue') %} {%
include('components/lnbits-header.vue') %} {% include('components/lnbits-header.vue') %} {%

View file

@ -0,0 +1,22 @@
<template id="lnbits-qrcode-scanner">
<q-dialog v-model="g.showScanner" position="top">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream
@detect="detect"
@camera-on="onInitQR"
class="rounded-borders"
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn
@click="g.showScanner = false"
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
</template>

View file

@ -156,14 +156,15 @@
icon="file_upload" icon="file_upload"
></q-btn> ></q-btn>
<q-btn <q-btn
v-if="g.hasCamera"
unelevated unelevated
color="secondary"
icon="qr_code_scanner" icon="qr_code_scanner"
color="secondary"
@click="openScanDialog(decodeQR)"
:disable=" :disable="
!this.g.wallet.canReceivePayments && !this.g.wallet.canReceivePayments &&
!this.g.wallet.canSendPayments !this.g.wallet.canSendPayments
" "
@click="showCamera"
> >
<q-tooltip <q-tooltip
><span v-text="$t('camera_tooltip')"></span ><span v-text="$t('camera_tooltip')"></span
@ -842,27 +843,6 @@
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="parse.camera.show" position="top">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream
@detect="decodeQR"
@camera-on="onInitQR"
class="rounded-borders"
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn
@click="closeCamera"
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
<div <div
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top" class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top"
> >
@ -877,12 +857,13 @@
<q-tab @click="showParseDialog" icon="file_upload" :label="$t('send')"> <q-tab @click="showParseDialog" icon="file_upload" :label="$t('send')">
</q-tab> </q-tab>
</q-tabs> </q-tabs>
<q-btn <q-btn
@click="openScanDialog(decodeQR)"
round round
size="35px"
unelevated unelevated
size="35px"
icon="qr_code_scanner" icon="qr_code_scanner"
@click="showCamera"
class="text-white bg-primary z-top vertical-bottom absolute-center absolute" class="text-white bg-primary z-top vertical-bottom absolute-center absolute"
> >
</q-btn> </q-btn>

View file

@ -135,6 +135,7 @@
"js/components/lnbits-header-wallets.js", "js/components/lnbits-header-wallets.js",
"js/components/lnbits-drawer.js", "js/components/lnbits-drawer.js",
"js/components/lnbits-theme.js", "js/components/lnbits-theme.js",
"js/components/lnbits-qrcode-scanner.js",
"js/components/lnbits-manage-extension-list.js", "js/components/lnbits-manage-extension-list.js",
"js/components/lnbits-manage-wallet-list.js", "js/components/lnbits-manage-wallet-list.js",
"js/components/lnbits-language-dropdown.js", "js/components/lnbits-language-dropdown.js",