Add etherscan/copy buttons in load/create safe flows

This commit is contained in:
Mikhail Mikheev 2019-09-11 19:35:36 +04:00
parent b44c30923d
commit 3c05220008
12 changed files with 138 additions and 74 deletions

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,35 @@
// @flow
import React from 'react'
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) => (navigator.clipboard ? (
<div className={classes.container}>
<Img src={CopyIcon} height={20} alt="Click to copy" onClick={() => copyToClipboard(content)} />
</div>
) : null)
export default withStyles(styles)(CopyBtn)

View File

@ -0,0 +1,53 @@
// @flow
import React from 'react'
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) => (
<a
className={classes.container}
href={getEtherScanLink(type, value, currentNetwork)}
target="_blank"
rel="noopener noreferrer"
title="Show Details on etherscan"
aria-label="Show Details on etherscan"
>
<Img src={SearchIcon} height={20} alt="Etherscan" />
</a>
)
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

@ -26,6 +26,6 @@ const EtherscanLink = ({ type, value, currentNetwork }: EtherscanLinkProps) => (
) )
export default connect<Object, Object, ?Function, ?Object>( export default connect<Object, Object, ?Function, ?Object>(
state => ({ currentNetwork: networkSelector(state) }), (state) => ({ currentNetwork: networkSelector(state) }),
null, null,
)(EtherscanLink) )(EtherscanLink)

View File

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

View File

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

View File

@ -2,30 +2,25 @@
import * as React from 'react' import * as React from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import OpenInNew from '@material-ui/icons/OpenInNew'
import { estimateGasForDeployingSafe } from '~/logic/contracts/safeContracts' import { estimateGasForDeployingSafe } from '~/logic/contracts/safeContracts'
import { getNamesFrom, getAccountsFrom } from '~/routes/open/utils/safeDataExtractor' import { getNamesFrom, getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import EtherscanBtn from '~/components/EtherscanBtn'
import CopyBtn from '~/components/CopyBtn'
import Identicon from '~/components/Identicon' import Identicon from '~/components/Identicon'
import OpenPaper from '~/components/Stepper/OpenPaper' import OpenPaper from '~/components/Stepper/OpenPaper'
import Col from '~/components/layout/Col' import Col from '~/components/layout/Col'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import Link from '~/components/layout/Link'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import { import {
sm, md, lg, border, secondary, background, sm, md, lg, border, background,
} from '~/theme/variables' } from '~/theme/variables'
import Hairline from '~/components/layout/Hairline' import Hairline from '~/components/layout/Hairline'
import { getEtherScanLink, getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { FIELD_NAME, FIELD_CONFIRMATIONS, getNumOwnersFrom } from '../fields' import { FIELD_NAME, FIELD_CONFIRMATIONS, getNumOwnersFrom } from '../fields'
const { useEffect, useState } = React const { useEffect, useState } = React
const openIconStyle = {
height: '16px',
color: secondary,
}
const styles = () => ({ const styles = () => ({
root: { root: {
minHeight: '300px', minHeight: '300px',
@ -58,6 +53,9 @@ const styles = () => ({
}, },
user: { user: {
justifyContent: 'left', justifyContent: 'left',
'& > p': {
marginRight: sm,
},
}, },
open: { open: {
paddingLeft: sm, paddingLeft: sm,
@ -69,15 +67,12 @@ const styles = () => ({
}) })
type Props = { type Props = {
network: string,
values: Object, values: Object,
classes: Object, classes: Object,
userAccount: string, userAccount: string,
} }
const ReviewComponent = ({ const ReviewComponent = ({ values, classes, userAccount }: Props) => {
values, classes, network, userAccount,
}: Props) => {
const [gasCosts, setGasCosts] = useState<string>('0.00') const [gasCosts, setGasCosts] = useState<string>('0.00')
const names = getNamesFrom(values) const names = getNamesFrom(values)
const addresses = getAccountsFrom(values) const addresses = getAccountsFrom(values)
@ -152,13 +147,8 @@ const ReviewComponent = ({
<Paragraph size="md" color="disabled" noMargin> <Paragraph size="md" color="disabled" noMargin>
{addresses[index]} {addresses[index]}
</Paragraph> </Paragraph>
<Link <CopyBtn content={addresses[index]} />
className={classes.open} <EtherscanBtn type="address" value={addresses[index]} />
to={getEtherScanLink('address', addresses[index], network)}
target="_blank"
>
<OpenInNew style={openIconStyle} />
</Link>
</Block> </Block>
</Block> </Block>
</Col> </Col>
@ -184,10 +174,10 @@ ETH in this wallet to fund this transaction.
const ReviewPage = withStyles(styles)(ReviewComponent) const ReviewPage = withStyles(styles)(ReviewComponent)
const Review = ({ network }: LayoutProps) => (controls: React.Node, { values }: Object) => ( const Review = () => (controls: React.Node, { values }: Object) => (
<> <>
<OpenPaper controls={controls} padding={false}> <OpenPaper controls={controls} padding={false}>
<ReviewPage network={network} values={values} /> <ReviewPage values={values} />
</OpenPaper> </OpenPaper>
</> </>
) )

View File

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

View File

@ -75,7 +75,7 @@ type Props = {
const Receive = ({ const Receive = ({
classes, onClose, safeAddress, safeName, etherScanLink, classes, onClose, safeAddress, safeName, etherScanLink,
}: Props) => ( }: Props) => (
<React.Fragment> <>
<Row align="center" grow className={classes.heading}> <Row align="center" grow className={classes.heading}>
<Paragraph className={classes.manage} weight="bolder" noMargin> <Paragraph className={classes.manage} weight="bolder" noMargin>
Receive funds Receive funds
@ -113,7 +113,7 @@ const Receive = ({
Done Done
</Button> </Button>
</Row> </Row>
</React.Fragment> </>
) )
export default withStyles(styles)(Receive) export default withStyles(styles)(Receive)

View File

@ -40,7 +40,7 @@ const Tokens = (props: Props) => {
} = props } = props
return ( return (
<React.Fragment> <>
<Row align="center" grow className={classes.heading}> <Row align="center" grow className={classes.heading}>
<Paragraph className={classes.manage} noMargin weight="bolder"> <Paragraph className={classes.manage} noMargin weight="bolder">
Manage Tokens Manage Tokens
@ -72,7 +72,7 @@ const Tokens = (props: Props) => {
tokens={tokens} tokens={tokens}
/> />
)} )}
</React.Fragment> </>
) )
} }

View File

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