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",
"polished": "^3.4.2",
"qrcode.react": "1.0.0",
"query-string": "^6.9.0",
"react": "16.12.0",
"react-dev-utils": "^10.0.0",
"react-dom": "16.12.0",

View File

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

View File

@ -9,23 +9,54 @@ import Row from '~/components/layout/Row'
import Review from '~/routes/open/components/ReviewInformation'
import SafeNameField from '~/routes/open/components/SafeNameForm'
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 { 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 initialValuesFrom = (userAccount: string) => ({
[getOwnerNameBy(0)]: 'My Wallet',
[getOwnerAddressBy(0)]: userAccount,
[FIELD_CONFIRMATIONS]: '1',
})
const initialValuesFrom = (userAccount: string, safeProps?: SafePropsType) => {
if (!safeProps) {
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 = {
provider: string,
userAccount: string,
network: string,
onCallSafeContractSubmit: (values: Object) => Promise<void>,
safeProps?: SafePropsType,
}
const iconStyle = {
@ -44,11 +75,14 @@ const formMutators = {
},
}
const Layout = ({
provider, userAccount, onCallSafeContractSubmit, network,
}: Props) => {
const Layout = (props: Props) => {
const {
provider, userAccount, onCallSafeContractSubmit, network, safeProps,
} = props
const steps = getSteps()
const initialValues = initialValuesFrom(userAccount)
const initialValues = initialValuesFrom(userAccount, safeProps)
return (
<>
@ -75,7 +109,7 @@ const Layout = ({
</Stepper>
</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 = {
classes: Object,
safeName?: string,
}
const styles = () => ({
@ -32,12 +33,13 @@ const styles = () => ({
},
})
const SafeName = ({ classes }: Props) => (
const SafeName = ({ classes, safeName }: Props) => (
<>
<Block margin="lg">
<Paragraph noMargin size="md" color="primary">
You are about to create a new Gnosis Safe wallet with one or more owners. First, let&apos;s give your new wallet
a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
You are about to create a new Gnosis Safe wallet with one or more owners. First, let&apos;s give your new
wallet
a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
</Paragraph>
</Block>
<Block margin="lg" className={classes.root}>
@ -48,23 +50,24 @@ const SafeName = ({ classes }: Props) => (
validate={required}
placeholder="Name of the new Safe"
text="Safe name"
defaultValue={safeName}
/>
</Block>
<Block margin="lg">
<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">
terms of use
terms of use
</a>
{' '}
and
and
{' '}
<a rel="noopener noreferrer" href="https://safe.gnosis.io/privacy" target="_blank">
privacy policy
privacy policy
</a>
. Most importantly, you confirm that your funds are held securely in the Gnosis Safe, a smart contract on the
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
. Most importantly, you confirm that your funds are held securely in the Gnosis Safe, a smart contract on the
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
</Paragraph>
</Block>
</>
@ -72,10 +75,13 @@ const SafeName = ({ classes }: Props) => (
const SafeNameForm = withStyles(styles)(SafeName)
const SafeNamePage = () => (controls: React.Node) => (
<OpenPaper controls={controls}>
<SafeNameForm />
</OpenPaper>
)
const SafeNamePage = () => (controls: React.Node, { values }) => {
const { safeName } = values
return (
<OpenPaper controls={controls}>
<SafeNameForm safeName={safeName} />
</OpenPaper>
)
}
export default SafeNamePage

View File

@ -4,6 +4,7 @@ import { withStyles } from '@material-ui/core/styles'
import InputAdornment from '@material-ui/core/InputAdornment'
import CheckCircle from '@material-ui/icons/CheckCircle'
import MenuItem from '@material-ui/core/MenuItem'
import { withRouter } from 'react-router-dom'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import SelectField from '~/components/forms/SelectField'
@ -70,6 +71,7 @@ const SafeOwners = (props: Props) => {
} = props
const validOwners = getNumOwnersFrom(values)
const [numOwners, setNumOwners] = useState<number>(validOwners)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
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) => (
<>

View File

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

View File

@ -1,6 +1,8 @@
// @flow
import * as React from 'react'
import { connect } from 'react-redux'
import queryString from 'query-string'
import { withRouter } from 'react-router-dom'
import Page from '~/components/layout/Page'
import {
getAccountsFrom, getThresholdFrom, getNamesFrom, getSafeNameFrom, getOwnersFrom,
@ -24,6 +26,30 @@ export type OpenState = {
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> => {
const numConfirmations = getThresholdFrom(values)
const name = getSafeNameFrom(values)
@ -75,8 +101,23 @@ class Open extends React.Component<Props> {
}
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 (
<Page>
<Layout
@ -84,10 +125,11 @@ class Open extends React.Component<Props> {
provider={provider}
userAccount={userAccount}
onCallSafeContractSubmit={this.onCallSafeContractSubmit}
safeProps={safeProps}
/>
</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')
type Props = {
provider: string
provider: string,
isOldMultisigMigration?: boolean,
}
const openIconStyle = {
@ -64,14 +65,20 @@ export const LoadSafe = ({ size, provider }: SafeProps) => (
</Button>
)
const Welcome = ({ provider }: Props) => (
<Block className={styles.safe}>
<Heading tag="h1" weight="bold" align="center" margin="lg">
Welcome to
const Welcome = ({ provider, isOldMultisigMigration }: Props) => {
const headingText = isOldMultisigMigration ? (
<>
We will replicate the owner structure from your existing Gnosis Multisig
<br />
Gnosis Safe For Teams
</Heading>
<Heading tag="h3" align="center" margin="xl">
to let you test the new interface.
<br />
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
<br />
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
experience.
{' '}
<a
className={styles.learnMoreLink}
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} />
</>
)
return (
<Block className={styles.safe}>
<Heading tag="h1" weight="bold" align="center" margin="lg">
Welcome to
<br />
Gnosis Safe For Teams
</Heading>
<Heading tag="h3" align="center" margin="xl">
{ headingText }
<a
className={styles.learnMoreLink}
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 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>
)
}
export default Welcome