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({
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
offline: !navigator.onLine,
hasCamera: false,
visibleDrawer: false,
extensions: WINDOW_SETTINGS.EXTENSIONS,
user: null,
@ -18,6 +19,7 @@ window.g = Vue.reactive({
fiatBalance: 0,
exchangeRate: 0,
fiatTracking: false,
showScanner: false,
payments: [],
walletEventListeners: [],
showNewWalletDialog: false,
@ -52,7 +54,8 @@ window.g = Vue.reactive({
),
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
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'
@ -78,3 +81,9 @@ if (navigator.serviceWorker != null) {
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) {
return LNbits.utils.formatSat(value / 1000)
},
closeCamera() {
this.parse.camera.show = false
},
showCamera() {
this.parse.camera.show = true
},
showReceiveDialog() {
this.receive.show = true
this.receive.status = 'pending'
@ -205,35 +199,6 @@ window.PageWallet = {
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() {
LNbits.api
.request('POST', '/api/v1/lnurlscan', this.g.wallet.adminkey, {
@ -281,8 +246,8 @@ window.PageWallet = {
LNbits.utils.notifyApiError(err)
})
},
decodeQR(res) {
this.parse.data.request = res[0].rawValue
decodeQR(val) {
this.parse.data.request = val
this.decodeRequest()
this.parse.camera.show = false
},

View file

@ -1,5 +1,15 @@
window.windowMixin = {
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') {
this.g.newWalletType = walletType
this.g.showNewWalletDialog = true

View file

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

View file

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

View file

@ -10,6 +10,7 @@ include('components/admin/extensions.vue') %} {%
include('components/admin/assets-config.vue') %} {%
include('components/admin/notifications.vue') %} {%
include('components/admin/server.vue') %} {%
include('components/lnbits-qrcode-scanner.vue') %} {%
include('components/lnbits-disclaimer.vue') %} {%
include('components/lnbits-footer.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"
></q-btn>
<q-btn
v-if="g.hasCamera"
unelevated
color="secondary"
icon="qr_code_scanner"
color="secondary"
@click="openScanDialog(decodeQR)"
:disable="
!this.g.wallet.canReceivePayments &&
!this.g.wallet.canSendPayments
"
@click="showCamera"
>
<q-tooltip
><span v-text="$t('camera_tooltip')"></span
@ -842,27 +843,6 @@
</q-card>
</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
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>
</q-tabs>
<q-btn
@click="openScanDialog(decodeQR)"
round
size="35px"
unelevated
size="35px"
icon="qr_code_scanner"
@click="showCamera"
class="text-white bg-primary z-top vertical-bottom absolute-center absolute"
>
</q-btn>

View file

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