Merge branch 'development' of github.com:gnosis/safe-react into 125-ens

This commit is contained in:
Mikhail Mikheev 2019-07-26 16:06:27 +04:00
commit 2beb8afc29
33 changed files with 682 additions and 912 deletions

View File

@ -53,6 +53,7 @@
"react-final-form-listeners": "^1.0.2", "react-final-form-listeners": "^1.0.2",
"react-hot-loader": "4.12.9", "react-hot-loader": "4.12.9",
"react-infinite-scroll-component": "^4.5.2", "react-infinite-scroll-component": "^4.5.2",
"react-qr-reader": "^2.2.1",
"react-redux": "7.1.0", "react-redux": "7.1.0",
"react-router-dom": "^5.0.1", "react-router-dom": "^5.0.1",
"recompose": "^0.30.0", "recompose": "^0.30.0",
@ -123,7 +124,7 @@
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
"mini-css-extract-plugin": "0.8.0", "mini-css-extract-plugin": "0.8.0",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"postcss-mixins": "^6.2.0", "postcss-mixins": "6.2.2",
"postcss-simple-vars": "^5.0.2", "postcss-simple-vars": "^5.0.2",
"pre-commit": "^1.2.2", "pre-commit": "^1.2.2",
"prettier-eslint-cli": "5.0.0", "prettier-eslint-cli": "5.0.0",

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 55.2 (78181) - https://sketchapp.com -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Icon-/-QR-Icon" transform="translate(-11.000000, -11.000000)" fill="#a2a8ba">
<g id="QR-Icon" transform="translate(2.000000, 2.000000)">
<g id="Group" transform="translate(9.000000, 9.000000)">
<path d="M2,2 L2,4 L4,4 L4,2 L2,2 Z M6,0 L6,6 L0,6 L0,0 L6,0 Z" id="Rectangle" fill-rule="nonzero"></path>
<path d="M2,12 L2,14 L4,14 L4,12 L2,12 Z M6,10 L6,16 L0,16 L0,10 L6,10 Z" id="Rectangle-Copy-2" fill-rule="nonzero"></path>
<path d="M12,2 L12,4 L14,4 L14,2 L12,2 Z M16,0 L16,6 L10,6 L10,0 L16,0 Z" id="Rectangle-Copy" fill-rule="nonzero"></path>
<rect id="Rectangle" x="7" y="2" width="2" height="4"></rect>
<path d="M7,9 L5,9 L5,7 L7,7 L9,7 L9,11 L7,11 L7,9 Z" id="Combined-Shape"></path>
<rect id="Rectangle-Copy-4" x="0" y="7" width="2" height="2"></rect>
<rect id="Rectangle-Copy-5" x="10" y="7" width="2" height="2"></rect>
<rect id="Rectangle-Copy-6" x="14" y="7" width="2" height="2"></rect>
<rect id="Rectangle-Copy-7" x="12" y="9" width="2" height="2"></rect>
<rect id="Rectangle-Copy-10" x="12" y="14" width="2" height="2"></rect>
<path d="M9,12 L10,12 L10,11 L12,11 L12,14 L10,14 L9,14 L9,16 L7,16 L7,12 L9,12 Z" id="Combined-Shape"></path>
<rect id="Rectangle-Copy-9" x="14" y="11" width="2" height="3"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import Page from '~/components/layout/Page'
import CircularProgress from '@material-ui/core/CircularProgress' import CircularProgress from '@material-ui/core/CircularProgress'
import Page from '~/components/layout/Page'
const centerStyle = { const centerStyle = {
margin: 'auto 0', margin: 'auto 0',

View File

@ -1,16 +1,17 @@
// @flow // @flow
import * as React from 'react'
import Stepper from '@material-ui/core/Stepper' import Stepper from '@material-ui/core/Stepper'
import FormStep from '@material-ui/core/Step' import FormStep from '@material-ui/core/Step'
import StepLabel from '@material-ui/core/StepLabel' import StepLabel from '@material-ui/core/StepLabel'
import StepContent from '@material-ui/core/StepContent' import StepContent from '@material-ui/core/StepContent'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import * as React from 'react'
import GnoForm from '~/components/forms/GnoForm' import GnoForm from '~/components/forms/GnoForm'
import Hairline from '~/components/layout/Hairline' import Hairline from '~/components/layout/Hairline'
import Button from '~/components/layout/Button'
import { history } from '~/store' import { history } from '~/store'
import Controls from './Controls' import Controls from './Controls'
const { useState, useEffect } = React
export { default as Step } from './Step' export { default as Step } from './Step'
type Props = { type Props = {
@ -18,20 +19,15 @@ type Props = {
onSubmit: (values: Object) => Promise<void>, onSubmit: (values: Object) => Promise<void>,
children: React.Node, children: React.Node,
classes: Object, classes: Object,
onReset?: () => void,
initialValues?: Object, initialValues?: Object,
disabledWhenValidating?: boolean, disabledWhenValidating?: boolean,
mutators?: Object,
testId?: string, testId?: string,
} }
type State = {
page: number,
values: Object,
}
type PageProps = { type PageProps = {
children: Function, children: Function,
prepareNextInitialProps: (values: Object) => {}, prepareNextInitialProps?: (values: Object) => {},
} }
const transitionProps = { const transitionProps = {
@ -41,121 +37,95 @@ const transitionProps = {
}, },
} }
class GnoStepper extends React.PureComponent<Props, State> { export const StepperPage = ({ children }: PageProps) => children
static Page = ({ children }: PageProps) => children
static FinishButton = ({ const GnoStepper = (props: Props) => {
component, to, title, ...props const [page, setPage] = useState<number>(0)
}) => ( const [values, setValues] = useState<Object>({})
<Button component={component} to={to} variant="contained" color="primary" {...props}>
{title}
</Button>
)
constructor(props: Props) { useEffect(() => {
super(props) if (props.initialValues) {
setValues(props.initialValues)
this.state = {
page: 0,
values: props.initialValues || {},
} }
}, [])
const getPageProps = (pages: React.Node): PageProps => React.Children.toArray(pages)[page].props
const updateInitialProps = (newInitialProps) => {
setValues(newInitialProps)
} }
onReset = () => { const getActivePageFrom = (pages: React.Node) => {
const { onReset, initialValues } = this.props const activePageProps = getPageProps(pages)
if (onReset) { const { children, ...restProps } = activePageProps
onReset()
return children({ ...restProps, updateInitialProps })
} }
this.setState(() => ({ const validate = (valuesToValidate: Object) => {
page: 0, const { children } = props
values: initialValues || {},
}))
}
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 activePage = React.Children.toArray(children)[page] 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 next = async (formValues: Object) => {
const { children } = this.props const { children } = props
const activePageProps = this.getPageProps(children) const activePageProps = getPageProps(children)
const { prepareNextInitialProps } = activePageProps const { prepareNextInitialProps } = activePageProps
let pageInitialProps let pageInitialProps
if (prepareNextInitialProps) { if (prepareNextInitialProps) {
pageInitialProps = await prepareNextInitialProps(values) pageInitialProps = await prepareNextInitialProps(formValues)
} }
const finalValues = { ...values, ...pageInitialProps } const finalValues = { ...formValues, ...pageInitialProps }
this.setState(state => ({
page: Math.min(state.page + 1, React.Children.count(children) - 1), setValues(finalValues)
values: finalValues, setPage(Math.min(page + 1, React.Children.count(children) - 1))
}))
} }
previous = () => { const previous = () => {
const { page } = this.state
const firstPage = page === 0 const firstPage = page === 0
if (firstPage) { if (firstPage) {
return history.goBack() return history.goBack()
} }
return this.setState(state => ({ return setPage(Math.max(page - 1, 0))
page: Math.max(state.page - 1, 0),
}))
} }
handleSubmit = async (values: Object) => { const handleSubmit = async (formValues: Object) => {
const { children, onSubmit } = this.props const { children, onSubmit } = props
const { page } = this.state
const isLastPage = page === React.Children.count(children) - 1 const isLastPage = page === React.Children.count(children) - 1
if (isLastPage) { if (isLastPage) {
return onSubmit(values) return onSubmit(formValues)
} }
return this.next(values) return next(formValues)
} }
isLastPage = (page) => { const isLastPage = (pageNumber) => {
const { steps } = this.props const { steps } = props
return page === steps.length - 1 return pageNumber === steps.length - 1
} }
render() {
const { const {
steps, children, classes, disabledWhenValidating = false, testId, steps, children, classes, disabledWhenValidating = false, testId, mutators,
} = this.props } = props
const { page, values } = this.state const activePage = getActivePageFrom(children)
const activePage = this.getActivePageFrom(children)
const lastPage = this.isLastPage(page) const lastPage = isLastPage(page)
const penultimate = this.isLastPage(page + 1) const penultimate = isLastPage(page + 1)
return ( return (
<React.Fragment> <React.Fragment>
<GnoForm onSubmit={this.handleSubmit} initialValues={values} validation={this.validate} testId={testId}> <GnoForm
onSubmit={handleSubmit}
initialValues={values}
validation={validate}
testId={testId}
formMutators={mutators}
>
{(submitting: boolean, validating: boolean, ...rest: any) => { {(submitting: boolean, validating: boolean, ...rest: any) => {
const disabled = disabledWhenValidating ? submitting || validating : submitting const disabled = disabledWhenValidating ? submitting || validating : submitting
const controls = ( const controls = (
@ -163,7 +133,7 @@ class GnoStepper extends React.PureComponent<Props, State> {
<Hairline /> <Hairline />
<Controls <Controls
disabled={disabled} disabled={disabled}
onPrevious={this.previous} onPrevious={previous}
firstPage={page === 0} firstPage={page === 0}
lastPage={lastPage} lastPage={lastPage}
penultimate={penultimate} penultimate={penultimate}
@ -185,7 +155,6 @@ class GnoStepper extends React.PureComponent<Props, State> {
</GnoForm> </GnoForm>
</React.Fragment> </React.Fragment>
) )
}
} }
const styles = { const styles = {

View File

@ -1,10 +1,12 @@
// @flow // @flow
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import React, { PureComponent } from 'react' import * as React from 'react'
import { capitalize } from '~/utils/css' import { capitalize } from '~/utils/css'
import { type Size } from '~/theme/size' import { type Size } from '~/theme/size'
import styles from './index.scss' import styles from './index.scss'
const { PureComponent } = React
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
type Props = { type Props = {

View File

@ -2,7 +2,7 @@
import * as React from 'react' import * as React from 'react'
import ChevronLeft from '@material-ui/icons/ChevronLeft' import ChevronLeft from '@material-ui/icons/ChevronLeft'
import IconButton from '@material-ui/core/IconButton' 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 Block from '~/components/layout/Block'
import Heading from '~/components/layout/Heading' import Heading from '~/components/layout/Heading'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
@ -46,11 +46,11 @@ const Layout = ({
<Heading tag="h2">Load existing Safe</Heading> <Heading tag="h2">Load existing Safe</Heading>
</Row> </Row>
<Stepper onSubmit={onLoadSafeSubmit} steps={steps} initialValues={initialValues} testId="load-safe-form"> <Stepper onSubmit={onLoadSafeSubmit} steps={steps} initialValues={initialValues} testId="load-safe-form">
<Stepper.Page validate={safeFieldsValidation}>{DetailsForm}</Stepper.Page> <StepperPage validate={safeFieldsValidation}>{DetailsForm}</StepperPage>
<Stepper.Page network={network}>{OwnerList}</Stepper.Page> <StepperPage network={network}>{OwnerList}</StepperPage>
<Stepper.Page network={network} userAddress={userAddress}> <StepperPage network={network} userAddress={userAddress}>
{ReviewInformation} {ReviewInformation}
</Stepper.Page> </StepperPage>
</Stepper> </Stepper>
</Block> </Block>
) : ( ) : (

View File

@ -1,20 +1,19 @@
// @flow // @flow
import * as React from 'react' 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 Block from '~/components/layout/Block'
import Heading from '~/components/layout/Heading' import Heading from '~/components/layout/Heading'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import IconButton from '@material-ui/core/IconButton'
import Review from '~/routes/open/components/ReviewInformation' import Review from '~/routes/open/components/ReviewInformation'
import ChevronLeft from '@material-ui/icons/ChevronLeft'
import SafeNameField from '~/routes/open/components/SafeNameForm' import SafeNameField from '~/routes/open/components/SafeNameForm'
import SafeThresholdField, { safeFieldsValidation } from '~/routes/open/components/SafeThresholdForm' import SafeOwnersFields from '~/routes/open/components/SafeOwnersConfirmationsForm'
import SafeOwnersFields from '~/routes/open/components/SafeOwnersForm'
import { getOwnerNameBy, getOwnerAddressBy, FIELD_CONFIRMATIONS } from '~/routes/open/components/fields' import { getOwnerNameBy, getOwnerAddressBy, FIELD_CONFIRMATIONS } from '~/routes/open/components/fields'
import { history } from '~/store' import { history } from '~/store'
import { secondary } from '~/theme/variables' import { secondary } from '~/theme/variables'
const getSteps = () => ['Start', 'Owners', 'Confirmations', 'Review'] const getSteps = () => ['Start', 'Owners and confirmations', 'Review']
const initialValuesFrom = (userAccount: string) => ({ const initialValuesFrom = (userAccount: string) => ({
[getOwnerNameBy(0)]: 'My Metamask (me)', [getOwnerNameBy(0)]: 'My Metamask (me)',
@ -39,6 +38,12 @@ const back = () => {
history.goBack() history.goBack()
} }
const formMutators = {
setValue: ([field, value], state, { changeValue }) => {
changeValue(state, field, () => value)
},
}
const Layout = ({ const Layout = ({
provider, userAccount, onCallSafeContractSubmit, network, provider, userAccount, onCallSafeContractSubmit, network,
}: Props) => { }: Props) => {
@ -55,15 +60,20 @@ const Layout = ({
</IconButton> </IconButton>
<Heading tag="h2">Create New Safe</Heading> <Heading tag="h2">Create New Safe</Heading>
</Row> </Row>
<Stepper onSubmit={onCallSafeContractSubmit} steps={steps} initialValues={initialValues} testId="create-safe-form"> <Stepper
<Stepper.Page>{SafeNameField}</Stepper.Page> onSubmit={onCallSafeContractSubmit}
<Stepper.Page>{SafeOwnersFields}</Stepper.Page> steps={steps}
<Stepper.Page validate={safeFieldsValidation}>{SafeThresholdField}</Stepper.Page> initialValues={initialValues}
<Stepper.Page network={network}>{Review}</Stepper.Page> mutators={formMutators}
testId="create-safe-form"
>
<StepperPage>{SafeNameField}</StepperPage>
<StepperPage>{SafeOwnersFields}</StepperPage>
<StepperPage network={network}>{Review}</StepperPage>
</Stepper> </Stepper>
</Block> </Block>
) : ( ) : (
<div>No metamask detected</div> <div>No web3 provider detected</div>
)} )}
</React.Fragment> </React.Fragment>
) )

View File

@ -129,7 +129,11 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
<Paragraph size="md" color="disabled" noMargin> <Paragraph size="md" color="disabled" noMargin>
{addresses[index]} {addresses[index]}
</Paragraph> </Paragraph>
<Link className={classes.open} to={getEtherScanLink('address', addresses[index], network)} target="_blank"> <Link
className={classes.open}
to={getEtherScanLink('address', addresses[index], network)}
target="_blank"
>
<OpenInNew style={openIconStyle} /> <OpenInNew style={openIconStyle} />
</Link> </Link>
</Block> </Block>
@ -143,10 +147,8 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
</Row> </Row>
<Row className={classes.info} align="center"> <Row className={classes.info} align="center">
<Paragraph noMargin color="primary" size="md"> <Paragraph noMargin color="primary" size="md">
{"You're about to create a new Safe."} You&apos;re about to create a new Safe and will have to confirm a transaction with your currently connected
</Paragraph> wallet. Make sure you have ETH in this wallet to fund this transaction.
<Paragraph noMargin color="primary" size="md">
Make sure you have enough ETH in your wallet client to fund this transaction.
</Paragraph> </Paragraph>
</Row> </Row>
</React.Fragment> </React.Fragment>

View File

@ -5,7 +5,6 @@ import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField' import TextField from '~/components/forms/TextField'
import { required } from '~/components/forms/validator' import { required } from '~/components/forms/validator'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import Row from '~/components/layout/Row'
import { FIELD_NAME } from '~/routes/open/components/fields' import { FIELD_NAME } from '~/routes/open/components/fields'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import OpenPaper from '~/components/Stepper/OpenPaper' import OpenPaper from '~/components/Stepper/OpenPaper'
@ -35,10 +34,25 @@ const styles = () => ({
const SafeName = ({ classes }: Props) => ( const SafeName = ({ classes }: Props) => (
<React.Fragment> <React.Fragment>
<Block margin="lg">
<Paragraph noMargin size="md" color="primary">
You are about to create a new Gnosis Safe wallet with one or more owners. First, let&apos;s give your new wallet
a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
</Paragraph>
</Block>
<Block margin="lg" className={classes.root}>
<Field
name={FIELD_NAME}
component={TextField}
type="text"
validate={required}
placeholder="Name of the new Safe"
text="Safe name"
/>
</Block>
<Block margin="lg"> <Block margin="lg">
<Paragraph noMargin size="md" color="primary" className={classes.links}> <Paragraph noMargin size="md" color="primary" className={classes.links}>
This setup will create a Safe with one or more owners. Optionally give the Safe a local name. By continuing you By continuing you consent with the
consent with the
{' '} {' '}
<a rel="noopener noreferrer" href="https://safe.gnosis.io/terms" target="_blank"> <a rel="noopener noreferrer" href="https://safe.gnosis.io/terms" target="_blank">
terms of use terms of use
@ -49,35 +63,10 @@ const SafeName = ({ classes }: Props) => (
<a rel="noopener noreferrer" href="https://safe.gnosis.io/privacy" target="_blank"> <a rel="noopener noreferrer" href="https://safe.gnosis.io/privacy" target="_blank">
privacy policy privacy policy
</a> </a>
. . 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.
</Paragraph> </Paragraph>
</Block> </Block>
<Row margin="md" className={classes.text}>
<Paragraph noMargin className={classes.dot} color="secondary">
&#9679;
</Paragraph>
<Paragraph noMargin size="md" color="primary" weight="bolder">
I understand that my funds are held securely in my Safe. They cannot be accessed by Gnosis.
</Paragraph>
</Row>
<Row margin="md">
<Paragraph noMargin className={classes.dot} color="secondary">
&#9679;
</Paragraph>
<Paragraph noMargin size="md" color="primary" weight="bolder">
My Safe is a smart contract on the Ethereum blockchain.
</Paragraph>
</Row>
<Block margin="lg" className={classes.root}>
<Field
name={FIELD_NAME}
component={TextField}
type="text"
validate={required}
placeholder="Name of the new Safe"
text="Safe name"
/>
</Block>
</React.Fragment> </React.Fragment>
) )

View File

@ -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 (
<Modal title="Receive Tokens" description="Receive Tokens Form" handleClose={onClose} open={isOpen}>
<Row align="center" grow className={classes.heading}>
<Paragraph className={classes.manage} weight="bolder" noMargin>
Scan QR
</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.close} />
</IconButton>
</Row>
<Hairline />
<Col layout="column" middle="xs" className={classes.detailsContainer}>
{hasWebcam === null ? (
<Block align="center" className={classes.loaderContainer}>
<CircularProgress />
</Block>
) : (
<QrReader
ref={scannerRef}
legacyMode={!hasWebcam}
onScan={(data) => {
if (data) onScan(data)
}}
onError={(err) => {
console.error(err)
}}
style={{ width: '400px', height: '400px' }}
/>
)}
</Col>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button
color="secondary"
className={classes.button}
minHeight={42}
minWidth={140}
onClick={onClose}
variant="contained"
>
Close
</Button>
<Button
color="primary"
className={classes.button}
minWidth={140}
minHeight={42}
onClick={() => {
if (hasWebcam) {
setHasWebcam(false)
} else {
openImageDialog()
}
}}
variant="contained"
>
Upload an image
</Button>
</Row>
</Modal>
)
}
export default withStyles(styles)(ScanQRModal)

View File

@ -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,
},
},
})

View File

@ -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()
},
)

View File

@ -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<number>(validOwners)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [scanQrForOwnerName, setScanQrForOwnerName] = useState<string | null>(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 (
<React.Fragment>
<Block className={classes.title}>
<Paragraph noMargin size="md" color="primary">
Specify the owners of the Safe.
</Paragraph>
</Block>
<Hairline />
<Row className={classes.header}>
<Col xs={4}>NAME</Col>
<Col xs={8}>ADDRESS</Col>
</Row>
<Hairline />
<Block margin="md" padding="md">
{[...Array(Number(numOwners))].map((x, index) => {
const addressName = getOwnerAddressBy(index)
return (
<Row key={`owner${index}`} className={classes.owner}>
<Col xs={4}>
<Field
className={classes.name}
name={getOwnerNameBy(index)}
component={TextField}
type="text"
validate={required}
placeholder="Owner Name*"
text="Owner Name"
/>
</Col>
<Col xs={6}>
<Field
name={addressName}
component={TextField}
inputAdornment={
noErrorsOn(addressName, errors) && {
endAdornment: (
<InputAdornment position="end">
<CheckCircle className={classes.check} />
</InputAdornment>
),
}
}
type="text"
validate={getAddressValidators(otherAccounts, index)}
placeholder="Owner Address*"
text="Owner Address"
/>
</Col>
<Col xs={1} center="xs" middle="xs" className={classes.remove}>
<Img
src={QRIcon}
height={20}
alt="Scan QR"
onClick={() => {
openQrModal(addressName)
}}
/>
</Col>
<Col xs={1} center="xs" middle="xs" className={classes.remove}>
{index > 0 && <Img src={trash} height={20} alt="Delete" onClick={onRemoveRow(index)} />}
</Col>
</Row>
)
})}
</Block>
<Row align="center" grow className={classes.add} margin="xl">
<Button color="secondary" onClick={onAddOwner} data-testid="add-owner-btn">
<Paragraph weight="bold" size="md" noMargin>
{ADD_OWNER_BUTTON}
</Paragraph>
</Button>
</Row>
<Block margin="md" padding="md" className={classes.owner}>
<Paragraph size="md" color="primary">
Any transaction requires the confirmation of:
</Paragraph>
<Row margin="xl" align="center">
<Col xs={2}>
<Field
name={FIELD_CONFIRMATIONS}
component={SelectField}
validate={composeValidators(required, mustBeInteger, minValue(1))}
data-testid="threshold-select-input"
>
{[...Array(Number(validOwners))].map((x, index) => (
<MenuItem key={`selectOwner${index}`} value={`${index + 1}`}>
{index + 1}
</MenuItem>
))}
</Field>
</Col>
<Col xs={10}>
<Paragraph size="lg" color="primary" noMargin className={classes.owners}>
out of
{' '}
{validOwners}
{' '}
owner(s)
</Paragraph>
</Col>
</Row>
</Block>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onScan={handleScan} onClose={closeQrModal} />}
</React.Fragment>
)
}
const SafeOwnersForm = withStyles(styles)(SafeOwners)
const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors, form }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} padding={false}>
<SafeOwnersForm
otherAccounts={getAccountsFrom(values)}
errors={errors}
form={form}
updateInitialProps={updateInitialProps}
values={values}
/>
</OpenPaper>
</React.Fragment>
)
export default SafeOwnersPage

View File

@ -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,
},
})

View File

@ -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))
}

View File

@ -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<Props, State> {
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 (
<React.Fragment>
<Block className={classes.title}>
<Paragraph noMargin size="md" color="primary">
Specify the owners of the Safe.
</Paragraph>
</Block>
<Hairline />
<Row className={classes.header}>
<Col xs={4}>NAME</Col>
<Col xs={8}>ADDRESS</Col>
</Row>
<Hairline />
<Block margin="md" padding="md">
{[...Array(Number(numOwners))].map((x, index) => {
const addressName = getOwnerAddressBy(index)
return (
<Row key={`owner${index}`} className={classes.owner}>
<Col xs={4}>
<Field
className={classes.name}
name={getOwnerNameBy(index)}
component={TextField}
type="text"
validate={required}
placeholder="Owner Name*"
text="Owner Name"
/>
</Col>
<Col xs={7}>
<Field
name={addressName}
component={TextField}
inputAdornment={
noErrorsOn(addressName, errors) && {
endAdornment: (
<InputAdornment position="end">
<CheckCircle className={classes.check} />
</InputAdornment>
),
}
}
type="text"
validate={getAddressValidators(otherAccounts, index)}
placeholder="Owner Address*"
text="Owner Address"
/>
</Col>
<Col xs={1} center="xs" middle="xs" className={classes.remove}>
{index > 0 && <Img src={trash} height={20} alt="Delete" onClick={this.onRemoveRow(index)} />}
</Col>
</Row>
)
})}
</Block>
<Row align="center" grow className={classes.add} margin="xl">
<Button color="secondary" onClick={this.onAddOwner} data-testid="add-owner-btn">
<Paragraph weight="bold" size="md" noMargin>
{ADD_OWNER_BUTTON}
</Paragraph>
</Button>
</Row>
</React.Fragment>
)
}
}
const SafeOwnersForm = withStyles(styles)(SafeOwners)
const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} padding={false}>
<SafeOwnersForm
otherAccounts={getAccountsFrom(values)}
errors={errors}
updateInitialProps={updateInitialProps}
values={values}
/>
</OpenPaper>
</React.Fragment>
)
export default SafeOwnersPage

