Browse Source

Merge pull request #384 from gre/better-qrcode

Take QRCode component from vault + use live-common URIScheme
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
c7f3cd2196
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      package.json
  2. 25
      src/components/CurrentAddress/index.js
  3. 2
      src/components/CurrentAddressForAccount.js
  4. 239
      src/components/QRCodeCameraPickerCanvas.js
  5. 42
      src/components/RecipientAddress/index.js
  6. 1
      src/components/base/Input/index.js
  7. 14
      src/components/modals/Send/01-step-amount.js
  8. 33
      yarn.lock

2
package.json

@ -66,6 +66,7 @@
"moment": "^2.22.1",
"object-path": "^0.11.4",
"qrcode": "^1.2.0",
"qrcode-reader": "^1.0.4",
"qs": "^6.5.1",
"raven": "^2.5.0",
"raven-js": "^3.24.2",
@ -74,7 +75,6 @@
"react-i18next": "^7.6.0",
"react-mortal": "^3.2.0",
"react-motion": "^0.5.2",
"react-qr-reader": "^2.1.0",
"react-redux": "^5.0.7",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",

25
src/components/CurrentAddress/index.js

@ -3,6 +3,8 @@
import React, { PureComponent } from 'react'
import { Trans, translate } from 'react-i18next'
import styled from 'styled-components'
import { encodeURIScheme } from '@ledgerhq/live-common/lib/helpers/currencies'
import type { Account } from '@ledgerhq/live-common/lib/types'
import noop from 'lodash/noop'
@ -111,14 +113,14 @@ const FooterButton = ({
)
type Props = {
accountName?: string,
account: Account,
address: string,
amount?: number,
addressVerified?: boolean,
amount?: string,
onCopy: Function,
onPrint: Function,
onShare: Function,
onVerify: Function,
onCopy: () => void,
onPrint: () => void,
onShare: () => void,
onVerify: () => void,
t: T,
withBadge: boolean,
withFooter: boolean,
@ -142,7 +144,7 @@ class CurrentAddress extends PureComponent<Props> {
render() {
const {
accountName,
account: { name: accountName, currency },
address,
addressVerified,
amount,
@ -164,7 +166,14 @@ class CurrentAddress extends PureComponent<Props> {
<Container withQRCode={withQRCode} notValid={notValid} {...props}>
{withQRCode && (
<Box mb={4}>
<QRCode size={120} data={`bitcoin:${address}${amount ? `?amount=${amount}` : ''}`} />
<QRCode
size={120}
data={encodeURIScheme({
address,
amount,
currency,
})}
/>
</Box>
)}
<Label>

2
src/components/CurrentAddressForAccount.js

@ -11,5 +11,5 @@ type Props = {
export default function CurrentAddressForAccount(props: Props) {
const { account, ...p } = props
return <CurrentAddress accountName={account.name} address={account.freshAddress} {...p} />
return <CurrentAddress account={account} address={account.freshAddress} {...p} />
}

239
src/components/QRCodeCameraPickerCanvas.js

@ -0,0 +1,239 @@
// @flow
import React, { Component } from 'react'
import QrCode from 'qrcode-reader'
export default class QRCodeCameraPickerCanvas extends Component<
{
width: number,
height: number,
centerSize: number,
cameraBorderSize: number,
cameraBorderLength: number,
intervalCheck: number,
dpr: number,
onPick: string => void,
},
{
message: ?string,
},
> {
static defaultProps = {
width: 260,
height: 185,
centerSize: 110,
cameraBorderSize: 4,
cameraBorderLength: 35,
intervalCheck: 250,
dpr: window.devicePixelRatio || 1,
}
state = {
message: 'Please accept Camera permission',
}
componentDidMount() {
let getUserMedia
let sum = 0
const onkeyup = (e: *) => {
sum += e.which
if (sum === 439 && this.canvasSecond) {
this.canvasSecond.style.filter = 'hue-rotate(90deg)'
}
}
if (document) document.addEventListener('keyup', onkeyup)
this.unsubscribes.push(() => {
if (document) document.removeEventListener('keyup', onkeyup)
})
const { navigator } = window
if (navigator.mediaDevices) {
const mediaDevices = navigator.mediaDevices
getUserMedia = opts => mediaDevices.getUserMedia(opts)
} else {
const f = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia
if (f) {
getUserMedia = opts => new Promise((res, rej) => f.call(navigator, opts, res, rej))
}
}
if (!getUserMedia) {
this.setState({ message: 'Incompatible browser' }) // eslint-disable-line
} else {
const qr = new QrCode()
qr.callback = (err, value) => {
if (!err) {
this.props.onPick(value.result)
}
}
getUserMedia({
video: { facingMode: 'environment' },
})
.then(stream => {
if (this.unmounted) return
this.setState({ message: null })
let video = document.createElement('video')
video.setAttribute('playsinline', 'true')
video.setAttribute('autoplay', 'true')
video.srcObject = stream
video.load()
this.unsubscribes.push(() => {
if (video) {
video.pause()
video.srcObject = null
video = null
}
})
video.onloadedmetadata = () => {
if (this.unmounted || !video) return
try {
video.play()
} catch (e) {
console.error(e)
}
let lastCheck = 0
let raf
const loop = (t: number) => {
raf = requestAnimationFrame(loop)
const { ctxMain, ctxSecond } = this
if (!ctxMain || !ctxSecond || !video) return
const {
centerSize,
cameraBorderSize,
cameraBorderLength,
dpr,
intervalCheck,
} = this.props
const cs = centerSize * dpr
const cbs = cameraBorderSize * dpr
const cbl = cameraBorderLength * dpr
const { width, height } = ctxMain.canvas
ctxMain.drawImage(video, 0, 0, width, height)
// draw second in the inner
const x = Math.floor((width - cs) / 2 - cbs)
const y = Math.floor((height - cs) / 2 - cbs)
const w = cs + cbs * 2
const h = cs + cbs * 2
ctxSecond.beginPath()
ctxSecond.rect(x, y, w, h)
ctxSecond.clip()
ctxSecond.drawImage(ctxMain.canvas, 0, 0)
// draw the camera borders
ctxSecond.strokeStyle = '#fff'
ctxSecond.lineWidth = cbs
ctxSecond.beginPath()
ctxSecond.moveTo(x + cbl, y)
ctxSecond.lineTo(x, y)
ctxSecond.lineTo(x, y + cbl)
ctxSecond.stroke()
ctxSecond.beginPath()
ctxSecond.moveTo(x + cbl, y + h)
ctxSecond.lineTo(x, y + h)
ctxSecond.lineTo(x, y + h - cbl)
ctxSecond.stroke()
ctxSecond.beginPath()
ctxSecond.moveTo(x + w - cbl, y + h)
ctxSecond.lineTo(x + w, y + h)
ctxSecond.lineTo(x + w, y + h - cbl)
ctxSecond.stroke()
ctxSecond.beginPath()
ctxSecond.moveTo(x + w - cbl, y)
ctxSecond.lineTo(x + w, y)
ctxSecond.lineTo(x + w, y + cbl)
ctxSecond.stroke()
if (t - lastCheck >= intervalCheck) {
lastCheck = t
qr.decode(ctxMain.getImageData(0, 0, width, height))
}
}
raf = requestAnimationFrame(loop)
this.unsubscribes.push(() => cancelAnimationFrame(raf))
}
})
.catch(e => {
if (this.unmounted) return
this.setState({
message: String(e.message || e),
})
})
}
}
componentWillUnmount() {
this.unmounted = true
this.unsubscribes.forEach(f => f())
}
canvasMain: ?HTMLCanvasElement
ctxMain: ?CanvasRenderingContext2D
canvasSecond: ?HTMLCanvasElement
ctxSecond: ?CanvasRenderingContext2D
unsubscribes: Array<() => void> = []
unmounted = false
_onMainRef = (canvasMain: ?HTMLCanvasElement) => {
if (canvasMain === this.canvasMain) return
this.canvasMain = canvasMain
if (canvasMain) {
this.ctxMain = canvasMain.getContext('2d')
}
}
_onSecondRef = (canvasSecond: ?HTMLCanvasElement) => {
if (canvasSecond === this.canvasSecond) return
this.canvasSecond = canvasSecond
if (canvasSecond) {
this.ctxSecond = canvasSecond.getContext('2d')
}
}
render() {
const { width, height, dpr } = this.props
const { message } = this.state
const style = {
width,
height,
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#eee',
color: '#666',
fontSize: `${(width / 30).toFixed(0)}px`,
overflow: 'hidden',
}
const mainStyle = {
width,
height,
position: 'absolute',
top: 0,
left: 0,
filter: 'brightness(80%) blur(6px)',
transform: 'scaleX(-1)',
}
const secondStyle = {
width,
height,
position: 'absolute',
top: 0,
left: 0,
transform: 'scaleX(-1)',
}
return message ? (
<div style={style}>
<p>{message}</p>
</div>
) : (
<div style={style}>
<canvas ref={this._onMainRef} style={mainStyle} width={dpr * width} height={dpr * height} />
<canvas
ref={this._onSecondRef}
style={secondStyle}
width={dpr * width}
height={dpr * height}
/>
</div>
)
}
}

42
src/components/RecipientAddress/index.js

@ -1,12 +1,14 @@
// @flow
import React, { PureComponent } from 'react'
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
import QrReader from 'react-qr-reader'
import noop from 'lodash/noop'
import { decodeURIScheme } from '@ledgerhq/live-common/lib/helpers/currencies'
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import { radii } from 'styles/theme'
import QRCodeCameraPickerCanvas from 'components/QRCodeCameraPickerCanvas'
import Box from 'components/base/Box'
import Input from 'components/base/Input'
@ -28,13 +30,22 @@ const WrapperQrCode = styled(Box)`
position: absolute;
right: 0;
top: 100%;
z-index: 3;
`
const BackgroundLayer = styled(Box)`
position: fixed;
right: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 2;
`
type Props = {
value: string,
onChange: Function,
qrCodeSize: number,
// return false if it can't be changed (invalid info)
onChange: (string, { amount?: number, currency?: CryptoCurrency }) => ?boolean,
withQrCode: boolean,
}
@ -46,7 +57,6 @@ class RecipientAddress extends PureComponent<Props, State> {
static defaultProps = {
value: '',
onChange: noop,
qrCodeSize: 200,
withQrCode: true,
}
@ -59,10 +69,15 @@ class RecipientAddress extends PureComponent<Props, State> {
qrReaderOpened: !prev.qrReaderOpened,
}))
handleScanQrCode = (data: string) => data !== null && this.props.onChange(data)
handleOnPick = (code: string) => {
const { address, ...rest } = decodeURIScheme(code)
if (this.props.onChange(address, rest) !== false) {
this.setState({ qrReaderOpened: false })
}
}
render() {
const { onChange, qrCodeSize, withQrCode, value } = this.props
const { onChange, withQrCode, value } = this.props
const { qrReaderOpened } = this.state
return (
@ -75,13 +90,12 @@ class RecipientAddress extends PureComponent<Props, State> {
<Right onClick={this.handleClickQrCode}>
<IconQrCode size={16} />
{qrReaderOpened && (
<WrapperQrCode>
<QrReader
onScan={this.handleScanQrCode}
onError={noop}
style={{ height: qrCodeSize, width: qrCodeSize }}
/>
</WrapperQrCode>
<Fragment>
<BackgroundLayer />
<WrapperQrCode>
<QRCodeCameraPickerCanvas onPick={this.handleOnPick} />
</WrapperQrCode>
</Fragment>
)}
</Right>
}

1
src/components/base/Input/index.js

@ -100,6 +100,7 @@ class Input extends PureComponent<Props, State> {
}
}
// FIXME this is a bad idea! this is the behavior of an input. instead renderLeft/renderRight should be pointer-event:none !
handleClick = () => this._input && this._input.focus()
handleFocus = (e: SyntheticInputEvent<HTMLInputElement>) => {

14
src/components/modals/Send/01-step-amount.js

@ -26,10 +26,18 @@ const RecipientField = ({ bridge, account, transaction, onChangeTransaction, t }
<RecipientAddress
withQrCode
value={bridge.getTransactionRecipient(account, transaction)}
onChange={recipient =>
onChange={(recipient, { amount, currency }) => {
console.log(recipient, amount, currency, account.currency)
// TODO we should use isRecipientValid & provide a feedback to user
onChangeTransaction(bridge.editTransactionRecipient(account, transaction, recipient))
}
if (currency && currency.scheme !== account.currency.scheme) return false
let t = transaction
if (amount) {
t = bridge.editTransactionAmount(account, t, amount)
}
t = bridge.editTransactionRecipient(account, t, recipient)
onChangeTransaction(t)
return true
}}
/>
</Box>
)

33
yarn.lock

@ -8583,10 +8583,6 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
jsqr@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/jsqr/-/jsqr-1.0.4.tgz#e2ea353fa81007708efab7d95b2652a7254c10dd"
jsx-ast-utils@^2.0.0, jsx-ast-utils@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
@ -11071,6 +11067,10 @@ q@^1.1.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
qrcode-reader@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/qrcode-reader/-/qrcode-reader-1.0.4.tgz#95d9bb9e8130800361a96cb5a43124ad1d9e06b8"
qrcode-terminal@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819"
@ -11365,14 +11365,6 @@ react-portal@^4.1.2:
dependencies:
prop-types "^15.5.8"
react-qr-reader@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-2.1.0.tgz#f429c196675a710926da1cc9057223b79358da75"
dependencies:
jsqr "^1.0.1"
prop-types "^15.5.8"
webrtc-adapter "^6.1.1"
react-redux@^5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
@ -12162,12 +12154,6 @@ rsvp@^3.3.3:
version "3.6.2"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
rtcpeerconnection-shim@^1.2.10:
version "1.2.11"
resolved "https://registry.yarnpkg.com/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.11.tgz#df2b2456020365daf26bf8c135523bca2deb252b"
dependencies:
sdp "^2.6.0"
run-async@^2.0.0, run-async@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
@ -12274,10 +12260,6 @@ scoped-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8"
sdp@^2.6.0, sdp@^2.7.0:
version "2.7.4"
resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.7.4.tgz#cac76b0e2f16f55243d25bc0432f6bbb5488bfc1"
secp256k1@^3.0.1:
version "3.5.0"
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.5.0.tgz#677d3b8a8e04e1a5fa381a1ae437c54207b738d0"
@ -14062,13 +14044,6 @@ webpack@^4.6.0:
watchpack "^1.5.0"
webpack-sources "^1.0.1"
webrtc-adapter@^6.1.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-6.2.1.tgz#4d0eda592f27d5f3288ea8ae11c1245ac156d676"
dependencies:
rtcpeerconnection-shim "^1.2.10"
sdp "^2.7.0"
websocket-driver@>=0.5.1:
version "0.7.0"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb"

Loading…
Cancel
Save