Merge pull request #118 from gnosis/load-safe-improvements

Load safe improvements
This commit is contained in:
Germán Martínez 2019-06-17 09:24:15 +02:00 committed by GitHub
commit 4b470d870f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 373 additions and 85 deletions

View File

@ -53,7 +53,7 @@ class HeaderComponent extends React.PureComponent<Props, State> {
let currentProvider: ProviderProps = await getProviderInfo() let currentProvider: ProviderProps = await getProviderInfo()
fetchProvider(currentProvider, openSnackbar) fetchProvider(currentProvider, openSnackbar)
this.providerListener = setInterval(async () => { this.providerListener = setInterval(async () => {
const newProvider: ProviderProps = await getProviderInfo() const newProvider: ProviderProps = await getProviderInfo()
if (JSON.stringify(currentProvider) !== JSON.stringify(newProvider)) { if (JSON.stringify(currentProvider) !== JSON.stringify(newProvider)) {
fetchProvider(newProvider, openSnackbar) fetchProvider(newProvider, openSnackbar)

View File

@ -7,12 +7,13 @@ import Heading from '~/components/layout/Heading'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import ReviewInformation from '~/routes/load/components/ReviewInformation' import ReviewInformation from '~/routes/load/components/ReviewInformation'
import OwnerList from '~/routes/load/components/OwnerList'
import DetailsForm, { safeFieldsValidation } from '~/routes/load/components/DetailsForm' import DetailsForm, { safeFieldsValidation } from '~/routes/load/components/DetailsForm'
import { history } from '~/store' import { history } from '~/store'
import { secondary } from '~/theme/variables' import { secondary } from '~/theme/variables'
import { type SelectorProps } from '~/routes/load/container/selector' import { type SelectorProps } from '~/routes/load/container/selector'
const getSteps = () => ['Details', 'Review'] const getSteps = () => ['Details', 'Owners', 'Review']
type Props = SelectorProps & { type Props = SelectorProps & {
onLoadSafeSubmit: (values: Object) => Promise<void>, onLoadSafeSubmit: (values: Object) => Promise<void>,
@ -32,6 +33,7 @@ const Layout = ({
provider, onLoadSafeSubmit, network, userAddress, provider, onLoadSafeSubmit, network, userAddress,
}: Props) => { }: Props) => {
const steps = getSteps() const steps = getSteps()
const initialValues = {}
return ( return (
<React.Fragment> <React.Fragment>
@ -43,15 +45,16 @@ const Layout = ({
</IconButton> </IconButton>
<Heading tag="h2">Load existing Safe</Heading> <Heading tag="h2">Load existing Safe</Heading>
</Row> </Row>
<Stepper onSubmit={onLoadSafeSubmit} steps={steps} testId="load-safe-form"> <Stepper onSubmit={onLoadSafeSubmit} steps={steps} initialValues={initialValues} testId="load-safe-form">
<Stepper.Page validate={safeFieldsValidation}>{DetailsForm}</Stepper.Page> <Stepper.Page validate={safeFieldsValidation}>{DetailsForm}</Stepper.Page>
<Stepper.Page network={network}>{OwnerList}</Stepper.Page>
<Stepper.Page network={network} userAddress={userAddress}> <Stepper.Page network={network} userAddress={userAddress}>
{ReviewInformation} {ReviewInformation}
</Stepper.Page> </Stepper.Page>
</Stepper> </Stepper>
</Block> </Block>
) : ( ) : (
<div>No metamask detected</div> <div>No account detected</div>
)} )}
</React.Fragment> </React.Fragment>
) )

View File

@ -0,0 +1,191 @@
// @flow
import * as React from 'react'
import Block from '~/components/layout/Block'
import { withStyles } from '@material-ui/core/styles'
import Field from '~/components/forms/Field'
import { required } from '~/components/forms/validator'
import TextField from '~/components/forms/TextField'
import OpenInNew from '@material-ui/icons/OpenInNew'
import Identicon from '~/components/Identicon'
import OpenPaper from '~/components/Stepper/OpenPaper'
import Col from '~/components/layout/Col'
import Row from '~/components/layout/Row'
import Link from '~/components/layout/Link'
import Paragraph from '~/components/layout/Paragraph'
import Hairline from '~/components/layout/Hairline'
import {
sm, md, lg, border, secondary,
} from '~/theme/variables'
import { getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields'
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import { FIELD_LOAD_ADDRESS, THRESHOLD } from '~/routes/load/components/fields'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
const openIconStyle = {
height: '16px',
color: secondary,
}
const styles = () => ({
details: {
padding: lg,
borderRight: `solid 1px ${border}`,
height: '100%',
},
owners: {
display: 'flex',
justifyContent: 'flex-start',
},
ownerNames: {
maxWidth: '400px',
},
ownerAddresses: {
alignItems: 'center',
marginLeft: `${sm}`,
},
address: {
paddingLeft: '6px',
},
open: {
paddingLeft: sm,
width: 'auto',
'&:hover': {
cursor: 'pointer',
},
},
title: {
padding: `${md} ${lg}`,
},
owner: {
padding: `0 ${lg}`,
marginBottom: '12px',
},
header: {
padding: `${sm} ${lg}`,
},
name: {
marginRight: `${sm}`,
},
})
type LayoutProps = {
network: string,
}
type Props = LayoutProps & {
values: Object,
classes: Object,
updateInitialProps: (initialValues: Object) => void,
}
type State = {
owners: Array<string>,
}
const calculateSafeValues = (owners: Array<string>, threshold: Number, values: Object) => {
const initialValues = { ...values }
for (let i = 0; i < owners.length; i += 1) {
initialValues[getOwnerAddressBy(i)] = owners[i]
}
initialValues[THRESHOLD] = threshold
return initialValues
}
class OwnerListComponent extends React.PureComponent<Props, State> {
state = {
owners: [],
}
mounted = false
componentDidMount = async () => {
this.mounted = true
const { values, updateInitialProps } = this.props
const safeAddress = values[FIELD_LOAD_ADDRESS]
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const owners = await gnosisSafe.getOwners()
const threshold = await gnosisSafe.getThreshold()
const initialValues = calculateSafeValues(owners.sort(), threshold, values)
updateInitialProps(initialValues)
if (!owners) {
return
}
if (this.mounted) {
this.setState(() => ({ owners: owners.sort() }))
}
}
componentWillUnmount() {
this.mounted = false
}
render() {
const { network, classes } = this.props
const { owners } = this.state
return (
<React.Fragment>
<Block className={classes.title}>
<Paragraph noMargin size="md" color="primary">
{`This Safe has ${owners.length} owners. Optional: Provide a name for each owner.`}
</Paragraph>
</Block>
<Hairline />
<Row className={classes.header}>
<Col xs={4}>NAME</Col>
<Col xs={8}>ADDRESS</Col>
</Row>
<Hairline />
<Block margin="md" padding="md">
{owners.map((x, index) => (
<Row key={owners[index].address} className={classes.owner}>
<Col xs={4}>
<Field
className={classes.name}
name={getOwnerNameBy(index)}
component={TextField}
type="text"
validate={required}
defaultValue={`Owner #${index + 1}`}
placeholder="Owner Name*"
text="Owner Name"
/>
</Col>
<Col xs={7}>
<Row className={classes.ownerAddresses}>
<Identicon address={owners[index]} diameter={32} />
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
{owners[index]}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink(owners[index], network)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Row>
</Col>
</Row>
)) }
</Block>
</React.Fragment>
)
}
}
const OwnerListPage = withStyles(styles)(OwnerListComponent)
const OwnerList = ({ updateInitialProps }: Object, network: string) => (controls: React$Node, { values }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} padding={false}>
<OwnerListPage
network={network}
updateInitialProps={updateInitialProps}
values={values}
/>
</OpenPaper>
</React.Fragment>
)
export default OwnerList

View File

@ -1,20 +1,24 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import classNames from 'classnames'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import OpenInNew from '@material-ui/icons/OpenInNew' import OpenInNew from '@material-ui/icons/OpenInNew'
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 Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import Link from '~/components/layout/Link' 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 { import {
xs, sm, lg, border, secondary, xs, sm, lg, border, secondary,
} from '~/theme/variables' } from '~/theme/variables'
import { getEtherScanLink, getWeb3 } from '~/logic/wallets/getWeb3' import { shortVersionOf } from '~/logic/wallets/ethAddresses'
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields' import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
import { sameAddress } from '~/logic/wallets/ethAddresses' import { getOwnerNameBy, getOwnerAddressBy, getNumOwnersFrom } from '~/routes/open/components/fields'
import { getGnosisSafeContract } from '~/logic/contracts/safeContracts' import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS, THRESHOLD } from '~/routes/load/components/fields'
const openIconStyle = { const openIconStyle = {
height: '16px', height: '16px',
@ -22,20 +26,31 @@ const openIconStyle = {
} }
const styles = () => ({ const styles = () => ({
root: {
minHeight: '300px',
},
details: { details: {
padding: lg, padding: lg,
borderRight: `solid 1px ${border}`, borderRight: `solid 1px ${border}`,
height: '100%', height: '100%',
}, },
name: { owners: {
letterSpacing: '-0.6px', padding: lg,
}, },
container: { name: {
marginTop: xs, textOverflow: 'ellipsis',
overflow: 'hidden',
},
userName: {
whiteSpace: 'nowrap',
},
owner: {
padding: sm,
paddingLeft: lg,
alignItems: 'center', alignItems: 'center',
}, },
address: { user: {
paddingLeft: '6px', justifyContent: 'left',
}, },
open: { open: {
paddingLeft: sm, paddingLeft: sm,
@ -44,6 +59,13 @@ const styles = () => ({
cursor: 'pointer', cursor: 'pointer',
}, },
}, },
container: {
marginTop: xs,
alignItems: 'center',
},
address: {
paddingLeft: '6px',
},
}) })
type LayoutProps = { type LayoutProps = {
@ -60,77 +82,113 @@ type State = {
isOwner: boolean, isOwner: boolean,
} }
const checkUserAddressOwner = (values: Object, userAddress: string): boolean => {
let isOwner: boolean = false
for (let i = 0; i < getNumOwnersFrom(values); i += 1) {
if (values[getOwnerAddressBy(i)] === userAddress) {
isOwner = true
break
}
}
return isOwner
}
class ReviewComponent extends React.PureComponent<Props, State> { class ReviewComponent extends React.PureComponent<Props, State> {
state = {
isOwner: false,
}
mounted = false
componentDidMount = async () => {
this.mounted = true
const { values, userAddress } = this.props
const safeAddress = values[FIELD_LOAD_ADDRESS]
const web3 = getWeb3()
const GnosisSafe = getGnosisSafeContract(web3)
const gnosisSafe = await GnosisSafe.at(safeAddress)
const owners = await gnosisSafe.getOwners()
if (!owners) {
return
}
const isOwner = owners.find((owner: string) => sameAddress(owner, userAddress)) !== undefined
if (this.mounted) {
this.setState(() => ({ isOwner }))
}
}
componentWillUnmount() {
this.mounted = false
}
render() { render() {
const { values, classes, network } = this.props const {
const { isOwner } = this.state values, classes, network, userAddress,
} = this.props
const isOwner = checkUserAddressOwner(values, userAddress)
const owners = getAccountsFrom(values)
const safeAddress = values[FIELD_LOAD_ADDRESS] const safeAddress = values[FIELD_LOAD_ADDRESS]
return ( return (
<React.Fragment> <React.Fragment>
<Block className={classes.details}> <Row className={classes.root}>
<Block margin="lg"> <Col xs={4} layout="column">
<Paragraph size="sm" color="disabled" noMargin> <Block className={classes.details}>
Name of the Safe <Block margin="lg">
</Paragraph> <Paragraph size="lg" color="primary" noMargin>
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}> Review details
{values[FIELD_LOAD_NAME]} </Paragraph>
</Paragraph> </Block>
</Block> <Block margin="lg">
<Block margin="lg"> <Paragraph size="sm" color="disabled" noMargin>
<Paragraph size="sm" color="disabled" noMargin> Name of the Safe
Safe address </Paragraph>
</Paragraph> <Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
<Row className={classes.container}> {values[FIELD_LOAD_NAME]}
<Identicon address={safeAddress} diameter={32} /> </Paragraph>
<Paragraph size="md" color="disabled" noMargin className={classes.address}> </Block>
{safeAddress} <Block margin="lg">
<Paragraph size="sm" color="disabled" noMargin>
Safe address
</Paragraph>
<Row className={classes.container}>
<Identicon address={safeAddress} diameter={32} />
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
{shortVersionOf(safeAddress, 4)}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink(safeAddress, network)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Row>
</Block>
<Block margin="lg">
<Paragraph size="sm" color="disabled" noMargin>
Connected wallet client is owner?
</Paragraph>
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
{isOwner ? 'Yes' : 'No (read-only)'}
</Paragraph>
</Block>
<Block margin="lg">
<Paragraph size="sm" color="disabled" noMargin>
Any transaction requires the confirmation of:
</Paragraph>
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
{`${values[THRESHOLD]} out of ${getNumOwnersFrom(values)} owners`}
</Paragraph>
</Block>
</Block>
</Col>
<Col xs={8} layout="column">
<Block className={classes.owners}>
<Paragraph size="lg" color="primary" noMargin>
{`${getNumOwnersFrom(values)} Safe owners`}
</Paragraph> </Paragraph>
<Link className={classes.open} to={getEtherScanLink(safeAddress, network)} target="_blank"> </Block>
<OpenInNew style={openIconStyle} /> <Hairline />
</Link> {owners.map((x, index) => (
</Row> <React.Fragment key={owners[index].address}>
</Block> <Row className={classes.owner}>
<Block margin="lg"> <Col xs={1} align="center">
<Paragraph size="sm" color="disabled" noMargin> <Identicon address={owners[index]} diameter={32} />
Connected wallet client is owner? </Col>
</Paragraph> <Col xs={11}>
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}> <Block className={classNames(classes.name, classes.userName)}>
{isOwner ? 'Yes' : 'No (read-only)'} <Paragraph size="lg" noMargin>
</Paragraph> {values[getOwnerNameBy(index)]}
</Block> </Paragraph>
</Block> <Block align="center" className={classes.user}>
<Paragraph size="md" color="disabled" noMargin>
{owners[index]}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink(owners[index], network)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Block>
</Block>
</Col>
</Row>
<Hairline />
</React.Fragment>
))}
</Col>
</Row>
</React.Fragment> </React.Fragment>
) )
} }

View File

@ -1,3 +1,4 @@
// @flow // @flow
export const FIELD_LOAD_NAME: string = 'name' export const FIELD_LOAD_NAME: string = 'name'
export const FIELD_LOAD_ADDRESS: string = 'address' export const FIELD_LOAD_ADDRESS: string = 'address'
export const THRESHOLD: Number = 'threshold'

View File

@ -10,13 +10,20 @@ import { history } from '~/store'
import selector, { type SelectorProps } from './selector' import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions' import actions, { type Actions } from './actions'
import Layout from '../components/Layout' import Layout from '../components/Layout'
import { getNamesFrom, getOwnersFrom } from '~/routes/open/utils/safeDataExtractor'
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '../components/fields' import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '../components/fields'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
type Props = SelectorProps & Actions type Props = SelectorProps & Actions
export const loadSafe = async (safeName: string, safeAddress: string, addSafe: Function) => { export const loadSafe = async (
safeName: string,
safeAddress: string,
owners: Array,
addSafe: Function
) => {
const safeProps = await buildSafe(safeAddress, safeName) const safeProps = await buildSafe(safeAddress, safeName)
safeProps.owners = owners
await addSafe(safeProps) await addSafe(safeProps)
const storedSafes = (await loadFromStorage(SAFES_KEY)) || {} const storedSafes = (await loadFromStorage(SAFES_KEY)) || {}
@ -31,8 +38,13 @@ class Load extends React.Component<Props> {
const { addSafe } = this.props const { addSafe } = this.props
const safeName = values[FIELD_LOAD_NAME] const safeName = values[FIELD_LOAD_NAME]
const safeAddress = values[FIELD_LOAD_ADDRESS] const safeAddress = values[FIELD_LOAD_ADDRESS]
const ownerNames = getNamesFrom(values)
await loadSafe(safeName, safeAddress, addSafe) const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const ownerAddresses = await gnosisSafe.getOwners()
const owners = getOwnersFrom(ownerNames, ownerAddresses.sort())
await loadSafe(safeName, safeAddress, owners, addSafe)
const url = `${SAFELIST_ADDRESS}/${safeAddress}` const url = `${SAFELIST_ADDRESS}/${safeAddress}`
history.push(url) history.push(url)

View File

@ -1,4 +1,6 @@
// @flow // @flow
import { makeOwner } from '~/routes/safe/store/models/owner'
export const getAccountsFrom = (values: Object): string[] => { export const getAccountsFrom = (values: Object): string[] => {
const accounts = Object.keys(values) const accounts = Object.keys(values)
.sort() .sort()
@ -15,6 +17,17 @@ export const getNamesFrom = (values: Object): string[] => {
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> => {
const owners = names.map((name: string, index: number) => makeOwner(
{ name, address: addresses[index] },
))
return owners
}
export const getThresholdFrom = (values: Object): number => Number(values.confirmations) export const getThresholdFrom = (values: Object): number => Number(values.confirmations)
export const getSafeNameFrom = (values: Object): string => values.name export const getSafeNameFrom = (values: Object): string => values.name

View File

@ -12,7 +12,7 @@ import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
import { sleep } from '~/utils/timer' import { sleep } from '~/utils/timer'
import { history } from '~/store' import { history } from '~/store'
import AppRoutes from '~/routes' import AppRoutes from '~/routes'
import { SAFELIST_ADDRESS, SETTINS_ADDRESS } from '~/routes/routes' import { SAFELIST_ADDRESS } from '~/routes/routes'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
export const EXPAND_BALANCE_INDEX = 0 export const EXPAND_BALANCE_INDEX = 0

View File

@ -1,3 +1,3 @@
// @flow // @flow
export * from './tokens' export * from './tokens'

View File

@ -6267,11 +6267,16 @@ ejs@^2.6.1:
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0"
integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==
electron-to-chromium@^1.3.122, electron-to-chromium@^1.3.150, electron-to-chromium@^1.3.47: electron-to-chromium@^1.3.122, electron-to-chromium@^1.3.47:
version "1.3.158" version "1.3.158"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.158.tgz#5e16909dcfd25ab7cd1665114ee381083a3ee858" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.158.tgz#5e16909dcfd25ab7cd1665114ee381083a3ee858"
integrity sha512-wJsJaWsViNQ129XPGmyO5gGs1jPMHr9vffjHAhUje1xZbEzQcqbENdvfyRD9q8UF0TgFQFCCUbaIpJarFbvsIg== integrity sha512-wJsJaWsViNQ129XPGmyO5gGs1jPMHr9vffjHAhUje1xZbEzQcqbENdvfyRD9q8UF0TgFQFCCUbaIpJarFbvsIg==
electron-to-chromium@^1.3.150:
version "1.3.155"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.155.tgz#ebf0cc8eeaffd6151d1efad60fd9e021fb45fd3a"
integrity sha512-/ci/XgZG8jkLYOgOe3mpJY1onxPPTDY17y7scldhnSjjZqV6VvREG/LvwhRuV7BJbnENFfuDWZkSqlTh4x9ZjQ==
elliptic@6.3.3: elliptic@6.3.3:
version "6.3.3" version "6.3.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.3.tgz#5482d9646d54bcb89fd7d994fc9e2e9568876e3f" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.3.tgz#5482d9646d54bcb89fd7d994fc9e2e9568876e3f"
@ -10728,11 +10733,16 @@ loglevel-colored-level-prefix@^1.0.0:
chalk "^1.1.3" chalk "^1.1.3"
loglevel "^1.4.1" loglevel "^1.4.1"
loglevel@^1.4.1, loglevel@^1.6.2: loglevel@^1.4.1:
version "1.6.3" version "1.6.3"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280"
integrity sha512-LoEDv5pgpvWgPF4kNYuIp0qqSJVWak/dML0RY74xlzMZiT9w77teNAwKYKWBTYjlokMirg+o3jBwp+vlLrcfAA== integrity sha512-LoEDv5pgpvWgPF4kNYuIp0qqSJVWak/dML0RY74xlzMZiT9w77teNAwKYKWBTYjlokMirg+o3jBwp+vlLrcfAA==
loglevel@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.2.tgz#668c77948a03dbd22502a3513ace1f62a80cc372"
integrity sha512-Jt2MHrCNdtIe1W6co3tF5KXGRkzF+TYffiQstfXa04mrss9IKXzAAXYWak8LbZseAQY03sH2GzMCMU0ZOUc9bg==
looper@^2.0.0: looper@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/looper/-/looper-2.0.0.tgz#66cd0c774af3d4fedac53794f742db56da8f09ec" resolved "https://registry.yarnpkg.com/looper/-/looper-2.0.0.tgz#66cd0c774af3d4fedac53794f742db56da8f09ec"
@ -13595,7 +13605,7 @@ react-transition-group@^4.0.0:
object-assign "^4.1.0" object-assign "^4.1.0"
prop-types "^15.5.10" prop-types "^15.5.10"
react@^16.8.3, react@^16.8.6: react@^16.7.0, react@^16.8.3, react@^16.8.6:
version "16.8.6" version "16.8.6"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==