View File

@ -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 (
<React.Fragment>
<Block margin="xs">
<Paragraph noMargin size="md" color="primary" weight="bolder">
Any transaction requires the confirmation of:
</Paragraph>
</Block>
<Row margin="xl" align="center">
<Col xs={2}>
<Field
name={FIELD_CONFIRMATIONS}
component={SelectField}
validate={composeValidators(required, mustBeInteger, minValue(1))}
data-testid="threshold-select-input"
>
{[...Array(Number(numOwners))].map((x, index) => (
<MenuItem key={`selectOwner${index}`} value={`${index + 1}`}>
{index + 1}
</MenuItem>
))}
</Field>
</Col>
<Col xs={10}>
<Paragraph size="lg" color="primary" noMargin className={classes.owners}>
out of
{' '}
{numOwners}
{' '}
owner(s)
</Paragraph>
</Col>
</Row>
</React.Fragment>
)
}
const SafeThresholdForm = withStyles(styles)(SafeThreshold)
const SafeOwnersPage = () => (controls: React.Node, { values }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} container={450}>
<SafeThresholdForm values={values} />
</OpenPaper>
</React.Fragment>
)
export default SafeOwnersPage

View File

@ -9,7 +9,7 @@ export const getOwnerAddressBy = (index: number) => `owner${index}Address`
export const getNumOwnersFrom = (values: Object) => { export const getNumOwnersFrom = (values: Object) => {
const accounts = Object.keys(values) const accounts = Object.keys(values)
.sort() .sort()
.filter(key => /^owner\d+Name$/.test(key)) .filter(key => /^owner\d+Address$/.test(key) && !!values[key])
return accounts.length return accounts.length
} }

