btcpayserver/BTCPayServer/Views/Shared/CameraScanner.cshtml
d11n f4fa7c927c
Wallet setup redesign (#2164)
* Prepare existing layouts and views

* Add icon view component and sprite svg

* Add wallet setup basics

* Add import method view basics

* Use external sprite file instead of inline svg

* Refactor hardware wallet setup flow

* Manually enter an xpub

* Prepare other views

* Update views and models

* Finalize wallet setup flow

* Updat tests, part 1

* Update tests, part 2

* Vaul: Fix missing retry button

* Add better Scan QR subtext

Still tbd.

* Make wallet account an advanced setting

* Prevent empty xpub

* Use textarea for seed input

* Remove redundant error message for missing file upload

* Confirm store updates after generating a new wallet

* Update wording

* Modify existing wallets

* Fix proposed method name

* Suggest using ColdCard Electrum export option only

Advise the user to use the electrum export of the coldcard instead of saying either electrum or wasabi export file … the electurm one contains more info, e.g. the wasabi one doesn't include the account key path.

* More concise WalletSetupMethod setting

* Test fix

* Update wallet removal code

* Fix back navigation quirk in change wallet case

* Fix behaviour on wallet enable/disable

* Fix initial wallet setup

* Improve modify view and messages

* Test fixes

* Seed import fix

Uses the correct form url for confirming addresses

* Quickfixes from design meeting

* Add enable toggle switch on modify page

* Confirm wallet removal

* Update setup view

* Update import view

* Icon finetuning

* Improve import options page

* Refactor QR code scanner

Allow for usage with and without modal

* Update copy and instructions on import pages

* Split generate options: Hot wallet and watch-only

* Implement hot wallet options correctly

* Minor test changes

* Navbar improvements

* Fix tables

* Fix badge color

* Routing related updates

Thanks @kukks for the suggestions!

* Wording updates

Thanks @kukks for the suggestions!

* Extend address types table for xpub import

Thanks @kukks for the suggestions!

* Rename controller

* Unify precondition checks

* Improve removal warning for hot wallets

* Add tooltip on why seed import is not recommended

* Add tooltip icon

* Add Specter import info
2021-02-11 19:48:54 +09:00

184 lines
5.4 KiB
Text

<template id="camera-qr-scanner-wrap">
<div v-if="modalId" :id="modalId" class="modal fade" data-backdrop="static">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{title}}
<span v-if="workload.length > 0">Animated QR detected: {{workload.length}} / {{workload[0].total}} scanned</span>
</h5>
<button type="button" class="close" aria-label="Close" v-on:click="close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<slot/>
</div>
</div>
</div>
</div>
<div v-else>
<slot></slot>
<div v-if="workload.length > 0">Animated QR detected: {{workload.length}} / {{workload[0].total}} scanned</div>
</div>
</template>
<div id="camera-qr-scanner-modal-app" v-cloak class="only-for-js">
<scanner-wrap v-bind="$data" v-on:close="close">
<div v-if="isLoaded && requestInput" class="d-flex justify-content-center align-items-center" :class="{'border border-secondary': !isModal}">
<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="camera" :track="paint"/>
</qrcode-drop-zone>
<qrcode-capture v-if="noStreamApiSupport" v-on:decode="onDecode" :camera="camera"/>
</div>
<div v-else-if="qrData || errorMessage">
<div v-if="errorMessage" class="alert alert-danger" role="alert">
{{errorMessage}}
</div>
<div class="text-break text-monospace">
{{qrData}}
</div>
<div class="mt-4">
<button type="button" class="btn btn-primary mr-1" v-if="qrData" v-on:click="submitData">Submit</button>
<button type="button" class="btn btn-secondary mr-1" v-on:click="retry">Retry</button>
<button type="button" class="btn btn-outline-secondary" v-if="isModal" v-on:click="close">Cancel</button>
</div>
</div>
</scanner-wrap>
</div>
<script>
function initCameraScanningApp(title, onDataSubmit, modalId) {
const isModal = !!modalId;
Vue.component('scanner-wrap', {
props: ["modalId", "title", "workload"],
template: "#camera-qr-scanner-wrap",
methods: {
close() {
this.$emit('close');
}
}
});
new Vue({
el: '#camera-qr-scanner-modal-app',
data() {
return {
isModal,
isLoaded: !isModal,
title: title,
modalId: modalId,
noStreamApiSupport: false,
qrData: null,
errorMessage: null,
workload: [],
camera: "auto"
}
},
mounted() {
if (this.isModal) {
const $modal = $("#" + this.modalId);
$modal.on("shown.bs.modal", () => { this.isLoaded = true; });
$modal.on("hide.bs.modal", () => { this.isLoaded = false; });
} else {
this.isLoaded = true;
}
},
computed: {
requestInput() {
return this.camera === 'auto' && this.errorMessage === null;
}
},
methods: {
setQrData (qrData) {
this.qrData = qrData;
this.camera = qrData ? "off" : "auto";
},
retry() {
this.camera = "off";
this.$nextTick(this.reset);
},
reset() {
this.setQrData(null);
this.errorMessage = null;
this.workload = [];
},
close() {
if (this.modalId) {
$("#" + this.modalId).modal('hide');
}
this.reset();
},
onDecode(content) {
if (this.qrData) return;
if (!content.toLowerCase().startsWith("ur:")) {
this.setQrData(content);
this.workload = [];
} else {
const [index, total] = window.bcur.extractSingleWorkload(content);
if (this.workload.length > 0) {
const currentTotal = this.workload[0].total;
if (total !== currentTotal) {
this.workload = [];
}
}
if (!this.workload.find(i => i.index === index)) {
this.workload.push({
index,
total,
data: content,
});
if (this.workload.length === total) {
const decoded = window.bcur.decodeUR(this.workload.map(i => i.data));
this.setQrData(decoded);
}
}
}
},
submitData() {
if (onDataSubmit) {
onDataSubmit(this.qrData);
}
this.close();
},
logErrors(promise) {
promise.catch(console.error)
},
paint(location, ctx) {
ctx.fillStyle = '#137547';
[
location.topLeftFinderPattern,
location.topRightFinderPattern,
location.bottomLeftFinderPattern
].forEach(({ x, y }) => {
ctx.fillRect(x - 5, y - 5, 10, 10);
})
},
onInit(promise) {
promise.then(() => {
this.errorMessage = null;
}).catch(error => {
if (error.name === 'StreamApiNotSupportedError') {
this.noStreamApiSupport = true;
} else if (error.name === 'NotAllowedError') {
this.errorMessage = 'A permission to the camera is needed to scan the QR code. Please grant the browser access and then retry.'
} else if (error.name === 'NotFoundError') {
this.errorMessage = 'A camera was not detected on your device.'
} else if (error.name === 'NotSupportedError') {
this.errorMessage = 'This page is served in non-secure context (HTTPS, localhost or file://)'
} else if (error.name === 'NotReadableError') {
this.errorMessage = 'Couldn\'t access your camera. Is it already in use?'
} else if (error.name === 'OverconstrainedError') {
this.errorMessage = 'Constraints don\'t match any installed camera.'
} else {
this.errorMessage = 'UNKNOWN ERROR: ' + error.message
}
})
}
}
});
}
</script>