Feature #122: Multisig migration (#315)

* Adds query-string package.json
Parses query string on open layout

* Implements load all the values on openSafe view from param querys

* Adds query params validation

* Moves query parse logic to open.jsx

* Changes default no metamask component on open page

* Replaces global isNaN

* Fix threshold parsing validation

* Updates the welcome component with new verbiage for open

* Renames isOpenSafe to isOldMultisigMigration

* Merge branch 'development' of https://github.com/gnosis/safe-react into 122-multisig-migration

# Conflicts:
#	src/routes/open/components/Layout.jsx

* Merge branch 'development' of https://github.com/gnosis/safe-react into 159-pending-transactions

# Conflicts:
#	src/routes/safe/components/Transactions/index.jsx
#	yarn.lock
This commit is contained in:
Agustin Pane 2019-12-11 08:26:09 -03:00 committed by Germán Martínez
parent f0b3172abe
commit e7ba5e5392
8 changed files with 172 additions and 64 deletions

View File

@ -54,6 +54,7 @@
"optimize-css-assets-webpack-plugin": "5.0.3", "optimize-css-assets-webpack-plugin": "5.0.3",
"polished": "^3.4.2", "polished": "^3.4.2",
"qrcode.react": "1.0.0", "qrcode.react": "1.0.0",
"query-string": "^6.9.0",
"react": "16.12.0", "react": "16.12.0",
"react-dev-utils": "^10.0.0", "react-dev-utils": "^10.0.0",
"react-dom": "16.12.0", "react-dom": "16.12.0",

View File