View File

@ -4,8 +4,8 @@ import { withStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import OpenInNew from '@material-ui/icons/OpenInNew' import OpenInNew from '@material-ui/icons/OpenInNew'
import Link from '~/components/layout/Link'
import QRCode from 'qrcode.react' import QRCode from 'qrcode.react'
import Link from '~/components/layout/Link'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import Identicon from '~/components/Identicon' import Identicon from '~/components/Identicon'
import Button from '~/components/layout/Button' import Button from '~/components/layout/Button'

View File

@ -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) => (
<OpenPaper controls={controls}>
<Heading tag="h2">Review the move token funds</Heading>
<Paragraph align="left">
<Bold>Destination: </Bold>
{' '}
{values[TKN_DESTINATION_PARAM]}
</Paragraph>
<Paragraph align="left">
<Bold>{`Amount to transfer: ${values[TKN_VALUE_PARAM]} ${symbol}`}</Bold>
</Paragraph>
<Block style={spinnerStyle}>{submitting && <CircularProgress size={50} />}</Block>
</OpenPaper>
)
export default ReviewTx

View File

@ -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) => (
<OpenPaper controls={controls}>
<Heading tag="h2" margin="lg">
Send tokens Transaction
</Heading>
<Heading tag="h4" margin="lg">
{`Available tokens: ${funds} ${symbol}`}
</Heading>
<Block margin="md">
<Field
name={TKN_DESTINATION_PARAM}
component={TextField}
type="text"
validate={composeValidators(required, mustBeEthereumAddress)}
placeholder="Destination*"
text="Destination"
/>
</Block>
<Block margin="md">
<Field
name={TKN_VALUE_PARAM}
component={TextField}
type="text"
validate={composeValidators(required, mustBeFloat, greaterThan(0), inLimit(Number(funds), 0, 'available balance', symbol))}
placeholder="Amount of tokens*"
text="Amount of Tokens"
/>
</Block>
</OpenPaper>
)
export default SendTokenForm

