Pull from dev, conflict fixes
This commit is contained in:
commit
5a2d70b057
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": ["airbnb", "plugin:flowtype/recommended"],
|
"extends": ["airbnb", "plugin:flowtype/recommended", "plugin:jsx-a11y/recommended"],
|
||||||
"parser": "babel-eslint",
|
"parser": "babel-eslint",
|
||||||
"plugins": ["jest", "flowtype"],
|
"plugins": ["jsx-a11y", "jest", "flowtype"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
|
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
|
||||||
"react/forbid-prop-types": [1, { "forbid": ["object", "any"] }],
|
"react/forbid-prop-types": [1, { "forbid": ["object", "any"] }],
|
||||||
|
|
|
@ -102,9 +102,7 @@ We use [SemVer](http://semver.org/) for versioning. For the versions available,
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
* **Adolfo Panizo** - [apanizo](https://github.com/apanizo)
|
See the list of [contributors](https://github.com/gnosis/gnosis-team-safe/contributors) who participated in this project.
|
||||||
|
|
||||||
See also the list of [contributors](https://github.com/gnosis/gnosis-team-safe/contributors) who participated in this project.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path fill="#B2B5B2" fill-rule="nonzero" d="M14 13h2V6h-5v2h2a1 1 0 0 1 1 1v4zM9 8V5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-3v3a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h3zm-2 2v7h5v-7H7z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 351 B |
|
@ -0,0 +1,66 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Img from '~/components/layout/Img'
|
||||||
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
|
import { xs } from '~/theme/variables'
|
||||||
|
import CopyIcon from './copy.svg'
|
||||||
|
|
||||||
|
const styles = () => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: xs,
|
||||||
|
borderRadius: '50%',
|
||||||
|
transition: 'background-color .2s ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#F0EFEE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type CopyBtnProps = {
|
||||||
|
content: string,
|
||||||
|
classes: Object,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CopyBtn = ({ content, classes }: CopyBtnProps) => {
|
||||||
|
if (!navigator.clipboard) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const [clicked, setClicked] = useState<boolean>(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title={clicked ? 'Copied' : 'Copy to clipboard'}
|
||||||
|
placement="top"
|
||||||
|
onClose={() => {
|
||||||
|
// this is fired before tooltip is closed
|
||||||
|
// added setTimeout so the user doesn't see the text changing/jumping
|
||||||
|
setTimeout(() => {
|
||||||
|
if (clicked) {
|
||||||
|
setClicked(false)
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={classes.container}>
|
||||||
|
<Img
|
||||||
|
src={CopyIcon}
|
||||||
|
height={20}
|
||||||
|
alt="Copy to clipboard"
|
||||||
|
onClick={() => {
|
||||||
|
copyToClipboard(content)
|
||||||
|
setClicked(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(CopyBtn)
|
|
@ -0,0 +1,54 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import Img from '~/components/layout/Img'
|
||||||
|
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
||||||
|
import { xs } from '~/theme/variables'
|
||||||
|
import { networkSelector } from '~/logic/wallets/store/selectors'
|
||||||
|
import SearchIcon from './search.svg'
|
||||||
|
|
||||||
|
const styles = () => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: xs,
|
||||||
|
borderRadius: '50%',
|
||||||
|
transition: 'background-color .2s ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#F0EFEE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type EtherscanBtnProps = {
|
||||||
|
type: 'tx' | 'address',
|
||||||
|
value: string,
|
||||||
|
currentNetwork: string,
|
||||||
|
classes: Object,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EtherscanBtn = ({
|
||||||
|
type, value, currentNetwork, classes,
|
||||||
|
}: EtherscanBtnProps) => (
|
||||||
|
<Tooltip title="Show details on Etherscan" placement="top">
|
||||||
|
<a
|
||||||
|
className={classes.container}
|
||||||
|
href={getEtherScanLink(type, value, currentNetwork)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Show details on Etherscan"
|
||||||
|
>
|
||||||
|
<Img src={SearchIcon} height={20} alt="Etherscan" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
|
||||||
|
const EtherscanBtnWithStyles = withStyles(styles)(EtherscanBtn)
|
||||||
|
|
||||||
|
export default connect<Object, Object, ?Function, ?Object>(
|
||||||
|
(state) => ({ currentNetwork: networkSelector(state) }),
|
||||||
|
null,
|
||||||
|
)(EtherscanBtnWithStyles)
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path fill="#B2B5B2" fill-rule="nonzero" d="M18.671 17.085a1.119 1.119 0 0 1-.002 1.587 1.126 1.126 0 0 1-1.586.003l-2.68-2.68a6.6 6.6 0 1 1 .862-10.061 6.603 6.603 0 0 1 .727 8.471l2.68 2.68zm-4.923-3.335a4.455 4.455 0 0 0-6.298-6.3 4.456 4.456 0 0 0 0 6.3 4.452 4.452 0 0 0 6.298 0z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 436 B |
|
@ -4,7 +4,7 @@ import * as React from 'react'
|
||||||
import styles from '~/components/layout/PageFrame/index.scss'
|
import styles from '~/components/layout/PageFrame/index.scss'
|
||||||
import Component from './index'
|
import Component from './index'
|
||||||
|
|
||||||
const FrameDecorator = story => (
|
const FrameDecorator = (story) => (
|
||||||
<div className={styles.frame}>
|
<div className={styles.frame}>
|
||||||
<div style={{ flex: '1' }} />
|
<div style={{ flex: '1' }} />
|
||||||
{story()}
|
{story()}
|
||||||
|
|
|
@ -77,7 +77,7 @@ const KeyRing = ({
|
||||||
const img = isWarning ? triangle : key
|
const img = isWarning ? triangle : key
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Block className={classes.root}>
|
<Block className={classes.root}>
|
||||||
<Block className={classes.key} style={keyStyle}>
|
<Block className={classes.key} style={keyStyle}>
|
||||||
<Img
|
<Img
|
||||||
|
@ -90,7 +90,7 @@ const KeyRing = ({
|
||||||
</Block>
|
</Block>
|
||||||
{!hideDot && <Dot className={classes.dot} style={dotStyle} />}
|
{!hideDot && <Dot className={classes.dot} style={dotStyle} />}
|
||||||
</Block>
|
</Block>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import UserDetails from './ProviderDetails/UserDetails'
|
||||||
import ProviderDisconnected from './ProviderInfo/ProviderDisconnected'
|
import ProviderDisconnected from './ProviderInfo/ProviderDisconnected'
|
||||||
import ConnectDetails from './ProviderDetails/ConnectDetails'
|
import ConnectDetails from './ProviderDetails/ConnectDetails'
|
||||||
|
|
||||||
const FrameDecorator = story => <div className={styles.frame}>{story()}</div>
|
const FrameDecorator = (story) => <div className={styles.frame}>{story()}</div>
|
||||||
|
|
||||||
storiesOf('Components /Header', module)
|
storiesOf('Components /Header', module)
|
||||||
.addDecorator(FrameDecorator)
|
.addDecorator(FrameDecorator)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as React from 'react'
|
||||||
import styles from '~/components/layout/PageFrame/index.scss'
|
import styles from '~/components/layout/PageFrame/index.scss'
|
||||||
import Component from './index'
|
import Component from './index'
|
||||||
|
|
||||||
const FrameDecorator = story => <div className={styles.frame}>{story()}</div>
|
const FrameDecorator = (story) => <div className={styles.frame}>{story()}</div>
|
||||||
|
|
||||||
storiesOf('Components', module)
|
storiesOf('Components', module)
|
||||||
.addDecorator(FrameDecorator)
|
.addDecorator(FrameDecorator)
|
||||||
|
|
|
@ -26,24 +26,34 @@ type Props = {
|
||||||
lastPage: boolean,
|
lastPage: boolean,
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
penultimate: boolean,
|
penultimate: boolean,
|
||||||
|
currentStep?: number,
|
||||||
|
buttonLabels?: Array<string>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Controls = ({
|
const Controls = ({
|
||||||
onPrevious, firstPage, penultimate, lastPage, disabled,
|
onPrevious,
|
||||||
|
firstPage,
|
||||||
|
penultimate,
|
||||||
|
lastPage,
|
||||||
|
disabled,
|
||||||
|
currentStep,
|
||||||
|
buttonLabels,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
// eslint-disable-next-line
|
|
||||||
const next = firstPage ? 'Start' : penultimate ? 'Review' : lastPage ? 'Submit' : 'Next'
|
|
||||||
const back = firstPage ? 'Cancel' : 'Back'
|
const back = firstPage ? 'Cancel' : 'Back'
|
||||||
|
|
||||||
|
let next
|
||||||
|
if (!buttonLabels) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
next = firstPage ? 'Start' : penultimate ? 'Review' : lastPage ? 'Submit' : 'Next'
|
||||||
|
} else {
|
||||||
|
// $FlowFixMe
|
||||||
|
next = buttonLabels[currentStep]
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row style={controlStyle} align="end" grow>
|
<Row style={controlStyle} align="end" grow>
|
||||||
<Col xs={12} end="xs">
|
<Col xs={12} end="xs">
|
||||||
<Button
|
<Button style={firstButtonStyle} type="button" onClick={onPrevious} size="small">
|
||||||
style={firstButtonStyle}
|
|
||||||
type="button"
|
|
||||||
onClick={onPrevious}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{back}
|
{back}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { lg } from '~/theme/variables'
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
root: {
|
root: {
|
||||||
margin: '10px',
|
margin: '10px',
|
||||||
maxWidth: '870px',
|
maxWidth: '770px',
|
||||||
boxShadow: '0 0 10px 0 rgba(33,48,77,0.10)',
|
boxShadow: '0 0 10px 0 rgba(33,48,77,0.10)',
|
||||||
},
|
},
|
||||||
padding: {
|
padding: {
|
||||||
|
@ -20,27 +20,16 @@ type Props = {
|
||||||
classes: Object,
|
classes: Object,
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
controls: React.Node,
|
controls: React.Node,
|
||||||
container?: number,
|
|
||||||
padding?: boolean,
|
padding?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateContainerStyleFrom = (container?: number) => ({
|
|
||||||
maxWidth: container ? `${container}px` : undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const OpenPaper = ({
|
const OpenPaper = ({
|
||||||
classes, children, controls, container, padding = true,
|
classes, children, controls, padding = true,
|
||||||
}: Props) => {
|
}: Props) => (
|
||||||
const containerStyle = generateContainerStyleFrom(container)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper className={classes.root} elevation={1}>
|
<Paper className={classes.root} elevation={1}>
|
||||||
<Block style={containerStyle} className={padding ? classes.padding : ''}>
|
<Block className={padding ? classes.padding : ''}>{children}</Block>
|
||||||
{children}
|
|
||||||
</Block>
|
|
||||||
{controls}
|
{controls}
|
||||||
</Paper>
|
</Paper>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
export default withStyles(styles)(OpenPaper)
|
export default withStyles(styles)(OpenPaper)
|
||||||
|
|
|
@ -19,6 +19,7 @@ type Props = {
|
||||||
onSubmit: (values: Object) => Promise<void>,
|
onSubmit: (values: Object) => Promise<void>,
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
|
buttonLabels: Array<string>,
|
||||||
initialValues?: Object,
|
initialValues?: Object,
|
||||||
disabledWhenValidating?: boolean,
|
disabledWhenValidating?: boolean,
|
||||||
mutators?: Object,
|
mutators?: Object,
|
||||||
|
@ -110,7 +111,7 @@ const GnoStepper = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
steps, children, classes, disabledWhenValidating = false, testId, mutators,
|
steps, children, classes, disabledWhenValidating = false, testId, mutators, buttonLabels,
|
||||||
} = props
|
} = props
|
||||||
const activePage = getActivePageFrom(children)
|
const activePage = getActivePageFrom(children)
|
||||||
|
|
||||||
|
@ -137,18 +138,32 @@ const GnoStepper = (props: Props) => {
|
||||||
firstPage={page === 0}
|
firstPage={page === 0}
|
||||||
lastPage={lastPage}
|
lastPage={lastPage}
|
||||||
penultimate={penultimate}
|
penultimate={penultimate}
|
||||||
|
buttonLabels={buttonLabels}
|
||||||
|
currentStep={page}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stepper classes={{ root: classes.root }} activeStep={page} orientation="vertical">
|
<Stepper classes={{ root: classes.root }} activeStep={page} orientation="vertical">
|
||||||
{steps.map((label) => (
|
{steps.map((label, index) => {
|
||||||
|
const labelProps = {}
|
||||||
|
const isClickable = index < page
|
||||||
|
|
||||||
|
if (isClickable) {
|
||||||
|
labelProps.onClick = () => {
|
||||||
|
setPage(index)
|
||||||
|
}
|
||||||
|
labelProps.className = classes.pointerCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<FormStep key={label}>
|
<FormStep key={label}>
|
||||||
<StepLabel>{label}</StepLabel>
|
<StepLabel {...labelProps}>{label}</StepLabel>
|
||||||
<StepContent TransitionProps={transitionProps}>{activePage(controls, ...rest)}</StepContent>
|
<StepContent TransitionProps={transitionProps}>{activePage(controls, ...rest)}</StepContent>
|
||||||
</FormStep>
|
</FormStep>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</Stepper>
|
</Stepper>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
@ -162,6 +177,14 @@ const styles = {
|
||||||
flex: '1 1 auto',
|
flex: '1 1 auto',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
|
pointerCursor: {
|
||||||
|
'& > .MuiStepLabel-iconContainer': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
'& > .MuiStepLabel-labelContainer': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(GnoStepper)
|
export default withStyles(styles)(GnoStepper)
|
||||||
|
|
|
@ -39,7 +39,7 @@ export const stableSort = (dataArray: List<any>, cmp: any, fixed: boolean): List
|
||||||
return a[1] - b[1]
|
return a[1] - b[1]
|
||||||
})
|
})
|
||||||
|
|
||||||
const sortedElems: List<any> = stabilizedThis.map(el => el[0])
|
const sortedElems: List<any> = stabilizedThis.map((el) => el[0])
|
||||||
|
|
||||||
return fixedElems.concat(sortedElems)
|
return fixedElems.concat(sortedElems)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ class TextField extends React.PureComponent<TextFieldProps> {
|
||||||
inputAdornment,
|
inputAdornment,
|
||||||
classes,
|
classes,
|
||||||
testId,
|
testId,
|
||||||
|
rows,
|
||||||
|
multiline,
|
||||||
...rest
|
...rest
|
||||||
} = this.props
|
} = this.props
|
||||||
const helperText = value ? text : undefined
|
const helperText = value ? text : undefined
|
||||||
|
@ -53,6 +55,8 @@ class TextField extends React.PureComponent<TextFieldProps> {
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
// data-testid={testId}
|
// data-testid={testId}
|
||||||
|
rows={rows}
|
||||||
|
multiline={multiline}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import { TextFieldProps } from '@material-ui/core/TextField'
|
||||||
|
import Field from '~/components/forms/Field'
|
||||||
|
import TextField from '~/components/forms/TextField'
|
||||||
|
|
||||||
|
const styles = () => ({
|
||||||
|
textarea: {
|
||||||
|
'& > div': {
|
||||||
|
height: '140px',
|
||||||
|
paddingTop: '0',
|
||||||
|
paddingBottom: '0',
|
||||||
|
alignItems: 'auto',
|
||||||
|
'& > textarea': {
|
||||||
|
fontSize: '15px',
|
||||||
|
letterSpacing: '-0.5px',
|
||||||
|
lineHeight: '20px',
|
||||||
|
height: '102px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const TextareaField = ({ classes, ...props }: TextFieldProps) => (
|
||||||
|
<Field
|
||||||
|
{...props}
|
||||||
|
component={TextField}
|
||||||
|
multiline
|
||||||
|
rows="5"
|
||||||
|
className={classes.textarea}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withStyles(styles)(TextareaField)
|
|
@ -18,11 +18,11 @@ export const simpleMemoize = (fn: Function) => {
|
||||||
|
|
||||||
type Field = boolean | string | null | typeof undefined
|
type Field = boolean | string | null | typeof undefined
|
||||||
|
|
||||||
export const required = simpleMemoize((value: Field) => (value ? undefined : 'Required'))
|
export const required = (value: Field) => (value ? undefined : 'Required')
|
||||||
|
|
||||||
export const mustBeInteger = (value: string) => (!Number.isInteger(Number(value)) || value.includes('.') ? 'Must be an integer' : undefined)
|
export const mustBeInteger = (value: string) => (!Number.isInteger(Number(value)) || value.includes('.') ? 'Must be an integer' : undefined)
|
||||||
|
|
||||||
export const mustBeFloat = (value: number) => (Number.isNaN(Number(value)) ? 'Must be a number' : undefined)
|
export const mustBeFloat = (value: number) => (value && Number.isNaN(Number(value)) ? 'Must be a number' : undefined)
|
||||||
|
|
||||||
export const greaterThan = (min: number) => (value: string) => {
|
export const greaterThan = (min: number) => (value: string) => {
|
||||||
if (Number.isNaN(Number(value)) || Number.parseFloat(value) > Number(min)) {
|
if (Number.isNaN(Number(value)) || Number.parseFloat(value) > Number(min)) {
|
||||||
|
@ -66,6 +66,14 @@ export const mustBeEthereumAddress = simpleMemoize((address: Field) => {
|
||||||
return isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name'
|
return isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const mustBeEthereumContractAddress = simpleMemoize(async (address: string) => {
|
||||||
|
const contractCode: string = await getWeb3().eth.getCode(address)
|
||||||
|
|
||||||
|
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === ''
|
||||||
|
? 'Address should be a valid Ethereum contract address or ENS name'
|
||||||
|
: undefined
|
||||||
|
})
|
||||||
|
|
||||||
export const minMaxLength = (minLen: string | number, maxLen: string | number) => (value: string) => (value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`)
|
export const minMaxLength = (minLen: string | number, maxLen: string | number) => (value: string) => (value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`)
|
||||||
|
|
||||||
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
|
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import {
|
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
|
||||||
TX_SERVICE_HOST,
|
|
||||||
SIGNATURES_VIA_METAMASK,
|
|
||||||
RELAY_API_URL,
|
|
||||||
} from '~/config/names'
|
|
||||||
|
|
||||||
const devConfig = {
|
const devConfig = {
|
||||||
[TX_SERVICE_HOST]: 'https://safe-transaction.staging.gnosisdev.com/api/v1/',
|
[TX_SERVICE_HOST]: 'https://safe-transaction.staging.gnosisdev.com/api/v1/',
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { ensureOnce } from '~/utils/singleton'
|
import { ensureOnce } from '~/utils/singleton'
|
||||||
import {
|
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
|
||||||
TX_SERVICE_HOST,
|
|
||||||
SIGNATURES_VIA_METAMASK,
|
|
||||||
RELAY_API_URL,
|
|
||||||
} from '~/config/names'
|
|
||||||
import devConfig from './development'
|
import devConfig from './development'
|
||||||
import testConfig from './testing'
|
import testConfig from './testing'
|
||||||
import prodConfig from './production'
|
import prodConfig from './production'
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import {
|
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
|
||||||
TX_SERVICE_HOST,
|
|
||||||
SIGNATURES_VIA_METAMASK,
|
|
||||||
RELAY_API_URL,
|
|
||||||
} from '~/config/names'
|
|
||||||
|
|
||||||
const prodConfig = {
|
const prodConfig = {
|
||||||
[TX_SERVICE_HOST]: 'https://safe-transaction.staging.gnosisdev.com/api/v1/',
|
[TX_SERVICE_HOST]: 'https://safe-transaction.staging.gnosisdev.com/api/v1/',
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import {
|
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
|
||||||
TX_SERVICE_HOST,
|
|
||||||
SIGNATURES_VIA_METAMASK,
|
|
||||||
RELAY_API_URL,
|
|
||||||
} from '~/config/names'
|
|
||||||
|
|
||||||
const testConfig = {
|
const testConfig = {
|
||||||
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
||||||
|
|
|
@ -75,6 +75,23 @@ export const deploySafeContract = async (safeAccounts: string[], numConfirmation
|
||||||
return proxyFactoryMaster.createProxy(safeMaster.address, gnosisSafeData, { from: userAccount, gas, gasPrice })
|
return proxyFactoryMaster.createProxy(safeMaster.address, gnosisSafeData, { from: userAccount, gas, gasPrice })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const estimateGasForDeployingSafe = async (
|
||||||
|
safeAccounts: string[],
|
||||||
|
numConfirmations: number,
|
||||||
|
userAccount: string,
|
||||||
|
) => {
|
||||||
|
const gnosisSafeData = await safeMaster.contract.methods
|
||||||
|
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', ZERO_ADDRESS, 0, ZERO_ADDRESS)
|
||||||
|
.encodeABI()
|
||||||
|
const proxyFactoryData = proxyFactoryMaster.contract.methods
|
||||||
|
.createProxy(safeMaster.address, gnosisSafeData)
|
||||||
|
.encodeABI()
|
||||||
|
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address)
|
||||||
|
const gasPrice = await calculateGasPrice()
|
||||||
|
|
||||||
|
return gas * parseInt(gasPrice, 10)
|
||||||
|
}
|
||||||
|
|
||||||
export const getGnosisSafeInstanceAt = async (safeAddress: string) => {
|
export const getGnosisSafeInstanceAt = async (safeAddress: string) => {
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const GnosisSafe = await getGnosisSafeContract(web3)
|
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||||
|
|
|
@ -110,7 +110,7 @@ export const executeTransaction = async (
|
||||||
const errMsg = await getErrorMessage(safeInstance.address, 0, executeDataUsedSignatures, sender)
|
const errMsg = await getErrorMessage(safeInstance.address, 0, executeDataUsedSignatures, sender)
|
||||||
console.log(`Error executing the TX: ${errMsg}`)
|
console.log(`Error executing the TX: ${errMsg}`)
|
||||||
|
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const getEthAsToken = (balance: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => {
|
export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => {
|
||||||
const activeTokens = List().withMutations(list => tokens.forEach((token: Token) => {
|
const activeTokens = List().withMutations((list) => tokens.forEach((token: Token) => {
|
||||||
const isDeactivated = isEther(token.symbol) || !token.status
|
const isDeactivated = isEther(token.symbol) || !token.status
|
||||||
if (isDeactivated) {
|
if (isDeactivated) {
|
||||||
return
|
return
|
||||||
|
@ -48,3 +48,5 @@ export const isAddressAToken = async (tokenAddress: string) => {
|
||||||
|
|
||||||
return call !== '0x'
|
return call !== '0x'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isTokenTransfer = async (data: string, value: number) => data.substring(0, 10) === '0xa9059cbb' && value === 0
|
||||||
|
|
|
@ -31,17 +31,6 @@ export const getCustomTokens = async (): Promise<List<TokenProps>> => {
|
||||||
return data ? List(data) : List()
|
return data ? List(data) : List()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setToken = async (safeAddress: string, token: Token) => {
|
|
||||||
const data: List<TokenProps> = await getCustomTokens()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await saveToStorage(CUSTOM_TOKENS_KEY, data.push(token))
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.log('Error adding token in localstorage')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const removeTokenFromStorage = async (safeAddress: string, token: Token) => {
|
export const removeTokenFromStorage = async (safeAddress: string, token: Token) => {
|
||||||
const data: List<TokenProps> = await getCustomTokens()
|
const data: List<TokenProps> = await getCustomTokens()
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||||
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields'
|
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { getSafeMasterContract } from '~/logic/contracts/safeContracts'
|
import { getSafeMasterContract } from '~/logic/contracts/safeContracts'
|
||||||
|
import { secondary } from '~/theme/variables'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
classes: Object,
|
classes: Object,
|
||||||
|
@ -33,6 +34,11 @@ const styles = () => ({
|
||||||
color: '#03AE60',
|
color: '#03AE60',
|
||||||
height: '20px',
|
height: '20px',
|
||||||
},
|
},
|
||||||
|
links: {
|
||||||
|
'&>a': {
|
||||||
|
color: secondary,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const SAFE_INSTANCE_ERROR = 'Address given is not a safe instance'
|
export const SAFE_INSTANCE_ERROR = 'Address given is not a safe instance'
|
||||||
|
@ -45,6 +51,7 @@ export const safeFieldsValidation = async (values: Object) => {
|
||||||
const errors = {}
|
const errors = {}
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const safeAddress = values[FIELD_LOAD_ADDRESS]
|
const safeAddress = values[FIELD_LOAD_ADDRESS]
|
||||||
|
|
||||||
if (!safeAddress || mustBeEthereumAddress(safeAddress) !== undefined) {
|
if (!safeAddress || mustBeEthereumAddress(safeAddress) !== undefined) {
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
@ -84,10 +91,13 @@ export const safeFieldsValidation = async (values: Object) => {
|
||||||
|
|
||||||
const Details = ({ classes, errors, form }: Props) => (
|
const Details = ({ classes, errors, form }: Props) => (
|
||||||
<>
|
<>
|
||||||
<Block margin="sm">
|
<Block margin="md">
|
||||||
<Paragraph noMargin size="lg" color="primary">
|
<Paragraph noMargin size="md" color="primary">
|
||||||
Adding an existing Safe only requires the Safe address. Optionally you can give it a name. In case your
|
You are about to load an existing Gnosis Safe. First, choose a name and enter the Safe address. The name is only
|
||||||
connected client is not the owner of the Safe, the interface will essentially provide you a read-only view.
|
stored locally and will never be shared with Gnosis or any third parties.
|
||||||
|
<br />
|
||||||
|
Your connected wallet does not have to be the owner of this Safe. In this case, the interface will provide you a
|
||||||
|
read-only view.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
<Block className={classes.root}>
|
<Block className={classes.root}>
|
||||||
|
@ -122,6 +132,23 @@ const Details = ({ classes, errors, form }: Props) => (
|
||||||
text="Safe Address"
|
text="Safe Address"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
|
<Block margin="sm">
|
||||||
|
<Paragraph noMargin size="md" color="primary" className={classes.links}>
|
||||||
|
By continuing you consent with the
|
||||||
|
{' '}
|
||||||
|
<a rel="noopener noreferrer" href="https://safe.gnosis.io/terms" target="_blank">
|
||||||
|
terms of use
|
||||||
|
</a>
|
||||||
|
{' '}
|
||||||
|
and
|
||||||
|
{' '}
|
||||||
|
<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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -129,7 +156,7 @@ const DetailsForm = withStyles(styles)(Details)
|
||||||
|
|
||||||
const DetailsPage = () => (controls: React.Node, { errors, form }: Object) => (
|
const DetailsPage = () => (controls: React.Node, { errors, form }: Object) => (
|
||||||
<>
|
<>
|
||||||
<OpenPaper controls={controls} container={605}>
|
<OpenPaper controls={controls}>
|
||||||
<DetailsForm errors={errors} form={form} />
|
<DetailsForm errors={errors} form={form} />
|
||||||
</OpenPaper>
|
</OpenPaper>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { history } from '~/store'
|
||||||
import { secondary } from '~/theme/variables'
|
import { secondary } from '~/theme/variables'
|
||||||
import { type SelectorProps } from '~/routes/load/container/selector'
|
import { type SelectorProps } from '~/routes/load/container/selector'
|
||||||
|
|
||||||
const getSteps = () => ['Details', 'Owners', 'Review']
|
const getSteps = () => ['Name and address', 'Owners', 'Review']
|
||||||
|
|
||||||
type Props = SelectorProps & {
|
type Props = SelectorProps & {
|
||||||
onLoadSafeSubmit: (values: Object) => Promise<void>,
|
onLoadSafeSubmit: (values: Object) => Promise<void>,
|
||||||
|
@ -35,6 +35,8 @@ const formMutators = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buttonLabels = ['Next', 'Review', 'Load']
|
||||||
|
|
||||||
const Layout = ({
|
const Layout = ({
|
||||||
provider, onLoadSafeSubmit, network, userAddress,
|
provider, onLoadSafeSubmit, network, userAddress,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
@ -56,6 +58,7 @@ const Layout = ({
|
||||||
steps={steps}
|
steps={steps}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
mutators={formMutators}
|
mutators={formMutators}
|
||||||
|
buttonLabels={buttonLabels}
|
||||||
testId="load-safe-form"
|
testId="load-safe-form"
|
||||||
>
|
>
|
||||||
<StepperPage validate={safeFieldsValidation}>{DetailsForm}</StepperPage>
|
<StepperPage validate={safeFieldsValidation}>{DetailsForm}</StepperPage>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Field from '~/components/forms/Field'
|
import Field from '~/components/forms/Field'
|
||||||
import { required } from '~/components/forms/validator'
|
import { required } from '~/components/forms/validator'
|
||||||
|
@ -10,22 +9,17 @@ import Identicon from '~/components/Identicon'
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import Link from '~/components/layout/Link'
|
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import EtherscanBtn from '~/components/EtherscanBtn'
|
||||||
|
import CopyBtn from '~/components/CopyBtn'
|
||||||
import {
|
import {
|
||||||
sm, md, lg, border, secondary, disabled, extraSmallFontSize,
|
sm, md, lg, border, disabled, extraSmallFontSize,
|
||||||
} from '~/theme/variables'
|
} from '~/theme/variables'
|
||||||
import { getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields'
|
import { getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields'
|
||||||
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
|
||||||
import { FIELD_LOAD_ADDRESS, THRESHOLD } from '~/routes/load/components/fields'
|
import { FIELD_LOAD_ADDRESS, THRESHOLD } from '~/routes/load/components/fields'
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
|
|
||||||
const openIconStyle = {
|
|
||||||
height: '16px',
|
|
||||||
color: secondary,
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
details: {
|
details: {
|
||||||
padding: lg,
|
padding: lg,
|
||||||
|
@ -45,6 +39,7 @@ const styles = () => ({
|
||||||
},
|
},
|
||||||
address: {
|
address: {
|
||||||
paddingLeft: '6px',
|
paddingLeft: '6px',
|
||||||
|
marginRight: sm,
|
||||||
},
|
},
|
||||||
open: {
|
open: {
|
||||||
paddingLeft: sm,
|
paddingLeft: sm,
|
||||||
|
@ -70,11 +65,7 @@ const styles = () => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
type LayoutProps = {
|
type Props = {
|
||||||
network: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = LayoutProps & {
|
|
||||||
values: Object,
|
values: Object,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
updateInitialProps: (initialValues: Object) => void,
|
updateInitialProps: (initialValues: Object) => void,
|
||||||
|
@ -92,7 +83,7 @@ const calculateSafeValues = (owners: Array<string>, threshold: Number, values: O
|
||||||
const OwnerListComponent = (props: Props) => {
|
const OwnerListComponent = (props: Props) => {
|
||||||
const [owners, setOwners] = useState<Array<string>>([])
|
const [owners, setOwners] = useState<Array<string>>([])
|
||||||
const {
|
const {
|
||||||
values, updateInitialProps, network, classes,
|
values, updateInitialProps, classes,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -122,7 +113,7 @@ const OwnerListComponent = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Block className={classes.title}>
|
<Block className={classes.title}>
|
||||||
<Paragraph noMargin size="lg" color="primary">
|
<Paragraph noMargin size="md" color="primary">
|
||||||
{`This Safe has ${owners.length} owners. Optional: Provide a name for each owner.`}
|
{`This Safe has ${owners.length} owners. Optional: Provide a name for each owner.`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -147,15 +138,14 @@ const OwnerListComponent = (props: Props) => {
|
||||||
text="Owner Name"
|
text="Owner Name"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={7}>
|
<Col xs={8}>
|
||||||
<Row className={classes.ownerAddresses}>
|
<Row className={classes.ownerAddresses}>
|
||||||
<Identicon address={address} diameter={32} />
|
<Identicon address={address} diameter={32} />
|
||||||
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
|
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
|
||||||
{address}
|
{address}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink('address', address, network)} target="_blank">
|
<CopyBtn content={address} />
|
||||||
<OpenInNew style={openIconStyle} />
|
<EtherscanBtn type="address" value={address} />
|
||||||
</Link>
|
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -2,29 +2,23 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Identicon from '~/components/Identicon'
|
import Identicon from '~/components/Identicon'
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import Link from '~/components/layout/Link'
|
import EtherscanBtn from '~/components/EtherscanBtn'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import CopyBtn from '~/components/CopyBtn'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import {
|
import {
|
||||||
xs, sm, lg, border, secondary,
|
xs, sm, lg, border,
|
||||||
} from '~/theme/variables'
|
} from '~/theme/variables'
|
||||||
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
|
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
|
||||||
import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
|
import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||||
import { getOwnerNameBy, getOwnerAddressBy, getNumOwnersFrom } from '~/routes/open/components/fields'
|
import { getOwnerNameBy, getOwnerAddressBy, getNumOwnersFrom } from '~/routes/open/components/fields'
|
||||||
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
|
||||||
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS, THRESHOLD } from '~/routes/load/components/fields'
|
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS, THRESHOLD } from '~/routes/load/components/fields'
|
||||||
|
|
||||||
const openIconStyle = {
|
|
||||||
height: '16px',
|
|
||||||
color: secondary,
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
root: {
|
root: {
|
||||||
minHeight: '300px',
|
minHeight: '300px',
|
||||||
|
@ -51,6 +45,9 @@ const styles = () => ({
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
justifyContent: 'left',
|
justifyContent: 'left',
|
||||||
|
'& > p': {
|
||||||
|
marginRight: sm,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
open: {
|
open: {
|
||||||
paddingLeft: sm,
|
paddingLeft: sm,
|
||||||
|
@ -65,15 +62,12 @@ const styles = () => ({
|
||||||
},
|
},
|
||||||
address: {
|
address: {
|
||||||
paddingLeft: '6px',
|
paddingLeft: '6px',
|
||||||
|
marginRight: sm,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
type LayoutProps = {
|
type Props = {
|
||||||
network: string,
|
|
||||||
userAddress: string,
|
userAddress: string,
|
||||||
}
|
|
||||||
|
|
||||||
type Props = LayoutProps & {
|
|
||||||
values: Object,
|
values: Object,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
}
|
}
|
||||||
|
@ -97,9 +91,7 @@ const checkUserAddressOwner = (values: Object, userAddress: string): boolean =>
|
||||||
|
|
||||||
class ReviewComponent extends React.PureComponent<Props, State> {
|
class ReviewComponent extends React.PureComponent<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { values, classes, userAddress } = this.props
|
||||||
values, classes, network, userAddress,
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
const isOwner = checkUserAddressOwner(values, userAddress)
|
const isOwner = checkUserAddressOwner(values, userAddress)
|
||||||
const owners = getAccountsFrom(values)
|
const owners = getAccountsFrom(values)
|
||||||
|
@ -132,9 +124,8 @@ class ReviewComponent extends React.PureComponent<Props, State> {
|
||||||
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
|
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
|
||||||
{shortVersionOf(safeAddress, 4)}
|
{shortVersionOf(safeAddress, 4)}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink('address', safeAddress, network)} target="_blank">
|
<CopyBtn content={safeAddress} />
|
||||||
<OpenInNew style={openIconStyle} />
|
<EtherscanBtn type="address" value={safeAddress} />
|
||||||
</Link>
|
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
<Block margin="lg">
|
<Block margin="lg">
|
||||||
|
@ -177,13 +168,8 @@ class ReviewComponent extends React.PureComponent<Props, State> {
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{address}
|
{address}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link
|
<CopyBtn content={address} />
|
||||||
className={classes.open}
|
<EtherscanBtn type="address" value={address} />
|
||||||
to={getEtherScanLink('address', address, network)}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<OpenInNew style={openIconStyle} />
|
|
||||||
</Link>
|
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -13,10 +13,10 @@ import { getOwnerNameBy, getOwnerAddressBy, FIELD_CONFIRMATIONS } from '~/routes
|
||||||
import { history } from '~/store'
|
import { history } from '~/store'
|
||||||
import { secondary } from '~/theme/variables'
|
import { secondary } from '~/theme/variables'
|
||||||
|
|
||||||
const getSteps = () => ['Start', 'Owners and confirmations', 'Review']
|
const getSteps = () => ['Name', 'Owners and confirmations', 'Review']
|
||||||
|
|
||||||
const initialValuesFrom = (userAccount: string) => ({
|
const initialValuesFrom = (userAccount: string) => ({
|
||||||
[getOwnerNameBy(0)]: 'My Metamask (me)',
|
[getOwnerNameBy(0)]: 'My Wallet',
|
||||||
[getOwnerAddressBy(0)]: userAccount,
|
[getOwnerAddressBy(0)]: userAccount,
|
||||||
[FIELD_CONFIRMATIONS]: '1',
|
[FIELD_CONFIRMATIONS]: '1',
|
||||||
})
|
})
|
||||||
|
@ -69,7 +69,9 @@ const Layout = ({
|
||||||
>
|
>
|
||||||
<StepperPage>{SafeNameField}</StepperPage>
|
<StepperPage>{SafeNameField}</StepperPage>
|
||||||
<StepperPage>{SafeOwnersFields}</StepperPage>
|
<StepperPage>{SafeOwnersFields}</StepperPage>
|
||||||
<StepperPage network={network}>{Review}</StepperPage>
|
<StepperPage network={network} userAccount={userAccount}>
|
||||||
|
{Review}
|
||||||
|
</StepperPage>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
</Block>
|
</Block>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { getProviderInfo } from '~/logic/wallets/getWeb3'
|
||||||
import { sleep } from '~/utils/timer'
|
import { sleep } from '~/utils/timer'
|
||||||
import Component from './Layout'
|
import Component from './Layout'
|
||||||
|
|
||||||
const FrameDecorator = story => <div className={styles.frame}>{story()}</div>
|
const FrameDecorator = (story) => <div className={styles.frame}>{story()}</div>
|
||||||
|
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
safeAddress: '',
|
safeAddress: '',
|
||||||
|
|
|
@ -2,26 +2,24 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
import { estimateGasForDeployingSafe } from '~/logic/contracts/safeContracts'
|
||||||
import { getNamesFrom, getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
|
import { getNamesFrom, getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
|
import EtherscanBtn from '~/components/EtherscanBtn'
|
||||||
|
import CopyBtn from '~/components/CopyBtn'
|
||||||
import Identicon from '~/components/Identicon'
|
import Identicon from '~/components/Identicon'
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import Link from '~/components/layout/Link'
|
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import {
|
import {
|
||||||
sm, md, lg, border, secondary, background,
|
sm, md, lg, border, background,
|
||||||
} from '~/theme/variables'
|
} from '~/theme/variables'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { FIELD_NAME, FIELD_CONFIRMATIONS, getNumOwnersFrom } from '../fields'
|
import { FIELD_NAME, FIELD_CONFIRMATIONS, getNumOwnersFrom } from '../fields'
|
||||||
|
|
||||||
const openIconStyle = {
|
const { useEffect, useState } = React
|
||||||
height: '16px',
|
|
||||||
color: secondary,
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
root: {
|
root: {
|
||||||
|
@ -55,6 +53,9 @@ const styles = () => ({
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
justifyContent: 'left',
|
justifyContent: 'left',
|
||||||
|
'& > p': {
|
||||||
|
marginRight: sm,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
open: {
|
open: {
|
||||||
paddingLeft: sm,
|
paddingLeft: sm,
|
||||||
|
@ -65,22 +66,40 @@ const styles = () => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
type LayoutProps = {
|
type Props = {
|
||||||
network: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = LayoutProps & {
|
|
||||||
values: Object,
|
values: Object,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
|
userAccount: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReviewComponent = ({ values, classes, network }: Props) => {
|
const ReviewComponent = ({ values, classes, userAccount }: Props) => {
|
||||||
|
const [gasCosts, setGasCosts] = useState<string>('0.00')
|
||||||
const names = getNamesFrom(values)
|
const names = getNamesFrom(values)
|
||||||
const addresses = getAccountsFrom(values)
|
const addresses = getAccountsFrom(values)
|
||||||
const numOwners = getNumOwnersFrom(values)
|
const numOwners = getNumOwnersFrom(values)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isCurrent = true
|
||||||
|
const estimateGas = async () => {
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const { fromWei, toBN } = web3.utils
|
||||||
|
const estimatedGasCosts = await estimateGasForDeployingSafe(addresses, numOwners, userAccount)
|
||||||
|
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
|
||||||
|
const roundedGasCosts = parseFloat(gasCostsAsEth).toFixed(3)
|
||||||
|
if (isCurrent) {
|
||||||
|
setGasCosts(roundedGasCosts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
estimateGas()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCurrent = false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Row className={classes.root}>
|
<Row className={classes.root}>
|
||||||
<Col xs={4} layout="column">
|
<Col xs={4} layout="column">
|
||||||
<Block className={classes.details}>
|
<Block className={classes.details}>
|
||||||
|
@ -129,13 +148,8 @@ 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
|
<CopyBtn content={addresses[index]} />
|
||||||
className={classes.open}
|
<EtherscanBtn type="address" value={addresses[index]} />
|
||||||
to={getEtherScanLink('address', addresses[index], network)}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<OpenInNew style={openIconStyle} />
|
|
||||||
</Link>
|
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -148,21 +162,26 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
|
||||||
<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 and will have to confirm a transaction with your currently connected
|
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.
|
wallet. The creation will cost approximately
|
||||||
|
{' '}
|
||||||
|
{gasCosts}
|
||||||
|
{' '}
|
||||||
|
ETH. The exact amount will be determined by your
|
||||||
|
wallet.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReviewPage = withStyles(styles)(ReviewComponent)
|
const ReviewPage = withStyles(styles)(ReviewComponent)
|
||||||
|
|
||||||
const Review = ({ network }: LayoutProps) => (controls: React.Node, { values }: Object) => (
|
const Review = () => (controls: React.Node, { values }: Object) => (
|
||||||
<React.Fragment>
|
<>
|
||||||
<OpenPaper controls={controls} padding={false}>
|
<OpenPaper controls={controls} padding={false}>
|
||||||
<ReviewPage network={network} values={values} />
|
<ReviewPage values={values} />
|
||||||
</OpenPaper>
|
</OpenPaper>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default Review
|
export default Review
|
||||||
|
|
|
@ -33,7 +33,7 @@ const styles = () => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
const SafeName = ({ classes }: Props) => (
|
const SafeName = ({ classes }: Props) => (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Block margin="lg">
|
<Block margin="lg">
|
||||||
<Paragraph noMargin size="md" color="primary">
|
<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
|
You are about to create a new Gnosis Safe wallet with one or more owners. First, let's give your new wallet
|
||||||
|
@ -67,13 +67,13 @@ const SafeName = ({ classes }: Props) => (
|
||||||
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
|
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
const SafeNameForm = withStyles(styles)(SafeName)
|
const SafeNameForm = withStyles(styles)(SafeName)
|
||||||
|
|
||||||
const SafeNamePage = () => (controls: React.Node) => (
|
const SafeNamePage = () => (controls: React.Node) => (
|
||||||
<OpenPaper controls={controls} container={600}>
|
<OpenPaper controls={controls}>
|
||||||
<SafeNameForm />
|
<SafeNameForm />
|
||||||
</OpenPaper>
|
</OpenPaper>
|
||||||
)
|
)
|
||||||
|
|
|
@ -109,7 +109,13 @@ const SafeOwners = (props: Props) => {
|
||||||
<>
|
<>
|
||||||
<Block className={classes.title}>
|
<Block className={classes.title}>
|
||||||
<Paragraph noMargin size="md" color="primary">
|
<Paragraph noMargin size="md" color="primary">
|
||||||
Specify the owners of the Safe.
|
Your Safe will have one or more owners. We have prefilled the first owner with your connected wallet details,
|
||||||
|
but you are free to change this to a different owner.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Add additional owners (e.g. wallets of your teammates) and specify how many of them have to confirm a
|
||||||
|
transaction before it gets executed. In general, the more confirmations required, the more secure is your
|
||||||
|
Safe.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import {
|
import { uniqueAddress } from '~/components/forms/validator'
|
||||||
uniqueAddress,
|
|
||||||
} from '~/components/forms/validator'
|
|
||||||
|
|
||||||
export const getAddressValidator = (addresses: string[], position: number) => {
|
export const getAddressValidator = (addresses: string[], position: number) => {
|
||||||
// thanks Rich Harris
|
// thanks Rich Harris
|
||||||
|
|
|
@ -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+Address$/.test(key) && !!values[key])
|
.filter((key) => /^owner\d+Address$/.test(key) && !!values[key])
|
||||||
|
|
||||||
return accounts.length
|
return accounts.length
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,10 @@ export const createSafe = async (values: Object, userAccount: string, addSafe: A
|
||||||
}
|
}
|
||||||
|
|
||||||
class Open extends React.Component<Props> {
|
class Open extends React.Component<Props> {
|
||||||
|
async componentDidMount() {
|
||||||
|
await initContracts()
|
||||||
|
}
|
||||||
|
|
||||||
onCallSafeContractSubmit = async (values) => {
|
onCallSafeContractSubmit = async (values) => {
|
||||||
try {
|
try {
|
||||||
const { userAccount, addSafe } = this.props
|
const { userAccount, addSafe } = this.props
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { createStructuredSelector } from 'reselect'
|
import { createStructuredSelector } from 'reselect'
|
||||||
import { providerNameSelector, userAccountSelector, networkSelector } from '~/logic/wallets/store/selectors'
|
import { providerNameSelector, userAccountSelector, networkSelector } from '~/logic/wallets/store/selectors'
|
||||||
|
|
||||||
export default createStructuredSelector({
|
export default createStructuredSelector<Object, *>({
|
||||||
provider: providerNameSelector,
|
provider: providerNameSelector,
|
||||||
network: networkSelector,
|
network: networkSelector,
|
||||||
userAccount: userAccountSelector,
|
userAccount: userAccountSelector,
|
||||||
|
|
|
@ -4,17 +4,17 @@ import { makeOwner } from '~/routes/safe/store/models/owner'
|
||||||
export const getAccountsFrom = (values: Object): string[] => {
|
export const getAccountsFrom = (values: Object): string[] => {
|
||||||
const accounts = Object.keys(values)
|
const accounts = Object.keys(values)
|
||||||
.sort()
|
.sort()
|
||||||
.filter(key => /^owner\d+Address$/.test(key))
|
.filter((key) => /^owner\d+Address$/.test(key))
|
||||||
|
|
||||||
return accounts.map(account => values[account]).slice(0, values.owners)
|
return accounts.map((account) => values[account]).slice(0, values.owners)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNamesFrom = (values: Object): string[] => {
|
export const getNamesFrom = (values: Object): string[] => {
|
||||||
const accounts = Object.keys(values)
|
const accounts = Object.keys(values)
|
||||||
.sort()
|
.sort()
|
||||||
.filter(key => /^owner\d+Name$/.test(key))
|
.filter((key) => /^owner\d+Name$/.test(key))
|
||||||
|
|
||||||
return accounts.map(account => values[account]).slice(0, values.owners)
|
return accounts.map((account) => values[account]).slice(0, values.owners)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOwnersFrom = (names: string[], addresses: string[]): Array<string, string> => {
|
export const getOwnersFrom = (names: string[], addresses: string[]): Array<string, string> => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import styles from '~/components/layout/PageFrame/index.scss'
|
||||||
import { ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
|
import { ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
|
||||||
import Component from './component'
|
import Component from './component'
|
||||||
|
|
||||||
const FrameDecorator = story => <div className={styles.frame}>{story()}</div>
|
const FrameDecorator = (story) => <div className={styles.frame}>{story()}</div>
|
||||||
|
|
||||||
storiesOf('Routes /opening', module)
|
storiesOf('Routes /opening', module)
|
||||||
.addDecorator(FrameDecorator)
|
.addDecorator(FrameDecorator)
|
||||||
|
|
|
@ -14,13 +14,13 @@ import Row from '~/components/layout/Row'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import {
|
import {
|
||||||
xxl, lg, sm, md, background, secondary,
|
lg, md, secondary, secondaryText,
|
||||||
} from '~/theme/variables'
|
} from '~/theme/variables'
|
||||||
import { copyToClipboard } from '~/utils/clipboard'
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
heading: {
|
heading: {
|
||||||
padding: `${sm} ${lg}`,
|
padding: `${md} ${lg}`,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
maxHeight: '75px',
|
maxHeight: '75px',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
|
@ -32,27 +32,27 @@ const styles = () => ({
|
||||||
height: '35px',
|
height: '35px',
|
||||||
width: '35px',
|
width: '35px',
|
||||||
},
|
},
|
||||||
detailsContainer: {
|
|
||||||
backgroundColor: background,
|
|
||||||
},
|
|
||||||
qrContainer: {
|
qrContainer: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
padding: md,
|
padding: md,
|
||||||
borderRadius: '3px',
|
borderRadius: '6px',
|
||||||
boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)',
|
border: `1px solid ${secondaryText}`,
|
||||||
},
|
},
|
||||||
safeName: {
|
safeName: {
|
||||||
margin: `${xxl} 0 20px`,
|
margin: `${lg} 0 ${lg}`,
|
||||||
},
|
},
|
||||||
buttonRow: {
|
buttonRow: {
|
||||||
height: '84px',
|
height: '84px',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
'& > button': {
|
||||||
|
fontFamily: 'Averta',
|
||||||
|
fontSize: '16px',
|
||||||
|
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||||
},
|
},
|
||||||
button: {
|
|
||||||
height: '42px',
|
|
||||||
},
|
},
|
||||||
addressContainer: {
|
addressContainer: {
|
||||||
marginTop: '28px',
|
marginTop: '25px',
|
||||||
|
marginBottom: '25px',
|
||||||
},
|
},
|
||||||
address: {
|
address: {
|
||||||
marginLeft: '6px',
|
marginLeft: '6px',
|
||||||
|
@ -75,7 +75,7 @@ type Props = {
|
||||||
const Receive = ({
|
const Receive = ({
|
||||||
classes, onClose, safeAddress, safeName, etherScanLink,
|
classes, onClose, safeAddress, safeName, etherScanLink,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Row align="center" grow className={classes.heading}>
|
<Row align="center" grow className={classes.heading}>
|
||||||
<Paragraph className={classes.manage} weight="bolder" noMargin>
|
<Paragraph className={classes.manage} weight="bolder" noMargin>
|
||||||
Receive funds
|
Receive funds
|
||||||
|
@ -84,7 +84,7 @@ const Receive = ({
|
||||||
<Close className={classes.close} />
|
<Close className={classes.close} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Row>
|
</Row>
|
||||||
<Col layout="column" middle="xs" className={classes.detailsContainer}>
|
<Col layout="column" middle="xs">
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Paragraph className={classes.safeName} weight="bolder" size="xl" noMargin>
|
<Paragraph className={classes.safeName} weight="bolder" size="xl" noMargin>
|
||||||
{safeName}
|
{safeName}
|
||||||
|
@ -109,11 +109,11 @@ const Receive = ({
|
||||||
</Col>
|
</Col>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button color="primary" className={classes.button} minWidth={140} onClick={onClose} variant="contained">
|
<Button color="primary" minWidth={140} onClick={onClose} variant="contained">
|
||||||
Done
|
Done
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default withStyles(styles)(Receive)
|
export default withStyles(styles)(Receive)
|
||||||
|
|
|
@ -24,7 +24,7 @@ const styles = () => ({
|
||||||
letterSpacing: -0.5,
|
letterSpacing: -0.5,
|
||||||
backgroundColor: border,
|
backgroundColor: border,
|
||||||
width: 'fit-content',
|
width: 'fit-content',
|
||||||
padding: '6px',
|
padding: '5px 10px',
|
||||||
marginTop: xs,
|
marginTop: xs,
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,9 +5,18 @@ import cn from 'classnames'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import Modal from '~/components/Modal'
|
import Modal from '~/components/Modal'
|
||||||
import ChooseTxType from './screens/ChooseTxType'
|
|
||||||
import SendFunds from './screens/SendFunds'
|
const ChooseTxType = React.lazy(() => import('./screens/ChooseTxType'))
|
||||||
import ReviewTx from './screens/ReviewTx'
|
|
||||||
|
const SendFunds = React.lazy(() => import('./screens/SendFunds'))
|
||||||
|
|
||||||
|
const ReviewTx = React.lazy(() => import('./screens/ReviewTx'))
|
||||||
|
|
||||||
|
const SendCustomTx = React.lazy(() => import('./screens/SendCustomTx'))
|
||||||
|
|
||||||
|
const ReviewCustomTx = React.lazy(() => import('./screens/ReviewCustomTx'))
|
||||||
|
|
||||||
|
type ActiveScreen = 'chooseTxType' | 'sendFunds' | 'reviewTx' | 'sendCustomTx' | 'reviewCustomTx'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
|
@ -20,19 +29,23 @@ type Props = {
|
||||||
tokens: List<Token>,
|
tokens: List<Token>,
|
||||||
selectedToken: string,
|
selectedToken: string,
|
||||||
createTransaction: Function,
|
createTransaction: Function,
|
||||||
|
activeScreenType: ActiveScreen
|
||||||
}
|
}
|
||||||
type ActiveScreen = 'chooseTxType' | 'sendFunds' | 'reviewTx'
|
|
||||||
|
|
||||||
type TxStateType =
|
type TxStateType =
|
||||||
| {
|
| {
|
||||||
token: Token,
|
token: Token,
|
||||||
recipientAddress: string,
|
recipientAddress: string,
|
||||||
amount: string,
|
amount: string,
|
||||||
|
data: string,
|
||||||
}
|
}
|
||||||
| Object
|
| Object
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
smallerModalWindow: {
|
scalableModalWindow: {
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
scalableStaticModalWindow: {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
position: 'static',
|
position: 'static',
|
||||||
},
|
},
|
||||||
|
@ -49,23 +62,27 @@ const Send = ({
|
||||||
tokens,
|
tokens,
|
||||||
selectedToken,
|
selectedToken,
|
||||||
createTransaction,
|
createTransaction,
|
||||||
|
activeScreenType,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('sendFunds')
|
const [activeScreen, setActiveScreen] = useState<ActiveScreen>(activeScreenType || 'chooseTxType')
|
||||||
const [tx, setTx] = useState<TxStateType>({})
|
const [tx, setTx] = useState<TxStateType>({})
|
||||||
const smallerModalSize = activeScreen === 'chooseTxType'
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveScreen(activeScreenType || 'chooseTxType')
|
||||||
|
setTx({})
|
||||||
|
}, [isOpen])
|
||||||
|
|
||||||
|
const scalableModalSize = activeScreen === 'chooseTxType'
|
||||||
|
|
||||||
const handleTxCreation = (txInfo) => {
|
const handleTxCreation = (txInfo) => {
|
||||||
setActiveScreen('reviewTx')
|
setActiveScreen('reviewTx')
|
||||||
setTx(txInfo)
|
setTx(txInfo)
|
||||||
}
|
}
|
||||||
const onClickBack = () => setActiveScreen('sendFunds')
|
|
||||||
|
|
||||||
useEffect(
|
const handleCustomTxCreation = (customTxInfo) => {
|
||||||
() => () => {
|
setActiveScreen('reviewCustomTx')
|
||||||
setActiveScreen('sendFunds')
|
setTx(customTxInfo)
|
||||||
setTx({})
|
}
|
||||||
},
|
|
||||||
[isOpen],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -73,14 +90,15 @@ const Send = ({
|
||||||
description="Send Tokens Form"
|
description="Send Tokens Form"
|
||||||
handleClose={onClose}
|
handleClose={onClose}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
paperClassName={cn(smallerModalSize && classes.smallerModalWindow)}
|
paperClassName={cn(
|
||||||
|
scalableModalSize ? classes.scalableStaticModalWindow : classes.scalableModalWindow,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<React.Fragment>
|
<>
|
||||||
{activeScreen === 'chooseTxType' && <ChooseTxType onClose={onClose} setActiveScreen={setActiveScreen} />}
|
{activeScreen === 'chooseTxType' && <ChooseTxType onClose={onClose} setActiveScreen={setActiveScreen} />}
|
||||||
{activeScreen === 'sendFunds' && (
|
{activeScreen === 'sendFunds' && (
|
||||||
<SendFunds
|
<SendFunds
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
setActiveScreen={setActiveScreen}
|
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
etherScanLink={etherScanLink}
|
etherScanLink={etherScanLink}
|
||||||
safeName={safeName}
|
safeName={safeName}
|
||||||
|
@ -95,15 +113,38 @@ const Send = ({
|
||||||
<ReviewTx
|
<ReviewTx
|
||||||
tx={tx}
|
tx={tx}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
setActiveScreen={setActiveScreen}
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
etherScanLink={etherScanLink}
|
etherScanLink={etherScanLink}
|
||||||
safeName={safeName}
|
safeName={safeName}
|
||||||
ethBalance={ethBalance}
|
ethBalance={ethBalance}
|
||||||
onClickBack={onClickBack}
|
|
||||||
createTransaction={createTransaction}
|
createTransaction={createTransaction}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
{activeScreen === 'sendCustomTx' && (
|
||||||
|
<SendCustomTx
|
||||||
|
onClose={onClose}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeName={safeName}
|
||||||
|
ethBalance={ethBalance}
|
||||||
|
onSubmit={handleCustomTxCreation}
|
||||||
|
initialValues={tx}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeScreen === 'reviewCustomTx' && (
|
||||||
|
<ReviewCustomTx
|
||||||
|
tx={tx}
|
||||||
|
onClose={onClose}
|
||||||
|
setActiveScreen={setActiveScreen}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeName={safeName}
|
||||||
|
ethBalance={ethBalance}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import classNames from 'classnames/bind'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
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'
|
||||||
|
@ -8,11 +9,14 @@ import Button from '~/components/layout/Button'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import { lg, sm } from '~/theme/variables'
|
import Img from '~/components/layout/Img'
|
||||||
|
import Token from '../assets/token.svg'
|
||||||
|
import Code from '../assets/code.svg'
|
||||||
|
import { lg, md, sm } from '~/theme/variables'
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
heading: {
|
heading: {
|
||||||
padding: `${sm} ${lg}`,
|
padding: `${md} ${lg}`,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
maxHeight: '75px',
|
maxHeight: '75px',
|
||||||
|
@ -26,10 +30,22 @@ const styles = () => ({
|
||||||
},
|
},
|
||||||
buttonColumn: {
|
buttonColumn: {
|
||||||
padding: '52px 0',
|
padding: '52px 0',
|
||||||
|
'& > button': {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontFamily: 'Averta',
|
||||||
},
|
},
|
||||||
secondButton: {
|
|
||||||
marginTop: 10,
|
|
||||||
},
|
},
|
||||||
|
firstButton: {
|
||||||
|
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
|
iconSmall: {
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
leftIcon: {
|
||||||
|
marginRight: sm,
|
||||||
|
},
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -39,7 +55,7 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
|
const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Row align="center" grow className={classes.heading}>
|
<Row align="center" grow className={classes.heading}>
|
||||||
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
Send
|
Send
|
||||||
|
@ -57,22 +73,24 @@ const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
|
||||||
minHeight={52}
|
minHeight={52}
|
||||||
onClick={() => setActiveScreen('sendFunds')}
|
onClick={() => setActiveScreen('sendFunds')}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
className={classes.firstButton}
|
||||||
>
|
>
|
||||||
SEND FUNDS
|
<Img src={Token} alt="Send funds" className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||||
|
Send funds
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
className={classes.secondButton}
|
|
||||||
minWidth={260}
|
minWidth={260}
|
||||||
minHeight={52}
|
minHeight={52}
|
||||||
onClick={onClose}
|
onClick={() => setActiveScreen('sendCustomTx')}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
>
|
>
|
||||||
SEND CUSTOM TRANSACTION
|
<Img src={Code} alt="Send custom transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||||
|
Send custom transaction
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default withStyles(styles)(ChooseTxType)
|
export default withStyles(styles)(ChooseTxType)
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Link from '~/components/layout/Link'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Img from '~/components/layout/Img'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Identicon from '~/components/Identicon'
|
||||||
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
|
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
|
import ArrowDown from '../assets/arrow-down.svg'
|
||||||
|
import { secondary } from '~/theme/variables'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
setActiveScreen: Function,
|
||||||
|
classes: Object,
|
||||||
|
safeAddress: string,
|
||||||
|
etherScanLink: string,
|
||||||
|
safeName: string,
|
||||||
|
ethBalance: string,
|
||||||
|
tx: Object,
|
||||||
|
createTransaction: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
const openIconStyle = {
|
||||||
|
height: '16px',
|
||||||
|
color: secondary,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReviewCustomTx = ({
|
||||||
|
onClose,
|
||||||
|
setActiveScreen,
|
||||||
|
classes,
|
||||||
|
safeAddress,
|
||||||
|
etherScanLink,
|
||||||
|
safeName,
|
||||||
|
ethBalance,
|
||||||
|
tx,
|
||||||
|
createTransaction,
|
||||||
|
}: Props) => (
|
||||||
|
<SharedSnackbarConsumer>
|
||||||
|
{({ openSnackbar }) => {
|
||||||
|
const submitTx = async () => {
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const txRecipient = tx.recipientAddress
|
||||||
|
const txData = tx.data
|
||||||
|
const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : 0
|
||||||
|
|
||||||
|
createTransaction(safeAddress, txRecipient, txValue, txData, openSnackbar)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
||||||
|
Send Funds
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block className={classes.container}>
|
||||||
|
<SafeInfo
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeName={safeName}
|
||||||
|
ethBalance={ethBalance}
|
||||||
|
/>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={1}>
|
||||||
|
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11} center="xs" layout="column">
|
||||||
|
<Hairline />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="xs">
|
||||||
|
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||||
|
Recipient
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md" align="center">
|
||||||
|
<Col xs={1}>
|
||||||
|
<Identicon address={tx.recipientAddress} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11} layout="column">
|
||||||
|
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
|
||||||
|
{tx.recipientAddress}
|
||||||
|
<Link to={etherScanLink} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="xs">
|
||||||
|
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||||
|
Value
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md" align="center">
|
||||||
|
<Img src={getEthAsToken().logoUri} height={28} alt="Ether" onError={setImageToPlaceholder} />
|
||||||
|
<Paragraph size="md" noMargin className={classes.value}>
|
||||||
|
{tx.value || 0}
|
||||||
|
{' ETH'}
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row margin="xs">
|
||||||
|
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||||
|
Data (hex encoded)
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md" align="center">
|
||||||
|
<Col className={classes.outerData}>
|
||||||
|
<Row size="md" className={classes.data}>
|
||||||
|
{tx.data}
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button minWidth={140} onClick={() => setActiveScreen('sendCustomTx')}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={submitTx}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
data-testid="submit-tx-btn"
|
||||||
|
className={classes.submitButton}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</SharedSnackbarConsumer>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withStyles(styles)(ReviewCustomTx)
|
|
@ -0,0 +1,60 @@
|
||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
lg, md, sm, secondaryText, border,
|
||||||
|
} from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '75px',
|
||||||
|
},
|
||||||
|
annotation: {
|
||||||
|
letterSpacing: '-1px',
|
||||||
|
color: secondaryText,
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginLeft: '20px',
|
||||||
|
},
|
||||||
|
headingText: {
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
marginLeft: sm,
|
||||||
|
},
|
||||||
|
outerData: {
|
||||||
|
borderRadius: '5px',
|
||||||
|
border: `1px solid ${border}`,
|
||||||
|
padding: '11px',
|
||||||
|
minHeight: '21px',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
overflow: 'auto',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'Averta',
|
||||||
|
maxHeight: '100px',
|
||||||
|
letterSpacing: 'normal',
|
||||||
|
fontStretch: 'normal',
|
||||||
|
lineHeight: '1.43',
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
'& > button': {
|
||||||
|
fontFamily: 'Averta',
|
||||||
|
fontSize: '16px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submitButton: {
|
||||||
|
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||||
|
marginLeft: '15px',
|
||||||
|
},
|
||||||
|
})
|
|
@ -27,11 +27,11 @@ import { styles } from './style'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
|
setActiveScreen: Function,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
etherScanLink: string,
|
etherScanLink: string,
|
||||||
safeName: string,
|
safeName: string,
|
||||||
onClickBack: Function,
|
|
||||||
ethBalance: string,
|
ethBalance: string,
|
||||||
tx: Object,
|
tx: Object,
|
||||||
createTransaction: Function,
|
createTransaction: Function,
|
||||||
|
@ -44,13 +44,13 @@ const openIconStyle = {
|
||||||
|
|
||||||
const ReviewTx = ({
|
const ReviewTx = ({
|
||||||
onClose,
|
onClose,
|
||||||
|
setActiveScreen,
|
||||||
classes,
|
classes,
|
||||||
safeAddress,
|
safeAddress,
|
||||||
etherScanLink,
|
etherScanLink,
|
||||||
safeName,
|
safeName,
|
||||||
ethBalance,
|
ethBalance,
|
||||||
tx,
|
tx,
|
||||||
onClickBack,
|
|
||||||
createTransaction,
|
createTransaction,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<SharedSnackbarConsumer>
|
<SharedSnackbarConsumer>
|
||||||
|
@ -138,20 +138,19 @@ const ReviewTx = ({
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClickBack}>
|
<Button minWidth={140} onClick={() => setActiveScreen('sendFunds')}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={classes.button}
|
|
||||||
onClick={submitTx}
|
onClick={submitTx}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
minHeight={42}
|
|
||||||
color="primary"
|
color="primary"
|
||||||
data-testid="submit-tx-btn"
|
data-testid="submit-tx-btn"
|
||||||
|
className={classes.submitButton}
|
||||||
>
|
>
|
||||||
SUBMIT
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = () => ({
|
||||||
heading: {
|
heading: {
|
||||||
padding: `${sm} ${lg}`,
|
padding: `${md} ${lg}`,
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
maxHeight: '75px',
|
maxHeight: '75px',
|
||||||
|
@ -32,8 +32,13 @@ export const styles = () => ({
|
||||||
buttonRow: {
|
buttonRow: {
|
||||||
height: '84px',
|
height: '84px',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
position: 'absolute',
|
'& > button': {
|
||||||
bottom: 0,
|
fontFamily: 'Averta',
|
||||||
width: '100%',
|
fontSize: '16px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submitButton: {
|
||||||
|
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||||
|
marginLeft: '15px',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import GnoForm from '~/components/forms/GnoForm'
|
||||||
|
import AddressInput from '~/components/forms/AddressInput'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import ButtonLink from '~/components/layout/ButtonLink'
|
||||||
|
import Field from '~/components/forms/Field'
|
||||||
|
import TextField from '~/components/forms/TextField'
|
||||||
|
import TextareaField from '~/components/forms/TextareaField'
|
||||||
|
import {
|
||||||
|
composeValidators,
|
||||||
|
mustBeFloat,
|
||||||
|
maxValue,
|
||||||
|
mustBeEthereumContractAddress,
|
||||||
|
} from '~/components/forms/validator'
|
||||||
|
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
|
import ArrowDown from '../assets/arrow-down.svg'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
classes: Object,
|
||||||
|
safeAddress: string,
|
||||||
|
etherScanLink: string,
|
||||||
|
safeName: string,
|
||||||
|
ethBalance: string,
|
||||||
|
onSubmit: Function,
|
||||||
|
initialValues: Object,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SendCustomTx = ({
|
||||||
|
classes,
|
||||||
|
onClose,
|
||||||
|
safeAddress,
|
||||||
|
etherScanLink,
|
||||||
|
safeName,
|
||||||
|
ethBalance,
|
||||||
|
onSubmit,
|
||||||
|
initialValues,
|
||||||
|
}: Props) => {
|
||||||
|
const handleSubmit = (values: Object) => {
|
||||||
|
if (values.data || values.value) {
|
||||||
|
onSubmit(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formMutators = {
|
||||||
|
setMax: (args, state, utils) => {
|
||||||
|
utils.changeValue(state, 'value', () => ethBalance)
|
||||||
|
},
|
||||||
|
setRecipient: (args, state, utils) => {
|
||||||
|
utils.changeValue(state, 'recipientAddress', () => args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Send custom transactions
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>1 of 2</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<GnoForm onSubmit={handleSubmit} formMutators={formMutators} initialValues={initialValues}>
|
||||||
|
{(...args) => {
|
||||||
|
const mutators = args[3]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<SafeInfo
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeName={safeName}
|
||||||
|
ethBalance={ethBalance}
|
||||||
|
/>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={1}>
|
||||||
|
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11} center="xs" layout="column">
|
||||||
|
<Hairline />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={12}>
|
||||||
|
<AddressInput
|
||||||
|
name="recipientAddress"
|
||||||
|
component={TextField}
|
||||||
|
placeholder="Recipient*"
|
||||||
|
text="Recipient*"
|
||||||
|
className={classes.addressInput}
|
||||||
|
fieldMutator={mutators.setRecipient}
|
||||||
|
validators={[mustBeEthereumContractAddress]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="xs">
|
||||||
|
<Col between="lg">
|
||||||
|
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||||
|
Value
|
||||||
|
</Paragraph>
|
||||||
|
<ButtonLink weight="bold" onClick={mutators.setMax}>
|
||||||
|
Send max
|
||||||
|
</ButtonLink>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col>
|
||||||
|
<Field
|
||||||
|
name="value"
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(
|
||||||
|
mustBeFloat,
|
||||||
|
maxValue(ethBalance),
|
||||||
|
)}
|
||||||
|
placeholder="Value*"
|
||||||
|
text="Value*"
|
||||||
|
className={classes.addressInput}
|
||||||
|
inputAdornment={{
|
||||||
|
endAdornment: <InputAdornment position="end">ETH</InputAdornment>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="sm">
|
||||||
|
<Col>
|
||||||
|
<TextareaField
|
||||||
|
name="data"
|
||||||
|
type="text"
|
||||||
|
placeholder="Data (hex encoded)*"
|
||||||
|
text="Data (hex encoded)*"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button minWidth={140} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
data-testid="review-tx-btn"
|
||||||
|
className={classes.submitButton}
|
||||||
|
>
|
||||||
|
Review
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</GnoForm>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(SendCustomTx)
|
|
@ -0,0 +1,45 @@
|
||||||
|
// @flow
|
||||||
|
import { lg, md } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '75px',
|
||||||
|
},
|
||||||
|
annotation: {
|
||||||
|
letterSpacing: '-1px',
|
||||||
|
color: '#a2a8ba',
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginLeft: '20px',
|
||||||
|
},
|
||||||
|
manage: {
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
formContainer: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
'& > button': {
|
||||||
|
fontFamily: 'Averta',
|
||||||
|
fontSize: '16px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submitButton: {
|
||||||
|
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||||
|
marginLeft: '15px',
|
||||||
|
},
|
||||||
|
dataInput: {
|
||||||
|
'& TextField-root-294': {
|
||||||
|
lineHeight: 'auto',
|
||||||
|
border: 'green',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -81,8 +81,20 @@ const SendFunds = ({
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
|
<GnoForm onSubmit={handleSubmit} formMutators={formMutators} initialValues={initialValues}>
|
||||||
|
{(...args) => {
|
||||||
|
const formState = args[2]
|
||||||
|
const mutators = args[3]
|
||||||
|
const { token } = formState.values
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<Block className={classes.formContainer}>
|
<Block className={classes.formContainer}>
|
||||||
<SafeInfo safeAddress={safeAddress} etherScanLink={etherScanLink} safeName={safeName} ethBalance={ethBalance} />
|
<SafeInfo
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeName={safeName}
|
||||||
|
ethBalance={ethBalance}
|
||||||
|
/>
|
||||||
<Row margin="md">
|
<Row margin="md">
|
||||||
<Col xs={1}>
|
<Col xs={1}>
|
||||||
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
|
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
|
||||||
|
@ -91,14 +103,6 @@ const SendFunds = ({
|
||||||
<Hairline />
|
<Hairline />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<GnoForm onSubmit={handleSubmit} formMutators={formMutators} initialValues={initialValues}>
|
|
||||||
{(...args) => {
|
|
||||||
const formState = args[2]
|
|
||||||
const mutators = args[3]
|
|
||||||
const { token } = formState.values
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Row margin="md">
|
<Row margin="md">
|
||||||
<Col xs={12}>
|
<Col xs={12}>
|
||||||
<AddressInput
|
<AddressInput
|
||||||
|
@ -154,18 +158,19 @@ const SendFunds = ({
|
||||||
</OnChange>
|
</OnChange>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClose}>
|
<Button minWidth={140} onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
minHeight={42}
|
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
color="primary"
|
color="primary"
|
||||||
data-testid="review-tx-btn"
|
data-testid="review-tx-btn"
|
||||||
|
className={classes.submitButton}
|
||||||
>
|
>
|
||||||
Review
|
Review
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -174,7 +179,6 @@ const SendFunds = ({
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</GnoForm>
|
</GnoForm>
|
||||||
</Block>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import {
|
import { lg, md, secondaryText } from '~/theme/variables'
|
||||||
lg, md, sm, secondaryText,
|
|
||||||
} from '~/theme/variables'
|
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = () => ({
|
||||||
heading: {
|
heading: {
|
||||||
padding: `${sm} ${lg}`,
|
padding: `${md} ${lg}`,
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
maxHeight: '75px',
|
maxHeight: '75px',
|
||||||
|
@ -29,5 +27,13 @@ export const styles = () => ({
|
||||||
buttonRow: {
|
buttonRow: {
|
||||||
height: '84px',
|
height: '84px',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
'& > button': {
|
||||||
|
fontFamily: 'Averta',
|
||||||
|
fontSize: '16px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submitButton: {
|
||||||
|
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||||
|
marginLeft: '15px',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
|
||||||
|
<path fill="#001428" fill-rule="nonzero" d="M0 1.556C0 .696.696 0 1.556 0h10.888C13.304 0 14 .696 14 1.556v10.888c0 .86-.696 1.556-1.556 1.556H1.556C.692 14 0 13.3 0 12.444V1.556zm2.333.777v9.334h3.111V10.11H3.89V3.89h1.555V2.333h-3.11zm7.778 7.778H8.556v1.556h3.11V2.333h-3.11V3.89h1.555v6.222z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 394 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="14" viewBox="0 0 20 14">
|
||||||
|
<path fill="#FFF" fill-rule="nonzero" d="M12.25 0a7 7 0 1 1 0 14 7 7 0 0 1 0-14zm0 12.25a5.25 5.25 0 1 0 0-10.5 5.25 5.25 0 0 0 0 10.5zM1.75 7a5.242 5.242 0 0 0 3.5 4.944v1.829A6.995 6.995 0 0 1 0 7 6.995 6.995 0 0 1 5.25.228v1.828A5.242 5.242 0 0 0 1.75 7z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 356 B |
|
@ -119,10 +119,10 @@ const AddCustomToken = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<GnoForm onSubmit={handleSubmit} initialValues={formValues} testId={ADD_CUSTOM_TOKEN_FORM}>
|
<GnoForm onSubmit={handleSubmit} initialValues={formValues} testId={ADD_CUSTOM_TOKEN_FORM}>
|
||||||
{() => (
|
{() => (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Block className={classes.formContainer}>
|
<Block className={classes.formContainer}>
|
||||||
<Paragraph noMargin className={classes.title} weight="bolder" size="lg">
|
<Paragraph noMargin className={classes.title} weight="bolder" size="lg">
|
||||||
Add custom token
|
Add custom token
|
||||||
|
@ -189,17 +189,17 @@ const AddCustomToken = (props: Props) => {
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={goBackToTokenList}>
|
<Button minHeight={42} minWidth={140} onClick={goBackToTokenList}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" className={classes.button} variant="contained" minWidth={140} color="primary">
|
<Button type="submit" variant="contained" minWidth={140} minHeight={42} color="primary">
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
</GnoForm>
|
</GnoForm>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,4 @@ export const styles = () => ({
|
||||||
height: '84px',
|
height: '84px',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
button: {
|
|
||||||
height: '42px',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import classNames from 'classnames/bind'
|
import classNames from 'classnames/bind'
|
||||||
import CallMade from '@material-ui/icons/CallMade'
|
|
||||||
import CallReceived from '@material-ui/icons/CallReceived'
|
|
||||||
import Checkbox from '@material-ui/core/Checkbox'
|
import Checkbox from '@material-ui/core/Checkbox'
|
||||||
import TableRow from '@material-ui/core/TableRow'
|
import TableRow from '@material-ui/core/TableRow'
|
||||||
import TableCell from '@material-ui/core/TableCell'
|
import TableCell from '@material-ui/core/TableCell'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import CallMade from '@material-ui/icons/CallMade'
|
||||||
|
import CallReceived from '@material-ui/icons/CallReceived'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
|
@ -178,7 +178,7 @@ class Balances extends React.Component<Props, State> {
|
||||||
onClick={() => this.showSendFunds(row.asset.name)}
|
onClick={() => this.showSendFunds(row.asset.name)}
|
||||||
testId="balance-send-btn"
|
testId="balance-send-btn"
|
||||||
>
|
>
|
||||||
<CallMade className={classNames(classes.leftIcon, classes.iconSmall)} />
|
<CallMade alt="Send Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
@ -189,7 +189,7 @@ class Balances extends React.Component<Props, State> {
|
||||||
className={classes.receive}
|
className={classes.receive}
|
||||||
onClick={this.onShow('Receive')}
|
onClick={this.onShow('Receive')}
|
||||||
>
|
>
|
||||||
<CallReceived className={classNames(classes.leftIcon, classes.iconSmall)} />
|
<CallReceived alt="Receive Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||||
Receive
|
Receive
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -207,6 +207,7 @@ class Balances extends React.Component<Props, State> {
|
||||||
tokens={activeTokens}
|
tokens={activeTokens}
|
||||||
selectedToken={sendFunds.selectedToken}
|
selectedToken={sendFunds.selectedToken}
|
||||||
createTransaction={createTransaction}
|
createTransaction={createTransaction}
|
||||||
|
activeScreenType="sendFunds"
|
||||||
/>
|
/>
|
||||||
<Modal
|
<Modal
|
||||||
title="Receive Tokens"
|
title="Receive Tokens"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { sm, xs } from '~/theme/variables'
|
import { sm } from '~/theme/variables'
|
||||||
|
|
||||||
export const styles = (theme: Object) => ({
|
export const styles = (theme: Object) => ({
|
||||||
root: {
|
root: {
|
||||||
|
@ -30,17 +30,25 @@ export const styles = (theme: Object) => ({
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
},
|
},
|
||||||
send: {
|
|
||||||
minWidth: '0px',
|
|
||||||
marginRight: sm,
|
|
||||||
width: '70px',
|
|
||||||
},
|
|
||||||
receive: {
|
receive: {
|
||||||
minWidth: '0px',
|
|
||||||
width: '95px',
|
width: '95px',
|
||||||
|
minWidth: '95px',
|
||||||
|
marginLeft: sm,
|
||||||
|
borderRadius: '4px',
|
||||||
|
'& > span': {
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
send: {
|
||||||
|
width: '75px',
|
||||||
|
minWidth: '75px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
'& > span': {
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
leftIcon: {
|
leftIcon: {
|
||||||
marginRight: xs,
|
marginRight: sm,
|
||||||
},
|
},
|
||||||
links: {
|
links: {
|
||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
|
|
|
@ -1,27 +1,35 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import classNames from 'classnames/bind'
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
import Tabs from '@material-ui/core/Tabs'
|
import Tabs from '@material-ui/core/Tabs'
|
||||||
import Tab from '@material-ui/core/Tab'
|
import Tab from '@material-ui/core/Tab'
|
||||||
|
import CallMade from '@material-ui/icons/CallMade'
|
||||||
|
import CallReceived from '@material-ui/icons/CallReceived'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Identicon from '~/components/Identicon'
|
import Identicon from '~/components/Identicon'
|
||||||
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 Button from '~/components/layout/Button'
|
||||||
import Link from '~/components/layout/Link'
|
import Link from '~/components/layout/Link'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Modal from '~/components/Modal'
|
||||||
|
import SendModal from './Balances/SendModal'
|
||||||
|
import Receive from './Balances/Receive'
|
||||||
import NoSafe from '~/components/NoSafe'
|
import NoSafe from '~/components/NoSafe'
|
||||||
import { type SelectorProps } from '~/routes/safe/container/selector'
|
import { type SelectorProps } from '~/routes/safe/container/selector'
|
||||||
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
||||||
import {
|
import {
|
||||||
sm, xs, secondary, smallFontSize, border, secondaryText,
|
secondary, border,
|
||||||
} from '~/theme/variables'
|
} from '~/theme/variables'
|
||||||
import { copyToClipboard } from '~/utils/clipboard'
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
import { type Actions } from '../container/actions'
|
import { type Actions } from '../container/actions'
|
||||||
import Balances from './Balances'
|
import Balances from './Balances'
|
||||||
import Transactions from './Transactions'
|
import Transactions from './Transactions'
|
||||||
import Settings from './Settings'
|
import Settings from './Settings'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn'
|
export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn'
|
||||||
export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn'
|
export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn'
|
||||||
|
@ -36,6 +44,12 @@ type Props = SelectorProps &
|
||||||
Actions & {
|
Actions & {
|
||||||
classes: Object,
|
classes: Object,
|
||||||
granted: boolean,
|
granted: boolean,
|
||||||
|
sendFunds: Object,
|
||||||
|
showReceive: boolean,
|
||||||
|
onShow: Function,
|
||||||
|
onHide: Function,
|
||||||
|
showSendFunds: Function,
|
||||||
|
hideSendFunds: Function
|
||||||
}
|
}
|
||||||
|
|
||||||
const openIconStyle = {
|
const openIconStyle = {
|
||||||
|
@ -43,40 +57,6 @@ const openIconStyle = {
|
||||||
color: secondary,
|
color: secondary,
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = () => ({
|
|
||||||
container: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
marginLeft: sm,
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
justifyContent: 'left',
|
|
||||||
},
|
|
||||||
open: {
|
|
||||||
paddingLeft: sm,
|
|
||||||
width: 'auto',
|
|
||||||
'&:hover': {
|
|
||||||
cursor: 'pointer',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
readonly: {
|
|
||||||
fontSize: smallFontSize,
|
|
||||||
letterSpacing: '0.5px',
|
|
||||||
color: '#ffffff',
|
|
||||||
backgroundColor: secondaryText,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
padding: `0 ${sm}`,
|
|
||||||
marginLeft: sm,
|
|
||||||
borderRadius: xs,
|
|
||||||
lineHeight: '28px',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
class Layout extends React.Component<Props, State> {
|
class Layout extends React.Component<Props, State> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
@ -112,6 +92,12 @@ class Layout extends React.Component<Props, State> {
|
||||||
updateSafe,
|
updateSafe,
|
||||||
transactions,
|
transactions,
|
||||||
userAddress,
|
userAddress,
|
||||||
|
sendFunds,
|
||||||
|
showReceive,
|
||||||
|
onShow,
|
||||||
|
onHide,
|
||||||
|
showSendFunds,
|
||||||
|
hideSendFunds,
|
||||||
} = this.props
|
} = this.props
|
||||||
const { tabIndex } = this.state
|
const { tabIndex } = this.state
|
||||||
|
|
||||||
|
@ -142,6 +128,32 @@ class Layout extends React.Component<Props, State> {
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
|
<Block className={classes.balance}>
|
||||||
|
<Row align="end" className={classes.actions}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
className={classes.send}
|
||||||
|
onClick={() => showSendFunds('Ether')}
|
||||||
|
disabled={!granted}
|
||||||
|
testId="balance-send-btn"
|
||||||
|
>
|
||||||
|
<CallMade alt="Send Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
className={classes.receive}
|
||||||
|
onClick={onShow('Receive')}
|
||||||
|
>
|
||||||
|
<CallReceived alt="Receive Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||||
|
Receive
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
<Row>
|
<Row>
|
||||||
<Tabs value={tabIndex} onChange={this.handleChange} indicatorColor="secondary" textColor="secondary">
|
<Tabs value={tabIndex} onChange={this.handleChange} indicatorColor="secondary" textColor="secondary">
|
||||||
|
@ -190,6 +202,31 @@ class Layout extends React.Component<Props, State> {
|
||||||
createTransaction={createTransaction}
|
createTransaction={createTransaction}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<SendModal
|
||||||
|
onClose={hideSendFunds}
|
||||||
|
isOpen={sendFunds.isOpen}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeAddress={address}
|
||||||
|
safeName={name}
|
||||||
|
ethBalance={ethBalance}
|
||||||
|
tokens={activeTokens}
|
||||||
|
selectedToken={sendFunds.selectedToken}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
activeScreenType="chooseTxType"
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
title="Receive Tokens"
|
||||||
|
description="Receive Tokens Form"
|
||||||
|
handleClose={onHide('Receive')}
|
||||||
|
open={showReceive}
|
||||||
|
>
|
||||||
|
<Receive
|
||||||
|
safeName={name}
|
||||||
|
safeAddress={address}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
onClose={onHide('Receive')}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { List } from 'immutable'
|
||||||
import styles from '~/components/layout/PageFrame/index.scss'
|
import styles from '~/components/layout/PageFrame/index.scss'
|
||||||
import Component from './Layout'
|
import Component from './Layout'
|
||||||
|
|
||||||
const FrameDecorator = story => <div className={styles.frame}>{story()}</div>
|
const FrameDecorator = (story) => <div className={styles.frame}>{story()}</div>
|
||||||
|
|
||||||
storiesOf('Routes /safe:address', module)
|
storiesOf('Routes /safe:address', module)
|
||||||
.addDecorator(FrameDecorator)
|
.addDecorator(FrameDecorator)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as React from 'react'
|
||||||
import styles from '~/components/layout/PageFrame/index.scss'
|
import styles from '~/components/layout/PageFrame/index.scss'
|
||||||
import Component from './index.jsx'
|
import Component from './index.jsx'
|
||||||
|
|
||||||
const FrameDecorator = story => <div className={styles.frame}>{story()}</div>
|
const FrameDecorator = (story) => <div className={styles.frame}>{story()}</div>
|
||||||
|
|
||||||
storiesOf('Components', module)
|
storiesOf('Components', module)
|
||||||
.addDecorator(FrameDecorator)
|
.addDecorator(FrameDecorator)
|
||||||
|
|
|
@ -44,7 +44,7 @@ const ReviewAddOwner = ({
|
||||||
onSubmit()
|
onSubmit()
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Row align="center" grow className={classes.heading}>
|
<Row align="center" grow className={classes.heading}>
|
||||||
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
Add new owner
|
Add new owner
|
||||||
|
@ -97,7 +97,7 @@ const ReviewAddOwner = ({
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
{owners.map(owner => (
|
{owners.map((owner) => (
|
||||||
<React.Fragment key={owner.address}>
|
<React.Fragment key={owner.address}>
|
||||||
<Row className={classes.owner}>
|
<Row className={classes.owner}>
|
||||||
<Col xs={1} align="center">
|
<Col xs={1} align="center">
|
||||||
|
@ -154,22 +154,22 @@ const ReviewAddOwner = ({
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
<Button minWidth={140} minHeight={42} onClick={onClickBack}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
className={classes.button}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
|
minHeight={42}
|
||||||
color="primary"
|
color="primary"
|
||||||
testId={ADD_OWNER_SUBMIT_BTN_TEST_ID}
|
testId={ADD_OWNER_SUBMIT_BTN_TEST_ID}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ const ThresholdForm = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Row align="center" grow className={classes.heading}>
|
<Row align="center" grow className={classes.heading}>
|
||||||
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
Add new owner
|
Add new owner
|
||||||
|
@ -52,7 +52,7 @@ const ThresholdForm = ({
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<GnoForm onSubmit={handleSubmit} initialValues={{ threshold: threshold.toString() }}>
|
<GnoForm onSubmit={handleSubmit} initialValues={{ threshold: threshold.toString() }}>
|
||||||
{() => (
|
{() => (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Block className={classes.formContainer}>
|
<Block className={classes.formContainer}>
|
||||||
<Row>
|
<Row>
|
||||||
<Paragraph weight="bolder" className={classes.headingText}>
|
<Paragraph weight="bolder" className={classes.headingText}>
|
||||||
|
@ -68,8 +68,8 @@ const ThresholdForm = ({
|
||||||
<Col xs={2}>
|
<Col xs={2}>
|
||||||
<Field
|
<Field
|
||||||
name="threshold"
|
name="threshold"
|
||||||
render={props => (
|
render={(props) => (
|
||||||
<React.Fragment>
|
<>
|
||||||
<SelectField {...props} disableError>
|
<SelectField {...props} disableError>
|
||||||
{[...Array(Number(owners.size + 1))].map((x, index) => (
|
{[...Array(Number(owners.size + 1))].map((x, index) => (
|
||||||
<MenuItem key={index} value={`${index + 1}`}>
|
<MenuItem key={index} value={`${index + 1}`}>
|
||||||
|
@ -82,7 +82,7 @@ const ThresholdForm = ({
|
||||||
{props.meta.error}
|
{props.meta.error}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
validate={composeValidators(required, mustBeInteger, minValue(1), maxValue(owners.size + 1))}
|
validate={composeValidators(required, mustBeInteger, minValue(1), maxValue(owners.size + 1))}
|
||||||
data-testid="threshold-select-input"
|
data-testid="threshold-select-input"
|
||||||
|
@ -101,24 +101,24 @@ owner(s)
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
<Button minWidth={140} minHeight={42} onClick={onClickBack}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={classes.button}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
|
minHeight={42}
|
||||||
color="primary"
|
color="primary"
|
||||||
testId={ADD_OWNER_THRESHOLD_NEXT_BTN_TEST_ID}
|
testId={ADD_OWNER_THRESHOLD_NEXT_BTN_TEST_ID}
|
||||||
>
|
>
|
||||||
Review
|
Review
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
</GnoForm>
|
</GnoForm>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ const EditOwnerComponent = ({
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<GnoForm onSubmit={handleSubmit}>
|
<GnoForm onSubmit={handleSubmit}>
|
||||||
{() => (
|
{() => (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Block className={classes.container}>
|
<Block className={classes.container}>
|
||||||
<Row margin="md">
|
<Row margin="md">
|
||||||
<Field
|
<Field
|
||||||
|
@ -102,14 +102,14 @@ const EditOwnerComponent = ({
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
<Button minWidth={140} minHeight={42} onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" className={classes.button} variant="contained" minWidth={140} color="primary" testId={SAVE_OWNER_CHANGES_BTN_TEST_ID}>
|
<Button type="submit" variant="contained" minWidth={140} minHeight={42} color="primary" testId={SAVE_OWNER_CHANGES_BTN_TEST_ID}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
</GnoForm>
|
</GnoForm>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -79,13 +79,14 @@ const CheckOwner = ({
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
<Button minWidth={140} minHeight={42} onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
|
minHeight={42}
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
testId={REMOVE_OWNER_MODAL_NEXT_BTN_TEST_ID}
|
testId={REMOVE_OWNER_MODAL_NEXT_BTN_TEST_ID}
|
||||||
|
|
|
@ -175,14 +175,14 @@ Safe owner(s)
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
<Button minWidth={140} minHeight={42} onClick={onClickBack}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
className={classes.button}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
minHeight={42}
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
color="primary"
|
color="primary"
|
||||||
testId={REMOVE_OWNER_REVIEW_BTN_TEST_ID}
|
testId={REMOVE_OWNER_REVIEW_BTN_TEST_ID}
|
||||||
|
|
|
@ -105,13 +105,13 @@ owner(s)
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
<Button minWidth={140} minHeight={42} onClick={onClickBack}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={classes.button}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
minHeight={42}
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
color="primary"
|
color="primary"
|
||||||
data-testid={REMOVE_OWNER_THRESHOLD_NEXT_BTN_TEST_ID}
|
data-testid={REMOVE_OWNER_THRESHOLD_NEXT_BTN_TEST_ID}
|
||||||
|
|
|
@ -58,7 +58,7 @@ const ReviewRemoveOwner = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Row align="center" grow className={classes.heading}>
|
<Row align="center" grow className={classes.heading}>
|
||||||
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
Replace owner
|
Replace owner
|
||||||
|
@ -112,7 +112,7 @@ const ReviewRemoveOwner = ({
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
{owners.map(
|
{owners.map(
|
||||||
owner => owner.address !== ownerAddress && (
|
(owner) => owner.address !== ownerAddress && (
|
||||||
<React.Fragment key={owner.address}>
|
<React.Fragment key={owner.address}>
|
||||||
<Row className={classes.owner}>
|
<Row className={classes.owner}>
|
||||||
<Col xs={1} align="center">
|
<Col xs={1} align="center">
|
||||||
|
@ -200,14 +200,14 @@ const ReviewRemoveOwner = ({
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
<Button minWidth={140} minHeight={42} onClick={onClickBack}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
className={classes.button}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
minHeight={42}
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
color="primary"
|
color="primary"
|
||||||
testId={REPLACE_OWNER_SUBMIT_BTN_TEST_ID}
|
testId={REPLACE_OWNER_SUBMIT_BTN_TEST_ID}
|
||||||
|
@ -215,7 +215,7 @@ const ReviewRemoveOwner = ({
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ const RemoveSafeComponent = ({
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
<Button minWidth={140} minHeight={42} onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
lg, md, sm, error, background,
|
lg, md, sm, error, background,
|
||||||
} from '~/theme/variables'
|
} from '~/theme/variables'
|
||||||
|
|
||||||
export const styles = (theme: Object) => ({
|
export const styles = () => ({
|
||||||
heading: {
|
heading: {
|
||||||
padding: `${sm} ${lg}`,
|
padding: `${sm} ${lg}`,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
@ -27,6 +27,7 @@ export const styles = (theme: Object) => ({
|
||||||
buttonRemove: {
|
buttonRemove: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
backgroundColor: error,
|
backgroundColor: error,
|
||||||
|
height: '42px',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
|
|
|
@ -103,10 +103,10 @@ owner(s)
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
<Button minWidth={140} onClick={onClose}>
|
||||||
BACK
|
BACK
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" color="primary" className={classes.button} minWidth={140} variant="contained">
|
<Button type="submit" color="primary" minWidth={140} variant="contained">
|
||||||
CHANGE
|
CHANGE
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const styles = () => ({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
'&:first-child': {
|
'&:first-child': {
|
||||||
borderRadius: '8px',
|
borderTopLeftRadius: '8px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
|
|
|
@ -60,7 +60,7 @@ const ApproveTxModal = ({
|
||||||
const { title, description } = getModalTitleAndDescription(thresholdReached)
|
const { title, description } = getModalTitleAndDescription(thresholdReached)
|
||||||
const oneConfirmationLeft = tx.confirmations.size + 1 === threshold
|
const oneConfirmationLeft = tx.confirmations.size + 1 === threshold
|
||||||
|
|
||||||
const handleExecuteCheckbox = () => setApproveAndExecute(prevApproveAndExecute => !prevApproveAndExecute)
|
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SharedSnackbarConsumer>
|
<SharedSnackbarConsumer>
|
||||||
|
@ -104,12 +104,11 @@ const ApproveTxModal = ({
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClose}>
|
<Button minWidth={140} minHeight={42} onClick={onClose}>
|
||||||
Exit
|
Exit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={classes.button}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
minWidth={214}
|
minWidth={214}
|
||||||
minHeight={42}
|
minHeight={42}
|
||||||
|
|
|
@ -65,12 +65,11 @@ const CancelTxModal = ({
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClose}>
|
<Button minWidth={140} minHeight={42} onClick={onClose}>
|
||||||
Exit
|
Exit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={classes.button}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
minWidth={214}
|
minWidth={214}
|
||||||
minHeight={42}
|
minHeight={42}
|
||||||
|
|
|
@ -13,11 +13,15 @@ export const TRANSACTIONS_DESC_ADD_OWNER_TEST_ID = 'tx-description-add-owner'
|
||||||
export const TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID = 'tx-description-remove-owner'
|
export const TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID = 'tx-description-remove-owner'
|
||||||
export const TRANSACTIONS_DESC_CHANGE_THRESHOLD_TEST_ID = 'tx-description-change-threshold'
|
export const TRANSACTIONS_DESC_CHANGE_THRESHOLD_TEST_ID = 'tx-description-change-threshold'
|
||||||
export const TRANSACTIONS_DESC_SEND_TEST_ID = 'tx-description-send'
|
export const TRANSACTIONS_DESC_SEND_TEST_ID = 'tx-description-send'
|
||||||
|
export const TRANSACTIONS_DESC_CUSTOM_TEST_ID = 'tx-description-custom'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = () => ({
|
||||||
txDataContainer: {
|
txDataContainer: {
|
||||||
padding: `${lg} ${md}`,
|
padding: `${lg} ${md}`,
|
||||||
},
|
},
|
||||||
|
txData: {
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -37,6 +41,11 @@ type DescriptionDescProps = {
|
||||||
newThreshold?: string,
|
newThreshold?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomDescProps = {
|
||||||
|
data: String,
|
||||||
|
classes: Obeject,
|
||||||
|
}
|
||||||
|
|
||||||
const TransferDescription = ({ value = '', symbol, recipient }: TransferDescProps) => (
|
const TransferDescription = ({ value = '', symbol, recipient }: TransferDescProps) => (
|
||||||
<Paragraph noMargin data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
|
<Paragraph noMargin data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
|
||||||
<Bold>
|
<Bold>
|
||||||
|
@ -79,9 +88,19 @@ const SettingsDescription = ({ removedOwner, addedOwner, newThreshold }: Descrip
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CustomDescription = ({ data, classes }: CustomDescProps) => (
|
||||||
|
<>
|
||||||
|
<Paragraph className={classes.txData} data-testid={TRANSACTIONS_DESC_CUSTOM_TEST_ID}>
|
||||||
|
<Bold>Data (hex encoded):</Bold>
|
||||||
|
<br />
|
||||||
|
{data}
|
||||||
|
</Paragraph>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
const TxDescription = ({ tx, classes }: Props) => {
|
const TxDescription = ({ tx, classes }: Props) => {
|
||||||
const {
|
const {
|
||||||
recipient, value, modifySettingsTx, removedOwner, addedOwner, newThreshold, cancellationTx,
|
recipient, value, modifySettingsTx, removedOwner, addedOwner, newThreshold, cancellationTx, customTx, data,
|
||||||
} = getTxData(tx)
|
} = getTxData(tx)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -89,7 +108,10 @@ const TxDescription = ({ tx, classes }: Props) => {
|
||||||
{modifySettingsTx && (
|
{modifySettingsTx && (
|
||||||
<SettingsDescription removedOwner={removedOwner} newThreshold={newThreshold} addedOwner={addedOwner} />
|
<SettingsDescription removedOwner={removedOwner} newThreshold={newThreshold} addedOwner={addedOwner} />
|
||||||
)}
|
)}
|
||||||
{!cancellationTx && !modifySettingsTx && (
|
{customTx && (
|
||||||
|
<CustomDescription data={data} classes={classes} />
|
||||||
|
)}
|
||||||
|
{!cancellationTx && !modifySettingsTx && !customTx && (
|
||||||
<TransferDescription value={value} symbol={tx.symbol} recipient={recipient} />
|
<TransferDescription value={value} symbol={tx.symbol} recipient={recipient} />
|
||||||
)}
|
)}
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -10,6 +10,8 @@ type DecodedTxData = {
|
||||||
newThreshold?: string,
|
newThreshold?: string,
|
||||||
addedOwner?: string,
|
addedOwner?: string,
|
||||||
cancellationTx?: boolean,
|
cancellationTx?: boolean,
|
||||||
|
customTx?: boolean,
|
||||||
|
data: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTxData = (tx: Transaction): DecodedTxData => {
|
export const getTxData = (tx: Transaction): DecodedTxData => {
|
||||||
|
@ -47,6 +49,9 @@ export const getTxData = (tx: Transaction): DecodedTxData => {
|
||||||
}
|
}
|
||||||
} else if (tx.cancellationTx) {
|
} else if (tx.cancellationTx) {
|
||||||
txData.cancellationTx = true
|
txData.cancellationTx = true
|
||||||
|
} else if (tx.customTx) {
|
||||||
|
txData.data = tx.data
|
||||||
|
txData.customTx = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return txData
|
return txData
|
||||||
|
|
|
@ -50,6 +50,8 @@ export const getTxTableData = (transactions: List<Transaction>): List<Transactio
|
||||||
txType = 'Modify Safe Settings'
|
txType = 'Modify Safe Settings'
|
||||||
} else if (tx.cancellationTx) {
|
} else if (tx.cancellationTx) {
|
||||||
txType = 'Cancellation transaction'
|
txType = 'Cancellation transaction'
|
||||||
|
} else if (tx.customTx) {
|
||||||
|
txType = 'Custom transaction'
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
sm, xs, smallFontSize, secondaryText,
|
||||||
|
} from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
marginLeft: sm,
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
justifyContent: 'left',
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
paddingLeft: sm,
|
||||||
|
width: 'auto',
|
||||||
|
'&:hover': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
fontSize: smallFontSize,
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
color: '#ffffff',
|
||||||
|
backgroundColor: secondaryText,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
padding: `0 ${sm}`,
|
||||||
|
marginLeft: sm,
|
||||||
|
borderRadius: xs,
|
||||||
|
lineHeight: '28px',
|
||||||
|
},
|
||||||
|
iconSmall: {
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
balance: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
receive: {
|
||||||
|
width: '95px',
|
||||||
|
minWidth: '95px',
|
||||||
|
marginLeft: sm,
|
||||||
|
borderRadius: '4px',
|
||||||
|
'& > span': {
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
send: {
|
||||||
|
width: '75px',
|
||||||
|
minWidth: '75px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
'& > span': {
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
leftIcon: {
|
||||||
|
marginRight: sm,
|
||||||
|
},
|
||||||
|
})
|
|
@ -6,6 +6,13 @@ import Layout from '~/routes/safe/components/Layout'
|
||||||
import selector, { type SelectorProps } from './selector'
|
import selector, { type SelectorProps } from './selector'
|
||||||
import actions, { type Actions } from './actions'
|
import actions, { type Actions } from './actions'
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
showReceive: boolean,
|
||||||
|
sendFunds: Object,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = 'Send' | 'Receive'
|
||||||
|
|
||||||
export type Props = Actions &
|
export type Props = Actions &
|
||||||
SelectorProps & {
|
SelectorProps & {
|
||||||
granted: boolean,
|
granted: boolean,
|
||||||
|
@ -13,7 +20,15 @@ export type Props = Actions &
|
||||||
|
|
||||||
const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000
|
const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000
|
||||||
|
|
||||||
class SafeView extends React.Component<Props> {
|
class SafeView extends React.Component<Props, State> {
|
||||||
|
state = {
|
||||||
|
sendFunds: {
|
||||||
|
isOpen: false,
|
||||||
|
selectedToken: undefined,
|
||||||
|
},
|
||||||
|
showReceive: false,
|
||||||
|
}
|
||||||
|
|
||||||
intervalId: IntervalID
|
intervalId: IntervalID
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -45,6 +60,32 @@ class SafeView extends React.Component<Props> {
|
||||||
clearInterval(this.intervalId)
|
clearInterval(this.intervalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onShow = (action: Action) => () => {
|
||||||
|
this.setState(() => ({ [`show${action}`]: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
onHide = (action: Action) => () => {
|
||||||
|
this.setState(() => ({ [`show${action}`]: false }))
|
||||||
|
}
|
||||||
|
|
||||||
|
showSendFunds = (token: Token) => {
|
||||||
|
this.setState({
|
||||||
|
sendFunds: {
|
||||||
|
isOpen: true,
|
||||||
|
selectedToken: token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSendFunds = () => {
|
||||||
|
this.setState({
|
||||||
|
sendFunds: {
|
||||||
|
isOpen: false,
|
||||||
|
selectedToken: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
checkForUpdates() {
|
checkForUpdates() {
|
||||||
const {
|
const {
|
||||||
safeUrl, activeTokens, fetchTokenBalances, fetchEtherBalance,
|
safeUrl, activeTokens, fetchTokenBalances, fetchEtherBalance,
|
||||||
|
@ -55,6 +96,7 @@ class SafeView extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { sendFunds, showReceive } = this.state
|
||||||
const {
|
const {
|
||||||
safe,
|
safe,
|
||||||
provider,
|
provider,
|
||||||
|
@ -85,6 +127,12 @@ class SafeView extends React.Component<Props> {
|
||||||
fetchTransactions={fetchTransactions}
|
fetchTransactions={fetchTransactions}
|
||||||
updateSafe={updateSafe}
|
updateSafe={updateSafe}
|
||||||
transactions={transactions}
|
transactions={transactions}
|
||||||
|
sendFunds={sendFunds}
|
||||||
|
showReceive={showReceive}
|
||||||
|
onShow={this.onShow}
|
||||||
|
onHide={this.onHide}
|
||||||
|
showSendFunds={this.showSendFunds}
|
||||||
|
hideSendFunds={this.hideSendFunds}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
import { addTransactions } from './addTransactions'
|
import { addTransactions } from './addTransactions'
|
||||||
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
|
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
import { isAddressAToken } from '~/logic/tokens/utils/tokenHelpers'
|
import { isTokenTransfer } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
import { TX_TYPE_EXECUTION } from '~/logic/safe/transactions/send'
|
import { TX_TYPE_EXECUTION } from '~/logic/safe/transactions/send'
|
||||||
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
|
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
|
||||||
|
|
||||||
|
@ -59,7 +59,8 @@ export const buildTransactionFrom = async (
|
||||||
)
|
)
|
||||||
const modifySettingsTx = tx.to === safeAddress && Number(tx.value) === 0 && !!tx.data
|
const modifySettingsTx = tx.to === safeAddress && Number(tx.value) === 0 && !!tx.data
|
||||||
const cancellationTx = tx.to === safeAddress && Number(tx.value) === 0 && !tx.data
|
const cancellationTx = tx.to === safeAddress && Number(tx.value) === 0 && !tx.data
|
||||||
const isTokenTransfer = await isAddressAToken(tx.to)
|
const customTx = tx.to !== safeAddress && !!tx.data
|
||||||
|
const isSendTokenTx = await isTokenTransfer(tx.data, tx.value)
|
||||||
|
|
||||||
let executionTxHash
|
let executionTxHash
|
||||||
const executionTx = confirmations.find((conf) => conf.type === TX_TYPE_EXECUTION)
|
const executionTx = confirmations.find((conf) => conf.type === TX_TYPE_EXECUTION)
|
||||||
|
@ -70,7 +71,7 @@ export const buildTransactionFrom = async (
|
||||||
|
|
||||||
let symbol = 'ETH'
|
let symbol = 'ETH'
|
||||||
let decodedParams
|
let decodedParams
|
||||||
if (isTokenTransfer) {
|
if (isSendTokenTx) {
|
||||||
const tokenContract = await getHumanFriendlyToken()
|
const tokenContract = await getHumanFriendlyToken()
|
||||||
const tokenInstance = await tokenContract.at(tx.to)
|
const tokenInstance = await tokenContract.at(tx.to)
|
||||||
symbol = await tokenInstance.symbol()
|
symbol = await tokenInstance.symbol()
|
||||||
|
@ -82,6 +83,8 @@ export const buildTransactionFrom = async (
|
||||||
}
|
}
|
||||||
} else if (modifySettingsTx && tx.data) {
|
} else if (modifySettingsTx && tx.data) {
|
||||||
decodedParams = await decodeParamsFromSafeMethod(tx.data)
|
decodedParams = await decodeParamsFromSafeMethod(tx.data)
|
||||||
|
} else if (customTx && tx.data) {
|
||||||
|
decodedParams = await decodeParamsFromSafeMethod(tx.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeTransaction({
|
return makeTransaction({
|
||||||
|
@ -97,9 +100,10 @@ export const buildTransactionFrom = async (
|
||||||
executionDate: tx.executionDate,
|
executionDate: tx.executionDate,
|
||||||
executionTxHash,
|
executionTxHash,
|
||||||
safeTxHash: tx.safeTxHash,
|
safeTxHash: tx.safeTxHash,
|
||||||
isTokenTransfer,
|
isTokenTransfer: isSendTokenTx,
|
||||||
decodedParams,
|
decodedParams,
|
||||||
modifySettingsTx,
|
modifySettingsTx,
|
||||||
|
customTx,
|
||||||
cancellationTx,
|
cancellationTx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { approveTransaction, executeTransaction, CALL } from '~/logic/safe/trans
|
||||||
const generateSignaturesFromTxConfirmations = (tx: Transaction, preApprovingOwner?: string) => {
|
const generateSignaturesFromTxConfirmations = (tx: Transaction, preApprovingOwner?: string) => {
|
||||||
// The constant parts need to be sorted so that the recovered signers are sorted ascending
|
// The constant parts need to be sorted so that the recovered signers are sorted ascending
|
||||||
// (natural order) by address (not checksummed).
|
// (natural order) by address (not checksummed).
|
||||||
let confirmedAdresses = tx.confirmations.map(conf => conf.owner.address)
|
let confirmedAdresses = tx.confirmations.map((conf) => conf.owner.address)
|
||||||
|
|
||||||
if (preApprovingOwner) {
|
if (preApprovingOwner) {
|
||||||
confirmedAdresses = confirmedAdresses.push(preApprovingOwner)
|
confirmedAdresses = confirmedAdresses.push(preApprovingOwner)
|
||||||
|
|
|
@ -18,6 +18,7 @@ export type TransactionProps = {
|
||||||
symbol: string,
|
symbol: string,
|
||||||
modifySettingsTx: boolean,
|
modifySettingsTx: boolean,
|
||||||
cancellationTx: boolean,
|
cancellationTx: boolean,
|
||||||
|
customTx: boolean,
|
||||||
safeTxHash: string,
|
safeTxHash: string,
|
||||||
executionTxHash?: string,
|
executionTxHash?: string,
|
||||||
cancelled?: boolean,
|
cancelled?: boolean,
|
||||||
|
@ -42,6 +43,7 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
modifySettingsTx: false,
|
modifySettingsTx: false,
|
||||||
cancellationTx: false,
|
cancellationTx: false,
|
||||||
|
customTx: false,
|
||||||
status: 'awaiting',
|
status: 'awaiting',
|
||||||
isTokenTransfer: false,
|
isTokenTransfer: false,
|
||||||
decodedParams: {},
|
decodedParams: {},
|
||||||
|
|
|
@ -14,12 +14,11 @@ const SafeList = ({ safes, provider }: Props) => {
|
||||||
const safesAvailable = safes && safes.count() > 0
|
const safesAvailable = safes && safes.count() > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
{ safesAvailable
|
{ safesAvailable
|
||||||
? <SafeTable safes={safes} />
|
? <SafeTable safes={safes} />
|
||||||
: <NoSafe provider={provider} text="No safes created, please create a new one" />
|
: <NoSafe provider={provider} text="No safes created, please create a new one" />}
|
||||||
}
|
</>
|
||||||
</React.Fragment>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import * as React from 'react'
|
||||||
import styles from '~/components/layout/PageFrame/index.scss'
|
import styles from '~/components/layout/PageFrame/index.scss'
|
||||||
import Component from './Layout'
|
import Component from './Layout'
|
||||||
|
|
||||||
const FrameDecorator = story => <div className={styles.frame}>{story()}</div>
|
const FrameDecorator = (story) => <div className={styles.frame}>{story()}</div>
|
||||||
|
|
||||||
storiesOf('Routes /safes', module)
|
storiesOf('Routes /safes', module)
|
||||||
.addDecorator(FrameDecorator)
|
.addDecorator(FrameDecorator)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import * as React from 'react'
|
||||||
import styles from '~/components/layout/PageFrame/index.scss'
|
import styles from '~/components/layout/PageFrame/index.scss'
|
||||||
import Component from './Layout'
|
import Component from './Layout'
|
||||||
|
|
||||||
const FrameDecorator = story => <div className={styles.frame}>{story()}</div>
|
const FrameDecorator = (story) => <div className={styles.frame}>{story()}</div>
|
||||||
|
|
||||||
storiesOf('Routes /welcome', module)
|
storiesOf('Routes /welcome', module)
|
||||||
.addDecorator(FrameDecorator)
|
.addDecorator(FrameDecorator)
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
// @flow
|
||||||
|
import { waitForElement } from '@testing-library/react'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { aNewStore } from '~/store'
|
||||||
|
import { sleep } from '~/utils/timer'
|
||||||
|
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
||||||
|
import { sendTokenTo, sendEtherTo } from '~/test/utils/tokenMovements'
|
||||||
|
import { renderSafeView } from '~/test/builder/safe.dom.utils'
|
||||||
|
import { dispatchAddTokenToList } from '~/test/utils/transactions/moveTokens.helper'
|
||||||
|
import TokenBalanceRecord from '~/routes/safe/store/models/tokenBalance'
|
||||||
|
import { calculateBalanceOf } from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||||
|
import updateActiveTokens from '~/routes/safe/store/actions/updateActiveTokens'
|
||||||
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
|
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
||||||
|
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
|
||||||
|
import { getBalanceInEtherOf } from '~/logic/wallets/getWeb3'
|
||||||
|
|
||||||
|
describe('DOM > Feature > Balances', () => {
|
||||||
|
let store
|
||||||
|
let safeAddress: string
|
||||||
|
beforeEach(async () => {
|
||||||
|
store = aNewStore()
|
||||||
|
safeAddress = await aMinedSafe(store)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Updates token balances automatically', async () => {
|
||||||
|
const tokensAmount = '100'
|
||||||
|
const tokenAddress = await sendTokenTo(safeAddress, tokensAmount)
|
||||||
|
await dispatchAddTokenToList(store, tokenAddress)
|
||||||
|
|
||||||
|
const SafeDom = await renderSafeView(store, safeAddress)
|
||||||
|
|
||||||
|
// Activate token
|
||||||
|
const safeTokenBalance = await calculateBalanceOf(tokenAddress, safeAddress, 18)
|
||||||
|
expect(safeTokenBalance).toBe(tokensAmount)
|
||||||
|
|
||||||
|
const balanceAsRecord = TokenBalanceRecord({
|
||||||
|
address: tokenAddress,
|
||||||
|
balance: safeTokenBalance,
|
||||||
|
})
|
||||||
|
store.dispatch(updateActiveTokens(safeAddress, List([tokenAddress])))
|
||||||
|
store.dispatch(updateSafe({ address: safeAddress, balances: List([balanceAsRecord]) }))
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
|
const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID)
|
||||||
|
expect(balanceRows.length).toBe(2)
|
||||||
|
|
||||||
|
await waitForElement(() => SafeDom.getByText(`${tokensAmount} OMG`))
|
||||||
|
|
||||||
|
await sendTokenTo(safeAddress, tokensAmount)
|
||||||
|
|
||||||
|
await waitForElement(() => SafeDom.getByText(`${parseInt(tokensAmount, 10) * 2} OMG`))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Updates ether balance automatically', async () => {
|
||||||
|
const etherAmount = '1'
|
||||||
|
await sendEtherTo(safeAddress, etherAmount)
|
||||||
|
|
||||||
|
const SafeDom = await renderSafeView(store, safeAddress)
|
||||||
|
|
||||||
|
const safeEthBalance = await getBalanceInEtherOf(safeAddress)
|
||||||
|
expect(safeEthBalance).toBe(etherAmount)
|
||||||
|
|
||||||
|
const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID)
|
||||||
|
expect(balanceRows.length).toBe(1)
|
||||||
|
|
||||||
|
await waitForElement(() => SafeDom.getByText(`${etherAmount} ETH`))
|
||||||
|
|
||||||
|
await sendEtherTo(safeAddress, etherAmount)
|
||||||
|
|
||||||
|
await waitForElement(() => SafeDom.getByText(`${parseInt(etherAmount, 10) * 2} ETH`))
|
||||||
|
})
|
||||||
|
})
|
|
@ -12,7 +12,10 @@ import { fillAndSubmitSendFundsForm } from './utils/transactions'
|
||||||
import { TRANSACTIONS_TAB_BTN_TEST_ID } from '~/routes/safe/components/Layout'
|
import { TRANSACTIONS_TAB_BTN_TEST_ID } from '~/routes/safe/components/Layout'
|
||||||
import { TRANSACTION_ROW_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable'
|
import { TRANSACTION_ROW_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable'
|
||||||
import { useTestAccountAt, resetTestAccount } from './utils/accounts'
|
import { useTestAccountAt, resetTestAccount } from './utils/accounts'
|
||||||
import { CONFIRM_TX_BTN_TEST_ID, EXECUTE_TX_BTN_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/ButtonRow'
|
import {
|
||||||
|
CONFIRM_TX_BTN_TEST_ID,
|
||||||
|
EXECUTE_TX_BTN_TEST_ID,
|
||||||
|
} from '~/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/ButtonRow'
|
||||||
import { APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal'
|
import { APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal'
|
||||||
|
|
||||||
afterEach(resetTestAccount)
|
afterEach(resetTestAccount)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { clickOnManageTokens, toggleToken, closeManageTokensModal } from './util
|
||||||
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
|
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
|
||||||
import { makeToken } from '~/logic/tokens/store/model/token'
|
import { makeToken } from '~/logic/tokens/store/model/token'
|
||||||
import '@testing-library/jest-dom/extend-expect'
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
|
import { getActiveTokens } from '~/logic/tokens/utils/tokensStorage'
|
||||||
|
|
||||||
describe('DOM > Feature > Enable and disable default tokens', () => {
|
describe('DOM > Feature > Enable and disable default tokens', () => {
|
||||||
let web3
|
let web3
|
||||||
|
@ -43,7 +44,7 @@ describe('DOM > Feature > Enable and disable default tokens', () => {
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allows to enable and disable tokens', async () => {
|
it('allows to enable and disable tokens, stores active ones in the local storage', async () => {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
const store = aNewStore()
|
const store = aNewStore()
|
||||||
const safeAddress = await aMinedSafe(store)
|
const safeAddress = await aMinedSafe(store)
|
||||||
|
@ -69,6 +70,13 @@ describe('DOM > Feature > Enable and disable default tokens', () => {
|
||||||
expect(balanceRows[1]).toHaveTextContent('FTE')
|
expect(balanceRows[1]).toHaveTextContent('FTE')
|
||||||
expect(balanceRows[2]).toHaveTextContent('STE')
|
expect(balanceRows[2]).toHaveTextContent('STE')
|
||||||
|
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
|
const tokensFromStorage = await getActiveTokens()
|
||||||
|
|
||||||
|
expect(Object.keys(tokensFromStorage)).toContain(firstErc20Token.address)
|
||||||
|
expect(Object.keys(tokensFromStorage)).toContain(secondErc20Token.address)
|
||||||
|
|
||||||
// disable tokens
|
// disable tokens
|
||||||
clickOnManageTokens(TokensDom)
|
clickOnManageTokens(TokensDom)
|
||||||
toggleToken(TokensDom, 'FTE')
|
toggleToken(TokensDom, 'FTE')
|
||||||
|
|
|
@ -7,7 +7,4 @@ function resetTestAccount() {
|
||||||
delete window.testAccountIndex
|
delete window.testAccountIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export { useTestAccountAt, resetTestAccount }
|
||||||
useTestAccountAt,
|
|
||||||
resetTestAccount,
|
|
||||||
}
|
|
||||||
|
|
|
@ -38,10 +38,7 @@ type FinsihedTx = {
|
||||||
finishedTransaction: boolean,
|
finishedTransaction: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const whenExecuted = (
|
export const whenExecuted = (SafeDom: React.Component<any, any>, ParentComponent: React.ElementType): Promise<void> => new Promise((resolve, reject) => {
|
||||||
SafeDom: React.Component<any, any>,
|
|
||||||
ParentComponent: React.ElementType,
|
|
||||||
): Promise<void> => new Promise((resolve, reject) => {
|
|
||||||
let times = 0
|
let times = 0
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (times >= MAX_TIMES_EXECUTED) {
|
if (times >= MAX_TIMES_EXECUTED) {
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const getLastTransaction = async (SafeDom: React.Component<any, any>) =>
|
||||||
|
|
||||||
export const checkRegisteredTxSend = async (
|
export const checkRegisteredTxSend = async (
|
||||||
SafeDom: React.Component<any, any>,
|
SafeDom: React.Component<any, any>,
|
||||||
ethAmount: number,
|
ethAmount: number | string,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
ethAddress: string,
|
ethAddress: string,
|
||||||
) => {
|
) => {
|
||||||
|
@ -34,20 +34,14 @@ export const checkRegisteredTxSend = async (
|
||||||
expect(txDescription).toHaveTextContent(`Send ${ethAmount} ${symbol} to:${shortVersionOf(ethAddress, 4)}`)
|
expect(txDescription).toHaveTextContent(`Send ${ethAmount} ${symbol} to:${shortVersionOf(ethAddress, 4)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkRegisteredTxAddOwner = async (
|
export const checkRegisteredTxAddOwner = async (SafeDom: React.Component<any, any>, ownerAddress: string) => {
|
||||||
SafeDom: React.Component<any, any>,
|
|
||||||
ownerAddress: string,
|
|
||||||
) => {
|
|
||||||
await getLastTransaction(SafeDom)
|
await getLastTransaction(SafeDom)
|
||||||
|
|
||||||
const txDescription = SafeDom.getAllByTestId(TRANSACTIONS_DESC_ADD_OWNER_TEST_ID)[0]
|
const txDescription = SafeDom.getAllByTestId(TRANSACTIONS_DESC_ADD_OWNER_TEST_ID)[0]
|
||||||
expect(txDescription).toHaveTextContent(`Add owner:${shortVersionOf(ownerAddress, 4)}`)
|
expect(txDescription).toHaveTextContent(`Add owner:${shortVersionOf(ownerAddress, 4)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkRegisteredTxRemoveOwner = async (
|
export const checkRegisteredTxRemoveOwner = async (SafeDom: React.Component<any, any>, ownerAddress: string) => {
|
||||||
SafeDom: React.Component<any, any>,
|
|
||||||
ownerAddress: string,
|
|
||||||
) => {
|
|
||||||
await getLastTransaction(SafeDom)
|
await getLastTransaction(SafeDom)
|
||||||
|
|
||||||
const txDescription = SafeDom.getAllByTestId(TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID)[0]
|
const txDescription = SafeDom.getAllByTestId(TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID)[0]
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
// @flow
|
// @flow
|
||||||
export const sleep: Function = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
export const sleep: Function = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|
Loading…
Reference in New Issue