Merge pull request #187 from gnosis/135-notifications

Feature #135: Notifications handling
This commit is contained in:
Germán Martínez 2019-09-30 07:08:43 +02:00 committed by GitHub
commit 28bbf574c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1776 additions and 1202 deletions

View File

@ -45,6 +45,7 @@
"immortal-db": "^1.0.2", "immortal-db": "^1.0.2",
"immutable": "^4.0.0-rc.9", "immutable": "^4.0.0-rc.9",
"material-ui-search-bar": "^1.0.0-beta.13", "material-ui-search-bar": "^1.0.0-beta.13",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"optimize-css-assets-webpack-plugin": "5.0.3", "optimize-css-assets-webpack-plugin": "5.0.3",
"qrcode.react": "^0.9.3", "qrcode.react": "^0.9.3",
"react": "16.9.0", "react": "16.9.0",
@ -55,11 +56,11 @@
"react-infinite-scroll-component": "4.5.3", "react-infinite-scroll-component": "4.5.3",
"react-qr-reader": "^2.2.1", "react-qr-reader": "^2.2.1",
"react-redux": "7.1.1", "react-redux": "7.1.1",
"react-router-dom": "5.1.0", "react-router-dom": "^5.1.0",
"recompose": "^0.30.0", "recompose": "^0.30.0",
"redux": "4.0.4", "redux": "4.0.4",
"redux-actions": "^2.3.0", "redux-actions": "^2.6.5",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.3.0",
"reselect": "^4.0.0", "reselect": "^4.0.0",
"web3": "1.2.1" "web3": "1.2.1"
}, },
@ -71,24 +72,24 @@
"@babel/plugin-proposal-do-expressions": "7.6.0", "@babel/plugin-proposal-do-expressions": "7.6.0",
"@babel/plugin-proposal-export-default-from": "7.5.2", "@babel/plugin-proposal-export-default-from": "7.5.2",
"@babel/plugin-proposal-export-namespace-from": "7.5.2", "@babel/plugin-proposal-export-namespace-from": "7.5.2",
"@babel/plugin-proposal-function-bind": "^7.0.0", "@babel/plugin-proposal-function-bind": "^7.2.0",
"@babel/plugin-proposal-function-sent": "7.5.0", "@babel/plugin-proposal-function-sent": "7.5.0",
"@babel/plugin-proposal-json-strings": "^7.0.0", "@babel/plugin-proposal-json-strings": "^7.2.0",
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", "@babel/plugin-proposal-logical-assignment-operators": "^7.2.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4", "@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
"@babel/plugin-proposal-numeric-separator": "^7.0.0", "@babel/plugin-proposal-numeric-separator": "^7.2.0",
"@babel/plugin-proposal-optional-chaining": "7.6.0", "@babel/plugin-proposal-optional-chaining": "7.6.0",
"@babel/plugin-proposal-pipeline-operator": "7.5.0", "@babel/plugin-proposal-pipeline-operator": "7.5.0",
"@babel/plugin-proposal-throw-expressions": "^7.0.0", "@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.0.0", "@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/plugin-transform-member-expression-literals": "^7.2.0", "@babel/plugin-transform-member-expression-literals": "^7.2.0",
"@babel/plugin-transform-property-literals": "^7.2.0", "@babel/plugin-transform-property-literals": "^7.2.0",
"@babel/polyfill": "7.6.0", "@babel/polyfill": "7.6.0",
"@babel/preset-env": "7.6.2", "@babel/preset-env": "7.6.2",
"@babel/preset-flow": "^7.0.0-beta.40", "@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0-beta.40", "@babel/preset-react": "^7.0.0",
"@sambego/storybook-state": "^1.0.7", "@sambego/storybook-state": "^1.3.6",
"@storybook/addon-actions": "5.2.1", "@storybook/addon-actions": "5.2.1",
"@storybook/addon-knobs": "5.2.1", "@storybook/addon-knobs": "5.2.1",
"@storybook/addon-links": "5.2.1", "@storybook/addon-links": "5.2.1",
@ -99,13 +100,13 @@
"babel-eslint": "10.0.3", "babel-eslint": "10.0.3",
"babel-jest": "24.9.0", "babel-jest": "24.9.0",
"babel-loader": "8.0.6", "babel-loader": "8.0.6",
"babel-plugin-dynamic-import-node": "^2.2.0", "babel-plugin-dynamic-import-node": "^2.3.0",
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0", "babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
"babel-plugin-transform-es3-property-literals": "^6.22.0", "babel-plugin-transform-es3-property-literals": "^6.22.0",
"classnames": "^2.2.5", "classnames": "^2.2.6",
"css-loader": "3.2.0", "css-loader": "3.2.0",
"detect-port": "^1.2.2", "detect-port": "^1.3.0",
"eslint": "5.16.0", "eslint": "6.4.0",
"eslint-config-airbnb": "18.0.1", "eslint-config-airbnb": "18.0.1",
"eslint-plugin-flowtype": "4.3.0", "eslint-plugin-flowtype": "4.3.0",
"eslint-plugin-import": "2.18.2", "eslint-plugin-import": "2.18.2",
@ -118,7 +119,7 @@
"flow-bin": "0.108.0", "flow-bin": "0.108.0",
"fs-extra": "8.1.0", "fs-extra": "8.1.0",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",
"html-webpack-plugin": "^3.0.4", "html-webpack-plugin": "^3.2.0",
"jest": "24.9.0", "jest": "24.9.0",
"jest-dom": "4.0.0", "jest-dom": "4.0.0",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
@ -130,7 +131,7 @@
"prettier-eslint-cli": "5.0.0", "prettier-eslint-cli": "5.0.0",
"run-with-testrpc": "0.3.1", "run-with-testrpc": "0.3.1",
"storybook-host": "5.1.0", "storybook-host": "5.1.0",
"storybook-router": "^0.3.3", "storybook-router": "^0.3.4",
"style-loader": "1.0.0", "style-loader": "1.0.0",
"truffle": "5.0.37", "truffle": "5.0.37",
"truffle-contract": "4.0.31", "truffle-contract": "4.0.31",
@ -141,6 +142,6 @@
"webpack-bundle-analyzer": "3.5.1", "webpack-bundle-analyzer": "3.5.1",
"webpack-cli": "3.3.9", "webpack-cli": "3.3.9",
"webpack-dev-server": "3.8.1", "webpack-dev-server": "3.8.1",
"webpack-manifest-plugin": "2.1.1" "webpack-manifest-plugin": "^2.1.1"
} }
} }

View File

