Merge branch 'development' of github.com:gnosis/safe-react into 125-ens
This commit is contained in:
commit
2beb8afc29
|
@ -53,6 +53,7 @@
|
||||||
"react-final-form-listeners": "^1.0.2",
|
"react-final-form-listeners": "^1.0.2",
|
||||||
"react-hot-loader": "4.12.9",
|
"react-hot-loader": "4.12.9",
|
||||||
"react-infinite-scroll-component": "^4.5.2",
|
"react-infinite-scroll-component": "^4.5.2",
|
||||||
|
"react-qr-reader": "^2.2.1",
|
||||||
"react-redux": "7.1.0",
|
"react-redux": "7.1.0",
|
||||||
"react-router-dom": "^5.0.1",
|
"react-router-dom": "^5.0.1",
|
||||||
"recompose": "^0.30.0",
|
"recompose": "^0.30.0",
|
||||||
|
@ -123,7 +124,7 @@
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"mini-css-extract-plugin": "0.8.0",
|
"mini-css-extract-plugin": "0.8.0",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"postcss-mixins": "^6.2.0",
|
"postcss-mixins": "6.2.2",
|
||||||
"postcss-simple-vars": "^5.0.2",
|
"postcss-simple-vars": "^5.0.2",
|
||||||
"pre-commit": "^1.2.2",
|
"pre-commit": "^1.2.2",
|
||||||
"prettier-eslint-cli": "5.0.0",
|
"prettier-eslint-cli": "5.0.0",
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 55.2 (78181) - https://sketchapp.com -->
|
||||||
|
<title>Group</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Icon-/-QR-Icon" transform="translate(-11.000000, -11.000000)" fill="#a2a8ba">
|
||||||
|
<g id="QR-Icon" transform="translate(2.000000, 2.000000)">
|
||||||
|
<g id="Group" transform="translate(9.000000, 9.000000)">
|
||||||
|
<path d="M2,2 L2,4 L4,4 L4,2 L2,2 Z M6,0 L6,6 L0,6 L0,0 L6,0 Z" id="Rectangle" fill-rule="nonzero"></path>
|
||||||
|
<path d="M2,12 L2,14 L4,14 L4,12 L2,12 Z M6,10 L6,16 L0,16 L0,10 L6,10 Z" id="Rectangle-Copy-2" fill-rule="nonzero"></path>
|
||||||
|
<path d="M12,2 L12,4 L14,4 L14,2 L12,2 Z M16,0 L16,6 L10,6 L10,0 L16,0 Z" id="Rectangle-Copy" fill-rule="nonzero"></path>
|
||||||
|
<rect id="Rectangle" x="7" y="2" width="2" height="4"></rect>
|
||||||
|
<path d="M7,9 L5,9 L5,7 L7,7 L9,7 L9,11 L7,11 L7,9 Z" id="Combined-Shape"></path>
|
||||||
|
<rect id="Rectangle-Copy-4" x="0" y="7" width="2" height="2"></rect>
|
||||||
|
<rect id="Rectangle-Copy-5" x="10" y="7" width="2" height="2"></rect>
|
||||||
|
<rect id="Rectangle-Copy-6" x="14" y="7" width="2" height="2"></rect>
|
||||||
|
<rect id="Rectangle-Copy-7" x="12" y="9" width="2" height="2"></rect>
|
||||||
|
<rect id="Rectangle-Copy-10" x="12" y="14" width="2" height="2"></rect>
|
||||||
|
<path d="M9,12 L10,12 L10,11 L12,11 L12,14 L10,14 L9,14 L9,16 L7,16 L7,12 L9,12 Z" id="Combined-Shape"></path>
|
||||||
|
<rect id="Rectangle-Copy-9" x="14" y="11" width="2" height="3"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import Page from '~/components/layout/Page'
|
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||||
|
import Page from '~/components/layout/Page'
|
||||||
|
|
||||||
const centerStyle = {
|
const centerStyle = {
|
||||||
margin: 'auto 0',
|
margin: 'auto 0',
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
import Stepper from '@material-ui/core/Stepper'
|
import Stepper from '@material-ui/core/Stepper'
|
||||||
import FormStep from '@material-ui/core/Step'
|
import FormStep from '@material-ui/core/Step'
|
||||||
import StepLabel from '@material-ui/core/StepLabel'
|
import StepLabel from '@material-ui/core/StepLabel'
|
||||||
import StepContent from '@material-ui/core/StepContent'
|
import StepContent from '@material-ui/core/StepContent'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import * as React from 'react'
|
|
||||||
import GnoForm from '~/components/forms/GnoForm'
|
import GnoForm from '~/components/forms/GnoForm'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import Button from '~/components/layout/Button'
|
|
||||||
import { history } from '~/store'
|
import { history } from '~/store'
|
||||||
import Controls from './Controls'
|
import Controls from './Controls'
|
||||||
|
|
||||||
|
const { useState, useEffect } = React
|
||||||
|
|
||||||
export { default as Step } from './Step'
|
export { default as Step } from './Step'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -18,20 +19,15 @@ type Props = {
|
||||||
onSubmit: (values: Object) => Promise<void>,
|
onSubmit: (values: Object) => Promise<void>,
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
onReset?: () => void,
|
|
||||||
initialValues?: Object,
|
initialValues?: Object,
|
||||||
disabledWhenValidating?: boolean,
|
disabledWhenValidating?: boolean,
|
||||||
|
mutators?: Object,
|
||||||
testId?: string,
|
testId?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
|
||||||
page: number,
|
|
||||||
values: Object,
|
|
||||||
}
|
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
children: Function,
|
children: Function,
|
||||||
prepareNextInitialProps: (values: Object) => {},
|
prepareNextInitialProps?: (values: Object) => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
const transitionProps = {
|
const transitionProps = {
|
||||||
|
@ -41,121 +37,95 @@ const transitionProps = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class GnoStepper extends React.PureComponent<Props, State> {
|
export const StepperPage = ({ children }: PageProps) => children
|
||||||
static Page = ({ children }: PageProps) => children
|
|
||||||
|
|
||||||
static FinishButton = ({
|
const GnoStepper = (props: Props) => {
|
||||||
component, to, title, ...props
|
const [page, setPage] = useState<number>(0)
|
||||||
}) => (
|
const [values, setValues] = useState<Object>({})
|
||||||
<Button component={component} to={to} variant="contained" color="primary" {...props}>
|
|
||||||
{title}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
useEffect(() => {
|
||||||
super(props)
|
if (props.initialValues) {
|
||||||
|
setValues(props.initialValues)
|
||||||
this.state = {
|
|
||||||
page: 0,
|
|
||||||
values: props.initialValues || {},
|
|
||||||
}
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getPageProps = (pages: React.Node): PageProps => React.Children.toArray(pages)[page].props
|
||||||
|
|
||||||
|
const updateInitialProps = (newInitialProps) => {
|
||||||
|
setValues(newInitialProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
onReset = () => {
|
const getActivePageFrom = (pages: React.Node) => {
|
||||||
const { onReset, initialValues } = this.props
|
const activePageProps = getPageProps(pages)
|
||||||
if (onReset) {
|
const { children, ...restProps } = activePageProps
|
||||||
onReset()
|
|
||||||
|
return children({ ...restProps, updateInitialProps })
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(() => ({
|
const validate = (valuesToValidate: Object) => {
|
||||||
page: 0,
|
const { children } = props
|
||||||
values: initialValues || {},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
getPageProps = (pages: React.Node): PageProps => {
|
|
||||||
const { page } = this.state
|
|
||||||
|
|
||||||
return React.Children.toArray(pages)[page].props
|
|
||||||
}
|
|
||||||
|
|
||||||
getActivePageFrom = (pages: React.Node) => {
|
|
||||||
const activePageProps = this.getPageProps(pages)
|
|
||||||
const { children, ...props } = activePageProps
|
|
||||||
|
|
||||||
return children({ ...props, updateInitialProps: this.updateInitialProps })
|
|
||||||
}
|
|
||||||
|
|
||||||
updateInitialProps = (values) => {
|
|
||||||
this.setState({ values })
|
|
||||||
}
|
|
||||||
|
|
||||||
validate = (values: Object) => {
|
|
||||||
const { children } = this.props
|
|
||||||
const { page } = this.state
|
|
||||||
|
|
||||||
const activePage = React.Children.toArray(children)[page]
|
const activePage = React.Children.toArray(children)[page]
|
||||||
return activePage.props.validate ? activePage.props.validate(values) : {}
|
return activePage.props.validate ? activePage.props.validate(valuesToValidate) : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
next = async (values: Object) => {
|
const next = async (formValues: Object) => {
|
||||||
const { children } = this.props
|
const { children } = props
|
||||||
const activePageProps = this.getPageProps(children)
|
const activePageProps = getPageProps(children)
|
||||||
const { prepareNextInitialProps } = activePageProps
|
const { prepareNextInitialProps } = activePageProps
|
||||||
|
|
||||||
let pageInitialProps
|
let pageInitialProps
|
||||||
if (prepareNextInitialProps) {
|
if (prepareNextInitialProps) {
|
||||||
pageInitialProps = await prepareNextInitialProps(values)
|
pageInitialProps = await prepareNextInitialProps(formValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalValues = { ...values, ...pageInitialProps }
|
const finalValues = { ...formValues, ...pageInitialProps }
|
||||||
this.setState(state => ({
|
|
||||||
page: Math.min(state.page + 1, React.Children.count(children) - 1),
|
setValues(finalValues)
|
||||||
values: finalValues,
|
setPage(Math.min(page + 1, React.Children.count(children) - 1))
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
previous = () => {
|
const previous = () => {
|
||||||
const { page } = this.state
|
|
||||||
|
|
||||||
const firstPage = page === 0
|
const firstPage = page === 0
|
||||||
if (firstPage) {
|
if (firstPage) {
|
||||||
return history.goBack()
|
return history.goBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.setState(state => ({
|
return setPage(Math.max(page - 1, 0))
|
||||||
page: Math.max(state.page - 1, 0),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = async (values: Object) => {
|
const handleSubmit = async (formValues: Object) => {
|
||||||
const { children, onSubmit } = this.props
|
const { children, onSubmit } = props
|
||||||
const { page } = this.state
|
|
||||||
const isLastPage = page === React.Children.count(children) - 1
|
const isLastPage = page === React.Children.count(children) - 1
|
||||||
if (isLastPage) {
|
if (isLastPage) {
|
||||||
return onSubmit(values)
|
return onSubmit(formValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.next(values)
|
return next(formValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
isLastPage = (page) => {
|
const isLastPage = (pageNumber) => {
|
||||||
const { steps } = this.props
|
const { steps } = props
|
||||||
return page === steps.length - 1
|
return pageNumber === steps.length - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
const {
|
||||||
steps, children, classes, disabledWhenValidating = false, testId,
|
steps, children, classes, disabledWhenValidating = false, testId, mutators,
|
||||||
} = this.props
|
} = props
|
||||||
const { page, values } = this.state
|
const activePage = getActivePageFrom(children)
|
||||||
const activePage = this.getActivePageFrom(children)
|
|
||||||
const lastPage = this.isLastPage(page)
|
const lastPage = isLastPage(page)
|
||||||
const penultimate = this.isLastPage(page + 1)
|
const penultimate = isLastPage(page + 1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<GnoForm onSubmit={this.handleSubmit} initialValues={values} validation={this.validate} testId={testId}>
|
<GnoForm
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
initialValues={values}
|
||||||
|
validation={validate}
|
||||||
|
testId={testId}
|
||||||
|
formMutators={mutators}
|
||||||
|
>
|
||||||
{(submitting: boolean, validating: boolean, ...rest: any) => {
|
{(submitting: boolean, validating: boolean, ...rest: any) => {
|
||||||
const disabled = disabledWhenValidating ? submitting || validating : submitting
|
const disabled = disabledWhenValidating ? submitting || validating : submitting
|
||||||
const controls = (
|
const controls = (
|
||||||
|
@ -163,7 +133,7 @@ class GnoStepper extends React.PureComponent<Props, State> {
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Controls
|
<Controls
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onPrevious={this.previous}
|
onPrevious={previous}
|
||||||
firstPage={page === 0}
|
firstPage={page === 0}
|
||||||
lastPage={lastPage}
|
lastPage={lastPage}
|
||||||
penultimate={penultimate}
|
penultimate={penultimate}
|
||||||
|
@ -185,7 +155,6 @@ class GnoStepper extends React.PureComponent<Props, State> {
|
||||||
</GnoForm>
|
</GnoForm>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import classNames from 'classnames/bind'
|
import classNames from 'classnames/bind'
|
||||||
import React, { PureComponent } from 'react'
|
import * as React from 'react'
|
||||||
import { capitalize } from '~/utils/css'
|
import { capitalize } from '~/utils/css'
|
||||||
import { type Size } from '~/theme/size'
|
import { type Size } from '~/theme/size'
|
||||||
import styles from './index.scss'
|
import styles from './index.scss'
|
||||||
|
|
||||||
|
const { PureComponent } = React
|
||||||
|
|
||||||
const cx = classNames.bind(styles)
|
const cx = classNames.bind(styles)
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ChevronLeft from '@material-ui/icons/ChevronLeft'
|
import ChevronLeft from '@material-ui/icons/ChevronLeft'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import Stepper from '~/components/Stepper'
|
import Stepper, { StepperPage } from '~/components/Stepper'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Heading from '~/components/layout/Heading'
|
import Heading from '~/components/layout/Heading'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
|
@ -46,11 +46,11 @@ const Layout = ({
|
||||||
<Heading tag="h2">Load existing Safe</Heading>
|
<Heading tag="h2">Load existing Safe</Heading>
|
||||||
</Row>
|
</Row>
|
||||||
<Stepper onSubmit={onLoadSafeSubmit} steps={steps} initialValues={initialValues} testId="load-safe-form">
|
<Stepper onSubmit={onLoadSafeSubmit} steps={steps} initialValues={initialValues} testId="load-safe-form">
|
||||||
<Stepper.Page validate={safeFieldsValidation}>{DetailsForm}</Stepper.Page>
|
<StepperPage validate={safeFieldsValidation}>{DetailsForm}</StepperPage>
|
||||||
<Stepper.Page network={network}>{OwnerList}</Stepper.Page>
|
<StepperPage network={network}>{OwnerList}</StepperPage>
|
||||||
<Stepper.Page network={network} userAddress={userAddress}>
|
<StepperPage network={network} userAddress={userAddress}>
|
||||||
{ReviewInformation}
|
{ReviewInformation}
|
||||||
</Stepper.Page>
|
</StepperPage>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
</Block>
|
</Block>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import Stepper from '~/components/Stepper'
|
import ChevronLeft from '@material-ui/icons/ChevronLeft'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import Stepper, { StepperPage } from '~/components/Stepper'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Heading from '~/components/layout/Heading'
|
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 Review from '~/routes/open/components/ReviewInformation'
|
import Review from '~/routes/open/components/ReviewInformation'
|
||||||
import ChevronLeft from '@material-ui/icons/ChevronLeft'
|
|
||||||
import SafeNameField from '~/routes/open/components/SafeNameForm'
|
import SafeNameField from '~/routes/open/components/SafeNameForm'
|
||||||
import SafeThresholdField, { safeFieldsValidation } from '~/routes/open/components/SafeThresholdForm'
|
import SafeOwnersFields from '~/routes/open/components/SafeOwnersConfirmationsForm'
|
||||||
import SafeOwnersFields from '~/routes/open/components/SafeOwnersForm'
|
|
||||||
import { getOwnerNameBy, getOwnerAddressBy, FIELD_CONFIRMATIONS } from '~/routes/open/components/fields'
|
import { getOwnerNameBy, getOwnerAddressBy, FIELD_CONFIRMATIONS } from '~/routes/open/components/fields'
|
||||||
import { history } from '~/store'
|
import { history } from '~/store'
|
||||||
import { secondary } from '~/theme/variables'
|
import { secondary } from '~/theme/variables'
|
||||||
|
|
||||||
const getSteps = () => ['Start', 'Owners', 'Confirmations', 'Review']
|
const getSteps = () => ['Start', 'Owners and confirmations', 'Review']
|
||||||
|
|
||||||
const initialValuesFrom = (userAccount: string) => ({
|
const initialValuesFrom = (userAccount: string) => ({
|
||||||
[getOwnerNameBy(0)]: 'My Metamask (me)',
|
[getOwnerNameBy(0)]: 'My Metamask (me)',
|
||||||
|
@ -39,6 +38,12 @@ const back = () => {
|
||||||
history.goBack()
|
history.goBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formMutators = {
|
||||||
|
setValue: ([field, value], state, { changeValue }) => {
|
||||||
|
changeValue(state, field, () => value)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const Layout = ({
|
const Layout = ({
|
||||||
provider, userAccount, onCallSafeContractSubmit, network,
|
provider, userAccount, onCallSafeContractSubmit, network,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
@ -55,15 +60,20 @@ const Layout = ({
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Heading tag="h2">Create New Safe</Heading>
|
<Heading tag="h2">Create New Safe</Heading>
|
||||||
</Row>
|
</Row>
|
||||||
<Stepper onSubmit={onCallSafeContractSubmit} steps={steps} initialValues={initialValues} testId="create-safe-form">
|
<Stepper
|
||||||
<Stepper.Page>{SafeNameField}</Stepper.Page>
|
onSubmit={onCallSafeContractSubmit}
|
||||||
<Stepper.Page>{SafeOwnersFields}</Stepper.Page>
|
steps={steps}
|
||||||
<Stepper.Page validate={safeFieldsValidation}>{SafeThresholdField}</Stepper.Page>
|
initialValues={initialValues}
|
||||||
<Stepper.Page network={network}>{Review}</Stepper.Page>
|
mutators={formMutators}
|
||||||
|
testId="create-safe-form"
|
||||||
|
>
|
||||||
|
<StepperPage>{SafeNameField}</StepperPage>
|
||||||
|
<StepperPage>{SafeOwnersFields}</StepperPage>
|
||||||
|
<StepperPage network={network}>{Review}</StepperPage>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
</Block>
|
</Block>
|
||||||
) : (
|
) : (
|
||||||
<div>No metamask detected</div>
|
<div>No web3 provider detected</div>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
@ -129,7 +129,11 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{addresses[index]}
|
{addresses[index]}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink('address', addresses[index], network)} target="_blank">
|
<Link
|
||||||
|
className={classes.open}
|
||||||
|
to={getEtherScanLink('address', addresses[index], network)}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -143,10 +147,8 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
|
||||||
</Row>
|
</Row>
|
||||||
<Row className={classes.info} align="center">
|
<Row className={classes.info} align="center">
|
||||||
<Paragraph noMargin color="primary" size="md">
|
<Paragraph noMargin color="primary" size="md">
|
||||||
{"You're about to create a new Safe."}
|
You're about to create a new Safe and will have to confirm a transaction with your currently connected
|
||||||
</Paragraph>
|
wallet. Make sure you have ETH in this wallet to fund this transaction.
|
||||||
<Paragraph noMargin color="primary" size="md">
|
|
||||||
Make sure you have enough ETH in your wallet client to fund this transaction.
|
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -5,7 +5,6 @@ import Field from '~/components/forms/Field'
|
||||||
import TextField from '~/components/forms/TextField'
|
import TextField from '~/components/forms/TextField'
|
||||||
import { required } from '~/components/forms/validator'
|
import { required } from '~/components/forms/validator'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Row from '~/components/layout/Row'
|
|
||||||
import { FIELD_NAME } from '~/routes/open/components/fields'
|
import { FIELD_NAME } from '~/routes/open/components/fields'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||||
|
@ -35,10 +34,25 @@ const styles = () => ({
|
||||||
|
|
||||||
const SafeName = ({ classes }: Props) => (
|
const SafeName = ({ classes }: Props) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
<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'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}>
|
||||||
|
<Field
|
||||||
|
name={FIELD_NAME}
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={required}
|
||||||
|
placeholder="Name of the new Safe"
|
||||||
|
text="Safe name"
|
||||||
|
/>
|
||||||
|
</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}>
|
||||||
This setup will create a Safe with one or more owners. Optionally give the Safe a local name. By continuing you
|
By continuing you consent with the
|
||||||
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
|
||||||
|
@ -49,35 +63,10 @@ const SafeName = ({ classes }: Props) => (
|
||||||
<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
|
||||||
|
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
<Row margin="md" className={classes.text}>
|
|
||||||
<Paragraph noMargin className={classes.dot} color="secondary">
|
|
||||||
●
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph noMargin size="md" color="primary" weight="bolder">
|
|
||||||
I understand that my funds are held securely in my Safe. They cannot be accessed by Gnosis.
|
|
||||||
</Paragraph>
|
|
||||||
</Row>
|
|
||||||
<Row margin="md">
|
|
||||||
<Paragraph noMargin className={classes.dot} color="secondary">
|
|
||||||
●
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph noMargin size="md" color="primary" weight="bolder">
|
|
||||||
My Safe is a smart contract on the Ethereum blockchain.
|
|
||||||
</Paragraph>
|
|
||||||
</Row>
|
|
||||||
<Block margin="lg" className={classes.root}>
|
|
||||||
<Field
|
|
||||||
name={FIELD_NAME}
|
|
||||||
component={TextField}
|
|
||||||
type="text"
|
|
||||||
validate={required}
|
|
||||||
placeholder="Name of the new Safe"
|
|
||||||
text="Safe name"
|
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import QrReader from 'react-qr-reader'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Modal from '~/components/Modal'
|
||||||
|
import { checkWebcam } from './utils'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
const { useEffect, useState } = React
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
classes: Object,
|
||||||
|
onScan: Function,
|
||||||
|
isOpen: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScanQRModal = ({
|
||||||
|
classes, onClose, isOpen, onScan,
|
||||||
|
}: Props) => {
|
||||||
|
const [hasWebcam, setHasWebcam] = useState(null)
|
||||||
|
const scannerRef: Object = React.createRef()
|
||||||
|
const openImageDialog = () => {
|
||||||
|
scannerRef.current.openImageDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkWebcam(
|
||||||
|
() => {
|
||||||
|
setHasWebcam(true)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
setHasWebcam(false)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// this fires only when the hasWebcam changes to false (null > false (user doesn't have webcam)
|
||||||
|
// , true > false (user switched from webcam to file upload))
|
||||||
|
// Doesn't fire on re-render
|
||||||
|
if (hasWebcam === false) {
|
||||||
|
openImageDialog()
|
||||||
|
}
|
||||||
|
}, [hasWebcam])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title="Receive Tokens" description="Receive Tokens Form" handleClose={onClose} open={isOpen}>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph className={classes.manage} weight="bolder" noMargin>
|
||||||
|
Scan QR
|
||||||
|
</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.close} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Col layout="column" middle="xs" className={classes.detailsContainer}>
|
||||||
|
{hasWebcam === null ? (
|
||||||
|
<Block align="center" className={classes.loaderContainer}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Block>
|
||||||
|
) : (
|
||||||
|
<QrReader
|
||||||
|
ref={scannerRef}
|
||||||
|
legacyMode={!hasWebcam}
|
||||||
|
onScan={(data) => {
|
||||||
|
if (data) onScan(data)
|
||||||
|
}}
|
||||||
|
onError={(err) => {
|
||||||
|
console.error(err)
|
||||||
|
}}
|
||||||
|
style={{ width: '400px', height: '400px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
className={classes.button}
|
||||||
|
minHeight={42}
|
||||||
|
minWidth={140}
|
||||||
|
onClick={onClose}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
className={classes.button}
|
||||||
|
minWidth={140}
|
||||||
|
minHeight={42}
|
||||||
|
onClick={() => {
|
||||||
|
if (hasWebcam) {
|
||||||
|
setHasWebcam(false)
|
||||||
|
} else {
|
||||||
|
openImageDialog()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Upload an image
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ScanQRModal)
|
|
@ -0,0 +1,35 @@
|
||||||
|
// @flow
|
||||||
|
import { lg, sm, background } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${sm} ${lg}`,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
maxHeight: '75px',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
loaderContainer: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
manage: {
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
detailsContainer: {
|
||||||
|
backgroundColor: background,
|
||||||
|
maxHeight: '420px',
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
'&:last-child': {
|
||||||
|
marginLeft: sm,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,15 @@
|
||||||
|
// @flow
|
||||||
|
navigator.getMedia = navigator.getUserMedia // use the proper vendor prefix
|
||||||
|
|| navigator.webkitGetUserMedia
|
||||||
|
|| navigator.mozGetUserMedia
|
||||||
|
|| navigator.msGetUserMedia
|
||||||
|
|
||||||
|
export const checkWebcam = (success: Function, err: Function) => navigator.getMedia(
|
||||||
|
{ video: true },
|
||||||
|
() => {
|
||||||
|
success()
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
err()
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,231 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
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 Field from '~/components/forms/Field'
|
||||||
|
import TextField from '~/components/forms/TextField'
|
||||||
|
import SelectField from '~/components/forms/SelectField'
|
||||||
|
import {
|
||||||
|
required, composeValidators, noErrorsOn, mustBeInteger, minValue,
|
||||||
|
} from '~/components/forms/validator'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Img from '~/components/layout/Img'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import {
|
||||||
|
FIELD_CONFIRMATIONS,
|
||||||
|
getOwnerNameBy,
|
||||||
|
getOwnerAddressBy,
|
||||||
|
getNumOwnersFrom,
|
||||||
|
} from '~/routes/open/components/fields'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||||
|
import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import trash from '~/assets/icons/trash.svg'
|
||||||
|
import QRIcon from '~/assets/icons/qrcode.svg'
|
||||||
|
import ScanQRModal from './ScanQRModal'
|
||||||
|
import { getAddressValidators } from './validators'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes: Object,
|
||||||
|
otherAccounts: string[],
|
||||||
|
errors: Object,
|
||||||
|
form: Object,
|
||||||
|
values: Object,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { useState } = React
|
||||||
|
|
||||||
|
export const ADD_OWNER_BUTTON = '+ ADD ANOTHER OWNER'
|
||||||
|
|
||||||
|
export const calculateValuesAfterRemoving = (index: number, notRemovedOwners: number, values: Object) => {
|
||||||
|
const initialValues = { ...values }
|
||||||
|
|
||||||
|
const numOwnersAfterRemoving = notRemovedOwners - 1
|
||||||
|
// muevo indices
|
||||||
|
for (let i = index; i < numOwnersAfterRemoving; i += 1) {
|
||||||
|
initialValues[getOwnerNameBy(i)] = values[getOwnerNameBy(i + 1)]
|
||||||
|
initialValues[getOwnerAddressBy(i)] = values[getOwnerAddressBy(i + 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (+values[FIELD_CONFIRMATIONS] === notRemovedOwners) {
|
||||||
|
initialValues[FIELD_CONFIRMATIONS] = numOwnersAfterRemoving.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete initialValues[getOwnerNameBy(index)]
|
||||||
|
delete initialValues[getOwnerAddressBy(index)]
|
||||||
|
|
||||||
|
return initialValues
|
||||||
|
}
|
||||||
|
|
||||||
|
const SafeOwners = (props: Props) => {
|
||||||
|
const {
|
||||||
|
classes, errors, otherAccounts, values, form,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const validOwners = getNumOwnersFrom(values)
|
||||||
|
const [numOwners, setNumOwners] = useState<number>(validOwners)
|
||||||
|
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
||||||
|
const [scanQrForOwnerName, setScanQrForOwnerName] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const openQrModal = (ownerName) => {
|
||||||
|
setScanQrForOwnerName(ownerName)
|
||||||
|
setQrModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeQrModal = () => {
|
||||||
|
setQrModalOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRemoveRow = (index: number) => () => {
|
||||||
|
const initialValues = calculateValuesAfterRemoving(index, numOwners, values)
|
||||||
|
form.reset(initialValues)
|
||||||
|
|
||||||
|
setNumOwners(numOwners - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAddOwner = () => {
|
||||||
|
setNumOwners(numOwners + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScan = (value) => {
|
||||||
|
let scannedAddress = value
|
||||||
|
|
||||||
|
if (scannedAddress.startsWith('ethereum:')) {
|
||||||
|
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
form.mutators.setValue(scanQrForOwnerName, scannedAddress)
|
||||||
|
closeQrModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Block className={classes.title}>
|
||||||
|
<Paragraph noMargin size="md" color="primary">
|
||||||
|
Specify the owners of the Safe.
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row className={classes.header}>
|
||||||
|
<Col xs={4}>NAME</Col>
|
||||||
|
<Col xs={8}>ADDRESS</Col>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block margin="md" padding="md">
|
||||||
|
{[...Array(Number(numOwners))].map((x, index) => {
|
||||||
|
const addressName = getOwnerAddressBy(index)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row key={`owner${index}`} className={classes.owner}>
|
||||||
|
<Col xs={4}>
|
||||||
|
<Field
|
||||||
|
className={classes.name}
|
||||||
|
name={getOwnerNameBy(index)}
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={required}
|
||||||
|
placeholder="Owner Name*"
|
||||||
|
text="Owner Name"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6}>
|
||||||
|
<Field
|
||||||
|
name={addressName}
|
||||||
|
component={TextField}
|
||||||
|
inputAdornment={
|
||||||
|
noErrorsOn(addressName, errors) && {
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<CheckCircle className={classes.check} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
validate={getAddressValidators(otherAccounts, index)}
|
||||||
|
placeholder="Owner Address*"
|
||||||
|
text="Owner Address"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={1} center="xs" middle="xs" className={classes.remove}>
|
||||||
|
<Img
|
||||||
|
src={QRIcon}
|
||||||
|
height={20}
|
||||||
|
alt="Scan QR"
|
||||||
|
onClick={() => {
|
||||||
|
openQrModal(addressName)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={1} center="xs" middle="xs" className={classes.remove}>
|
||||||
|
{index > 0 && <Img src={trash} height={20} alt="Delete" onClick={onRemoveRow(index)} />}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Block>
|
||||||
|
<Row align="center" grow className={classes.add} margin="xl">
|
||||||
|
<Button color="secondary" onClick={onAddOwner} data-testid="add-owner-btn">
|
||||||
|
<Paragraph weight="bold" size="md" noMargin>
|
||||||
|
{ADD_OWNER_BUTTON}
|
||||||
|
</Paragraph>
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
<Block margin="md" padding="md" className={classes.owner}>
|
||||||
|
<Paragraph size="md" color="primary">
|
||||||
|
Any transaction requires the confirmation of:
|
||||||
|
</Paragraph>
|
||||||
|
<Row margin="xl" align="center">
|
||||||
|
<Col xs={2}>
|
||||||
|
<Field
|
||||||
|
name={FIELD_CONFIRMATIONS}
|
||||||
|
component={SelectField}
|
||||||
|
validate={composeValidators(required, mustBeInteger, minValue(1))}
|
||||||
|
data-testid="threshold-select-input"
|
||||||
|
>
|
||||||
|
{[...Array(Number(validOwners))].map((x, index) => (
|
||||||
|
<MenuItem key={`selectOwner${index}`} value={`${index + 1}`}>
|
||||||
|
{index + 1}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
</Col>
|
||||||
|
<Col xs={10}>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin className={classes.owners}>
|
||||||
|
out of
|
||||||
|
{' '}
|
||||||
|
{validOwners}
|
||||||
|
{' '}
|
||||||
|
owner(s)
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onScan={handleScan} onClose={closeQrModal} />}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SafeOwnersForm = withStyles(styles)(SafeOwners)
|
||||||
|
|
||||||
|
const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors, form }: Object) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<OpenPaper controls={controls} padding={false}>
|
||||||
|
<SafeOwnersForm
|
||||||
|
otherAccounts={getAccountsFrom(values)}
|
||||||
|
errors={errors}
|
||||||
|
form={form}
|
||||||
|
updateInitialProps={updateInitialProps}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
</OpenPaper>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default SafeOwnersPage
|
|
@ -0,0 +1,44 @@
|
||||||
|
// @flow
|
||||||
|
import { md, lg, sm } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
root: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
padding: `0 ${lg}`,
|
||||||
|
marginTop: '12px',
|
||||||
|
'&:first-child': {
|
||||||
|
marginTop: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
padding: `${sm} ${lg}`,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
marginRight: `${sm}`,
|
||||||
|
},
|
||||||
|
trash: {
|
||||||
|
top: '5px',
|
||||||
|
},
|
||||||
|
add: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
check: {
|
||||||
|
color: '#03AE60',
|
||||||
|
height: '20px',
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
height: '56px',
|
||||||
|
maxWidth: '50px',
|
||||||
|
'&:hover': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
owners: {
|
||||||
|
paddingLeft: md,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,17 @@
|
||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
required,
|
||||||
|
composeValidators,
|
||||||
|
uniqueAddress,
|
||||||
|
mustBeEthereumAddress,
|
||||||
|
} from '~/components/forms/validator'
|
||||||
|
|
||||||
|
export const getAddressValidators = (addresses: string[], position: number) => {
|
||||||
|
// thanks Rich Harris
|
||||||
|
// https://twitter.com/Rich_Harris/status/1125850391155965952
|
||||||
|
const copy = addresses.slice()
|
||||||
|
copy[position] = copy[copy.length - 1]
|
||||||
|
copy.pop()
|
||||||
|
|
||||||
|
return composeValidators(required, mustBeEthereumAddress, uniqueAddress(copy))
|
||||||
|
}
|
|
@ -1,212 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react'
|
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
|
||||||
import InputAdornment from '@material-ui/core/InputAdornment'
|
|
||||||
import CheckCircle from '@material-ui/icons/CheckCircle'
|
|
||||||
import Field from '~/components/forms/Field'
|
|
||||||
import TextField from '~/components/forms/TextField'
|
|
||||||
import {
|
|
||||||
required,
|
|
||||||
composeValidators,
|
|
||||||
uniqueAddress,
|
|
||||||
mustBeEthereumAddress,
|
|
||||||
noErrorsOn,
|
|
||||||
} from '~/components/forms/validator'
|
|
||||||
import Block from '~/components/layout/Block'
|
|
||||||
import Button from '~/components/layout/Button'
|
|
||||||
import Row from '~/components/layout/Row'
|
|
||||||
import Img from '~/components/layout/Img'
|
|
||||||
import Col from '~/components/layout/Col'
|
|
||||||
import { getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields'
|
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
|
||||||
import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
|
|
||||||
import Hairline from '~/components/layout/Hairline'
|
|
||||||
import { md, lg, sm } from '~/theme/variables'
|
|
||||||
import trash from '~/assets/icons/trash.svg'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
classes: Object,
|
|
||||||
otherAccounts: string[],
|
|
||||||
errors: Object,
|
|
||||||
values: Object,
|
|
||||||
updateInitialProps: (initialValues: Object) => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
numOwners: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = () => ({
|
|
||||||
root: {
|
|
||||||
display: 'flex',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
padding: `${md} ${lg}`,
|
|
||||||
},
|
|
||||||
owner: {
|
|
||||||
padding: `0 ${lg}`,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
padding: `${sm} ${lg}`,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
marginRight: `${sm}`,
|
|
||||||
},
|
|
||||||
trash: {
|
|
||||||
top: '5px',
|
|
||||||
},
|
|
||||||
add: {
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
check: {
|
|
||||||
color: '#03AE60',
|
|
||||||
height: '20px',
|
|
||||||
},
|
|
||||||
remove: {
|
|
||||||
height: '56px',
|
|
||||||
marginTop: '12px',
|
|
||||||
maxWidth: '50px',
|
|
||||||
'&:hover': {
|
|
||||||
cursor: 'pointer',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const getAddressValidators = (addresses: string[], position: number) => {
|
|
||||||
// thanks Rich Harris
|
|
||||||
// https://twitter.com/Rich_Harris/status/1125850391155965952
|
|
||||||
const copy = addresses.slice()
|
|
||||||
copy[position] = copy[copy.length - 1]
|
|
||||||
copy.pop()
|
|
||||||
|
|
||||||
return composeValidators(required, mustBeEthereumAddress, uniqueAddress(copy))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ADD_OWNER_BUTTON = '+ ADD ANOTHER OWNER'
|
|
||||||
|
|
||||||
export const calculateValuesAfterRemoving = (index: number, notRemovedOwners: number, values: Object) => {
|
|
||||||
const initialValues = { ...values }
|
|
||||||
const numOwnersAfterRemoving = notRemovedOwners - 1
|
|
||||||
// muevo indices
|
|
||||||
for (let i = index; i < numOwnersAfterRemoving; i += 1) {
|
|
||||||
initialValues[getOwnerNameBy(i)] = values[getOwnerNameBy(i + 1)]
|
|
||||||
initialValues[getOwnerAddressBy(i)] = values[getOwnerAddressBy(i + 1)]
|
|
||||||
}
|
|
||||||
|
|
||||||
delete initialValues[getOwnerNameBy(numOwnersAfterRemoving)]
|
|
||||||
delete initialValues[getOwnerAddressBy(numOwnersAfterRemoving)]
|
|
||||||
|
|
||||||
return initialValues
|
|
||||||
}
|
|
||||||
|
|
||||||
class SafeOwners extends React.PureComponent<Props, State> {
|
|
||||||
state = {
|
|
||||||
numOwners: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemoveRow = (index: number) => () => {
|
|
||||||
const { values, updateInitialProps } = this.props
|
|
||||||
const { numOwners } = this.state
|
|
||||||
const initialValues = calculateValuesAfterRemoving(index, numOwners, values)
|
|
||||||
updateInitialProps(initialValues)
|
|
||||||
|
|
||||||
this.setState(state => ({
|
|
||||||
numOwners: state.numOwners - 1,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddOwner = () => {
|
|
||||||
this.setState(state => ({
|
|
||||||
numOwners: state.numOwners + 1,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { classes, errors, otherAccounts } = this.props
|
|
||||||
const { numOwners } = this.state
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Block className={classes.title}>
|
|
||||||
<Paragraph noMargin size="md" color="primary">
|
|
||||||
Specify the owners of the Safe.
|
|
||||||
</Paragraph>
|
|
||||||
</Block>
|
|
||||||
<Hairline />
|
|
||||||
<Row className={classes.header}>
|
|
||||||
<Col xs={4}>NAME</Col>
|
|
||||||
<Col xs={8}>ADDRESS</Col>
|
|
||||||
</Row>
|
|
||||||
<Hairline />
|
|
||||||
<Block margin="md" padding="md">
|
|
||||||
{[...Array(Number(numOwners))].map((x, index) => {
|
|
||||||
const addressName = getOwnerAddressBy(index)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row key={`owner${index}`} className={classes.owner}>
|
|
||||||
<Col xs={4}>
|
|
||||||
<Field
|
|
||||||
className={classes.name}
|
|
||||||
name={getOwnerNameBy(index)}
|
|
||||||
component={TextField}
|
|
||||||
type="text"
|
|
||||||
validate={required}
|
|
||||||
placeholder="Owner Name*"
|
|
||||||
text="Owner Name"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col xs={7}>
|
|
||||||
<Field
|
|
||||||
name={addressName}
|
|
||||||
component={TextField}
|
|
||||||
inputAdornment={
|
|
||||||
noErrorsOn(addressName, errors) && {
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<CheckCircle className={classes.check} />
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type="text"
|
|
||||||
validate={getAddressValidators(otherAccounts, index)}
|
|
||||||
placeholder="Owner Address*"
|
|
||||||
text="Owner Address"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col xs={1} center="xs" middle="xs" className={classes.remove}>
|
|
||||||
{index > 0 && <Img src={trash} height={20} alt="Delete" onClick={this.onRemoveRow(index)} />}
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Block>
|
|
||||||
<Row align="center" grow className={classes.add} margin="xl">
|
|
||||||
<Button color="secondary" onClick={this.onAddOwner} data-testid="add-owner-btn">
|
|
||||||
<Paragraph weight="bold" size="md" noMargin>
|
|
||||||
{ADD_OWNER_BUTTON}
|
|
||||||
</Paragraph>
|
|
||||||
</Button>
|
|
||||||
</Row>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SafeOwnersForm = withStyles(styles)(SafeOwners)
|
|
||||||
|
|
||||||
const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors }: Object) => (
|
|
||||||
<React.Fragment>
|
|
||||||
<OpenPaper controls={controls} padding={false}>
|
|
||||||
<SafeOwnersForm
|
|
||||||
otherAccounts={getAccountsFrom(values)}
|
|
||||||
errors={errors}
|
|
||||||
updateInitialProps={updateInitialProps}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
</OpenPaper>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default SafeOwnersPage
|
|
|
@ -1,91 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react'
|
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem'
|
|
||||||
import Field from '~/components/forms/Field'
|
|
||||||
import SelectField from '~/components/forms/SelectField'
|
|
||||||
import {
|
|
||||||
composeValidators, minValue, mustBeInteger, required,
|
|
||||||
} from '~/components/forms/validator'
|
|
||||||
import Block from '~/components/layout/Block'
|
|
||||||
import Row from '~/components/layout/Row'
|
|
||||||
import Col from '~/components/layout/Col'
|
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
|
||||||
import { FIELD_CONFIRMATIONS, getNumOwnersFrom } from '~/routes/open/components/fields'
|
|
||||||
import { md } from '~/theme/variables'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
classes: Object,
|
|
||||||
values: Object,
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = () => ({
|
|
||||||
owners: {
|
|
||||||
paddingLeft: md,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const CONFIRMATIONS_ERROR = 'Number of confirmations can not be higher than the number of owners'
|
|
||||||
|
|
||||||
export const safeFieldsValidation = (values: Object) => {
|
|
||||||
const errors = {}
|
|
||||||
|
|
||||||
const numOwners = getNumOwnersFrom(values)
|
|
||||||
if (numOwners < Number.parseInt(values[FIELD_CONFIRMATIONS], 10)) {
|
|
||||||
errors[FIELD_CONFIRMATIONS] = CONFIRMATIONS_ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const SafeThreshold = ({ classes, values }: Props) => {
|
|
||||||
const numOwners = getNumOwnersFrom(values)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Block margin="xs">
|
|
||||||
<Paragraph noMargin size="md" color="primary" weight="bolder">
|
|
||||||
Any transaction requires the confirmation of:
|
|
||||||
</Paragraph>
|
|
||||||
</Block>
|
|
||||||
<Row margin="xl" align="center">
|
|
||||||
<Col xs={2}>
|
|
||||||
<Field
|
|
||||||
name={FIELD_CONFIRMATIONS}
|
|
||||||
component={SelectField}
|
|
||||||
validate={composeValidators(required, mustBeInteger, minValue(1))}
|
|
||||||
data-testid="threshold-select-input"
|
|
||||||
>
|
|
||||||
{[...Array(Number(numOwners))].map((x, index) => (
|
|
||||||
<MenuItem key={`selectOwner${index}`} value={`${index + 1}`}>
|
|
||||||
{index + 1}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Field>
|
|
||||||
</Col>
|
|
||||||
<Col xs={10}>
|
|
||||||
<Paragraph size="lg" color="primary" noMargin className={classes.owners}>
|
|
||||||
out of
|
|
||||||
{' '}
|
|
||||||
{numOwners}
|
|
||||||
{' '}
|
|
||||||
owner(s)
|
|
||||||
</Paragraph>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SafeThresholdForm = withStyles(styles)(SafeThreshold)
|
|
||||||
|
|
||||||
const SafeOwnersPage = () => (controls: React.Node, { values }: Object) => (
|
|
||||||
<React.Fragment>
|
|
||||||
<OpenPaper controls={controls} container={450}>
|
|
||||||
<SafeThresholdForm values={values} />
|
|
||||||
</OpenPaper>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default SafeOwnersPage
|
|
|
@ -9,7 +9,7 @@ export const getOwnerAddressBy = (index: number) => `owner${index}Address`
|
||||||
export const getNumOwnersFrom = (values: Object) => {
|
export const getNumOwnersFrom = (values: Object) => {
|
||||||
const accounts = Object.keys(values)
|
const accounts = Object.keys(values)
|
||||||
.sort()
|
.sort()
|
||||||
.filter(key => /^owner\d+Name$/.test(key))
|
.filter(key => /^owner\d+Address$/.test(key) && !!values[key])
|
||||||
|
|
||||||
return accounts.length
|
return accounts.length
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { withStyles } from '@material-ui/core/styles'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
import Link from '~/components/layout/Link'
|
|
||||||
import QRCode from 'qrcode.react'
|
import QRCode from 'qrcode.react'
|
||||||
|
import Link from '~/components/layout/Link'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Identicon from '~/components/Identicon'
|
import Identicon from '~/components/Identicon'
|
||||||
import Button from '~/components/layout/Button'
|
import Button from '~/components/layout/Button'
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react'
|
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
|
||||||
import Block from '~/components/layout/Block'
|
|
||||||
import Bold from '~/components/layout/Bold'
|
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
|
||||||
import Heading from '~/components/layout/Heading'
|
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
|
||||||
import { TKN_DESTINATION_PARAM, TKN_VALUE_PARAM } from '~/routes/safe/components/SendToken/SendTokenForm/index'
|
|
||||||
|
|
||||||
type FormProps = {
|
|
||||||
values: Object,
|
|
||||||
submitting: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
symbol: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
const spinnerStyle = {
|
|
||||||
minHeight: '50px',
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReviewTx = ({ symbol }: Props) => (controls: React.Node, { values, submitting }: FormProps) => (
|
|
||||||
<OpenPaper controls={controls}>
|
|
||||||
<Heading tag="h2">Review the move token funds</Heading>
|
|
||||||
<Paragraph align="left">
|
|
||||||
<Bold>Destination: </Bold>
|
|
||||||
{' '}
|
|
||||||
{values[TKN_DESTINATION_PARAM]}
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph align="left">
|
|
||||||
<Bold>{`Amount to transfer: ${values[TKN_VALUE_PARAM]} ${symbol}`}</Bold>
|
|
||||||
</Paragraph>
|
|
||||||
<Block style={spinnerStyle}>{submitting && <CircularProgress size={50} />}</Block>
|
|
||||||
</OpenPaper>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ReviewTx
|
|
|
@ -1,53 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react'
|
|
||||||
import Field from '~/components/forms/Field'
|
|
||||||
import TextField from '~/components/forms/TextField'
|
|
||||||
import {
|
|
||||||
composeValidators, inLimit, mustBeFloat, required, greaterThan, mustBeEthereumAddress,
|
|
||||||
} from '~/components/forms/validator'
|
|
||||||
import Block from '~/components/layout/Block'
|
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
|
||||||
import Heading from '~/components/layout/Heading'
|
|
||||||
|
|
||||||
export const CONFIRMATIONS_ERROR = 'Number of confirmations can not be higher than the number of owners'
|
|
||||||
|
|
||||||
export const TKN_DESTINATION_PARAM = 'tknDestination'
|
|
||||||
export const TKN_VALUE_PARAM = 'tknValue'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
funds: string,
|
|
||||||
symbol: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
const SendTokenForm = ({ funds, symbol }: Props) => (controls: React.Node) => (
|
|
||||||
<OpenPaper controls={controls}>
|
|
||||||
<Heading tag="h2" margin="lg">
|
|
||||||
Send tokens Transaction
|
|
||||||
</Heading>
|
|
||||||
<Heading tag="h4" margin="lg">
|
|
||||||
{`Available tokens: ${funds} ${symbol}`}
|
|
||||||
</Heading>
|
|
||||||
<Block margin="md">
|
|
||||||
<Field
|
|
||||||
name={TKN_DESTINATION_PARAM}
|
|
||||||
component={TextField}
|
|
||||||
type="text"
|
|
||||||
validate={composeValidators(required, mustBeEthereumAddress)}
|
|
||||||
placeholder="Destination*"
|
|
||||||
text="Destination"
|
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
<Block margin="md">
|
|
||||||
<Field
|
|
||||||
name={TKN_VALUE_PARAM}
|
|
||||||
component={TextField}
|
|
||||||
type="text"
|
|
||||||
validate={composeValidators(required, mustBeFloat, greaterThan(0), inLimit(Number(funds), 0, 'available balance', symbol))}
|
|
||||||
placeholder="Amount of tokens*"
|
|
||||||
text="Amount of Tokens"
|
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
</OpenPaper>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default SendTokenForm
|
|
|
@ -1,10 +0,0 @@
|
||||||
// @flow
|
|
||||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
|
||||||
|
|
||||||
export type Actions = {
|
|
||||||
fetchTransactions: typeof fetchTransactions,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
fetchTransactions,
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react'
|
|
||||||
import { BigNumber } from 'bignumber.js'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import Stepper from '~/components/Stepper'
|
|
||||||
import { sleep } from '~/utils/timer'
|
|
||||||
import { type Safe } from '~/routes/safe/store/models/safe'
|
|
||||||
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
|
||||||
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
|
||||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
|
||||||
import { toNative } from '~/logic/wallets/tokens'
|
|
||||||
import { createTransaction } from '~/logic/safe/safeFrontendOperations'
|
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
|
||||||
import actions, { type Actions } from './actions'
|
|
||||||
import selector, { type SelectorProps } from './selector'
|
|
||||||
import SendTokenForm, { TKN_DESTINATION_PARAM, TKN_VALUE_PARAM } from './SendTokenForm'
|
|
||||||
import ReviewTx from './ReviewTx'
|
|
||||||
|
|
||||||
const getSteps = () => ['Fill Move Token form', 'Review Move Token form']
|
|
||||||
|
|
||||||
type Props = SelectorProps &
|
|
||||||
Actions & {
|
|
||||||
safe: Safe,
|
|
||||||
token: Token,
|
|
||||||
onReset: () => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
done: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SEE_TXS_BUTTON_TEXT = 'VISIT TXS'
|
|
||||||
|
|
||||||
const getTransferData = async (tokenAddress: string, to: string, amount: BigNumber) => {
|
|
||||||
const StandardToken = await getStandardTokenContract()
|
|
||||||
const myToken = await StandardToken.at(tokenAddress)
|
|
||||||
|
|
||||||
return myToken.contract.transfer(to, amount).encodeABI()
|
|
||||||
}
|
|
||||||
|
|
||||||
const processTokenTransfer = async (safe: Safe, token: Token, to: string, amount: string, userAddress: string) => {
|
|
||||||
const safeAddress = safe.get('address')
|
|
||||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
|
||||||
const nonce = await gnosisSafe.nonce()
|
|
||||||
const symbol = token.get('symbol')
|
|
||||||
const name = `Send ${amount} ${symbol} to ${to}`
|
|
||||||
const value = isEther(symbol) ? amount : '0'
|
|
||||||
const tokenAddress = token.get('address')
|
|
||||||
const destination = isEther(symbol) ? to : tokenAddress
|
|
||||||
const data = isEther(symbol)
|
|
||||||
? EMPTY_DATA
|
|
||||||
: await getTransferData(tokenAddress, to, toNative(amount, token.get('decimals')))
|
|
||||||
|
|
||||||
return createTransaction(safe, name, destination, value, nonce, userAddress, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SendToken extends React.Component<Props, State> {
|
|
||||||
state = {
|
|
||||||
done: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
onTransaction = async (values: Object) => {
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
safe, token, userAddress, fetchTransactions,
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
const amount = values[TKN_VALUE_PARAM]
|
|
||||||
const destination = values[TKN_DESTINATION_PARAM]
|
|
||||||
|
|
||||||
await processTokenTransfer(safe, token, destination, amount, userAddress)
|
|
||||||
await sleep(1500)
|
|
||||||
fetchTransactions(safe.get('address'))
|
|
||||||
this.setState({ done: true })
|
|
||||||
} catch (error) {
|
|
||||||
this.setState({ done: false })
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.log('Error while moving ERC20 token funds ' + error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onReset = () => {
|
|
||||||
const { onReset } = this.props
|
|
||||||
|
|
||||||
this.setState({ done: false })
|
|
||||||
onReset() // This is for show the TX list component
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { done } = this.state
|
|
||||||
const { token } = this.props
|
|
||||||
const steps = getSteps()
|
|
||||||
const finishedButton = <Stepper.FinishButton title={SEE_TXS_BUTTON_TEXT} />
|
|
||||||
const symbol = token.get('symbol')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Stepper
|
|
||||||
finishedTransaction={done}
|
|
||||||
finishedButton={finishedButton}
|
|
||||||
onSubmit={this.onTransaction}
|
|
||||||
steps={steps}
|
|
||||||
onReset={this.onReset}
|
|
||||||
>
|
|
||||||
<Stepper.Page funds={token.get('funds')} symbol={symbol}>
|
|
||||||
{SendTokenForm}
|
|
||||||
</Stepper.Page>
|
|
||||||
<Stepper.Page symbol={symbol}>{ReviewTx}</Stepper.Page>
|
|
||||||
</Stepper>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
selector,
|
|
||||||
actions,
|
|
||||||
)(SendToken)
|
|
|
@ -1,11 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { createStructuredSelector } from 'reselect'
|
|
||||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
|
||||||
|
|
||||||
export type SelectorProps = {
|
|
||||||
userAddress: typeof userAccountSelector,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createStructuredSelector<Object, *>({
|
|
||||||
userAddress: userAccountSelector,
|
|
||||||
})
|
|
|
@ -1,34 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react'
|
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
|
||||||
import Block from '~/components/layout/Block'
|
|
||||||
import Bold from '~/components/layout/Bold'
|
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
|
||||||
import Heading from '~/components/layout/Heading'
|
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
|
||||||
import { THRESHOLD_PARAM } from '~/routes/safe/components/Threshold/ThresholdForm'
|
|
||||||
|
|
||||||
type FormProps = {
|
|
||||||
values: Object,
|
|
||||||
submitting: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
const spinnerStyle = {
|
|
||||||
minHeight: '50px',
|
|
||||||
}
|
|
||||||
|
|
||||||
const Review = () => (controls: React.Node, { values, submitting }: FormProps) => (
|
|
||||||
<OpenPaper controls={controls}>
|
|
||||||
<Heading tag="h2">Review the Threshold operation</Heading>
|
|
||||||
<Paragraph align="left">
|
|
||||||
<Bold>The new threshold will be: </Bold>
|
|
||||||
{' '}
|
|
||||||
{values[THRESHOLD_PARAM]}
|
|
||||||
</Paragraph>
|
|
||||||
<Block style={spinnerStyle}>
|
|
||||||
{ submitting && <CircularProgress size={50} /> }
|
|
||||||
</Block>
|
|
||||||
</OpenPaper>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default Review
|
|
|
@ -1,46 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react'
|
|
||||||
import Block from '~/components/layout/Block'
|
|
||||||
import Heading from '~/components/layout/Heading'
|
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
|
||||||
import Field from '~/components/forms/Field'
|
|
||||||
import TextField from '~/components/forms/TextField'
|
|
||||||
import {
|
|
||||||
composeValidators, minValue, maxValue, mustBeInteger, required,
|
|
||||||
} from '~/components/forms/validator'
|
|
||||||
import { type Safe } from '~/routes/safe/store/models/safe'
|
|
||||||
|
|
||||||
export const THRESHOLD_PARAM = 'threshold'
|
|
||||||
|
|
||||||
type ThresholdProps = {
|
|
||||||
numOwners: number,
|
|
||||||
safe: Safe,
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThresholdForm = ({ numOwners, safe }: ThresholdProps) => (controls: React.Node) => (
|
|
||||||
<OpenPaper controls={controls}>
|
|
||||||
<Heading tag="h2" margin="lg">
|
|
||||||
{'Change safe\'s threshold'}
|
|
||||||
</Heading>
|
|
||||||
<Heading tag="h4" margin="lg">
|
|
||||||
{`Safe's owners: ${numOwners} and Safe's threshold: ${safe.get('threshold')}`}
|
|
||||||
</Heading>
|
|
||||||
<Block margin="md">
|
|
||||||
<Field
|
|
||||||
name={THRESHOLD_PARAM}
|
|
||||||
component={TextField}
|
|
||||||
type="text"
|
|
||||||
validate={composeValidators(
|
|
||||||
required,
|
|
||||||
mustBeInteger,
|
|
||||||
minValue(1),
|
|
||||||
maxValue(numOwners),
|
|
||||||
)}
|
|
||||||
placeholder="New threshold"
|
|
||||||
text="Safe's threshold"
|
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
</OpenPaper>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ThresholdForm
|
|
|
@ -1,10 +0,0 @@
|
||||||
// @flow
|
|
||||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
|
||||||
|
|
||||||
export type Actions = {
|
|
||||||
fetchTransactions: typeof fetchTransactions,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
fetchTransactions,
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react'
|
|
||||||
import Stepper from '~/components/Stepper'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import { createTransaction } from '~/logic/safe/safeFrontendOperations'
|
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
|
||||||
import { type Safe } from '~/routes/safe/store/models/safe'
|
|
||||||
import ThresholdForm, { THRESHOLD_PARAM } from './ThresholdForm'
|
|
||||||
import selector, { type SelectorProps } from './selector'
|
|
||||||
import actions, { type Actions } from './actions'
|
|
||||||
import Review from './Review'
|
|
||||||
|
|
||||||
type Props = SelectorProps & Actions & {
|
|
||||||
numOwners: number,
|
|
||||||
safe: Safe,
|
|
||||||
onReset: () => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSteps = () => [
|
|
||||||
'Fill Change threshold Form', 'Review change threshold operation',
|
|
||||||
]
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
done: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CHANGE_THRESHOLD_RESET_BUTTON_TEXT = 'SEE TXs'
|
|
||||||
|
|
||||||
class Threshold extends React.PureComponent<Props, State> {
|
|
||||||
state = {
|
|
||||||
done: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
onThreshold = async (values: Object) => {
|
|
||||||
try {
|
|
||||||
const { safe, userAddress, fetchTransactions } = this.props // , fetchThreshold } = this.props
|
|
||||||
const newThreshold = values[THRESHOLD_PARAM]
|
|
||||||
const safeAddress = safe.get('address')
|
|
||||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
|
||||||
const nonce = await gnosisSafe.nonce()
|
|
||||||
const data = gnosisSafe.contract.changeThreshold(newThreshold).encodeABI()
|
|
||||||
await createTransaction(safe, `Change Safe's threshold [${nonce}]`, safeAddress, '0', nonce, userAddress, data)
|
|
||||||
await fetchTransactions(safeAddress)
|
|
||||||
this.setState({ done: true })
|
|
||||||
} catch (error) {
|
|
||||||
this.setState({ done: false })
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.log('Error while changing threshold ' + error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onReset = () => {
|
|
||||||
const { onReset } = this.props
|
|
||||||
this.setState({ done: false })
|
|
||||||
|
|
||||||
onReset()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { numOwners, safe } = this.props
|
|
||||||
const { done } = this.state
|
|
||||||
const steps = getSteps()
|
|
||||||
const finishedButton = <Stepper.FinishButton title={CHANGE_THRESHOLD_RESET_BUTTON_TEXT} />
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Stepper
|
|
||||||
finishedTransaction={done}
|
|
||||||
finishedButton={finishedButton}
|
|
||||||
onSubmit={this.onThreshold}
|
|
||||||
steps={steps}
|
|
||||||
onReset={this.onReset}
|
|
||||||
>
|
|
||||||
<Stepper.Page numOwners={numOwners} safe={safe}>
|
|
||||||
{ ThresholdForm }
|
|
||||||
</Stepper.Page>
|
|
||||||
<Stepper.Page>
|
|
||||||
{ Review }
|
|
||||||
</Stepper.Page>
|
|
||||||
</Stepper>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(selector, actions)(Threshold)
|
|
|
@ -1,11 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { createStructuredSelector } from 'reselect'
|
|
||||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
|
||||||
|
|
||||||
export type SelectorProps = {
|
|
||||||
userAddress: typeof userAccountSelector,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createStructuredSelector<Object, *>({
|
|
||||||
userAddress: userAccountSelector,
|
|
||||||
})
|
|
|
@ -2,9 +2,6 @@
|
||||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
|
||||||
const web3 = getWeb3()
|
|
||||||
const { toBN, fromWei } = web3.utils
|
|
||||||
|
|
||||||
type DecodedTxData = {
|
type DecodedTxData = {
|
||||||
recipient: string,
|
recipient: string,
|
||||||
value?: string,
|
value?: string,
|
||||||
|
@ -16,6 +13,9 @@ type DecodedTxData = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTxData = (tx: Transaction): DecodedTxData => {
|
export const getTxData = (tx: Transaction): DecodedTxData => {
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const { toBN, fromWei } = web3.utils
|
||||||
|
|
||||||
const txData = {}
|
const txData = {}
|
||||||
|
|
||||||
if (tx.isTokenTransfer && tx.decodedParams) {
|
if (tx.isTokenTransfer && tx.decodedParams) {
|
||||||
|
|
|
@ -14,9 +14,6 @@ export const TX_TABLE_STATUS_ID = 'status'
|
||||||
export const TX_TABLE_RAW_TX_ID = 'tx'
|
export const TX_TABLE_RAW_TX_ID = 'tx'
|
||||||
export const TX_TABLE_EXPAND_ICON = 'expand'
|
export const TX_TABLE_EXPAND_ICON = 'expand'
|
||||||
|
|
||||||
const web3 = getWeb3()
|
|
||||||
const { toBN, fromWei } = web3.utils
|
|
||||||
|
|
||||||
type TxData = {
|
type TxData = {
|
||||||
nonce: number,
|
nonce: number,
|
||||||
type: string,
|
type: string,
|
||||||
|
@ -29,6 +26,9 @@ type TxData = {
|
||||||
export const formatDate = (date: Date): string => format(date, 'MMM D, YYYY - HH:mm:ss')
|
export const formatDate = (date: Date): string => format(date, 'MMM D, YYYY - HH:mm:ss')
|
||||||
|
|
||||||
export const getTxAmount = (tx: Transaction) => {
|
export const getTxAmount = (tx: Transaction) => {
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const { toBN, fromWei } = web3.utils
|
||||||
|
|
||||||
let txAmount = 'n/a'
|
let txAmount = 'n/a'
|
||||||
|
|
||||||
if (tx.isTokenTransfer && tx.decodedParams) {
|
if (tx.isTokenTransfer && tx.decodedParams) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { type Store } from 'redux'
|
||||||
import { render, fireEvent, cleanup } from '@testing-library/react'
|
import { render, fireEvent, cleanup } from '@testing-library/react'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { ConnectedRouter } from 'connected-react-router'
|
import { ConnectedRouter } from 'connected-react-router'
|
||||||
import { ADD_OWNER_BUTTON } from '~/routes/open/components/SafeOwnersForm'
|
import { ADD_OWNER_BUTTON } from '~/routes/open/components/SafeOwnersConfirmationsForm'
|
||||||
import Open from '~/routes/open/container/Open'
|
import Open from '~/routes/open/container/Open'
|
||||||
import { aNewStore, history, type GlobalState } from '~/store'
|
import { aNewStore, history, type GlobalState } from '~/store'
|
||||||
import { sleep } from '~/utils/timer'
|
import { sleep } from '~/utils/timer'
|
||||||
|
@ -80,14 +80,12 @@ const deploySafe = async (createSafeForm: any, threshold: number, numOwners: num
|
||||||
fireEvent.change(ownerNameInput, { target: { value: `Owner ${i + 1}` } })
|
fireEvent.change(ownerNameInput, { target: { value: `Owner ${i + 1}` } })
|
||||||
fireEvent.change(ownerAddressInput, { target: { value: accounts[i] } })
|
fireEvent.change(ownerAddressInput, { target: { value: accounts[i] } })
|
||||||
}
|
}
|
||||||
fireEvent.submit(form)
|
|
||||||
await sleep(600)
|
|
||||||
|
|
||||||
// Fill Threshold
|
// Fill Threshold
|
||||||
// The test is fragile here, MUI select btn is hard to find
|
// The test is fragile here, MUI select btn is hard to find
|
||||||
const thresholdSelect = createSafeForm.getAllByRole('button')[1]
|
const thresholdSelect = createSafeForm.getAllByRole('button')[2]
|
||||||
|
|
||||||
fireEvent.click(thresholdSelect)
|
fireEvent.click(thresholdSelect)
|
||||||
|
|
||||||
const thresholdOptions = createSafeForm.getAllByRole('option')
|
const thresholdOptions = createSafeForm.getAllByRole('option')
|
||||||
fireEvent.click(thresholdOptions[numOwners - 1])
|
fireEvent.click(thresholdOptions[numOwners - 1])
|
||||||
fireEvent.submit(form)
|
fireEvent.submit(form)
|
||||||
|
|
44
yarn.lock
44
yarn.lock
|
@ -10658,6 +10658,11 @@ jsprim@^1.2.2:
|
||||||
json-schema "0.2.3"
|
json-schema "0.2.3"
|
||||||
verror "1.10.0"
|
verror "1.10.0"
|
||||||
|
|
||||||
|
jsqr@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsqr/-/jsqr-1.2.0.tgz#f93fc65fa7d1ded78b1bcb020fa044352b04261a"
|
||||||
|
integrity sha512-wKcQS9QC2VHGk7aphWCp1RrFyC0CM6fMgC5prZZ2KV/Lk6OKNoCod9IR6bao+yx3KPY0gZFC5dc+h+KFzCI0Wg==
|
||||||
|
|
||||||
jss-plugin-camel-case@10.0.0-alpha.17:
|
jss-plugin-camel-case@10.0.0-alpha.17:
|
||||||
version "10.0.0-alpha.17"
|
version "10.0.0-alpha.17"
|
||||||
resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.0.0-alpha.17.tgz#6f7c9d9742e349bb061e53cd9b1c3cb006169a67"
|
resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.0.0-alpha.17.tgz#6f7c9d9742e349bb061e53cd9b1c3cb006169a67"
|
||||||
|
@ -13161,7 +13166,7 @@ postcss-minify-selectors@^4.0.2:
|
||||||
postcss "^7.0.0"
|
postcss "^7.0.0"
|
||||||
postcss-selector-parser "^3.0.0"
|
postcss-selector-parser "^3.0.0"
|
||||||
|
|
||||||
postcss-mixins@^6.2.0:
|
postcss-mixins@6.2.2:
|
||||||
version "6.2.2"
|
version "6.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.2.2.tgz#3acea63271e2c75db62fb80bc1c29e1a609a4742"
|
resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.2.2.tgz#3acea63271e2c75db62fb80bc1c29e1a609a4742"
|
||||||
integrity sha512-QqEZamiAMguYR6d2h73XXEHZgkxs03PlbU0PqgqtdCnbRlMLFNQgsfL/Td0rjIe2SwpLXOQyB9uoiLWa4GR7tg==
|
integrity sha512-QqEZamiAMguYR6d2h73XXEHZgkxs03PlbU0PqgqtdCnbRlMLFNQgsfL/Td0rjIe2SwpLXOQyB9uoiLWa4GR7tg==
|
||||||
|
@ -14109,6 +14114,15 @@ react-popper@^1.3.3:
|
||||||
typed-styles "^0.0.7"
|
typed-styles "^0.0.7"
|
||||||
warning "^4.0.2"
|
warning "^4.0.2"
|
||||||
|
|
||||||
|
react-qr-reader@^2.2.1:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-2.2.1.tgz#dc89046d1c1a1da837a683dd970de5926817d55b"
|
||||||
|
integrity sha512-EL5JEj53u2yAOgtpAKAVBzD/SiKWn0Bl7AZy6ZrSf1lub7xHwtaXe6XSx36Wbhl1VMGmvmrwYMRwO1aSCT2fwA==
|
||||||
|
dependencies:
|
||||||
|
jsqr "^1.2.0"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
webrtc-adapter "^7.2.1"
|
||||||
|
|
||||||
react-redux@7.1.0:
|
react-redux@7.1.0:
|
||||||
version "7.1.0"
|
version "7.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2"
|
||||||
|
@ -14980,6 +14994,13 @@ rsvp@^4.8.4:
|
||||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
||||||
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
|
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
|
||||||
|
|
||||||
|
rtcpeerconnection-shim@^1.2.15:
|
||||||
|
version "1.2.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz#e7cc189a81b435324c4949aa3dfb51888684b243"
|
||||||
|
integrity sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==
|
||||||
|
dependencies:
|
||||||
|
sdp "^2.6.0"
|
||||||
|
|
||||||
run-async@^2.2.0:
|
run-async@^2.2.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
|
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
|
||||||
|
@ -15172,6 +15193,11 @@ scryptsy@^1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
pbkdf2 "^3.0.3"
|
pbkdf2 "^3.0.3"
|
||||||
|
|
||||||
|
sdp@^2.6.0, sdp@^2.9.0:
|
||||||
|
version "2.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.9.0.tgz#2eed2d9c0b26c81ff87593107895c68d6fb9a0a6"
|
||||||
|
integrity sha512-XAVZQO4qsfzVTHorF49zCpkdxiGmPNjA8ps8RcJGtGP3QJ/A8I9/SVg/QnkAFDMXIyGbHZBBFwYBw6WdnhT96w==
|
||||||
|
|
||||||
seamless-immutable@^7.1.3:
|
seamless-immutable@^7.1.3:
|
||||||
version "7.1.4"
|
version "7.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8"
|
resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8"
|
||||||
|
@ -18958,6 +18984,14 @@ webpack@^4.33.0:
|
||||||
watchpack "^1.5.0"
|
watchpack "^1.5.0"
|
||||||
webpack-sources "^1.3.0"
|
webpack-sources "^1.3.0"
|
||||||
|
|
||||||
|
webrtc-adapter@^7.2.1:
|
||||||
|
version "7.2.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-7.2.8.tgz#1373fa874559c655aa713830c2836511588d77ab"
|
||||||
|
integrity sha512-d/rZVIIqqPqu/1I9rabhI+hmVhNtT+MoJk0eipCJasiVM9L9ZOTBoVhZmtC/naB4G8GTvnCaassrDz5IqWZP6w==
|
||||||
|
dependencies:
|
||||||
|
rtcpeerconnection-shim "^1.2.15"
|
||||||
|
sdp "^2.9.0"
|
||||||
|
|
||||||
websocket-driver@>=0.5.1:
|
websocket-driver@>=0.5.1:
|
||||||
version "0.7.3"
|
version "0.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9"
|
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9"
|
||||||
|
@ -18972,9 +19006,9 @@ websocket-extensions@>=0.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
|
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
|
||||||
integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
|
integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
|
||||||
|
|
||||||
websocket@1.0.26, "websocket@github:frozeman/WebSocket-Node#browserifyCompatible":
|
websocket@1.0.26, "websocket@git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible":
|
||||||
version "1.0.26"
|
version "1.0.26"
|
||||||
resolved "https://codeload.github.com/frozeman/WebSocket-Node/tar.gz/6c72925e3f8aaaea8dc8450f97627e85263999f2"
|
resolved "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2"
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^2.2.0"
|
debug "^2.2.0"
|
||||||
nan "^2.3.3"
|
nan "^2.3.3"
|
||||||
|
@ -18992,9 +19026,9 @@ websocket@^1.0.28:
|
||||||
typedarray-to-buffer "^3.1.5"
|
typedarray-to-buffer "^3.1.5"
|
||||||
yaeti "^0.0.6"
|
yaeti "^0.0.6"
|
||||||
|
|
||||||
"websocket@git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible":
|
"websocket@github:frozeman/WebSocket-Node#browserifyCompatible":
|
||||||
version "1.0.26"
|
version "1.0.26"
|
||||||
resolved "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2"
|
resolved "https://codeload.github.com/frozeman/WebSocket-Node/tar.gz/6c72925e3f8aaaea8dc8450f97627e85263999f2"
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^2.2.0"
|
debug "^2.2.0"
|
||||||
nan "^2.3.3"
|
nan "^2.3.3"
|
||||||
|
|
Loading…
Reference in New Issue