View File

@ -1,10 +0,0 @@
// @flow
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
export type Actions = {
fetchTransactions: typeof fetchTransactions,
}
export default {
fetchTransactions,
}

View File

@ -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<Props, State> {
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 = <Stepper.FinishButton title={SEE_TXS_BUTTON_TEXT} />
const symbol = token.get('symbol')
return (
<React.Fragment>
<Stepper
finishedTransaction={done}
finishedButton={finishedButton}
onSubmit={this.onTransaction}
steps={steps}
onReset={this.onReset}
>
<Stepper.Page funds={token.get('funds')} symbol={symbol}>
{SendTokenForm}
</Stepper.Page>
<Stepper.Page symbol={symbol}>{ReviewTx}</Stepper.Page>
</Stepper>
</React.Fragment>
)
}
}
export default connect(
selector,
actions,
)(SendToken)

View File

@ -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<Object, *>({
userAddress: userAccountSelector,
})

View File

@ -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) => (
<OpenPaper controls={controls}>
<Heading tag="h2">Review the Threshold operation</Heading>
<Paragraph align="left">
<Bold>The new threshold will be: </Bold>
{' '}
{values[THRESHOLD_PARAM]}
</Paragraph>
<Block style={spinnerStyle}>
{ submitting && <CircularProgress size={50} /> }
</Block>
</OpenPaper>
)
export default Review

