diff --git a/package.json b/package.json index 860e3e91..b0a11c6b 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "immortal-db": "^1.0.2", "immutable": "^4.0.0-rc.9", "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", "qrcode.react": "^0.9.3", "react": "16.9.0", @@ -55,11 +56,11 @@ "react-infinite-scroll-component": "4.5.3", "react-qr-reader": "^2.2.1", "react-redux": "7.1.1", - "react-router-dom": "5.1.0", + "react-router-dom": "^5.1.0", "recompose": "^0.30.0", "redux": "4.0.4", - "redux-actions": "^2.3.0", - "redux-thunk": "^2.2.0", + "redux-actions": "^2.6.5", + "redux-thunk": "^2.3.0", "reselect": "^4.0.0", "web3": "1.2.1" }, @@ -71,24 +72,24 @@ "@babel/plugin-proposal-do-expressions": "7.6.0", "@babel/plugin-proposal-export-default-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-json-strings": "^7.0.0", - "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-logical-assignment-operators": "^7.2.0", "@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-pipeline-operator": "7.5.0", - "@babel/plugin-proposal-throw-expressions": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.0.0", - "@babel/plugin-syntax-import-meta": "^7.0.0", + "@babel/plugin-proposal-throw-expressions": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/plugin-transform-member-expression-literals": "^7.2.0", "@babel/plugin-transform-property-literals": "^7.2.0", "@babel/polyfill": "7.6.0", "@babel/preset-env": "7.6.2", - "@babel/preset-flow": "^7.0.0-beta.40", - "@babel/preset-react": "^7.0.0-beta.40", - "@sambego/storybook-state": "^1.0.7", + "@babel/preset-flow": "^7.0.0", + "@babel/preset-react": "^7.0.0", + "@sambego/storybook-state": "^1.3.6", "@storybook/addon-actions": "5.2.1", "@storybook/addon-knobs": "5.2.1", "@storybook/addon-links": "5.2.1", @@ -99,13 +100,13 @@ "babel-eslint": "10.0.3", "babel-jest": "24.9.0", "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-property-literals": "^6.22.0", - "classnames": "^2.2.5", + "classnames": "^2.2.6", "css-loader": "3.2.0", - "detect-port": "^1.2.2", - "eslint": "5.16.0", + "detect-port": "^1.3.0", + "eslint": "6.4.0", "eslint-config-airbnb": "18.0.1", "eslint-plugin-flowtype": "4.3.0", "eslint-plugin-import": "2.18.2", @@ -118,7 +119,7 @@ "flow-bin": "0.108.0", "fs-extra": "8.1.0", "html-loader": "^0.5.5", - "html-webpack-plugin": "^3.0.4", + "html-webpack-plugin": "^3.2.0", "jest": "24.9.0", "jest-dom": "4.0.0", "json-loader": "^0.5.7", @@ -130,7 +131,7 @@ "prettier-eslint-cli": "5.0.0", "run-with-testrpc": "0.3.1", "storybook-host": "5.1.0", - "storybook-router": "^0.3.3", + "storybook-router": "^0.3.4", "style-loader": "1.0.0", "truffle": "5.0.37", "truffle-contract": "4.0.31", @@ -141,6 +142,6 @@ "webpack-bundle-analyzer": "3.5.1", "webpack-cli": "3.3.9", "webpack-dev-server": "3.8.1", - "webpack-manifest-plugin": "2.1.1" + "webpack-manifest-plugin": "^2.1.1" } } diff --git a/src/components/Header/component/Layout.jsx b/src/components/Header/component/Layout.jsx index 877647ea..d5e5f85e 100644 --- a/src/components/Header/component/Layout.jsx +++ b/src/components/Header/component/Layout.jsx @@ -32,7 +32,8 @@ const styles = () => ({ padding: 0, boxShadow: '0 0 10px 0 rgba(33, 48, 77, 0.1)', minWidth: '280px', - left: '4px', + borderRadius: '8px', + marginTop: '11px', }, summary: { borderBottom: `solid 2px ${border}`, @@ -47,6 +48,9 @@ const styles = () => ({ flexBasis: '95px', flexGrow: 0, }, + popper: { + zIndex: 2000, + }, }) const Layout = openHoc(({ @@ -63,17 +67,18 @@ const Layout = openHoc(({ - {(providerRef) => ( - + {({ TransitionProps }) => ( - - - {providerDetails} - - + <> + + + {providerDetails} + + + )} diff --git a/src/components/Header/component/Provider.jsx b/src/components/Header/component/Provider.jsx index dda09c76..272283fb 100644 --- a/src/components/Header/component/Provider.jsx +++ b/src/components/Header/component/Provider.jsx @@ -5,6 +5,7 @@ import IconButton from '@material-ui/core/IconButton' import ExpandLess from '@material-ui/icons/ExpandLess' import ExpandMore from '@material-ui/icons/ExpandMore' import Col from '~/components/layout/Col' +import Divider from '~/components/layout/Divider' import { type Open } from '~/components/hoc/OpenHoc' import { sm, md } from '~/theme/variables' @@ -20,7 +21,8 @@ const styles = () => ({ height: '100%', display: 'flex', alignItems: 'center', - flexBasis: '250px', + flexBasis: '284px', + marginRight: '20px', }, provider: { padding: `${sm} ${md}`, @@ -54,12 +56,14 @@ class Provider extends React.Component { return ( <>
+ {info} {open ? : } +
{children(this.myRef)} diff --git a/src/components/Header/component/ProviderDetails/UserDetails.jsx b/src/components/Header/component/ProviderDetails/UserDetails.jsx index 1bdd6027..0c315325 100644 --- a/src/components/Header/component/ProviderDetails/UserDetails.jsx +++ b/src/components/Header/component/ProviderDetails/UserDetails.jsx @@ -143,7 +143,7 @@ const UserDetails = ({ - Client + Wallet {provider === 'safe' diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index 0511119b..c4b53361 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -1,11 +1,11 @@ // @flow import * as React from 'react' import { connect } from 'react-redux' +import { withSnackbar } from 'notistack' 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 type { ProviderProps } from '~/logic/wallets/store/model/provider' +import { NOTIFICATIONS, showSnackbar } from '~/logic/notifications' import ProviderAccesible from './component/ProviderInfo/ProviderAccesible' import UserDetails from './component/ProviderDetails/UserDetails' import ProviderDisconnected from './component/ProviderInfo/ProviderDisconnected' @@ -16,7 +16,7 @@ import selector, { type SelectorProps } from './selector' type Props = Actions & SelectorProps & { - openSnackbar: (message: string, variant: Variant) => void, + enqueueSnackbar: Function, } type State = { @@ -39,31 +39,33 @@ class HeaderComponent extends React.PureComponent { } componentDidCatch(error: Error, info: Info) { - const { openSnackbar } = this.props + const { enqueueSnackbar, closeSnackbar } = this.props + this.setState({ hasError: true }) - openSnackbar(WALLET_ERROR_MSG, 'error') + showSnackbar(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG, enqueueSnackbar, closeSnackbar) logComponentStack(error, info) } onDisconnect = () => { - const { removeProvider, openSnackbar } = this.props + const { removeProvider, enqueueSnackbar, closeSnackbar } = this.props + clearInterval(this.providerListener) - removeProvider(openSnackbar) + removeProvider(enqueueSnackbar, closeSnackbar) } onConnect = async () => { - const { fetchProvider, openSnackbar } = this.props + const { fetchProvider, enqueueSnackbar, closeSnackbar } = this.props clearInterval(this.providerListener) let currentProvider: ProviderProps = await getProviderInfo() - fetchProvider(currentProvider, openSnackbar) + fetchProvider(currentProvider, enqueueSnackbar, closeSnackbar) this.providerListener = setInterval(async () => { const newProvider: ProviderProps = await getProviderInfo() - if (JSON.stringify(currentProvider) !== JSON.stringify(newProvider)) { - fetchProvider(newProvider, openSnackbar) + if (currentProvider && JSON.stringify(currentProvider) !== JSON.stringify(newProvider)) { + fetchProvider(newProvider, enqueueSnackbar, closeSnackbar) } currentProvider = newProvider }, 2000) @@ -111,13 +113,7 @@ class HeaderComponent extends React.PureComponent { } } -const Header = connect( +export default connect( selector, actions, -)(HeaderComponent) - -const HeaderSnack = () => ( - {({ openSnackbar }) =>
} -) - -export default HeaderSnack +)(withSnackbar(HeaderComponent)) diff --git a/src/components/Notifier/actions.js b/src/components/Notifier/actions.js new file mode 100644 index 00000000..7cfffca1 --- /dev/null +++ b/src/components/Notifier/actions.js @@ -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, +} diff --git a/src/components/Notifier/index.js b/src/components/Notifier/index.js new file mode 100644 index 00000000..c4a9fd10 --- /dev/null +++ b/src/components/Notifier/index.js @@ -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), +) diff --git a/src/components/Notifier/selector.js b/src/components/Notifier/selector.js new file mode 100644 index 00000000..148b3d3b --- /dev/null +++ b/src/components/Notifier/selector.js @@ -0,0 +1,7 @@ +// @flow +import { createStructuredSelector } from 'reselect' +import { notificationsListSelector } from '~/logic/notifications/store/selectors' + +export default createStructuredSelector({ + notifications: notificationsListSelector, +}) diff --git a/src/components/SharedSnackBar/index.jsx b/src/components/SharedSnackBar/index.jsx deleted file mode 100644 index c22c9213..00000000 --- a/src/components/SharedSnackBar/index.jsx +++ /dev/null @@ -1,105 +0,0 @@ -// @flow -import * as React from 'react' -import { Snackbar } from '@material-ui/core' -import SnackbarContent from '~/components/SnackbarContent' - -export const SharedSnackbar = () => ( - - {(value) => { - const { - snackbarIsOpen, message, closeSnackbar, variant, - } = value - - return ( - - - - ) - }} - -) - -type SnackbarContext = { - openSnackbar: Function, - closeSnackbar: Function, - snackbarIsOpen: boolean, - message: string, - variant: string, -} - -const SharedSnackbarContext = React.createContext({ - 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 { - 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 ( - - - {children} - - ) - } -} - -export const SharedSnackbarConsumer = SharedSnackbarContext.Consumer diff --git a/src/components/SnackbarContent/index.jsx b/src/components/SnackbarContent/index.jsx deleted file mode 100644 index 7d6a4798..00000000 --- a/src/components/SnackbarContent/index.jsx +++ /dev/null @@ -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) => ( - - - -) - -const Message = ({ classes, message, variant }: MessageProps) => { - const Icon = variantIcon[variant] - - return ( - - - {message} - - ) -} - -const GnoSnackbarContent = ({ - variant, classes, message, onClose, -}: Props) => { - const action = onClose ? [] : undefined - const messageComponent = - - return ( - - ) -} - -export default withStyles(styles)(GnoSnackbarContent) diff --git a/src/components/layout/PageFrame/assets/alert.svg b/src/components/layout/PageFrame/assets/alert.svg new file mode 100644 index 00000000..0e20a926 --- /dev/null +++ b/src/components/layout/PageFrame/assets/alert.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/layout/PageFrame/assets/check.svg b/src/components/layout/PageFrame/assets/check.svg new file mode 100644 index 00000000..ad3cd87b --- /dev/null +++ b/src/components/layout/PageFrame/assets/check.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/layout/PageFrame/assets/error.svg b/src/components/layout/PageFrame/assets/error.svg new file mode 100644 index 00000000..b4948e8f --- /dev/null +++ b/src/components/layout/PageFrame/assets/error.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/layout/PageFrame/index.jsx b/src/components/layout/PageFrame/index.jsx index 6159ca41..9c8b1fa8 100644 --- a/src/components/layout/PageFrame/index.jsx +++ b/src/components/layout/PageFrame/index.jsx @@ -1,23 +1,85 @@ // @flow 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 { 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' +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 = { children: React.Node, + classes: Object, } -const PageFrame = ({ children }: Props) => ( - -
+const PageFrame = ({ children, classes }: Props) => ( +
+ , + error: Error, + warning: Warning, + info: '', + }} + > +
{children} -
- + +
) -export default PageFrame +export default withStyles(notificationStyles)(PageFrame) diff --git a/src/logic/notifications/index.js b/src/logic/notifications/index.js new file mode 100644 index 00000000..df61614c --- /dev/null +++ b/src/logic/notifications/index.js @@ -0,0 +1,3 @@ +// @flow +export * from './notificationTypes' +export * from './notificationBuilder' diff --git a/src/logic/notifications/notificationBuilder.js b/src/logic/notifications/notificationBuilder.js new file mode 100644 index 00000000..84b5b908 --- /dev/null +++ b/src/logic/notifications/notificationBuilder.js @@ -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) => ( + closeSnackbar(key)}> + + + ), +}) diff --git a/src/logic/notifications/notificationTypes.js b/src/logic/notifications/notificationTypes.js new file mode 100644 index 00000000..9d92ebbd --- /dev/null +++ b/src/logic/notifications/notificationTypes.js @@ -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 }, + }, +} diff --git a/src/logic/notifications/store/actions/closeSnackbar.js b/src/logic/notifications/store/actions/closeSnackbar.js new file mode 100644 index 00000000..4e63a1a9 --- /dev/null +++ b/src/logic/notifications/store/actions/closeSnackbar.js @@ -0,0 +1,8 @@ +// @flow +import { createAction } from 'redux-actions' + +export const CLOSE_SNACKBAR = 'CLOSE_SNACKBAR' + +const closeSnackbar = createAction(CLOSE_SNACKBAR) + +export default closeSnackbar diff --git a/src/logic/notifications/store/actions/enqueueSnackbar.js b/src/logic/notifications/store/actions/enqueueSnackbar.js new file mode 100644 index 00000000..e14abe0c --- /dev/null +++ b/src/logic/notifications/store/actions/enqueueSnackbar.js @@ -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(ENQUEUE_SNACKBAR) + +const enqueueSnackbar = (notification: NotificationProps) => ( + dispatch: ReduxDispatch, + getState: GetState, +) => { + const newNotification = { + ...notification, + key: new Date().getTime(), + } + dispatch(addSnackbar(newNotification)) +} + +export default enqueueSnackbar diff --git a/src/logic/notifications/store/actions/removeSnackbar.js b/src/logic/notifications/store/actions/removeSnackbar.js new file mode 100644 index 00000000..8ecaee83 --- /dev/null +++ b/src/logic/notifications/store/actions/removeSnackbar.js @@ -0,0 +1,8 @@ +// @flow +import { createAction } from 'redux-actions' + +export const REMOVE_SNACKBAR = 'REMOVE_SNACKBAR' + +const removeSnackbar = createAction(REMOVE_SNACKBAR) + +export default removeSnackbar diff --git a/src/logic/notifications/store/models/notification.js b/src/logic/notifications/store/models/notification.js new file mode 100644 index 00000000..7452faa9 --- /dev/null +++ b/src/logic/notifications/store/models/notification.js @@ -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 = Record({ + key: 0, + message: '', + options: {}, + dismissed: false, +}) + +export type Notification = RecordOf diff --git a/src/logic/notifications/store/reducer/notifications.js b/src/logic/notifications/store/reducer/notifications.js new file mode 100644 index 00000000..c4ae1c2b --- /dev/null +++ b/src/logic/notifications/store/reducer/notifications.js @@ -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 + +export default handleActions( + { + [ENQUEUE_SNACKBAR]: (state: NotificationReducerState, action: ActionType): NotificationReducerState => { + const notification: NotificationProps = action.payload + + return state.set(notification.key, makeNotification(notification)) + }, + [CLOSE_SNACKBAR]: (state: NotificationReducerState, action: ActionType): 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): NotificationReducerState => { + const key = action.payload + + return state.delete(key) + }, + }, + Map({ + notifications: Map(), + }), +) diff --git a/src/logic/notifications/store/selectors/index.js b/src/logic/notifications/store/selectors/index.js new file mode 100644 index 00000000..2ae6eb96 --- /dev/null +++ b/src/logic/notifications/store/selectors/index.js @@ -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 => state[NOTIFICATIONS_REDUCER_ID] + +export const notificationsListSelector: Selector> = createSelector( + notificationsMapSelector, + (notifications: Map): List => notifications.toList(), +) diff --git a/src/logic/safe/transactions/gas.js b/src/logic/safe/transactions/gas.js index 1ae8e0d0..2a0da8cb 100644 --- a/src/logic/safe/transactions/gas.js +++ b/src/logic/safe/transactions/gas.js @@ -82,8 +82,7 @@ export const generateTxGasEstimateFrom = async ( // Add 10k else we will fail in case of nested calls return txGasEstimate.toNumber() + 10000 } catch (error) { - // eslint-disable-next-line - console.log('Error calculating tx gas estimation ' + error) + console.error('Error calculating tx gas estimation', error) return 0 } } @@ -128,8 +127,7 @@ export const calculateTxFee = async ( return estimate } catch (error) { - // eslint-disable-next-line - console.log('Error calculating tx gas estimation ' + error) + console.error('Error calculating tx gas estimation', error) return 0 } } diff --git a/src/logic/safe/transactions/index.js b/src/logic/safe/transactions/index.js index 056e5633..63d8a8b0 100644 --- a/src/logic/safe/transactions/index.js +++ b/src/logic/safe/transactions/index.js @@ -1,7 +1,6 @@ // @flow export * from './gas' export * from './send' -export * from './safeBlockchainOperations' export * from './safeTxSignerEIP712' export * from './txHistory' -export * from './notifications' +export * from './notifiedTransactions' diff --git a/src/logic/safe/transactions/notifications.js b/src/logic/safe/transactions/notifications.js deleted file mode 100644 index 65bdf16e..00000000 --- a/src/logic/safe/transactions/notifications.js +++ /dev/null @@ -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', -} diff --git a/src/logic/safe/transactions/notifiedTransactions.js b/src/logic/safe/transactions/notifiedTransactions.js new file mode 100644 index 00000000..1d60ec83 --- /dev/null +++ b/src/logic/safe/transactions/notifiedTransactions.js @@ -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', +} diff --git a/src/logic/safe/transactions/safeBlockchainOperations.js b/src/logic/safe/transactions/safeBlockchainOperations.js deleted file mode 100644 index 8e0863e5..00000000 --- a/src/logic/safe/transactions/safeBlockchainOperations.js +++ /dev/null @@ -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, -// // ) => { -// // 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 -// // } diff --git a/src/logic/safe/transactions/send.js b/src/logic/safe/transactions/send.js index c98bdf94..ba1044a3 100644 --- a/src/logic/safe/transactions/send.js +++ b/src/logic/safe/transactions/send.js @@ -1,14 +1,8 @@ // @flow import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' 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 { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses' -import { getErrorMessage } from '~/test/utils/ethereumErrors' export const CALL = 0 export const TX_TYPE_EXECUTION = 'execution' @@ -44,13 +38,10 @@ export const getApprovalTransaction = async ( const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address) return contract.methods.approveHash(txHash) - } catch (error) { - /* eslint-disable */ - const executeData = safeInstance.contract.methods.approveHash(txHash).encodeABI() - const errMsg = await getErrorMessage(safeInstance.address, 0, executeData, sender) - console.log(`Error executing the TX: ${errMsg}`) + } catch (err) { + console.error(`Error while approving transaction: ${err}`) - throw error + throw err } } @@ -62,58 +53,16 @@ export const getExecutionTransaction = async ( operation: Operation, nonce: string | number, 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 { const web3 = getWeb3() 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) - } catch (error) { - /* eslint-disable */ - 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}`) + } catch (err) { + console.error(`Error while creating transaction: ${err}`) - 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 -} diff --git a/src/logic/safe/utils/safeStorage.js b/src/logic/safe/utils/safeStorage.js index da498908..c8e092d0 100644 --- a/src/logic/safe/utils/safeStorage.js +++ b/src/logic/safe/utils/safeStorage.js @@ -22,8 +22,7 @@ export const saveSafes = async (safes: Object) => { try { await saveToStorage(SAFES_KEY, safes) } catch (err) { - // eslint-disable-next-line - console.log('Error storing safe info in localstorage') + console.error('Error storing safe info in localstorage', err) } } @@ -32,8 +31,7 @@ export const setOwners = async (safeAddress: string, owners: List) => { const ownersAsMap = Map(owners.map((owner: Owner) => [owner.address.toLowerCase(), owner.name])) await saveToStorage(`${OWNERS_KEY}-${safeAddress}`, ownersAsMap) } catch (err) { - // eslint-disable-next-line - console.log('Error storing owners in localstorage') + console.error('Error storing owners in localstorage', err) } } diff --git a/src/logic/tokens/store/actions/fetchTokens.js b/src/logic/tokens/store/actions/fetchTokens.js index d3654f96..abaad8bc 100644 --- a/src/logic/tokens/store/actions/fetchTokens.js +++ b/src/logic/tokens/store/actions/fetchTokens.js @@ -40,8 +40,7 @@ export const fetchTokens = () => async (dispatch: ReduxDispatch) => dispatch(saveTokens(tokens)) } catch (err) { - // eslint-disable-next-line - console.log('Error fetching token list ' + err) + console.error('Error fetching token list', err) return Promise.resolve() } diff --git a/src/logic/tokens/utils/tokensStorage.js b/src/logic/tokens/utils/tokensStorage.js index 0016ebbb..ec1785c8 100644 --- a/src/logic/tokens/utils/tokensStorage.js +++ b/src/logic/tokens/utils/tokensStorage.js @@ -14,8 +14,7 @@ export const saveActiveTokens = async (tokens: Map) => { try { await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS()) } catch (err) { - // eslint-disable-next-line - console.log('Error storing tokens in localstorage') + console.error('Error storing tokens in localstorage', err) } } @@ -38,8 +37,7 @@ export const removeTokenFromStorage = async (safeAddress: string, token: Token) const index = data.indexOf(token) await saveToStorage(CUSTOM_TOKENS_KEY, data.remove(index)) } catch (err) { - // eslint-disable-next-line - console.log('Error removing token in localstorage') + console.error('Error removing token in localstorage', err) } } diff --git a/src/logic/wallets/getWeb3.js b/src/logic/wallets/getWeb3.js index b88660f7..92543002 100644 --- a/src/logic/wallets/getWeb3.js +++ b/src/logic/wallets/getWeb3.js @@ -76,7 +76,14 @@ export const getProviderInfo: Function = async (): Promise => { if (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) { web3Provider = window.web3.currentProvider } else { @@ -114,6 +121,10 @@ export const getAddressFromENS = async (name: string) => { } export const getBalanceInEtherOf = async (safeAddress: string) => { + if (!web3) { + return '0' + } + const funds: String = await web3.eth.getBalance(safeAddress) if (!funds) { diff --git a/src/logic/wallets/store/actions/fetchProvider.js b/src/logic/wallets/store/actions/fetchProvider.js index c19f52ce..3e67c79c 100644 --- a/src/logic/wallets/store/actions/fetchProvider.js +++ b/src/logic/wallets/store/actions/fetchProvider.js @@ -3,6 +3,7 @@ import type { Dispatch as ReduxDispatch } from 'redux' import { ETHEREUM_NETWORK_IDS, ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3' import type { ProviderProps } from '~/logic/wallets/store/model/provider' import { makeProvider } from '~/logic/wallets/store/model/provider' +import { NOTIFICATIONS, showSnackbar } from '~/logic/notifications' import addProvider from './addProvider' export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: ProviderProps) => { @@ -21,30 +22,40 @@ export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: Pr dispatch(addProvider(walletRecord)) } -const SUCCESS_MSG = 'Wallet connected sucessfully' -const UNLOCK_MSG = 'Unlock your wallet to connect' -const WRONG_NETWORK = 'You are connected to wrong network. Please use RINKEBY' -export const WALLET_ERROR_MSG = 'Error connecting to your wallet' - -const handleProviderNotification = (openSnackbar: Function, provider: ProviderProps) => { +const handleProviderNotification = ( + dispatch: ReduxDispatch<*>, + provider: ProviderProps, + enqueueSnackbar: Function, + closeSnackbar: Function, +) => { const { loaded, available, network } = provider if (!loaded) { - openSnackbar(WALLET_ERROR_MSG, 'error') + showSnackbar(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG, enqueueSnackbar, closeSnackbar) return } if (ETHEREUM_NETWORK_IDS[network] !== ETHEREUM_NETWORK.RINKEBY) { - openSnackbar(WRONG_NETWORK, 'error') + showSnackbar(NOTIFICATIONS.WRONG_NETWORK_RINKEBY_MSG, enqueueSnackbar, closeSnackbar) return } + showSnackbar(NOTIFICATIONS.RINKEBY_VERSION_MSG, enqueueSnackbar, closeSnackbar) - const msg = available ? SUCCESS_MSG : UNLOCK_MSG - const variant = available ? 'success' : 'warning' - openSnackbar(msg, variant) + if (available) { + // NOTE: + // 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<*>) => { - handleProviderNotification(openSnackbar, provider) +export default (provider: ProviderProps, enqueueSnackbar: Function, closeSnackbar: Function) => ( + dispatch: ReduxDispatch<*>, +) => { + handleProviderNotification(dispatch, provider, enqueueSnackbar, closeSnackbar) processProviderResponse(dispatch, provider) } diff --git a/src/logic/wallets/store/actions/removeProvider.js b/src/logic/wallets/store/actions/removeProvider.js index 00b35973..ac81f7d6 100644 --- a/src/logic/wallets/store/actions/removeProvider.js +++ b/src/logic/wallets/store/actions/removeProvider.js @@ -1,9 +1,10 @@ // @flow import type { Dispatch as ReduxDispatch } from 'redux' import { makeProvider, type ProviderProps, type Provider } from '~/logic/wallets/store/model/provider' +import { NOTIFICATIONS, showSnackbar } from '~/logic/notifications' import addProvider from './addProvider' -export default (openSnackbar: Function) => async (dispatch: ReduxDispatch<*>) => { +export default (enqueueSnackbar: Function, closeSnackbar: Function) => async (dispatch: ReduxDispatch<*>) => { const providerProps: ProviderProps = { name: '', available: false, @@ -13,7 +14,7 @@ export default (openSnackbar: Function) => async (dispatch: ReduxDispatch<*>) => } const provider: Provider = makeProvider(providerProps) - openSnackbar('Wallet disconnected succesfully', 'info') + showSnackbar(NOTIFICATIONS.WALLET_DISCONNECTED_MSG, enqueueSnackbar, closeSnackbar) dispatch(addProvider(provider)) } diff --git a/src/routes/load/container/Load.jsx b/src/routes/load/container/Load.jsx index 86d3b6b3..181d0ec8 100644 --- a/src/routes/load/container/Load.jsx +++ b/src/routes/load/container/Load.jsx @@ -50,8 +50,7 @@ class Load extends React.Component { const url = `${SAFELIST_ADDRESS}/${safeAddress}` history.push(url) } catch (error) { - // eslint-disable-next-line - console.log('Error while loading the Safe' + error) + console.error('Error while loading the Safe', error) } } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewCustomTx/index.jsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewCustomTx/index.jsx index e391ada9..aa54f6cf 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCustomTx/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCustomTx/index.jsx @@ -4,7 +4,7 @@ import OpenInNew from '@material-ui/icons/OpenInNew' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import IconButton from '@material-ui/core/IconButton' -import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import { withSnackbar } from 'notistack' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' import Link from '~/components/layout/Link' @@ -13,11 +13,12 @@ import Button from '~/components/layout/Button' import Img from '~/components/layout/Img' import Block from '~/components/layout/Block' import Identicon from '~/components/Identicon' -import { copyToClipboard } from '~/utils/clipboard' import Hairline from '~/components/layout/Hairline' +import { copyToClipboard } from '~/utils/clipboard' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils' import { getWeb3 } from '~/logic/wallets/getWeb3' +import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers' import ArrowDown from '../assets/arrow-down.svg' import { secondary } from '~/theme/variables' @@ -33,6 +34,8 @@ type Props = { ethBalance: string, tx: Object, createTransaction: Function, + enqueueSnackbar: Function, + closeSnackbar: Function, } const openIconStyle = { @@ -50,110 +53,117 @@ const ReviewCustomTx = ({ ethBalance, tx, createTransaction, -}: Props) => ( - - {({ openSnackbar }) => { - const submitTx = async () => { - const web3 = getWeb3() - const txRecipient = tx.recipientAddress - const txData = tx.data - const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : 0 + enqueueSnackbar, + closeSnackbar, +}: Props) => { + const submitTx = async () => { + const web3 = getWeb3() + const txRecipient = tx.recipientAddress + const txData = tx.data + const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : 0 - createTransaction(safeAddress, txRecipient, txValue, txData, openSnackbar) - onClose() - } + createTransaction( + safeAddress, + txRecipient, + txValue, + txData, + TX_NOTIFICATION_TYPES.STANDARD_TX, + enqueueSnackbar, + closeSnackbar, + ) + onClose() + } - return ( - <> - - - Send Funds + return ( + <> + + + Send Funds + + 2 of 2 + + + + + + + + + + Arrow Down + + + + + + + + Recipient + + + + + + + + + {tx.recipientAddress} + + + - 2 of 2 - - - - - - - - - - Arrow Down - - - - + + + + + Value + + + + Ether + + {tx.value || 0} + {' ETH'} + + + + + Data (hex encoded) + + + + + + {tx.data} - - - Recipient - - - - - - - - - {tx.recipientAddress} - - - - - - - - - Value - - - - Ether - - {tx.value || 0} - {' ETH'} - - - - - Data (hex encoded) - - - - - - {tx.data} - - - - - - - - - - - ) - }} - -) + + + + + + + + + + ) +} -export default withStyles(styles)(ReviewCustomTx) + +export default withStyles(styles)(withSnackbar(ReviewCustomTx)) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.jsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.jsx index b301038e..0aa41c67 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.jsx @@ -4,7 +4,7 @@ import OpenInNew from '@material-ui/icons/OpenInNew' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import IconButton from '@material-ui/core/IconButton' -import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import { withSnackbar } from 'notistack' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' 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 { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { getWeb3 } from '~/logic/wallets/getWeb3' +import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import ArrowDown from '../assets/arrow-down.svg' import { secondary } from '~/theme/variables' import { isEther } from '~/logic/tokens/utils/tokenHelpers' @@ -35,6 +36,8 @@ type Props = { ethBalance: string, tx: Object, createTransaction: Function, + enqueueSnackbar: Function, + closeSnackbar: Function, } const openIconStyle = { @@ -52,111 +55,117 @@ const ReviewTx = ({ ethBalance, tx, createTransaction, -}: Props) => ( - - {({ openSnackbar }) => { - const submitTx = async () => { - const web3 = getWeb3() - const isSendingETH = isEther(tx.token.symbol) - const txRecipient = isSendingETH ? tx.recipientAddress : tx.token.address - let txData = EMPTY_DATA - let txAmount = web3.utils.toWei(tx.amount, 'ether') + enqueueSnackbar, + closeSnackbar, +}: Props) => { + const submitTx = async () => { + const web3 = getWeb3() + const isSendingETH = isEther(tx.token.symbol) + const txRecipient = isSendingETH ? tx.recipientAddress : tx.token.address + let txData = EMPTY_DATA + let txAmount = web3.utils.toWei(tx.amount, 'ether') - if (!isSendingETH) { - const StandardToken = await getStandardTokenContract() - const tokenInstance = await StandardToken.at(tx.token.address) + if (!isSendingETH) { + const StandardToken = await getStandardTokenContract() + const tokenInstance = await StandardToken.at(tx.token.address) - txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI() - // txAmount should be 0 if we send tokens - // the real value is encoded in txData and will be used by the contract - // if txAmount > 0 it would send ETH from the safe - txAmount = 0 - } + txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI() + // txAmount should be 0 if we send tokens + // the real value is encoded in txData and will be used by the contract + // if txAmount > 0 it would send ETH from the safe + txAmount = 0 + } - createTransaction(safeAddress, txRecipient, txAmount, txData, openSnackbar) - onClose() - } + createTransaction( + safeAddress, + txRecipient, + txAmount, + txData, + TX_NOTIFICATION_TYPES.STANDARD_TX, + enqueueSnackbar, + closeSnackbar, + ) + onClose() + } - return ( - <> - - - Send Funds + return ( + <> + + + Send Funds + + 2 of 2 + + + + + + + + + + Arrow Down + + + + + + + + Recipient + + + + + + + + + {tx.recipientAddress} + + + - 2 of 2 - - - - - - - - - - Arrow Down - - - - - - - - Recipient - - - - - - - - - {tx.recipientAddress} - - - - - - - - - Amount - - - - {tx.token.name} - - {tx.amount} - {' '} - {tx.token.symbol} - - - - - - - - - - ) - }} - -) + + + + + Amount + + + + {tx.token.name} + + {tx.amount} + {' '} + {tx.token.symbol} + + + + + + + + + + ) +} -export default withStyles(styles)(ReviewTx) +export default withStyles(styles)(withSnackbar(ReviewTx)) diff --git a/src/routes/safe/components/Settings/ChangeSafeName/index.jsx b/src/routes/safe/components/Settings/ChangeSafeName/index.jsx index 886016eb..764d7994 100644 --- a/src/routes/safe/components/Settings/ChangeSafeName/index.jsx +++ b/src/routes/safe/components/Settings/ChangeSafeName/index.jsx @@ -1,17 +1,19 @@ // @flow import React from 'react' import { withStyles } from '@material-ui/core/styles' +import { withSnackbar } from 'notistack' import Block from '~/components/layout/Block' import Col from '~/components/layout/Col' import Field from '~/components/forms/Field' import Heading from '~/components/layout/Heading' -import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' import { composeValidators, required, minMaxLength } from '~/components/forms/validator' import TextField from '~/components/forms/TextField' import GnoForm from '~/components/forms/GnoForm' import Row from '~/components/layout/Row' import Paragraph from '~/components/layout/Paragraph' import Button from '~/components/layout/Button' +import { getNofiticationsFromTxType, showSnackbar } from '~/logic/notifications' +import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { styles } from './style' export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input' @@ -22,17 +24,20 @@ type Props = { safeAddress: string, safeName: string, updateSafe: Function, - openSnackbar: Function, + enqueueSnackbar: Function, + closeSnackbar: Function, } const ChangeSafeName = (props: Props) => { const { - classes, safeAddress, safeName, updateSafe, openSnackbar, + classes, safeAddress, safeName, updateSafe, enqueueSnackbar, closeSnackbar, } = props const handleSubmit = (values) => { 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 ( @@ -80,10 +85,4 @@ const ChangeSafeName = (props: Props) => { ) } -const withSnackbar = (props) => ( - - {({ openSnackbar }) => } - -) - -export default withStyles(styles)(withSnackbar) +export default withStyles(styles)(withSnackbar(ChangeSafeName)) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx index 1ace6021..66d51631 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx @@ -2,10 +2,11 @@ import React, { useState, useEffect } from 'react' import { List } from 'immutable' import { withStyles } from '@material-ui/core/styles' -import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import { withSnackbar } from 'notistack' import Modal from '~/components/Modal' import { type Owner } from '~/routes/safe/store/models/owner' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' +import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import OwnerForm from './screens/OwnerForm' import ThresholdForm from './screens/ThresholdForm' import ReviewAddOwner from './screens/Review' @@ -29,6 +30,8 @@ type Props = { network: string, addSafeOwner: Function, createTransaction: Function, + enqueueSnackbar: Function, + closeSnackbar: Function, } type ActiveScreen = 'selectOwner' | 'selectThreshold' | 'reviewAddOwner' @@ -36,14 +39,23 @@ export const sendAddOwner = async ( values: Object, safeAddress: string, ownersOld: List, - openSnackbar: Function, + enqueueSnackbar: Function, + closeSnackbar: Function, createTransaction: Function, addSafeOwner: Function, ) => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) 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) { addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress }) @@ -61,6 +73,8 @@ const AddOwner = ({ network, createTransaction, addSafeOwner, + enqueueSnackbar, + closeSnackbar, }: Props) => { const [activeScreen, setActiveScreen] = useState('selectOwner') const [values, setValues] = useState({}) @@ -98,59 +112,50 @@ const AddOwner = ({ setActiveScreen('reviewAddOwner') } - return ( - <> - - {({ openSnackbar }) => { - const onAddOwner = async () => { - onClose() - try { - sendAddOwner(values, safeAddress, owners, openSnackbar, createTransaction, addSafeOwner) - } catch (error) { - // eslint-disable-next-line - console.log('Error while removing an owner ' + error) - } - } + const onAddOwner = async () => { + onClose() + try { + sendAddOwner(values, safeAddress, owners, enqueueSnackbar, closeSnackbar, createTransaction, addSafeOwner) + } catch (error) { + console.error('Error while removing an owner', error) + } + } - return ( - - <> - {activeScreen === 'selectOwner' && ( - - )} - {activeScreen === 'selectThreshold' && ( - - )} - {activeScreen === 'reviewAddOwner' && ( - - )} - - - ) - }} - - + return ( + + <> + {activeScreen === 'selectOwner' && ( + + )} + {activeScreen === 'selectThreshold' && ( + + )} + {activeScreen === 'reviewAddOwner' && ( + + )} + + ) } -export default withStyles(styles)(AddOwner) +export default withStyles(styles)(withSnackbar(AddOwner)) diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx index 6fe47db6..c17386c3 100644 --- a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx @@ -1,5 +1,6 @@ // @flow import React from 'react' +import { withSnackbar } from 'notistack' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import OpenInNew from '@material-ui/icons/OpenInNew' @@ -14,8 +15,10 @@ import Field from '~/components/forms/Field' import TextField from '~/components/forms/TextField' import Paragraph from '~/components/layout/Paragraph' import Identicon from '~/components/Identicon' -import { getEtherScanLink } from '~/logic/wallets/getWeb3' 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 { styles } from './style' import { secondary } from '~/theme/variables' @@ -37,6 +40,8 @@ type Props = { network: string, selectedOwnerName: string, editSafeOwner: Function, + enqueueSnackbar: Function, + closeSnackbar: Function, } const EditOwnerComponent = ({ @@ -48,9 +53,15 @@ const EditOwnerComponent = ({ selectedOwnerName, editSafeOwner, network, + enqueueSnackbar, + closeSnackbar, }: Props) => { const handleSubmit = (values) => { editSafeOwner({ safeAddress, ownerAddress, ownerName: values.ownerName }) + + const notification = getNofiticationsFromTxType(TX_NOTIFICATION_TYPES.OWNER_NAME_CHANGE_TX) + showSnackbar(notification.afterExecution, enqueueSnackbar, closeSnackbar) + onClose() } @@ -116,6 +127,6 @@ const EditOwnerComponent = ({ ) } -const EditOwnerModal = withStyles(styles)(EditOwnerComponent) +const EditOwnerModal = withStyles(styles)(withSnackbar(EditOwnerComponent)) export default EditOwnerModal diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx index c245965b..be59b517 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx @@ -2,10 +2,11 @@ import React, { useState, useEffect } from 'react' import { List } from 'immutable' import { withStyles } from '@material-ui/core/styles' -import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import { withSnackbar } from 'notistack' import Modal from '~/components/Modal' import { type Owner } from '~/routes/safe/store/models/owner' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts' +import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import CheckOwner from './screens/CheckOwner' import ThresholdForm from './screens/ThresholdForm' import ReviewRemoveOwner from './screens/Review' @@ -31,7 +32,10 @@ type Props = { network: string, createTransaction: Function, removeSafeOwner: Function, + enqueueSnackbar: Function, + closeSnackbar: Function, } + type ActiveScreen = 'checkOwner' | 'selectThreshold' | 'reviewRemoveOwner' export const sendRemoveOwner = async ( @@ -40,7 +44,8 @@ export const sendRemoveOwner = async ( ownerAddressToRemove: string, ownerNameToRemove: string, ownersOld: List, - openSnackbar: Function, + enqueueSnackbar: Function, + closeSnackbar: Function, createTransaction: Function, removeSafeOwner: Function, ) => { @@ -54,7 +59,15 @@ export const sendRemoveOwner = async ( .removeOwner(prevAddress, ownerAddressToRemove, 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) { removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove }) @@ -74,6 +87,8 @@ const RemoveOwner = ({ network, createTransaction, removeSafeOwner, + enqueueSnackbar, + closeSnackbar, }: Props) => { const [activeScreen, setActiveScreen] = useState('checkOwner') const [values, setValues] = useState({}) @@ -104,71 +119,64 @@ const RemoveOwner = ({ setActiveScreen('reviewRemoveOwner') } - return ( - <> - - {({ openSnackbar }) => { - const onRemoveOwner = () => { - onClose() - sendRemoveOwner( - values, - safeAddress, - ownerAddress, - ownerName, - owners, - openSnackbar, - createTransaction, - removeSafeOwner, - ) - } + const onRemoveOwner = () => { + onClose() + sendRemoveOwner( + values, + safeAddress, + ownerAddress, + ownerName, + owners, + enqueueSnackbar, + closeSnackbar, + createTransaction, + removeSafeOwner, + ) + } - return ( - - <> - {activeScreen === 'checkOwner' && ( - - )} - {activeScreen === 'selectThreshold' && ( - - )} - {activeScreen === 'reviewRemoveOwner' && ( - - )} - - - ) - }} - - + return ( + + <> + {activeScreen === 'checkOwner' && ( + + )} + {activeScreen === 'selectThreshold' && ( + + )} + {activeScreen === 'reviewRemoveOwner' && ( + + )} + + ) } -export default withStyles(styles)(RemoveOwner) +export default withStyles(styles)(withSnackbar(RemoveOwner)) diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx index 02b9037e..498b9a62 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx @@ -2,8 +2,9 @@ import React, { useState, useEffect } from 'react' import { List } from 'immutable' import { withStyles } from '@material-ui/core/styles' -import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import { withSnackbar } from 'notistack' import Modal from '~/components/Modal' +import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts' import OwnerForm from './screens/OwnerForm' import ReviewReplaceOwner from './screens/Review' @@ -29,6 +30,8 @@ type Props = { threshold: string, createTransaction: Function, replaceSafeOwner: Function, + enqueueSnackbar: Function, + closeSnackbar: Function, } type ActiveScreen = 'checkOwner' | 'reviewReplaceOwner' @@ -36,7 +39,8 @@ export const sendReplaceOwner = async ( values: Object, safeAddress: string, ownerAddressToRemove: string, - openSnackbar: Function, + enqueueSnackbar: Function, + closeSnackbar: Function, createTransaction: Function, replaceSafeOwner: Function, ) => { @@ -50,7 +54,15 @@ export const sendReplaceOwner = async ( .swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress) .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) { replaceSafeOwner({ @@ -75,6 +87,8 @@ const ReplaceOwner = ({ threshold, createTransaction, replaceSafeOwner, + enqueueSnackbar, + closeSnackbar, }: Props) => { const [activeScreen, setActiveScreen] = useState('checkOwner') const [values, setValues] = useState({}) @@ -96,67 +110,59 @@ const ReplaceOwner = ({ setActiveScreen('reviewReplaceOwner') } - return ( - <> - - {({ openSnackbar }) => { - const onReplaceOwner = () => { - onClose() - try { - sendReplaceOwner( - values, - safeAddress, - ownerAddress, - openSnackbar, - createTransaction, - replaceSafeOwner, - ) - } catch (error) { - // eslint-disable-next-line - console.log('Error while removing an owner ' + error) - } - } + const onReplaceOwner = () => { + onClose() + try { + sendReplaceOwner( + values, + safeAddress, + ownerAddress, + enqueueSnackbar, + closeSnackbar, + createTransaction, + replaceSafeOwner, + ) + } catch (error) { + console.error('Error while removing an owner', error) + } + } - return ( - - <> - {activeScreen === 'checkOwner' && ( - - )} - {activeScreen === 'reviewReplaceOwner' && ( - - )} - - - ) - }} - - + return ( + + <> + {activeScreen === 'checkOwner' && ( + + )} + {activeScreen === 'reviewReplaceOwner' && ( + + )} + + ) } -export default withStyles(styles)(ReplaceOwner) +export default withStyles(styles)(withSnackbar(ReplaceOwner)) diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.jsx b/src/routes/safe/components/Settings/ThresholdSettings/index.jsx index 701fa6c6..8afaf2a8 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.jsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { withStyles } from '@material-ui/core/styles' import { List } from 'immutable' -import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import { withSnackbar } from 'notistack' import Heading from '~/components/layout/Heading' import Button from '~/components/layout/Button' import Bold from '~/components/layout/Bold' @@ -10,10 +10,11 @@ import Block from '~/components/layout/Block' import Row from '~/components/layout/Row' import Modal from '~/components/Modal' import Paragraph from '~/components/layout/Paragraph' +import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import ChangeThreshold from './ChangeThreshold' import type { Owner } from '~/routes/safe/store/models/owner' -import { styles } from './style' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' +import { styles } from './style' type Props = { owners: List, @@ -22,10 +23,19 @@ type Props = { createTransaction: Function, safeAddress: string, granted: boolean, + enqueueSnackbar: Function, + closeSnackbar: Function, } const ThresholdSettings = ({ - owners, threshold, classes, createTransaction, safeAddress, granted, + owners, + threshold, + classes, + createTransaction, + safeAddress, + granted, + enqueueSnackbar, + closeSnackbar, }: Props) => { const [isModalOpen, setModalOpen] = useState(false) @@ -33,66 +43,66 @@ const ThresholdSettings = ({ 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 ( <> - - {({ openSnackbar }) => { - const onChangeThreshold = async (newThreshold) => { - const safeInstance = await getGnosisSafeInstanceAt(safeAddress) - const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI() - - createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar) - } - - return ( - <> - - Required confirmations - - Any transaction requires the confirmation of: - - - {threshold} - {' '} - out of - {' '} - {owners.size} - {' '} - owners - - {owners.size > 1 && granted && ( - - - - )} - - - - - - ) - }} - + + Required confirmations + + Any transaction requires the confirmation of: + + + {threshold} + {' '} + out of + {' '} + {owners.size} + {' '} + owners + + {owners.size > 1 && granted && ( + + + + )} + + + + ) } -export default withStyles(styles)(ThresholdSettings) +export default withStyles(styles)(withSnackbar(ThresholdSettings)) diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx index 95630467..aeba3ae7 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx @@ -5,7 +5,7 @@ import IconButton from '@material-ui/core/IconButton' import { withStyles } from '@material-ui/core/styles' import FormControlLabel from '@material-ui/core/FormControlLabel' import Checkbox from '@material-ui/core/Checkbox' -import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import { withSnackbar } from 'notistack' import Modal from '~/components/Modal' import Hairline from '~/components/layout/Hairline' import Button from '~/components/layout/Button' @@ -13,6 +13,7 @@ import Row from '~/components/layout/Row' import Bold from '~/components/layout/Bold' import Block from '~/components/layout/Block' import Paragraph from '~/components/layout/Paragraph' +import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { type Transaction } from '~/routes/safe/store/models/transaction' import { styles } from './style' @@ -29,6 +30,8 @@ type Props = { threshold: number, thresholdReached: boolean, userAddress: string, + enqueueSnackbar: Function, + closeSnackbar: Function, } const getModalTitleAndDescription = (thresholdReached: boolean) => { @@ -55,6 +58,8 @@ const ApproveTxModal = ({ threshold, thresholdReached, userAddress, + enqueueSnackbar, + closeSnackbar, }: Props) => { const [approveAndExecute, setApproveAndExecute] = useState(false) const { title, description } = getModalTitleAndDescription(thresholdReached) @@ -62,68 +67,70 @@ const ApproveTxModal = ({ const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute) - return ( - - {({ openSnackbar }) => { - const approveTx = () => { - processTransaction(safeAddress, tx, openSnackbar, userAddress, approveAndExecute) - onClose() - } + const approveTx = () => { + processTransaction( + safeAddress, + tx, + userAddress, + TX_NOTIFICATION_TYPES.CONFIRMATION_TX, + enqueueSnackbar, + closeSnackbar, + approveAndExecute, + ) + onClose() + } - return ( - - - - {title} + return ( + + + + {title} + + + + + + + + + {description} + + Transaction nonce: +
+ {tx.nonce} +
+ {!thresholdReached && oneConfirmationLeft && ( + <> + + Approving transaction does not execute it immediately. If you want to approve and execute the + transaction right away, click on checkbox below. - - - -
- - - - {description} - - Transaction nonce: -
- {tx.nonce} -
- {!thresholdReached && oneConfirmationLeft && ( - <> - - Approving transaction does not execute it immediately. If you want to approve and execute the - transaction right away, click on checkbox below. - - } - label="Execute transaction" - /> - - )} -
-
- - - - -
- ) - }} -
+ } + label="Execute transaction" + /> + + )} + + + + + + + ) } -export default withStyles(styles)(ApproveTxModal) +export default withStyles(styles)(withSnackbar(ApproveTxModal)) diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/CancelTxModal/index.jsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/CancelTxModal/index.jsx index 05c25121..e7b34275 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/CancelTxModal/index.jsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/CancelTxModal/index.jsx @@ -3,7 +3,7 @@ import React from 'react' import Close from '@material-ui/icons/Close' import IconButton from '@material-ui/core/IconButton' import { withStyles } from '@material-ui/core/styles' -import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import { withSnackbar } from 'notistack' import Modal from '~/components/Modal' import Hairline from '~/components/layout/Hairline' import Button from '~/components/layout/Button' @@ -13,6 +13,7 @@ import Block from '~/components/layout/Block' import Paragraph from '~/components/layout/Paragraph' import { type Transaction } from '~/routes/safe/store/models/transaction' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' +import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { styles } from './style' type Props = { @@ -22,67 +23,80 @@ type Props = { createTransaction: Function, tx: Transaction, safeAddress: string, + enqueueSnackbar: Function, + closeSnackbar: Function, } const CancelTxModal = ({ - onClose, isOpen, classes, createTransaction, tx, safeAddress, -}: Props) => ( - - {({ openSnackbar }) => { - const sendReplacementTransaction = () => { - createTransaction(safeAddress, safeAddress, 0, EMPTY_DATA, openSnackbar) - onClose() - } + onClose, + isOpen, + classes, + createTransaction, + tx, + safeAddress, + enqueueSnackbar, + closeSnackbar, +}: Props) => { + const sendReplacementTransaction = () => { + createTransaction( + safeAddress, + safeAddress, + 0, + EMPTY_DATA, + TX_NOTIFICATION_TYPES.CANCELLATION_TX, + enqueueSnackbar, + closeSnackbar, + ) + onClose() + } - return ( - + + + Cancel transaction + + + + + + + + + + This action will cancel this transaction. A separate transaction will be performed to submit the + cancellation. + + + Transaction nonce: +
+ {tx.nonce} +
+
+
+ + + - - -
- ) - }} -
-) + Cancel Transaction + + + + ) +} -export default withStyles(styles)(CancelTxModal) +export default withStyles(styles)(withSnackbar(CancelTxModal)) diff --git a/src/routes/safe/store/actions/addSafe.js b/src/routes/safe/store/actions/addSafe.js index 5a6bbf1b..ccc307ea 100644 --- a/src/routes/safe/store/actions/addSafe.js +++ b/src/routes/safe/store/actions/addSafe.js @@ -24,10 +24,7 @@ export const addSafe = createAction(ADD_SAFE, (s safe, })) -const saveSafe = (safe: Safe) => ( - dispatch: ReduxDispatch, - getState: GetState, -) => { +const saveSafe = (safe: Safe) => (dispatch: ReduxDispatch, getState: GetState) => { const state = getState() const safeList = safesListSelector(state) diff --git a/src/routes/safe/store/actions/addTransactions.js b/src/routes/safe/store/actions/addTransactions.js index 50086b5a..f608ffd8 100644 --- a/src/routes/safe/store/actions/addTransactions.js +++ b/src/routes/safe/store/actions/addTransactions.js @@ -2,4 +2,5 @@ import { createAction } from 'redux-actions' export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS' + export const addTransactions = createAction(ADD_TRANSACTIONS) diff --git a/src/routes/safe/store/actions/createTransaction.js b/src/routes/safe/store/actions/createTransaction.js index 9f9f573a..4478bd8a 100644 --- a/src/routes/safe/store/actions/createTransaction.js +++ b/src/routes/safe/store/actions/createTransaction.js @@ -9,21 +9,29 @@ import { getApprovalTransaction, getExecutionTransaction, CALL, - type Notifications, - DEFAULT_NOTIFICATIONS, + type NotifiedTransaction, TX_TYPE_CONFIRMATION, TX_TYPE_EXECUTION, saveTxToHistory, } 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 = ( safeAddress: string, to: string, valueInWei: string, txData: string = EMPTY_DATA, - openSnackbar: Function, + notifiedTransaction: NotifiedTransaction, + enqueueSnackbar: Function, + closeSnackbar: Function, shouldExecute?: boolean, - notifications?: Notifications = DEFAULT_NOTIFICATIONS, ) => async (dispatch: ReduxDispatch, getState: GetState) => { const state: GlobalState = getState() @@ -33,19 +41,26 @@ const createTransaction = ( const nonce = (await safeInstance.nonce()).toString() 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 tx try { if (isExecution) { - tx = await getExecutionTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from) + tx = await getExecutionTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from, sigs) } else { tx = await getApprovalTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from) } - const sendParams = { - from, - } - + const sendParams = { from } // if not set owner management tests will fail on ganache if (process.env.NODE_ENV === 'test') { sendParams.gas = '7000000' @@ -55,12 +70,21 @@ const createTransaction = ( .send(sendParams) .once('transactionHash', (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) => { console.error('Tx error: ', error) }) .then(async (receipt) => { + closeSnackbar(pendingExecutionKey) await saveTxToHistory( safeInstance, to, @@ -72,17 +96,22 @@ const createTransaction = ( from, isExecution ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION, ) + if (isExecution) { + showSnackbar(notificationsQueue.afterExecution, enqueueSnackbar, closeSnackbar) + } return receipt.transactionHash }) - - openSnackbar( - isExecution ? notifications.AFTER_EXECUTION : notifications.CREATED_MORE_CONFIRMATIONS_NEEDED, - 'success', - ) } catch (err) { - openSnackbar(notifications.ERROR, 'error') - console.error(`Error while creating transaction: ${err}`) + closeSnackbar(beforeExecutionKey) + 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)) diff --git a/src/routes/safe/store/actions/fetchTransactions.js b/src/routes/safe/store/actions/fetchTransactions.js index a56949a3..2dcf4b15 100644 --- a/src/routes/safe/store/actions/fetchTransactions.js +++ b/src/routes/safe/store/actions/fetchTransactions.js @@ -14,7 +14,7 @@ import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { addTransactions } from './addTransactions' import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens' 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' let web3 diff --git a/src/routes/safe/store/actions/processTransaction.js b/src/routes/safe/store/actions/processTransaction.js index 010d62ef..d2ea959d 100644 --- a/src/routes/safe/store/actions/processTransaction.js +++ b/src/routes/safe/store/actions/processTransaction.js @@ -13,6 +13,13 @@ import { TX_TYPE_EXECUTION, TX_TYPE_CONFIRMATION, } 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://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26 @@ -38,8 +45,10 @@ const generateSignaturesFromTxConfirmations = (tx: Transaction, preApprovingOwne const processTransaction = ( safeAddress: string, tx: Transaction, - openSnackbar: Function, userAddress: string, + notifiedTransaction: NotifiedTransaction, + enqueueSnackbar: Function, + closeSnackbar: Function, approveAndExecute?: boolean, ) => async (dispatch: ReduxDispatch, getState: GetState) => { const state: GlobalState = getState() @@ -49,54 +58,84 @@ const processTransaction = ( const nonce = (await safeInstance.nonce()).toString() const threshold = (await safeInstance.getThreshold()).toNumber() 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 transaction - if (shouldExecute) { - transaction = await getExecutionTransaction(safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, from, sigs) - } else { - 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( + try { + if (shouldExecute) { + transaction = await getExecutionTransaction( safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, - receipt.transactionHash, 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)) diff --git a/src/store/index.js b/src/store/index.js index 0d514224..1e971f98 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,14 +5,18 @@ import { combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store, } from 'redux' 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 safeStorage from '~/routes/safe/store/middleware/safeStorage' -import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/logic/tokens/store/reducer/tokens' import transactions, { type State as TransactionsState, TRANSACTIONS_REDUCER_ID, } 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() @@ -25,6 +29,7 @@ export type GlobalState = { safes: SafeState, tokens: TokensState, transactions: TransactionsState, + notifications: NotificationsState, } export type GetState = () => GlobalState @@ -35,11 +40,13 @@ const reducers: Reducer = combineReducers({ [SAFE_REDUCER_ID]: safe, [TOKEN_REDUCER_ID]: tokens, [TRANSACTIONS_REDUCER_ID]: transactions, + [NOTIFICATIONS_REDUCER_ID]: notifications, }) -export const store: Store = createStore( +export const store: Store = createStore(reducers, finalCreateStore) + +export const aNewStore = (localState?: Object): Store => createStore( reducers, + localState, finalCreateStore, ) - -export const aNewStore = (localState?: Object): Store => createStore(reducers, localState, finalCreateStore) diff --git a/src/theme/mui.js b/src/theme/mui.js index 01d96c8a..06404c67 100644 --- a/src/theme/mui.js +++ b/src/theme/mui.js @@ -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: { colorSecondary: { color: secondaryText, }, }, + MuiSnackbar: { + root: { + width: '280px', + }, + }, + MuiSnackbarContent: { + message: { + maxWidth: '260px', + '& img': { + marginRight: '5px', + }, + }, + action: { + paddingLeft: 0, + }, + }, MuiTab: { root: { fontFamily: 'Averta, monospace', diff --git a/src/utils/storage/signatures.js b/src/utils/storage/signatures.js index 2708cb98..a7cf5d3c 100644 --- a/src/utils/storage/signatures.js +++ b/src/utils/storage/signatures.js @@ -15,8 +15,7 @@ export const storeSignature = async (safeAddress: string, nonce: number, signatu const updatedSubjects = subjects.set(key, signatures) await saveToStorage(signaturesKey, updatedSubjects) } catch (err) { - // eslint-disable-next-line - console.log('Error storing signatures in localstorage') + console.error('Error storing signatures in localstorage', err) } } diff --git a/src/utils/storage/transactions.js b/src/utils/storage/transactions.js index ec29e002..5aca3d26 100644 --- a/src/utils/storage/transactions.js +++ b/src/utils/storage/transactions.js @@ -12,8 +12,7 @@ export const storeSubject = async (safeAddress: string, nonce: number, subject: const updatedSubjects = subjects.set(nonce, subject) saveToStorage(key, updatedSubjects) } catch (err) { - // eslint-disable-next-line - console.log('Error storing transaction subject in localstorage') + console.error('Error storing transaction subject in localstorage', err) } } diff --git a/yarn.lock b/yarn.lock index 29c2ebbc..64684c41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -418,7 +418,7 @@ "@babel/helper-plugin-utils" "^7.0.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" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.2.0.tgz#94dc2cdc505cafc4e225c0014335a01648056bf7" integrity sha512-qOFJ/eX1Is78sywwTxDcsntLOdb5ZlHVVqUz5xznq8ldAfOVIyZzp1JE2rzHnaksZIhrqMrwIpQL/qcEprnVbw== @@ -435,7 +435,7 @@ "@babel/helper-wrap-function" "^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" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg== @@ -443,7 +443,7 @@ "@babel/helper-plugin-utils" "^7.0.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" 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== @@ -459,7 +459,7 @@ "@babel/helper-plugin-utils" "^7.0.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" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.2.0.tgz#646854daf4cd22fd6733f6076013a936310443ac" integrity sha512-DohMOGDrZiMKS7LthjUZNNcWl8TAf5BZDwZAH4wpm55FuJTHgfqPGdibg7rZDmont/8Yg0zA03IgT6XLeP+4sg== @@ -515,7 +515,7 @@ "@babel/helper-plugin-utils" "^7.0.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" 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== @@ -562,7 +562,7 @@ dependencies: "@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" 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== @@ -604,7 +604,7 @@ dependencies: "@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" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.2.0.tgz#2333ef4b875553a3bcd1e93f8ebc09f5b9213a40" integrity sha512-Hq6kFSZD7+PHkmBN8bCpHR6J8QEoCuEV/B38AIQscYjgMZkGlXB7cHNFzP5jR4RCh5545yP1ujHdmO7hAgKtBA== @@ -1269,7 +1269,7 @@ js-levenshtein "^1.1.3" 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" resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.0.0.tgz#afd764835d9535ec63d8c7d4caf1c06457263da2" integrity sha512-bJOHrYOPqJZCkPVbG1Lot2r5OSsB+iUOaxiHdlOeB1yPWS6evswVHwvkDLZ54WTaTRIk89ds0iHmGZSnxlPejQ== @@ -1277,7 +1277,7 @@ "@babel/helper-plugin-utils" "^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" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0" integrity sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w== @@ -1906,7 +1906,7 @@ resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.0.2.tgz#1d94f02800b094753f9271c206a26c2a06ca14ee" integrity sha512-8/qcMh15507AnXJ3lBeuhsdFwnWQqnp68EpUuHlYPixJ5vjVmls7/Jq48cnUlrZI8Jd9U1jkhfCl0gaT5KMgVw== -"@sambego/storybook-state@^1.0.7": +"@sambego/storybook-state@^1.3.6": version "1.3.6" resolved "https://registry.yarnpkg.com/@sambego/storybook-state/-/storybook-state-1.3.6.tgz#9a6511095d200b8ab2d6bc39def81c90312dd420" 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" 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: version "6.2.0" 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" 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: version "1.0.3" 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: 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" 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== @@ -5512,7 +5522,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.5: +classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -6738,7 +6748,7 @@ detect-port-alt@1.1.6: address "^1.0.1" debug "^2.6.0" -detect-port@^1.2.2, detect-port@^1.3.0: +detect-port@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1" integrity sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ== @@ -7359,6 +7369,14 @@ eslint-scope@^4.0.0, eslint-scope@^4.0.3: esrecurse "^4.1.0" 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: version "1.4.0" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.0.tgz#e2c3c8dba768425f897cf0f9e51fe2e241485d4c" @@ -7366,12 +7384,67 @@ eslint-utils@^1.3.1: dependencies: 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: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" 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" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg== @@ -7430,6 +7503,15 @@ espree@^5.0.1: acorn-jsx "^5.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: version "3.1.3" 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" 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: version "6.1.0" 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" integrity sha512-QCOY1/oHmo2BNwsTzuYlW51JLXSxfmMvve+2/9i2cbhxXxT6SuhsUWzcIoMwUi0HZW/NIQBSyJaj7fbcsimoKg== -html-webpack-plugin@^3.0.4: +html-webpack-plugin@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s= @@ -10001,6 +10090,25 @@ inquirer@^6.2.0, inquirer@^6.2.2: strip-ansi "^5.1.0" 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: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -10291,7 +10399,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0: +is-glob@^4.0.0, is-glob@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" 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" 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: version "2.0.1" 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" react-is "^16.9.0" -react-router-dom@5.1.0: +react-router-dom@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.0.tgz#48ad018d71fb7835212587e4c90bd2e3d2417e31" 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" integrity sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw== -redux-actions@^2.3.0: +redux-actions@^2.6.5: version "2.6.5" resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.6.5.tgz#bdca548768ee99832a63910c276def85e821a27e" integrity sha512-pFhEcWFTYNk7DhQgxMGnbsB1H2glqhQJRQrtPb96kD3hWiZRzXHwwmFPswg6V2MjraXRXWNmuP9P84tvdLAJmw== @@ -15152,7 +15269,7 @@ redux-saga@1.0.0: dependencies: "@redux-saga/core" "^1.0.0" -redux-thunk@^2.2.0: +redux-thunk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" 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" 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" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -16547,7 +16664,7 @@ storybook-host@5.1.0: ramda "^0.25.0" tinycolor2 "^1.4.1" -storybook-router@^0.3.3: +storybook-router@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/storybook-router/-/storybook-router-0.3.4.tgz#27c0c0de5eafa03b9003a850ac40e2b0c2a3ee65" 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" 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: version "1.0.0" 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" 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: version "2.1.1" 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" uuid "^3.3.2" -webpack-manifest-plugin@2.1.1: +webpack-manifest-plugin@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-2.1.1.tgz#6b3e280327815b83152c79f42d0ca13b665773c4" integrity sha512-2zqJ6mvc3yoiqfDjghAIpljhLSDh/G7vqGrzYcYqqRCd/ZZZCAuc/YPE5xG0LGpLgDJRhUNV1H+znyyhIxahzA== @@ -20267,4 +20394,4 @@ yauzl@^2.4.2: integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= dependencies: buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" \ No newline at end of file + fd-slicer "~1.1.0"