@ -32,7 +32,8 @@ const styles = () => ({
padding: 0, padding: 0,
boxShadow: '0 0 10px 0 rgba(33, 48, 77, 0.1)', boxShadow: '0 0 10px 0 rgba(33, 48, 77, 0.1)',
minWidth: '280px', minWidth: '280px',
left: '4px', borderRadius: '8px',
marginTop: '11px',
}, },
summary: { summary: {
borderBottom: `solid 2px ${border}`, borderBottom: `solid 2px ${border}`,
@ -47,6 +48,9 @@ const styles = () => ({
flexBasis: '95px', flexBasis: '95px',
flexGrow: 0, flexGrow: 0,
}, },
popper: {
zIndex: 2000,
},
}) })
const Layout = openHoc(({ const Layout = openHoc(({
@ -63,17 +67,18 @@ const Layout = openHoc(({
<SafeListHeader /> <SafeListHeader />
<Divider /> <Divider />
<Spacer /> <Spacer />
<Divider />
<Provider open={open} toggle={toggle} info={providerInfo}> <Provider open={open} toggle={toggle} info={providerInfo}>
{(providerRef) => ( {(providerRef) => (
<Popper open={open} anchorEl={providerRef.current} placement="bottom-end"> <Popper open={open} anchorEl={providerRef.current} placement="bottom" className={classes.popper}>
{({ TransitionProps }) => ( {({ TransitionProps }) => (
<Grow {...TransitionProps}> <Grow {...TransitionProps}>
<ClickAwayListener onClickAway={clickAway} mouseEvent="onClick" touchEvent={false}> <>
<List className={classes.root} component="div"> <ClickAwayListener onClickAway={clickAway} mouseEvent="onClick" touchEvent={false}>
{providerDetails} <List className={classes.root} component="div">
</List> {providerDetails}
</ClickAwayListener> </List>
</ClickAwayListener>
</>
</Grow> </Grow>
)} )}
</Popper> </Popper>

View File

@ -5,6 +5,7 @@ import IconButton from '@material-ui/core/IconButton'
import ExpandLess from '@material-ui/icons/ExpandLess' import ExpandLess from '@material-ui/icons/ExpandLess'
import ExpandMore from '@material-ui/icons/ExpandMore' import ExpandMore from '@material-ui/icons/ExpandMore'
import Col from '~/components/layout/Col' import Col from '~/components/layout/Col'
import Divider from '~/components/layout/Divider'
import { type Open } from '~/components/hoc/OpenHoc' import { type Open } from '~/components/hoc/OpenHoc'
import { sm, md } from '~/theme/variables' import { sm, md } from '~/theme/variables'
@ -20,7 +21,8 @@ const styles = () => ({
height: '100%', height: '100%',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexBasis: '250px', flexBasis: '284px',
marginRight: '20px',
}, },
provider: { provider: {
padding: `${sm} ${md}`, padding: `${sm} ${md}`,
@ -54,12 +56,14 @@ class Provider extends React.Component<Props> {
return ( return (
<> <>
<div ref={this.myRef} className={classes.root}> <div ref={this.myRef} className={classes.root}>
<Divider />
<Col end="sm" middle="xs" className={classes.provider} onClick={toggle}> <Col end="sm" middle="xs" className={classes.provider} onClick={toggle}>
{info} {info}
<IconButton disableRipple className={classes.expand}> <IconButton disableRipple className={classes.expand}>
{open ? <ExpandLess /> : <ExpandMore />} {open ? <ExpandLess /> : <ExpandMore />}
</IconButton> </IconButton>
</Col> </Col>
<Divider />
</div> </div>
{children(this.myRef)} {children(this.myRef)}
</> </>

View File

@ -143,7 +143,7 @@ const UserDetails = ({
<Hairline margin="xs" /> <Hairline margin="xs" />
<Row className={classes.details}> <Row className={classes.details}>
<Paragraph noMargin align="right" className={classes.labels}> <Paragraph noMargin align="right" className={classes.labels}>
Client Wallet
</Paragraph> </Paragraph>
<Spacer /> <Spacer />
{provider === 'safe' {provider === 'safe'

View File

@ -1,11 +1,11 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { withSnackbar } from 'notistack'
import { logComponentStack, type Info } from '~/utils/logBoundaries' import { logComponentStack, type Info } from '~/utils/logBoundaries'
import { SharedSnackbarConsumer, type Variant } from '~/components/SharedSnackBar'
import { WALLET_ERROR_MSG } from '~/logic/wallets/store/actions'
import { getProviderInfo } from '~/logic/wallets/getWeb3' import { getProviderInfo } from '~/logic/wallets/getWeb3'
import type { ProviderProps } from '~/logic/wallets/store/model/provider' import type { ProviderProps } from '~/logic/wallets/store/model/provider'
import { NOTIFICATIONS, showSnackbar } from '~/logic/notifications'
import ProviderAccesible from './component/ProviderInfo/ProviderAccesible' import ProviderAccesible from './component/ProviderInfo/ProviderAccesible'
import UserDetails from './component/ProviderDetails/UserDetails' import UserDetails from './component/ProviderDetails/UserDetails'
import ProviderDisconnected from './component/ProviderInfo/ProviderDisconnected' import ProviderDisconnected from './component/ProviderInfo/ProviderDisconnected'
@ -16,7 +16,7 @@ import selector, { type SelectorProps } from './selector'
type Props = Actions & type Props = Actions &
SelectorProps & { SelectorProps & {
openSnackbar: (message: string, variant: Variant) => void, enqueueSnackbar: Function,
} }
type State = { type State = {
@ -39,31 +39,33 @@ class HeaderComponent extends React.PureComponent<Props, State> {
} }
componentDidCatch(error: Error, info: Info) { componentDidCatch(error: Error, info: Info) {
const { openSnackbar } = this.props const { enqueueSnackbar, closeSnackbar } = this.props
this.setState({ hasError: true }) this.setState({ hasError: true })
openSnackbar(WALLET_ERROR_MSG, 'error') showSnackbar(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG, enqueueSnackbar, closeSnackbar)
logComponentStack(error, info) logComponentStack(error, info)
} }
onDisconnect = () => { onDisconnect = () => {
const { removeProvider, openSnackbar } = this.props const { removeProvider, enqueueSnackbar, closeSnackbar } = this.props
clearInterval(this.providerListener) clearInterval(this.providerListener)
removeProvider(openSnackbar) removeProvider(enqueueSnackbar, closeSnackbar)
} }
onConnect = async () => { onConnect = async () => {
const { fetchProvider, openSnackbar } = this.props const { fetchProvider, enqueueSnackbar, closeSnackbar } = this.props
clearInterval(this.providerListener) clearInterval(this.providerListener)
let currentProvider: ProviderProps = await getProviderInfo() let currentProvider: ProviderProps = await getProviderInfo()
fetchProvider(currentProvider, openSnackbar) fetchProvider(currentProvider, enqueueSnackbar, closeSnackbar)
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 (currentProvider && JSON.stringify(currentProvider) !== JSON.stringify(newProvider)) {
fetchProvider(newProvider, openSnackbar) fetchProvider(newProvider, enqueueSnackbar, closeSnackbar)
} }
currentProvider = newProvider currentProvider = newProvider
}, 2000) }, 2000)
@ -111,13 +113,7 @@ class HeaderComponent extends React.PureComponent<Props, State> {
} }
} }
const Header = connect( export default connect(
selector, selector,
actions, actions,
)(HeaderComponent) )(withSnackbar(HeaderComponent))
const HeaderSnack = () => (
<SharedSnackbarConsumer>{({ openSnackbar }) => <Header openSnackbar={openSnackbar} />}</SharedSnackbarConsumer>
)
export default HeaderSnack

View File

@ -0,0 +1,16 @@
// @flow
import enqueueSnackbar from '~/logic/notifications/store/actions/enqueueSnackbar'
import closeSnackbar from '~/logic/notifications/store/actions/closeSnackbar'
import removeSnackbar from '~/logic/notifications/store/actions/removeSnackbar'
export type Actions = {
enqueueSnackbar: typeof enqueueSnackbar,
closeSnackbar: typeof closeSnackbar,
removeSnackbar: typeof removeSnackbar,
}
export default {
enqueueSnackbar,
closeSnackbar,
removeSnackbar,
}

View File

@ -0,0 +1,73 @@
// @flow
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { withSnackbar } from 'notistack'
import actions from './actions'
import selector from './selector'
class Notifier extends Component {
displayed = []
shouldComponentUpdate({ notifications: newSnacks = [] }) {
const { notifications: currentSnacks, closeSnackbar, removeSnackbar } = this.props
if (!newSnacks.size) {
this.displayed = []
return false
}
let notExists = false
for (let i = 0; i < newSnacks.size; i += 1) {
const newSnack = newSnacks.get(i)
if (newSnack.dismissed) {
closeSnackbar(newSnack.key)
removeSnackbar(newSnack.key)
}
if (notExists) {
continue
}
notExists = notExists || !currentSnacks.filter(({ key }) => newSnack.key === key).length
}
return notExists
}
componentDidUpdate() {
const { notifications = [], enqueueSnackbar, removeSnackbar } = this.props
notifications.forEach((notification) => {
// Do nothing if snackbar is already displayed
if (this.displayed.includes(notification.key)) {
return
}
// Display snackbar using notistack
enqueueSnackbar(notification.message, {
...notification.options,
onClose: (event, reason, key) => {
if (notification.options.onClose) {
notification.options.onClose(event, reason, key)
}
// Dispatch action to remove snackbar from redux store
removeSnackbar(key)
},
})
// Keep track of snackbars that we've displayed
this.storeDisplayed(notification.key)
})
}
storeDisplayed = (id) => {
this.displayed = [...this.displayed, id]
}
render() {
return null
}
}
export default withSnackbar(
connect(
selector,
actions,
)(Notifier),
)

View File

@ -0,0 +1,7 @@
// @flow
import { createStructuredSelector } from 'reselect'
import { notificationsListSelector } from '~/logic/notifications/store/selectors'
export default createStructuredSelector<Object, *>({
notifications: notificationsListSelector,
})

View File

@ -1,105 +0,0 @@
// @flow
import * as React from 'react'
import { Snackbar } from '@material-ui/core'
import SnackbarContent from '~/components/SnackbarContent'
export const SharedSnackbar = () => (
<SharedSnackbarConsumer>
{(value) => {
const {
snackbarIsOpen, message, closeSnackbar, variant,
} = value
return (
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
open={snackbarIsOpen}
autoHideDuration={4000}
onClose={closeSnackbar}
>
<SnackbarContent onClose={closeSnackbar} message={message} variant={variant} />
</Snackbar>
)
}}
</SharedSnackbarConsumer>
)
type SnackbarContext = {
openSnackbar: Function,
closeSnackbar: Function,
snackbarIsOpen: boolean,
message: string,
variant: string,
}
const SharedSnackbarContext = React.createContext<SnackbarContext>({
openSnackbar: undefined,
closeSnackbar: undefined,
snackbarIsOpen: false,
message: '',
variant: 'info',
})
type Props = {
children: React.Node,
}
export type Variant = 'success' | 'error' | 'warning' | 'info'
type State = {
isOpen: boolean,
message: string,
variant: Variant,
}
export class SharedSnackbarProvider extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
isOpen: false,
message: '',
variant: 'info',
}
}
openSnackbar = (message: string, variant: Variant) => {
this.setState({
message,
variant,
isOpen: true,
})
}
closeSnackbar = () => {
this.setState({
message: '',
isOpen: false,
})
}
render() {
const { children } = this.props
const { message, variant, isOpen } = this.state
return (
<SharedSnackbarContext.Provider
value={{
openSnackbar: this.openSnackbar,
closeSnackbar: this.closeSnackbar,
snackbarIsOpen: isOpen,
message,
variant,
}}
>
<SharedSnackbar />
{children}
</SharedSnackbarContext.Provider>
)
}
}
export const SharedSnackbarConsumer = SharedSnackbarContext.Consumer

View File

@ -1,110 +0,0 @@
// @flow
import SnackbarContent from '@material-ui/core/SnackbarContent'
import classNames from 'classnames/bind'
import * as React from 'react'
import CloseIcon from '@material-ui/icons/Close'
import CheckCircleIcon from '@material-ui/icons/CheckCircle'
import ErrorIcon from '@material-ui/icons/Error'
import InfoIcon from '@material-ui/icons/Info'
import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles'
import WarningIcon from '@material-ui/icons/Warning'
import { type WithStyles } from '~/theme/mui'
import {
secondary, warning, connected, error,
} from '~/theme/variables'
type Variant = 'success' | 'error' | 'warning' | 'info'
type MessageProps = WithStyles & {
variant: Variant,
message: string,
}
type Props = MessageProps & {
onClose?: () => void,
}
type CloseProps = WithStyles & {
onClose: () => void,
}
const variantIcon = {
success: CheckCircleIcon,
warning: WarningIcon,
error: ErrorIcon,
info: InfoIcon,
}
const styles = (theme) => ({
success: {
backgroundColor: '#ffffff',
},
successIcon: {
color: connected,
},
warning: {
backgroundColor: '#fff3e2',
},
warningIcon: {
color: warning,
},
error: {
backgroundColor: '#ffe6ea',
},
errorIcon: {
color: error,
},
info: {
backgroundColor: '#ffffff',
},
infoIcon: {
color: secondary,
},
icon: {
fontSize: 20,
},
iconVariant: {
opacity: 0.9,
marginRight: theme.spacing(1),
},
message: {
display: 'flex',
alignItems: 'center',
},
})
const Close = ({ classes, onClose }: CloseProps) => (
<IconButton key="close" aria-label="Close" color="inherit" className={classes.close} onClick={onClose}>
<CloseIcon className={classes.icon} />
</IconButton>
)
const Message = ({ classes, message, variant }: MessageProps) => {
const Icon = variantIcon[variant]
return (
<span id="client-snackbar" className={classes.message}>
<Icon className={classNames(classes.icon, classes.iconVariant, classes[`${variant}Icon`])} />
{message}
</span>
)
}
const GnoSnackbarContent = ({
variant, classes, message, onClose,
}: Props) => {
const action = onClose ? [<Close key="close" onClose={onClose} classes={classes} />] : undefined
const messageComponent = <Message classes={classes} message={message} variant={variant} />
return (
<SnackbarContent
className={classNames(classes[variant])}
aria-describedby="client-snackbar"
message={messageComponent}
action={action}
/>
)
}
export default withStyles(styles)(GnoSnackbarContent)

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="18" viewBox="0 0 22 18">
<g fill="none" fill-rule="nonzero">
<path fill="#FFC05F" d="M1.734 18h18.532a1 1 0 0 0 .865-1.501L11.865.495a1 1 0 0 0-1.73 0L.869 16.499A1 1 0 0 0 1.734 18z"/>
<path fill="#FFF" d="M12 12h-2V6h2zM12 15h-2v-2h2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 335 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="20" viewBox="0 0 22 20">
<g fill="none" fill-rule="evenodd">
<path fill="#008C73" d="M11 0C5.489 0 1 4.489 1 10s4.489 10 10 10 10-4.489 10-10S16.511 0 11 0z"/>
<path fill="#FFF" d="M10.124 13.75L6 9.406l1.245-1.312 2.88 3.034 4.63-4.878L16 7.561z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<g fill="none" fill-rule="evenodd">
<path fill="#F02525" fill-rule="nonzero" d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0z"/>
<path fill="#FFF" d="M11 15H9v-2h2zM11 11H9V5h2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@ -1,23 +1,85 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import Header from '~/components/Header' import { SnackbarProvider } from 'notistack'
import { withStyles } from '@material-ui/core/styles'
import SidebarProvider from '~/components/Sidebar' import SidebarProvider from '~/components/Sidebar'
import { SharedSnackbarProvider } from '~/components/SharedSnackBar' import Header from '~/components/Header'
import Img from '~/components/layout/Img'
import Notifier from '~/components/Notifier'
import AlertLogo from './assets/alert.svg'
import CheckLogo from './assets/check.svg'
import ErrorLogo from './assets/error.svg'
import styles from './index.scss' import styles from './index.scss'
const notificationStyles = {
success: {
background: '#ffffff',
fontFamily: 'Averta',
fontSize: '14px',
lineHeight: 1.43,
color: '#001428',
minHeight: '58px',
boxShadow: '0 0 10px 0 rgba(212, 212, 211, 0.59)',
},
error: {
background: '#ffe6ea',
fontFamily: 'Averta',
fontSize: '14px',
lineHeight: 1.43,
color: '#001428',
minHeight: '58px',
boxShadow: '0 0 10px 0 rgba(212, 212, 211, 0.59)',
},
warning: {
background: '#fff3e2',
fontFamily: 'Averta',
fontSize: '14px',
lineHeight: 1.43,
color: '#001428',
minHeight: '58px',
boxShadow: '0 0 10px 0 rgba(212, 212, 211, 0.59)',
},
info: {
background: '#e8673c',
fontFamily: 'Averta',
fontSize: '14px',
lineHeight: 1.43,
color: '#ffffff',
minHeight: '58px',
boxShadow: '0 0 10px 0 rgba(212, 212, 211, 0.59)',
},
}
type Props = { type Props = {
children: React.Node, children: React.Node,
classes: Object,
} }
const PageFrame = ({ children }: Props) => ( const PageFrame = ({ children, classes }: Props) => (
<SharedSnackbarProvider> <div className={styles.frame}>
<div className={styles.frame}> <SnackbarProvider
maxSnack={5}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
classes={{
variantSuccess: classes.success,
variantError: classes.error,
variantWarning: classes.warning,
variantInfo: classes.info,
}}
iconVariant={{
success: <Img src={CheckLogo} alt="Success" />,
error: <Img src={ErrorLogo} alt="Error" />,
warning: <Img src={AlertLogo} alt="Warning" />,
info: '',
}}
>
<Notifier />
<SidebarProvider> <SidebarProvider>
<Header /> <Header />
{children} {children}
</SidebarProvider> </SidebarProvider>
</div> </SnackbarProvider>
</SharedSnackbarProvider> </div>
) )
export default PageFrame export default withStyles(notificationStyles)(PageFrame)

View File

@ -0,0 +1,3 @@
// @flow
export * from './notificationTypes'
export * from './notificationBuilder'

View File

@ -0,0 +1,159 @@
// @flow
import * as React from 'react'
import { IconButton } from '@material-ui/core'
import { Close as IconClose } from '@material-ui/icons'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { type Notification, NOTIFICATIONS } from './notificationTypes'
type NotificationsQueue = {
beforeExecution: Notification,
pendingExecution: {
noMoreConfirmationsNeeded: Notification,
moreConfirmationsNeeded: Notification,
},
afterExecution: Notification,
afterExecutionError: Notification,
afterRejection: Notification,
}
const standardTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_TX_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
afterExecution: NOTIFICATIONS.TX_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG,
}
const confirmationTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_TX_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_CONFIRMATION_PENDING_MSG,
moreConfirmationsNeeded: null,
},
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
afterExecution: NOTIFICATIONS.TX_CONFIRMATION_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.TX_CONFIRMATION_FAILED_MSG,
}
const cancellationTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_TX_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
afterExecution: NOTIFICATIONS.TX_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG,
}
const ownerChangeTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_OWNER_CHANGE_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.ONWER_CHANGE_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.ONWER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.ONWER_CHANGE_REJECTED_MSG,
afterExecution: NOTIFICATIONS.OWNER_CHANGE_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.ONWER_CHANGE_FAILED_MSG,
}
const safeNameChangeNotificationsQueue: NotificationsQueue = {
beforeExecution: null,
pendingExecution: {
noMoreConfirmationsNeeded: null,
moreConfirmationsNeeded: null,
},
afterRejection: null,
afterExecution: NOTIFICATIONS.SAFE_NAME_CHANGE_EXECUTED_MSG,
afterExecutionError: null,
}
const ownerNameChangeNotificationsQueue: NotificationsQueue = {
beforeExecution: null,
pendingExecution: {
noMoreConfirmationsNeeded: null,
moreConfirmationsNeeded: null,
},
afterRejection: null,
afterExecution: NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG,
afterExecutionError: null,
}
const thresholdChangeTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_THRESHOLD_CHANGE_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.THRESHOLD_CHANGE_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.THRESHOLD_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.THRESHOLD_CHANGE_REJECTED_MSG,
afterExecution: NOTIFICATIONS.THRESHOLD_CHANGE_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.THRESHOLD_CHANGE_FAILED_MSG,
}
const defaultNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_TX_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
afterExecution: NOTIFICATIONS.TX_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG,
}
export const getNofiticationsFromTxType = (txType: string) => {
let notificationsQueue: NotificationsQueue
switch (txType) {
case TX_NOTIFICATION_TYPES.STANDARD_TX: {
notificationsQueue = standardTxNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.CONFIRMATION_TX: {
notificationsQueue = confirmationTxNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.CANCELLATION_TX: {
notificationsQueue = cancellationTxNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.OWNER_CHANGE_TX: {
notificationsQueue = ownerChangeTxNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX: {
notificationsQueue = safeNameChangeNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.OWNER_NAME_CHANGE_TX: {
notificationsQueue = ownerNameChangeNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.THRESHOLD_CHANGE_TX: {
notificationsQueue = thresholdChangeTxNotificationsQueue
break
}
default: {
notificationsQueue = defaultNotificationsQueue
break
}
}
return notificationsQueue
}
export const showSnackbar = (
notification: Notification,
enqueueSnackbar: Function,
closeSnackbar: Function,
) => enqueueSnackbar(notification.message, {
...notification.options,
action: (key) => (
<IconButton onClick={() => closeSnackbar(key)}>
<IconClose />
</IconButton>
),
})

View File

@ -0,0 +1,224 @@
// @flow
export const SUCCESS = 'success'
export const ERROR = 'error'
export const WARNING = 'warning'
export const INFO = 'info'
const shortDuration = 5000
const longDuration = 10000
export type Variant = SUCCESS | ERROR | WARNING | INFO
export type Notification = {
message: string,
options: {
variant: Variant,
persist: boolean,
autoHideDuration?: shortDuration | longDuration,
preventDuplicate: boolean,
},
}
export type Notifications = {
// Wallet Connection
CONNECT_WALLET_MSG: Notification,
CONNECT_WALLET_READ_MODE_MSG: Notification,
WALLET_CONNECTED_MSG: Notification,
WALLET_DISCONNECTED_MSG: Notification,
UNLOCK_WALLET_MSG: Notification,
CONNECT_WALLET_ERROR_MSG: Notification,
// Regular/Custom Transactions
SIGN_TX_MSG: Notification,
TX_PENDING_MSG: Notification,
TX_PENDING_MORE_CONFIRMATIONS_MSG: Notification,
TX_REJECTED_MSG: Notification,
TX_EXECUTED_MSG: Notification,
TX_FAILED_MSG: Notification,
// Approval Transactions
TX_CONFIRMATION_PENDING_MSG: Notification,
TX_CONFIRMATION_EXECUTED_MSG: Notification,
TX_CONFIRMATION_FAILED_MSG: Notification,
// Safe Name
SAFE_NAME_CHANGED_MSG: Notification,
// Owner Name
OWNER_NAME_CHANGE_EXECUTED_MSG: Notification,
// Owners
SIGN_OWNER_CHANGE_MSG: Notification,
ONWER_CHANGE_PENDING_MSG: Notification,
ONWER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: Notification,
ONWER_CHANGE_REJECTED_MSG: Notification,
ONWER_CHANGE_EXECUTED_MSG: Notification,
ONWER_CHANGE_FAILED_MSG: Notification,
// Threshold
SIGN_THRESHOLD_CHANGE_MSG: Notification,
THRESHOLD_CHANGE_PENDING_MSG: Notification,
THRESHOLD_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: Notification,
THRESHOLD_CHANGE_REJECTED_MSG: Notification,
THRESHOLD_CHANGE_EXECUTED_MSG: Notification,
THRESHOLD_CHANGE_FAILED_MSG: Notification,
// Rinkeby version
RINKEBY_VERSION_MSG: Notification,
WRONG_NETWORK_RINKEBY_MSG: Notification,
WRONG_NETWOEK_MAINNET_MSG: Notification,
}
export const NOTIFICATIONS: Notifications = {
// Wallet Connection
CONNECT_WALLET_MSG: {
message: 'Please connect wallet to continue',
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
CONNECT_WALLET_READ_MODE_MSG: {
message: 'You are in read-only mode: Please connect wallet',
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
WALLET_CONNECTED_MSG: {
message: 'Wallet connected',
options: {
variant: SUCCESS,
persist: false,
autoHideDuration: shortDuration,
},
},
WALLET_DISCONNECTED_MSG: {
message: 'Wallet disconnected',
options: {
variant: SUCCESS,
persist: false,
autoHideDuration: shortDuration,
},
},
UNLOCK_WALLET_MSG: {
message: 'Unlock your wallet to connect',
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
CONNECT_WALLET_ERROR_MSG: {
message: 'Error connecting to your wallet',
options: { variant: ERROR, persist: true },
},
// Regular/Custom Transactions
SIGN_TX_MSG: {
message: 'Please sign the transaction',
options: { variant: SUCCESS, persist: true },
},
TX_PENDING_MSG: {
message: 'Transaction pending',
options: { variant: SUCCESS, persist: true },
},
TX_PENDING_MORE_CONFIRMATIONS_MSG: {
message: 'Transaction pending: More confirmations required to execute',
options: { variant: SUCCESS, persist: true },
},
TX_REJECTED_MSG: {
message: 'Transaction rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
TX_EXECUTED_MSG: {
message: 'Transaction successfully executed',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
},
TX_FAILED_MSG: {
message: 'Transaction failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
// Approval Transactions
TX_CONFIRMATION_PENDING_MSG: {
message: 'Confirmation transaction pending',
options: { variant: SUCCESS, persist: true },
},
TX_CONFIRMATION_EXECUTED_MSG: {
message: 'Confirmation transaction succesful',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
},
TX_CONFIRMATION_FAILED_MSG: {
message: 'Confirmation transaction failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
// Safe Name
SAFE_NAME_CHANGE_EXECUTED_MSG: {
message: 'Safe name changed',
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
},
// Owner Name
OWNER_NAME_CHANGE_EXECUTED_MSG: {
message: 'Owner name changed',
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
},
// Owners
SIGN_OWNER_CHANGE_MSG: {
message: 'Please sign the owner change',
options: { variant: SUCCESS, persist: true },
},
ONWER_CHANGE_PENDING_MSG: {
message: 'Owner change pending',
options: { variant: SUCCESS, persist: true },
},
ONWER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: {
message: 'Owner change pending: More confirmations required to execute',
options: { variant: SUCCESS, persist: true },
},
ONWER_CHANGE_REJECTED_MSG: {
message: 'Owner change rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
OWNER_CHANGE_EXECUTED_MSG: {
message: 'Owner change successfully executed',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
},
ONWER_CHANGE_FAILED_MSG: {
message: 'Owner change failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
// Threshold
SIGN_THRESHOLD_CHANGE_MSG: {
message: 'Please sign the required confirmations change',
options: { variant: SUCCESS, persist: true },
},
THRESHOLD_CHANGE_PENDING_MSG: {
message: 'Required confirmations change pending',
options: { variant: SUCCESS, persist: true },
},
THRESHOLD_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: {
message: 'Required confirmations change pending: More confirmations required to execute',
options: { variant: SUCCESS, persist: true },
},
THRESHOLD_CHANGE_REJECTED_MSG: {
message: 'Required confirmations change rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
THRESHOLD_CHANGE_EXECUTED_MSG: {
message: 'Required confirmations change successfully executed',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
},
THRESHOLD_CHANGE_FAILED_MSG: {
message: 'Required confirmations change failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
// Network
RINKEBY_VERSION_MSG: {
message: "Rinkeby Version: Don't send mainnet assets to this Safe",
options: { variant: INFO, persist: true, preventDuplicate: true },
},
WRONG_NETWORK_RINKEBY_MSG: {
message: 'Wrong network: Please use Rinkeby',
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
WRONG_NETWOEK_MAINNET_MSG: {
message: 'Wrong network: Please use Mainnet',
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
}

View File

@ -0,0 +1,8 @@
// @flow
import { createAction } from 'redux-actions'
export const CLOSE_SNACKBAR = 'CLOSE_SNACKBAR'
const closeSnackbar = createAction<string, *>(CLOSE_SNACKBAR)
export default closeSnackbar

View File

@ -0,0 +1,22 @@
// @flow
import { createAction } from 'redux-actions'
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
import { type GlobalState } from '~/store'
import { type NotificationProps } from '~/logic/notifications/store/models/notification'
export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
const addSnackbar = createAction<string, *>(ENQUEUE_SNACKBAR)
const enqueueSnackbar = (notification: NotificationProps) => (
dispatch: ReduxDispatch<GlobalState>,
getState: GetState<GlobalState>,
) => {
const newNotification = {
...notification,
key: new Date().getTime(),
}
dispatch(addSnackbar(newNotification))
}
export default enqueueSnackbar

View File

@ -0,0 +1,8 @@
// @flow
import { createAction } from 'redux-actions'
export const REMOVE_SNACKBAR = 'REMOVE_SNACKBAR'
const removeSnackbar = createAction<string, *>(REMOVE_SNACKBAR)
export default removeSnackbar

View File

@ -0,0 +1,19 @@
// @flow
import { Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
export type NotificationProps = {
key?: number,
message: string,
options: Object,
dismissed: boolean,
}
export const makeNotification: RecordFactory<NotificationProps> = Record({
key: 0,
message: '',
options: {},
dismissed: false,
})
export type Notification = RecordOf<NotificationProps>

View File

@ -0,0 +1,35 @@
// @flow
import { Map } from 'immutable'
import { handleActions, type ActionType } from 'redux-actions'
import { makeNotification, type NotificationProps } from '~/logic/notifications/store/models/notification'
import { ENQUEUE_SNACKBAR } from '../actions/enqueueSnackbar'
import { CLOSE_SNACKBAR } from '../actions/closeSnackbar'
import { REMOVE_SNACKBAR } from '../actions/removeSnackbar'
export const NOTIFICATIONS_REDUCER_ID = 'notifications'
export type NotificationReducerState = Map<string, *>
export default handleActions<NotificationReducerState, *>(
{
[ENQUEUE_SNACKBAR]: (state: NotificationReducerState, action: ActionType<Function>): NotificationReducerState => {
const notification: NotificationProps = action.payload
return state.set(notification.key, makeNotification(notification))
},
[CLOSE_SNACKBAR]: (state: NotificationReducerState, action: ActionType<Function>): NotificationReducerState => {
const { notification }: { notification: NotificationProps } = action.payload
notification.dismissed = true
return state.update(notification.key, (prev) => prev.merge(notification))
},
[REMOVE_SNACKBAR]: (state: NotificationReducerState, action: ActionType<Function>): NotificationReducerState => {
const key = action.payload
return state.delete(key)
},
},
Map({
notifications: Map(),
}),
)

View File

@ -0,0 +1,15 @@
// @flow
import { List, Map } from 'immutable'
import { createSelector, type Selector } from 'reselect'
import { type GlobalState } from '~/store'
import { NOTIFICATIONS_REDUCER_ID } from '~/logic/notifications/store/reducer/notifications'
import { type Notification } from '~/logic/notifications/store/models/notification'
export const notificationsMapSelector = (
state: GlobalState,
): Map<string, Notification> => state[NOTIFICATIONS_REDUCER_ID]
export const notificationsListSelector: Selector<GlobalState, {}, List<Notification>> = createSelector(
notificationsMapSelector,
(notifications: Map<string, Notification>): List<Notification> => notifications.toList(),
)

View File

@ -82,8 +82,7 @@ export const generateTxGasEstimateFrom = async (
// Add 10k else we will fail in case of nested calls // Add 10k else we will fail in case of nested calls
return txGasEstimate.toNumber() + 10000 return txGasEstimate.toNumber() + 10000
} catch (error) { } catch (error) {
// eslint-disable-next-line console.error('Error calculating tx gas estimation', error)
console.log('Error calculating tx gas estimation ' + error)
return 0 return 0
} }
} }
@ -128,8 +127,7 @@ export const calculateTxFee = async (
return estimate return estimate
} catch (error) { } catch (error) {
// eslint-disable-next-line console.error('Error calculating tx gas estimation', error)
console.log('Error calculating tx gas estimation ' + error)
return 0 return 0
} }
} }

View File

@ -1,7 +1,6 @@
// @flow // @flow
export * from './gas' export * from './gas'
export * from './send' export * from './send'
export * from './safeBlockchainOperations'
export * from './safeTxSignerEIP712' export * from './safeTxSignerEIP712'
export * from './txHistory' export * from './txHistory'
export * from './notifications' export * from './notifiedTransactions'

View File

@ -1,14 +0,0 @@
// @flow
export type Notifications = {
BEFORE_EXECUTION_OR_CREATION: string,
AFTER_EXECUTION: string,
CREATED_MORE_CONFIRMATIONS_NEEDED: string,
ERROR: string,
}
export const DEFAULT_NOTIFICATIONS: Notifications = {
BEFORE_EXECUTION_OR_CREATION: 'Transaction in progress',
AFTER_EXECUTION: 'Transaction successfully executed',
CREATED_MORE_CONFIRMATIONS_NEEDED: 'Transaction in progress: More confirmations required to execute',
ERROR: 'Transaction failed',
}

View File

@ -0,0 +1,21 @@
// @flow
export type NotifiedTransaction = {
STANDARD_TX: string,
CONFIRMATION_TX: string,
CANCELLATION_TX: string,
OWNER_CHANGE_TX: string,
SAFE_NAME_CHANGE_TX: string,
OWNER_NAME_CHANGE_TX: string,
THRESHOLD_CHANGE_TX: string,
}
export const TX_NOTIFICATION_TYPES: NotifiedTransaction = {
STANDARD_TX: 'STANDARD_TX',
CONFIRMATION_TX: 'CONFIRMATION_TX',
CANCELLATION_TX: 'CANCELLATION_TX',
OWNER_CHANGE_TX: 'OWNER_CHANGE_TX',
SAFE_NAME_CHANGE_TX: 'SAFE_NAME_CHANGE_TX',
OWNER_NAME_CHANGE_TX: 'OWNER_NAME_CHANGE_TX',
THRESHOLD_CHANGE_TX: 'THRESHOLD_CHANGE_TX',
}

View File

@ -1,142 +0,0 @@
// // @flow
// import { List } from 'immutable'
// import { calculateGasOf, checkReceiptStatus, calculateGasPrice } from '~/logic/wallets/ethTransactions'
// import { type Operation, saveTxToHistory } from '~/logic/safe/transactions'
// import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
// import { buildSignaturesFrom } from '~/logic/safe/safeTxSigner'
// import { generateMetamaskSignature, generateTxGasEstimateFrom, estimateDataGas } from '~/logic/safe/transactions'
// import { storeSignature, getSignaturesFrom } from '~/utils/storage/signatures'
// import { signaturesViaMetamask } from '~/config'
// export const approveTransaction = async (
// safeAddress: string,
// to: string,
// valueInWei: number,
// data: string,
// operation: Operation,
// nonce: number,
// sender: string,
// ) => {
// const gasPrice = await calculateGasPrice()
// if (signaturesViaMetamask()) {
// // return executeTransaction(safeAddress, to, valueInWei, data, operation, nonce, sender)
// const safe = await getGnosisSafeInstanceAt(safeAddress)
// const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
// const signature = await generateMetamaskSignature(
// safe,
// safeAddress,
// // sender
// to,
// valueInWei,
// nonce,
// data,
// operation,
// txGasEstimate,
// )
// storeSignature(safeAddress, nonce, signature)
// return undefined
// }
// const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
// const contractTxHash = await gnosisSafe.getTransactionHash(to, valueInWei, data, operation, 0, 0, 0, 0, 0, nonce)
// const approveData = gnosisSafe.contract.methods.approveHash(contractTxHash).encodeABI()
// const gas = await calculateGasOf(approveData, sender, safeAddress)
// const txReceipt = await gnosisSafe.approveHash(contractTxHash, { from: sender, gas, gasPrice })
// const txHash = txReceipt.tx
// await checkReceiptStatus(txHash)
// await saveTxToHistory(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'confirmation')
// return txHash
// }
// // export const executeTransaction = async (
// // safeAddress: string,
// // to: string,
// // valueInWei: number,
// // data: string,
// // operation: Operation,
// // nonce: number,
// // sender: string,
// // ownersWhoHasSigned: List<string>,
// // ) => {
// // const gasPrice = await calculateGasPrice()
// // if (signaturesViaMetamask()) {
// // const safe = await getSafeEthereumInstance(safeAddress)
// // const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
// // const signature = await generateMetamaskSignature(
// // safe,
// // safeAddress,
// // sender,
// // to,
// // valueInWei,
// // nonce,
// // data,
// // operation,
// // txGasEstimate,
// // )
// // storeSignature(safeAddress, nonce, signature)
// // const sigs = getSignaturesFrom(safeAddress, nonce)
// // const threshold = await safe.getThreshold()
// // const gas = await estimateDataGas(
// // safe,
// // to,
// // valueInWei,
// // data,
// // operation,
// // txGasEstimate,
// // 0,
// // nonce,
// // Number(threshold),
// // 0,
// // )
// // const numOwners = await safe.getOwners()
// // const gasIncludingRemovingStoreUpfront = gas + txGasEstimate + numOwners.length * 15000
// // const txReceipt = await safe.execTransaction(
// // to,
// // valueInWei,
// // data,
// // operation,
// // txGasEstimate,
// // 0, // dataGasEstimate
// // 0, // gasPrice
// // 0, // txGasToken
// // 0, // refundReceiver
// // sigs,
// // { from: sender, gas: gasIncludingRemovingStoreUpfront, gasPrice },
// // )
// // const txHash = txReceipt.tx
// // await checkReceiptStatus(txHash)
// // // await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'execution')
// // return txHash
// // }
// // const gnosisSafe = await getSafeEthereumInstance(safeAddress)
// // const signatures = buildSignaturesFrom(ownersWhoHasSigned, sender)
// // const txExecutionData = gnosisSafe.contract.methods
// // .execTransaction(to, valueInWei, data, operation, 0, 0, 0, 0, 0, signatures)
// // .encodeABI()
// // const gas = await calculateGasOf(txExecutionData, sender, safeAddress)
// // const numOwners = await gnosisSafe.getOwners()
// // const gasIncludingRemovingStoreUpfront = gas + numOwners.length * 15000
// // const txReceipt = await gnosisSafe.execTransaction(to, valueInWei, data, operation, 0, 0, 0, 0, 0, signatures, {
// // from: sender,
// // gas: gasIncludingRemovingStoreUpfront,
// // gasPrice,
// // })
// // const txHash = txReceipt.tx
// // await checkReceiptStatus(txHash)
// // await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'execution')
// // return txHash
// // }

View File

@ -1,14 +1,8 @@
// @flow // @flow
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
import { type Token } from '~/logic/tokens/store/model/token'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { type Operation } from '~/logic/safe/transactions' import { type Operation } from '~/logic/safe/transactions'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses' import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
import { getErrorMessage } from '~/test/utils/ethereumErrors'
export const CALL = 0 export const CALL = 0
export const TX_TYPE_EXECUTION = 'execution' export const TX_TYPE_EXECUTION = 'execution'
@ -44,13 +38,10 @@ export const getApprovalTransaction = async (
const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address) const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address)
return contract.methods.approveHash(txHash) return contract.methods.approveHash(txHash)
} catch (error) { } catch (err) {
/* eslint-disable */ console.error(`Error while approving transaction: ${err}`)
const executeData = safeInstance.contract.methods.approveHash(txHash).encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeData, sender)
console.log(`Error executing the TX: ${errMsg}`)
throw error throw err
} }
} }
@ -62,58 +53,16 @@ export const getExecutionTransaction = async (
operation: Operation, operation: Operation,
nonce: string | number, nonce: string | number,
sender: string, sender: string,
signatures?: string, sigs: string,
) => { ) => {
let sigs = signatures
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
if (!sigs) {
sigs = `0x000000000000000000000000${sender.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
}
try { try {
const web3 = getWeb3() const web3 = getWeb3()
const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address) const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address)
return contract.methods.execTransaction(to, valueInWei, data, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs) return contract.methods.execTransaction(to, valueInWei, data, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
} catch (error) { } catch (err) {
/* eslint-disable */ console.error(`Error while creating transaction: ${err}`)
const executeDataUsedSignatures = safeInstance.contract.methods
.execTransaction(to, valueInWei, data, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
.encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeDataUsedSignatures, sender)
console.log(`Error executing the TX: ${errMsg}`)
throw error throw err
} }
} }
export const createTransaction = async (safeAddress: string, to: string, valueInEth: string, token: Token) => {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const web3 = getWeb3()
const from = web3.currentProvider.selectedAddress
const threshold = await safeInstance.getThreshold()
const nonce = (await safeInstance.nonce()).toString()
const valueInWei = web3.utils.toWei(valueInEth, 'ether')
const isExecution = threshold.toNumber() === 1
let txData = EMPTY_DATA
if (!isEther(token.symbol)) {
const StandardToken = await getStandardTokenContract()
const sendToken = await StandardToken.at(token.address)
txData = sendToken.contract.transfer(to, valueInWei).encodeABI()
}
let txHash
if (isExecution) {
txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
} else {
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
}
return txHash
}

View File

@ -22,8 +22,7 @@ export const saveSafes = async (safes: Object) => {
try { try {
await saveToStorage(SAFES_KEY, safes) await saveToStorage(SAFES_KEY, safes)
} catch (err) { } catch (err) {
// eslint-disable-next-line console.error('Error storing safe info in localstorage', err)
console.log('Error storing safe info in localstorage')
} }
} }
@ -32,8 +31,7 @@ export const setOwners = async (safeAddress: string, owners: List<Owner>) => {
const ownersAsMap = Map(owners.map((owner: Owner) => [owner.address.toLowerCase(), owner.name])) const ownersAsMap = Map(owners.map((owner: Owner) => [owner.address.toLowerCase(), owner.name]))
await saveToStorage(`${OWNERS_KEY}-${safeAddress}`, ownersAsMap) await saveToStorage(`${OWNERS_KEY}-${safeAddress}`, ownersAsMap)
} catch (err) { } catch (err) {
// eslint-disable-next-line console.error('Error storing owners in localstorage', err)
console.log('Error storing owners in localstorage')
} }
} }

View File

@ -40,8 +40,7 @@ export const fetchTokens = () => async (dispatch: ReduxDispatch<GlobalState>) =>
dispatch(saveTokens(tokens)) dispatch(saveTokens(tokens))
} catch (err) { } catch (err) {
// eslint-disable-next-line console.error('Error fetching token list', err)
console.log('Error fetching token list ' + err)
return Promise.resolve() return Promise.resolve()
} }

View File

@ -14,8 +14,7 @@ export const saveActiveTokens = async (tokens: Map<string, Token>) => {
try { try {
await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS()) await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS())
} catch (err) { } catch (err) {
// eslint-disable-next-line console.error('Error storing tokens in localstorage', err)
console.log('Error storing tokens in localstorage')
} }
} }
@ -38,8 +37,7 @@ export const removeTokenFromStorage = async (safeAddress: string, token: Token)
const index = data.indexOf(token) const index = data.indexOf(token)
await saveToStorage(CUSTOM_TOKENS_KEY, data.remove(index)) await saveToStorage(CUSTOM_TOKENS_KEY, data.remove(index))
} catch (err) { } catch (err) {
// eslint-disable-next-line console.error('Error removing token in localstorage', err)
console.log('Error removing token in localstorage')
} }
} }

View File

@ -76,7 +76,14 @@ export const getProviderInfo: Function = async (): Promise<ProviderProps> => {
if (window.ethereum) { if (window.ethereum) {
web3Provider = window.ethereum web3Provider = window.ethereum
await web3Provider.enable() try {
const accounts = await web3Provider.enable()
if (!accounts) {
throw new Error()
}
} catch (error) {
console.error('Error when enabling web3 provider', error)
}
} else if (window.web3) { } else if (window.web3) {
web3Provider = window.web3.currentProvider web3Provider = window.web3.currentProvider
} else { } else {
@ -114,6 +121,10 @@ export const getAddressFromENS = async (name: string) => {
} }
export const getBalanceInEtherOf = async (safeAddress: string) => { export const getBalanceInEtherOf = async (safeAddress: string) => {
if (!web3) {
return '0'
}
const funds: String = await web3.eth.getBalance(safeAddress) const funds: String = await web3.eth.getBalance(safeAddress)
if (!funds) { if (!funds) {

View File

@ -3,6 +3,7 @@ import type { Dispatch as ReduxDispatch } from 'redux'
import { ETHEREUM_NETWORK_IDS, ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3' import { ETHEREUM_NETWORK_IDS, ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
import type { ProviderProps } from '~/logic/wallets/store/model/provider' import type { ProviderProps } from '~/logic/wallets/store/model/provider'
import { makeProvider } from '~/logic/wallets/store/model/provider' import { makeProvider } from '~/logic/wallets/store/model/provider'
import { NOTIFICATIONS, showSnackbar } from '~/logic/notifications'
import addProvider from './addProvider' import addProvider from './addProvider'
export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: ProviderProps) => { export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: ProviderProps) => {
@ -21,30 +22,40 @@ export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: Pr
dispatch(addProvider(walletRecord)) dispatch(addProvider(walletRecord))
} }
const SUCCESS_MSG = 'Wallet connected sucessfully' const handleProviderNotification = (
const UNLOCK_MSG = 'Unlock your wallet to connect' dispatch: ReduxDispatch<*>,
const WRONG_NETWORK = 'You are connected to wrong network. Please use RINKEBY' provider: ProviderProps,
export const WALLET_ERROR_MSG = 'Error connecting to your wallet' enqueueSnackbar: Function,
closeSnackbar: Function,
const handleProviderNotification = (openSnackbar: Function, provider: ProviderProps) => { ) => {
const { loaded, available, network } = provider const { loaded, available, network } = provider
if (!loaded) { if (!loaded) {
openSnackbar(WALLET_ERROR_MSG, 'error') showSnackbar(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG, enqueueSnackbar, closeSnackbar)
return return
} }
if (ETHEREUM_NETWORK_IDS[network] !== ETHEREUM_NETWORK.RINKEBY) { if (ETHEREUM_NETWORK_IDS[network] !== ETHEREUM_NETWORK.RINKEBY) {
openSnackbar(WRONG_NETWORK, 'error') showSnackbar(NOTIFICATIONS.WRONG_NETWORK_RINKEBY_MSG, enqueueSnackbar, closeSnackbar)
return return
} }
showSnackbar(NOTIFICATIONS.RINKEBY_VERSION_MSG, enqueueSnackbar, closeSnackbar)
const msg = available ? SUCCESS_MSG : UNLOCK_MSG if (available) {
const variant = available ? 'success' : 'warning' // NOTE:
openSnackbar(msg, variant) // if you want to be able to dispatch a `closeSnackbar` action later on,
// you SHOULD pass your own `key` in the options. `key` can be any sequence
// of number or characters, but it has to be unique to a given snackbar.
showSnackbar(NOTIFICATIONS.WALLET_CONNECTED_MSG, enqueueSnackbar, closeSnackbar)
} else {
showSnackbar(NOTIFICATIONS.UNLOCK_WALLET_MSG, enqueueSnackbar, closeSnackbar)
}
} }
export default (provider: ProviderProps, openSnackbar: Function) => (dispatch: ReduxDispatch<*>) => { export default (provider: ProviderProps, enqueueSnackbar: Function, closeSnackbar: Function) => (
handleProviderNotification(openSnackbar, provider) dispatch: ReduxDispatch<*>,
) => {
handleProviderNotification(dispatch, provider, enqueueSnackbar, closeSnackbar)
processProviderResponse(dispatch, provider) processProviderResponse(dispatch, provider)
} }

View File

@ -1,9 +1,10 @@
// @flow // @flow
import type { Dispatch as ReduxDispatch } from 'redux' import type { Dispatch as ReduxDispatch } from 'redux'
import { makeProvider, type ProviderProps, type Provider } from '~/logic/wallets/store/model/provider' import { makeProvider, type ProviderProps, type Provider } from '~/logic/wallets/store/model/provider'
import { NOTIFICATIONS, showSnackbar } from '~/logic/notifications'
import addProvider from './addProvider' import addProvider from './addProvider'
export default (openSnackbar: Function) => async (dispatch: ReduxDispatch<*>) => { export default (enqueueSnackbar: Function, closeSnackbar: Function) => async (dispatch: ReduxDispatch<*>) => {
const providerProps: ProviderProps = { const providerProps: ProviderProps = {
name: '', name: '',
available: false, available: false,
@ -13,7 +14,7 @@ export default (openSnackbar: Function) => async (dispatch: ReduxDispatch<*>) =>
} }
const provider: Provider = makeProvider(providerProps) const provider: Provider = makeProvider(providerProps)
openSnackbar('Wallet disconnected succesfully', 'info') showSnackbar(NOTIFICATIONS.WALLET_DISCONNECTED_MSG, enqueueSnackbar, closeSnackbar)
dispatch(addProvider(provider)) dispatch(addProvider(provider))
} }

View File

@ -50,8 +50,7 @@ class Load extends React.Component<Props> {
const url = `${SAFELIST_ADDRESS}/${safeAddress}` const url = `${SAFELIST_ADDRESS}/${safeAddress}`
history.push(url) history.push(url)
} catch (error) { } catch (error) {
// eslint-disable-next-line console.error('Error while loading the Safe', error)
console.log('Error while loading the Safe' + error)
} }
} }

View File

@ -4,7 +4,7 @@ import OpenInNew from '@material-ui/icons/OpenInNew'
import { withStyles } from '@material-ui/core/styles' 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 { SharedSnackbarConsumer } from '~/components/SharedSnackBar' import { withSnackbar } from 'notistack'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import Link from '~/components/layout/Link' import Link from '~/components/layout/Link'
@ -13,11 +13,12 @@ import Button from '~/components/layout/Button'
import Img from '~/components/layout/Img' import Img from '~/components/layout/Img'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import Identicon from '~/components/Identicon' import Identicon from '~/components/Identicon'
import { copyToClipboard } from '~/utils/clipboard'
import Hairline from '~/components/layout/Hairline' import Hairline from '~/components/layout/Hairline'
import { copyToClipboard } from '~/utils/clipboard'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils' import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers' import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
import ArrowDown from '../assets/arrow-down.svg' import ArrowDown from '../assets/arrow-down.svg'
import { secondary } from '~/theme/variables' import { secondary } from '~/theme/variables'
@ -33,6 +34,8 @@ type Props = {
ethBalance: string, ethBalance: string,
tx: Object, tx: Object,
createTransaction: Function, createTransaction: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
const openIconStyle = { const openIconStyle = {
@ -50,110 +53,117 @@ const ReviewCustomTx = ({
ethBalance, ethBalance,
tx, tx,
createTransaction, createTransaction,
}: Props) => ( enqueueSnackbar,
<SharedSnackbarConsumer> closeSnackbar,
{({ openSnackbar }) => { }: Props) => {
const submitTx = async () => { const submitTx = async () => {
const web3 = getWeb3() const web3 = getWeb3()
const txRecipient = tx.recipientAddress const txRecipient = tx.recipientAddress
const txData = tx.data const txData = tx.data
const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : 0 const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : 0
createTransaction(safeAddress, txRecipient, txValue, txData, openSnackbar) createTransaction(
onClose() safeAddress,
} txRecipient,
txValue,
txData,
TX_NOTIFICATION_TYPES.STANDARD_TX,
enqueueSnackbar,
closeSnackbar,
)
onClose()
}
return ( return (
<> <>
<Row align="center" grow className={classes.heading}> <Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin> <Paragraph weight="bolder" className={classes.headingText} noMargin>
Send Funds Send Funds
</Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<SafeInfo
safeAddress={safeAddress}
etherScanLink={etherScanLink}
safeName={safeName}
ethBalance={ethBalance}
/>
<Row margin="md">
<Col xs={1}>
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
</Col>
<Col xs={11} center="xs" layout="column">
<Hairline />
</Col>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Recipient
</Paragraph>
</Row>
<Row margin="md" align="center">
<Col xs={1}>
<Identicon address={tx.recipientAddress} diameter={32} />
</Col>
<Col xs={11} layout="column">
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
{tx.recipientAddress}
<Link to={etherScanLink} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Paragraph> </Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph> </Col>
<IconButton onClick={onClose} disableRipple> </Row>
<Close className={classes.closeIcon} /> <Row margin="xs">
</IconButton> <Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
</Row> Value
<Hairline /> </Paragraph>
<Block className={classes.container}> </Row>
<SafeInfo <Row margin="md" align="center">
safeAddress={safeAddress} <Img src={getEthAsToken().logoUri} height={28} alt="Ether" onError={setImageToPlaceholder} />
etherScanLink={etherScanLink} <Paragraph size="md" noMargin className={classes.value}>
safeName={safeName} {tx.value || 0}
ethBalance={ethBalance} {' ETH'}
/> </Paragraph>
<Row margin="md"> </Row>
<Col xs={1}> <Row margin="xs">
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} /> <Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
</Col> Data (hex encoded)
<Col xs={11} center="xs" layout="column"> </Paragraph>
<Hairline /> </Row>
</Col> <Row margin="md" align="center">
<Col className={classes.outerData}>
<Row size="md" className={classes.data}>
{tx.data}
</Row> </Row>
<Row margin="xs"> </Col>
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin> </Row>
Recipient </Block>
</Paragraph> <Hairline />
</Row> <Row align="center" className={classes.buttonRow}>
<Row margin="md" align="center"> <Button minWidth={140} onClick={() => setActiveScreen('sendCustomTx')}>
<Col xs={1}> Back
<Identicon address={tx.recipientAddress} diameter={32} /> </Button>
</Col> <Button
<Col xs={11} layout="column"> type="submit"
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin> onClick={submitTx}
{tx.recipientAddress} variant="contained"
<Link to={etherScanLink} target="_blank"> minWidth={140}
<OpenInNew style={openIconStyle} /> color="primary"
</Link> data-testid="submit-tx-btn"
</Paragraph> className={classes.submitButton}
</Col> >
</Row> Submit
<Row margin="xs"> </Button>
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin> </Row>
Value </>
</Paragraph> )
</Row> }
<Row margin="md" align="center">
<Img src={getEthAsToken().logoUri} height={28} alt="Ether" onError={setImageToPlaceholder} />
<Paragraph size="md" noMargin className={classes.value}>
{tx.value || 0}
{' ETH'}
</Paragraph>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Data (hex encoded)
</Paragraph>
</Row>
<Row margin="md" align="center">
<Col className={classes.outerData}>
<Row size="md" className={classes.data}>
{tx.data}
</Row>
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={() => setActiveScreen('sendCustomTx')}>
Back
</Button>
<Button
type="submit"
onClick={submitTx}
variant="contained"
minWidth={140}
color="primary"
data-testid="submit-tx-btn"
className={classes.submitButton}
>
Submit
</Button>
</Row>
</>
)
}}
</SharedSnackbarConsumer>
)
export default withStyles(styles)(ReviewCustomTx)
export default withStyles(styles)(withSnackbar(ReviewCustomTx))

View File

@ -4,7 +4,7 @@ import OpenInNew from '@material-ui/icons/OpenInNew'
import { withStyles } from '@material-ui/core/styles' 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 { SharedSnackbarConsumer } from '~/components/SharedSnackBar' import { withSnackbar } from 'notistack'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import Link from '~/components/layout/Link' import Link from '~/components/layout/Link'
@ -20,6 +20,7 @@ import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens' import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import ArrowDown from '../assets/arrow-down.svg' import ArrowDown from '../assets/arrow-down.svg'
import { secondary } from '~/theme/variables' import { secondary } from '~/theme/variables'
import { isEther } from '~/logic/tokens/utils/tokenHelpers' import { isEther } from '~/logic/tokens/utils/tokenHelpers'
@ -35,6 +36,8 @@ type Props = {
ethBalance: string, ethBalance: string,
tx: Object, tx: Object,
createTransaction: Function, createTransaction: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
const openIconStyle = { const openIconStyle = {
@ -52,111 +55,117 @@ const ReviewTx = ({
ethBalance, ethBalance,
tx, tx,
createTransaction, createTransaction,
}: Props) => ( enqueueSnackbar,
<SharedSnackbarConsumer> closeSnackbar,
{({ openSnackbar }) => { }: Props) => {
const submitTx = async () => { const submitTx = async () => {
const web3 = getWeb3() const web3 = getWeb3()
const isSendingETH = isEther(tx.token.symbol) const isSendingETH = isEther(tx.token.symbol)
const txRecipient = isSendingETH ? tx.recipientAddress : tx.token.address const txRecipient = isSendingETH ? tx.recipientAddress : tx.token.address
let txData = EMPTY_DATA let txData = EMPTY_DATA
let txAmount = web3.utils.toWei(tx.amount, 'ether') let txAmount = web3.utils.toWei(tx.amount, 'ether')
if (!isSendingETH) { if (!isSendingETH) {
const StandardToken = await getStandardTokenContract() const StandardToken = await getStandardTokenContract()
const tokenInstance = await StandardToken.at(tx.token.address) const tokenInstance = await StandardToken.at(tx.token.address)
txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI() txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI()
// txAmount should be 0 if we send tokens // txAmount should be 0 if we send tokens
// the real value is encoded in txData and will be used by the contract // the real value is encoded in txData and will be used by the contract
// if txAmount > 0 it would send ETH from the safe // if txAmount > 0 it would send ETH from the safe
txAmount = 0 txAmount = 0
} }
createTransaction(safeAddress, txRecipient, txAmount, txData, openSnackbar) createTransaction(
onClose() safeAddress,
} txRecipient,
txAmount,
txData,
TX_NOTIFICATION_TYPES.STANDARD_TX,
enqueueSnackbar,
closeSnackbar,
)
onClose()
}
return ( return (
<> <>
<Row align="center" grow className={classes.heading}> <Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin> <Paragraph weight="bolder" className={classes.headingText} noMargin>
Send Funds Send Funds
</Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<SafeInfo
safeAddress={safeAddress}
etherScanLink={etherScanLink}
safeName={safeName}
ethBalance={ethBalance}
/>
<Row margin="md">
<Col xs={1}>
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
</Col>
<Col xs={11} center="xs" layout="column">
<Hairline />
</Col>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Recipient
</Paragraph>
</Row>
<Row margin="md" align="center">
<Col xs={1}>
<Identicon address={tx.recipientAddress} diameter={32} />
</Col>
<Col xs={11} layout="column">
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
{tx.recipientAddress}
<Link to={etherScanLink} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Paragraph> </Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph> </Col>
<IconButton onClick={onClose} disableRipple> </Row>
<Close className={classes.closeIcon} /> <Row margin="xs">
</IconButton> <Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
</Row> Amount
<Hairline /> </Paragraph>
<Block className={classes.container}> </Row>
<SafeInfo <Row margin="md" align="center">
safeAddress={safeAddress} <Img src={tx.token.logoUri} height={28} alt={tx.token.name} onError={setImageToPlaceholder} />
etherScanLink={etherScanLink} <Paragraph size="md" noMargin className={classes.amount}>
safeName={safeName} {tx.amount}
ethBalance={ethBalance} {' '}
/> {tx.token.symbol}
<Row margin="md"> </Paragraph>
<Col xs={1}> </Row>
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} /> </Block>
</Col> <Hairline style={{ position: 'absolute', bottom: 85 }} />
<Col xs={11} center="xs" layout="column"> <Row align="center" className={classes.buttonRow}>
<Hairline /> <Button minWidth={140} onClick={() => setActiveScreen('sendFunds')}>
</Col> Back
</Row> </Button>
<Row margin="xs"> <Button
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin> type="submit"
Recipient onClick={submitTx}
</Paragraph> variant="contained"
</Row> minWidth={140}
<Row margin="md" align="center"> color="primary"
<Col xs={1}> data-testid="submit-tx-btn"
<Identicon address={tx.recipientAddress} diameter={32} /> className={classes.submitButton}
</Col> >
<Col xs={11} layout="column"> Submit
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin> </Button>
{tx.recipientAddress} </Row>
<Link to={etherScanLink} target="_blank"> </>
<OpenInNew style={openIconStyle} /> )
</Link> }
</Paragraph>
</Col>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Amount
</Paragraph>
</Row>
<Row margin="md" align="center">
<Img src={tx.token.logoUri} height={28} alt={tx.token.name} onError={setImageToPlaceholder} />
<Paragraph size="md" noMargin className={classes.amount}>
{tx.amount}
{' '}
{tx.token.symbol}
</Paragraph>
</Row>
</Block>
<Hairline style={{ position: 'absolute', bottom: 85 }} />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={() => setActiveScreen('sendFunds')}>
Back
</Button>
<Button
type="submit"
onClick={submitTx}
variant="contained"
minWidth={140}
color="primary"
data-testid="submit-tx-btn"
className={classes.submitButton}
>
Submit
</Button>
</Row>
</>
)
}}
</SharedSnackbarConsumer>
)
export default withStyles(styles)(ReviewTx) export default withStyles(styles)(withSnackbar(ReviewTx))

View File

@ -1,17 +1,19 @@
// @flow // @flow
import React from 'react' import React from 'react'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import { withSnackbar } from 'notistack'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import Col from '~/components/layout/Col' import Col from '~/components/layout/Col'
import Field from '~/components/forms/Field' import Field from '~/components/forms/Field'
import Heading from '~/components/layout/Heading' import Heading from '~/components/layout/Heading'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import { composeValidators, required, minMaxLength } from '~/components/forms/validator' import { composeValidators, required, minMaxLength } from '~/components/forms/validator'
import TextField from '~/components/forms/TextField' import TextField from '~/components/forms/TextField'
import GnoForm from '~/components/forms/GnoForm' import GnoForm from '~/components/forms/GnoForm'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import Button from '~/components/layout/Button' import Button from '~/components/layout/Button'
import { getNofiticationsFromTxType, showSnackbar } from '~/logic/notifications'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { styles } from './style' import { styles } from './style'
export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input' export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input'
@ -22,17 +24,20 @@ type Props = {
safeAddress: string, safeAddress: string,
safeName: string, safeName: string,
updateSafe: Function, updateSafe: Function,
openSnackbar: Function, enqueueSnackbar: Function,
closeSnackbar: Function,
} }
const ChangeSafeName = (props: Props) => { const ChangeSafeName = (props: Props) => {
const { const {
classes, safeAddress, safeName, updateSafe, openSnackbar, classes, safeAddress, safeName, updateSafe, enqueueSnackbar, closeSnackbar,
} = props } = props
const handleSubmit = (values) => { const handleSubmit = (values) => {
updateSafe({ address: safeAddress, name: values.safeName }) updateSafe({ address: safeAddress, name: values.safeName })
openSnackbar('Safe name changed', 'success')
const notification = getNofiticationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX)
showSnackbar(notification.afterExecution, enqueueSnackbar, closeSnackbar)
} }
return ( return (
@ -80,10 +85,4 @@ const ChangeSafeName = (props: Props) => {
) )
} }
const withSnackbar = (props) => ( export default withStyles(styles)(withSnackbar(ChangeSafeName))
<SharedSnackbarConsumer>
{({ openSnackbar }) => <ChangeSafeName {...props} openSnackbar={openSnackbar} />}
</SharedSnackbarConsumer>
)
export default withStyles(styles)(withSnackbar)

View File

@ -2,10 +2,11 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { List } from 'immutable' import { List } from 'immutable'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' import { withSnackbar } from 'notistack'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
import { type Owner } from '~/routes/safe/store/models/owner' import { type Owner } from '~/routes/safe/store/models/owner'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import OwnerForm from './screens/OwnerForm' import OwnerForm from './screens/OwnerForm'
import ThresholdForm from './screens/ThresholdForm' import ThresholdForm from './screens/ThresholdForm'
import ReviewAddOwner from './screens/Review' import ReviewAddOwner from './screens/Review'
@ -29,6 +30,8 @@ type Props = {
network: string, network: string,
addSafeOwner: Function, addSafeOwner: Function,
createTransaction: Function, createTransaction: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
type ActiveScreen = 'selectOwner' | 'selectThreshold' | 'reviewAddOwner' type ActiveScreen = 'selectOwner' | 'selectThreshold' | 'reviewAddOwner'
@ -36,14 +39,23 @@ export const sendAddOwner = async (
values: Object, values: Object,
safeAddress: string, safeAddress: string,
ownersOld: List<Owner>, ownersOld: List<Owner>,
openSnackbar: Function, enqueueSnackbar: Function,
closeSnackbar: Function,
createTransaction: Function, createTransaction: Function,
addSafeOwner: Function, addSafeOwner: Function,
) => { ) => {
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const txData = gnosisSafe.contract.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI() const txData = gnosisSafe.contract.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
const txHash = await createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar) const txHash = await createTransaction(
safeAddress,
safeAddress,
0,
txData,
TX_NOTIFICATION_TYPES.OWNER_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
)
if (txHash) { if (txHash) {
addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress }) addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress })
@ -61,6 +73,8 @@ const AddOwner = ({
network, network,
createTransaction, createTransaction,
addSafeOwner, addSafeOwner,
enqueueSnackbar,
closeSnackbar,
}: Props) => { }: Props) => {
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('selectOwner') const [activeScreen, setActiveScreen] = useState<ActiveScreen>('selectOwner')
const [values, setValues] = useState<Object>({}) const [values, setValues] = useState<Object>({})
@ -98,59 +112,50 @@ const AddOwner = ({
setActiveScreen('reviewAddOwner') setActiveScreen('reviewAddOwner')
} }
return ( const onAddOwner = async () => {
<> onClose()
<SharedSnackbarConsumer> try {
{({ openSnackbar }) => { sendAddOwner(values, safeAddress, owners, enqueueSnackbar, closeSnackbar, createTransaction, addSafeOwner)
const onAddOwner = async () => { } catch (error) {
onClose() console.error('Error while removing an owner', error)
try { }
sendAddOwner(values, safeAddress, owners, openSnackbar, createTransaction, addSafeOwner) }
} catch (error) {
// eslint-disable-next-line
console.log('Error while removing an owner ' + error)
}
}
return ( return (
<Modal <Modal
title="Add owner to Safe" title="Add owner to Safe"
description="Add owner to Safe" description="Add owner to Safe"
handleClose={onClose} handleClose={onClose}
open={isOpen} open={isOpen}
paperClassName={classes.biggerModalWindow} paperClassName={classes.biggerModalWindow}
> >
<> <>
{activeScreen === 'selectOwner' && ( {activeScreen === 'selectOwner' && (
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} owners={owners} /> <OwnerForm onClose={onClose} onSubmit={ownerSubmitted} owners={owners} />
)} )}
{activeScreen === 'selectThreshold' && ( {activeScreen === 'selectThreshold' && (
<ThresholdForm <ThresholdForm
onClose={onClose} onClose={onClose}
owners={owners} owners={owners}
threshold={threshold} threshold={threshold}
onClickBack={onClickBack} onClickBack={onClickBack}
onSubmit={thresholdSubmitted} onSubmit={thresholdSubmitted}
/> />
)} )}
{activeScreen === 'reviewAddOwner' && ( {activeScreen === 'reviewAddOwner' && (
<ReviewAddOwner <ReviewAddOwner
onClose={onClose} onClose={onClose}
safeName={safeName} safeName={safeName}
owners={owners} owners={owners}
network={network} network={network}
values={values} values={values}
onClickBack={onClickBack} onClickBack={onClickBack}
onSubmit={onAddOwner} onSubmit={onAddOwner}
/> />
)} )}
</> </>
</Modal> </Modal>
)
}}
</SharedSnackbarConsumer>
</>
) )
} }
export default withStyles(styles)(AddOwner) export default withStyles(styles)(withSnackbar(AddOwner))

View File

@ -1,5 +1,6 @@
// @flow // @flow
import React from 'react' import React from 'react'
import { withSnackbar } from 'notistack'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import OpenInNew from '@material-ui/icons/OpenInNew' import OpenInNew from '@material-ui/icons/OpenInNew'
@ -14,8 +15,10 @@ import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField' import TextField from '~/components/forms/TextField'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import Identicon from '~/components/Identicon' import Identicon from '~/components/Identicon'
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import { composeValidators, required, minMaxLength } from '~/components/forms/validator' import { composeValidators, required, minMaxLength } from '~/components/forms/validator'
import { getNofiticationsFromTxType, showSnackbar } from '~/logic/notifications'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
import { styles } from './style' import { styles } from './style'
import { secondary } from '~/theme/variables' import { secondary } from '~/theme/variables'
@ -37,6 +40,8 @@ type Props = {
network: string, network: string,
selectedOwnerName: string, selectedOwnerName: string,
editSafeOwner: Function, editSafeOwner: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
const EditOwnerComponent = ({ const EditOwnerComponent = ({
@ -48,9 +53,15 @@ const EditOwnerComponent = ({
selectedOwnerName, selectedOwnerName,
editSafeOwner, editSafeOwner,
network, network,
enqueueSnackbar,
closeSnackbar,
}: Props) => { }: Props) => {
const handleSubmit = (values) => { const handleSubmit = (values) => {
editSafeOwner({ safeAddress, ownerAddress, ownerName: values.ownerName }) editSafeOwner({ safeAddress, ownerAddress, ownerName: values.ownerName })
const notification = getNofiticationsFromTxType(TX_NOTIFICATION_TYPES.OWNER_NAME_CHANGE_TX)
showSnackbar(notification.afterExecution, enqueueSnackbar, closeSnackbar)
onClose() onClose()
} }
@ -116,6 +127,6 @@ const EditOwnerComponent = ({
) )
} }
const EditOwnerModal = withStyles(styles)(EditOwnerComponent) const EditOwnerModal = withStyles(styles)(withSnackbar(EditOwnerComponent))
export default EditOwnerModal export default EditOwnerModal

View File

@ -2,10 +2,11 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { List } from 'immutable' import { List } from 'immutable'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' import { withSnackbar } from 'notistack'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
import { type Owner } from '~/routes/safe/store/models/owner' import { type Owner } from '~/routes/safe/store/models/owner'
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import CheckOwner from './screens/CheckOwner' import CheckOwner from './screens/CheckOwner'
import ThresholdForm from './screens/ThresholdForm' import ThresholdForm from './screens/ThresholdForm'
import ReviewRemoveOwner from './screens/Review' import ReviewRemoveOwner from './screens/Review'
@ -31,7 +32,10 @@ type Props = {
network: string, network: string,
createTransaction: Function, createTransaction: Function,
removeSafeOwner: Function, removeSafeOwner: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
type ActiveScreen = 'checkOwner' | 'selectThreshold' | 'reviewRemoveOwner' type ActiveScreen = 'checkOwner' | 'selectThreshold' | 'reviewRemoveOwner'
export const sendRemoveOwner = async ( export const sendRemoveOwner = async (
@ -40,7 +44,8 @@ export const sendRemoveOwner = async (
ownerAddressToRemove: string, ownerAddressToRemove: string,
ownerNameToRemove: string, ownerNameToRemove: string,
ownersOld: List<Owner>, ownersOld: List<Owner>,
openSnackbar: Function, enqueueSnackbar: Function,
closeSnackbar: Function,
createTransaction: Function, createTransaction: Function,
removeSafeOwner: Function, removeSafeOwner: Function,
) => { ) => {
@ -54,7 +59,15 @@ export const sendRemoveOwner = async (
.removeOwner(prevAddress, ownerAddressToRemove, values.threshold) .removeOwner(prevAddress, ownerAddressToRemove, values.threshold)
.encodeABI() .encodeABI()
const txHash = await createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar) const txHash = await createTransaction(
safeAddress,
safeAddress,
0,
txData,
TX_NOTIFICATION_TYPES.OWNER_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
)
if (txHash) { if (txHash) {
removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove }) removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove })
@ -74,6 +87,8 @@ const RemoveOwner = ({
network, network,
createTransaction, createTransaction,
removeSafeOwner, removeSafeOwner,
enqueueSnackbar,
closeSnackbar,
}: Props) => { }: Props) => {
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('checkOwner') const [activeScreen, setActiveScreen] = useState<ActiveScreen>('checkOwner')
const [values, setValues] = useState<Object>({}) const [values, setValues] = useState<Object>({})
@ -104,71 +119,64 @@ const RemoveOwner = ({
setActiveScreen('reviewRemoveOwner') setActiveScreen('reviewRemoveOwner')
} }
return ( const onRemoveOwner = () => {
<> onClose()
<SharedSnackbarConsumer> sendRemoveOwner(
{({ openSnackbar }) => { values,
const onRemoveOwner = () => { safeAddress,
onClose() ownerAddress,
sendRemoveOwner( ownerName,
values, owners,
safeAddress, enqueueSnackbar,
ownerAddress, closeSnackbar,
ownerName, createTransaction,
owners, removeSafeOwner,
openSnackbar, )
createTransaction, }
removeSafeOwner,
)
}
return ( return (
<Modal <Modal
title="Remove owner from Safe" title="Remove owner from Safe"
description="Remove owner from Safe" description="Remove owner from Safe"
handleClose={onClose} handleClose={onClose}
open={isOpen} open={isOpen}
paperClassName={classes.biggerModalWindow} paperClassName={classes.biggerModalWindow}
> >
<> <>
{activeScreen === 'checkOwner' && ( {activeScreen === 'checkOwner' && (
<CheckOwner <CheckOwner
onClose={onClose} onClose={onClose}
ownerAddress={ownerAddress} ownerAddress={ownerAddress}
ownerName={ownerName} ownerName={ownerName}
network={network} network={network}
onSubmit={ownerSubmitted} onSubmit={ownerSubmitted}
/> />
)} )}
{activeScreen === 'selectThreshold' && ( {activeScreen === 'selectThreshold' && (
<ThresholdForm <ThresholdForm
onClose={onClose} onClose={onClose}
owners={owners} owners={owners}
threshold={threshold} threshold={threshold}
onClickBack={onClickBack} onClickBack={onClickBack}
onSubmit={thresholdSubmitted} onSubmit={thresholdSubmitted}
/> />
)} )}
{activeScreen === 'reviewRemoveOwner' && ( {activeScreen === 'reviewRemoveOwner' && (
<ReviewRemoveOwner <ReviewRemoveOwner
onClose={onClose} onClose={onClose}
safeName={safeName} safeName={safeName}
owners={owners} owners={owners}
network={network} network={network}
values={values} values={values}
ownerAddress={ownerAddress} ownerAddress={ownerAddress}
ownerName={ownerName} ownerName={ownerName}
onClickBack={onClickBack} onClickBack={onClickBack}
onSubmit={onRemoveOwner} onSubmit={onRemoveOwner}
/> />
)} )}
</> </>
</Modal> </Modal>
)
}}
</SharedSnackbarConsumer>
</>
) )
} }
export default withStyles(styles)(RemoveOwner) export default withStyles(styles)(withSnackbar(RemoveOwner))

View File

@ -2,8 +2,9 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { List } from 'immutable' import { List } from 'immutable'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' import { withSnackbar } from 'notistack'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts'
import OwnerForm from './screens/OwnerForm' import OwnerForm from './screens/OwnerForm'
import ReviewReplaceOwner from './screens/Review' import ReviewReplaceOwner from './screens/Review'
@ -29,6 +30,8 @@ type Props = {
threshold: string, threshold: string,
createTransaction: Function, createTransaction: Function,
replaceSafeOwner: Function, replaceSafeOwner: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
type ActiveScreen = 'checkOwner' | 'reviewReplaceOwner' type ActiveScreen = 'checkOwner' | 'reviewReplaceOwner'
@ -36,7 +39,8 @@ export const sendReplaceOwner = async (
values: Object, values: Object,
safeAddress: string, safeAddress: string,
ownerAddressToRemove: string, ownerAddressToRemove: string,
openSnackbar: Function, enqueueSnackbar: Function,
closeSnackbar: Function,
createTransaction: Function, createTransaction: Function,
replaceSafeOwner: Function, replaceSafeOwner: Function,
) => { ) => {
@ -50,7 +54,15 @@ export const sendReplaceOwner = async (
.swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress) .swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress)
.encodeABI() .encodeABI()
const txHash = await createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar) const txHash = await createTransaction(
safeAddress,
safeAddress,
0,
txData,
TX_NOTIFICATION_TYPES.OWNER_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
)
if (txHash) { if (txHash) {
replaceSafeOwner({ replaceSafeOwner({
@ -75,6 +87,8 @@ const ReplaceOwner = ({
threshold, threshold,
createTransaction, createTransaction,
replaceSafeOwner, replaceSafeOwner,
enqueueSnackbar,
closeSnackbar,
}: Props) => { }: Props) => {
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('checkOwner') const [activeScreen, setActiveScreen] = useState<ActiveScreen>('checkOwner')
const [values, setValues] = useState<Object>({}) const [values, setValues] = useState<Object>({})
@ -96,67 +110,59 @@ const ReplaceOwner = ({
setActiveScreen('reviewReplaceOwner') setActiveScreen('reviewReplaceOwner')
} }
return ( const onReplaceOwner = () => {
<> onClose()
<SharedSnackbarConsumer> try {
{({ openSnackbar }) => { sendReplaceOwner(
const onReplaceOwner = () => { values,
onClose() safeAddress,
try { ownerAddress,
sendReplaceOwner( enqueueSnackbar,
values, closeSnackbar,
safeAddress, createTransaction,
ownerAddress, replaceSafeOwner,
openSnackbar, )
createTransaction, } catch (error) {
replaceSafeOwner, console.error('Error while removing an owner', error)
) }
} catch (error) { }
// eslint-disable-next-line
console.log('Error while removing an owner ' + error)
}
}
return ( return (
<Modal <Modal
title="Replace owner from Safe" title="Replace owner from Safe"
description="Replace owner from Safe" description="Replace owner from Safe"
handleClose={onClose} handleClose={onClose}
open={isOpen} open={isOpen}
paperClassName={classes.biggerModalWindow} paperClassName={classes.biggerModalWindow}
> >
<> <>
{activeScreen === 'checkOwner' && ( {activeScreen === 'checkOwner' && (
<OwnerForm <OwnerForm
onClose={onClose} onClose={onClose}
ownerAddress={ownerAddress} ownerAddress={ownerAddress}
ownerName={ownerName} ownerName={ownerName}
owners={owners} owners={owners}
network={network} network={network}
onSubmit={ownerSubmitted} onSubmit={ownerSubmitted}
/> />
)} )}
{activeScreen === 'reviewReplaceOwner' && ( {activeScreen === 'reviewReplaceOwner' && (
<ReviewReplaceOwner <ReviewReplaceOwner
onClose={onClose} onClose={onClose}
safeName={safeName} safeName={safeName}
owners={owners} owners={owners}
network={network} network={network}
values={values} values={values}
ownerAddress={ownerAddress} ownerAddress={ownerAddress}
ownerName={ownerName} ownerName={ownerName}
onClickBack={onClickBack} onClickBack={onClickBack}
onSubmit={onReplaceOwner} onSubmit={onReplaceOwner}
threshold={threshold} threshold={threshold}
/> />
)} )}
</> </>
</Modal> </Modal>
)
}}
</SharedSnackbarConsumer>
</>
) )
} }
export default withStyles(styles)(ReplaceOwner) export default withStyles(styles)(withSnackbar(ReplaceOwner))

View File

@ -2,7 +2,7 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import { List } from 'immutable' import { List } from 'immutable'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' import { withSnackbar } from 'notistack'
import Heading from '~/components/layout/Heading' import Heading from '~/components/layout/Heading'
import Button from '~/components/layout/Button' import Button from '~/components/layout/Button'
import Bold from '~/components/layout/Bold' import Bold from '~/components/layout/Bold'
@ -10,10 +10,11 @@ import Block from '~/components/layout/Block'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import ChangeThreshold from './ChangeThreshold' import ChangeThreshold from './ChangeThreshold'
import type { Owner } from '~/routes/safe/store/models/owner' import type { Owner } from '~/routes/safe/store/models/owner'
import { styles } from './style'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { styles } from './style'
type Props = { type Props = {
owners: List<Owner>, owners: List<Owner>,
@ -22,10 +23,19 @@ type Props = {
createTransaction: Function, createTransaction: Function,
safeAddress: string, safeAddress: string,
granted: boolean, granted: boolean,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
const ThresholdSettings = ({ const ThresholdSettings = ({
owners, threshold, classes, createTransaction, safeAddress, granted, owners,
threshold,
classes,
createTransaction,
safeAddress,
granted,
enqueueSnackbar,
closeSnackbar,
}: Props) => { }: Props) => {
const [isModalOpen, setModalOpen] = useState(false) const [isModalOpen, setModalOpen] = useState(false)
@ -33,66 +43,66 @@ const ThresholdSettings = ({
setModalOpen((prevOpen) => !prevOpen) setModalOpen((prevOpen) => !prevOpen)
} }
const onChangeThreshold = async (newThreshold) => {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI()
createTransaction(
safeAddress,
safeAddress,
0,
txData,
TX_NOTIFICATION_TYPES.THRESHOLD_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
)
}
return ( return (
<> <>
<SharedSnackbarConsumer> <Block className={classes.container}>
{({ openSnackbar }) => { <Heading tag="h2">Required confirmations</Heading>
const onChangeThreshold = async (newThreshold) => { <Paragraph>
const safeInstance = await getGnosisSafeInstanceAt(safeAddress) Any transaction requires the confirmation of:
const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI() </Paragraph>
<Paragraph size="lg" className={classes.ownersText}>
createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar) <Bold>{threshold}</Bold>
} {' '}
out of
return ( {' '}
<> <Bold>{owners.size}</Bold>
<Block className={classes.container}> {' '}
<Heading tag="h2">Required confirmations</Heading> owners
<Paragraph> </Paragraph>
Any transaction requires the confirmation of: {owners.size > 1 && granted && (
</Paragraph> <Row className={classes.buttonRow}>
<Paragraph size="lg" className={classes.ownersText}> <Button
<Bold>{threshold}</Bold> color="primary"
{' '} minWidth={120}
out of className={classes.modifyBtn}
{' '} onClick={toggleModal}
<Bold>{owners.size}</Bold> variant="contained"
{' '} >
owners Modify
</Paragraph> </Button>
{owners.size > 1 && granted && ( </Row>
<Row className={classes.buttonRow}> )}
<Button </Block>
color="primary" <Modal
minWidth={120} title="Change Required Confirmations"
className={classes.modifyBtn} description="Change Required Confirmations Form"
onClick={toggleModal} handleClose={toggleModal}
variant="contained" open={isModalOpen}
> >
Modify <ChangeThreshold
</Button> onClose={toggleModal}
</Row> owners={owners}
)} threshold={threshold}
</Block> onChangeThreshold={onChangeThreshold}
<Modal />
title="Change Required Confirmations" </Modal>
description="Change Required Confirmations Form"
handleClose={toggleModal}
open={isModalOpen}
>
<ChangeThreshold
onClose={toggleModal}
owners={owners}
threshold={threshold}
onChangeThreshold={onChangeThreshold}
/>
</Modal>
</>
)
}}
</SharedSnackbarConsumer>
</> </>
) )
} }
export default withStyles(styles)(ThresholdSettings) export default withStyles(styles)(withSnackbar(ThresholdSettings))

View File

@ -5,7 +5,7 @@ import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import FormControlLabel from '@material-ui/core/FormControlLabel' import FormControlLabel from '@material-ui/core/FormControlLabel'
import Checkbox from '@material-ui/core/Checkbox' import Checkbox from '@material-ui/core/Checkbox'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' import { withSnackbar } from 'notistack'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
import Hairline from '~/components/layout/Hairline' import Hairline from '~/components/layout/Hairline'
import Button from '~/components/layout/Button' import Button from '~/components/layout/Button'
@ -13,6 +13,7 @@ import Row from '~/components/layout/Row'
import Bold from '~/components/layout/Bold' import Bold from '~/components/layout/Bold'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { type Transaction } from '~/routes/safe/store/models/transaction' import { type Transaction } from '~/routes/safe/store/models/transaction'
import { styles } from './style' import { styles } from './style'
@ -29,6 +30,8 @@ type Props = {
threshold: number, threshold: number,
thresholdReached: boolean, thresholdReached: boolean,
userAddress: string, userAddress: string,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
const getModalTitleAndDescription = (thresholdReached: boolean) => { const getModalTitleAndDescription = (thresholdReached: boolean) => {
@ -55,6 +58,8 @@ const ApproveTxModal = ({
threshold, threshold,
thresholdReached, thresholdReached,
userAddress, userAddress,
enqueueSnackbar,
closeSnackbar,
}: Props) => { }: Props) => {
const [approveAndExecute, setApproveAndExecute] = useState<boolean>(false) const [approveAndExecute, setApproveAndExecute] = useState<boolean>(false)
const { title, description } = getModalTitleAndDescription(thresholdReached) const { title, description } = getModalTitleAndDescription(thresholdReached)
@ -62,68 +67,70 @@ const ApproveTxModal = ({
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute) const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
return ( const approveTx = () => {
<SharedSnackbarConsumer> processTransaction(
{({ openSnackbar }) => { safeAddress,
const approveTx = () => { tx,
processTransaction(safeAddress, tx, openSnackbar, userAddress, approveAndExecute) userAddress,
onClose() TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
} enqueueSnackbar,
closeSnackbar,
approveAndExecute,
)
onClose()
}
return ( return (
<Modal title={title} description={description} handleClose={onClose} open={isOpen}> <Modal title={title} description={description} handleClose={onClose} open={isOpen}>
<Row align="center" grow className={classes.heading}> <Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin> <Paragraph weight="bolder" className={classes.headingText} noMargin>
{title} {title}
</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<Row>
<Paragraph>{description}</Paragraph>
<Paragraph size="sm" color="medium">
Transaction nonce:
<br />
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
</Paragraph>
{!thresholdReached && oneConfirmationLeft && (
<>
<Paragraph color="error">
Approving transaction does not execute it immediately. If you want to approve and execute the
transaction right away, click on checkbox below.
</Paragraph> </Paragraph>
<IconButton onClick={onClose} disableRipple> <FormControlLabel
<Close className={classes.closeIcon} /> control={<Checkbox onChange={handleExecuteCheckbox} checked={approveAndExecute} color="primary" />}
</IconButton> label="Execute transaction"
</Row> />
<Hairline /> </>
<Block className={classes.container}> )}
<Row> </Row>
<Paragraph>{description}</Paragraph> </Block>
<Paragraph size="sm" color="medium"> <Row align="center" className={classes.buttonRow}>
Transaction nonce: <Button minWidth={140} minHeight={42} onClick={onClose}>
<br /> Exit
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold> </Button>
</Paragraph> <Button
{!thresholdReached && oneConfirmationLeft && ( type="submit"
<> variant="contained"
<Paragraph color="error"> minWidth={214}
Approving transaction does not execute it immediately. If you want to approve and execute the minHeight={42}
transaction right away, click on checkbox below. color="primary"
</Paragraph> onClick={approveTx}
<FormControlLabel testId={APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID}
control={<Checkbox onChange={handleExecuteCheckbox} checked={approveAndExecute} color="primary" />} >
label="Execute transaction" {title}
/> </Button>
</> </Row>
)} </Modal>
</Row>
</Block>
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Exit
</Button>
<Button
type="submit"
variant="contained"
minWidth={214}
minHeight={42}
color="primary"
onClick={approveTx}
testId={APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID}
>
{title}
</Button>
</Row>
</Modal>
)
}}
</SharedSnackbarConsumer>
) )
} }
export default withStyles(styles)(ApproveTxModal) export default withStyles(styles)(withSnackbar(ApproveTxModal))

View File

@ -3,7 +3,7 @@ import React from 'react'
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 { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' import { withSnackbar } from 'notistack'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
import Hairline from '~/components/layout/Hairline' import Hairline from '~/components/layout/Hairline'
import Button from '~/components/layout/Button' import Button from '~/components/layout/Button'
@ -13,6 +13,7 @@ import Block from '~/components/layout/Block'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import { type Transaction } from '~/routes/safe/store/models/transaction' import { type Transaction } from '~/routes/safe/store/models/transaction'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { styles } from './style' import { styles } from './style'
type Props = { type Props = {
@ -22,67 +23,80 @@ type Props = {
createTransaction: Function, createTransaction: Function,
tx: Transaction, tx: Transaction,
safeAddress: string, safeAddress: string,
enqueueSnackbar: Function,
closeSnackbar: Function,
} }
const CancelTxModal = ({ const CancelTxModal = ({
onClose, isOpen, classes, createTransaction, tx, safeAddress, onClose,
}: Props) => ( isOpen,
<SharedSnackbarConsumer> classes,
{({ openSnackbar }) => { createTransaction,
const sendReplacementTransaction = () => { tx,
createTransaction(safeAddress, safeAddress, 0, EMPTY_DATA, openSnackbar) safeAddress,
onClose() enqueueSnackbar,
} closeSnackbar,
}: Props) => {
const sendReplacementTransaction = () => {
createTransaction(
safeAddress,
safeAddress,
0,
EMPTY_DATA,
TX_NOTIFICATION_TYPES.CANCELLATION_TX,
enqueueSnackbar,
closeSnackbar,
)
onClose()
}
return ( return (
<Modal <Modal
title="Cancel Transaction" title="Cancel Transaction"
description="Cancel Transaction" description="Cancel Transaction"
handleClose={onClose} handleClose={onClose}
open={isOpen} open={isOpen}
// paperClassName={cn(smallerModalSize && classes.smallerModalWindow)} // paperClassName={cn(smallerModalSize && classes.smallerModalWindow)}
>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin>
Cancel transaction
</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<Row>
<Paragraph>
This action will cancel this transaction. A separate transaction will be performed to submit the
cancellation.
</Paragraph>
<Paragraph size="sm" color="medium">
Transaction nonce:
<br />
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
</Paragraph>
</Row>
</Block>
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Exit
</Button>
<Button
type="submit"
variant="contained"
minWidth={214}
minHeight={42}
color="secondary"
onClick={sendReplacementTransaction}
> >
<Row align="center" grow className={classes.heading}> Cancel Transaction
<Paragraph weight="bolder" className={classes.headingText} noMargin> </Button>
Cancel transaction </Row>
</Paragraph> </Modal>
<IconButton onClick={onClose} disableRipple> )
<Close className={classes.closeIcon} /> }
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<Row>
<Paragraph>
This action will cancel this transaction. A separate transaction will be performed to submit the
cancellation.
</Paragraph>
<Paragraph size="sm" color="medium">
Transaction nonce:
<br />
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
</Paragraph>
</Row>
</Block>
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Exit
</Button>
<Button
type="submit"
variant="contained"
minWidth={214}
minHeight={42}
color="secondary"
onClick={sendReplacementTransaction}
>
Cancel Transaction
</Button>
</Row>
</Modal>
)
}}
</SharedSnackbarConsumer>
)
export default withStyles(styles)(CancelTxModal) export default withStyles(styles)(withSnackbar(CancelTxModal))

View File

@ -24,10 +24,7 @@ export const addSafe = createAction<string, Function, ActionReturn>(ADD_SAFE, (s
safe, safe,
})) }))
const saveSafe = (safe: Safe) => ( const saveSafe = (safe: Safe) => (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
dispatch: ReduxDispatch<GlobalState>,
getState: GetState<GlobalState>,
) => {
const state = getState() const state = getState()
const safeList = safesListSelector(state) const safeList = safesListSelector(state)

View File

@ -2,4 +2,5 @@
import { createAction } from 'redux-actions' import { createAction } from 'redux-actions'
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS' export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS) export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)

View File

@ -9,21 +9,29 @@ import {
getApprovalTransaction, getApprovalTransaction,
getExecutionTransaction, getExecutionTransaction,
CALL, CALL,
type Notifications, type NotifiedTransaction,
DEFAULT_NOTIFICATIONS,
TX_TYPE_CONFIRMATION, TX_TYPE_CONFIRMATION,
TX_TYPE_EXECUTION, TX_TYPE_EXECUTION,
saveTxToHistory, saveTxToHistory,
} from '~/logic/safe/transactions' } from '~/logic/safe/transactions'
import {
type Notification,
type NotificationsQueue,
getNofiticationsFromTxType,
showSnackbar,
} from '~/logic/notifications'
import { getErrorMessage } from '~/test/utils/ethereumErrors'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
const createTransaction = ( const createTransaction = (
safeAddress: string, safeAddress: string,
to: string, to: string,
valueInWei: string, valueInWei: string,
txData: string = EMPTY_DATA, txData: string = EMPTY_DATA,
openSnackbar: Function, notifiedTransaction: NotifiedTransaction,
enqueueSnackbar: Function,
closeSnackbar: Function,
shouldExecute?: boolean, shouldExecute?: boolean,
notifications?: Notifications = DEFAULT_NOTIFICATIONS,
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => { ) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
const state: GlobalState = getState() const state: GlobalState = getState()
@ -33,19 +41,26 @@ const createTransaction = (
const nonce = (await safeInstance.nonce()).toString() const nonce = (await safeInstance.nonce()).toString()
const isExecution = threshold.toNumber() === 1 || shouldExecute const isExecution = threshold.toNumber() === 1 || shouldExecute
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
const sigs = `0x000000000000000000000000${from.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
const notificationsQueue: NotificationsQueue = getNofiticationsFromTxType(notifiedTransaction)
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
let pendingExecutionKey
let txHash let txHash
let tx let tx
try { try {
if (isExecution) { if (isExecution) {
tx = await getExecutionTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from) tx = await getExecutionTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from, sigs)
} else { } else {
tx = await getApprovalTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from) tx = await getApprovalTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
} }
const sendParams = { const sendParams = { from }
from,
}
// if not set owner management tests will fail on ganache // if not set owner management tests will fail on ganache
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000' sendParams.gas = '7000000'
@ -55,12 +70,21 @@ const createTransaction = (
.send(sendParams) .send(sendParams)
.once('transactionHash', (hash) => { .once('transactionHash', (hash) => {
txHash = hash txHash = hash
openSnackbar(notifications.BEFORE_EXECUTION_OR_CREATION, 'success') closeSnackbar(beforeExecutionKey)
const pendingExecutionNotification: Notification = isExecution ? {
message: notificationsQueue.pendingExecution.noMoreConfirmationsNeeded.message,
options: notificationsQueue.pendingExecution.noMoreConfirmationsNeeded.options,
} : {
message: notificationsQueue.pendingExecution.moreConfirmationsNeeded.message,
options: notificationsQueue.pendingExecution.moreConfirmationsNeeded.options,
}
pendingExecutionKey = showSnackbar(pendingExecutionNotification, enqueueSnackbar, closeSnackbar)
}) })
.on('error', (error) => { .on('error', (error) => {
console.error('Tx error: ', error) console.error('Tx error: ', error)
}) })
.then(async (receipt) => { .then(async (receipt) => {
closeSnackbar(pendingExecutionKey)
await saveTxToHistory( await saveTxToHistory(
safeInstance, safeInstance,
to, to,
@ -72,17 +96,22 @@ const createTransaction = (
from, from,
isExecution ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION, isExecution ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
) )
if (isExecution) {
showSnackbar(notificationsQueue.afterExecution, enqueueSnackbar, closeSnackbar)
}
return receipt.transactionHash return receipt.transactionHash
}) })
openSnackbar(
isExecution ? notifications.AFTER_EXECUTION : notifications.CREATED_MORE_CONFIRMATIONS_NEEDED,
'success',
)
} catch (err) { } catch (err) {
openSnackbar(notifications.ERROR, 'error') closeSnackbar(beforeExecutionKey)
console.error(`Error while creating transaction: ${err}`) closeSnackbar(pendingExecutionKey)
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
const executeDataUsedSignatures = safeInstance.contract.methods
.execTransaction(to, valueInWei, txData, CALL, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
.encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeDataUsedSignatures, from)
console.error(`Error executing the TX: ${errMsg}`)
} }
dispatch(fetchTransactions(safeAddress)) dispatch(fetchTransactions(safeAddress))

View File

@ -14,7 +14,7 @@ import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { addTransactions } from './addTransactions' import { addTransactions } from './addTransactions'
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens' import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
import { isTokenTransfer } from '~/logic/tokens/utils/tokenHelpers' import { isTokenTransfer } from '~/logic/tokens/utils/tokenHelpers'
import { TX_TYPE_EXECUTION } from '~/logic/safe/transactions/send' import { TX_TYPE_EXECUTION } from '~/logic/safe/transactions'
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds' import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
let web3 let web3

View File

@ -13,6 +13,13 @@ import {
TX_TYPE_EXECUTION, TX_TYPE_EXECUTION,
TX_TYPE_CONFIRMATION, TX_TYPE_CONFIRMATION,
} from '~/logic/safe/transactions' } from '~/logic/safe/transactions'
import {
type Notification,
type NotificationsQueue,
getNofiticationsFromTxType,
showSnackbar,
} from '~/logic/notifications'
import { getErrorMessage } from '~/test/utils/ethereumErrors'
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures // https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
// https://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26 // https://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26
@ -38,8 +45,10 @@ const generateSignaturesFromTxConfirmations = (tx: Transaction, preApprovingOwne
const processTransaction = ( const processTransaction = (
safeAddress: string, safeAddress: string,
tx: Transaction, tx: Transaction,
openSnackbar: Function,
userAddress: string, userAddress: string,
notifiedTransaction: NotifiedTransaction,
enqueueSnackbar: Function,
closeSnackbar: Function,
approveAndExecute?: boolean, approveAndExecute?: boolean,
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => { ) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
const state: GlobalState = getState() const state: GlobalState = getState()
@ -49,54 +58,84 @@ const processTransaction = (
const nonce = (await safeInstance.nonce()).toString() const nonce = (await safeInstance.nonce()).toString()
const threshold = (await safeInstance.getThreshold()).toNumber() const threshold = (await safeInstance.getThreshold()).toNumber()
const shouldExecute = threshold === tx.confirmations.size || approveAndExecute const shouldExecute = threshold === tx.confirmations.size || approveAndExecute
const sigs = generateSignaturesFromTxConfirmations(tx, approveAndExecute && userAddress)
let sigs = generateSignaturesFromTxConfirmations(tx, approveAndExecute && userAddress)
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
if (!sigs) {
sigs = `0x000000000000000000000000${from.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
}
const notificationsQueue: NotificationsQueue = getNofiticationsFromTxType(notifiedTransaction)
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
let pendingExecutionKey
let txHash let txHash
let transaction let transaction
if (shouldExecute) { try {
transaction = await getExecutionTransaction(safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, from, sigs) if (shouldExecute) {
} else { transaction = await getExecutionTransaction(
transaction = await getApprovalTransaction(safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, from)
}
const sendParams = {
from,
}
// if not set owner management tests will fail on ganache
if (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000'
}
await transaction
.send(sendParams)
.once('transactionHash', (hash) => {
txHash = hash
openSnackbar(
shouldExecute ? 'Transaction has been submitted' : 'Approval transaction has been submitted',
'success',
)
})
.on('error', (error) => {
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
await saveTxToHistory(
safeInstance, safeInstance,
tx.recipient, tx.recipient,
tx.value, tx.value,
tx.data, tx.data,
CALL, CALL,
nonce, nonce,
receipt.transactionHash,
from, from,
shouldExecute ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION, sigs,
) )
} else {
transaction = await getApprovalTransaction(safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, from)
}
return receipt.transactionHash const sendParams = { from }
}) // if not set owner management tests will fail on ganache
if (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000'
}
openSnackbar(shouldExecute ? 'Transaction has been confirmed' : 'Approval transaction has been confirmed', 'success') await transaction
.send(sendParams)
.once('transactionHash', (hash) => {
txHash = hash
closeSnackbar(beforeExecutionKey)
const notification: Notification = {
message: notificationsQueue.pendingExecution.noMoreConfirmationsNeeded.message,
options: notificationsQueue.pendingExecution.noMoreConfirmationsNeeded.options,
}
pendingExecutionKey = showSnackbar(notification, enqueueSnackbar, closeSnackbar)
})
.on('error', (error) => {
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
closeSnackbar(pendingExecutionKey)
await saveTxToHistory(
safeInstance,
tx.recipient,
tx.value,
tx.data,
CALL,
nonce,
receipt.transactionHash,
from,
shouldExecute ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
)
showSnackbar(notificationsQueue.afterExecution, enqueueSnackbar, closeSnackbar)
return receipt.transactionHash
})
} catch (err) {
closeSnackbar(beforeExecutionKey)
closeSnackbar(pendingExecutionKey)
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
const executeData = safeInstance.contract.methods.approveHash(txHash).encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeData, from)
console.error(`Error executing the TX: ${errMsg}`)
}
dispatch(fetchTransactions(safeAddress)) dispatch(fetchTransactions(safeAddress))

View File

@ -5,14 +5,18 @@ import {
combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store, combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store,
} from 'redux' } from 'redux'
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/logic/wallets/store/reducer/provider'
import safe, { SAFE_REDUCER_ID, type SafeReducerState as SafeState } from '~/routes/safe/store/reducer/safe' import safe, { SAFE_REDUCER_ID, type SafeReducerState as SafeState } from '~/routes/safe/store/reducer/safe'
import safeStorage from '~/routes/safe/store/middleware/safeStorage' import safeStorage from '~/routes/safe/store/middleware/safeStorage'
import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/logic/tokens/store/reducer/tokens'
import transactions, { import transactions, {
type State as TransactionsState, type State as TransactionsState,
TRANSACTIONS_REDUCER_ID, TRANSACTIONS_REDUCER_ID,
} from '~/routes/safe/store/reducer/transactions' } from '~/routes/safe/store/reducer/transactions'
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/logic/wallets/store/reducer/provider'
import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/logic/tokens/store/reducer/tokens'
import notifications, {
NOTIFICATIONS_REDUCER_ID,
type State as NotificationsState,
} from '~/logic/notifications/store/reducer/notifications'
export const history = createBrowserHistory() export const history = createBrowserHistory()
@ -25,6 +29,7 @@ export type GlobalState = {
safes: SafeState, safes: SafeState,
tokens: TokensState, tokens: TokensState,
transactions: TransactionsState, transactions: TransactionsState,
notifications: NotificationsState,
} }
export type GetState = () => GlobalState export type GetState = () => GlobalState
@ -35,11 +40,13 @@ const reducers: Reducer<GlobalState> = combineReducers({
[SAFE_REDUCER_ID]: safe, [SAFE_REDUCER_ID]: safe,
[TOKEN_REDUCER_ID]: tokens, [TOKEN_REDUCER_ID]: tokens,
[TRANSACTIONS_REDUCER_ID]: transactions, [TRANSACTIONS_REDUCER_ID]: transactions,
[NOTIFICATIONS_REDUCER_ID]: notifications,
}) })
export const store: Store<GlobalState> = createStore( export const store: Store<GlobalState> = createStore(reducers, finalCreateStore)
export const aNewStore = (localState?: Object): Store<GlobalState> => createStore(
reducers, reducers,
localState,
finalCreateStore, finalCreateStore,
) )
export const aNewStore = (localState?: Object): Store<GlobalState> => createStore(reducers, localState, finalCreateStore)

View File

@ -199,18 +199,27 @@ export default createMuiTheme({
}, },
}, },
}, },
MuiSnackbarContent: {
root: {
boxShadow: '0 0 10px 0 rgba(33, 48, 77, 0.1)',
borderRadius: '3px',
color: primary,
},
},
MuiSvgIcon: { MuiSvgIcon: {
colorSecondary: { colorSecondary: {
color: secondaryText, color: secondaryText,
}, },
}, },
MuiSnackbar: {
root: {
width: '280px',
},
},
MuiSnackbarContent: {
message: {
maxWidth: '260px',
'& img': {
marginRight: '5px',
},
},
action: {
paddingLeft: 0,
},
},
MuiTab: { MuiTab: {
root: { root: {
fontFamily: 'Averta, monospace', fontFamily: 'Averta, monospace',

View File

@ -15,8 +15,7 @@ export const storeSignature = async (safeAddress: string, nonce: number, signatu
const updatedSubjects = subjects.set(key, signatures) const updatedSubjects = subjects.set(key, signatures)
await saveToStorage(signaturesKey, updatedSubjects) await saveToStorage(signaturesKey, updatedSubjects)
} catch (err) { } catch (err) {
// eslint-disable-next-line console.error('Error storing signatures in localstorage', err)
console.log('Error storing signatures in localstorage')
} }
} }

View File

@ -12,8 +12,7 @@ export const storeSubject = async (safeAddress: string, nonce: number, subject:
const updatedSubjects = subjects.set(nonce, subject) const updatedSubjects = subjects.set(nonce, subject)
saveToStorage(key, updatedSubjects) saveToStorage(key, updatedSubjects)
} catch (err) { } catch (err) {
// eslint-disable-next-line console.error('Error storing transaction subject in localstorage', err)
console.log('Error storing transaction subject in localstorage')
} }
} }

173
yarn.lock
View File

@ -418,7 +418,7 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-export-namespace-from" "^7.2.0" "@babel/plugin-syntax-export-namespace-from" "^7.2.0"
"@babel/plugin-proposal-function-bind@^7.0.0": "@babel/plugin-proposal-function-bind@^7.2.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.2.0.tgz#94dc2cdc505cafc4e225c0014335a01648056bf7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.2.0.tgz#94dc2cdc505cafc4e225c0014335a01648056bf7"
integrity sha512-qOFJ/eX1Is78sywwTxDcsntLOdb5ZlHVVqUz5xznq8ldAfOVIyZzp1JE2rzHnaksZIhrqMrwIpQL/qcEprnVbw== integrity sha512-qOFJ/eX1Is78sywwTxDcsntLOdb5ZlHVVqUz5xznq8ldAfOVIyZzp1JE2rzHnaksZIhrqMrwIpQL/qcEprnVbw==
@ -435,7 +435,7 @@
"@babel/helper-wrap-function" "^7.2.0" "@babel/helper-wrap-function" "^7.2.0"
"@babel/plugin-syntax-function-sent" "^7.2.0" "@babel/plugin-syntax-function-sent" "^7.2.0"
"@babel/plugin-proposal-json-strings@^7.0.0", "@babel/plugin-proposal-json-strings@^7.2.0": "@babel/plugin-proposal-json-strings@^7.2.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg== integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==
@ -443,7 +443,7 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-json-strings" "^7.2.0" "@babel/plugin-syntax-json-strings" "^7.2.0"
"@babel/plugin-proposal-logical-assignment-operators@^7.0.0": "@babel/plugin-proposal-logical-assignment-operators@^7.2.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.2.0.tgz#8a5cea6c42a7c87446959e02fff5fad012c56f57" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.2.0.tgz#8a5cea6c42a7c87446959e02fff5fad012c56f57"
integrity sha512-0w797xwdPXKk0m3Js74hDi0mCTZplIu93MOSfb1ZLd/XFe3abWypx1QknVk0J+ohnsjYpvjH4Gwfo2i3RicB6Q== integrity sha512-0w797xwdPXKk0m3Js74hDi0mCTZplIu93MOSfb1ZLd/XFe3abWypx1QknVk0J+ohnsjYpvjH4Gwfo2i3RicB6Q==
@ -459,7 +459,7 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-nullish-coalescing-operator" "^7.2.0" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.2.0"
"@babel/plugin-proposal-numeric-separator@^7.0.0": "@babel/plugin-proposal-numeric-separator@^7.2.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.2.0.tgz#646854daf4cd22fd6733f6076013a936310443ac" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.2.0.tgz#646854daf4cd22fd6733f6076013a936310443ac"
integrity sha512-DohMOGDrZiMKS7LthjUZNNcWl8TAf5BZDwZAH4wpm55FuJTHgfqPGdibg7rZDmont/8Yg0zA03IgT6XLeP+4sg== integrity sha512-DohMOGDrZiMKS7LthjUZNNcWl8TAf5BZDwZAH4wpm55FuJTHgfqPGdibg7rZDmont/8Yg0zA03IgT6XLeP+4sg==
@ -515,7 +515,7 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-pipeline-operator" "^7.5.0" "@babel/plugin-syntax-pipeline-operator" "^7.5.0"
"@babel/plugin-proposal-throw-expressions@^7.0.0": "@babel/plugin-proposal-throw-expressions@^7.2.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.2.0.tgz#2d9e452d370f139000e51db65d0a85dc60c64739" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.2.0.tgz#2d9e452d370f139000e51db65d0a85dc60c64739"
integrity sha512-adsydM8DQF4i5DLNO4ySAU5VtHTPewOtNBV3u7F4lNMPADFF9bWQ+iDtUUe8+033cYCUz+bFlQdXQJmJOwoLpw== integrity sha512-adsydM8DQF4i5DLNO4ySAU5VtHTPewOtNBV3u7F4lNMPADFF9bWQ+iDtUUe8+033cYCUz+bFlQdXQJmJOwoLpw==
@ -562,7 +562,7 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-dynamic-import@7.2.0", "@babel/plugin-syntax-dynamic-import@^7.0.0", "@babel/plugin-syntax-dynamic-import@^7.2.0": "@babel/plugin-syntax-dynamic-import@7.2.0", "@babel/plugin-syntax-dynamic-import@^7.2.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612"
integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w== integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==
@ -604,7 +604,7 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-import-meta@^7.0.0": "@babel/plugin-syntax-import-meta@^7.2.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.2.0.tgz#2333ef4b875553a3bcd1e93f8ebc09f5b9213a40" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.2.0.tgz#2333ef4b875553a3bcd1e93f8ebc09f5b9213a40"
integrity sha512-Hq6kFSZD7+PHkmBN8bCpHR6J8QEoCuEV/B38AIQscYjgMZkGlXB7cHNFzP5jR4RCh5545yP1ujHdmO7hAgKtBA== integrity sha512-Hq6kFSZD7+PHkmBN8bCpHR6J8QEoCuEV/B38AIQscYjgMZkGlXB7cHNFzP5jR4RCh5545yP1ujHdmO7hAgKtBA==
@ -1269,7 +1269,7 @@
js-levenshtein "^1.1.3" js-levenshtein "^1.1.3"
semver "^5.5.0" semver "^5.5.0"
"@babel/preset-flow@^7.0.0", "@babel/preset-flow@^7.0.0-beta.40": "@babel/preset-flow@^7.0.0":
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.0.0.tgz#afd764835d9535ec63d8c7d4caf1c06457263da2" resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.0.0.tgz#afd764835d9535ec63d8c7d4caf1c06457263da2"
integrity sha512-bJOHrYOPqJZCkPVbG1Lot2r5OSsB+iUOaxiHdlOeB1yPWS6evswVHwvkDLZ54WTaTRIk89ds0iHmGZSnxlPejQ== integrity sha512-bJOHrYOPqJZCkPVbG1Lot2r5OSsB+iUOaxiHdlOeB1yPWS6evswVHwvkDLZ54WTaTRIk89ds0iHmGZSnxlPejQ==
@ -1277,7 +1277,7 @@
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-flow-strip-types" "^7.0.0" "@babel/plugin-transform-flow-strip-types" "^7.0.0"
"@babel/preset-react@7.0.0", "@babel/preset-react@^7.0.0", "@babel/preset-react@^7.0.0-beta.40": "@babel/preset-react@7.0.0", "@babel/preset-react@^7.0.0":
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0"
integrity sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w== integrity sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==
@ -1906,7 +1906,7 @@
resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.0.2.tgz#1d94f02800b094753f9271c206a26c2a06ca14ee" resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.0.2.tgz#1d94f02800b094753f9271c206a26c2a06ca14ee"
integrity sha512-8/qcMh15507AnXJ3lBeuhsdFwnWQqnp68EpUuHlYPixJ5vjVmls7/Jq48cnUlrZI8Jd9U1jkhfCl0gaT5KMgVw== integrity sha512-8/qcMh15507AnXJ3lBeuhsdFwnWQqnp68EpUuHlYPixJ5vjVmls7/Jq48cnUlrZI8Jd9U1jkhfCl0gaT5KMgVw==
"@sambego/storybook-state@^1.0.7": "@sambego/storybook-state@^1.3.6":
version "1.3.6" version "1.3.6"
resolved "https://registry.yarnpkg.com/@sambego/storybook-state/-/storybook-state-1.3.6.tgz#9a6511095d200b8ab2d6bc39def81c90312dd420" resolved "https://registry.yarnpkg.com/@sambego/storybook-state/-/storybook-state-1.3.6.tgz#9a6511095d200b8ab2d6bc39def81c90312dd420"
integrity sha512-bTUE1ZTtI9ICyqz6l5gtUfo0/W77fPP7KOAd/HI1jM7m1Jxjxs1k1Qbcrqmxg1vaHemlXVkvxVCZf8BT9RzxGw== integrity sha512-bTUE1ZTtI9ICyqz6l5gtUfo0/W77fPP7KOAd/HI1jM7m1Jxjxs1k1Qbcrqmxg1vaHemlXVkvxVCZf8BT9RzxGw==
@ -3037,6 +3037,11 @@ acorn-jsx@^5.0.0:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg== integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==
acorn-jsx@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f"
integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==
acorn-walk@^6.0.1, acorn-walk@^6.1.1: acorn-walk@^6.0.1, acorn-walk@^6.1.1:
version "6.2.0" version "6.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
@ -3062,6 +3067,11 @@ acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.0, acorn@^6.2.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.1.tgz#3ed8422d6dec09e6121cc7a843ca86a330a86b51" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.1.tgz#3ed8422d6dec09e6121cc7a843ca86a330a86b51"
integrity sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q== integrity sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==
acorn@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.0.0.tgz#26b8d1cd9a9b700350b71c0905546f64d1284e7a"
integrity sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ==
address@1.0.3: address@1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
@ -3915,7 +3925,7 @@ babel-plugin-dynamic-import-node@2.2.0:
dependencies: dependencies:
object.assign "^4.1.0" object.assign "^4.1.0"
babel-plugin-dynamic-import-node@^2.2.0, babel-plugin-dynamic-import-node@^2.3.0: babel-plugin-dynamic-import-node@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ== integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
@ -5512,7 +5522,7 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
classnames@^2.2.5: classnames@^2.2.5, classnames@^2.2.6:
version "2.2.6" version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@ -6738,7 +6748,7 @@ detect-port-alt@1.1.6:
address "^1.0.1" address "^1.0.1"
debug "^2.6.0" debug "^2.6.0"
detect-port@^1.2.2, detect-port@^1.3.0: detect-port@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1" resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1"
integrity sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ== integrity sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ==
@ -7359,6 +7369,14 @@ eslint-scope@^4.0.0, eslint-scope@^4.0.3:
esrecurse "^4.1.0" esrecurse "^4.1.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-scope@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9"
integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==
dependencies:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-utils@^1.3.1: eslint-utils@^1.3.1:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.0.tgz#e2c3c8dba768425f897cf0f9e51fe2e241485d4c" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.0.tgz#e2c3c8dba768425f897cf0f9e51fe2e241485d4c"
@ -7366,12 +7384,67 @@ eslint-utils@^1.3.1:
dependencies: dependencies:
eslint-visitor-keys "^1.0.0" eslint-visitor-keys "^1.0.0"
eslint-utils@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
dependencies:
eslint-visitor-keys "^1.0.0"
eslint-visitor-keys@^1.0.0: eslint-visitor-keys@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
eslint@5.16.0, eslint@^5.0.0, eslint@^5.5.0: eslint-visitor-keys@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
eslint@6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.4.0.tgz#5aa9227c3fbe921982b2eda94ba0d7fae858611a"
integrity sha512-WTVEzK3lSFoXUovDHEbkJqCVPEPwbhCq4trDktNI6ygs7aO41d4cDT0JFAT5MivzZeVLWlg7vHL+bgrQv/t3vA==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.10.0"
chalk "^2.1.0"
cross-spawn "^6.0.5"
debug "^4.0.1"
doctrine "^3.0.0"
eslint-scope "^5.0.0"
eslint-utils "^1.4.2"
eslint-visitor-keys "^1.1.0"
espree "^6.1.1"
esquery "^1.0.1"
esutils "^2.0.2"
file-entry-cache "^5.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0"
globals "^11.7.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
inquirer "^6.4.1"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.3.0"
lodash "^4.17.14"
minimatch "^3.0.4"
mkdirp "^0.5.1"
natural-compare "^1.4.0"
optionator "^0.8.2"
progress "^2.0.0"
regexpp "^2.0.1"
semver "^6.1.2"
strip-ansi "^5.2.0"
strip-json-comments "^3.0.1"
table "^5.2.3"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
eslint@^5.0.0, eslint@^5.5.0:
version "5.16.0" version "5.16.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea"
integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg== integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==
@ -7430,6 +7503,15 @@ espree@^5.0.1:
acorn-jsx "^5.0.0" acorn-jsx "^5.0.0"
eslint-visitor-keys "^1.0.0" eslint-visitor-keys "^1.0.0"
espree@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de"
integrity sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==
dependencies:
acorn "^7.0.0"
acorn-jsx "^5.0.2"
eslint-visitor-keys "^1.1.0"
esprima@^3.1.3, esprima@~3.1.0: esprima@^3.1.3, esprima@~3.1.0:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
@ -9004,6 +9086,13 @@ glob-parent@^3.1.0:
is-glob "^3.1.0" is-glob "^3.1.0"
path-dirname "^1.0.0" path-dirname "^1.0.0"
glob-parent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954"
integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==
dependencies:
is-glob "^4.0.1"
glob-stream@^6.1.0: glob-stream@^6.1.0:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4"
@ -9637,7 +9726,7 @@ html-tag-names@^1.1.1:
resolved "https://registry.yarnpkg.com/html-tag-names/-/html-tag-names-1.1.4.tgz#51c559e36a077b5eb6c71e6cb49b1d70fffc9124" resolved "https://registry.yarnpkg.com/html-tag-names/-/html-tag-names-1.1.4.tgz#51c559e36a077b5eb6c71e6cb49b1d70fffc9124"
integrity sha512-QCOY1/oHmo2BNwsTzuYlW51JLXSxfmMvve+2/9i2cbhxXxT6SuhsUWzcIoMwUi0HZW/NIQBSyJaj7fbcsimoKg== integrity sha512-QCOY1/oHmo2BNwsTzuYlW51JLXSxfmMvve+2/9i2cbhxXxT6SuhsUWzcIoMwUi0HZW/NIQBSyJaj7fbcsimoKg==
html-webpack-plugin@^3.0.4: html-webpack-plugin@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s= integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s=
@ -10001,6 +10090,25 @@ inquirer@^6.2.0, inquirer@^6.2.2:
strip-ansi "^5.1.0" strip-ansi "^5.1.0"
through "^2.3.6" through "^2.3.6"
inquirer@^6.4.1:
version "6.5.2"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca"
integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==
dependencies:
ansi-escapes "^3.2.0"
chalk "^2.4.2"
cli-cursor "^2.1.0"
cli-width "^2.0.0"
external-editor "^3.0.3"
figures "^2.0.0"
lodash "^4.17.12"
mute-stream "0.0.7"
run-async "^2.2.0"
rxjs "^6.4.0"
string-width "^2.1.0"
strip-ansi "^5.1.0"
through "^2.3.6"
internal-ip@^4.3.0: internal-ip@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907"
@ -10291,7 +10399,7 @@ is-glob@^3.1.0:
dependencies: dependencies:
is-extglob "^2.1.0" is-extglob "^2.1.0"
is-glob@^4.0.0: is-glob@^4.0.0, is-glob@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
@ -12785,6 +12893,15 @@ normalize-url@^4.1.0:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.3.0.tgz#9c49e10fc1876aeb76dba88bf1b2b5d9fa57b2ee" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.3.0.tgz#9c49e10fc1876aeb76dba88bf1b2b5d9fa57b2ee"
integrity sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ== integrity sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ==
"notistack@https://github.com/gnosis/notistack.git#v0.9.4":
version "0.9.4"
resolved "https://github.com/gnosis/notistack.git#077aa51fd066f2c3198b2f5ce773f741e21f24df"
dependencies:
classnames "^2.2.6"
hoist-non-react-statics "^3.3.0"
prop-types "^15.7.2"
react-is "^16.9.0"
now-and-later@^2.0.0: now-and-later@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c"
@ -14776,7 +14893,7 @@ react-redux@7.1.1:
prop-types "^15.7.2" prop-types "^15.7.2"
react-is "^16.9.0" react-is "^16.9.0"
react-router-dom@5.1.0: react-router-dom@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.0.tgz#48ad018d71fb7835212587e4c90bd2e3d2417e31" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.0.tgz#48ad018d71fb7835212587e4c90bd2e3d2417e31"
integrity sha512-OkxKbMKjO7IkYqnoaZNX19MnwgjhxwZE871cPUTq0YU2wpIw7QwGxSnSoNRMOa7wO1TwvJJMFpgiEB4C/gVhTw== integrity sha512-OkxKbMKjO7IkYqnoaZNX19MnwgjhxwZE871cPUTq0YU2wpIw7QwGxSnSoNRMOa7wO1TwvJJMFpgiEB4C/gVhTw==
@ -15108,7 +15225,7 @@ reduce-reducers@^0.4.3:
resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.4.3.tgz#8e052618801cd8fc2714b4915adaa8937eb6d66c" resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.4.3.tgz#8e052618801cd8fc2714b4915adaa8937eb6d66c"
integrity sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw== integrity sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw==
redux-actions@^2.3.0: redux-actions@^2.6.5:
version "2.6.5" version "2.6.5"
resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.6.5.tgz#bdca548768ee99832a63910c276def85e821a27e" resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.6.5.tgz#bdca548768ee99832a63910c276def85e821a27e"
integrity sha512-pFhEcWFTYNk7DhQgxMGnbsB1H2glqhQJRQrtPb96kD3hWiZRzXHwwmFPswg6V2MjraXRXWNmuP9P84tvdLAJmw== integrity sha512-pFhEcWFTYNk7DhQgxMGnbsB1H2glqhQJRQrtPb96kD3hWiZRzXHwwmFPswg6V2MjraXRXWNmuP9P84tvdLAJmw==
@ -15152,7 +15269,7 @@ redux-saga@1.0.0:
dependencies: dependencies:
"@redux-saga/core" "^1.0.0" "@redux-saga/core" "^1.0.0"
redux-thunk@^2.2.0: redux-thunk@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
@ -15963,7 +16080,7 @@ semver@6.2.0, semver@^6.0.0, semver@^6.1.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db"
integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==
semver@^6.2.0, semver@^6.3.0: semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
version "6.3.0" version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
@ -16547,7 +16664,7 @@ storybook-host@5.1.0:
ramda "^0.25.0" ramda "^0.25.0"
tinycolor2 "^1.4.1" tinycolor2 "^1.4.1"
storybook-router@^0.3.3: storybook-router@^0.3.4:
version "0.3.4" version "0.3.4"
resolved "https://registry.yarnpkg.com/storybook-router/-/storybook-router-0.3.4.tgz#27c0c0de5eafa03b9003a850ac40e2b0c2a3ee65" resolved "https://registry.yarnpkg.com/storybook-router/-/storybook-router-0.3.4.tgz#27c0c0de5eafa03b9003a850ac40e2b0c2a3ee65"
integrity sha512-WU8kyx06R5zFa3KT1TZey2fOadj0nFhWs5yuq3iVcfSbUhtwSg/QNGF7V6IXjiBOtYqmvN2/hlUnYneC/oQ16w== integrity sha512-WU8kyx06R5zFa3KT1TZey2fOadj0nFhWs5yuq3iVcfSbUhtwSg/QNGF7V6IXjiBOtYqmvN2/hlUnYneC/oQ16w==
@ -16764,6 +16881,11 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
strip-json-comments@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7"
integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==
style-loader@1.0.0: style-loader@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.0.0.tgz#1d5296f9165e8e2c85d24eee0b7caf9ec8ca1f82" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.0.0.tgz#1d5296f9165e8e2c85d24eee0b7caf9ec8ca1f82"
@ -18357,6 +18479,11 @@ v8-compile-cache@2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==
v8-compile-cache@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==
v8flags@^2.1.1: v8flags@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"
@ -19662,7 +19789,7 @@ webpack-log@^2.0.0:
ansi-colors "^3.0.0" ansi-colors "^3.0.0"
uuid "^3.3.2" uuid "^3.3.2"
webpack-manifest-plugin@2.1.1: webpack-manifest-plugin@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-2.1.1.tgz#6b3e280327815b83152c79f42d0ca13b665773c4" resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-2.1.1.tgz#6b3e280327815b83152c79f42d0ca13b665773c4"
integrity sha512-2zqJ6mvc3yoiqfDjghAIpljhLSDh/G7vqGrzYcYqqRCd/ZZZCAuc/YPE5xG0LGpLgDJRhUNV1H+znyyhIxahzA== integrity sha512-2zqJ6mvc3yoiqfDjghAIpljhLSDh/G7vqGrzYcYqqRCd/ZZZCAuc/YPE5xG0LGpLgDJRhUNV1H+znyyhIxahzA==
@ -20267,4 +20394,4 @@ yauzl@^2.4.2:
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
dependencies: dependencies:
buffer-crc32 "~0.2.3" buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0" fd-slicer "~1.1.0"