View File

@ -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) => (
<OpenPaper controls={controls}>
<Heading tag="h2" margin="lg">
{'Change safe\'s threshold'}
</Heading>
<Heading tag="h4" margin="lg">
{`Safe's owners: ${numOwners} and Safe's threshold: ${safe.get('threshold')}`}
</Heading>
<Block margin="md">
<Field
name={THRESHOLD_PARAM}
component={TextField}
type="text"
validate={composeValidators(
required,
mustBeInteger,
minValue(1),
maxValue(numOwners),
)}
placeholder="New threshold"
text="Safe's threshold"
/>
</Block>
</OpenPaper>
)
export default ThresholdForm

View File

@ -1,10 +0,0 @@
// @flow
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
export type Actions = {
fetchTransactions: typeof fetchTransactions,
}
export default {
fetchTransactions,
}

View File

@ -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<Props, State> {
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 = <Stepper.FinishButton title={CHANGE_THRESHOLD_RESET_BUTTON_TEXT} />
return (
<React.Fragment>
<Stepper
finishedTransaction={done}
finishedButton={finishedButton}
onSubmit={this.onThreshold}
steps={steps}
onReset={this.onReset}
>
<Stepper.Page numOwners={numOwners} safe={safe}>
{ ThresholdForm }
</Stepper.Page>
<Stepper.Page>
{ Review }
</Stepper.Page>
</Stepper>
</React.Fragment>
)
}
}
export default connect(selector, actions)(Threshold)

