diff --git a/package.json b/package.json index b26872ae..95d6be2e 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "react-final-form-listeners": "^1.0.2", "react-hot-loader": "4.12.9", "react-infinite-scroll-component": "^4.5.2", + "react-qr-reader": "^2.2.1", "react-redux": "7.1.0", "react-router-dom": "^5.0.1", "recompose": "^0.30.0", @@ -123,7 +124,7 @@ "json-loader": "^0.5.7", "mini-css-extract-plugin": "0.8.0", "postcss-loader": "^3.0.0", - "postcss-mixins": "^6.2.0", + "postcss-mixins": "6.2.2", "postcss-simple-vars": "^5.0.2", "pre-commit": "^1.2.2", "prettier-eslint-cli": "5.0.0", diff --git a/src/assets/icons/qrcode.svg b/src/assets/icons/qrcode.svg new file mode 100644 index 00000000..59b5d707 --- /dev/null +++ b/src/assets/icons/qrcode.svg @@ -0,0 +1,26 @@ + + + + Group + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/Loader/index.jsx b/src/components/Loader/index.jsx index 5ae3f7b9..78906e6b 100644 --- a/src/components/Loader/index.jsx +++ b/src/components/Loader/index.jsx @@ -1,7 +1,7 @@ // @flow import * as React from 'react' -import Page from '~/components/layout/Page' import CircularProgress from '@material-ui/core/CircularProgress' +import Page from '~/components/layout/Page' const centerStyle = { margin: 'auto 0', diff --git a/src/components/Stepper/index.jsx b/src/components/Stepper/index.jsx index 7d3de182..2b2bae41 100644 --- a/src/components/Stepper/index.jsx +++ b/src/components/Stepper/index.jsx @@ -1,16 +1,17 @@ // @flow +import * as React from 'react' import Stepper from '@material-ui/core/Stepper' import FormStep from '@material-ui/core/Step' import StepLabel from '@material-ui/core/StepLabel' import StepContent from '@material-ui/core/StepContent' import { withStyles } from '@material-ui/core/styles' -import * as React from 'react' import GnoForm from '~/components/forms/GnoForm' import Hairline from '~/components/layout/Hairline' -import Button from '~/components/layout/Button' import { history } from '~/store' import Controls from './Controls' +const { useState, useEffect } = React + export { default as Step } from './Step' type Props = { @@ -18,20 +19,15 @@ type Props = { onSubmit: (values: Object) => Promise, children: React.Node, classes: Object, - onReset?: () => void, initialValues?: Object, disabledWhenValidating?: boolean, + mutators?: Object, testId?: string, } -type State = { - page: number, - values: Object, -} - type PageProps = { children: Function, - prepareNextInitialProps: (values: Object) => {}, + prepareNextInitialProps?: (values: Object) => {}, } const transitionProps = { @@ -41,151 +37,124 @@ const transitionProps = { }, } -class GnoStepper extends React.PureComponent { - static Page = ({ children }: PageProps) => children +export const StepperPage = ({ children }: PageProps) => children - static FinishButton = ({ - component, to, title, ...props - }) => ( - - ) +const GnoStepper = (props: Props) => { + const [page, setPage] = useState(0) + const [values, setValues] = useState({}) - constructor(props: Props) { - super(props) - - this.state = { - page: 0, - values: props.initialValues || {}, + useEffect(() => { + if (props.initialValues) { + setValues(props.initialValues) } + }, []) + + const getPageProps = (pages: React.Node): PageProps => React.Children.toArray(pages)[page].props + + const updateInitialProps = (newInitialProps) => { + setValues(newInitialProps) } - onReset = () => { - const { onReset, initialValues } = this.props - if (onReset) { - onReset() - } + const getActivePageFrom = (pages: React.Node) => { + const activePageProps = getPageProps(pages) + const { children, ...restProps } = activePageProps - this.setState(() => ({ - page: 0, - values: initialValues || {}, - })) + return children({ ...restProps, updateInitialProps }) } - getPageProps = (pages: React.Node): PageProps => { - const { page } = this.state - - return React.Children.toArray(pages)[page].props - } - - getActivePageFrom = (pages: React.Node) => { - const activePageProps = this.getPageProps(pages) - const { children, ...props } = activePageProps - - return children({ ...props, updateInitialProps: this.updateInitialProps }) - } - - updateInitialProps = (values) => { - this.setState({ values }) - } - - validate = (values: Object) => { - const { children } = this.props - const { page } = this.state + const validate = (valuesToValidate: Object) => { + const { children } = props const activePage = React.Children.toArray(children)[page] - return activePage.props.validate ? activePage.props.validate(values) : {} + return activePage.props.validate ? activePage.props.validate(valuesToValidate) : {} } - next = async (values: Object) => { - const { children } = this.props - const activePageProps = this.getPageProps(children) + const next = async (formValues: Object) => { + const { children } = props + const activePageProps = getPageProps(children) const { prepareNextInitialProps } = activePageProps let pageInitialProps if (prepareNextInitialProps) { - pageInitialProps = await prepareNextInitialProps(values) + pageInitialProps = await prepareNextInitialProps(formValues) } - const finalValues = { ...values, ...pageInitialProps } - this.setState(state => ({ - page: Math.min(state.page + 1, React.Children.count(children) - 1), - values: finalValues, - })) + const finalValues = { ...formValues, ...pageInitialProps } + + setValues(finalValues) + setPage(Math.min(page + 1, React.Children.count(children) - 1)) } - previous = () => { - const { page } = this.state - + const previous = () => { const firstPage = page === 0 if (firstPage) { return history.goBack() } - return this.setState(state => ({ - page: Math.max(state.page - 1, 0), - })) + return setPage(Math.max(page - 1, 0)) } - handleSubmit = async (values: Object) => { - const { children, onSubmit } = this.props - const { page } = this.state + const handleSubmit = async (formValues: Object) => { + const { children, onSubmit } = props const isLastPage = page === React.Children.count(children) - 1 if (isLastPage) { - return onSubmit(values) + return onSubmit(formValues) } - return this.next(values) + return next(formValues) } - isLastPage = (page) => { - const { steps } = this.props - return page === steps.length - 1 + const isLastPage = (pageNumber) => { + const { steps } = props + return pageNumber === steps.length - 1 } - render() { - const { - steps, children, classes, disabledWhenValidating = false, testId, - } = this.props - const { page, values } = this.state - const activePage = this.getActivePageFrom(children) - const lastPage = this.isLastPage(page) - const penultimate = this.isLastPage(page + 1) + const { + steps, children, classes, disabledWhenValidating = false, testId, mutators, + } = props + const activePage = getActivePageFrom(children) - return ( - - - {(submitting: boolean, validating: boolean, ...rest: any) => { - const disabled = disabledWhenValidating ? submitting || validating : submitting - const controls = ( - - - - - ) + const lastPage = isLastPage(page) + const penultimate = isLastPage(page + 1) - return ( - - {steps.map(label => ( - - {label} - {activePage(controls, ...rest)} - - ))} - - ) - }} - - - ) - } + return ( + + + {(submitting: boolean, validating: boolean, ...rest: any) => { + const disabled = disabledWhenValidating ? submitting || validating : submitting + const controls = ( + + + + + ) + + return ( + + {steps.map(label => ( + + {label} + {activePage(controls, ...rest)} + + ))} + + ) + }} + + + ) } const styles = { diff --git a/src/components/layout/Block/index.jsx b/src/components/layout/Block/index.jsx index d56d9cbc..e1aced02 100644 --- a/src/components/layout/Block/index.jsx +++ b/src/components/layout/Block/index.jsx @@ -1,10 +1,12 @@ // @flow import classNames from 'classnames/bind' -import React, { PureComponent } from 'react' +import * as React from 'react' import { capitalize } from '~/utils/css' import { type Size } from '~/theme/size' import styles from './index.scss' +const { PureComponent } = React + const cx = classNames.bind(styles) type Props = { diff --git a/src/routes/load/components/Layout.jsx b/src/routes/load/components/Layout.jsx index 0fa0a213..6e9ab43b 100644 --- a/src/routes/load/components/Layout.jsx +++ b/src/routes/load/components/Layout.jsx @@ -2,7 +2,7 @@ import * as React from 'react' import ChevronLeft from '@material-ui/icons/ChevronLeft' import IconButton from '@material-ui/core/IconButton' -import Stepper from '~/components/Stepper' +import Stepper, { StepperPage } from '~/components/Stepper' import Block from '~/components/layout/Block' import Heading from '~/components/layout/Heading' import Row from '~/components/layout/Row' @@ -46,11 +46,11 @@ const Layout = ({ Load existing Safe - {DetailsForm} - {OwnerList} - + {DetailsForm} + {OwnerList} + {ReviewInformation} - + ) : ( diff --git a/src/routes/open/components/Layout.jsx b/src/routes/open/components/Layout.jsx index e2a545b4..26c0f7c0 100644 --- a/src/routes/open/components/Layout.jsx +++ b/src/routes/open/components/Layout.jsx @@ -1,20 +1,19 @@ // @flow import * as React from 'react' -import Stepper from '~/components/Stepper' +import ChevronLeft from '@material-ui/icons/ChevronLeft' +import IconButton from '@material-ui/core/IconButton' +import Stepper, { StepperPage } from '~/components/Stepper' import Block from '~/components/layout/Block' import Heading from '~/components/layout/Heading' import Row from '~/components/layout/Row' -import IconButton from '@material-ui/core/IconButton' import Review from '~/routes/open/components/ReviewInformation' -import ChevronLeft from '@material-ui/icons/ChevronLeft' import SafeNameField from '~/routes/open/components/SafeNameForm' -import SafeThresholdField, { safeFieldsValidation } from '~/routes/open/components/SafeThresholdForm' -import SafeOwnersFields from '~/routes/open/components/SafeOwnersForm' +import SafeOwnersFields from '~/routes/open/components/SafeOwnersConfirmationsForm' import { getOwnerNameBy, getOwnerAddressBy, FIELD_CONFIRMATIONS } from '~/routes/open/components/fields' import { history } from '~/store' import { secondary } from '~/theme/variables' -const getSteps = () => ['Start', 'Owners', 'Confirmations', 'Review'] +const getSteps = () => ['Start', 'Owners and confirmations', 'Review'] const initialValuesFrom = (userAccount: string) => ({ [getOwnerNameBy(0)]: 'My Metamask (me)', @@ -39,6 +38,12 @@ const back = () => { history.goBack() } +const formMutators = { + setValue: ([field, value], state, { changeValue }) => { + changeValue(state, field, () => value) + }, +} + const Layout = ({ provider, userAccount, onCallSafeContractSubmit, network, }: Props) => { @@ -55,15 +60,20 @@ const Layout = ({ Create New Safe - - {SafeNameField} - {SafeOwnersFields} - {SafeThresholdField} - {Review} + + {SafeNameField} + {SafeOwnersFields} + {Review} ) : ( -
No metamask detected
+
No web3 provider detected
)} ) diff --git a/src/routes/open/components/ReviewInformation/index.jsx b/src/routes/open/components/ReviewInformation/index.jsx index 940ea653..056e72c0 100644 --- a/src/routes/open/components/ReviewInformation/index.jsx +++ b/src/routes/open/components/ReviewInformation/index.jsx @@ -129,7 +129,11 @@ const ReviewComponent = ({ values, classes, network }: Props) => { {addresses[index]} - + @@ -143,10 +147,8 @@ const ReviewComponent = ({ values, classes, network }: Props) => { - {"You're about to create a new Safe."} - - - Make sure you have enough ETH in your wallet client to fund this transaction. + You're about to create a new Safe and will have to confirm a transaction with your currently connected + wallet. Make sure you have ETH in this wallet to fund this transaction. diff --git a/src/routes/open/components/SafeNameForm/index.jsx b/src/routes/open/components/SafeNameForm/index.jsx index 055a7894..aade39f6 100644 --- a/src/routes/open/components/SafeNameForm/index.jsx +++ b/src/routes/open/components/SafeNameForm/index.jsx @@ -5,7 +5,6 @@ import Field from '~/components/forms/Field' import TextField from '~/components/forms/TextField' import { required } from '~/components/forms/validator' import Block from '~/components/layout/Block' -import Row from '~/components/layout/Row' import { FIELD_NAME } from '~/routes/open/components/fields' import Paragraph from '~/components/layout/Paragraph' import OpenPaper from '~/components/Stepper/OpenPaper' @@ -35,10 +34,25 @@ const styles = () => ({ const SafeName = ({ classes }: Props) => ( + + + You are about to create a new Gnosis Safe wallet with one or more owners. First, let's give your new wallet + a name. This name is only stored locally and will never be shared with Gnosis or any third parties. + + + + + - This setup will create a Safe with one or more owners. Optionally give the Safe a local name. By continuing you - consent with the + By continuing you consent with the {' '} terms of use @@ -49,35 +63,10 @@ const SafeName = ({ classes }: Props) => ( privacy policy - . + . Most importantly, you confirm that your funds are held securely in the Gnosis Safe, a smart contract on the + Ethereum blockchain. These funds cannot be accessed by Gnosis at any point. - - - ● - - - I understand that my funds are held securely in my Safe. They cannot be accessed by Gnosis. - - - - - ● - - - My Safe is a smart contract on the Ethereum blockchain. - - - - - ) diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/index.jsx b/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/index.jsx new file mode 100644 index 00000000..80405529 --- /dev/null +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/index.jsx @@ -0,0 +1,119 @@ +// @flow +import * as React from 'react' +import QrReader from 'react-qr-reader' +import { withStyles } from '@material-ui/core/styles' +import Close from '@material-ui/icons/Close' +import IconButton from '@material-ui/core/IconButton' +import CircularProgress from '@material-ui/core/CircularProgress' +import Paragraph from '~/components/layout/Paragraph' +import Button from '~/components/layout/Button' +import Block from '~/components/layout/Block' +import Row from '~/components/layout/Row' +import Hairline from '~/components/layout/Hairline' +import Col from '~/components/layout/Col' +import Modal from '~/components/Modal' +import { checkWebcam } from './utils' +import { styles } from './style' + +const { useEffect, useState } = React + +type Props = { + onClose: () => void, + classes: Object, + onScan: Function, + isOpen: boolean, +} + +const ScanQRModal = ({ + classes, onClose, isOpen, onScan, +}: Props) => { + const [hasWebcam, setHasWebcam] = useState(null) + const scannerRef: Object = React.createRef() + const openImageDialog = () => { + scannerRef.current.openImageDialog() + } + + useEffect(() => { + checkWebcam( + () => { + setHasWebcam(true) + }, + () => { + setHasWebcam(false) + }, + ) + }, []) + + useEffect(() => { + // this fires only when the hasWebcam changes to false (null > false (user doesn't have webcam) + // , true > false (user switched from webcam to file upload)) + // Doesn't fire on re-render + if (hasWebcam === false) { + openImageDialog() + } + }, [hasWebcam]) + + return ( + + + + Scan QR + + + + + + + + {hasWebcam === null ? ( + + + + ) : ( + { + if (data) onScan(data) + }} + onError={(err) => { + console.error(err) + }} + style={{ width: '400px', height: '400px' }} + /> + )} + + + + + + + + ) +} + +export default withStyles(styles)(ScanQRModal) diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/style.js b/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/style.js new file mode 100644 index 00000000..50d04702 --- /dev/null +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/style.js @@ -0,0 +1,35 @@ +// @flow +import { lg, sm, background } from '~/theme/variables' + +export const styles = () => ({ + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'space-between', + maxHeight: '75px', + boxSizing: 'border-box', + }, + loaderContainer: { + width: '100%', + height: '100%', + }, + manage: { + fontSize: '24px', + }, + close: { + height: '35px', + width: '35px', + }, + detailsContainer: { + backgroundColor: background, + maxHeight: '420px', + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + }, + button: { + '&:last-child': { + marginLeft: sm, + }, + }, +}) diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/utils.js b/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/utils.js new file mode 100644 index 00000000..50bdc089 --- /dev/null +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/utils.js @@ -0,0 +1,15 @@ +// @flow +navigator.getMedia = navigator.getUserMedia // use the proper vendor prefix + || navigator.webkitGetUserMedia + || navigator.mozGetUserMedia + || navigator.msGetUserMedia + +export const checkWebcam = (success: Function, err: Function) => navigator.getMedia( + { video: true }, + () => { + success() + }, + () => { + err() + }, +) diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/index.jsx b/src/routes/open/components/SafeOwnersConfirmationsForm/index.jsx new file mode 100644 index 00000000..bd36a6cf --- /dev/null +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/index.jsx @@ -0,0 +1,231 @@ +// @flow +import * as React from 'react' +import { withStyles } from '@material-ui/core/styles' +import InputAdornment from '@material-ui/core/InputAdornment' +import CheckCircle from '@material-ui/icons/CheckCircle' +import MenuItem from '@material-ui/core/MenuItem' +import Field from '~/components/forms/Field' +import TextField from '~/components/forms/TextField' +import SelectField from '~/components/forms/SelectField' +import { + required, composeValidators, noErrorsOn, mustBeInteger, minValue, +} from '~/components/forms/validator' +import Block from '~/components/layout/Block' +import Button from '~/components/layout/Button' +import Row from '~/components/layout/Row' +import Img from '~/components/layout/Img' +import Col from '~/components/layout/Col' +import { + FIELD_CONFIRMATIONS, + getOwnerNameBy, + getOwnerAddressBy, + getNumOwnersFrom, +} from '~/routes/open/components/fields' +import Paragraph from '~/components/layout/Paragraph' +import OpenPaper from '~/components/Stepper/OpenPaper' +import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor' +import Hairline from '~/components/layout/Hairline' +import trash from '~/assets/icons/trash.svg' +import QRIcon from '~/assets/icons/qrcode.svg' +import ScanQRModal from './ScanQRModal' +import { getAddressValidators } from './validators' +import { styles } from './style' + +type Props = { + classes: Object, + otherAccounts: string[], + errors: Object, + form: Object, + values: Object, +} + +const { useState } = React + +export const ADD_OWNER_BUTTON = '+ ADD ANOTHER OWNER' + +export const calculateValuesAfterRemoving = (index: number, notRemovedOwners: number, values: Object) => { + const initialValues = { ...values } + + const numOwnersAfterRemoving = notRemovedOwners - 1 + // muevo indices + for (let i = index; i < numOwnersAfterRemoving; i += 1) { + initialValues[getOwnerNameBy(i)] = values[getOwnerNameBy(i + 1)] + initialValues[getOwnerAddressBy(i)] = values[getOwnerAddressBy(i + 1)] + } + + if (+values[FIELD_CONFIRMATIONS] === notRemovedOwners) { + initialValues[FIELD_CONFIRMATIONS] = numOwnersAfterRemoving.toString() + } + + delete initialValues[getOwnerNameBy(index)] + delete initialValues[getOwnerAddressBy(index)] + + return initialValues +} + +const SafeOwners = (props: Props) => { + const { + classes, errors, otherAccounts, values, form, + } = props + + const validOwners = getNumOwnersFrom(values) + const [numOwners, setNumOwners] = useState(validOwners) + const [qrModalOpen, setQrModalOpen] = useState(false) + const [scanQrForOwnerName, setScanQrForOwnerName] = useState(null) + + const openQrModal = (ownerName) => { + setScanQrForOwnerName(ownerName) + setQrModalOpen(true) + } + + const closeQrModal = () => { + setQrModalOpen(false) + } + + const onRemoveRow = (index: number) => () => { + const initialValues = calculateValuesAfterRemoving(index, numOwners, values) + form.reset(initialValues) + + setNumOwners(numOwners - 1) + } + + const onAddOwner = () => { + setNumOwners(numOwners + 1) + } + + const handleScan = (value) => { + let scannedAddress = value + + if (scannedAddress.startsWith('ethereum:')) { + scannedAddress = scannedAddress.replace('ethereum:', '') + } + + form.mutators.setValue(scanQrForOwnerName, scannedAddress) + closeQrModal() + } + + return ( + + + + Specify the owners of the Safe. + + + + + NAME + ADDRESS + + + + {[...Array(Number(numOwners))].map((x, index) => { + const addressName = getOwnerAddressBy(index) + + return ( + + + + + + + + + ), + } + } + type="text" + validate={getAddressValidators(otherAccounts, index)} + placeholder="Owner Address*" + text="Owner Address" + /> + + + Scan QR { + openQrModal(addressName) + }} + /> + + + {index > 0 && Delete} + + + ) + })} + + + + + + + Any transaction requires the confirmation of: + + + + + {[...Array(Number(validOwners))].map((x, index) => ( + + {index + 1} + + ))} + + + + + out of + {' '} + {validOwners} + {' '} +owner(s) + + + + + {qrModalOpen && } + + ) +} + +const SafeOwnersForm = withStyles(styles)(SafeOwners) + +const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors, form }: Object) => ( + + + + + +) + +export default SafeOwnersPage diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/style.js b/src/routes/open/components/SafeOwnersConfirmationsForm/style.js new file mode 100644 index 00000000..600a4b72 --- /dev/null +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/style.js @@ -0,0 +1,44 @@ +// @flow +import { md, lg, sm } from '~/theme/variables' + +export const styles = () => ({ + root: { + display: 'flex', + }, + title: { + padding: `${md} ${lg}`, + }, + owner: { + padding: `0 ${lg}`, + marginTop: '12px', + '&:first-child': { + marginTop: 0, + }, + }, + header: { + padding: `${sm} ${lg}`, + }, + name: { + marginRight: `${sm}`, + }, + trash: { + top: '5px', + }, + add: { + justifyContent: 'center', + }, + check: { + color: '#03AE60', + height: '20px', + }, + remove: { + height: '56px', + maxWidth: '50px', + '&:hover': { + cursor: 'pointer', + }, + }, + owners: { + paddingLeft: md, + }, +}) diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/validators.js b/src/routes/open/components/SafeOwnersConfirmationsForm/validators.js new file mode 100644 index 00000000..97f877d7 --- /dev/null +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/validators.js @@ -0,0 +1,17 @@ +// @flow +import { + required, + composeValidators, + uniqueAddress, + mustBeEthereumAddress, +} from '~/components/forms/validator' + +export const getAddressValidators = (addresses: string[], position: number) => { + // thanks Rich Harris + // https://twitter.com/Rich_Harris/status/1125850391155965952 + const copy = addresses.slice() + copy[position] = copy[copy.length - 1] + copy.pop() + + return composeValidators(required, mustBeEthereumAddress, uniqueAddress(copy)) +} diff --git a/src/routes/open/components/SafeOwnersForm/index.jsx b/src/routes/open/components/SafeOwnersForm/index.jsx deleted file mode 100644 index cec64ddc..00000000 --- a/src/routes/open/components/SafeOwnersForm/index.jsx +++ /dev/null @@ -1,212 +0,0 @@ -// @flow -import * as React from 'react' -import { withStyles } from '@material-ui/core/styles' -import InputAdornment from '@material-ui/core/InputAdornment' -import CheckCircle from '@material-ui/icons/CheckCircle' -import Field from '~/components/forms/Field' -import TextField from '~/components/forms/TextField' -import { - required, - composeValidators, - uniqueAddress, - mustBeEthereumAddress, - noErrorsOn, -} from '~/components/forms/validator' -import Block from '~/components/layout/Block' -import Button from '~/components/layout/Button' -import Row from '~/components/layout/Row' -import Img from '~/components/layout/Img' -import Col from '~/components/layout/Col' -import { getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields' -import Paragraph from '~/components/layout/Paragraph' -import OpenPaper from '~/components/Stepper/OpenPaper' -import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor' -import Hairline from '~/components/layout/Hairline' -import { md, lg, sm } from '~/theme/variables' -import trash from '~/assets/icons/trash.svg' - -type Props = { - classes: Object, - otherAccounts: string[], - errors: Object, - values: Object, - updateInitialProps: (initialValues: Object) => void, -} - -type State = { - numOwners: number, -} - -const styles = () => ({ - root: { - display: 'flex', - }, - title: { - padding: `${md} ${lg}`, - }, - owner: { - padding: `0 ${lg}`, - }, - header: { - padding: `${sm} ${lg}`, - }, - name: { - marginRight: `${sm}`, - }, - trash: { - top: '5px', - }, - add: { - justifyContent: 'center', - }, - check: { - color: '#03AE60', - height: '20px', - }, - remove: { - height: '56px', - marginTop: '12px', - maxWidth: '50px', - '&:hover': { - cursor: 'pointer', - }, - }, -}) - -const getAddressValidators = (addresses: string[], position: number) => { - // thanks Rich Harris - // https://twitter.com/Rich_Harris/status/1125850391155965952 - const copy = addresses.slice() - copy[position] = copy[copy.length - 1] - copy.pop() - - return composeValidators(required, mustBeEthereumAddress, uniqueAddress(copy)) -} - -export const ADD_OWNER_BUTTON = '+ ADD ANOTHER OWNER' - -export const calculateValuesAfterRemoving = (index: number, notRemovedOwners: number, values: Object) => { - const initialValues = { ...values } - const numOwnersAfterRemoving = notRemovedOwners - 1 - // muevo indices - for (let i = index; i < numOwnersAfterRemoving; i += 1) { - initialValues[getOwnerNameBy(i)] = values[getOwnerNameBy(i + 1)] - initialValues[getOwnerAddressBy(i)] = values[getOwnerAddressBy(i + 1)] - } - - delete initialValues[getOwnerNameBy(numOwnersAfterRemoving)] - delete initialValues[getOwnerAddressBy(numOwnersAfterRemoving)] - - return initialValues -} - -class SafeOwners extends React.PureComponent { - state = { - numOwners: 1, - } - - onRemoveRow = (index: number) => () => { - const { values, updateInitialProps } = this.props - const { numOwners } = this.state - const initialValues = calculateValuesAfterRemoving(index, numOwners, values) - updateInitialProps(initialValues) - - this.setState(state => ({ - numOwners: state.numOwners - 1, - })) - } - - onAddOwner = () => { - this.setState(state => ({ - numOwners: state.numOwners + 1, - })) - } - - render() { - const { classes, errors, otherAccounts } = this.props - const { numOwners } = this.state - - return ( - - - - Specify the owners of the Safe. - - - - - NAME - ADDRESS - - - - {[...Array(Number(numOwners))].map((x, index) => { - const addressName = getOwnerAddressBy(index) - - return ( - - - - - - - - - ), - } - } - type="text" - validate={getAddressValidators(otherAccounts, index)} - placeholder="Owner Address*" - text="Owner Address" - /> - - - {index > 0 && Delete} - - - ) - })} - - - - - - ) - } -} - -const SafeOwnersForm = withStyles(styles)(SafeOwners) - -const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors }: Object) => ( - - - - - -) - -export default SafeOwnersPage diff --git a/src/routes/open/components/SafeThresholdForm/index.jsx b/src/routes/open/components/SafeThresholdForm/index.jsx deleted file mode 100644 index 9571525f..00000000 --- a/src/routes/open/components/SafeThresholdForm/index.jsx +++ /dev/null @@ -1,91 +0,0 @@ -// @flow -import * as React from 'react' -import { withStyles } from '@material-ui/core/styles' -import MenuItem from '@material-ui/core/MenuItem' -import Field from '~/components/forms/Field' -import SelectField from '~/components/forms/SelectField' -import { - composeValidators, minValue, mustBeInteger, required, -} from '~/components/forms/validator' -import Block from '~/components/layout/Block' -import Row from '~/components/layout/Row' -import Col from '~/components/layout/Col' -import Paragraph from '~/components/layout/Paragraph' -import OpenPaper from '~/components/Stepper/OpenPaper' -import { FIELD_CONFIRMATIONS, getNumOwnersFrom } from '~/routes/open/components/fields' -import { md } from '~/theme/variables' - -type Props = { - classes: Object, - values: Object, -} - -const styles = () => ({ - owners: { - paddingLeft: md, - }, -}) - -export const CONFIRMATIONS_ERROR = 'Number of confirmations can not be higher than the number of owners' - -export const safeFieldsValidation = (values: Object) => { - const errors = {} - - const numOwners = getNumOwnersFrom(values) - if (numOwners < Number.parseInt(values[FIELD_CONFIRMATIONS], 10)) { - errors[FIELD_CONFIRMATIONS] = CONFIRMATIONS_ERROR - } - - return errors -} - -const SafeThreshold = ({ classes, values }: Props) => { - const numOwners = getNumOwnersFrom(values) - - return ( - - - - Any transaction requires the confirmation of: - - - - - - {[...Array(Number(numOwners))].map((x, index) => ( - - {index + 1} - - ))} - - - - - out of - {' '} - {numOwners} - {' '} - owner(s) - - - - - ) -} - -const SafeThresholdForm = withStyles(styles)(SafeThreshold) - -const SafeOwnersPage = () => (controls: React.Node, { values }: Object) => ( - - - - - -) - -export default SafeOwnersPage diff --git a/src/routes/open/components/fields.js b/src/routes/open/components/fields.js index 897eb4e4..fa280c90 100644 --- a/src/routes/open/components/fields.js +++ b/src/routes/open/components/fields.js @@ -9,7 +9,7 @@ export const getOwnerAddressBy = (index: number) => `owner${index}Address` export const getNumOwnersFrom = (values: Object) => { const accounts = Object.keys(values) .sort() - .filter(key => /^owner\d+Name$/.test(key)) + .filter(key => /^owner\d+Address$/.test(key) && !!values[key]) return accounts.length } diff --git a/src/routes/safe/components/Balances/Receive/index.jsx b/src/routes/safe/components/Balances/Receive/index.jsx index 841f6f2a..50c3300a 100644 --- a/src/routes/safe/components/Balances/Receive/index.jsx +++ b/src/routes/safe/components/Balances/Receive/index.jsx @@ -4,8 +4,8 @@ import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import IconButton from '@material-ui/core/IconButton' import OpenInNew from '@material-ui/icons/OpenInNew' -import Link from '~/components/layout/Link' import QRCode from 'qrcode.react' +import Link from '~/components/layout/Link' import Paragraph from '~/components/layout/Paragraph' import Identicon from '~/components/Identicon' import Button from '~/components/layout/Button' diff --git a/src/routes/safe/components/SendToken/ReviewTx/index.jsx b/src/routes/safe/components/SendToken/ReviewTx/index.jsx deleted file mode 100644 index e2a41932..00000000 --- a/src/routes/safe/components/SendToken/ReviewTx/index.jsx +++ /dev/null @@ -1,39 +0,0 @@ -// @flow -import * as React from 'react' -import CircularProgress from '@material-ui/core/CircularProgress' -import Block from '~/components/layout/Block' -import Bold from '~/components/layout/Bold' -import OpenPaper from '~/components/Stepper/OpenPaper' -import Heading from '~/components/layout/Heading' -import Paragraph from '~/components/layout/Paragraph' -import { TKN_DESTINATION_PARAM, TKN_VALUE_PARAM } from '~/routes/safe/components/SendToken/SendTokenForm/index' - -type FormProps = { - values: Object, - submitting: boolean, -} - -type Props = { - symbol: string, -} - -const spinnerStyle = { - minHeight: '50px', -} - -const ReviewTx = ({ symbol }: Props) => (controls: React.Node, { values, submitting }: FormProps) => ( - - Review the move token funds - - Destination: - {' '} - {values[TKN_DESTINATION_PARAM]} - - - {`Amount to transfer: ${values[TKN_VALUE_PARAM]} ${symbol}`} - - {submitting && } - -) - -export default ReviewTx diff --git a/src/routes/safe/components/SendToken/SendTokenForm/index.jsx b/src/routes/safe/components/SendToken/SendTokenForm/index.jsx deleted file mode 100644 index 1095a7fe..00000000 --- a/src/routes/safe/components/SendToken/SendTokenForm/index.jsx +++ /dev/null @@ -1,53 +0,0 @@ -// @flow -import * as React from 'react' -import Field from '~/components/forms/Field' -import TextField from '~/components/forms/TextField' -import { - composeValidators, inLimit, mustBeFloat, required, greaterThan, mustBeEthereumAddress, -} from '~/components/forms/validator' -import Block from '~/components/layout/Block' -import OpenPaper from '~/components/Stepper/OpenPaper' -import Heading from '~/components/layout/Heading' - -export const CONFIRMATIONS_ERROR = 'Number of confirmations can not be higher than the number of owners' - -export const TKN_DESTINATION_PARAM = 'tknDestination' -export const TKN_VALUE_PARAM = 'tknValue' - -type Props = { - funds: string, - symbol: string, -} - -const SendTokenForm = ({ funds, symbol }: Props) => (controls: React.Node) => ( - - - Send tokens Transaction - - - {`Available tokens: ${funds} ${symbol}`} - - - - - - - - -) - -export default SendTokenForm diff --git a/src/routes/safe/components/SendToken/actions.js b/src/routes/safe/components/SendToken/actions.js deleted file mode 100644 index 681ad469..00000000 --- a/src/routes/safe/components/SendToken/actions.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' - -export type Actions = { - fetchTransactions: typeof fetchTransactions, -} - -export default { - fetchTransactions, -} diff --git a/src/routes/safe/components/SendToken/index.jsx b/src/routes/safe/components/SendToken/index.jsx deleted file mode 100644 index 84899436..00000000 --- a/src/routes/safe/components/SendToken/index.jsx +++ /dev/null @@ -1,119 +0,0 @@ -// @flow -import * as React from 'react' -import { BigNumber } from 'bignumber.js' -import { connect } from 'react-redux' -import Stepper from '~/components/Stepper' -import { sleep } from '~/utils/timer' -import { type Safe } from '~/routes/safe/store/models/safe' -import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens' -import { type Token } from '~/logic/tokens/store/model/token' -import { isEther } from '~/logic/tokens/utils/tokenHelpers' -import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' -import { toNative } from '~/logic/wallets/tokens' -import { createTransaction } from '~/logic/safe/safeFrontendOperations' -import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' -import actions, { type Actions } from './actions' -import selector, { type SelectorProps } from './selector' -import SendTokenForm, { TKN_DESTINATION_PARAM, TKN_VALUE_PARAM } from './SendTokenForm' -import ReviewTx from './ReviewTx' - -const getSteps = () => ['Fill Move Token form', 'Review Move Token form'] - -type Props = SelectorProps & - Actions & { - safe: Safe, - token: Token, - onReset: () => void, - } - -type State = { - done: boolean, -} - -export const SEE_TXS_BUTTON_TEXT = 'VISIT TXS' - -const getTransferData = async (tokenAddress: string, to: string, amount: BigNumber) => { - const StandardToken = await getStandardTokenContract() - const myToken = await StandardToken.at(tokenAddress) - - return myToken.contract.transfer(to, amount).encodeABI() -} - -const processTokenTransfer = async (safe: Safe, token: Token, to: string, amount: string, userAddress: string) => { - const safeAddress = safe.get('address') - const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) - const nonce = await gnosisSafe.nonce() - const symbol = token.get('symbol') - const name = `Send ${amount} ${symbol} to ${to}` - const value = isEther(symbol) ? amount : '0' - const tokenAddress = token.get('address') - const destination = isEther(symbol) ? to : tokenAddress - const data = isEther(symbol) - ? EMPTY_DATA - : await getTransferData(tokenAddress, to, toNative(amount, token.get('decimals'))) - - return createTransaction(safe, name, destination, value, nonce, userAddress, data) -} - -class SendToken extends React.Component { - state = { - done: false, - } - - onTransaction = async (values: Object) => { - try { - const { - safe, token, userAddress, fetchTransactions, - } = this.props - - const amount = values[TKN_VALUE_PARAM] - const destination = values[TKN_DESTINATION_PARAM] - - await processTokenTransfer(safe, token, destination, amount, userAddress) - await sleep(1500) - fetchTransactions(safe.get('address')) - this.setState({ done: true }) - } catch (error) { - this.setState({ done: false }) - // eslint-disable-next-line - console.log('Error while moving ERC20 token funds ' + error) - } - } - - onReset = () => { - const { onReset } = this.props - - this.setState({ done: false }) - onReset() // This is for show the TX list component - } - - render() { - const { done } = this.state - const { token } = this.props - const steps = getSteps() - const finishedButton = - const symbol = token.get('symbol') - - return ( - - - - {SendTokenForm} - - {ReviewTx} - - - ) - } -} - -export default connect( - selector, - actions, -)(SendToken) diff --git a/src/routes/safe/components/SendToken/selector.js b/src/routes/safe/components/SendToken/selector.js deleted file mode 100644 index 8701f74b..00000000 --- a/src/routes/safe/components/SendToken/selector.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -import { createStructuredSelector } from 'reselect' -import { userAccountSelector } from '~/logic/wallets/store/selectors' - -export type SelectorProps = { - userAddress: typeof userAccountSelector, -} - -export default createStructuredSelector({ - userAddress: userAccountSelector, -}) diff --git a/src/routes/safe/components/Threshold/Review/index.jsx b/src/routes/safe/components/Threshold/Review/index.jsx deleted file mode 100644 index 459ca795..00000000 --- a/src/routes/safe/components/Threshold/Review/index.jsx +++ /dev/null @@ -1,34 +0,0 @@ -// @flow -import * as React from 'react' -import CircularProgress from '@material-ui/core/CircularProgress' -import Block from '~/components/layout/Block' -import Bold from '~/components/layout/Bold' -import OpenPaper from '~/components/Stepper/OpenPaper' -import Heading from '~/components/layout/Heading' -import Paragraph from '~/components/layout/Paragraph' -import { THRESHOLD_PARAM } from '~/routes/safe/components/Threshold/ThresholdForm' - -type FormProps = { - values: Object, - submitting: boolean, -} - -const spinnerStyle = { - minHeight: '50px', -} - -const Review = () => (controls: React.Node, { values, submitting }: FormProps) => ( - - Review the Threshold operation - - The new threshold will be: - {' '} - {values[THRESHOLD_PARAM]} - - - { submitting && } - - -) - -export default Review diff --git a/src/routes/safe/components/Threshold/ThresholdForm/index.jsx b/src/routes/safe/components/Threshold/ThresholdForm/index.jsx deleted file mode 100644 index 9c6b14b5..00000000 --- a/src/routes/safe/components/Threshold/ThresholdForm/index.jsx +++ /dev/null @@ -1,46 +0,0 @@ -// @flow -import * as React from 'react' -import Block from '~/components/layout/Block' -import Heading from '~/components/layout/Heading' -import OpenPaper from '~/components/Stepper/OpenPaper' -import Field from '~/components/forms/Field' -import TextField from '~/components/forms/TextField' -import { - composeValidators, minValue, maxValue, mustBeInteger, required, -} from '~/components/forms/validator' -import { type Safe } from '~/routes/safe/store/models/safe' - -export const THRESHOLD_PARAM = 'threshold' - -type ThresholdProps = { - numOwners: number, - safe: Safe, -} - -const ThresholdForm = ({ numOwners, safe }: ThresholdProps) => (controls: React.Node) => ( - - - {'Change safe\'s threshold'} - - - {`Safe's owners: ${numOwners} and Safe's threshold: ${safe.get('threshold')}`} - - - - - -) - -export default ThresholdForm diff --git a/src/routes/safe/components/Threshold/actions.js b/src/routes/safe/components/Threshold/actions.js deleted file mode 100644 index 681ad469..00000000 --- a/src/routes/safe/components/Threshold/actions.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' - -export type Actions = { - fetchTransactions: typeof fetchTransactions, -} - -export default { - fetchTransactions, -} diff --git a/src/routes/safe/components/Threshold/index.jsx b/src/routes/safe/components/Threshold/index.jsx deleted file mode 100644 index 1f698aa2..00000000 --- a/src/routes/safe/components/Threshold/index.jsx +++ /dev/null @@ -1,86 +0,0 @@ -// @flow -import * as React from 'react' -import Stepper from '~/components/Stepper' -import { connect } from 'react-redux' -import { createTransaction } from '~/logic/safe/safeFrontendOperations' -import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' -import { type Safe } from '~/routes/safe/store/models/safe' -import ThresholdForm, { THRESHOLD_PARAM } from './ThresholdForm' -import selector, { type SelectorProps } from './selector' -import actions, { type Actions } from './actions' -import Review from './Review' - -type Props = SelectorProps & Actions & { - numOwners: number, - safe: Safe, - onReset: () => void, -} - -const getSteps = () => [ - 'Fill Change threshold Form', 'Review change threshold operation', -] - -type State = { - done: boolean, -} - -export const CHANGE_THRESHOLD_RESET_BUTTON_TEXT = 'SEE TXs' - -class Threshold extends React.PureComponent { - state = { - done: false, - } - - onThreshold = async (values: Object) => { - try { - const { safe, userAddress, fetchTransactions } = this.props // , fetchThreshold } = this.props - const newThreshold = values[THRESHOLD_PARAM] - const safeAddress = safe.get('address') - const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) - const nonce = await gnosisSafe.nonce() - const data = gnosisSafe.contract.changeThreshold(newThreshold).encodeABI() - await createTransaction(safe, `Change Safe's threshold [${nonce}]`, safeAddress, '0', nonce, userAddress, data) - await fetchTransactions(safeAddress) - this.setState({ done: true }) - } catch (error) { - this.setState({ done: false }) - // eslint-disable-next-line - console.log('Error while changing threshold ' + error) - } - } - - onReset = () => { - const { onReset } = this.props - this.setState({ done: false }) - - onReset() - } - - render() { - const { numOwners, safe } = this.props - const { done } = this.state - const steps = getSteps() - const finishedButton = - - return ( - - - - { ThresholdForm } - - - { Review } - - - - ) - } -} - -export default connect(selector, actions)(Threshold) diff --git a/src/routes/safe/components/Threshold/selector.js b/src/routes/safe/components/Threshold/selector.js deleted file mode 100644 index 8701f74b..00000000 --- a/src/routes/safe/components/Threshold/selector.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -import { createStructuredSelector } from 'reselect' -import { userAccountSelector } from '~/logic/wallets/store/selectors' - -export type SelectorProps = { - userAddress: typeof userAccountSelector, -} - -export default createStructuredSelector({ - userAddress: userAccountSelector, -}) diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/TxDescription/utils.js b/src/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/TxDescription/utils.js index 7bb034a8..dd9744da 100644 --- a/src/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/TxDescription/utils.js +++ b/src/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/TxDescription/utils.js @@ -2,9 +2,6 @@ import { type Transaction } from '~/routes/safe/store/models/transaction' import { getWeb3 } from '~/logic/wallets/getWeb3' -const web3 = getWeb3() -const { toBN, fromWei } = web3.utils - type DecodedTxData = { recipient: string, value?: string, @@ -16,6 +13,9 @@ type DecodedTxData = { } export const getTxData = (tx: Transaction): DecodedTxData => { + const web3 = getWeb3() + const { toBN, fromWei } = web3.utils + const txData = {} if (tx.isTokenTransfer && tx.decodedParams) { diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/columns.js b/src/routes/safe/components/TransactionsNew/TxsTable/columns.js index 1db556a0..6902b8b4 100644 --- a/src/routes/safe/components/TransactionsNew/TxsTable/columns.js +++ b/src/routes/safe/components/TransactionsNew/TxsTable/columns.js @@ -14,9 +14,6 @@ export const TX_TABLE_STATUS_ID = 'status' export const TX_TABLE_RAW_TX_ID = 'tx' export const TX_TABLE_EXPAND_ICON = 'expand' -const web3 = getWeb3() -const { toBN, fromWei } = web3.utils - type TxData = { nonce: number, type: string, @@ -29,6 +26,9 @@ type TxData = { export const formatDate = (date: Date): string => format(date, 'MMM D, YYYY - HH:mm:ss') export const getTxAmount = (tx: Transaction) => { + const web3 = getWeb3() + const { toBN, fromWei } = web3.utils + let txAmount = 'n/a' if (tx.isTokenTransfer && tx.decodedParams) { diff --git a/src/test/safe.dom.create.test.js b/src/test/safe.dom.create.test.js index 971c9485..22f5d2d9 100644 --- a/src/test/safe.dom.create.test.js +++ b/src/test/safe.dom.create.test.js @@ -4,7 +4,7 @@ import { type Store } from 'redux' import { render, fireEvent, cleanup } from '@testing-library/react' import { Provider } from 'react-redux' import { ConnectedRouter } from 'connected-react-router' -import { ADD_OWNER_BUTTON } from '~/routes/open/components/SafeOwnersForm' +import { ADD_OWNER_BUTTON } from '~/routes/open/components/SafeOwnersConfirmationsForm' import Open from '~/routes/open/container/Open' import { aNewStore, history, type GlobalState } from '~/store' import { sleep } from '~/utils/timer' @@ -80,14 +80,12 @@ const deploySafe = async (createSafeForm: any, threshold: number, numOwners: num fireEvent.change(ownerNameInput, { target: { value: `Owner ${i + 1}` } }) fireEvent.change(ownerAddressInput, { target: { value: accounts[i] } }) } - fireEvent.submit(form) - await sleep(600) // Fill Threshold // The test is fragile here, MUI select btn is hard to find - const thresholdSelect = createSafeForm.getAllByRole('button')[1] - + const thresholdSelect = createSafeForm.getAllByRole('button')[2] fireEvent.click(thresholdSelect) + const thresholdOptions = createSafeForm.getAllByRole('option') fireEvent.click(thresholdOptions[numOwners - 1]) fireEvent.submit(form) diff --git a/yarn.lock b/yarn.lock index 1fb1484f..4ec40ded 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10658,6 +10658,11 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jsqr@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/jsqr/-/jsqr-1.2.0.tgz#f93fc65fa7d1ded78b1bcb020fa044352b04261a" + integrity sha512-wKcQS9QC2VHGk7aphWCp1RrFyC0CM6fMgC5prZZ2KV/Lk6OKNoCod9IR6bao+yx3KPY0gZFC5dc+h+KFzCI0Wg== + jss-plugin-camel-case@10.0.0-alpha.17: version "10.0.0-alpha.17" resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.0.0-alpha.17.tgz#6f7c9d9742e349bb061e53cd9b1c3cb006169a67" @@ -13161,7 +13166,7 @@ postcss-minify-selectors@^4.0.2: postcss "^7.0.0" postcss-selector-parser "^3.0.0" -postcss-mixins@^6.2.0: +postcss-mixins@6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.2.2.tgz#3acea63271e2c75db62fb80bc1c29e1a609a4742" integrity sha512-QqEZamiAMguYR6d2h73XXEHZgkxs03PlbU0PqgqtdCnbRlMLFNQgsfL/Td0rjIe2SwpLXOQyB9uoiLWa4GR7tg== @@ -14109,6 +14114,15 @@ react-popper@^1.3.3: typed-styles "^0.0.7" warning "^4.0.2" +react-qr-reader@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-2.2.1.tgz#dc89046d1c1a1da837a683dd970de5926817d55b" + integrity sha512-EL5JEj53u2yAOgtpAKAVBzD/SiKWn0Bl7AZy6ZrSf1lub7xHwtaXe6XSx36Wbhl1VMGmvmrwYMRwO1aSCT2fwA== + dependencies: + jsqr "^1.2.0" + prop-types "^15.7.2" + webrtc-adapter "^7.2.1" + react-redux@7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2" @@ -14980,6 +14994,13 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +rtcpeerconnection-shim@^1.2.15: + version "1.2.15" + resolved "https://registry.yarnpkg.com/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz#e7cc189a81b435324c4949aa3dfb51888684b243" + integrity sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw== + dependencies: + sdp "^2.6.0" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -15172,6 +15193,11 @@ scryptsy@^1.2.1: dependencies: pbkdf2 "^3.0.3" +sdp@^2.6.0, sdp@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.9.0.tgz#2eed2d9c0b26c81ff87593107895c68d6fb9a0a6" + integrity sha512-XAVZQO4qsfzVTHorF49zCpkdxiGmPNjA8ps8RcJGtGP3QJ/A8I9/SVg/QnkAFDMXIyGbHZBBFwYBw6WdnhT96w== + seamless-immutable@^7.1.3: version "7.1.4" resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8" @@ -18958,6 +18984,14 @@ webpack@^4.33.0: watchpack "^1.5.0" webpack-sources "^1.3.0" +webrtc-adapter@^7.2.1: + version "7.2.8" + resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-7.2.8.tgz#1373fa874559c655aa713830c2836511588d77ab" + integrity sha512-d/rZVIIqqPqu/1I9rabhI+hmVhNtT+MoJk0eipCJasiVM9L9ZOTBoVhZmtC/naB4G8GTvnCaassrDz5IqWZP6w== + dependencies: + rtcpeerconnection-shim "^1.2.15" + sdp "^2.9.0" + websocket-driver@>=0.5.1: version "0.7.3" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" @@ -18972,9 +19006,9 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== -websocket@1.0.26, "websocket@github:frozeman/WebSocket-Node#browserifyCompatible": +websocket@1.0.26, "websocket@git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible": version "1.0.26" - resolved "https://codeload.github.com/frozeman/WebSocket-Node/tar.gz/6c72925e3f8aaaea8dc8450f97627e85263999f2" + resolved "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2" dependencies: debug "^2.2.0" nan "^2.3.3" @@ -18992,9 +19026,9 @@ websocket@^1.0.28: typedarray-to-buffer "^3.1.5" yaeti "^0.0.6" -"websocket@git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible": +"websocket@github:frozeman/WebSocket-Node#browserifyCompatible": version "1.0.26" - resolved "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2" + resolved "https://codeload.github.com/frozeman/WebSocket-Node/tar.gz/6c72925e3f8aaaea8dc8450f97627e85263999f2" dependencies: debug "^2.2.0" nan "^2.3.3"