Merge branch 'development' of github.com:gnosis/safe-react into 125-ens
This commit is contained in:
commit
2beb8afc29
|
@ -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",
|
||||
|
|
|
@ -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 |
|
@ -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',
|
||||
|
|
|
@ -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<void>,
|
||||
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<Props, State> {
|
||||
static Page = ({ children }: PageProps) => children
|
||||
export const StepperPage = ({ children }: PageProps) => children
|
||||
|
||||
static FinishButton = ({
|
||||
component, to, title, ...props
|
||||
}) => (
|
||||
<Button component={component} to={to} variant="contained" color="primary" {...props}>
|
||||
{title}
|
||||
</Button>
|
||||
)
|
||||
const GnoStepper = (props: Props) => {
|
||||
const [page, setPage] = useState<number>(0)
|
||||
const [values, setValues] = useState<Object>({})
|
||||
|
||||
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 (
|
||||
<React.Fragment>
|
||||
<GnoForm onSubmit={this.handleSubmit} initialValues={values} validation={this.validate} testId={testId}>
|
||||
{(submitting: boolean, validating: boolean, ...rest: any) => {
|
||||
const disabled = disabledWhenValidating ? submitting || validating : submitting
|
||||
const controls = (
|
||||
<React.Fragment>
|
||||
<Hairline />
|
||||
<Controls
|
||||
disabled={disabled}
|
||||
onPrevious={this.previous}
|
||||
firstPage={page === 0}
|
||||
lastPage={lastPage}
|
||||
penultimate={penultimate}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
const lastPage = isLastPage(page)
|
||||
const penultimate = isLastPage(page + 1)
|
||||
|
||||
return (
|
||||
<Stepper classes={{ root: classes.root }} activeStep={page} orientation="vertical">
|
||||
{steps.map(label => (
|
||||
<FormStep key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
<StepContent TransitionProps={transitionProps}>{activePage(controls, ...rest)}</StepContent>
|
||||
</FormStep>
|
||||
))}
|
||||
</Stepper>
|
||||
)
|
||||
}}
|
||||
</GnoForm>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<GnoForm
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={values}
|
||||
validation={validate}
|
||||
testId={testId}
|
||||
formMutators={mutators}
|
||||
>
|
||||
{(submitting: boolean, validating: boolean, ...rest: any) => {
|
||||
const disabled = disabledWhenValidating ? submitting || validating : submitting
|
||||
const controls = (
|
||||
<React.Fragment>
|
||||
<Hairline />
|
||||
<Controls
|
||||
disabled={disabled}
|
||||
onPrevious={previous}
|
||||
firstPage={page === 0}
|
||||
lastPage={lastPage}
|
||||
penultimate={penultimate}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
return (
|
||||
<Stepper classes={{ root: classes.root }} activeStep={page} orientation="vertical">
|
||||
{steps.map(label => (
|
||||
<FormStep key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
<StepContent TransitionProps={transitionProps}>{activePage(controls, ...rest)}</StepContent>
|
||||
</FormStep>
|
||||
))}
|
||||
</Stepper>
|
||||
)
|
||||
}}
|
||||
</GnoForm>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = ({
|
|||
<Heading tag="h2">Load existing Safe</Heading>
|
||||
</Row>
|
||||
<Stepper onSubmit={onLoadSafeSubmit} steps={steps} initialValues={initialValues} testId="load-safe-form">
|
||||
<Stepper.Page validate={safeFieldsValidation}>{DetailsForm}</Stepper.Page>
|
||||
<Stepper.Page network={network}>{OwnerList}</Stepper.Page>
|
||||
<Stepper.Page network={network} userAddress={userAddress}>
|
||||
<StepperPage validate={safeFieldsValidation}>{DetailsForm}</StepperPage>
|
||||
<StepperPage network={network}>{OwnerList}</StepperPage>
|
||||
<StepperPage network={network} userAddress={userAddress}>
|
||||
{ReviewInformation}
|
||||
</Stepper.Page>
|
||||
</StepperPage>
|
||||
</Stepper>
|
||||
</Block>
|
||||
) : (
|
||||
|
|
|
@ -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 = ({
|
|||
</IconButton>
|
||||
<Heading tag="h2">Create New Safe</Heading>
|
||||
</Row>
|
||||
<Stepper onSubmit={onCallSafeContractSubmit} steps={steps} initialValues={initialValues} testId="create-safe-form">
|
||||
<Stepper.Page>{SafeNameField}</Stepper.Page>
|
||||
<Stepper.Page>{SafeOwnersFields}</Stepper.Page>
|
||||
<Stepper.Page validate={safeFieldsValidation}>{SafeThresholdField}</Stepper.Page>
|
||||
<Stepper.Page network={network}>{Review}</Stepper.Page>
|
||||
<Stepper
|
||||
onSubmit={onCallSafeContractSubmit}
|
||||
steps={steps}
|
||||
initialValues={initialValues}
|
||||
mutators={formMutators}
|
||||
testId="create-safe-form"
|
||||
>
|
||||
<StepperPage>{SafeNameField}</StepperPage>
|
||||
<StepperPage>{SafeOwnersFields}</StepperPage>
|
||||
<StepperPage network={network}>{Review}</StepperPage>
|
||||
</Stepper>
|
||||
</Block>
|
||||
) : (
|
||||
<div>No metamask detected</div>
|
||||
<div>No web3 provider detected</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
|
@ -129,7 +129,11 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
|
|||
<Paragraph size="md" color="disabled" noMargin>
|
||||
{addresses[index]}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
@ -143,10 +147,8 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
|
|||
</Row>
|
||||
<Row className={classes.info} align="center">
|
||||
<Paragraph noMargin color="primary" size="md">
|
||||
{"You're about to create a new Safe."}
|
||||
</Paragraph>
|
||||
<Paragraph noMargin color="primary" size="md">
|
||||
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.
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -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) => (
|
||||
<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'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">
|
||||
<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
|
||||
consent with the
|
||||
By continuing you consent with the
|
||||
{' '}
|
||||
<a rel="noopener noreferrer" href="https://safe.gnosis.io/terms" target="_blank">
|
||||
terms of use
|
||||
|
@ -49,35 +63,10 @@ const SafeName = ({ classes }: Props) => (
|
|||
<a rel="noopener noreferrer" href="https://safe.gnosis.io/privacy" target="_blank">
|
||||
privacy policy
|
||||
</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>
|
||||
</Block>
|
||||
<Row margin="md" className={classes.text}>
|
||||
<Paragraph noMargin className={classes.dot} color="secondary">
|
||||
●
|
||||
</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">
|
||||
●
|
||||
</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>
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
})
|
|
@ -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()
|
||||
},
|
||||
)
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -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))
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1,10 +0,0 @@
|
|||
// @flow
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
|
||||
export type Actions = {
|
||||
fetchTransactions: typeof fetchTransactions,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchTransactions,
|
||||
}
|
|
@ -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)
|
|
@ -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,
|
||||
})
|
|
@ -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
|
|
@ -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
|
|
@ -1,10 +0,0 @@
|
|||
// @flow
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
|
||||
export type Actions = {
|
||||
fetchTransactions: typeof fetchTransactions,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchTransactions,
|
||||
}
|
|
@ -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)
|
|
@ -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,
|
||||
})
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
44
yarn.lock
44
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"
|
||||
|
|
Loading…
Reference in New Issue