Pull from dev, conflict fixes

This commit is contained in:
Mikhail Mikheev 2019-09-19 15:53:55 +04:00
commit 5a2d70b057
92 changed files with 1416 additions and 412 deletions

View File

@ -1,7 +1,7 @@
{
"extends": ["airbnb", "plugin:flowtype/recommended"],
"extends": ["airbnb", "plugin:flowtype/recommended", "plugin:jsx-a11y/recommended"],
"parser": "babel-eslint",
"plugins": ["jest", "flowtype"],
"plugins": ["jsx-a11y", "jest", "flowtype"],
"rules": {
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/forbid-prop-types": [1, { "forbid": ["object", "any"] }],

View File

@ -102,9 +102,7 @@ We use [SemVer](http://semver.org/) for versioning. For the versions available,
## Authors
* **Adolfo Panizo** - [apanizo](https://github.com/apanizo)
See also the list of [contributors](https://github.com/gnosis/gnosis-team-safe/contributors) who participated in this project.
See the list of [contributors](https://github.com/gnosis/gnosis-team-safe/contributors) who participated in this project.
## License

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import * as React from 'react'
import styles from '~/components/layout/PageFrame/index.scss'
import Component from './index'
const FrameDecorator = story => (
const FrameDecorator = (story) => (
<div className={styles.frame}>
<div style={{ flex: '1' }} />
{story()}

View File

@ -77,7 +77,7 @@ const KeyRing = ({
const img = isWarning ? triangle : key
return (
<React.Fragment>
<>
<Block className={classes.root}>
<Block className={classes.key} style={keyStyle}>
<Img
@ -90,7 +90,7 @@ const KeyRing = ({
</Block>
{!hideDot && <Dot className={classes.dot} style={dotStyle} />}
</Block>
</React.Fragment>
</>
)
}

View File

@ -8,7 +8,7 @@ import UserDetails from './ProviderDetails/UserDetails'
import ProviderDisconnected from './ProviderInfo/ProviderDisconnected'
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)
.addDecorator(FrameDecorator)

View File

@ -4,7 +4,7 @@ import * as React from 'react'
import styles from '~/components/layout/PageFrame/index.scss'
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)
.addDecorator(FrameDecorator)

View File

@ -26,24 +26,34 @@ type Props = {
lastPage: boolean,
disabled: boolean,
penultimate: boolean,
currentStep?: number,
buttonLabels?: Array<string>,
}
const Controls = ({
onPrevious, firstPage, penultimate, lastPage, disabled,
onPrevious,
firstPage,
penultimate,
lastPage,
disabled,
currentStep,
buttonLabels,
}: Props) => {
// eslint-disable-next-line
const next = firstPage ? 'Start' : penultimate ? 'Review' : lastPage ? 'Submit' : 'Next'
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 (
<Row style={controlStyle} align="end" grow>
<Col xs={12} end="xs">
<Button
style={firstButtonStyle}
type="button"
onClick={onPrevious}
size="small"
>
<Button style={firstButtonStyle} type="button" onClick={onPrevious} size="small">
{back}
</Button>
<Button

View File

@ -8,7 +8,7 @@ import { lg } from '~/theme/variables'
const styles = () => ({
root: {
margin: '10px',
maxWidth: '870px',
maxWidth: '770px',
boxShadow: '0 0 10px 0 rgba(33,48,77,0.10)',
},
padding: {
@ -20,27 +20,16 @@ type Props = {
classes: Object,
children: React.Node,
controls: React.Node,
container?: number,
padding?: boolean,
}
const generateContainerStyleFrom = (container?: number) => ({
maxWidth: container ? `${container}px` : undefined,
})
const OpenPaper = ({
classes, children, controls, container, padding = true,
}: Props) => {
const containerStyle = generateContainerStyleFrom(container)
return (
<Paper className={classes.root} elevation={1}>
<Block style={containerStyle} className={padding ? classes.padding : ''}>
{children}
</Block>
{controls}
</Paper>
)
}
classes, children, controls, padding = true,
}: Props) => (
<Paper className={classes.root} elevation={1}>
<Block className={padding ? classes.padding : ''}>{children}</Block>
{controls}
</Paper>
)
export default withStyles(styles)(OpenPaper)

View File

@ -19,6 +19,7 @@ type Props = {
onSubmit: (values: Object) => Promise<void>,
children: React.Node,
classes: Object,
buttonLabels: Array<string>,
initialValues?: Object,
disabledWhenValidating?: boolean,
mutators?: Object,
@ -110,7 +111,7 @@ const GnoStepper = (props: Props) => {
}
const {
steps, children, classes, disabledWhenValidating = false, testId, mutators,
steps, children, classes, disabledWhenValidating = false, testId, mutators, buttonLabels,
} = props
const activePage = getActivePageFrom(children)
@ -137,18 +138,32 @@ const GnoStepper = (props: Props) => {
firstPage={page === 0}
lastPage={lastPage}
penultimate={penultimate}
buttonLabels={buttonLabels}
currentStep={page}
/>
</>
)
return (
<Stepper classes={{ root: classes.root }} activeStep={page} orientation="vertical">
{steps.map((label) => (
<FormStep key={label}>
<StepLabel>{label}</StepLabel>
<StepContent TransitionProps={transitionProps}>{activePage(controls, ...rest)}</StepContent>
</FormStep>
))}
{steps.map((label, index) => {
const labelProps = {}
const isClickable = index < page
if (isClickable) {
labelProps.onClick = () => {
setPage(index)
}
labelProps.className = classes.pointerCursor
}
return (
<FormStep key={label}>
<StepLabel {...labelProps}>{label}</StepLabel>
<StepContent TransitionProps={transitionProps}>{activePage(controls, ...rest)}</StepContent>
</FormStep>
)
})}
</Stepper>
)
}}
@ -162,6 +177,14 @@ const styles = {
flex: '1 1 auto',
backgroundColor: 'transparent',
},
pointerCursor: {
'& > .MuiStepLabel-iconContainer': {
cursor: 'pointer',
},
'& > .MuiStepLabel-labelContainer': {
cursor: 'pointer',
},
},
}
export default withStyles(styles)(GnoStepper)

View File

@ -39,7 +39,7 @@ export const stableSort = (dataArray: List<any>, cmp: any, fixed: boolean): List
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)
}

View File

@ -30,6 +30,8 @@ class TextField extends React.PureComponent<TextFieldProps> {
inputAdornment,
classes,
testId,
rows,
multiline,
...rest
} = this.props
const helperText = value ? text : undefined
@ -53,6 +55,8 @@ class TextField extends React.PureComponent<TextFieldProps> {
onChange={onChange}
value={value}
// data-testid={testId}
rows={rows}
multiline={multiline}
/>
)
}

View File

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

View File

@ -18,11 +18,11 @@ export const simpleMemoize = (fn: Function) => {
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 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) => {
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'
})
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 ADDRESS_REPEATED_ERROR = 'Address already introduced'

View File

@ -1,9 +1,5 @@
// @flow
import {
TX_SERVICE_HOST,
SIGNATURES_VIA_METAMASK,
RELAY_API_URL,
} from '~/config/names'
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
const devConfig = {
[TX_SERVICE_HOST]: 'https://safe-transaction.staging.gnosisdev.com/api/v1/',

View File

@ -1,10 +1,6 @@
// @flow
import { ensureOnce } from '~/utils/singleton'
import {
TX_SERVICE_HOST,
SIGNATURES_VIA_METAMASK,
RELAY_API_URL,
} from '~/config/names'
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
import devConfig from './development'
import testConfig from './testing'
import prodConfig from './production'

View File

@ -1,9 +1,5 @@
// @flow
import {
TX_SERVICE_HOST,
SIGNATURES_VIA_METAMASK,
RELAY_API_URL,
} from '~/config/names'
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
const prodConfig = {
[TX_SERVICE_HOST]: 'https://safe-transaction.staging.gnosisdev.com/api/v1/',

View File

@ -1,9 +1,5 @@
// @flow
import {
TX_SERVICE_HOST,
SIGNATURES_VIA_METAMASK,
RELAY_API_URL,
} from '~/config/names'
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
const testConfig = {
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',

View File

@ -75,6 +75,23 @@ export const deploySafeContract = async (safeAccounts: string[], numConfirmation
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) => {
const web3 = getWeb3()
const GnosisSafe = await getGnosisSafeContract(web3)

View File

@ -109,8 +109,8 @@ export const executeTransaction = async (
.encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeDataUsedSignatures, sender)
console.log(`Error executing the TX: ${errMsg}`)
throw error;
throw error
}
}

View File

@ -21,7 +21,7 @@ export const getEthAsToken = (balance: string) => {
}
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
if (isDeactivated) {
return
@ -48,3 +48,5 @@ export const isAddressAToken = async (tokenAddress: string) => {
return call !== '0x'
}
export const isTokenTransfer = async (data: string, value: number) => data.substring(0, 10) === '0xa9059cbb' && value === 0

View File

@ -31,17 +31,6 @@ export const getCustomTokens = async (): Promise<List<TokenProps>> => {
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) => {
const data: List<TokenProps> = await getCustomTokens()

View File

@ -16,6 +16,7 @@ import OpenPaper from '~/components/Stepper/OpenPaper'
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { getSafeMasterContract } from '~/logic/contracts/safeContracts'
import { secondary } from '~/theme/variables'
type Props = {
classes: Object,
@ -33,6 +34,11 @@ const styles = () => ({
color: '#03AE60',
height: '20px',
},
links: {
'&>a': {
color: secondary,
},
},
})
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 web3 = getWeb3()
const safeAddress = values[FIELD_LOAD_ADDRESS]
if (!safeAddress || mustBeEthereumAddress(safeAddress) !== undefined) {
return errors
}
@ -84,10 +91,13 @@ export const safeFieldsValidation = async (values: Object) => {
const Details = ({ classes, errors, form }: Props) => (
<>
<Block margin="sm">
<Paragraph noMargin size="lg" color="primary">
Adding an existing Safe only requires the Safe address. Optionally you can give it a name. In case your
connected client is not the owner of the Safe, the interface will essentially provide you a read-only view.
<Block margin="md">
<Paragraph noMargin size="md" color="primary">
You are about to load an existing Gnosis Safe. First, choose a name and enter the Safe address. The name is only
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>
</Block>
<Block className={classes.root}>
@ -122,6 +132,23 @@ const Details = ({ classes, errors, form }: Props) => (
text="Safe Address"
/>
</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) => (
<>
<OpenPaper controls={controls} container={605}>
<OpenPaper controls={controls}>
<DetailsForm errors={errors} form={form} />
</OpenPaper>
</>

View File

@ -13,7 +13,7 @@ import { history } from '~/store'
import { secondary } from '~/theme/variables'
import { type SelectorProps } from '~/routes/load/container/selector'
const getSteps = () => ['Details', 'Owners', 'Review']
const getSteps = () => ['Name and address', 'Owners', 'Review']
type Props = SelectorProps & {
onLoadSafeSubmit: (values: Object) => Promise<void>,
@ -35,6 +35,8 @@ const formMutators = {
},
}
const buttonLabels = ['Next', 'Review', 'Load']
const Layout = ({
provider, onLoadSafeSubmit, network, userAddress,
}: Props) => {
@ -56,6 +58,7 @@ const Layout = ({
steps={steps}
initialValues={initialValues}
mutators={formMutators}
buttonLabels={buttonLabels}
testId="load-safe-form"
>
<StepperPage validate={safeFieldsValidation}>{DetailsForm}</StepperPage>

View File

@ -1,7 +1,6 @@
// @flow
import React, { useState, useEffect } from 'react'
import { withStyles } from '@material-ui/core/styles'
import OpenInNew from '@material-ui/icons/OpenInNew'
import Block from '~/components/layout/Block'
import Field from '~/components/forms/Field'
import { required } from '~/components/forms/validator'
@ -10,22 +9,17 @@ import Identicon from '~/components/Identicon'
import OpenPaper from '~/components/Stepper/OpenPaper'
import Col from '~/components/layout/Col'
import Row from '~/components/layout/Row'
import Link from '~/components/layout/Link'
import Paragraph from '~/components/layout/Paragraph'
import Hairline from '~/components/layout/Hairline'
import EtherscanBtn from '~/components/EtherscanBtn'
import CopyBtn from '~/components/CopyBtn'
import {
sm, md, lg, border, secondary, disabled, extraSmallFontSize,
sm, md, lg, border, disabled, extraSmallFontSize,
} from '~/theme/variables'
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 { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
const openIconStyle = {
height: '16px',
color: secondary,
}
const styles = () => ({
details: {
padding: lg,
@ -45,6 +39,7 @@ const styles = () => ({
},
address: {
paddingLeft: '6px',
marginRight: sm,
},
open: {
paddingLeft: sm,
@ -70,11 +65,7 @@ const styles = () => ({
},
})
type LayoutProps = {
network: string,
}
type Props = LayoutProps & {
type Props = {
values: Object,
classes: Object,
updateInitialProps: (initialValues: Object) => void,
@ -92,7 +83,7 @@ const calculateSafeValues = (owners: Array<string>, threshold: Number, values: O
const OwnerListComponent = (props: Props) => {
const [owners, setOwners] = useState<Array<string>>([])
const {
values, updateInitialProps, network, classes,
values, updateInitialProps, classes,
} = props
useEffect(() => {
@ -122,7 +113,7 @@ const OwnerListComponent = (props: Props) => {
return (
<>
<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.`}
</Paragraph>
</Block>
@ -147,15 +138,14 @@ const OwnerListComponent = (props: Props) => {
text="Owner Name"
/>
</Col>
<Col xs={7}>
<Col xs={8}>
<Row className={classes.ownerAddresses}>
<Identicon address={address} diameter={32} />
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
{address}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink('address', address, network)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
<CopyBtn content={address} />
<EtherscanBtn type="address" value={address} />
</Row>
</Col>
</Row>

View File

@ -2,29 +2,23 @@
import * as React from 'react'
import classNames from 'classnames'
import { withStyles } from '@material-ui/core/styles'
import OpenInNew from '@material-ui/icons/OpenInNew'
import Block from '~/components/layout/Block'
import Identicon from '~/components/Identicon'
import OpenPaper from '~/components/Stepper/OpenPaper'
import Col from '~/components/layout/Col'
import Row from '~/components/layout/Row'
import Link from '~/components/layout/Link'
import EtherscanBtn from '~/components/EtherscanBtn'
import Paragraph from '~/components/layout/Paragraph'
import CopyBtn from '~/components/CopyBtn'
import Hairline from '~/components/layout/Hairline'
import {
xs, sm, lg, border, secondary,
xs, sm, lg, border,
} from '~/theme/variables'
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
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'
const openIconStyle = {
height: '16px',
color: secondary,
}
const styles = () => ({
root: {
minHeight: '300px',
@ -51,6 +45,9 @@ const styles = () => ({
},
user: {
justifyContent: 'left',
'& > p': {
marginRight: sm,
},
},
open: {
paddingLeft: sm,
@ -65,15 +62,12 @@ const styles = () => ({
},
address: {
paddingLeft: '6px',
marginRight: sm,
},
})
type LayoutProps = {
network: string,
type Props = {
userAddress: string,
}
type Props = LayoutProps & {
values: Object,
classes: Object,
}
@ -97,9 +91,7 @@ const checkUserAddressOwner = (values: Object, userAddress: string): boolean =>
class ReviewComponent extends React.PureComponent<Props, State> {
render() {
const {
values, classes, network, userAddress,
} = this.props
const { values, classes, userAddress } = this.props
const isOwner = checkUserAddressOwner(values, userAddress)
const owners = getAccountsFrom(values)
@ -132,9 +124,8 @@ class ReviewComponent extends React.PureComponent<Props, State> {
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
{shortVersionOf(safeAddress, 4)}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink('address', safeAddress, network)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
<CopyBtn content={safeAddress} />
<EtherscanBtn type="address" value={safeAddress} />
</Row>
</Block>
<Block margin="lg">
@ -177,13 +168,8 @@ class ReviewComponent extends React.PureComponent<Props, State> {
<Paragraph size="md" color="disabled" noMargin>
{address}
</Paragraph>
<Link
className={classes.open}
to={getEtherScanLink('address', address, network)}
target="_blank"
>
<OpenInNew style={openIconStyle} />
</Link>
<CopyBtn content={address} />
<EtherscanBtn type="address" value={address} />
</Block>
</Block>
</Col>

View File

@ -13,10 +13,10 @@ import { getOwnerNameBy, getOwnerAddressBy, FIELD_CONFIRMATIONS } from '~/routes
import { history } from '~/store'
import { secondary } from '~/theme/variables'
const getSteps = () => ['Start', 'Owners and confirmations', 'Review']
const getSteps = () => ['Name', 'Owners and confirmations', 'Review']
const initialValuesFrom = (userAccount: string) => ({
[getOwnerNameBy(0)]: 'My Metamask (me)',
[getOwnerNameBy(0)]: 'My Wallet',
[getOwnerAddressBy(0)]: userAccount,
[FIELD_CONFIRMATIONS]: '1',
})
@ -69,7 +69,9 @@ const Layout = ({
>
<StepperPage>{SafeNameField}</StepperPage>
<StepperPage>{SafeOwnersFields}</StepperPage>
<StepperPage network={network}>{Review}</StepperPage>
<StepperPage network={network} userAccount={userAccount}>
{Review}
</StepperPage>
</Stepper>
</Block>
) : (

View File

@ -8,7 +8,7 @@ import { getProviderInfo } from '~/logic/wallets/getWeb3'
import { sleep } from '~/utils/timer'
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({
safeAddress: '',

View File

@ -2,26 +2,24 @@
import * as React from 'react'
import classNames from 'classnames'
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 Block from '~/components/layout/Block'
import EtherscanBtn from '~/components/EtherscanBtn'
import CopyBtn from '~/components/CopyBtn'
import Identicon from '~/components/Identicon'
import OpenPaper from '~/components/Stepper/OpenPaper'
import Col from '~/components/layout/Col'
import Row from '~/components/layout/Row'
import Link from '~/components/layout/Link'
import Paragraph from '~/components/layout/Paragraph'
import {
sm, md, lg, border, secondary, background,
sm, md, lg, border, background,
} from '~/theme/variables'
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'
const openIconStyle = {
height: '16px',
color: secondary,
}
const { useEffect, useState } = React
const styles = () => ({
root: {
@ -55,6 +53,9 @@ const styles = () => ({
},
user: {
justifyContent: 'left',
'& > p': {
marginRight: sm,
},
},
open: {
paddingLeft: sm,
@ -65,22 +66,40 @@ const styles = () => ({
},
})
type LayoutProps = {
network: string,
}
type Props = LayoutProps & {
type Props = {
values: 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 addresses = getAccountsFrom(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 (
<React.Fragment>
<>
<Row className={classes.root}>
<Col xs={4} layout="column">
<Block className={classes.details}>
@ -129,13 +148,8 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
<Paragraph size="md" color="disabled" noMargin>
{addresses[index]}
</Paragraph>
<Link
className={classes.open}
to={getEtherScanLink('address', addresses[index], network)}
target="_blank"
>
<OpenInNew style={openIconStyle} />
</Link>
<CopyBtn content={addresses[index]} />
<EtherscanBtn type="address" value={addresses[index]} />
</Block>
</Block>
</Col>
@ -148,21 +162,26 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
<Row className={classes.info} align="center">
<Paragraph noMargin color="primary" size="md">
You&apos;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>
</Row>
</React.Fragment>
</>
)
}
const ReviewPage = withStyles(styles)(ReviewComponent)
const Review = ({ network }: LayoutProps) => (controls: React.Node, { values }: Object) => (
<React.Fragment>
const Review = () => (controls: React.Node, { values }: Object) => (
<>
<OpenPaper controls={controls} padding={false}>
<ReviewPage network={network} values={values} />
<ReviewPage values={values} />
</OpenPaper>
</React.Fragment>
</>
)
export default Review

View File

@ -33,7 +33,7 @@ const styles = () => ({
})
const SafeName = ({ classes }: Props) => (
<React.Fragment>
<>
<Block margin="lg">
<Paragraph noMargin size="md" color="primary">
You are about to create a new Gnosis Safe wallet with one or more owners. First, let&apos;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.
</Paragraph>
</Block>
</React.Fragment>
</>
)
const SafeNameForm = withStyles(styles)(SafeName)
const SafeNamePage = () => (controls: React.Node) => (
<OpenPaper controls={controls} container={600}>
<OpenPaper controls={controls}>
<SafeNameForm />
</OpenPaper>
)

View File

@ -109,7 +109,13 @@ const SafeOwners = (props: Props) => {
<>
<Block className={classes.title}>
<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>
</Block>
<Hairline />

View File

@ -1,7 +1,5 @@
// @flow
import {
uniqueAddress,
} from '~/components/forms/validator'
import { uniqueAddress } from '~/components/forms/validator'
export const getAddressValidator = (addresses: string[], position: number) => {
// thanks Rich Harris

View File

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

View File

@ -59,6 +59,10 @@ export const createSafe = async (values: Object, userAccount: string, addSafe: A
}
class Open extends React.Component<Props> {
async componentDidMount() {
await initContracts()
}
onCallSafeContractSubmit = async (values) => {
try {
const { userAccount, addSafe } = this.props

View File

@ -2,7 +2,7 @@
import { createStructuredSelector } from 'reselect'
import { providerNameSelector, userAccountSelector, networkSelector } from '~/logic/wallets/store/selectors'
export default createStructuredSelector({
export default createStructuredSelector<Object, *>({
provider: providerNameSelector,
network: networkSelector,
userAccount: userAccountSelector,

View File

@ -4,17 +4,17 @@ import { makeOwner } from '~/routes/safe/store/models/owner'
export const getAccountsFrom = (values: Object): string[] => {
const accounts = Object.keys(values)
.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[] => {
const accounts = Object.keys(values)
.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> => {

View File

@ -5,7 +5,7 @@ import styles from '~/components/layout/PageFrame/index.scss'
import { ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
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)
.addDecorator(FrameDecorator)

View File

@ -14,13 +14,13 @@ import Row from '~/components/layout/Row'
import Hairline from '~/components/layout/Hairline'
import Col from '~/components/layout/Col'
import {
xxl, lg, sm, md, background, secondary,
lg, md, secondary, secondaryText,
} from '~/theme/variables'
import { copyToClipboard } from '~/utils/clipboard'
const styles = () => ({
heading: {
padding: `${sm} ${lg}`,
padding: `${md} ${lg}`,
justifyContent: 'space-between',
maxHeight: '75px',
boxSizing: 'border-box',
@ -32,27 +32,27 @@ const styles = () => ({
height: '35px',
width: '35px',
},
detailsContainer: {
backgroundColor: background,
},
qrContainer: {
backgroundColor: '#fff',
padding: md,
borderRadius: '3px',
boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)',
borderRadius: '6px',
border: `1px solid ${secondaryText}`,
},
safeName: {
margin: `${xxl} 0 20px`,
margin: `${lg} 0 ${lg}`,
},
buttonRow: {
height: '84px',
justifyContent: 'center',
},
button: {
height: '42px',
'& > button': {
fontFamily: 'Averta',
fontSize: '16px',
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
},
},
addressContainer: {
marginTop: '28px',
marginTop: '25px',
marginBottom: '25px',
},
address: {
marginLeft: '6px',
@ -75,7 +75,7 @@ type Props = {
const Receive = ({
classes, onClose, safeAddress, safeName, etherScanLink,
}: Props) => (
<React.Fragment>
<>
<Row align="center" grow className={classes.heading}>
<Paragraph className={classes.manage} weight="bolder" noMargin>
Receive funds
@ -84,7 +84,7 @@ const Receive = ({
<Close className={classes.close} />
</IconButton>
</Row>
<Col layout="column" middle="xs" className={classes.detailsContainer}>
<Col layout="column" middle="xs">
<Hairline />
<Paragraph className={classes.safeName} weight="bolder" size="xl" noMargin>
{safeName}
@ -109,11 +109,11 @@ const Receive = ({
</Col>
<Hairline />
<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
</Button>
</Row>
</React.Fragment>
</>
)
export default withStyles(styles)(Receive)

View File

@ -24,7 +24,7 @@ const styles = () => ({
letterSpacing: -0.5,
backgroundColor: border,
width: 'fit-content',
padding: '6px',
padding: '5px 10px',
marginTop: xs,
borderRadius: '3px',
},

View File

@ -5,9 +5,18 @@ import cn from 'classnames'
import { withStyles } from '@material-ui/core/styles'
import { type Token } from '~/logic/tokens/store/model/token'
import Modal from '~/components/Modal'
import ChooseTxType from './screens/ChooseTxType'
import SendFunds from './screens/SendFunds'
import ReviewTx from './screens/ReviewTx'
const ChooseTxType = React.lazy(() => import('./screens/ChooseTxType'))
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 = {
onClose: () => void,
@ -20,19 +29,23 @@ type Props = {
tokens: List<Token>,
selectedToken: string,
createTransaction: Function,
activeScreenType: ActiveScreen
}
type ActiveScreen = 'chooseTxType' | 'sendFunds' | 'reviewTx'
type TxStateType =
| {
token: Token,
recipientAddress: string,
amount: string,
data: string,
}
| Object
const styles = () => ({
smallerModalWindow: {
scalableModalWindow: {
height: 'auto',
},
scalableStaticModalWindow: {
height: 'auto',
position: 'static',
},
@ -49,23 +62,27 @@ const Send = ({
tokens,
selectedToken,
createTransaction,
activeScreenType,
}: Props) => {
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('sendFunds')
const [activeScreen, setActiveScreen] = useState<ActiveScreen>(activeScreenType || 'chooseTxType')
const [tx, setTx] = useState<TxStateType>({})
const smallerModalSize = activeScreen === 'chooseTxType'
useEffect(() => {
setActiveScreen(activeScreenType || 'chooseTxType')
setTx({})
}, [isOpen])
const scalableModalSize = activeScreen === 'chooseTxType'
const handleTxCreation = (txInfo) => {
setActiveScreen('reviewTx')
setTx(txInfo)
}
const onClickBack = () => setActiveScreen('sendFunds')
useEffect(
() => () => {
setActiveScreen('sendFunds')
setTx({})
},
[isOpen],
)
const handleCustomTxCreation = (customTxInfo) => {
setActiveScreen('reviewCustomTx')
setTx(customTxInfo)
}
return (
<Modal
@ -73,14 +90,15 @@ const Send = ({
description="Send Tokens Form"
handleClose={onClose}
open={isOpen}
paperClassName={cn(smallerModalSize && classes.smallerModalWindow)}
paperClassName={cn(
scalableModalSize ? classes.scalableStaticModalWindow : classes.scalableModalWindow,
)}
>
<React.Fragment>
<>
{activeScreen === 'chooseTxType' && <ChooseTxType onClose={onClose} setActiveScreen={setActiveScreen} />}
{activeScreen === 'sendFunds' && (
<SendFunds
onClose={onClose}
setActiveScreen={setActiveScreen}
safeAddress={safeAddress}
etherScanLink={etherScanLink}
safeName={safeName}
@ -95,15 +113,38 @@ const Send = ({
<ReviewTx
tx={tx}
onClose={onClose}
setActiveScreen={setActiveScreen}
safeAddress={safeAddress}
etherScanLink={etherScanLink}
safeName={safeName}
ethBalance={ethBalance}
onClickBack={onClickBack}
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>
)
}

View File

@ -1,5 +1,6 @@
// @flow
import * as React from 'react'
import classNames from 'classnames/bind'
import { withStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import IconButton from '@material-ui/core/IconButton'
@ -8,11 +9,14 @@ import Button from '~/components/layout/Button'
import Row from '~/components/layout/Row'
import Col from '~/components/layout/Col'
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 = () => ({
heading: {
padding: `${sm} ${lg}`,
padding: `${md} ${lg}`,
justifyContent: 'space-between',
boxSizing: 'border-box',
maxHeight: '75px',
@ -26,10 +30,22 @@ const styles = () => ({
},
buttonColumn: {
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 = {
@ -39,7 +55,7 @@ type Props = {
}
const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
<React.Fragment>
<>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.manage} noMargin>
Send
@ -57,22 +73,24 @@ const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
minHeight={52}
onClick={() => setActiveScreen('sendFunds')}
variant="contained"
className={classes.firstButton}
>
SEND FUNDS
<Img src={Token} alt="Send funds" className={classNames(classes.leftIcon, classes.iconSmall)} />
Send funds
</Button>
<Button
color="primary"
className={classes.secondButton}
minWidth={260}
minHeight={52}
onClick={onClose}
onClick={() => setActiveScreen('sendCustomTx')}
variant="outlined"
>
SEND CUSTOM TRANSACTION
<Img src={Code} alt="Send custom transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
Send custom transaction
</Button>
</Col>
</Row>
</React.Fragment>
</>
)
export default withStyles(styles)(ChooseTxType)

View File

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

View File

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

View File

@ -27,11 +27,11 @@ import { styles } from './style'
type Props = {
onClose: () => void,
setActiveScreen: Function,
classes: Object,
safeAddress: string,
etherScanLink: string,
safeName: string,
onClickBack: Function,
ethBalance: string,
tx: Object,
createTransaction: Function,
@ -44,13 +44,13 @@ const openIconStyle = {
const ReviewTx = ({
onClose,
setActiveScreen,
classes,
safeAddress,
etherScanLink,
safeName,
ethBalance,
tx,
onClickBack,
createTransaction,
}: Props) => (
<SharedSnackbarConsumer>
@ -138,20 +138,19 @@ const ReviewTx = ({
</Block>
<Hairline style={{ position: 'absolute', bottom: 85 }} />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClickBack}>
<Button minWidth={140} onClick={() => setActiveScreen('sendFunds')}>
Back
</Button>
<Button
type="submit"
className={classes.button}
onClick={submitTx}
variant="contained"
minWidth={140}
minHeight={42}
color="primary"
data-testid="submit-tx-btn"
className={classes.submitButton}
>
SUBMIT
Submit
</Button>
</Row>
</>

View File

@ -5,7 +5,7 @@ import {
export const styles = () => ({
heading: {
padding: `${sm} ${lg}`,
padding: `${md} ${lg}`,
justifyContent: 'flex-start',
boxSizing: 'border-box',
maxHeight: '75px',
@ -32,8 +32,13 @@ export const styles = () => ({
buttonRow: {
height: '84px',
justifyContent: 'center',
position: 'absolute',
bottom: 0,
width: '100%',
'& > button': {
fontFamily: 'Averta',
fontSize: '16px',
},
},
submitButton: {
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
marginLeft: '15px',
},
})

View File

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

View File

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

View File

@ -81,24 +81,28 @@ const SendFunds = ({
</IconButton>
</Row>
<Hairline />
<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>
<GnoForm onSubmit={handleSubmit} formMutators={formMutators} initialValues={initialValues}>
{(...args) => {
const formState = args[2]
const mutators = args[3]
const { token } = formState.values
return (
<>
<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}>
<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
@ -154,27 +158,27 @@ const SendFunds = ({
</OnChange>
</Col>
</Row>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
variant="contained"
minHeight={42}
minWidth={140}
color="primary"
data-testid="review-tx-btn"
>
Review
</Button>
</Row>
</>
)
}}
</GnoForm>
</Block>
</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>
</>
)
}

View File

@ -1,11 +1,9 @@
// @flow
import {
lg, md, sm, secondaryText,
} from '~/theme/variables'
import { lg, md, secondaryText } from '~/theme/variables'
export const styles = () => ({
heading: {
padding: `${sm} ${lg}`,
padding: `${md} ${lg}`,
justifyContent: 'flex-start',
boxSizing: 'border-box',
maxHeight: '75px',
@ -29,5 +27,13 @@ export const styles = () => ({
buttonRow: {
height: '84px',
justifyContent: 'center',
'& > button': {
fontFamily: 'Averta',
fontSize: '16px',
},
},
submitButton: {
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
marginLeft: '15px',
},
})

View File

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

View File

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

View File

@ -119,10 +119,10 @@ const AddCustomToken = (props: Props) => {
}
return (
<React.Fragment>
<>
<GnoForm onSubmit={handleSubmit} initialValues={formValues} testId={ADD_CUSTOM_TOKEN_FORM}>
{() => (
<React.Fragment>
<>
<Block className={classes.formContainer}>
<Paragraph noMargin className={classes.title} weight="bolder" size="lg">
Add custom token
@ -189,17 +189,17 @@ const AddCustomToken = (props: Props) => {
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={goBackToTokenList}>
<Button minHeight={42} minWidth={140} onClick={goBackToTokenList}>
Cancel
</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
</Button>
</Row>
</React.Fragment>
</>
)}
</GnoForm>
</React.Fragment>
</>
)
}

View File

@ -28,7 +28,4 @@ export const styles = () => ({
height: '84px',
justifyContent: 'center',
},
button: {
height: '42px',
},
})

View File

@ -2,12 +2,12 @@
import * as React from 'react'
import { List } from 'immutable'
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 TableRow from '@material-ui/core/TableRow'
import TableCell from '@material-ui/core/TableCell'
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 Col from '~/components/layout/Col'
import Row from '~/components/layout/Row'
@ -178,7 +178,7 @@ class Balances extends React.Component<Props, State> {
onClick={() => this.showSendFunds(row.asset.name)}
testId="balance-send-btn"
>
<CallMade className={classNames(classes.leftIcon, classes.iconSmall)} />
<CallMade alt="Send Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
Send
</Button>
)}
@ -189,7 +189,7 @@ class Balances extends React.Component<Props, State> {
className={classes.receive}
onClick={this.onShow('Receive')}
>
<CallReceived className={classNames(classes.leftIcon, classes.iconSmall)} />
<CallReceived alt="Receive Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
Receive
</Button>
</Row>
@ -207,6 +207,7 @@ class Balances extends React.Component<Props, State> {
tokens={activeTokens}
selectedToken={sendFunds.selectedToken}
createTransaction={createTransaction}
activeScreenType="sendFunds"
/>
<Modal
title="Receive Tokens"

View File

@ -1,5 +1,5 @@
// @flow
import { sm, xs } from '~/theme/variables'
import { sm } from '~/theme/variables'
export const styles = (theme: Object) => ({
root: {
@ -30,17 +30,25 @@ export const styles = (theme: Object) => ({
justifyContent: 'flex-end',
visibility: 'hidden',
},
send: {
minWidth: '0px',
marginRight: sm,
width: '70px',
},
receive: {
minWidth: '0px',
width: '95px',
minWidth: '95px',
marginLeft: sm,
borderRadius: '4px',
'& > span': {
fontSize: '14px',
},
},
send: {
width: '75px',
minWidth: '75px',
borderRadius: '4px',
'& > span': {
fontSize: '14px',
},
},
leftIcon: {
marginRight: xs,
marginRight: sm,
},
links: {
textDecoration: 'underline',

View File

@ -1,27 +1,35 @@
// @flow
import * as React from 'react'
import classNames from 'classnames/bind'
import OpenInNew from '@material-ui/icons/OpenInNew'
import Tabs from '@material-ui/core/Tabs'
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 Hairline from '~/components/layout/Hairline'
import Block from '~/components/layout/Block'
import Identicon from '~/components/Identicon'
import Heading from '~/components/layout/Heading'
import Row from '~/components/layout/Row'
import Button from '~/components/layout/Button'
import Link from '~/components/layout/Link'
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 { type SelectorProps } from '~/routes/safe/container/selector'
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import {
sm, xs, secondary, smallFontSize, border, secondaryText,
secondary, border,
} from '~/theme/variables'
import { copyToClipboard } from '~/utils/clipboard'
import { type Actions } from '../container/actions'
import Balances from './Balances'
import Transactions from './Transactions'
import Settings from './Settings'
import { styles } from './style'
export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn'
export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn'
@ -36,6 +44,12 @@ type Props = SelectorProps &
Actions & {
classes: Object,
granted: boolean,
sendFunds: Object,
showReceive: boolean,
onShow: Function,
onHide: Function,
showSendFunds: Function,
hideSendFunds: Function
}
const openIconStyle = {
@ -43,40 +57,6 @@ const openIconStyle = {
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> {
constructor(props) {
super(props)
@ -112,6 +92,12 @@ class Layout extends React.Component<Props, State> {
updateSafe,
transactions,
userAddress,
sendFunds,
showReceive,
onShow,
onHide,
showSendFunds,
hideSendFunds,
} = this.props
const { tabIndex } = this.state
@ -142,6 +128,32 @@ class Layout extends React.Component<Props, State> {
</Link>
</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>
<Row>
<Tabs value={tabIndex} onChange={this.handleChange} indicatorColor="secondary" textColor="secondary">
@ -190,6 +202,31 @@ class Layout extends React.Component<Props, State> {
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>
</>
)
}

View File

@ -5,7 +5,7 @@ import { List } from 'immutable'
import styles from '~/components/layout/PageFrame/index.scss'
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)
.addDecorator(FrameDecorator)

View File

@ -4,7 +4,7 @@ import * as React from 'react'
import styles from '~/components/layout/PageFrame/index.scss'
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)
.addDecorator(FrameDecorator)

View File

@ -44,7 +44,7 @@ const ReviewAddOwner = ({
onSubmit()
}
return (
<React.Fragment>
<>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.manage} noMargin>
Add new owner
@ -97,7 +97,7 @@ const ReviewAddOwner = ({
</Paragraph>
</Row>
<Hairline />
{owners.map(owner => (
{owners.map((owner) => (
<React.Fragment key={owner.address}>
<Row className={classes.owner}>
<Col xs={1} align="center">
@ -154,22 +154,22 @@ const ReviewAddOwner = ({
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
<Button minWidth={140} minHeight={42} onClick={onClickBack}>
Back
</Button>
<Button
type="submit"
onClick={handleSubmit}
className={classes.button}
variant="contained"
minWidth={140}
minHeight={42}
color="primary"
testId={ADD_OWNER_SUBMIT_BTN_TEST_ID}
>
Submit
</Button>
</Row>
</React.Fragment>
</>
)
}

View File

@ -39,7 +39,7 @@ const ThresholdForm = ({
}
return (
<React.Fragment>
<>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.manage} noMargin>
Add new owner
@ -52,7 +52,7 @@ const ThresholdForm = ({
<Hairline />
<GnoForm onSubmit={handleSubmit} initialValues={{ threshold: threshold.toString() }}>
{() => (
<React.Fragment>
<>
<Block className={classes.formContainer}>
<Row>
<Paragraph weight="bolder" className={classes.headingText}>
@ -68,8 +68,8 @@ const ThresholdForm = ({
<Col xs={2}>
<Field
name="threshold"
render={props => (
<React.Fragment>
render={(props) => (
<>
<SelectField {...props} disableError>
{[...Array(Number(owners.size + 1))].map((x, index) => (
<MenuItem key={index} value={`${index + 1}`}>
@ -82,7 +82,7 @@ const ThresholdForm = ({
{props.meta.error}
</Paragraph>
)}
</React.Fragment>
</>
)}
validate={composeValidators(required, mustBeInteger, minValue(1), maxValue(owners.size + 1))}
data-testid="threshold-select-input"
@ -101,24 +101,24 @@ owner(s)
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
<Button minWidth={140} minHeight={42} onClick={onClickBack}>
Back
</Button>
<Button
type="submit"
className={classes.button}
variant="contained"
minWidth={140}
minHeight={42}
color="primary"
testId={ADD_OWNER_THRESHOLD_NEXT_BTN_TEST_ID}
>
Review
</Button>
</Row>
</React.Fragment>
</>
)}
</GnoForm>
</React.Fragment>
</>
)
}

View File

@ -73,7 +73,7 @@ const EditOwnerComponent = ({
<Hairline />
<GnoForm onSubmit={handleSubmit}>
{() => (
<React.Fragment>
<>
<Block className={classes.container}>
<Row margin="md">
<Field
@ -102,14 +102,14 @@ const EditOwnerComponent = ({
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClose}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Cancel
</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
</Button>
</Row>
</React.Fragment>
</>
)}
</GnoForm>
</Modal>

View File

@ -79,13 +79,14 @@ const CheckOwner = ({
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClose}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
variant="contained"
minWidth={140}
minHeight={42}
color="primary"
onClick={handleSubmit}
testId={REMOVE_OWNER_MODAL_NEXT_BTN_TEST_ID}

View File

@ -175,14 +175,14 @@ Safe owner(s)
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
<Button minWidth={140} minHeight={42} onClick={onClickBack}>
Back
</Button>
<Button
type="submit"
onClick={handleSubmit}
className={classes.button}
variant="contained"
minHeight={42}
minWidth={140}
color="primary"
testId={REMOVE_OWNER_REVIEW_BTN_TEST_ID}

View File

@ -105,13 +105,13 @@ owner(s)
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
<Button minWidth={140} minHeight={42} onClick={onClickBack}>
Back
</Button>
<Button
type="submit"
className={classes.button}
variant="contained"
minHeight={42}
minWidth={140}
color="primary"
data-testid={REMOVE_OWNER_THRESHOLD_NEXT_BTN_TEST_ID}

View File

@ -58,7 +58,7 @@ const ReviewRemoveOwner = ({
}
return (
<React.Fragment>
<>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.manage} noMargin>
Replace owner
@ -112,7 +112,7 @@ const ReviewRemoveOwner = ({
</Row>
<Hairline />
{owners.map(
owner => owner.address !== ownerAddress && (
(owner) => owner.address !== ownerAddress && (
<React.Fragment key={owner.address}>
<Row className={classes.owner}>
<Col xs={1} align="center">
@ -200,14 +200,14 @@ const ReviewRemoveOwner = ({
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
<Button minWidth={140} minHeight={42} onClick={onClickBack}>
Back
</Button>
<Button
type="submit"
onClick={handleSubmit}
className={classes.button}
variant="contained"
minHeight={42}
minWidth={140}
color="primary"
testId={REPLACE_OWNER_SUBMIT_BTN_TEST_ID}
@ -215,7 +215,7 @@ const ReviewRemoveOwner = ({
Submit
</Button>
</Row>
</React.Fragment>
</>
)
}

View File

@ -82,7 +82,7 @@ const RemoveSafeComponent = ({
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClose}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Cancel
</Button>
<Button

View File

@ -3,7 +3,7 @@ import {
lg, md, sm, error, background,
} from '~/theme/variables'
export const styles = (theme: Object) => ({
export const styles = () => ({
heading: {
padding: `${sm} ${lg}`,
justifyContent: 'space-between',
@ -27,6 +27,7 @@ export const styles = (theme: Object) => ({
buttonRemove: {
color: '#fff',
backgroundColor: error,
height: '42px',
},
name: {
textOverflow: 'ellipsis',

View File

@ -103,10 +103,10 @@ owner(s)
</Block>
<Hairline style={{ position: 'absolute', bottom: 85 }} />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClose}>
<Button minWidth={140} onClick={onClose}>
BACK
</Button>
<Button type="submit" color="primary" className={classes.button} minWidth={140} variant="contained">
<Button type="submit" color="primary" minWidth={140} variant="contained">
CHANGE
</Button>
</Row>

View File

@ -24,7 +24,7 @@ export const styles = () => ({
alignItems: 'center',
cursor: 'pointer',
'&:first-child': {
borderRadius: '8px',
borderTopLeftRadius: '8px',
},
},
active: {

View File

@ -60,7 +60,7 @@ const ApproveTxModal = ({
const { title, description } = getModalTitleAndDescription(thresholdReached)
const oneConfirmationLeft = tx.confirmations.size + 1 === threshold
const handleExecuteCheckbox = () => setApproveAndExecute(prevApproveAndExecute => !prevApproveAndExecute)
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
return (
<SharedSnackbarConsumer>
@ -104,12 +104,11 @@ const ApproveTxModal = ({
</Row>
</Block>
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClose}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Exit
</Button>
<Button
type="submit"
className={classes.button}
variant="contained"
minWidth={214}
minHeight={42}

View File

@ -65,12 +65,11 @@ const CancelTxModal = ({
</Row>
</Block>
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClose}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Exit
</Button>
<Button
type="submit"
className={classes.button}
variant="contained"
minWidth={214}
minHeight={42}

View File

@ -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_CHANGE_THRESHOLD_TEST_ID = 'tx-description-change-threshold'
export const TRANSACTIONS_DESC_SEND_TEST_ID = 'tx-description-send'
export const TRANSACTIONS_DESC_CUSTOM_TEST_ID = 'tx-description-custom'
export const styles = () => ({
txDataContainer: {
padding: `${lg} ${md}`,
},
txData: {
wordBreak: 'break-all',
},
})
type Props = {
@ -37,6 +41,11 @@ type DescriptionDescProps = {
newThreshold?: string,
}
type CustomDescProps = {
data: String,
classes: Obeject,
}
const TransferDescription = ({ value = '', symbol, recipient }: TransferDescProps) => (
<Paragraph noMargin data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
<Bold>
@ -46,7 +55,7 @@ const TransferDescription = ({ value = '', symbol, recipient }: TransferDescProp
{' '}
{symbol}
{' '}
to:
to:
</Bold>
<br />
<EtherscanLink type="address" value={recipient} />
@ -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 {
recipient, value, modifySettingsTx, removedOwner, addedOwner, newThreshold, cancellationTx,
recipient, value, modifySettingsTx, removedOwner, addedOwner, newThreshold, cancellationTx, customTx, data,
} = getTxData(tx)
return (
@ -89,7 +108,10 @@ const TxDescription = ({ tx, classes }: Props) => {
{modifySettingsTx && (
<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} />
)}
</Block>

View File

@ -10,6 +10,8 @@ type DecodedTxData = {
newThreshold?: string,
addedOwner?: string,
cancellationTx?: boolean,
customTx?: boolean,
data: string,
}
export const getTxData = (tx: Transaction): DecodedTxData => {
@ -47,6 +49,9 @@ export const getTxData = (tx: Transaction): DecodedTxData => {
}
} else if (tx.cancellationTx) {
txData.cancellationTx = true
} else if (tx.customTx) {
txData.data = tx.data
txData.customTx = true
}
return txData

View File

@ -50,6 +50,8 @@ export const getTxTableData = (transactions: List<Transaction>): List<Transactio
txType = 'Modify Safe Settings'
} else if (tx.cancellationTx) {
txType = 'Cancellation transaction'
} else if (tx.customTx) {
txType = 'Custom transaction'
}
return {

View File

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

View File

@ -6,6 +6,13 @@ import Layout from '~/routes/safe/components/Layout'
import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions'
type State = {
showReceive: boolean,
sendFunds: Object,
}
type Action = 'Send' | 'Receive'
export type Props = Actions &
SelectorProps & {
granted: boolean,
@ -13,7 +20,15 @@ export type Props = Actions &
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
componentDidMount() {
@ -45,6 +60,32 @@ class SafeView extends React.Component<Props> {
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() {
const {
safeUrl, activeTokens, fetchTokenBalances, fetchEtherBalance,
@ -55,6 +96,7 @@ class SafeView extends React.Component<Props> {
}
render() {
const { sendFunds, showReceive } = this.state
const {
safe,
provider,
@ -85,6 +127,12 @@ class SafeView extends React.Component<Props> {
fetchTransactions={fetchTransactions}
updateSafe={updateSafe}
transactions={transactions}
sendFunds={sendFunds}
showReceive={showReceive}
onShow={this.onShow}
onHide={this.onHide}
showSendFunds={this.showSendFunds}
hideSendFunds={this.hideSendFunds}
/>
</Page>
)

View File

@ -13,7 +13,7 @@ import { getWeb3 } from '~/logic/wallets/getWeb3'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { addTransactions } from './addTransactions'
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 { 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 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
const executionTx = confirmations.find((conf) => conf.type === TX_TYPE_EXECUTION)
@ -70,7 +71,7 @@ export const buildTransactionFrom = async (
let symbol = 'ETH'
let decodedParams
if (isTokenTransfer) {
if (isSendTokenTx) {
const tokenContract = await getHumanFriendlyToken()
const tokenInstance = await tokenContract.at(tx.to)
symbol = await tokenInstance.symbol()
@ -82,6 +83,8 @@ export const buildTransactionFrom = async (
}
} else if (modifySettingsTx && tx.data) {
decodedParams = await decodeParamsFromSafeMethod(tx.data)
} else if (customTx && tx.data) {
decodedParams = await decodeParamsFromSafeMethod(tx.data)
}
return makeTransaction({
@ -97,9 +100,10 @@ export const buildTransactionFrom = async (
executionDate: tx.executionDate,
executionTxHash,
safeTxHash: tx.safeTxHash,
isTokenTransfer,
isTokenTransfer: isSendTokenTx,
decodedParams,
modifySettingsTx,
customTx,
cancellationTx,
})
}

View File

@ -12,7 +12,7 @@ import { approveTransaction, executeTransaction, CALL } from '~/logic/safe/trans
const generateSignaturesFromTxConfirmations = (tx: Transaction, preApprovingOwner?: string) => {
// The constant parts need to be sorted so that the recovered signers are sorted ascending
// (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) {
confirmedAdresses = confirmedAdresses.push(preApprovingOwner)

View File

@ -18,6 +18,7 @@ export type TransactionProps = {
symbol: string,
modifySettingsTx: boolean,
cancellationTx: boolean,
customTx: boolean,
safeTxHash: string,
executionTxHash?: string,
cancelled?: boolean,
@ -42,6 +43,7 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
cancelled: false,
modifySettingsTx: false,
cancellationTx: false,
customTx: false,
status: 'awaiting',
isTokenTransfer: false,
decodedParams: {},

View File

@ -14,12 +14,11 @@ const SafeList = ({ safes, provider }: Props) => {
const safesAvailable = safes && safes.count() > 0
return (
<React.Fragment>
<>
{ safesAvailable
? <SafeTable safes={safes} />
: <NoSafe provider={provider} text="No safes created, please create a new one" />
}
</React.Fragment>
: <NoSafe provider={provider} text="No safes created, please create a new one" />}
</>
)
}

View File

@ -5,7 +5,7 @@ import * as React from 'react'
import styles from '~/components/layout/PageFrame/index.scss'
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)
.addDecorator(FrameDecorator)

View File

@ -5,7 +5,7 @@ import * as React from 'react'
import styles from '~/components/layout/PageFrame/index.scss'
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)
.addDecorator(FrameDecorator)

View File

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

View File

@ -12,7 +12,10 @@ import { fillAndSubmitSendFundsForm } from './utils/transactions'
import { TRANSACTIONS_TAB_BTN_TEST_ID } from '~/routes/safe/components/Layout'
import { TRANSACTION_ROW_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable'
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'
afterEach(resetTestAccount)

View File

@ -11,6 +11,7 @@ import { clickOnManageTokens, toggleToken, closeManageTokensModal } from './util
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
import { makeToken } from '~/logic/tokens/store/model/token'
import '@testing-library/jest-dom/extend-expect'
import { getActiveTokens } from '~/logic/tokens/utils/tokensStorage'
describe('DOM > Feature > Enable and disable default tokens', () => {
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
const store = aNewStore()
const safeAddress = await aMinedSafe(store)
@ -69,6 +70,13 @@ describe('DOM > Feature > Enable and disable default tokens', () => {
expect(balanceRows[1]).toHaveTextContent('FTE')
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
clickOnManageTokens(TokensDom)
toggleToken(TokensDom, 'FTE')

View File

@ -7,7 +7,4 @@ function resetTestAccount() {
delete window.testAccountIndex
}
export {
useTestAccountAt,
resetTestAccount,
}
export { useTestAccountAt, resetTestAccount }

View File

@ -38,10 +38,7 @@ type FinsihedTx = {
finishedTransaction: boolean,
}
export const whenExecuted = (
SafeDom: React.Component<any, any>,
ParentComponent: React.ElementType,
): Promise<void> => new Promise((resolve, reject) => {
export const whenExecuted = (SafeDom: React.Component<any, any>, ParentComponent: React.ElementType): Promise<void> => new Promise((resolve, reject) => {
let times = 0
const interval = setInterval(() => {
if (times >= MAX_TIMES_EXECUTED) {

View File

@ -24,7 +24,7 @@ export const getLastTransaction = async (SafeDom: React.Component<any, any>) =>
export const checkRegisteredTxSend = async (
SafeDom: React.Component<any, any>,
ethAmount: number,
ethAmount: number | string,
symbol: string,
ethAddress: string,
) => {
@ -34,20 +34,14 @@ export const checkRegisteredTxSend = async (
expect(txDescription).toHaveTextContent(`Send ${ethAmount} ${symbol} to:${shortVersionOf(ethAddress, 4)}`)
}
export const checkRegisteredTxAddOwner = async (
SafeDom: React.Component<any, any>,
ownerAddress: string,
) => {
export const checkRegisteredTxAddOwner = async (SafeDom: React.Component<any, any>, ownerAddress: string) => {
await getLastTransaction(SafeDom)
const txDescription = SafeDom.getAllByTestId(TRANSACTIONS_DESC_ADD_OWNER_TEST_ID)[0]
expect(txDescription).toHaveTextContent(`Add owner:${shortVersionOf(ownerAddress, 4)}`)
}
export const checkRegisteredTxRemoveOwner = async (
SafeDom: React.Component<any, any>,
ownerAddress: string,
) => {
export const checkRegisteredTxRemoveOwner = async (SafeDom: React.Component<any, any>, ownerAddress: string) => {
await getLastTransaction(SafeDom)
const txDescription = SafeDom.getAllByTestId(TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID)[0]

View File

@ -1,2 +1,2 @@
// @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))