From 549ae16b0e4e1bec261608a761ccda8efbe1c632 Mon Sep 17 00:00:00 2001 From: slowbackspace Date: Thu, 17 Jan 2019 17:12:12 +0100 Subject: [PATCH] add modal for scanning qr codes --- src/actions/ModalActions.js | 33 +++++ src/actions/constants/modal.js | 2 + src/components/modals/QrModal/index.js | 160 +++++++++++++++++++++++++ src/components/modals/index.js | 19 +++ src/reducers/ModalReducer.js | 7 ++ 5 files changed, 221 insertions(+) create mode 100644 src/components/modals/QrModal/index.js diff --git a/src/actions/ModalActions.js b/src/actions/ModalActions.js index cbb48e9d..47f98776 100644 --- a/src/actions/ModalActions.js +++ b/src/actions/ModalActions.js @@ -1,3 +1,4 @@ +/* eslint-disable import/no-named-as-default-member */ /* @flow */ import TrezorConnect, { UI } from 'trezor-connect'; @@ -9,6 +10,10 @@ import type { ThunkAction, AsyncAction, Action, GetState, Dispatch, TrezorDevice, } from 'flowtype'; import type { State } from 'reducers/ModalReducer'; +import type { parsedURI } from 'utils/cryptoUriParser'; + +import sendEthereumFormActions from './ethereum/SendFormActions'; +import sendRippleFormActions from './ripple/SendFormActions'; export type ModalAction = { type: typeof MODAL.CLOSE @@ -16,8 +21,11 @@ export type ModalAction = { type: typeof MODAL.OPEN_EXTERNAL_WALLET, id: string, url: string, +} | { + type: typeof MODAL.OPEN_SCAN_QR, }; + export const onPinSubmit = (value: string): Action => { TrezorConnect.uiResponse({ type: UI.RECEIVE_PIN, payload: value }); return { @@ -139,6 +147,29 @@ export const gotoExternalWallet = (id: string, url: string): ThunkAction => (dis }); }; +export const openQrModal = (): ThunkAction => (dispatch: Dispatch): void => { + dispatch({ + type: MODAL.OPEN_SCAN_QR, + }); +}; + +export const onQrScan = (parsedUri: parsedURI, networkType: string): ThunkAction => (dispatch: Dispatch): void => { + const { address = '', amount } = parsedUri; + switch (networkType) { + case 'ethereum': + dispatch(sendEthereumFormActions.onAddressChange(address)); + if (amount) dispatch(sendEthereumFormActions.onAmountChange(amount)); + break; + case 'ripple': + dispatch(sendRippleFormActions.onAddressChange(address)); + if (amount) dispatch(sendRippleFormActions.onAmountChange(amount)); + break; + default: + break; + } +}; + + export default { onPinSubmit, onPassphraseSubmit, @@ -149,4 +180,6 @@ export default { onDuplicateDevice, onWalletTypeRequest, gotoExternalWallet, + openQrModal, + onQrScan, }; \ No newline at end of file diff --git a/src/actions/constants/modal.js b/src/actions/constants/modal.js index 6e53d156..3383e91a 100644 --- a/src/actions/constants/modal.js +++ b/src/actions/constants/modal.js @@ -5,3 +5,5 @@ export const OPEN_EXTERNAL_WALLET: 'modal__external_wallet' = 'modal__external_w export const CONTEXT_NONE: 'modal_ctx_none' = 'modal_ctx_none'; export const CONTEXT_DEVICE: 'modal_ctx_device' = 'modal_ctx_device'; export const CONTEXT_EXTERNAL_WALLET: 'modal_ctx_external-wallet' = 'modal_ctx_external-wallet'; +export const OPEN_SCAN_QR: 'modal__open_scan_qr' = 'modal__open_scan_qr'; +export const CONTEXT_SCAN_QR: 'modal__ctx_scan_qr' = 'modal__ctx_scan_qr'; diff --git a/src/components/modals/QrModal/index.js b/src/components/modals/QrModal/index.js new file mode 100644 index 00000000..d2b6b0d2 --- /dev/null +++ b/src/components/modals/QrModal/index.js @@ -0,0 +1,160 @@ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* @flow */ + +import * as React from 'react'; +import PropTypes from 'prop-types'; +import QrReader from 'react-qr-reader'; +import styled from 'styled-components'; + +import colors from 'config/colors'; +import icons from 'config/icons'; + +import { H2 } from 'components/Heading'; +import P from 'components/Paragraph'; +import Icon from 'components/Icon'; +import Link from 'components/Link'; + +import { parseUri } from 'utils/cryptoUriParser'; +import type { parsedURI } from 'utils/cryptoUriParser'; +import type { Props as BaseProps } from '../Container'; + +const Wrapper = styled.div` + width: 100%; + max-width: 620px; + padding: 40px 0px 20px 0px; +`; + +const Padding = styled.div` + padding: 0px 48px; +`; + +const CloseLink = styled(Link)` + position: absolute; + right: 15px; + top: 15px; +`; + +const CameraPlaceholder = styled(P)` + text-align: center; + padding: 10px 0; +`; + +const Error = styled(P)` + text-align: center; + padding: 10px 0; + color: ${colors.ERROR_PRIMARY}; +`; + +const StyledQrReader = styled(QrReader)` + padding: 10px 0; +`; + +// TODO fix types +type Props = { + onScan: (data: parsedURI) => any, + onError?: (error: any) => any, + onCancel?: $ElementType<$ElementType, 'onCancel'>; +} + +type State = { + readerLoaded: boolean, + error: any, +}; + +class QrModal extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + readerLoaded: false, + error: null, + }; + } + + onLoad = () => { + this.setState({ + readerLoaded: true, + }); + } + + handleScan = (data: string) => { + if (data) { + try { + const parsedUri = parseUri(data); + if (parsedUri) { + this.props.onScan(parsedUri); + // reset error + this.setState({ + error: null, + }); + // close window + this.handleCancel(); + } + } catch (error) { + this.handleError(error); + } + } + } + + handleError = (err: any) => { + console.log(err); + this.setState({ + error: err, + }); + + if (this.props.onError) { + this.props.onError(err); + } + } + + handleCancel = () => { + if (this.props.onCancel) { + this.props.onCancel(); + } + } + + + render() { + return ( + + + + + +

Scan an address from a QR code

+ {!this.state.readerLoaded && ( + + Waiting for camera... + ) + } +
+ + + {this.state.error && ( + + {this.state.error.toString()} + + )} + +
+ ); + } +} + +QrModal.propTypes = { + onScan: PropTypes.func.isRequired, + onError: PropTypes.func, + onCancel: PropTypes.func, +}; + +export default QrModal; diff --git a/src/components/modals/index.js b/src/components/modals/index.js index 0e5c5782..cd7edba8 100644 --- a/src/components/modals/index.js +++ b/src/components/modals/index.js @@ -29,6 +29,8 @@ import Nem from 'components/modals/external/Nem'; import Cardano from 'components/modals/external/Cardano'; import Stellar from 'components/modals/external/Stellar'; +import QrModal from 'components/modals/QrModal'; + import type { Props } from './Container'; const ModalContainer = styled.div` @@ -166,6 +168,20 @@ const getExternalContextModal = (props: Props) => { } }; +const getQrModal = (props: Props) => { + const { modalActions, selectedAccount } = props; + + if (!selectedAccount.network) return null; + const networkType = selectedAccount.network.type; + + return ( + modalActions.onQrScan(parsedUri, networkType)} + /> + ); +}; + // modal container component const Modal = (props: Props) => { const { modal } = props; @@ -179,6 +195,9 @@ const Modal = (props: Props) => { case MODAL.CONTEXT_EXTERNAL_WALLET: component = getExternalContextModal(props); break; + case MODAL.CONTEXT_SCAN_QR: + component = getQrModal(props); + break; default: break; } diff --git a/src/reducers/ModalReducer.js b/src/reducers/ModalReducer.js index 4f3168df..266ced8b 100644 --- a/src/reducers/ModalReducer.js +++ b/src/reducers/ModalReducer.js @@ -18,6 +18,8 @@ export type State = { } | { context: typeof MODAL.CONTEXT_EXTERNAL_WALLET, windowType?: string; +} | { + context: typeof MODAL.CONTEXT_SCAN_QR, } const initialState: State = { @@ -91,6 +93,11 @@ export default function modal(state: State = initialState, action: Action): Stat windowType: action.id, }; + case MODAL.OPEN_SCAN_QR: + return { + context: MODAL.CONTEXT_SCAN_QR, + }; + default: return state; }