2021-10-21 15:02:25 +02:00
@inject BTCPayServer.Security.ContentSecurityPolicies csp
@{
csp.Add("worker-src", "blob:");
}
2021-02-11 11:48:54 +01:00
<template id="camera-qr-scanner-wrap">
2021-05-19 04:39:27 +02:00
<div v-if="modalId" :id="modalId" class="modal fade" data-bs-backdrop="static">
2021-02-11 11:48:54 +01:00
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
2021-05-19 04:39:27 +02:00
<h5 class="modal-title">
{{title}}
</h5>
2024-10-25 15:48:53 +02:00
<button type="button" class="btn-close" aria-label="@StringLocalizer["Close"]" v-on:click="close">
2021-05-19 04:39:27 +02:00
<vc:icon symbol="close"/>
</button>
2021-02-11 11:48:54 +01:00
</div>
<div class="modal-body">
<slot/>
</div>
</div>
</div>
</div>
<div v-else>
<slot></slot>
</div>
</template>
2020-10-21 14:03:11 +02:00
2021-02-11 11:48:54 +01:00
<div id="camera-qr-scanner-modal-app" v-cloak class="only-for-js">
2021-07-19 12:21:01 +02:00
<scanner-wrap v-bind="$data" v-on:close="close">
2022-08-31 12:27:06 +02:00
<div class="d-flex justify-content-center align-items-center" :class="{'border border-secondary': !isModal}">
2021-07-19 12:21:01 +02:00
<div class="spinner-border text-secondary position-absolute" role="status"></div>
<qrcode-drop-zone v-on:decode="onDecode" v-on:init="logErrors">
<qrcode-stream v-on:decode="onDecode" v-on:init="onInit" :camera="cameraOff? 'off': cameras[camera]" :device-id="cameras[camera]" :track="paint"/>
</qrcode-drop-zone>
<qrcode-capture v-if="noStreamApiSupport" v-on:decode="onDecode" :camera="cameraOff? 'off': cameras[camera]" :device-id="cameras[camera]"/>
</div>
2022-08-31 12:27:06 +02:00
<div v-if="isLoaded">
<div v-if="errorMessage" class="alert alert-danger mt-3" role="alert">
2021-07-19 12:21:01 +02:00
{{errorMessage}}
</div>
2022-08-31 12:27:06 +02:00
<div v-if="successMessage" class="alert alert-success mt-3" role="alert">
{{successMessage}}
</div>
<div v-else-if="qrData" class="alert alert-info font-monospace text-truncate mt-3">
2021-07-19 12:21:01 +02:00
{{qrData}}
</div>
2022-08-31 12:27:06 +02:00
<div v-else-if="decoder">
<div class="my-3">BC UR: {{decoder.expectedPartCount()}} parts, {{decoder.getProgress() * 100}}% completed</div>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" :style="{width: `${decoder.getProgress() * 100}%`}" id="progressbar"></div>
</div>
</div>
2024-03-21 10:30:23 +01:00
<div v-else-if="bbqrDecoder">
<div class="my-3">BBQR: {{bbqrDecoder.total}} parts, {{bbqrDecoder.progress * 100}}% completed</div>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" :style="{width: `${bbqrDecoder.progress * 100}%`}" id="progressbar"></div>
</div>
</div>
2022-08-31 12:27:06 +02:00
<div class="mt-3 text-center">
2021-07-19 12:21:01 +02:00
<button type="button" class="btn btn-primary me-1" v-if="qrData" v-on:click="submitData">Submit</button>
2022-08-31 12:27:06 +02:00
<button type="button" class="btn btn-secondary me-1" v-if="qrData" v-on:click="retry">Retry</button>
<button type="button" class="btn btn-secondary" v-if="requestInput && cameras.length > 2" v-on:click="nextCamera">Switch camera</button>
2021-07-19 12:21:01 +02:00
</div>
</div>
</scanner-wrap>
2020-10-21 14:03:11 +02:00
</div>
<script>
2022-01-18 15:18:33 +01:00
function initCameraScanningApp(title, onDataSubmit, modalId, submitOnScan = false) {
2021-02-11 11:48:54 +01:00
const isModal = !!modalId;
Vue.component('scanner-wrap', {
2022-08-31 12:27:06 +02:00
props: ["modalId", "title", "decoder"],
2021-02-11 11:48:54 +01:00
template: "#camera-qr-scanner-wrap",
methods: {
close() {
this.$emit('close');
}
}
});
2020-10-21 14:03:11 +02:00
2021-07-19 12:21:01 +02:00
const app = new Vue({
2020-10-21 14:03:11 +02:00
el: '#camera-qr-scanner-modal-app',
2021-02-11 11:48:54 +01:00
data() {
return {
isModal,
isLoaded: !isModal,
title: title,
modalId: modalId,
noStreamApiSupport: false,
qrData: null,
2022-08-31 12:27:06 +02:00
decoder: null,
2024-03-21 10:30:23 +01:00
bbqrDecoder: null,
2021-02-11 11:48:54 +01:00
errorMessage: null,
2022-08-31 12:27:06 +02:00
successMessage: null,
2021-07-19 12:21:01 +02:00
camera: 0,
2021-07-23 21:21:16 +02:00
cameraOff: true,
2021-07-19 12:21:01 +02:00
cameras: ["auto"],
2022-01-18 15:18:33 +01:00
submitOnScan
2021-02-11 11:48:54 +01:00
}
2020-10-21 14:03:11 +02:00
},
2021-02-11 11:48:54 +01:00
mounted() {
if (this.isModal) {
2021-05-19 04:39:27 +02:00
const modal = document.getElementById(this.modalId);
2021-07-23 21:21:16 +02:00
modal.addEventListener('shown.bs.modal', () => { this.isLoaded = true; this.cameraOff = false; });
modal.addEventListener('hide.bs.modal', () => { this.isLoaded = false; this.cameraOff = true; });
2021-02-11 11:48:54 +01:00
} else {
this.isLoaded = true;
2021-07-23 21:21:16 +02:00
this.cameraOff = false;
2021-02-11 11:48:54 +01:00
}
2020-10-21 14:03:11 +02:00
},
2021-02-11 11:48:54 +01:00
computed: {
requestInput() {
2022-08-31 12:27:06 +02:00
return !this.cameraOff && !this.errorMessage && !this.successMessage && !this.qrData;
2020-10-21 14:03:11 +02:00
}
},
2021-02-11 11:48:54 +01:00
methods: {
2022-08-31 12:27:06 +02:00
nextCamera() {
2021-12-24 09:27:00 +01:00
if (this.camera === 0){
this.camera++;
2022-08-31 12:27:06 +02:00
} else if (this.camera === this.cameras.length - 1) {
2021-12-24 09:27:00 +01:00
this.camera = 0;
} else {
2021-07-19 12:21:01 +02:00
this.camera++;
}
},
2022-08-31 12:27:06 +02:00
setQrData(qrData) {
2021-02-11 11:48:54 +01:00
this.qrData = qrData;
2021-07-19 12:21:01 +02:00
this.cameraOff = !!qrData;
2022-08-31 12:27:06 +02:00
2022-01-18 15:18:33 +01:00
if (this.qrData && this.submitOnScan) {
this.submitData();
}
2021-02-11 11:48:54 +01:00
},
retry() {
2021-07-19 12:21:01 +02:00
this.cameraOff = true;
2021-02-11 11:48:54 +01:00
this.$nextTick(this.reset);
2020-10-21 14:03:11 +02:00
},
2021-02-11 11:48:54 +01:00
reset() {
this.setQrData(null);
2022-08-31 12:27:06 +02:00
this.successMessage = null;
2021-02-11 11:48:54 +01:00
this.errorMessage = null;
2021-11-25 10:25:22 +01:00
this.decoder = null;
2024-03-21 10:30:23 +01:00
this.bbqrDecoder = null;
2020-10-21 14:03:11 +02:00
},
2021-02-11 11:48:54 +01:00
close() {
if (this.modalId) {
2021-05-19 04:39:27 +02:00
const modal = bootstrap.Modal.getInstance(document.getElementById(this.modalId));
modal.hide();
2020-10-21 14:03:11 +02:00
}
2021-02-11 11:48:54 +01:00
this.reset();
},
onDecode(content) {
if (this.qrData) return;
2022-08-31 12:27:06 +02:00
const isUR = content.toLowerCase().startsWith("ur:");
2024-03-21 10:30:23 +01:00
const isBBQr = content.startsWith("B$");
console.debug(content);
2022-08-31 12:27:06 +02:00
try {
2024-03-21 10:30:23 +01:00
if (isBBQr){
this.decoder = null;
const total = parseInt(content.substr(4, 2), 36);
const current = parseInt(content.substr(6, 2), 36);
const format = content.substr(2,1);
const type = content.substr(3, 1);
if (!this.bbqrDecoder ||
this.bbqrDecoder.total !== total ||
this.bbqrDecoder.format !== format ||
this.bbqrDecoder.type !== type) {
this.bbqrDecoder = {
total,
format,
type,
data: new Array(total),
progress: 1/total
};
}
this.bbqrDecoder.data[current] = content;
const progress = this.bbqrDecoder.data.filter(value => value !== undefined).length / total;
this.bbqrDecoder.progress = progress;
if (progress >= 1) {
try {
const joinResult = BBQr.joinQRs(this.bbqrDecoder.data);
function buf2hex(buffer) { // buffer is an ArrayBuffer
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
const result = buf2hex(joinResult.raw);
this.setQrData(result);
this.successMessage = `BBQr ${type} decoded`;
}catch (error){
this.errorMessage = error.message;
}
}
} else if (!isUR) {
2022-08-31 12:27:06 +02:00
this.setQrData(content);
} else {
2024-03-21 10:30:23 +01:00
this.bbqrDecoder = null;
2022-08-31 12:27:06 +02:00
this.decoder = this.decoder || new window.URlib.URRegistryDecoder();
if (this.decoder.receivePart(content)) {
if (this.decoder.isComplete()) {
if (this.decoder.isSuccess()) {
const ur = this.decoder.resultUR();
this.setQrData(ur);
this.successMessage = `UR ${ur.type} decoded`;
} else if (this.decoder.isError()) {
this.errorMessage = this.decoder.resultError();
2021-11-25 10:25:22 +01:00
}
}
}
2022-08-31 12:27:06 +02:00
}
} catch (error) {
console.error(error);
this.errorMessage = error.message;
}
2020-10-21 14:03:11 +02:00
},
2021-02-11 11:48:54 +01:00
submitData() {
2022-08-31 12:27:06 +02:00
if (onDataSubmit) {
onDataSubmit(this.qrData);
}
this.close();
},
2021-02-11 11:48:54 +01:00
logErrors(promise) {
2020-10-21 14:03:11 +02:00
promise.catch(console.error)
},
2021-10-21 15:22:51 +02:00
paint(detectedCodes, ctx) {
for (const detectedCode of detectedCodes) {
const [ firstPoint, ...otherPoints ] = detectedCode.cornerPoints
ctx.strokeStyle = "#51b13e";
ctx.beginPath();
ctx.moveTo(firstPoint.x, firstPoint.y);
for (const { x, y } of otherPoints) {
ctx.lineTo(x, y);
}
ctx.lineTo(firstPoint.x, firstPoint.y);
ctx.closePath();
ctx.stroke();
}
2020-10-21 14:03:11 +02:00
},
2021-02-11 11:48:54 +01:00
onInit(promise) {
promise.then(() => {
2022-08-31 12:27:06 +02:00
if (app.cameras.length === 1) {
navigator.mediaDevices.enumerateDevices().then(devices => {
2021-07-19 12:21:01 +02:00
for (const device of devices) {
2022-08-31 12:27:06 +02:00
if (device.kind === "videoinput"){
2021-07-19 12:21:01 +02:00
app.cameras.push( device.deviceId)
}
}
});
}
2021-02-11 11:48:54 +01:00
}).catch(error => {
if (error.name === 'StreamApiNotSupportedError') {
this.noStreamApiSupport = true;
} else if (error.name === 'NotAllowedError') {
2024-11-12 03:16:56 +01:00
this.errorMessage = @Safe.Json(StringLocalizer["A permission to the camera is needed to scan the QR code. Please grant the browser access and then retry."])
2021-02-11 11:48:54 +01:00
} else if (error.name === 'NotFoundError') {
2024-11-12 03:16:56 +01:00
this.errorMessage = @Safe.Json(StringLocalizer["A camera was not detected on your device."])
2021-02-11 11:48:54 +01:00
} else if (error.name === 'NotSupportedError') {
2024-11-12 03:16:56 +01:00
this.errorMessage = @Safe.Json(StringLocalizer["This page is served in non-secure context (HTTPS, localhost or file://)"])
2021-02-11 11:48:54 +01:00
} else if (error.name === 'NotReadableError') {
2024-11-12 03:16:56 +01:00
this.errorMessage = @Safe.Json(StringLocalizer["Could not access your camera. Is it already in use?"])
2021-02-11 11:48:54 +01:00
} else if (error.name === 'OverconstrainedError') {
2024-11-12 03:16:56 +01:00
this.errorMessage = @Safe.Json(StringLocalizer["Constraints do not match any installed camera."])
2021-02-11 11:48:54 +01:00
} else {
this.errorMessage = 'UNKNOWN ERROR: ' + error.message
}
})
2020-10-21 14:03:11 +02:00
}
}
});
}
</script>