bitcoin-qr
Version:
A zero-dependency, zero-framework QR code web component for Bitcoin on-chain, Lightning, and unified BIP-21 payments.
793 lines (792 loc) • 27.5 kB
JavaScript
import { h } from "@stencil/core";
import QRCodeStyling from "qr-code-styling";
export class BitcoinQR {
constructor() {
this.unified = undefined;
this.bitcoin = undefined;
this.lightning = undefined;
this.parameters = undefined;
this.callback = undefined;
this.isPolling = undefined;
this.pollInterval = undefined;
this.imageEmbedded = undefined;
this.debug = undefined;
this.width = undefined;
this.height = undefined;
this.type = undefined;
this.margin = undefined;
this.image = undefined;
this.shape = undefined;
this.qrTypeNumber = undefined;
this.qrMode = undefined;
this.qrErrorCorrectionLevel = undefined;
this.imageHideBackgroundDots = undefined;
this.imageSize = undefined;
this.imageCrossOrigin = undefined;
this.imageMargin = undefined;
this.dotsType = undefined;
this.dotsColor = undefined;
this.dotsRotation = undefined;
this.cornersSquareType = undefined;
this.cornersSquareColor = undefined;
this.cornersDotType = undefined;
this.cornersDotColor = undefined;
this.backgroundRound = undefined;
this.backgroundColor = undefined;
this.qr = undefined;
}
// TODO: clear timers when polling is cancelled
poll() {
if (this.debug) {
console.debug('[bitcoin-qr]: Polling - ', this.isPolling, this.pollInterval, this.callback);
}
if (!this.callback) {
return;
}
if (!this.isPolling) {
return;
}
setTimeout(() => {
try {
this.callback();
this.poll();
}
catch (e) {
throw new Error(String(e));
}
}, this.pollInterval);
}
get uri() {
if (!(this.bitcoin || this.lightning || this.unified)) {
throw new Error('[bitcoin-qr]: Must pass at least one of the following props to bitcoin-qr: bitcoin, lightning, unified');
}
// TODO: unified bip21 validation
if (this.unified) {
return encodeURI(this.unified);
}
// We only use lightning as protocol if there is no on-chain bitcoin.
// Otherwise we use it as a parameter.
// See https://github.com/lightning/bolts/blob/master/10-payment-encoding.md#encoding-overview
const protocol = this.bitcoin ? 'bitcoin' : 'lightning';
const pathname = this.bitcoin ? this.bitcoin : this.lightning;
let _uri;
try {
_uri = `${protocol}:${pathname}`;
}
catch (e) {
throw new Error(`[bitcoin-qr]: Invalid URL format: "${protocol}:${pathname}"`);
}
if (!this.parameters && (!this.lightning || !this.bitcoin)) {
// Don't append parameters if there are none
return encodeURI(_uri);
}
try {
const isLightningOnly = this.lightning && !(this.bitcoin || this.unified);
const params = isLightningOnly ? this.parameters : `lightning=${this.lightning}&${this.parameters}`;
_uri = `${_uri}?${params}`;
}
catch (e) {
throw new Error(`[bitcoin-qr]: Invalid URLSearchParams format: "${this.parameters}"`);
}
// TODO: should there be an option to not encode the uri?
return encodeURI(_uri);
}
getDefinedProps() {
if (!this.uri) {
throw new Error('[bitcoin-qr]: Must pass at least one of the following props to bitcoin-qr: bitcoin, lightning, unified');
}
const optionsKeys = [
'unified',
'bitcoin',
'lightning',
'parameters',
'isPolling',
'pollInterval',
'width',
'height',
'type',
'margin',
'image',
'shape',
'qrTypeNumber',
'qrMode',
'qrErrorCorrectionLevel',
'imageEmbedded',
'imageHideBackgroundDots',
'imageSize',
'imageCrossOrigin',
'imageMargin',
'dotsType',
'dotsColor',
// TODO: gradient support
// 'dotsGradient',
'dotsType',
// 'dotsRotation',
'cornersSquareType',
'cornersSquareColor',
// 'cornersSquareGradient',
'cornersDotType',
'cornersDotColor',
// 'cornersDotGradient',
'backgroundRound',
'backgroundColor',
// 'backgroundGradient',
];
const options = {};
const nestedOptionKeyPrefixes = ['qr', 'image', 'dots', 'cornersSquare', 'cornersDot', 'background'];
optionsKeys.forEach(key => {
if (this[key] !== undefined) {
// Some options have nesting in qr-code-styling, so we need to build the object accordingly
// e.x.
// imageOptions {
// imageSize: 0.4,
// margin: 1
// }
// Image is a special case because it has a top-level key just called "image"
const prefix = nestedOptionKeyPrefixes.find(prefix => key.startsWith(prefix));
if (key !== 'image' && key !== 'imageEmbedded' && prefix) {
const optionKey = `${prefix}Options`;
if (optionKey) {
if (!options[optionKey]) {
options[optionKey] = {};
}
// imageSize is the one exception where the option has the key in both the top-level and nested object
if (key === 'imageSize') {
options[optionKey][key] = this[key];
return;
}
let nestedKey = key.replace(`${prefix}`, '');
nestedKey = nestedKey[0].toLowerCase() + nestedKey.slice(1);
const nestedOption = options[optionKey];
nestedOption[nestedKey] = this[key];
}
}
options[key] = this[key];
}
});
return Object.assign({ data: this.uri }, options);
}
getImageOverlay() {
var _a, _b;
const shadowContainer = this.bitcoinQR.shadowRoot.getElementById('bitcoin-qr-container');
// If image is not embedded, append an <img> element overlaying it and centered
if (!this.imageEmbedded && this.image) {
const img = document.createElement('img');
img.width = (_a = this.imageSize) !== null && _a !== void 0 ? _a : 50;
img.height = (_b = this.imageSize) !== null && _b !== void 0 ? _b : 50;
img.src = this.image;
img.style.position = 'absolute';
img.style.top = '50%';
img.style.left = '50%';
img.style.transform = 'translate(-50%, -50%)';
shadowContainer.appendChild(img);
}
}
componentWillLoad() {
if (this.debug) {
console.debug('[bitcoin-qr]: debug mode enabled');
}
if (!this.pollInterval) {
console.warn('[bitcoin-qr]: Attribute "poll-interval" not provided, defaulting to poll every 5 seconds');
this.pollInterval = 5000;
}
if (!this.width) {
console.warn('[bitcoin-qr]: Attribute "width" not provided, defaulting to 300px');
this.width = 300;
}
if (!this.height) {
this.height = 300;
}
// FIXME: is this not working properly?
if (!this.type) {
this.type = 'svg';
}
// If image is not embedded, don't pass to qr-code-styling
this.qr = new QRCodeStyling(Object.assign(Object.assign({}, this.getDefinedProps()), { image: this.imageEmbedded ? this.image : undefined }));
if (this.debug) {
console.debug('[bitcoin-qr]: Component will load with props', this.getDefinedProps());
}
}
componentDidLoad() {
const shadowContainer = this.bitcoinQR.shadowRoot.getElementById('bitcoin-qr-container');
shadowContainer.childElementCount > 0 ? this.qr.update(this.getDefinedProps()) : this.qr.append(shadowContainer);
shadowContainer.style.position = 'relative'; // For image overlay
shadowContainer.style.width = `${this.width}px`;
shadowContainer.style.height = `${this.height}px`;
shadowContainer.style.display = 'block';
this.getImageOverlay();
this.poll();
if (this.debug) {
console.debug('[bitcoin-qr]: Component aljsdfsk with props', this.getDefinedProps());
}
}
componentShouldUpdate(_new, _old, propName) {
// Define which props should not trigger an rerender
const nonRerenderProps = ['isPolling', 'pollInterval', 'callback', 'debug'];
if (nonRerenderProps.includes(propName)) {
return false;
}
else {
this.qr.update(Object.assign(Object.assign({}, this.getDefinedProps()), { image: this.imageEmbedded ? this.image : undefined }));
this.getImageOverlay();
if (this.debug) {
console.debug('[bitcoin-qr]: Component updated with props', this.getDefinedProps());
}
return true;
}
}
// TODO:
// i.e. optional copy on click instead of link/uri action
render() {
return h("a", { key: '20496b5016e98bbaa90edbb6664c18d47c3f80df', id: "bitcoin-qr-container", href: this.uri });
}
static get is() { return "bitcoin-qr"; }
static get encapsulation() { return "shadow"; }
static get properties() {
return {
"unified": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "unified",
"reflect": false
},
"bitcoin": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "bitcoin",
"reflect": false
},
"lightning": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "lightning",
"reflect": false
},
"parameters": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "parameters",
"reflect": false
},
"callback": {
"type": "unknown",
"mutable": false,
"complexType": {
"original": "() => void",
"resolved": "() => void",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
}
},
"isPolling": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "is-polling",
"reflect": false
},
"pollInterval": {
"type": "number",
"mutable": true,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "poll-interval",
"reflect": false
},
"imageEmbedded": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "image-embedded",
"reflect": false
},
"debug": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "debug",
"reflect": false
},
"width": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "width",
"reflect": false
},
"height": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "height",
"reflect": false
},
"type": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'canvas' | 'svg'",
"resolved": "\"canvas\" | \"svg\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "type",
"reflect": false
},
"margin": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "margin",
"reflect": false
},
"image": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "image",
"reflect": false
},
"shape": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'square' | 'circle'",
"resolved": "\"circle\" | \"square\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "shape",
"reflect": false
},
"qrTypeNumber": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "qr-type-number",
"reflect": false
},
"qrMode": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'Numeric' | 'Alphanumeric' | 'Byte' | 'Kanji'",
"resolved": "\"Alphanumeric\" | \"Byte\" | \"Kanji\" | \"Numeric\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "qr-mode",
"reflect": false
},
"qrErrorCorrectionLevel": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'L' | 'M' | 'Q' | 'H'",
"resolved": "\"H\" | \"L\" | \"M\" | \"Q\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "qr-error-correction-level",
"reflect": false
},
"imageHideBackgroundDots": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "image-hide-background-dots",
"reflect": false
},
"imageSize": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "image-size",
"reflect": false
},
"imageCrossOrigin": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "image-cross-origin",
"reflect": false
},
"imageMargin": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "image-margin",
"reflect": false
},
"dotsType": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'square' | 'dots' | 'rounded' | 'classy' | 'classy-rounded' | 'extra-rounded'",
"resolved": "\"classy\" | \"classy-rounded\" | \"dots\" | \"extra-rounded\" | \"rounded\" | \"square\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "dots-type",
"reflect": false
},
"dotsColor": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "dots-color",
"reflect": false
},
"dotsRotation": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "dots-rotation",
"reflect": false
},
"cornersSquareType": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'square' | 'extra-rounded' | 'dot'",
"resolved": "\"dot\" | \"extra-rounded\" | \"square\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "corners-square-type",
"reflect": false
},
"cornersSquareColor": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "corners-square-color",
"reflect": false
},
"cornersDotType": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'square' | 'dot'",
"resolved": "\"dot\" | \"square\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "corners-dot-type",
"reflect": false
},
"cornersDotColor": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "corners-dot-color",
"reflect": false
},
"backgroundRound": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "background-round",
"reflect": false
},
"backgroundColor": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "background-color",
"reflect": false
}
};
}
static get states() {
return {
"qr": {}
};
}
static get elementRef() { return "bitcoinQR"; }
static get watchers() {
return [{
"propName": "isPolling",
"methodName": "poll"
}, {
"propName": "pollInterval",
"methodName": "poll"
}, {
"propName": "callback",
"methodName": "poll"
}];
}
}