Commit 6464e443 authored by Kenny Tran's avatar Kenny Tran Committed by Chi Kei Chan

Create QrCode component and use for recipient (#78)

parent 4cb00d95
This diff is collapsed.
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 1.5H1.5V7.5H7.5V1.5ZM1.5 0H0V1.5V7.5V9H1.5H7.5H9V7.5V1.5V0H7.5H1.5ZM4.5 3H3V4.5V6H4.5H6V4.5V3H4.5ZM1.5 19.5V13.5H7.5V19.5H1.5ZM0 12H1.5H7.5H9V13.5V19.5V21H7.5H1.5H0V19.5V13.5V12ZM4.5 15H3V16.5V18H4.5H6V16.5V15H4.5ZM13.5 1.5H19.5V7.5H13.5V1.5ZM12 0H13.5H19.5H21V1.5V7.5V9H19.5H13.5H12V7.5V1.5V0ZM16.5 3H15V4.5V6H16.5H18V4.5V3H16.5ZM16.5 12H12V21H13.5V16.5H15V18H21V12H19.5V13.5H16.5V12ZM18 19.5H16.5V21H18V19.5ZM19.5 19.5H21V21H19.5V19.5Z" fill="black"/>
</svg>
...@@ -15,4 +15,27 @@ ...@@ -15,4 +15,27 @@
color: $chalice-gray; color: $chalice-gray;
} }
} }
&__recipient-row {
display: flex;
justify-content: center;
align-items: center;
}
&__input-container {
flex: 1;
}
&__qr-container {
padding: 10px;
background: $concrete-gray;
border: 1px solid $mercury-gray;
border-radius: 10px;
margin-right: 16px;
height: 21px;
& > img {
height: 21px;
}
}
} }
...@@ -2,6 +2,7 @@ import React, { Component } from 'react'; ...@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react'; import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import QrCode from '../QrCode';
import './address-input-panel.scss'; import './address-input-panel.scss';
class AddressInputPanel extends Component { class AddressInputPanel extends Component {
...@@ -29,20 +30,25 @@ class AddressInputPanel extends Component { ...@@ -29,20 +30,25 @@ class AddressInputPanel extends Component {
return ( return (
<div className="currency-input-panel"> <div className="currency-input-panel">
<div className="currency-input-panel__container"> <div className="currency-input-panel__container address-input-panel__recipient-row">
<div className="currency-input-panel__label-row"> <div className="address-input-panel__input-container">
<div className="currency-input-panel__label-container"> <div className="currency-input-panel__label-row">
<span className="currency-input-panel__label">Recipient Address</span> <div className="currency-input-panel__label-container">
<span className="currency-input-panel__label">Recipient Address</span>
</div>
</div>
<div className="currency-input-panel__input-row">
<input
type="text"
className="address-input-panel__input"
placeholder="0x1234..."
onChange={e => onChange(e.target.value)}
value={value}
/>
</div> </div>
</div> </div>
<div className="currency-input-panel__input-row"> <div className="address-input-panel__qr-container">
<input <QrCode onValueReceived={value => onChange(value)} />
type="text"
className="address-input-panel__input"
placeholder="0x1234..."
onChange={e => onChange(e.target.value)}
value={value}
/>
</div> </div>
</div> </div>
</div> </div>
......
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { CSSTransitionGroup } from "react-transition-group";
import Modal from '../Modal';
import QrCodeSVG from '../../assets/images/qr-code.svg';
import QrScanner from '../../libraries/qr-scanner';
import './qr-code.scss';
class QrCode extends Component {
static propTypes = {
onValueReceived: PropTypes.func,
};
static defaultProps = {
onValueReceived() {},
};
state = {
videoOpen: false,
stream: null,
};
constructor(props) {
super(props);
}
componentDidUpdate() {
const { videoOpen, stream } = this.state;
if (videoOpen && !stream && this.videoRef) {
this.startStreamingVideo(this.videoRef)
} else if (!videoOpen && stream) {
this.setState({stream: null});
}
}
startStreamingVideo(videoRef) {
if (navigator && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({video: { facingMode: 'user'}, audio: false})
.then((stream) => {
videoRef.srcObject = stream;
new QrScanner(videoRef, (val) => {
this.closeVideo();
this.props.onValueReceived(val);
})
this.setState({
stream: stream.getTracks()[0]
});
})
.catch((error) => {
this.closeVideo();
console.error(error);
});
}
}
openVideo = () => {
this.setState({videoOpen: true});
}
closeVideo = () => {
if (this.state.stream) {
this.state.stream.stop();
}
this.setState({videoOpen: false, stream: null});
this.videoRef = null;
};
setVideoRef = (element) => {
this.videoRef = element;
}
renderQrReader() {
if (this.state.videoOpen) {
return (
<Modal key="modal" onClose={this.closeVideo}>
<CSSTransitionGroup
transitionName="qr-modal"
transitionAppear={true}
transitionLeave={true}
transitionAppearTimeout={200}
transitionLeaveTimeout={200}
transitionEnterTimeout={200}
>
<div className="qr-code__modal">
<video
playsInline
muted
autoPlay
ref={this.setVideoRef}
className="qr-code__video"
>
</video>
</div>
</CSSTransitionGroup>
</Modal>
);
}
return null;
}
render() {
const {
onValueReceived,
} = this.props;
return [
<img key="icon" src={QrCodeSVG} onClick={() => {
this.state.videoOpen ? this.closeVideo() : this.openVideo();
}} />,
this.renderQrReader()
]
}
}
export default QrCode;
.qr-code {
&__video {
height: 100%;
width: 100%;
}
&__modal {
z-index: 2000;
position: absolute;
top: 100px;
bottom: 100px;
right: 100px;
left: 100px;
}
}
/*
@asset(/libraries/qr-scanner/qr-scanner-worker.min.js) */
'use strict';export default class QrScanner{constructor(video,onDecode,canvasSize=QrScanner.DEFAULT_CANVAS_SIZE){this.$video=video;this.$canvas=document.createElement("canvas");this._onDecode=onDecode;this._active=false;this.$canvas.width=canvasSize;this.$canvas.height=canvasSize;this._sourceRect={x:0,y:0,width:canvasSize,height:canvasSize};this.$video.addEventListener("canplay",()=>this._updateSourceRect());this.$video.addEventListener("play",()=>{this._updateSourceRect();this._scanFrame()},false);
this._qrWorker=new Worker(QrScanner.WORKER_PATH)}_updateSourceRect(){const smallestDimension=Math.min(this.$video.videoWidth,this.$video.videoHeight);const sourceRectSize=Math.round(2/3*smallestDimension);this._sourceRect.width=this._sourceRect.height=sourceRectSize;this._sourceRect.x=(this.$video.videoWidth-sourceRectSize)/2;this._sourceRect.y=(this.$video.videoHeight-sourceRectSize)/2}_scanFrame(){if(this.$video.paused||this.$video.ended)return false;requestAnimationFrame(()=>{QrScanner.scanImage(this.$video,
this._sourceRect,this._qrWorker,this.$canvas,true).then(this._onDecode,(error)=>{if(error!=="QR code not found.")console.error(error)}).then(()=>this._scanFrame())})}_getCameraStream(facingMode,exact=false){const constraintsToTry=[{width:{min:1024}},{width:{min:768}},{}];if(facingMode){if(exact)facingMode={exact:facingMode};constraintsToTry.forEach((constraint)=>constraint.facingMode=facingMode)}return this._getMatchingCameraStream(constraintsToTry)}_getMatchingCameraStream(constraintsToTry){if(constraintsToTry.length===
0)return Promise.reject("Camera not found.");return navigator.mediaDevices.getUserMedia({video:constraintsToTry.shift()}).catch(()=>this._getMatchingCameraStream(constraintsToTry))}start(){if(this._active)return Promise.resolve();this._active=true;clearTimeout(this._offTimeout);let facingMode="environment";return this._getCameraStream("environment",true).catch(()=>{facingMode="user";return this._getCameraStream()}).then((stream)=>{this.$video.srcObject=stream;this._setVideoMirror(facingMode)}).catch((e)=>
{this._active=false;throw e;})}stop(){if(!this._active)return;this._active=false;this.$video.pause();this._offTimeout=setTimeout(()=>{this.$video.srcObject.getTracks()[0].stop();this.$video.srcObject=null},3E3)}_setVideoMirror(facingMode){const scaleFactor=facingMode==="user"?-1:1;this.$video.style.transform="scaleX("+scaleFactor+")"}setGrayscaleWeights(red,green,blue){this._qrWorker.postMessage({type:"grayscaleWeights",data:{red,green,blue}})}static scanImage(imageOrFileOrUrl,sourceRect=null,worker=
null,canvas=null,fixedCanvasSize=false,alsoTryWithoutSourceRect=false){const promise=new Promise((resolve,reject)=>{worker=worker||new Worker(QrScanner.WORKER_PATH);let timeout,onMessage,onError;onMessage=(event)=>{if(event.data.type!=="qrResult")return;worker.removeEventListener("message",onMessage);worker.removeEventListener("error",onError);clearTimeout(timeout);if(event.data.data!==null)resolve(event.data.data);else reject("QR code not found.")};onError=()=>{worker.removeEventListener("message",
onMessage);worker.removeEventListener("error",onError);clearTimeout(timeout);reject("Worker error.")};worker.addEventListener("message",onMessage);worker.addEventListener("error",onError);timeout=setTimeout(onError,3E3);QrScanner._loadImage(imageOrFileOrUrl).then((image)=>{const imageData=QrScanner._getImageData(image,sourceRect,canvas,fixedCanvasSize);worker.postMessage({type:"decode",data:imageData},[imageData.data.buffer])}).catch(reject)});if(sourceRect&&alsoTryWithoutSourceRect)return promise.catch(()=>
QrScanner.scanImage(imageOrFileOrUrl,null,worker,canvas,fixedCanvasSize));else return promise}static _getImageData(image,sourceRect=null,canvas=null,fixedCanvasSize=false){canvas=canvas||document.createElement("canvas");const sourceRectX=sourceRect&&sourceRect.x?sourceRect.x:0;const sourceRectY=sourceRect&&sourceRect.y?sourceRect.y:0;const sourceRectWidth=sourceRect&&sourceRect.width?sourceRect.width:image.width||image.videoWidth;const sourceRectHeight=sourceRect&&sourceRect.height?sourceRect.height:
image.height||image.videoHeight;if(!fixedCanvasSize&&(canvas.width!==sourceRectWidth||canvas.height!==sourceRectHeight)){canvas.width=sourceRectWidth;canvas.height=sourceRectHeight}const context=canvas.getContext("2d",{alpha:false});context.imageSmoothingEnabled=false;context.drawImage(image,sourceRectX,sourceRectY,sourceRectWidth,sourceRectHeight,0,0,canvas.width,canvas.height);return context.getImageData(0,0,canvas.width,canvas.height)}static _loadImage(imageOrFileOrUrl){if(imageOrFileOrUrl instanceof
HTMLCanvasElement||imageOrFileOrUrl instanceof HTMLVideoElement||window.ImageBitmap&&imageOrFileOrUrl instanceof window.ImageBitmap||window.OffscreenCanvas&&imageOrFileOrUrl instanceof window.OffscreenCanvas)return Promise.resolve(imageOrFileOrUrl);else if(imageOrFileOrUrl instanceof Image)return QrScanner._awaitImageLoad(imageOrFileOrUrl).then(()=>imageOrFileOrUrl);else if(imageOrFileOrUrl instanceof File||imageOrFileOrUrl instanceof URL||typeof imageOrFileOrUrl==="string"){const image=new Image;
if(imageOrFileOrUrl instanceof File)image.src=URL.createObjectURL(imageOrFileOrUrl);else image.src=imageOrFileOrUrl;return QrScanner._awaitImageLoad(image).then(()=>{if(imageOrFileOrUrl instanceof File)URL.revokeObjectURL(image.src);return image})}else return Promise.reject("Unsupported image type.")}static _awaitImageLoad(image){return new Promise((resolve,reject)=>{if(image.complete&&image.naturalWidth!==0)resolve();else{let onLoad,onError;onLoad=()=>{image.removeEventListener("load",onLoad);image.removeEventListener("error",
onError);resolve()};onError=()=>{image.removeEventListener("load",onLoad);image.removeEventListener("error",onError);reject("Image load error")};image.addEventListener("load",onLoad);image.addEventListener("error",onError)}})}}QrScanner.DEFAULT_CANVAS_SIZE=400;QrScanner.WORKER_PATH="/libraries/qr-scanner/qr-scanner-worker.min.js";
//# sourceMappingURL=qr-scanner.min.js.map
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment