Merge branch 'development' into 139-load-create-improvements

This commit is contained in:
Germán Martínez 2019-09-13 12:11:30 +02:00
commit ea94e07ba2
72 changed files with 1007 additions and 278 deletions

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

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

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

@ -7,6 +7,7 @@ import {
composeValidators,
required,
mustBeEthereumAddress,
mustBeEthereumContractAddress,
} from '~/components/forms/validator'
import { getAddressFromENS } from '~/logic/wallets/getWeb3'
@ -19,6 +20,7 @@ type Props = {
testId?: string,
validators?: Function[],
inputAdornment?: React.Element,
mustBeContract?: boolean,
}
const isValidEnsName = (name) => /^([\w-]+\.)+(eth|test|xyz|luxe)$/.test(name)
@ -35,6 +37,7 @@ const AddressInput = ({
testId,
inputAdornment,
validators = [],
mustBeContract,
}: Props): React.Element<*> => (
<>
<Field
@ -44,6 +47,7 @@ const AddressInput = ({
validate={composeValidators(
required,
mustBeEthereumAddress,
mustBeContract && mustBeEthereumContractAddress,
...validators,
)}
inputAdornment={inputAdornment}

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

@ -22,7 +22,7 @@ export const required = simpleMemoize((value: Field) => (value ? undefined : 'Re
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'
@ -75,7 +83,7 @@ export const uniqueAddress = (addresses: string[] | List<string>) => simpleMemoi
return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined
})
export const composeValidators = (...validators: Function[]): FieldValidator => (value: Field) => validators.reduce((error, validator) => error || validator(value), undefined)
export const composeValidators = (...validators: Function[]): FieldValidator => (value: Field) => validators.reduce((error, validator) => (error || (validator && validator(value)), undefined))
export const inLimit = (limit: number, base: number, baseText: string, symbol: string = 'ETH') => (value: string) => {
const amount = Number(value)

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

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

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

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

@ -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',
@ -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,7 +109,7 @@ 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>

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,174 @@
// @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,
} 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}
mustBeContract
/>
</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

@ -124,7 +124,7 @@ class Tokens extends React.Component<Props, State> {
const filteredTokens = filterBy(filter, tokens)
return (
<React.Fragment>
<>
<Block className={classes.root}>
<Row align="center" className={cn(classes.padding, classes.actions)}>
<Search className={classes.search} />
@ -179,7 +179,7 @@ class Tokens extends React.Component<Props, State> {
)
})}
</MuiList>
</React.Fragment>
</>
)
}
}

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

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

@ -59,6 +59,7 @@ 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 customTx = tx.to !== safeAddress && !!tx.data
const isTokenTransfer = await isAddressAToken(tx.to)
let executionTxHash
@ -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({
@ -100,6 +103,7 @@ export const buildTransactionFrom = async (
isTokenTransfer,
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

@ -24,7 +24,7 @@ export const buildSafe = (storedSafe: SafeProps) => {
const addresses = storedSafe.owners.map((owner: OwnerProps) => owner.address)
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
const activeTokens = List(storedSafe.activeTokens)
const balances = storedSafe.balances.map(balance => TokenBalance(balance))
const balances = storedSafe.balances.map((balance) => TokenBalance(balance))
const safe: SafeProps = {
...storedSafe,
@ -67,7 +67,7 @@ export default handleActions<State, *>(
const safe = action.payload
const safeAddress = safe.address
return state.update(safeAddress, prevSafe => prevSafe.merge(safe))
return state.update(safeAddress, (prevSafe) => prevSafe.merge(safe))
},
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: State, action: ActionType<Function>): State => {
const tokenAddress = action.payload
@ -77,7 +77,7 @@ export default handleActions<State, *>(
const safeActiveTokens = map.getIn([safeAddress, 'activeTokens'])
const activeTokens = safeActiveTokens.push(tokenAddress)
map.update(safeAddress, prevSafe => prevSafe.merge({ activeTokens }))
map.update(safeAddress, (prevSafe) => prevSafe.merge({ activeTokens }))
})
})
@ -91,7 +91,7 @@ export default handleActions<State, *>(
// with initial props and it would overwrite existing ones
if (state.has(safe.address)) {
return state.update(safe.address, prevSafe => prevSafe.merge(safe))
return state.update(safe.address, (prevSafe) => prevSafe.merge(safe))
}
return state.set(safe.address, SafeRecord(safe))
@ -104,15 +104,15 @@ export default handleActions<State, *>(
[ADD_SAFE_OWNER]: (state: State, action: ActionType<Function>): State => {
const { safeAddress, ownerName, ownerAddress } = action.payload
return state.update(safeAddress, prevSafe => prevSafe.merge({
return state.update(safeAddress, (prevSafe) => prevSafe.merge({
owners: prevSafe.owners.push(makeOwner({ address: ownerAddress, name: ownerName })),
}))
},
[REMOVE_SAFE_OWNER]: (state: State, action: ActionType<Function>): State => {
const { safeAddress, ownerAddress } = action.payload
return state.update(safeAddress, prevSafe => prevSafe.merge({
owners: prevSafe.owners.filter(o => o.address.toLowerCase() !== ownerAddress.toLowerCase()),
return state.update(safeAddress, (prevSafe) => prevSafe.merge({
owners: prevSafe.owners.filter((o) => o.address.toLowerCase() !== ownerAddress.toLowerCase()),
}))
},
[REPLACE_SAFE_OWNER]: (state: State, action: ActionType<Function>): State => {
@ -120,9 +120,9 @@ export default handleActions<State, *>(
safeAddress, oldOwnerAddress, ownerName, ownerAddress,
} = action.payload
return state.update(safeAddress, prevSafe => prevSafe.merge({
return state.update(safeAddress, (prevSafe) => prevSafe.merge({
owners: prevSafe.owners
.filter(o => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase())
.filter((o) => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase())
.push(makeOwner({ address: ownerAddress, name: ownerName })),
}))
},
@ -131,9 +131,9 @@ export default handleActions<State, *>(
return state.update(safeAddress, (prevSafe) => {
const ownerToUpdateIndex = prevSafe.owners.findIndex(
o => o.address.toLowerCase() === ownerAddress.toLowerCase(),
(o) => o.address.toLowerCase() === ownerAddress.toLowerCase(),
)
const updatedOwners = prevSafe.owners.update(ownerToUpdateIndex, owner => owner.set('name', ownerName))
const updatedOwners = prevSafe.owners.update(ownerToUpdateIndex, (owner) => owner.set('name', ownerName))
return prevSafe.merge({ owners: updatedOwners })
})
},

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

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

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

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