@ -19,6 +19,7 @@ type Props = {
testId?: string, testId?: string,
validators?: Function[], validators?: Function[],
inputAdornment?: React.Element, inputAdornment?: React.Element,
defaultValue?: string,
} }
const isValidEnsName = (name) => /^([\w-]+\.)+(eth|test|xyz|luxe)$/.test(name) const isValidEnsName = (name) => /^([\w-]+\.)+(eth|test|xyz|luxe)$/.test(name)
@ -35,6 +36,7 @@ const AddressInput = ({
testId, testId,
inputAdornment, inputAdornment,
validators = [], validators = [],
defaultValue,
}: Props): React.Element<*> => ( }: Props): React.Element<*> => (
<> <>
<Field <Field
@ -51,6 +53,7 @@ const AddressInput = ({
text={text} text={text}
className={className} className={className}
testId={testId} testId={testId}
defaultValue={defaultValue}
/> />
<OnChange name={name}> <OnChange name={name}>
{async (value) => { {async (value) => {

View File

@ -9,23 +9,54 @@ import Row from '~/components/layout/Row'
import Review from '~/routes/open/components/ReviewInformation' import Review from '~/routes/open/components/ReviewInformation'
import SafeNameField from '~/routes/open/components/SafeNameForm' import SafeNameField from '~/routes/open/components/SafeNameForm'
import SafeOwnersFields from '~/routes/open/components/SafeOwnersConfirmationsForm' import SafeOwnersFields from '~/routes/open/components/SafeOwnersConfirmationsForm'
import { getOwnerNameBy, getOwnerAddressBy, FIELD_CONFIRMATIONS } from '~/routes/open/components/fields' import {
getOwnerNameBy,
getOwnerAddressBy,
FIELD_CONFIRMATIONS,
FIELD_SAFE_NAME,
} from '~/routes/open/components/fields'
import { history } from '~/store' import { history } from '~/store'
import { secondary, sm } from '~/theme/variables' import { secondary, sm } from '~/theme/variables'
import type { SafePropsType } from '~/routes/open/container/Open'
import Welcome from '~/routes/welcome/components/Layout'
const getSteps = () => ['Name', 'Owners and confirmations', 'Review'] const getSteps = () => ['Name', 'Owners and confirmations', 'Review']
const initialValuesFrom = (userAccount: string) => ({
[getOwnerNameBy(0)]: 'My Wallet', const initialValuesFrom = (userAccount: string, safeProps?: SafePropsType) => {
[getOwnerAddressBy(0)]: userAccount, if (!safeProps) {
[FIELD_CONFIRMATIONS]: '1', return ({
}) [getOwnerNameBy(0)]: 'My Wallet',
[getOwnerAddressBy(0)]: userAccount,
[FIELD_CONFIRMATIONS]: '1',
})
}
let obj = {}
const {
ownerAddresses, ownerNames, threshold, name,
} = safeProps
// eslint-disable-next-line no-restricted-syntax
for (const [index, value] of ownerAddresses.entries()) {
const safeName = ownerNames[index] ? ownerNames[index] : 'My Wallet'
obj = {
...obj,
[getOwnerAddressBy(index)]: value,
[getOwnerNameBy(index)]: safeName,
}
}
return ({
...obj,
[FIELD_CONFIRMATIONS]: threshold || '1',
[FIELD_SAFE_NAME]: name,
})
}
type Props = { type Props = {
provider: string, provider: string,
userAccount: string, userAccount: string,
network: string, network: string,
onCallSafeContractSubmit: (values: Object) => Promise<void>, onCallSafeContractSubmit: (values: Object) => Promise<void>,
safeProps?: SafePropsType,
} }
const iconStyle = { const iconStyle = {
@ -44,11 +75,14 @@ const formMutators = {
}, },
} }
const Layout = ({
provider, userAccount, onCallSafeContractSubmit, network, const Layout = (props: Props) => {
}: Props) => { const {
provider, userAccount, onCallSafeContractSubmit, network, safeProps,
} = props
const steps = getSteps() const steps = getSteps()
const initialValues = initialValuesFrom(userAccount)
const initialValues = initialValuesFrom(userAccount, safeProps)
return ( return (
<> <>
@ -75,7 +109,7 @@ const Layout = ({
</Stepper> </Stepper>
</Block> </Block>
) : ( ) : (
<div>No web3 provider detected</div> <Welcome provider={provider} isOldMultisigMigration />
)} )}
</> </>
) )

View File

@ -12,6 +12,7 @@ import { sm, secondary } from '~/theme/variables'
type Props = { type Props = {
classes: Object, classes: Object,
safeName?: string,
} }
const styles = () => ({ const styles = () => ({
@ -32,12 +33,13 @@ const styles = () => ({
}, },
}) })
const SafeName = ({ classes }: Props) => ( const SafeName = ({ classes, safeName }: Props) => (
<> <>
<Block margin="lg"> <Block margin="lg">
<Paragraph noMargin size="md" color="primary"> <Paragraph noMargin size="md" color="primary">
You are about to create a new Gnosis Safe wallet with one or more owners. First, let&apos;s give your new wallet You are about to create a new Gnosis Safe wallet with one or more owners. First, let&apos;s give your new
a name. This name is only stored locally and will never be shared with Gnosis or any third parties. wallet
a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
</Paragraph> </Paragraph>
</Block> </Block>
<Block margin="lg" className={classes.root}> <Block margin="lg" className={classes.root}>
@ -48,23 +50,24 @@ const SafeName = ({ classes }: Props) => (
validate={required} validate={required}
placeholder="Name of the new Safe" placeholder="Name of the new Safe"
text="Safe name" text="Safe name"
defaultValue={safeName}
/> />
</Block> </Block>
<Block margin="lg"> <Block margin="lg">
<Paragraph noMargin size="md" color="primary" className={classes.links}> <Paragraph noMargin size="md" color="primary" className={classes.links}>
By continuing you consent with the By continuing you consent with the
{' '} {' '}
<a rel="noopener noreferrer" href="https://safe.gnosis.io/terms" target="_blank"> <a rel="noopener noreferrer" href="https://safe.gnosis.io/terms" target="_blank">
terms of use terms of use
</a> </a>
{' '} {' '}
and and
{' '} {' '}
<a rel="noopener noreferrer" href="https://safe.gnosis.io/privacy" target="_blank"> <a rel="noopener noreferrer" href="https://safe.gnosis.io/privacy" target="_blank">
privacy policy privacy policy
</a> </a>
. Most importantly, you confirm that your funds are held securely in the Gnosis Safe, a smart contract on the . 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. Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
</Paragraph> </Paragraph>
</Block> </Block>
</> </>
@ -72,10 +75,13 @@ const SafeName = ({ classes }: Props) => (
const SafeNameForm = withStyles(styles)(SafeName) const SafeNameForm = withStyles(styles)(SafeName)
const SafeNamePage = () => (controls: React.Node) => ( const SafeNamePage = () => (controls: React.Node, { values }) => {
<OpenPaper controls={controls}> const { safeName } = values
<SafeNameForm /> return (
</OpenPaper> <OpenPaper controls={controls}>
) <SafeNameForm safeName={safeName} />
</OpenPaper>
)
}
export default SafeNamePage export default SafeNamePage

View File

@ -4,6 +4,7 @@ import { withStyles } from '@material-ui/core/styles'
import InputAdornment from '@material-ui/core/InputAdornment' import InputAdornment from '@material-ui/core/InputAdornment'
import CheckCircle from '@material-ui/icons/CheckCircle' import CheckCircle from '@material-ui/icons/CheckCircle'
import MenuItem from '@material-ui/core/MenuItem' import MenuItem from '@material-ui/core/MenuItem'
import { withRouter } from 'react-router-dom'
import Field from '~/components/forms/Field' import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField' import TextField from '~/components/forms/TextField'
import SelectField from '~/components/forms/SelectField' import SelectField from '~/components/forms/SelectField'
@ -70,6 +71,7 @@ const SafeOwners = (props: Props) => {
} = props } = props
const validOwners = getNumOwnersFrom(values) const validOwners = getNumOwnersFrom(values)
const [numOwners, setNumOwners] = useState<number>(validOwners) const [numOwners, setNumOwners] = useState<number>(validOwners)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false) const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [scanQrForOwnerName, setScanQrForOwnerName] = useState<string | null>(null) const [scanQrForOwnerName, setScanQrForOwnerName] = useState<string | null>(null)
@ -222,7 +224,7 @@ owner(s)
) )
} }
const SafeOwnersForm = withStyles(styles)(SafeOwners) const SafeOwnersForm = withStyles(styles)(withRouter(SafeOwners))
const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors, form }: Object) => ( const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors, form }: Object) => (
<> <>

View File

@ -2,6 +2,7 @@
export const FIELD_NAME: string = 'name' export const FIELD_NAME: string = 'name'
export const FIELD_CONFIRMATIONS: string = 'confirmations' export const FIELD_CONFIRMATIONS: string = 'confirmations'
export const FIELD_OWNERS: string = 'owners' export const FIELD_OWNERS: string = 'owners'
export const FIELD_SAFE_NAME: string = 'safeName'
export const getOwnerNameBy = (index: number) => `owner${index}Name` export const getOwnerNameBy = (index: number) => `owner${index}Name`
export const getOwnerAddressBy = (index: number) => `owner${index}Address` export const getOwnerAddressBy = (index: number) => `owner${index}Address`

View File

@ -1,6 +1,8 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import queryString from 'query-string'
import { withRouter } from 'react-router-dom'
import Page from '~/components/layout/Page' import Page from '~/components/layout/Page'
import { import {
getAccountsFrom, getThresholdFrom, getNamesFrom, getSafeNameFrom, getOwnersFrom, getAccountsFrom, getThresholdFrom, getNamesFrom, getSafeNameFrom, getOwnersFrom,
@ -24,6 +26,30 @@ export type OpenState = {
safeAddress: string, safeAddress: string,
} }
export type SafePropsType = {
name: string,
ownerAddresses: string[],
ownerNames: string[],
threshold: string,
}
const validateQueryParams = (ownerAddresses?: string[], ownerNames?: string[], threshold?: string, safeName?: string) => {
if (!ownerAddresses || !ownerNames || !threshold || !safeName) {
return false
}
if (!ownerAddresses.length === 0 || ownerNames.length === 0) {
return false
}
if (Number.isNaN(Number(threshold))) {
return false
}
if (threshold > ownerAddresses.length) {
return false
}
return true
}
export const createSafe = async (values: Object, userAccount: string, addSafe: AddSafe): Promise<OpenState> => { export const createSafe = async (values: Object, userAccount: string, addSafe: AddSafe): Promise<OpenState> => {
const numConfirmations = getThresholdFrom(values) const numConfirmations = getThresholdFrom(values)
const name = getSafeNameFrom(values) const name = getSafeNameFrom(values)
@ -75,8 +101,23 @@ class Open extends React.Component<Props> {
} }
render() { render() {
const { provider, userAccount, network } = this.props const {
provider, userAccount, network, location,
} = this.props
const query: SafePropsType = queryString.parse(location.search, { arrayFormat: 'comma' })
const {
name, owneraddresses, ownernames, threshold,
} = query
let safeProps = null
if (validateQueryParams(owneraddresses, ownernames, threshold, name)) {
safeProps = {
name,
ownerAddresses: owneraddresses,
ownerNames: ownernames,
threshold,
}
}
return ( return (
<Page> <Page>
<Layout <Layout
@ -84,10 +125,11 @@ class Open extends React.Component<Props> {
provider={provider} provider={provider}
userAccount={userAccount} userAccount={userAccount}
onCallSafeContractSubmit={this.onCallSafeContractSubmit} onCallSafeContractSubmit={this.onCallSafeContractSubmit}
safeProps={safeProps}
/> />
</Page> </Page>
) )
} }
} }
export default connect(selector, actions)(Open) export default connect(selector, actions)(withRouter(Open))

View File

@ -15,7 +15,8 @@ const safe = require('../assets/safe.svg')
const plus = require('../assets/new.svg') const plus = require('../assets/new.svg')
type Props = { type Props = {
provider: string provider: string,
isOldMultisigMigration?: boolean,
} }
const openIconStyle = { const openIconStyle = {
@ -64,14 +65,20 @@ export const LoadSafe = ({ size, provider }: SafeProps) => (
</Button> </Button>
) )
const Welcome = ({ provider }: Props) => (
<Block className={styles.safe}> const Welcome = ({ provider, isOldMultisigMigration }: Props) => {
<Heading tag="h1" weight="bold" align="center" margin="lg"> const headingText = isOldMultisigMigration ? (
Welcome to <>
We will replicate the owner structure from your existing Gnosis Multisig
<br /> <br />
Gnosis Safe For Teams to let you test the new interface.
</Heading> <br />
<Heading tag="h3" align="center" margin="xl"> As soon as you feel comfortable, start moving funds to your new Safe.
<br />
{' '}
</>
) : (
<>
Gnosis Safe for Teams is the most secure way to manage crypto funds Gnosis Safe for Teams is the most secure way to manage crypto funds
<br /> <br />
collectively. It is an improvement of the Gnosis MultiSig, which is used collectively. It is an improvement of the Gnosis MultiSig, which is used
@ -85,34 +92,46 @@ const Welcome = ({ provider }: Props) => (
design, formally verified smart contracts and vastly improved user design, formally verified smart contracts and vastly improved user
experience. experience.
{' '} {' '}
<a </>
className={styles.learnMoreLink} )
href="https://safe.gnosis.io/teams" return (
target="_blank" <Block className={styles.safe}>
rel="noopener noreferrer" <Heading tag="h1" weight="bold" align="center" margin="lg">
> Welcome to
Learn more <br />
<OpenInNew style={openIconStyle} /> Gnosis Safe For Teams
</a> </Heading>
</Heading> <Heading tag="h3" align="center" margin="xl">
{provider ? ( { headingText }
<> <a
<Block className={styles.safeActions} margin="md"> className={styles.learnMoreLink}
<CreateSafe size="large" provider={provider} /> href="https://safe.gnosis.io/teams"
target="_blank"
rel="noopener noreferrer"
>
Learn more
<OpenInNew style={openIconStyle} />
</a>
</Heading>
{provider ? (
<>
<Block className={styles.safeActions} margin="md">
<CreateSafe size="large" provider={provider} />
</Block>
<Block className={styles.safeActions} margin="md">
<LoadSafe size="large" provider={provider} />
</Block>
</>
) : (
<Block margin="md" className={styles.connectWallet}>
<Heading tag="h3" align="center" margin="md">
Get Started by Connecting a Wallet
</Heading>
<ConnectButton minWidth={240} minHeight={42} />
</Block> </Block>
<Block className={styles.safeActions} margin="md"> )}
<LoadSafe size="large" provider={provider} /> </Block>
</Block> )
</> }
) : (
<Block margin="md" className={styles.connectWallet}>
<Heading tag="h3" align="center" margin="md">
Get Started by Connecting a Wallet
</Heading>
<ConnectButton minWidth={240} minHeight={42} />
</Block>
)}
</Block>
)
export default Welcome export default Welcome