View File

@ -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<Object, *>({
userAddress: userAccountSelector,
})

View File

@ -2,9 +2,6 @@
import { type Transaction } from '~/routes/safe/store/models/transaction' import { type Transaction } from '~/routes/safe/store/models/transaction'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
const web3 = getWeb3()
const { toBN, fromWei } = web3.utils
type DecodedTxData = { type DecodedTxData = {
recipient: string, recipient: string,
value?: string, value?: string,
@ -16,6 +13,9 @@ type DecodedTxData = {
} }
export const getTxData = (tx: Transaction): DecodedTxData => { export const getTxData = (tx: Transaction): DecodedTxData => {
const web3 = getWeb3()
const { toBN, fromWei } = web3.utils
const txData = {} const txData = {}
if (tx.isTokenTransfer && tx.decodedParams) { if (tx.isTokenTransfer && tx.decodedParams) {

View File

@ -14,9 +14,6 @@ export const TX_TABLE_STATUS_ID = 'status'
export const TX_TABLE_RAW_TX_ID = 'tx' export const TX_TABLE_RAW_TX_ID = 'tx'
export const TX_TABLE_EXPAND_ICON = 'expand' export const TX_TABLE_EXPAND_ICON = 'expand'
const web3 = getWeb3()
const { toBN, fromWei } = web3.utils
type TxData = { type TxData = {
nonce: number, nonce: number,
type: string, type: string,
@ -29,6 +26,9 @@ type TxData = {
export const formatDate = (date: Date): string => format(date, 'MMM D, YYYY - HH:mm:ss') export const formatDate = (date: Date): string => format(date, 'MMM D, YYYY - HH:mm:ss')
export const getTxAmount = (tx: Transaction) => { export const getTxAmount = (tx: Transaction) => {
const web3 = getWeb3()
const { toBN, fromWei } = web3.utils
let txAmount = 'n/a' let txAmount = 'n/a'
if (tx.isTokenTransfer && tx.decodedParams) { if (tx.isTokenTransfer && tx.decodedParams) {

View File

@ -4,7 +4,7 @@ import { type Store } from 'redux'
import { render, fireEvent, cleanup } from '@testing-library/react' import { render, fireEvent, cleanup } from '@testing-library/react'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router' 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 Open from '~/routes/open/container/Open'
import { aNewStore, history, type GlobalState } from '~/store' import { aNewStore, history, type GlobalState } from '~/store'
import { sleep } from '~/utils/timer' 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(ownerNameInput, { target: { value: `Owner ${i + 1}` } })
fireEvent.change(ownerAddressInput, { target: { value: accounts[i] } }) fireEvent.change(ownerAddressInput, { target: { value: accounts[i] } })
} }
fireEvent.submit(form)
await sleep(600)
// Fill Threshold // Fill Threshold
// The test is fragile here, MUI select btn is hard to find // 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) fireEvent.click(thresholdSelect)
const thresholdOptions = createSafeForm.getAllByRole('option') const thresholdOptions = createSafeForm.getAllByRole('option')
fireEvent.click(thresholdOptions[numOwners - 1]) fireEvent.click(thresholdOptions[numOwners - 1])
fireEvent.submit(form) fireEvent.submit(form)

View File

@ -10658,6 +10658,11 @@ jsprim@^1.2.2:
json-schema "0.2.3" json-schema "0.2.3"
verror "1.10.0" 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: jss-plugin-camel-case@10.0.0-alpha.17:
version "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" 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 "^7.0.0"
postcss-selector-parser "^3.0.0" postcss-selector-parser "^3.0.0"
postcss-mixins@^6.2.0: postcss-mixins@6.2.2:
version "6.2.2" version "6.2.2"
resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.2.2.tgz#3acea63271e2c75db62fb80bc1c29e1a609a4742" resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.2.2.tgz#3acea63271e2c75db62fb80bc1c29e1a609a4742"
integrity sha512-QqEZamiAMguYR6d2h73XXEHZgkxs03PlbU0PqgqtdCnbRlMLFNQgsfL/Td0rjIe2SwpLXOQyB9uoiLWa4GR7tg== integrity sha512-QqEZamiAMguYR6d2h73XXEHZgkxs03PlbU0PqgqtdCnbRlMLFNQgsfL/Td0rjIe2SwpLXOQyB9uoiLWa4GR7tg==
@ -14109,6 +14114,15 @@ react-popper@^1.3.3:
typed-styles "^0.0.7" typed-styles "^0.0.7"
warning "^4.0.2" 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: react-redux@7.1.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2" 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" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== 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: run-async@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
@ -15172,6 +15193,11 @@ scryptsy@^1.2.1:
dependencies: dependencies:
pbkdf2 "^3.0.3" 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: seamless-immutable@^7.1.3:
version "7.1.4" version "7.1.4"
resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8" 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" watchpack "^1.5.0"
webpack-sources "^1.3.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: websocket-driver@>=0.5.1:
version "0.7.3" version "0.7.3"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" 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" resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== 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" 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: dependencies:
debug "^2.2.0" debug "^2.2.0"
nan "^2.3.3" nan "^2.3.3"
@ -18992,9 +19026,9 @@ websocket@^1.0.28:
typedarray-to-buffer "^3.1.5" typedarray-to-buffer "^3.1.5"
yaeti "^0.0.6" yaeti "^0.0.6"
"websocket@git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible": "websocket@github:frozeman/WebSocket-Node#browserifyCompatible":
version "1.0.26" 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: dependencies:
debug "^2.2.0" debug "^2.2.0"
nan "^2.3.3" nan "^2.3.3"