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
co-authored by Tiago Vasconcelos
parent 4da651b74a
commit 6449276003
12 changed files with 111 additions and 65 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -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
}
}
}
})
+10 -1
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')
})
}
+2 -37
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
},
+10
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
+1
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",
+1
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 %}
+1
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') %} {%
@@ -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>
+6 -25
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>
+1
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",