UNPKG

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
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" }]; } }