Merge pull request #1291 from gnosis/release-2.11.0

Gnosis Safe Multisig - Public Release v2.11.0
This commit is contained in:
Mikhail Mikheev 2020-09-01 13:29:16 +04:00 committed by GitHub
commit 1ad57e60e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
270 changed files with 6191 additions and 2870 deletions

8
.storybook/main.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
stories: ['../src/**/*.stories.tsx'],
addons: [
'@storybook/preset-create-react-app',
'@storybook/addon-actions',
'@storybook/addon-links',
],
};

View File

@ -0,0 +1,6 @@
<style>
html, body, #root {
height: 100%;
margin: 0;
}
</style>

26
.storybook/preview.js Normal file
View File

@ -0,0 +1,26 @@
import React from 'react'
import { MemoryRouter } from 'react-router-dom'
import { addDecorator } from '@storybook/react'
import { ThemeProvider, createGlobalStyle } from 'styled-components'
import { theme } from '@gnosis.pm/safe-react-components'
import averta from 'src/assets/fonts/Averta-normal.woff2'
import avertaBold from 'src/assets/fonts/Averta-ExtraBold.woff2'
const GlobalStyles = createGlobalStyle`
@font-face {
font-family: 'Averta';
src: local('Averta'), local('Averta Bold'),
url(${averta}) format('woff2'),
url(${avertaBold}) format('woff');
}
`
addDecorator((storyFn) => (
<ThemeProvider theme={theme}>
<MemoryRouter>
<GlobalStyles />
{storyFn()}
</MemoryRouter>
</ThemeProvider>
))

View File

@ -1,6 +1,6 @@
{ {
"name": "safe-react", "name": "safe-react",
"version": "2.10.2", "version": "2.11.0",
"description": "Allowing crypto users manage funds in a safer way", "description": "Allowing crypto users manage funds in a safer way",
"website": "https://github.com/gnosis/safe-react#readme", "website": "https://github.com/gnosis/safe-react#readme",
"bugs": { "bugs": {
@ -40,11 +40,14 @@
"start": "react-app-rewired start", "start": "react-app-rewired start",
"test": "NODE_ENV=test && react-app-rewired test --env=jsdom", "test": "NODE_ENV=test && react-app-rewired test --env=jsdom",
"test:coverage": "yarn test --coverage --watchAll=false", "test:coverage": "yarn test --coverage --watchAll=false",
"coveralls": "cat ./coverage/lcov.info | coveralls" "coveralls": "cat ./coverage/lcov.info | coveralls",
"storybook": "start-storybook -p 9009 -s public",
"build-storybook": "build-storybook -s public"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "lint-staged --allow-empty" "pre-commit": "lint-staged --allow-empty",
"pre-push": "tsc"
} }
}, },
"lint-staged": { "lint-staged": {
@ -161,8 +164,9 @@
] ]
}, },
"dependencies": { "dependencies": {
"@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk.git#development",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2", "@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#7bb55de", "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#1bf397f",
"@gnosis.pm/util-contracts": "2.0.6", "@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid": "5.19.1", "@ledgerhq/hw-transport-node-hid": "5.19.1",
"@material-ui/core": "4.11.0", "@material-ui/core": "4.11.0",
@ -172,7 +176,7 @@
"async-sema": "^3.1.0", "async-sema": "^3.1.0",
"axios": "0.19.2", "axios": "0.19.2",
"bignumber.js": "9.0.0", "bignumber.js": "9.0.0",
"bnc-onboard": "1.10.3", "bnc-onboard": "1.11.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"concurrently": "^5.2.0", "concurrently": "^5.2.0",
"connected-react-router": "6.8.0", "connected-react-router": "6.8.0",
@ -227,22 +231,27 @@
"web3-utils": "^1.2.11" "web3-utils": "^1.2.11"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-actions": "^5.3.19",
"@storybook/addon-links": "^5.3.19",
"@storybook/addons": "^5.3.19",
"@storybook/preset-create-react-app": "^3.1.4",
"@storybook/react": "^5.3.19",
"@testing-library/jest-dom": "5.11.2", "@testing-library/jest-dom": "5.11.2",
"@testing-library/react": "10.4.7", "@testing-library/react": "10.4.8",
"@testing-library/user-event": "12.0.17", "@testing-library/user-event": "12.1.0",
"@typechain/web3-v1": "^1.0.0", "@typechain/web3-v1": "^1.0.0",
"@types/history": "4.6.2", "@types/history": "4.6.2",
"@types/jest": "^26.0.7", "@types/jest": "^26.0.9",
"@types/lodash.memoize": "^4.1.6", "@types/lodash.memoize": "^4.1.6",
"@types/node": "14.0.27", "@types/node": "14.6.0",
"@types/react": "^16.9.43", "@types/react": "^16.9.47",
"@types/react-dom": "^16.9.6", "@types/react-dom": "^16.9.6",
"@types/react-redux": "^7.1.9", "@types/react-redux": "^7.1.9",
"@types/react-router-dom": "^5.1.5", "@types/react-router-dom": "^5.1.5",
"@types/styled-components": "^5.1.1", "@types/styled-components": "^5.1.2",
"@typescript-eslint/eslint-plugin": "3.7.1", "@typescript-eslint/eslint-plugin": "3.9.1",
"@typescript-eslint/parser": "3.7.1", "@typescript-eslint/parser": "3.9.1",
"autoprefixer": "9.8.5", "autoprefixer": "9.8.6",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"dotenv-expand": "^5.1.0", "dotenv-expand": "^5.1.0",
@ -262,6 +271,7 @@
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"prettier": "2.0.5", "prettier": "2.0.5",
"react-app-rewired": "^2.1.6", "react-app-rewired": "^2.1.6",
"react-docgen-typescript-loader": "^3.7.2",
"truffle": "5.1.36", "truffle": "5.1.36",
"typechain": "^2.0.0", "typechain": "^2.0.0",
"typescript": "3.9.7", "typescript": "3.9.7",

View File

@ -1,7 +1,7 @@
const os = require('os'); const os = require('os');
const { dialog } = require('electron'); const { dialog } = require('electron');
const log = require('electron-log'); const log = require('electron-log');
const settings = require('electron-settings').default; const settings = require('electron-settings');
const { autoUpdater } = require("electron-updater"); const { autoUpdater } = require("electron-updater");

View File

@ -14,7 +14,7 @@ import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline' import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { lg, md, screenSm, secondaryText, sm } from 'src/theme/variables' import { lg, md, screenSm, secondaryText, sm } from 'src/theme/variables'
import { copyToClipboard } from 'src/utils/clipboard' import { copyToClipboard } from 'src/utils/clipboard'

View File

Before

Width:  |  Height:  |  Size: 337 B

After

Width:  |  Height:  |  Size: 337 B

View File

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 345 B

View File

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 324 B

View File

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1,157 @@
import React, { useContext, useEffect } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { SnackbarProvider } from 'notistack'
import { useSelector } from 'react-redux'
import { useRouteMatch, useHistory } from 'react-router-dom'
import styled from 'styled-components'
import AlertIcon from './assets/alert.svg'
import CheckIcon from './assets/check.svg'
import ErrorIcon from './assets/error.svg'
import InfoIcon from './assets/info.svg'
import AppLayout from 'src/components/AppLayout'
import SafeListSidebarProvider, { SafeListSidebarContext } from 'src/components/SafeListSidebar'
import CookiesBanner from 'src/components/CookiesBanner'
import Notifier from 'src/components/Notifier'
import Backdrop from 'src/components/layout/Backdrop'
import Img from 'src/components/layout/Img'
import { getNetwork } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
import { networkSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import Modal from 'src/components/Modal'
import SendModal from 'src/routes/safe/components/Balances/SendModal'
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
import useSafeActions from 'src/logic/safe/hooks/useSafeActions'
import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors/index'
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
import { grantedSelector } from 'src/routes/safe/container/selector'
import Receive from './ModalReceive'
import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems'
const notificationStyles = {
success: {
background: '#fff',
},
error: {
background: '#ffe6ea',
},
warning: {
background: '#fff3e2',
},
info: {
background: '#fff',
},
}
const Frame = styled.div`
display: flex;
flex-direction: column;
flex: 1 1 auto;
max-width: 100%;
`
const desiredNetwork = getNetwork()
const useStyles = makeStyles(notificationStyles)
const App: React.FC = ({ children }) => {
const classes = useStyles()
const currentNetwork = useSelector(networkSelector)
const isWrongNetwork = currentNetwork !== ETHEREUM_NETWORK.UNKNOWN && currentNetwork !== desiredNetwork
const { toggleSidebar } = useContext(SafeListSidebarContext)
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
const history = useHistory()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector)
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector)
const currentCurrency = useSelector(currentCurrencySelector)
const granted = useSelector(grantedSelector)
const sidebarItems = useSidebarItems()
useLoadSafe(safeAddress)
useSafeScheduledUpdates(safeAddress)
const sendFunds = safeActionsState.sendFunds as { isOpen: boolean; selectedToken: string }
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
const balance = !!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : null
useEffect(() => {
if (matchSafe?.isExact) {
history.push(WELCOME_ADDRESS)
return
}
}, [matchSafe, history])
const onReceiveShow = () => onShow('Receive')
const onReceiveHide = () => onHide('Receive')
return (
<Frame>
<Backdrop isOpen={isWrongNetwork} />
<SnackbarProvider
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
classes={{
variantSuccess: classes.success,
variantError: classes.error,
variantWarning: classes.warning,
variantInfo: classes.info,
}}
iconVariant={{
error: <Img alt="Error" src={ErrorIcon} />,
info: <Img alt="Info" src={InfoIcon} />,
success: <Img alt="Success" src={CheckIcon} />,
warning: <Img alt="Warning" src={AlertIcon} />,
}}
maxSnack={5}
>
<>
<Notifier />
<AppLayout
sidebarItems={sidebarItems}
safeAddress={safeAddress}
safeName={safeName}
balance={balance}
granted={granted}
onToggleSafeList={toggleSidebar}
onReceiveClick={onReceiveShow}
onNewTransactionClick={() => showSendFunds('')}
>
{children}
</AppLayout>
<SendModal
activeScreenType="chooseTxType"
isOpen={sendFunds.isOpen}
onClose={hideSendFunds}
selectedToken={sendFunds.selectedToken}
/>
<Modal
description="Receive Tokens Form"
handleClose={onReceiveHide}
open={safeActionsState.showReceive as boolean}
title="Receive Tokens"
>
<Receive onClose={onReceiveHide} />
</Modal>
</>
</SnackbarProvider>
<CookiesBanner />
</Frame>
)
}
const WrapperAppWithSidebar: React.FC = ({ children }) => (
<SafeListSidebarProvider>
<App>{children}</App>
</SafeListSidebarProvider>
)
export default WrapperAppWithSidebar

View File

@ -0,0 +1,61 @@
import React from 'react'
import { Icon } from '@gnosis.pm/safe-react-components'
import { ListItemType } from 'src/components/List'
import Layout from '.'
export default {
title: 'Layout',
component: Layout,
parameters: {
componentSubtitle: 'It provides a custom layout used in Safe Multisig',
},
}
const items: ListItemType[] = [
{
label: 'Assets',
icon: <Icon size="md" type="assets" />,
href: '#',
},
{
label: 'Settings',
icon: <Icon size="md" type="settings" />,
href: '#',
subItems: [
{
label: 'Safe Details',
href: '#',
},
{
label: 'Owners',
href: '#',
},
{
label: 'Policies',
href: '#',
},
{
label: 'Advanced',
href: '#',
},
],
},
]
export const Base = (): React.ReactElement => {
return (
<Layout
sidebarItems={items}
safeAddress="0xEE63624cC4Dd2355B16b35eFaadF3F7450A9438B"
safeName="someName"
granted={true}
balance={null}
onToggleSafeList={() => console.log}
onReceiveClick={() => console.log}
onNewTransactionClick={() => console.log}
>
<div>The content goes here</div>
</Layout>
)
}

View File

@ -44,7 +44,7 @@ const useStyles = makeStyles({
const appVersion = process.env.REACT_APP_APP_VERSION ? `v${process.env.REACT_APP_APP_VERSION} ` : 'Versions' const appVersion = process.env.REACT_APP_APP_VERSION ? `v${process.env.REACT_APP_APP_VERSION} ` : 'Versions'
const Footer = () => { const Footer = (): React.ReactElement => {
const date = new Date() const date = new Date()
const classes = useStyles() const classes = useStyles()
const dispatch = useDispatch() const dispatch = useDispatch()

View File

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 228 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 300 B

After

Width:  |  Height:  |  Size: 300 B

View File

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 237 B

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

@ -6,14 +6,11 @@ import { withStyles } from '@material-ui/core/styles'
import * as React from 'react' import * as React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import NetworkLabel from './NetworkLabel'
import Provider from './Provider' import Provider from './Provider'
import SafeListHeader from './SafeListHeader'
import Spacer from 'src/components/Spacer' import Spacer from 'src/components/Spacer'
import openHoc from 'src/components/hoc/OpenHoc' import openHoc from 'src/components/hoc/OpenHoc'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Divider from 'src/components/layout/Divider'
import Img from 'src/components/layout/Img' import Img from 'src/components/layout/Img'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { border, headerHeight, md, screenSm, sm } from 'src/theme/variables' import { border, headerHeight, md, screenSm, sm } from 'src/theme/variables'
@ -41,11 +38,12 @@ const styles = () => ({
zIndex: 1301, zIndex: 1301,
}, },
logo: { logo: {
flexBasis: '95px', flexBasis: '114px',
flexShrink: '0', flexShrink: '0',
flexGrow: '0', flexGrow: '0',
maxWidth: '55px', maxWidth: '55px',
padding: sm, padding: sm,
marginTop: '4px',
[`@media (min-width: ${screenSm}px)`]: { [`@media (min-width: ${screenSm}px)`]: {
maxWidth: 'none', maxWidth: 'none',
paddingLeft: md, paddingLeft: md,
@ -61,13 +59,9 @@ const Layout = openHoc(({ classes, clickAway, open, providerDetails, providerInf
<Row className={classes.summary}> <Row className={classes.summary}>
<Col className={classes.logo} middle="xs" start="xs"> <Col className={classes.logo} middle="xs" start="xs">
<Link to="/"> <Link to="/">
<Img alt="Gnosis Team Safe" height={32} src={logo} testId="heading-gnosis-logo" /> <Img alt="Gnosis Team Safe" height={36} src={logo} testId="heading-gnosis-logo" />
</Link> </Link>
</Col> </Col>
<Divider />
<SafeListHeader />
<Divider />
<NetworkLabel />
<Spacer /> <Spacer />
<Provider <Provider
info={providerInfo} info={providerInfo}

View File

@ -2,7 +2,7 @@ import { withStyles } from '@material-ui/core/styles'
import * as React from 'react' import * as React from 'react'
import ConnectButton from 'src/components/ConnectButton' import ConnectButton from 'src/components/ConnectButton'
import CircleDot from 'src/components/Header/components/CircleDot' import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'

View File

@ -2,11 +2,9 @@ import { withStyles } from '@material-ui/core/styles'
import Dot from '@material-ui/icons/FiberManualRecord' import Dot from '@material-ui/icons/FiberManualRecord'
import classNames from 'classnames' import classNames from 'classnames'
import * as React from 'react' import * as React from 'react'
import { EthHashInfo, Identicon } from '@gnosis.pm/safe-react-components'
import CopyBtn from 'src/components/CopyBtn' import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import EtherscanBtn from 'src/components/EtherscanBtn'
import CircleDot from 'src/components/Header/components/CircleDot'
import Identicon from 'src/components/Identicon'
import Spacer from 'src/components/Spacer' import Spacer from 'src/components/Spacer'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button' import Button from 'src/components/layout/Button'
@ -14,7 +12,6 @@ import Hairline from 'src/components/layout/Hairline'
import Img from 'src/components/layout/Img' import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
import { background, connected as connectedBg, lg, md, sm, warning, xs } from 'src/theme/variables' import { background, connected as connectedBg, lg, md, sm, warning, xs } from 'src/theme/variables'
import { upperFirst } from 'src/utils/css' import { upperFirst } from 'src/utils/css'
@ -93,8 +90,6 @@ const styles = () => ({
const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard, provider, userAddress }) => { const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard, provider, userAddress }) => {
const status = connected ? 'Connected' : 'Connection error' const status = connected ? 'Connected' : 'Connection error'
const address = userAddress ? shortVersionOf(userAddress, 4) : 'Address not available'
const identiconAddress = userAddress || 'random'
const color = connected ? 'primary' : 'warning' const color = connected ? 'primary' : 'warning'
return ( return (
@ -102,20 +97,16 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
<Block className={classes.container}> <Block className={classes.container}>
<Row align="center" className={classes.identicon} margin="md"> <Row align="center" className={classes.identicon} margin="md">
{connected ? ( {connected ? (
<Identicon address={identiconAddress} diameter={60} /> <Identicon address={userAddress || 'random'} size="lg" />
) : ( ) : (
<CircleDot circleSize={75} dotRight={25} dotSize={25} dotTop={50} hideDot keySize={30} mode="warning" /> <CircleDot circleSize={75} dotRight={25} dotSize={25} dotTop={50} hideDot keySize={30} mode="warning" />
)} )}
</Row> </Row>
<Block className={classes.user} justify="center"> <Block className={classes.user} justify="center">
<Paragraph className={classes.address} noMargin size="sm"> {userAddress ? (
{address} <EthHashInfo hash={userAddress} showCopyBtn showEtherscanBtn shortenHash={4} network={network} />
</Paragraph> ) : (
{userAddress && ( 'Address not available'
<>
<CopyBtn content={userAddress} increaseZindex />
<EtherscanBtn increaseZindex type="address" value={userAddress} />
</>
)} )}
</Block> </Block>
</Block> </Block>

View File

@ -1,12 +1,11 @@
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import * as React from 'react' import * as React from 'react'
import { EthHashInfo, Text } from '@gnosis.pm/safe-react-components'
import NetworkLabel from '../NetworkLabel' import NetworkLabel from '../NetworkLabel'
import CircleDot from 'src/components/Header/components/CircleDot' import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import Identicon from 'src/components/Identicon'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
import WalletIcon from '../WalletIcon' import WalletIcon from '../WalletIcon'
import { connected as connectedBg, screenSm, sm } from 'src/theme/variables' import { connected as connectedBg, screenSm, sm } from 'src/theme/variables'
@ -69,10 +68,7 @@ interface ProviderInfoProps {
const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInfoProps): React.ReactElement => { const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInfoProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const cutAddress = connected ? shortVersionOf(userAddress, 4) : 'Connection Error' const addressColor = connected ? 'text' : 'warning'
const color = connected ? 'primary' : 'warning'
const identiconAddress = userAddress || 'random'
return ( return (
<> <>
{!connected && <CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={14} mode="warning" />} {!connected && <CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={14} mode="warning" />}
@ -89,10 +85,25 @@ const ProviderInfo = ({ connected, provider, userAddress, network }: ProviderInf
{provider} {provider}
</Paragraph> </Paragraph>
<div className={classes.providerContainer}> <div className={classes.providerContainer}>
{connected && <Identicon address={identiconAddress} className={classes.identicon} diameter={10} />} {connected ? (
<Paragraph className={classes.address} color={color} noMargin size="xs"> <EthHashInfo
hash={userAddress}
shortenHash={4}
showIdenticon
identiconSize="xs"
textColor={addressColor}
textSize="sm"
network={network}
/>
) : (
<Text size="md" color={addressColor}>
Connection Error
</Text>
)}
{/* <Paragraph className={classes.address} color={color} noMargin size="xs">
{cutAddress} {cutAddress}
</Paragraph> </Paragraph> */}
</div> </div>
</Col> </Col>
<Col className={classes.networkLabel} layout="column" start="sm"> <Col className={classes.networkLabel} layout="column" start="sm">

View File

@ -1,7 +1,7 @@
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import * as React from 'react' import * as React from 'react'
import CircleDot from 'src/components/Header/components/CircleDot' import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,83 @@
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import Layout from './components/Layout'
import ConnectDetails from './components/ProviderDetails/ConnectDetails'
import UserDetails from './components/ProviderDetails/UserDetails'
import ProviderAccessible from './components/ProviderInfo/ProviderAccessible'
import ProviderDisconnected from './components/ProviderInfo/ProviderDisconnected'
import {
availableSelector,
loadedSelector,
networkSelector,
providerNameSelector,
userAccountSelector,
} from 'src/logic/wallets/store/selectors'
import { removeProvider } from 'src/logic/wallets/store/actions'
import { onboard } from 'src/components/ConnectButton'
import { loadLastUsedProvider } from 'src/logic/wallets/store/middlewares/providerWatcher'
const HeaderComponent = (): React.ReactElement => {
const provider = useSelector(providerNameSelector)
const userAddress = useSelector(userAccountSelector)
const network = useSelector(networkSelector)
const loaded = useSelector(loadedSelector)
const available = useSelector(availableSelector)
const dispatch = useDispatch()
useEffect(() => {
const tryToConnectToLastUsedProvider = async () => {
const lastUsedProvider = await loadLastUsedProvider()
if (lastUsedProvider) {
const hasSelectedWallet = await onboard.walletSelect(lastUsedProvider)
if (hasSelectedWallet) {
await onboard.walletCheck()
}
}
}
tryToConnectToLastUsedProvider()
}, [])
const openDashboard = () => {
const { wallet } = onboard.getState()
return wallet.type === 'sdk' && wallet.dashboard
}
const onDisconnect = () => {
dispatch(removeProvider())
}
const getProviderInfoBased = () => {
if (!loaded) {
return <ProviderDisconnected />
}
return <ProviderAccessible connected={available} provider={provider} network={network} userAddress={userAddress} />
}
const getProviderDetailsBased = () => {
if (!loaded) {
return <ConnectDetails />
}
return (
<UserDetails
connected={available}
network={network}
onDisconnect={onDisconnect}
openDashboard={openDashboard()}
provider={provider}
userAddress={userAddress}
/>
)
}
const info = getProviderInfoBased()
const details = getProviderDetailsBased()
return <Layout providerDetails={details} providerInfo={info} />
}
export default HeaderComponent

View File

@ -0,0 +1,20 @@
import React from 'react'
import WalletInfo from './index'
export default {
title: 'Layout/WalletInfo',
component: WalletInfo,
}
export const SimpleLayout = (): React.ReactElement => (
<WalletInfo
address="0xEE63624cC4Dd2355B16b35eFaadF3F7450A9438B"
granted={true}
safeName="My Wallet"
balance="$111,111"
onToggleSafeList={() => ({})}
onReceiveClick={console.log}
onNewTransactionClick={console.log}
/>
)

View File

@ -0,0 +1,155 @@
import React from 'react'
import styled from 'styled-components'
import {
Icon,
FixedIcon,
EthHashInfo,
Text,
Identicon,
Button,
CopyToClipboardBtn,
EtherscanButton,
} from '@gnosis.pm/safe-react-components'
import { getNetwork } from 'src/config'
import FlexSpacer from 'src/components/FlexSpacer'
export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN'
const Container = styled.div`
max-width: 200px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`
const IdenticonContainer = styled.div`
width: 100%;
margin: 8px;
display: flex;
justify-content: space-between;
div:first-of-type {
width: 32px;
}
`
const IconContainer = styled.div`
width: 100px;
display: flex;
padding: 8px 0;
justify-content: space-evenly;
`
const StyledButton = styled(Button)`
*:first-child {
margin: 0 4px 0 0;
}
`
const StyledEthHashInfo = styled(EthHashInfo)`
p {
color: ${({ theme }) => theme.colors.placeHolder};
font-size: 14px;
}
`
const StyledLabel = styled.div`
background-color: ${({ theme }) => theme.colors.icon};
margin: 8px 0 0 0 !important;
padding: 4px 8px;
border-radius: 4px;
letter-spacing: 1px;
p {
line-height: 18px;
}
`
const StyledText = styled(Text)`
margin: 8px 0 16px 0;
`
const UnStyledButton = styled.button`
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline-color: ${({ theme }) => theme.colors.separator};
display: flex;
align-items: center;
`
type Props = {
address: string | null
safeName: string
granted: boolean
balance: string | null
onToggleSafeList: () => void
onReceiveClick: () => void
onNewTransactionClick: () => void
}
const SafeHeader = ({
address,
safeName,
balance,
granted,
onToggleSafeList,
onReceiveClick,
onNewTransactionClick,
}: Props): React.ReactElement => {
if (!address) {
return (
<Container>
<IdenticonContainer>
<FlexSpacer />
<div>
<FixedIcon type="notConnected" />
</div>
<UnStyledButton onClick={onToggleSafeList} data-testid={TOGGLE_SIDEBAR_BTN_TESTID}>
<Icon size="md" type="circleDropdown" />
</UnStyledButton>
</IdenticonContainer>
</Container>
)
}
return (
<Container>
<IdenticonContainer>
<FlexSpacer />
<Identicon address={address} size="lg" />
<UnStyledButton onClick={onToggleSafeList} data-testid={TOGGLE_SIDEBAR_BTN_TESTID}>
<Icon size="md" type="circleDropdown" />
</UnStyledButton>
</IdenticonContainer>
<Text size="xl">{safeName}</Text>
<StyledEthHashInfo hash={address} shortenHash={4} textSize="sm" />
<IconContainer>
<UnStyledButton onClick={onReceiveClick}>
<Icon size="sm" type="qrCode" tooltip="Show QR" />
</UnStyledButton>
<CopyToClipboardBtn textToCopy={address} />
<EtherscanButton value={address} network={getNetwork()} />
</IconContainer>
{granted ? null : (
<StyledLabel>
<Text size="sm" color="white">
READ ONLY
</Text>
</StyledLabel>
)}
<StyledText size="xl">{balance}</StyledText>
<StyledButton size="md" disabled={!granted} color="primary" variant="contained" onClick={onNewTransactionClick}>
<FixedIcon type="arrowSentWhite" />
<Text size="lg" color="white">
New Transaction
</Text>
</StyledButton>
</Container>
)
}
export default SafeHeader

View File

@ -0,0 +1,54 @@
import React from 'react'
import Sidebar from './index'
import { ListItemType } from 'src/components/List'
import { Icon } from '@gnosis.pm/safe-react-components'
export default {
title: 'Layout/Sidebar',
component: Sidebar,
}
const items: ListItemType[] = [
{
label: 'Assets',
icon: <Icon size="md" type="assets" />,
href: '#',
},
{
label: 'Settings',
icon: <Icon size="md" type="settings" />,
href: '#',
subItems: [
{
label: 'Safe Details',
href: '#',
},
{
label: 'Owners',
href: '#',
},
{
label: 'Policies',
href: '#',
},
{
label: 'Advanced',
href: '#',
},
],
},
]
export const Base = (): React.ReactElement => (
<Sidebar
items={items}
balance="111"
safeAddress="0xEE63624cC4Dd2355B16b35eFaadF3F7450A9438B"
safeName="someName"
granted={true}
onReceiveClick={console.log}
onNewTransactionClick={console.log}
onToggleSafeList={() => console.log}
/>
)

View File

@ -0,0 +1,90 @@
import React from 'react'
import styled from 'styled-components'
import { Divider, IconText } from '@gnosis.pm/safe-react-components'
import List, { ListItemType } from 'src/components/List'
import SafeHeader from './SafeHeader'
const StyledDivider = styled(Divider)`
margin: 16px -8px 0;
`
const HelpContainer = styled.div`
height: 58px;
`
const HelpCenterLink = styled.a`
height: 30px;
width: 166px;
padding: 10px 0 0 16px;
margin: 10px 0px;
text-decoration: none;
display: block;
&:hover {
border-radius: 8px;
background-color: ${({ theme }) => theme.colors.background};
box-sizing: content-box;
}
p {
font-family: ${({ theme }) => theme.fonts.fontFamily};
font-size: 0.76em;
font-weight: 600;
line-height: 1.5;
letter-spacing: 1px;
color: ${({ theme }) => theme.colors.placeHolder};
text-transform: uppercase;
padding: 0 0 0 4px;
}
`
type Props = {
safeAddress: string | null
safeName: string | null
balance: string | null
granted: boolean
onToggleSafeList: () => void
onReceiveClick: () => void
onNewTransactionClick: () => void
items: ListItemType[]
}
const Sidebar = ({
items,
balance,
safeAddress,
safeName,
granted,
onToggleSafeList,
onReceiveClick,
onNewTransactionClick,
}: Props): React.ReactElement => {
return (
<>
<SafeHeader
address={safeAddress}
safeName={safeName}
granted={granted}
balance={balance}
onToggleSafeList={onToggleSafeList}
onReceiveClick={onReceiveClick}
onNewTransactionClick={onNewTransactionClick}
/>
{items.length ? (
<>
<StyledDivider />
<List items={items} />
</>
) : null}
<HelpContainer>
<StyledDivider />
<HelpCenterLink href="https://help.gnosis-safe.io/en/" target="_blank" title="Help Center of Gnosis Safe">
<IconText text="HELP CENTER" iconSize="md" textSize="md" color="placeHolder" iconType="question" />
</HelpCenterLink>
</HelpContainer>
</>
)
}
export default Sidebar

View File

@ -0,0 +1,58 @@
import React, { useMemo } from 'react'
import { useRouteMatch } from 'react-router-dom'
import { ListItemType } from 'src/components/List'
import ListIcon from 'src/components/List/ListIcon'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
const useSidebarItems = (): ListItemType[] => {
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
const matchSafeWithAction = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress/:safeAction` }) as {
url: string
params: Record<string, string>
}
const sidebarItems = useMemo((): ListItemType[] => {
if (!matchSafe || !matchSafeWithAddress) {
return []
}
return [
{
label: 'ASSETS',
icon: <ListIcon type="assets" />,
selected: matchSafeWithAction?.params.safeAction === 'balances',
href: `${matchSafeWithAddress?.url}/balances`,
},
{
label: 'TRANSACTIONS',
icon: <ListIcon type="transactionsInactive" />,
selected: matchSafeWithAction?.params.safeAction === 'transactions',
href: `${matchSafeWithAddress?.url}/transactions`,
},
{
label: 'AddressBook',
icon: <ListIcon type="addressBook" />,
selected: matchSafeWithAction?.params.safeAction === 'address-book',
href: `${matchSafeWithAddress?.url}/address-book`,
},
{
label: 'Apps',
icon: <ListIcon type="apps" />,
selected: matchSafeWithAction?.params.safeAction === 'apps',
href: `${matchSafeWithAddress?.url}/apps`,
},
{
label: 'Settings',
icon: <ListIcon type="settings" />,
selected: matchSafeWithAction?.params.safeAction === 'settings',
href: `${matchSafeWithAddress?.url}/settings`,
},
]
}, [matchSafe, matchSafeWithAction, matchSafeWithAddress])
return sidebarItems
}
export { useSidebarItems }

View File

@ -0,0 +1,108 @@
import React from 'react'
import styled from 'styled-components'
import { ListItemType } from 'src/components/List'
import Header from './Header'
import Footer from './Footer'
import Sidebar from './Sidebar'
const Grid = styled.div`
height: 100%;
overflow: auto;
background-color: ${({ theme }) => theme.colors.background};
display: grid;
grid-template-columns: 200px 1fr;
grid-template-rows: 54px 1fr;
grid-template-areas:
'topbar topbar'
'sidebar body';
`
const GridTopbarWrapper = styled.nav`
background-color: white;
box-shadow: 0 2px 4px 0 rgba(212, 212, 211, 0.59);
border-bottom: 2px solid ${({ theme }) => theme.colors.separator};
z-index: 999;
grid-area: topbar;
`
const GridSidebarWrapper = styled.aside`
width: 200px;
padding: 8px;
height: 100%;
background-color: ${({ theme }) => theme.colors.white};
border-right: 2px solid ${({ theme }) => theme.colors.separator};
display: flex;
flex-direction: column;
box-sizing: border-box;
grid-area: sidebar;
div:last-of-type {
margin-top: auto;
}
`
const GridBodyWrapper = styled.section`
margin: 0 16px 0 16px;
grid-area: body;
display: flex;
flex-direction: column;
align-content: stretch;
`
export const BodyWrapper = styled.div`
flex: 1 100%;
`
export const FooterWrapper = styled.footer`
margin: 0 16px;
`
type Props = {
sidebarItems: ListItemType[]
safeAddress: string | null
safeName: string | null
balance: string | null
granted: boolean
onToggleSafeList: () => void
onReceiveClick: () => void
onNewTransactionClick: () => void
}
const Layout: React.FC<Props> = ({
balance,
safeAddress,
safeName,
granted,
onToggleSafeList,
onReceiveClick,
onNewTransactionClick,
children,
sidebarItems,
}): React.ReactElement => (
<Grid>
<GridTopbarWrapper>
<Header />
</GridTopbarWrapper>
<GridSidebarWrapper>
<Sidebar
items={sidebarItems}
safeAddress={safeAddress}
safeName={safeName}
balance={balance}
granted={granted}
onToggleSafeList={onToggleSafeList}
onReceiveClick={onReceiveClick}
onNewTransactionClick={onNewTransactionClick}
/>
</GridSidebarWrapper>
<GridBodyWrapper>
<BodyWrapper>{children}</BodyWrapper>
<FooterWrapper>
<Footer />
</FooterWrapper>
</GridBodyWrapper>
</Grid>
)
export default Layout

View File

@ -30,13 +30,13 @@ const useStyles = makeStyles({
interface CopyBtnProps { interface CopyBtnProps {
className?: string className?: string
content: string content: string
increaseZindex?: boolean increaseZIndex?: boolean
} }
const CopyBtn = ({ className = '', content, increaseZindex = false }: CopyBtnProps): React.ReactElement => { const CopyBtn = ({ className = '', content, increaseZIndex = false }: CopyBtnProps): React.ReactElement => {
const [clicked, setClicked] = useState(false) const [clicked, setClicked] = useState(false)
const classes = useStyles() const classes = useStyles()
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {} const customClasses = increaseZIndex ? { popper: classes.increasedPopperZindex } : {}
return ( return (
<Tooltip <Tooltip

View File

@ -17,7 +17,7 @@ interface EtherscanLinkProps {
className?: string className?: string
cut?: number cut?: number
knownAddress?: boolean knownAddress?: boolean
type?: 'tx' | 'address' type: 'tx' | 'address'
value: string value: string
} }

View File

@ -0,0 +1,7 @@
import React from 'react'
// This component is used to create an empty div element to use inside a flex container.
// It can be added into a flex component and use to justify content leaving space around with easier alignment rules.
const FlexSpacer = (): React.ReactElement => <div></div>
export default FlexSpacer

View File

@ -1,6 +0,0 @@
import { fetchProvider, removeProvider } from 'src/logic/wallets/store/actions'
export default {
fetchProvider,
removeProvider,
}

View File

@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 330"><title>gnosis_safe_teams_2019_logo_all_rgb</title><path d="M161,14.2A146.54,146.54,0,1,0,307.56,160.74,146.54,146.54,0,0,0,161,14.2ZM268.27,168.74h-68a41.41,41.41,0,1,1,.32-14.41h67.68a7.21,7.21,0,1,1,0,14.41Z"/><path d="M347.28,203.9h9.86l29.27,51.46h.22l29.26-51.46h9.76v79.6h-9.76V222.84h-.22l-25.9,44.62h-6.62l-25.89-44.62h-.12V283.5h-9.86Z"/><path d="M442.8,261.41V228.9h9.08v29.93c0,12.33,4.71,17,13.34,17s16.26-7.51,16.26-20.51V228.9h9.08v54.6h-9.08v-9.42h-.11a21.28,21.28,0,0,1-18.05,10.09C450.76,284.17,442.8,277,442.8,261.41Z"/><path d="M507.71,201h9.08V283.5h-9.08Z"/><path d="M535.74,267.24V236.75h-7.06V228.9h7.06V210.29l9.08-1V228.9h18v7.85h-18V265.9c0,6.61,1.46,9.75,6.62,9.75a20.58,20.58,0,0,0,9.3-2.8l2.36,8c-3,1.79-6.73,3.36-13.23,3.36C540.22,284.17,535.74,278.23,535.74,267.24Z"/><path d="M573.3,212.42a6.62,6.62,0,1,1,6.61,6.5A6.53,6.53,0,0,1,573.3,212.42Zm2,16.48h9.08v54.6h-9.08Z"/><path d="M597.29,276.43l4.6-6.95c4.26,4.15,9.08,6.62,16.7,6.62,6.62,0,10.43-3.25,10.43-7.4,0-4.82-5-6.17-13-9.08-8.85-3.14-16.36-6.73-16.36-16.71,0-8.41,7.62-14.68,17.93-14.68A28.46,28.46,0,0,1,636.42,235l-4.71,7.18a20.61,20.61,0,0,0-14.46-5.83c-4.26,0-8.41,2.46-8.41,6.61s4.37,6.39,10.31,8.52c11.55,4.49,19.17,6.51,19.06,17.16,0,8.52-6.5,15.58-19.62,15.58A31.37,31.37,0,0,1,597.29,276.43Z"/><path d="M649.76,212.42a6.62,6.62,0,1,1,6.61,6.5A6.53,6.53,0,0,1,649.76,212.42Zm2,16.48h9.08v54.6h-9.08Z"/><path d="M680.48,300.43l2.24-8.08c6.28,3.59,11.77,5.72,19.62,5.72,12.33,0,18.27-7.17,18.27-16.37V273h-.22c-4,5.83-10.54,9.53-18.95,9.53-14.91,0-26.46-10.76-26.46-27,0-15.58,10.77-27.24,26.13-27.24a23.6,23.6,0,0,1,19.17,9.41h.22V228.9h9.08v50.56c0,15.7-7.85,26.91-26.9,26.91A42.46,42.46,0,0,1,680.48,300.43ZM720.84,255c0-11-8-18.39-18.16-18.39-11.1,0-18.5,8-18.5,18.61,0,11.55,8.07,18.84,18.38,18.84C713.55,274.08,720.84,265.9,720.84,255Z"/><path d="M341.67,132.53c0-23.1,14.91-41,40.36-41,11.55,0,24.11,4.82,32.29,14.24l-13.23,11.21A26.79,26.79,0,0,0,382,109.09c-11.32,0-20.74,8.64-20.74,23.44,0,12.89,8,23.43,21.53,23.43a27.69,27.69,0,0,0,13.79-3.48v-9.42H381.14V127.59h33.41v35.77c-7.29,5.6-19.4,9.75-31.73,9.75C359.84,173.11,341.67,158.42,341.67,132.53Z"/><path d="M426.09,117.39h17.49v6.39h.23a21.26,21.26,0,0,1,16.48-7.4c11.32,0,19.62,5.61,19.62,23v32.84H462.42V143.63c0-9.09-3-11.66-8.75-11.66-6.05,0-10.09,4.14-10.09,12.66v27.58H426.09Z"/><path d="M488.65,144.86c0-16.71,12-28.48,29.71-28.48,17.27,0,29.6,11.55,29.6,28.48s-12.33,28.36-29.6,28.36C500.65,173.22,488.65,161.45,488.65,144.86Zm41.6,0c0-7.63-5-12.89-11.89-12.89-7.06,0-12,5.38-12,12.89,0,7.29,5,12.89,12,12.89C525,157.75,530.25,152.37,530.25,144.86Z"/><path d="M553.12,165.15l8.18-11.32c4.6,4.15,9.31,6.16,14.35,6.16,3.48,0,5.27-1.23,5.27-3.25,0-1.79-1.68-2.91-8.52-5-9.2-2.81-17.27-7.29-17.27-17.83,0-11.1,9.09-17.49,20.52-17.49a32.3,32.3,0,0,1,20.74,6.84l-8.3,12.22c-3.81-3.81-8.52-5.72-12.22-5.72-2,0-4.37.9-4.37,3,0,1.68,2,2.92,7.51,4.82,12.11,4.15,18.73,7.29,18.73,18.05,0,10.2-7.29,17.6-22.31,17.6C566.79,173.22,559.17,170.53,553.12,165.15Z"/><path d="M605.58,100.46c0-5.38,4.38-9.86,10.43-9.86s10.54,4.37,10.54,9.86c0,5.72-4.49,10.09-10.54,10.09S605.58,106.18,605.58,100.46Zm1.69,16.93h17.48v54.82H607.27Z"/><path d="M633.16,165.15l8.19-11.32c4.59,4.15,9.3,6.16,14.35,6.16,3.47,0,5.27-1.23,5.27-3.25,0-1.79-1.69-2.91-8.52-5-9.2-2.81-17.27-7.29-17.27-17.83,0-11.1,9.08-17.49,20.52-17.49a32.31,32.31,0,0,1,20.74,6.84l-8.3,12.22c-3.81-3.81-8.52-5.72-12.22-5.72-2,0-4.37.9-4.37,3,0,1.68,2,2.92,7.51,4.82,12.11,4.15,18.72,7.29,18.72,18.05,0,10.2-7.28,17.6-22.31,17.6C646.84,173.22,639.22,170.53,633.16,165.15Z"/><path d="M710,160.55l11-13.34c6.06,6.62,13.35,9.42,18.84,9.42,6.28,0,9-3.14,9-6.73,0-4.37-2.8-6.27-11.77-9.3-12.11-4.15-24.22-9.87-24.22-25.56,0-13.23,11.32-23.21,26.12-23.44,10.43-.22,20,3.82,27.58,10.32L755.92,115.6c-6.84-5.38-11.77-7.51-16.25-7.51s-7.4,2.35-7.4,6.16,2.8,5.83,11.43,9.19c14,5.39,24.78,9,24.78,24.33,0,18.73-16,25.45-29.26,25.45A40.52,40.52,0,0,1,710,160.55Z"/><path d="M774.87,144.86c0-16.82,10.87-28.48,25-28.48A19.66,19.66,0,0,1,815,123.22l.22-.11v-5.72h17.6v54.82h-16.7V166.5l-.23-.12c-3.58,4.6-8.52,6.84-15.13,6.84C785.74,173.22,774.87,161.79,774.87,144.86Zm41.15,0c0-7.51-5-13-11.55-13-6.73,0-11.77,5.16-11.77,13s4.82,12.89,11.77,12.89C810.75,157.75,816,152.71,816,144.86Z"/><path d="M846.51,131.52h-5.27V117.39h5.27v-8.63c0-13.57,7.06-20.18,18-20.18a27.42,27.42,0,0,1,14.8,4.26l-3.25,12.67a17.51,17.51,0,0,0-6.84-2c-3.14,0-5.16,1.57-5.16,5.94v8h10.43v14.13H864.11v40.69h-17.6Z"/><path d="M932.84,149.45H895.5c1.12,6.28,6.39,9.53,13.12,9.53A17.29,17.29,0,0,0,921.4,153l10.09,10.32c-4.6,5.72-12.22,9.86-24.66,9.86-16.6,0-28.71-11.43-28.71-28.47,0-16.6,11.66-28.37,28.26-28.37,15.47,0,26.79,11.55,26.79,27.81C933.17,145.76,933,148.33,932.84,149.45ZM895.62,139h20.51c-1.12-5.38-4.71-8.75-10-8.75S896.74,133.54,895.62,139Z"/></svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,63 +0,0 @@
import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles'
import ExpandLessIcon from '@material-ui/icons/ExpandLess'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import * as React from 'react'
import { connect } from 'react-redux'
import { SidebarContext } from 'src/components/Sidebar'
import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph'
import { safesCountSelector } from 'src/routes/safe/store/selectors'
import { border, md, screenSm, sm, xs } from 'src/theme/variables'
import { AppReduxState } from 'src/store'
export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN'
const useStyles = makeStyles({
container: {
flexGrow: 0,
alignItems: 'center',
padding: `0 ${sm}`,
[`@media (min-width: ${screenSm}px)`]: {
paddingLeft: md,
paddingRight: md,
},
},
counter: {
background: border,
borderRadius: '3px',
lineHeight: 'normal',
marginLeft: sm,
padding: xs,
},
icon: {
marginLeft: sm,
},
})
const { useContext } = React
const SafeListHeader = ({ safesCount }) => {
const classes = useStyles()
const { isOpen, toggleSidebar } = useContext(SidebarContext)
return (
<Col className={classes.container} middle="xs" start="xs">
Safes
<Paragraph className={classes.counter} size="xs" data-testid="safe-counter-heading">
{safesCount}
</Paragraph>
<IconButton
aria-label="Expand Safe List"
className={classes.icon}
data-testid={TOGGLE_SIDEBAR_BTN_TESTID}
onClick={toggleSidebar}
>
{isOpen ? <ExpandLessIcon /> : <ExpandMoreIcon color="secondary" />}
</IconButton>
</Col>
)
}
export default connect((state: AppReduxState) => ({ safesCount: safesCountSelector(state) }), null)(SafeListHeader)

View File

@ -1,95 +0,0 @@
import { withSnackbar } from 'notistack'
import * as React from 'react'
import { connect } from 'react-redux'
import actions from './actions'
import Layout from './components/Layout'
import ConnectDetails from './components/ProviderDetails/ConnectDetails'
import UserDetails from './components/ProviderDetails/UserDetails'
import ProviderAccessible from './components/ProviderInfo/ProviderAccessible'
import ProviderDisconnected from './components/ProviderInfo/ProviderDisconnected'
import selector from './selector'
import { onboard } from 'src/components/ConnectButton'
import { NOTIFICATIONS, showSnackbar } from 'src/logic/notifications'
import { loadLastUsedProvider } from 'src/logic/wallets/store/middlewares/providerWatcher'
import { logComponentStack } from 'src/utils/logBoundaries'
class HeaderComponent extends React.PureComponent<any, any> {
constructor(props) {
super(props)
this.state = {
hasError: false,
}
}
async componentDidMount() {
const lastUsedProvider = await loadLastUsedProvider()
if (lastUsedProvider) {
const hasSelectedWallet = await onboard.walletSelect(lastUsedProvider)
if (hasSelectedWallet) {
await onboard.walletCheck()
}
}
}
componentDidCatch(error, info) {
const { closeSnackbar, enqueueSnackbar } = this.props
this.setState({ hasError: true })
showSnackbar(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG, enqueueSnackbar, closeSnackbar)
logComponentStack(error, info)
}
getOpenDashboard = () => {
const { wallet } = onboard.getState()
return wallet.type === 'sdk' && wallet.dashboard
}
onDisconnect = () => {
const { closeSnackbar, enqueueSnackbar, removeProvider } = this.props
removeProvider(enqueueSnackbar, closeSnackbar)
}
getProviderInfoBased = () => {
const { hasError } = this.state
const { available, loaded, provider, userAddress, network } = this.props
if (hasError || !loaded) {
return <ProviderDisconnected />
}
return <ProviderAccessible connected={available} provider={provider} network={network} userAddress={userAddress} />
}
getProviderDetailsBased = () => {
const { hasError } = this.state
const { available, loaded, network, provider, userAddress } = this.props
if (hasError || !loaded) {
return <ConnectDetails />
}
return (
<UserDetails
connected={available}
network={network}
onDisconnect={this.onDisconnect}
openDashboard={this.getOpenDashboard()}
provider={provider}
userAddress={userAddress}
/>
)
}
render() {
const info = this.getProviderInfoBased()
const details = this.getProviderDetailsBased()
return <Layout providerDetails={details} providerInfo={info} />
}
}
export default connect(selector, actions)(withSnackbar(HeaderComponent))

View File

@ -1,17 +0,0 @@
import { createStructuredSelector } from 'reselect'
import {
availableSelector,
loadedSelector,
networkSelector,
providerNameSelector,
userAccountSelector,
} from 'src/logic/wallets/store/selectors'
export default createStructuredSelector({
provider: providerNameSelector,
userAddress: userAccountSelector,
network: networkSelector,
loaded: loadedSelector,
available: availableSelector,
})

View File

@ -0,0 +1,15 @@
import React from 'react'
import styled from 'styled-components'
import { Icon, IconTypes } from '@gnosis.pm/safe-react-components'
const StyledIcon = styled(Icon)`
min-width: 32px !important;
`
type Props = {
type: IconTypes
}
const ListItemIcon = ({ type }: Props): React.ReactElement => <StyledIcon type={type} color="placeHolder" size="md" />
export default ListItemIcon

View File

@ -0,0 +1,168 @@
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import ListMui from '@material-ui/core/List'
import ListItem, { ListItemProps } from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'
import Collapse from '@material-ui/core/Collapse'
import { FixedIcon } from '@gnosis.pm/safe-react-components'
const StyledListItem = styled(ListItem)<ListItemProps>`
&.MuiButtonBase-root.MuiListItem-root {
margin: 4px 0;
}
&.MuiListItem-button:hover {
border-radius: 8px;
}
&.MuiListItem-root.Mui-selected {
background-color: ${({ theme }) => theme.colors.background};
border-radius: 8px;
color: ${({ theme }) => theme.colors.primary};
span {
color: ${({ theme }) => theme.colors.primary};
}
.icon-color {
fill: ${({ theme }) => theme.colors.primary};
}
}
`
const StyledListSubItem = styled(ListItem)<ListItemProps>`
&.MuiButtonBase-root.MuiListItem-root {
color: ${({ theme }) => theme.colors.text};
}
&.MuiButtonBase-root.MuiListItem-root.Mui-selected {
color: ${({ theme }) => theme.colors.primary};
}
`
const StyledListItemText = styled(ListItemText)`
span {
font-family: ${({ theme }) => theme.fonts.fontFamily};
font-size: 0.76em;
font-weight: 600;
line-height: 1.5;
letter-spacing: 1px;
color: ${({ theme }) => theme.colors.placeHolder};
text-transform: uppercase;
}
`
const StyledListSubItemText = styled(ListItemText)`
span {
text-transform: none;
font-weight: 400;
font-size: 0.85em;
letter-spacing: 0px;
color: ${({ theme }) => theme.colors.secondary};
font-family: ${({ theme }) => theme.fonts.fontFamily};
}
`
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
maxWidth: 200,
backgroundColor: theme.palette.background.paper,
},
nested: {
paddingLeft: theme.spacing(3),
},
}),
)
export type ListItemType = {
label: string
href: string
icon?: React.ReactNode
selected?: boolean
subItems?: ListItemType[]
}
type Props = {
items: ListItemType[]
}
const List = ({ items }: Props): React.ReactElement => {
const classes = useStyles()
const [groupCollapseStatus, setGroupCollapseStatus] = useState({})
const onItemClick = (item: ListItemType) => {
if (item.subItems) {
const cp = { ...groupCollapseStatus }
cp[item.label] = cp[item.label] ? false : true
setGroupCollapseStatus(cp)
}
}
const isSubItemSelected = (item: ListItemType): boolean => {
const res = item.subItems?.find((subItem) => subItem.selected)
return res !== undefined
}
const getListItem = (item: ListItemType, isSubItem = true) => {
const onClick = () => onItemClick(item)
const ListItemAux = isSubItem ? StyledListSubItem : StyledListItem
const ListItemTextAux = isSubItem ? StyledListSubItemText : StyledListItemText
return (
<ListItemAux
button
// For some reason when wrapping a MUI component with styled() component prop gets lost in types
// But this prop is totally valid
// eslint-disable-next-line
// @ts-ignore
component={Link}
to={item.href}
key={item.label}
onClick={onClick}
selected={item.selected || isSubItemSelected(item)}
>
{item.icon && item.icon}
<ListItemTextAux primary={item.label} />
{item.subItems &&
(groupCollapseStatus[item.label] ? <FixedIcon type="chevronUp" /> : <FixedIcon type="chevronDown" />)}
</ListItemAux>
)
}
useEffect(() => {
if (Object.keys(groupCollapseStatus).length) {
return
}
items.forEach((i) => {
if (isSubItemSelected(i)) {
setGroupCollapseStatus({ ...groupCollapseStatus, ...{ [i.label]: true } })
}
})
}, [groupCollapseStatus, items])
return (
<ListMui component="nav" aria-labelledby="nested-list-subheader" className={classes.root}>
{items.map((i) => (
<div key={i.label}>
{getListItem(i, false)}
{i.subItems && (
<Collapse in={groupCollapseStatus[i.label]} timeout="auto" unmountOnExit>
<ListMui component="div" disablePadding>
{i.subItems.map((subItem) => getListItem(subItem))}
</ListMui>
</Collapse>
)}
</div>
))}
</ListMui>
)
}
export default List

View File

@ -0,0 +1,37 @@
import React from 'react'
import List, { ListItemType } from '.'
import ListIcon from './ListIcon'
const items: ListItemType[] = [
{
label: 'Assets',
icon: <ListIcon type="assets" />,
href: '#',
subItems: [
{
icon: <ListIcon type="assets" />,
label: 'Coins',
href: '#',
},
{
icon: <ListIcon type="collectibles" />,
selected: true,
label: 'Collectives',
href: '#',
},
],
},
{
label: 'Transactions',
icon: <ListIcon type="transactionsInactive" />,
href: '#',
},
]
export default {
title: 'Data Display/List',
component: List,
}
export const SimpleList = (): React.ReactElement => <List items={items} />

View File

@ -11,6 +11,10 @@ const Wrapper = styled.div``
const Item = styled.div` const Item = styled.div`
border-bottom: solid 2px rgb(232, 231, 230); border-bottom: solid 2px rgb(232, 231, 230);
&:last-child {
border-bottom: none;
}
.container { .container {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;

View File

@ -5,8 +5,8 @@ import React from 'react'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { ThemeProvider } from 'styled-components' import { ThemeProvider } from 'styled-components'
import Loader from '../Loader' import Loader from 'src/components/Loader'
import PageFrame from '../layout/PageFrame' import App from 'src/components/App'
import AppRoutes from 'src/routes' import AppRoutes from 'src/routes'
import { history, store } from 'src/store' import { history, store } from 'src/store'
@ -16,13 +16,11 @@ import { wrapInSuspense } from 'src/utils/wrapInSuspense'
import './index.module.scss' import './index.module.scss'
import './OnboardCustom.module.scss' import './OnboardCustom.module.scss'
const Root = () => ( const Root = (): React.ReactElement => (
<ThemeProvider theme={styledTheme}> <ThemeProvider theme={styledTheme}>
<Provider store={store}> <Provider store={store}>
<MuiThemeProvider theme={theme}> <MuiThemeProvider theme={theme}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>{<App>{wrapInSuspense(<AppRoutes />, <Loader />)}</App>}</ConnectedRouter>
<PageFrame>{wrapInSuspense(<AppRoutes />, <Loader />)}</PageFrame>
</ConnectedRouter>
</MuiThemeProvider> </MuiThemeProvider>
</Provider> </Provider>
</ThemeProvider> </ThemeProvider>

View File

@ -23,7 +23,7 @@ const useStyles = makeStyles({
}, },
}) })
const DefaultBadge = () => { const DefaultBadge = (): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
return ( return (

View File

Before

Width:  |  Height:  |  Size: 482 B

After

Width:  |  Height:  |  Size: 482 B

View File

@ -0,0 +1,147 @@
import MuiList from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import { makeStyles } from '@material-ui/core/styles'
import { EthHashInfo, Icon, Text, ButtonLink } from '@gnosis.pm/safe-react-components'
import * as React from 'react'
import styled from 'styled-components'
import { SafeRecord } from 'src/logic/safe/store/models/safe'
import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe'
import { SetDefaultSafe } from 'src/logic/safe/store/actions/setDefaultSafe'
import { getNetwork } from 'src/config'
import DefaultBadge from './DefaultBadge'
import Hairline from 'src/components/layout/Hairline'
import Link from 'src/components/layout/Link'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
export const SIDEBAR_SAFELIST_ROW_TESTID = 'SIDEBAR_SAFELIST_ROW_TESTID'
const StyledIcon = styled(Icon)`
margin-right: 4px;
`
const AddressWrapper = styled.div`
display: flex;
padding: 5px 0;
width: 100%;
justify-content: space-between;
> nth-child(2) {
display: flex;
align-items: center;
}
`
const StyledButtonLink = styled(ButtonLink)`
visibility: hidden;
white-space: nowrap;
`
const AddressDetails = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 175px;
div {
margin-left: 0px;
padding: 5px 20px;
img {
margin-right: 5px;
}
p {
margin-top: 3px;
}
}
`
const useStyles = makeStyles({
list: {
height: '100%',
overflowX: 'hidden',
overflowY: 'auto',
padding: 0,
},
listItemRoot: {
paddingTop: 0,
paddingBottom: 0,
'&:hover .safeListMakeDefaultButton': {
visibility: 'initial',
},
'&:focus .safeListMakeDefaultButton': {
visibility: 'initial',
},
},
noIcon: {
visibility: 'hidden',
width: '28px',
},
})
type Props = {
currentSafe: string | null
defaultSafe: DefaultSafe
safes: SafeRecord[]
onSafeClick: () => void
setDefaultSafe: SetDefaultSafe
}
const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes, setDefaultSafe }: Props): React.ReactElement => {
const classes = useStyles()
return (
<MuiList className={classes.list}>
{safes.map((safe) => (
<React.Fragment key={safe.address}>
<Link
data-testid={SIDEBAR_SAFELIST_ROW_TESTID}
onClick={onSafeClick}
to={`${SAFELIST_ADDRESS}/${safe.address}/balances`}
>
<ListItem classes={{ root: classes.listItemRoot }}>
{sameAddress(currentSafe, safe.address) ? (
<StyledIcon type="check" size="md" color="primary" />
) : (
<div className={classes.noIcon}>placeholder</div>
)}
<AddressWrapper>
<EthHashInfo
hash={safe.address}
name={safe.name}
showIdenticon
shortenHash={4}
network={getNetwork()}
/>
<AddressDetails>
<Text size="xl">{`${formatAmount(safe.ethBalance)} ETH`}</Text>
{sameAddress(defaultSafe, safe.address) ? (
<DefaultBadge />
) : (
<StyledButtonLink
className="safeListMakeDefaultButton"
textSize="sm"
onClick={() => {
setDefaultSafe(safe.address)
}}
color="primary"
>
Make default
</StyledButtonLink>
)}
</AddressDetails>
</AddressWrapper>
</ListItem>
</Link>
<Hairline />
</React.Fragment>
))}
</MuiList>
)
}
export default SafeList

View File

@ -15,14 +15,14 @@ import Hairline from 'src/components/layout/Hairline'
import Link from 'src/components/layout/Link' import Link from 'src/components/layout/Link'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { WELCOME_ADDRESS } from 'src/routes/routes' import { WELCOME_ADDRESS } from 'src/routes/routes'
import setDefaultSafe from 'src/routes/safe/store/actions/setDefaultSafe' import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe'
import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
const { useEffect, useMemo, useState } = React const { useEffect, useMemo, useState } = React
export const SidebarContext = React.createContext({ export const SafeListSidebarContext = React.createContext({
isOpen: false, isOpen: false,
toggleSidebar: () => {}, toggleSidebar: () => {},
}) })
@ -35,7 +35,7 @@ const filterBy = (filter, safes) =>
safe.name.toLowerCase().includes(filter.toLowerCase()), safe.name.toLowerCase().includes(filter.toLowerCase()),
) )
const Sidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeAction }) => { const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeAction }) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [filter, setFilter] = useState('') const [filter, setFilter] = useState('')
const classes = useSidebarStyles() const classes = useSidebarStyles()
@ -74,7 +74,7 @@ const Sidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeActi
const filteredSafes = useMemo(() => filterBy(filter, safes), [safes, filter]) const filteredSafes = useMemo(() => filterBy(filter, safes), [safes, filter])
return ( return (
<SidebarContext.Provider value={{ isOpen, toggleSidebar }}> <SafeListSidebarContext.Provider value={{ isOpen, toggleSidebar }}>
<Drawer <Drawer
classes={{ paper: classes.sidebarPaper }} classes={{ paper: classes.sidebarPaper }}
className={classes.sidebar} className={classes.sidebar}
@ -119,7 +119,7 @@ const Sidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeActi
/> />
</Drawer> </Drawer>
{children} {children}
</SidebarContext.Provider> </SafeListSidebarContext.Provider>
) )
} }
@ -130,4 +130,4 @@ export default connect(
currentSafe: safeParamAddressFromStateSelector(state), currentSafe: safeParamAddressFromStateSelector(state),
}), }),
{ setDefaultSafeAction: setDefaultSafe }, { setDefaultSafeAction: setDefaultSafe },
)(Sidebar) )(SafeListSidebar)

View File

@ -1,6 +1,6 @@
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { safesListSelector } from 'src/routes/safe/store/selectors' import { safesListSelector } from 'src/logic/safe/store/selectors'
export const sortedSafeListSelector = createSelector(safesListSelector, (safes) => export const sortedSafeListSelector = createSelector(safesListSelector, (safes) =>
safes.sort((a, b) => (a.name > b.name ? 1 : -1)), safes.sort((a, b) => (a.name > b.name ? 1 : -1)),

View File

@ -1,126 +0,0 @@
import MuiList from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import { makeStyles } from '@material-ui/core/styles'
import * as React from 'react'
import DefaultBadge from './DefaultBadge'
import check from 'src/assets/icons/check.svg'
import Identicon from 'src/components/Identicon'
import ButtonLink from 'src/components/layout/ButtonLink'
import Hairline from 'src/components/layout/Hairline'
import Img from 'src/components/layout/Img'
import Link from 'src/components/layout/Link'
import Paragraph from 'src/components/layout/Paragraph'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { sameAddress, shortVersionOf } from 'src/logic/wallets/ethAddresses'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { disabled, md, mediumFontSize, primary, sm } from 'src/theme/variables'
export const SIDEBAR_SAFELIST_ROW_TESTID = 'SIDEBAR_SAFELIST_ROW_TESTID'
const useStyles = makeStyles({
icon: {
marginRight: sm,
},
checkIcon: {
marginRight: '10px',
},
list: {
height: '100%',
overflowX: 'hidden',
overflowY: 'auto',
padding: 0,
},
listItemRoot: {
paddingTop: 0,
paddingBottom: 0,
'&:hover $makeDefaultBtn': {
visibility: 'initial',
},
'&:focus $makeDefaultBtn': {
visibility: 'initial',
},
},
safeName: {
color: primary,
overflowWrap: 'break-word',
},
safeAddress: {
color: disabled,
fontSize: mediumFontSize,
},
makeDefaultBtn: {
padding: 0,
marginLeft: md,
visibility: 'hidden',
},
noIcon: {
visibility: 'hidden',
width: '28px',
},
})
const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes, setDefaultSafe }) => {
const classes = useStyles()
return (
<MuiList className={classes.list}>
{safes.map((safe) => (
<React.Fragment key={safe.address}>
<Link
data-testid={SIDEBAR_SAFELIST_ROW_TESTID}
onClick={onSafeClick}
to={`${SAFELIST_ADDRESS}/${safe.address}/balances`}
>
<ListItem classes={{ root: classes.listItemRoot }}>
{sameAddress(currentSafe, safe.address) ? (
<ListItemIcon>
<Img alt="check" className={classes.checkIcon} src={check} />
</ListItemIcon>
) : (
<div className={classes.noIcon}>placeholder</div>
)}
<ListItemIcon>
<Identicon address={safe.address} className={classes.icon} diameter={32} />
</ListItemIcon>
<ListItemText
classes={{
primary: classes.safeName,
secondary: classes.safeAddress,
}}
primary={safe.name}
secondary={shortVersionOf(safe.address, 4)}
/>
<Paragraph color="primary" size="lg">
{`${formatAmount(safe.ethBalance)} ETH`}
</Paragraph>
{sameAddress(defaultSafe, safe.address) ? (
<DefaultBadge />
) : (
<ButtonLink
className={classes.makeDefaultBtn}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setDefaultSafe(safe.address)
}}
size="sm"
>
Make default
</ButtonLink>
)}
</ListItem>
</Link>
<Hairline />
</React.Fragment>
))}
</MuiList>
)
}
export default SafeList

View File

@ -59,7 +59,7 @@ export const ok = (): undefined => undefined
export const mustBeEthereumAddress = memoize( export const mustBeEthereumAddress = memoize(
(address: string): ValidatorReturnType => { (address: string): ValidatorReturnType => {
const startsWith0x = address.startsWith('0x') const startsWith0x = address?.startsWith('0x')
const isAddress = getWeb3().utils.isAddress(address) const isAddress = getWeb3().utils.isAddress(address)
return startsWith0x && isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name' return startsWith0x && isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name'

View File

@ -4,7 +4,7 @@
display: flex; display: flex;
flex: 1 0 auto; flex: 1 0 auto;
flex-direction: column; flex-direction: column;
padding: 96px 200px 0px 200px; padding: 12px 0 0 0;
} }
@media only screen and (max-width: #{$screenLg}px) { @media only screen and (max-width: #{$screenLg}px) {

View File

@ -1,6 +0,0 @@
.frame {
display: flex;
flex-direction: column;
flex: 1 1 auto;
max-width: 100%;
}

View File

@ -1,84 +0,0 @@
import { withStyles } from '@material-ui/core/styles'
import { SnackbarProvider } from 'notistack'
import * as React from 'react'
import { connect } from 'react-redux'
import AlertIcon from './assets/alert.svg'
import CheckIcon from './assets/check.svg'
import ErrorIcon from './assets/error.svg'
import InfoIcon from './assets/info.svg'
import styles from './index.module.scss'
import CookiesBanner from 'src/components/CookiesBanner'
import Footer from 'src/components/Footer'
import Header from 'src/components/Header'
import Notifier from 'src/components/Notifier'
import SidebarProvider from 'src/components/Sidebar'
import Backdrop from 'src/components/layout/Backdrop'
import Img from 'src/components/layout/Img'
import { getNetwork } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
import { networkSelector } from 'src/logic/wallets/store/selectors'
import { AppReduxState } from 'src/store'
const notificationStyles = {
success: {
background: '#fff',
},
error: {
background: '#ffe6ea',
},
warning: {
background: '#fff3e2',
},
info: {
background: '#fff',
},
}
const desiredNetwork = getNetwork()
const PageFrame = ({ children, classes, currentNetwork }) => {
const isWrongNetwork = currentNetwork !== ETHEREUM_NETWORK.UNKNOWN && currentNetwork !== desiredNetwork
return (
<div className={styles.frame}>
<Backdrop isOpen={isWrongNetwork} />
<SnackbarProvider
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
classes={{
variantSuccess: classes.success,
variantError: classes.error,
variantWarning: classes.warning,
variantInfo: classes.info,
}}
iconVariant={{
error: <Img alt="Error" src={ErrorIcon} />,
info: <Img alt="Info" src={InfoIcon} />,
success: <Img alt="Success" src={CheckIcon} />,
warning: <Img alt="Warning" src={AlertIcon} />,
}}
maxSnack={5}
>
<>
<Notifier />
<SidebarProvider>
<Header />
{children}
<Footer />
</SidebarProvider>
</>
</SnackbarProvider>
<CookiesBanner />
</div>
)
}
export default withStyles(notificationStyles)(
connect(
(state: AppReduxState) => ({
currentNetwork: networkSelector(state),
}),
null,
)(PageFrame),
)

View File

@ -59,6 +59,9 @@ export const getTxServiceUriFrom = (safeAddress) =>
export const getIncomingTxServiceUriTo = (safeAddress) => export const getIncomingTxServiceUriTo = (safeAddress) =>
`safes/${safeAddress}/incoming-transfers/` `safes/${safeAddress}/incoming-transfers/`
export const getAllTransactionsUriFrom = (safeAddress: string): string =>
`safes/${safeAddress}/all-transactions/`
export const getSafeCreationTxUri = (safeAddress) => `safes/${safeAddress}/creation/` export const getSafeCreationTxUri = (safeAddress) => `safes/${safeAddress}/creation/`
export const getRelayUrl = () => getConfig()[RELAY_API_URL] export const getRelayUrl = () => getConfig()[RELAY_API_URL]

View File

@ -5,8 +5,8 @@ import ReactDOM from 'react-dom'
import Root from 'src/components/Root' import Root from 'src/components/Root'
import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage' import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage'
import loadActiveTokens from 'src/logic/tokens/store/actions/loadActiveTokens' import loadActiveTokens from 'src/logic/tokens/store/actions/loadActiveTokens'
import loadDefaultSafe from 'src/routes/safe/store/actions/loadDefaultSafe' import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe'
import loadSafesFromStorage from 'src/routes/safe/store/actions/loadSafesFromStorage' import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
import { store } from 'src/store' import { store } from 'src/store'
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] }) BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })

View File

@ -3,7 +3,7 @@ import { List } from 'immutable'
import { loadAddressBook } from 'src/logic/addressBook/store/actions/loadAddressBook' import { loadAddressBook } from 'src/logic/addressBook/store/actions/loadAddressBook'
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook' import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
import { getAddressBookFromStorage } from 'src/logic/addressBook/utils' import { getAddressBookFromStorage } from 'src/logic/addressBook/utils'
import { safesListSelector } from 'src/routes/safe/store/selectors' import { safesListSelector } from 'src/logic/safe/store/selectors'
const loadAddressBookFromStorage = () => async (dispatch, getState) => { const loadAddressBookFromStorage = () => async (dispatch, getState) => {
try { try {

View File

@ -4,7 +4,7 @@ import { createSelector } from 'reselect'
import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook' import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook'
import { AddressBookMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d' import { AddressBookMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d'
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
export const addressBookMapSelector = (state: AppReduxState): AddressBookMap => export const addressBookMapSelector = (state: AppReduxState): AddressBookMap =>
state[ADDRESS_BOOK_REDUCER_ID].get('addressBook') state[ADDRESS_BOOK_REDUCER_ID].get('addressBook')

View File

@ -1,7 +1,7 @@
import { List } from 'immutable' import { List } from 'immutable'
import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { AddressBookEntryProps } from './../model/addressBook' import { AddressBookEntryProps } from './../model/addressBook'
import { SafeOwner } from 'src/routes/safe/store/models/safe' import { SafeOwner } from 'src/logic/safe/store/models/safe'
const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY' const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY'

View File

@ -3,7 +3,7 @@ import { NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/O
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles' import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
import { safeActiveAssetsSelector } from 'src/routes/safe/store/selectors' import { safeActiveAssetsSelector } from 'src/logic/safe/store/selectors'
export const nftAssetsSelector = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID] export const nftAssetsSelector = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
export const nftTokensSelector = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID] export const nftTokensSelector = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]

View File

@ -5,7 +5,7 @@ import {
CurrencyReducerMap, CurrencyReducerMap,
CurrencyValuesState, CurrencyValuesState,
} from 'src/logic/currencyValues/store/reducer/currencyValues' } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
import { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues' import { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'

View File

@ -2,14 +2,14 @@ import { IconButton } from '@material-ui/core'
import { Close as IconClose } from '@material-ui/icons' import { Close as IconClose } from '@material-ui/icons'
import * as React from 'react' import * as React from 'react'
import { NOTIFICATIONS } from './notificationTypes' import { Notification, NOTIFICATIONS } from './notificationTypes'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar' import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { getAppInfoFromOrigin } from 'src/routes/safe/components/Apps/utils' import { getAppInfoFromOrigin } from 'src/routes/safe/components/Apps/utils'
import { store } from 'src/store' import { store } from 'src/store'
const setNotificationOrigin = (notification, origin) => { const setNotificationOrigin = (notification: Notification, origin: string): Notification => {
if (!origin) { if (!origin) {
return notification return notification
} }
@ -18,18 +18,18 @@ const setNotificationOrigin = (notification, origin) => {
return { ...notification, message: `${appInfo.name}: ${notification.message}` } return { ...notification, message: `${appInfo.name}: ${notification.message}` }
} }
const getStandardTxNotificationsQueue = (origin) => { const getStandardTxNotificationsQueue = (
return { origin: string,
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin), ): Record<string, Record<string, Notification> | Notification> => ({
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin), beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin), pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
afterExecution: { afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin),
noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin), afterExecution: {
moreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG, origin), noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin),
}, moreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG, origin),
afterExecutionError: setNotificationOrigin(NOTIFICATIONS.TX_FAILED_MSG, origin), },
} afterExecutionError: setNotificationOrigin(NOTIFICATIONS.TX_FAILED_MSG, origin),
} })
const waitingTransactionNotificationsQueue = { const waitingTransactionNotificationsQueue = {
beforeExecution: null, beforeExecution: null,
@ -40,7 +40,7 @@ const waitingTransactionNotificationsQueue = {
afterExecutionError: null, afterExecutionError: null,
} }
const getConfirmationTxNotificationsQueue = (origin) => { const getConfirmationTxNotificationsQueue = (origin: string) => {
return { return {
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin), beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_CONFIRMATION_PENDING_MSG, origin), pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_CONFIRMATION_PENDING_MSG, origin),
@ -53,7 +53,7 @@ const getConfirmationTxNotificationsQueue = (origin) => {
} }
} }
const getCancellationTxNotificationsQueue = (origin) => { const getCancellationTxNotificationsQueue = (origin: string) => {
return { return {
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin), beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin), pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
@ -199,9 +199,13 @@ export const getNotificationsFromTxType: any = (txType, origin) => {
return notificationsQueue return notificationsQueue
} }
export const enhanceSnackbarForAction: any = (notification, key, onClick) => ({ export const enhanceSnackbarForAction = (
notification: Notification,
key?: number | string,
onClick?: () => void,
): Notification => ({
...notification, ...notification,
key, key: key || notification.key,
options: { options: {
...notification.options, ...notification.options,
onClick, onClick,
@ -213,14 +217,3 @@ export const enhanceSnackbarForAction: any = (notification, key, onClick) => ({
), ),
}, },
}) })
export const showSnackbar: any = (notification, enqueueSnackbar, closeSnackbar) =>
enqueueSnackbar(notification.message, {
...notification.options,
// eslint-disable-next-line react/display-name
action: (key) => (
<IconButton onClick={() => closeSnackbar(key)}>
<IconClose />
</IconButton>
),
})

View File

@ -1,3 +1,5 @@
import { OptionsObject } from 'notistack'
import { getNetwork } from 'src/config' import { getNetwork } from 'src/config'
import { capitalize } from 'src/utils/css' import { capitalize } from 'src/utils/css'
@ -9,7 +11,50 @@ export const INFO = 'info'
const shortDuration = 5000 const shortDuration = 5000
const longDuration = 10000 const longDuration = 10000
export const NOTIFICATIONS = { export type NotificationId = keyof typeof NOTIFICATION_IDS
export type Notification = {
message: string
options: OptionsObject
key?: number | string
}
const NOTIFICATION_IDS = {
CONNECT_WALLET_MSG: 'CONNECT_WALLET_MSG',
CONNECT_WALLET_READ_MODE_MSG: 'CONNECT_WALLET_READ_MODE_MSG',
WALLET_CONNECTED_MSG: 'WALLET_CONNECTED_MSG',
WALLET_DISCONNECTED_MSG: 'WALLET_DISCONNECTED_MSG',
UNLOCK_WALLET_MSG: 'UNLOCK_WALLET_MSG',
CONNECT_WALLET_ERROR_MSG: 'CONNECT_WALLET_ERROR_MSG',
SIGN_TX_MSG: 'SIGN_TX_MSG',
TX_PENDING_MSG: 'TX_PENDING_MSG',
TX_REJECTED_MSG: 'TX_REJECTED_MSG',
TX_EXECUTED_MSG: 'TX_EXECUTED_MSG',
TX_CANCELLATION_EXECUTED_MSG: 'TX_CANCELLATION_EXECUTED_MSG',
TX_FAILED_MSG: 'TX_FAILED_MSG',
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: 'TX_EXECUTED_MORE_CONFIRMATIONS_MSG',
TX_WAITING_MSG: 'TX_WAITING_MSG',
TX_INCOMING_MSG: 'TX_INCOMING_MSG',
TX_CONFIRMATION_PENDING_MSG: 'TX_CONFIRMATION_PENDING_MSG',
TX_CONFIRMATION_EXECUTED_MSG: 'TX_CONFIRMATION_EXECUTED_MSG',
TX_CONFIRMATION_FAILED_MSG: 'TX_CONFIRMATION_FAILED_MSG',
SAFE_NAME_CHANGED_MSG: 'SAFE_NAME_CHANGED_MSG',
OWNER_NAME_CHANGE_EXECUTED_MSG: 'OWNER_NAME_CHANGE_EXECUTED_MSG',
SIGN_SETTINGS_CHANGE_MSG: 'SIGN_SETTINGS_CHANGE_MSG',
SETTINGS_CHANGE_PENDING_MSG: 'SETTINGS_CHANGE_PENDING_MSG',
SETTINGS_CHANGE_REJECTED_MSG: 'SETTINGS_CHANGE_REJECTED_MSG',
SETTINGS_CHANGE_EXECUTED_MSG: 'SETTINGS_CHANGE_EXECUTED_MSG',
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: 'SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG',
SETTINGS_CHANGE_FAILED_MSG: 'SETTINGS_CHANGE_FAILED_MSG',
RINKEBY_VERSION_MSG: 'RINKEBY_VERSION_MSG',
WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG',
ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS',
ADDRESS_BOOK_EDIT_ENTRY_SUCCESS: 'ADDRESS_BOOK_EDIT_ENTRY_SUCCESS',
ADDRESS_BOOK_DELETE_ENTRY_SUCCESS: 'ADDRESS_BOOK_DELETE_ENTRY_SUCCESS',
SAFE_NEW_VERSION_AVAILABLE: 'SAFE_NEW_VERSION_AVAILABLE',
}
export const NOTIFICATIONS: Record<NotificationId, Notification> = {
// Wallet Connection // Wallet Connection
CONNECT_WALLET_MSG: { CONNECT_WALLET_MSG: {
message: 'Please connect wallet to continue', message: 'Please connect wallet to continue',

View File

@ -1,15 +0,0 @@
import { createAction } from 'redux-actions'
export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
const addSnackbar = createAction(ENQUEUE_SNACKBAR)
const enqueueSnackbar = (notification) => (dispatch) => {
const newNotification = {
...notification,
key: notification.key || new Date().getTime(),
}
dispatch(addSnackbar(newNotification))
}
export default enqueueSnackbar

View File

@ -0,0 +1,43 @@
import React from 'react'
import { AnyAction } from 'redux'
import { ThunkAction } from 'redux-thunk'
import { createAction } from 'redux-actions'
import { IconButton } from '@material-ui/core'
import { Close as IconClose } from '@material-ui/icons'
import { Notification } from 'src/logic/notifications/notificationTypes'
import closeSnackbarAction from './closeSnackbar'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { AppReduxState } from 'src/store'
export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
const addSnackbar = createAction(ENQUEUE_SNACKBAR)
const enqueueSnackbar = (
notification: Notification,
key?: string | number,
onClick?: () => void,
): ThunkAction<string | number, AppReduxState, undefined, AnyAction> => (dispatch: Dispatch) => {
key = notification.key || new Date().getTime() + Math.random()
const newNotification = {
...notification,
key,
options: {
...notification.options,
onClick,
// eslint-disable-next-line react/display-name
action: (actionKey) => (
<IconButton onClick={() => dispatch(closeSnackbarAction({ key: actionKey }))}>
<IconClose />
</IconButton>
),
},
}
dispatch(addSnackbar(newNotification))
return key
}
export default enqueueSnackbar

View File

@ -8,7 +8,7 @@ import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAsse
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens' import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances' import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances'
import { Dispatch } from 'src/routes/safe/store/actions/types.d' import { Dispatch } from 'src/logic/safe/store/actions/types.d'
export const useFetchTokens = (safeAddress: string): void => { export const useFetchTokens = (safeAddress: string): void => {
const dispatch = useDispatch<Dispatch>() const dispatch = useDispatch<Dispatch>()

View File

@ -4,11 +4,11 @@ import { useDispatch } from 'react-redux'
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage' import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe' import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchLatestMasterContractVersion from 'src/routes/safe/store/actions/fetchLatestMasterContractVersion' import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion'
import fetchSafe from 'src/routes/safe/store/actions/fetchSafe' import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions' import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import fetchSafeCreationTx from 'src/routes/safe/store/actions/fetchSafeCreationTx' import fetchSafeCreationTx from 'src/logic/safe/store/actions/fetchSafeCreationTx'
import { Dispatch } from 'src/routes/safe/store/actions/types.d' import { Dispatch } from 'src/logic/safe/store/actions/types.d'
export const useLoadSafe = (safeAddress: string): void => { export const useLoadSafe = (safeAddress: string): void => {
const dispatch = useDispatch<Dispatch>() const dispatch = useDispatch<Dispatch>()

View File

@ -0,0 +1,71 @@
import { useState, useMemo } from 'react'
const INITIAL_STATE = {
sendFunds: {
isOpen: false,
selectedToken: undefined,
},
showReceive: false,
}
type Response = {
onShow: (action: string) => void
onHide: (action: string) => void
showSendFunds: (token: string) => void
hideSendFunds: () => void
safeActionsState: Record<string, unknown>
}
const useSafeActions = (): Response => {
const [safeActionsState, setSafeActionsState] = useState(INITIAL_STATE)
const onShow = useMemo(
() => (action) => {
setSafeActionsState((prevState) => ({
...prevState,
[`show${action}`]: true,
}))
},
[],
)
const onHide = useMemo(
() => (action) => {
setSafeActionsState((prevState) => ({
...prevState,
[`show${action}`]: false,
}))
},
[],
)
const showSendFunds = useMemo(
() => (token) => {
setSafeActionsState((prevState) => ({
...prevState,
sendFunds: {
isOpen: true,
selectedToken: token,
},
}))
},
[],
)
const hideSendFunds = useMemo(
() => () => {
setSafeActionsState((prevState) => ({
...prevState,
sendFunds: {
isOpen: false,
selectedToken: undefined,
},
}))
},
[],
)
return { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds }
}
export default useSafeActions

View File

@ -3,9 +3,9 @@ import { batch, useDispatch } from 'react-redux'
import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles' import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchEtherBalance from 'src/routes/safe/store/actions/fetchEtherBalance' import fetchEtherBalance from 'src/logic/safe/store/actions/fetchEtherBalance'
import { checkAndUpdateSafe } from 'src/routes/safe/store/actions/fetchSafe' import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions' import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import { TIMEOUT } from 'src/utils/constants' import { TIMEOUT } from 'src/utils/constants'
export const useSafeScheduledUpdates = (safeAddress: string): void => { export const useSafeScheduledUpdates = (safeAddress: string): void => {
@ -36,11 +36,11 @@ export const useSafeScheduledUpdates = (safeAddress: string): void => {
if (safeAddress) { if (safeAddress) {
fetchSafeData(safeAddress) fetchSafeData(safeAddress)
}
return () => { return () => {
mounted = false mounted = false
clearTimeout(timer.current) clearTimeout(timer.current)
}
} }
}, [dispatch, safeAddress]) }, [dispatch, safeAddress])
} }

View File

@ -1,6 +1,6 @@
import { getNewTxNonce, shouldExecuteTransaction } from 'src/routes/safe/store/actions/utils' import { getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { TxServiceModel } from 'src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions' import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
describe('Store actions utils > getNewTxNonce', () => { describe('Store actions utils > getNewTxNonce', () => {
it(`Should return passed predicted transaction nonce if it's a valid value`, async () => { it(`Should return passed predicted transaction nonce if it's a valid value`, async () => {

View File

@ -0,0 +1,36 @@
import { List } from 'immutable'
import { createAction } from 'redux-actions'
import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import { safesListSelector } from 'src/logic/safe/store/selectors'
import { Dispatch } from 'redux'
import { AppReduxState } from 'src/store'
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
export const ADD_SAFE = 'ADD_SAFE'
export const buildOwnersFrom = (names: string[], addresses: string[]): List<SafeOwner> => {
const owners = names.map((name, index) => makeOwner({ name, address: addresses[index] }))
return List(owners)
}
export const addSafe = createAction(ADD_SAFE, (safe) => ({
safe,
}))
const saveSafe = (safe: SafeRecordProps) => (dispatch: Dispatch, getState: () => AppReduxState): void => {
const state = getState()
const safeList = safesListSelector(state)
dispatch(addSafe(safe))
if (safeList.size === 0) {
dispatch(setDefaultSafe(safe.address))
}
}
export default saveSafe

View File

@ -0,0 +1,89 @@
import axios, { AxiosResponse } from 'axios'
import { getAllTransactionsUriFrom, getTxServiceHost } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
import { Transaction } from '../../models/types/transactions'
export type ServiceUriParams = {
safeAddress: string
limit: number
offset: number
orderBy?: string // todo: maybe this should be key of MultiSigTransaction | keyof EthereumTransaction
queued?: boolean
trusted?: boolean
}
type TransactionDTO = {
count: number
next?: string
previous?: string
results: Transaction[]
}
const getAllTransactionsUri = (safeAddress: string): string => {
const host = getTxServiceHost()
const address = checksumAddress(safeAddress)
const base = getAllTransactionsUriFrom(address)
return `${host}${base}`
}
const fetchAllTransactions = async (
urlParams: ServiceUriParams,
eTag: string | null,
): Promise<{ responseEtag: string; results: Transaction[]; count?: number }> => {
const { safeAddress, limit, offset, orderBy, queued, trusted } = urlParams
try {
const url = getAllTransactionsUri(safeAddress)
const config = {
params: {
limit,
offset,
orderBy,
queued,
trusted,
},
headers: eTag ? { 'If-None-Match': eTag } : undefined,
}
const response: AxiosResponse<TransactionDTO> = await axios.get(url, config)
if (response.data.count > 0) {
const { etag } = response.headers
if (eTag !== etag) {
return {
responseEtag: etag,
results: response.data.results,
count: response.data.count,
}
}
}
} catch (err) {
if (!(err && err.response && err.response.status === 304)) {
console.error(`Requests for outgoing transactions for ${safeAddress || 'unknown'} failed with 404`, err)
} else {
// NOTE: this is the expected implementation, currently the backend is not returning 304.
// So I check if the returned etag is the same instead (see above)
}
}
return { responseEtag: eTag, results: [] }
}
const etagsByPage = {}
export const loadAllTransactions = async (
uriParams: ServiceUriParams,
): Promise<{
transactions: Transaction[]
totalTransactionsAmount?: number
}> => {
const previousEtag = etagsByPage && etagsByPage[uriParams.offset]
const { responseEtag, results, count } = await fetchAllTransactions(uriParams, previousEtag)
etagsByPage[uriParams.offset] = responseEtag
return {
transactions: results,
totalTransactionsAmount: count,
}
}

View File

@ -0,0 +1,14 @@
import { createAction } from 'redux-actions'
import { Transaction } from '../../models/types/transactions'
export const LOAD_MORE_TRANSACTIONS = 'LOAD_MORE_TRANSACTIONS'
export type LoadMoreTransactionsAction = {
payload: {
safeAddress: string
transactions: Transaction[]
totalTransactionsAmount: number
}
}
export const loadMore = createAction(LOAD_MORE_TRANSACTIONS)

View File

@ -1,13 +1,12 @@
import { push } from 'connected-react-router' import { push } from 'connected-react-router'
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { WithSnackbarProps } from 'notistack'
import { batch } from 'react-redux' import { batch } from 'react-redux'
import semverSatisfies from 'semver/functions/satisfies' import semverSatisfies from 'semver/functions/satisfies'
import { ThunkAction } from 'redux-thunk' import { ThunkAction } from 'redux-thunk'
import { onboardUser } from 'src/components/ConnectButton' import { onboardUser } from 'src/components/ConnectButton'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { getNotificationsFromTxType, showSnackbar } from 'src/logic/notifications' import { getNotificationsFromTxType } from 'src/logic/notifications'
import { import {
CALL, CALL,
getApprovalTransaction, getApprovalTransaction,
@ -22,21 +21,23 @@ import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { providerSelector } from 'src/logic/wallets/store/selectors' import { providerSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS } from 'src/routes/routes' import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { addOrUpdateCancellationTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { addOrUpdateTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions' import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import { removeCancellationTransaction } from 'src/routes/safe/store/actions/transactions/removeCancellationTransaction' import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { removeTransaction } from 'src/routes/safe/store/actions/transactions/removeTransaction' import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { removeCancellationTransaction } from 'src/logic/safe/store/actions/transactions/removeCancellationTransaction'
import { removeTransaction } from 'src/logic/safe/store/actions/transactions/removeTransaction'
import { import {
generateSafeTxHash, generateSafeTxHash,
mockTransaction, mockTransaction,
TxToMock, TxToMock,
} from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers' } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/routes/safe/store/actions/utils' import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { getErrorMessage } from 'src/test/utils/ethereumErrors' import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { makeConfirmation } from '../models/confirmation' import { makeConfirmation } from '../models/confirmation'
import fetchTransactions from './transactions/fetchTransactions' import fetchTransactions from './transactions/fetchTransactions'
import { safeTransactionsSelector } from 'src/routes/safe/store/selectors' import { safeTransactionsSelector } from 'src/logic/safe/store/selectors'
import { Transaction, TransactionStatus, TxArgs } from 'src/routes/safe/store/models/types/transaction' import { Transaction, TransactionStatus, TxArgs } from 'src/logic/safe/store/models/types/transaction'
import { AnyAction } from 'redux' import { AnyAction } from 'redux'
import { PayableTx } from 'src/types/contracts/types.d' import { PayableTx } from 'src/types/contracts/types.d'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
@ -95,7 +96,7 @@ export const storeTx = async (
} }
} }
interface CreateTransaction extends WithSnackbarProps { interface CreateTransactionArgs {
navigateToTransactionsTab?: boolean navigateToTransactionsTab?: boolean
notifiedTransaction: string notifiedTransaction: string
operation?: number operation?: number
@ -108,23 +109,22 @@ interface CreateTransaction extends WithSnackbarProps {
} }
type CreateTransactionAction = ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction> type CreateTransactionAction = ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction>
type ConfirmEventHandler = (safeTxHash: string) => void
const createTransaction = ({ const createTransaction = (
safeAddress, {
to, safeAddress,
valueInWei, to,
txData = EMPTY_DATA, valueInWei,
notifiedTransaction, txData = EMPTY_DATA,
enqueueSnackbar, notifiedTransaction,
closeSnackbar, txNonce,
txNonce, operation = CALL,
operation = CALL, navigateToTransactionsTab = true,
navigateToTransactionsTab = true, origin = null,
origin = null, }: CreateTransactionArgs,
}: CreateTransaction): CreateTransactionAction => async ( onUserConfirm?: ConfirmEventHandler,
dispatch: Dispatch, ): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => {
getState: () => AppReduxState,
): Promise<void> => {
const state = getState() const state = getState()
if (navigateToTransactionsTab) { if (navigateToTransactionsTab) {
@ -149,7 +149,7 @@ const createTransaction = ({
)}000000000000000000000000000000000000000000000000000000000000000001` )}000000000000000000000000000000000000000000000000000000000000000001`
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin) const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar) const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
let pendingExecutionKey let pendingExecutionKey
@ -179,17 +179,20 @@ const createTransaction = ({
const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet) const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
if (signature) { if (signature) {
closeSnackbar(beforeExecutionKey) dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
await saveTxToHistory({ ...txArgs, signature, origin }) await saveTxToHistory({ ...txArgs, signature, origin })
showSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded, enqueueSnackbar, closeSnackbar) dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
dispatch(fetchTransactions(safeAddress)) dispatch(fetchTransactions(safeAddress))
return return
} }
} }
const tx = isExecution ? await getExecutionTransaction(txArgs) : await getApprovalTransaction(txArgs) const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
const tx = isExecution
? await getExecutionTransaction(txArgs)
: await getApprovalTransaction(safeInstance, safeTxHash)
const sendParams: PayableTx = { from, value: 0 } const sendParams: PayableTx = { from, value: 0 }
// if not set owner management tests will fail on ganache // if not set owner management tests will fail on ganache
@ -201,7 +204,7 @@ const createTransaction = ({
...txArgs, ...txArgs,
confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
value: txArgs.valueInWei, value: txArgs.valueInWei,
safeTxHash: generateSafeTxHash(safeAddress, txArgs), safeTxHash,
submissionDate: new Date().toISOString(), submissionDate: new Date().toISOString(),
} }
const mockedTx = await mockTransaction(txToMock, safeAddress, state) const mockedTx = await mockTransaction(txToMock, safeAddress, state)
@ -209,11 +212,12 @@ const createTransaction = ({
await tx await tx
.send(sendParams) .send(sendParams)
.once('transactionHash', async (hash) => { .once('transactionHash', async (hash) => {
onUserConfirm?.(safeTxHash)
try { try {
txHash = hash txHash = hash
closeSnackbar(beforeExecutionKey) dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar) pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
await Promise.all([ await Promise.all([
saveTxToHistory({ ...txArgs, txHash, origin }), saveTxToHistory({ ...txArgs, txHash, origin }),
@ -233,21 +237,21 @@ const createTransaction = ({
} }
}) })
.on('error', (error) => { .on('error', (error) => {
closeSnackbar(pendingExecutionKey) dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
removeTxFromStore(mockedTx, safeAddress, dispatch, state) removeTxFromStore(mockedTx, safeAddress, dispatch, state)
console.error('Tx error: ', error) console.error('Tx error: ', error)
}) })
.then(async (receipt) => { .then(async (receipt) => {
if (pendingExecutionKey) { if (pendingExecutionKey) {
closeSnackbar(pendingExecutionKey) dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
} }
showSnackbar( dispatch(
isExecution enqueueSnackbar(
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded isExecution
: notificationsQueue.afterExecution.moreConfirmationsNeeded, ? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
enqueueSnackbar, : notificationsQueue.afterExecution.moreConfirmationsNeeded,
closeSnackbar, ),
) )
const toStoreTx = isExecution const toStoreTx = isExecution
@ -283,13 +287,13 @@ const createTransaction = ({
: notificationsQueue.afterExecutionError.message : notificationsQueue.afterExecutionError.message
console.error(`Error creating the TX: `, err) console.error(`Error creating the TX: `, err)
closeSnackbar(beforeExecutionKey) dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
if (pendingExecutionKey) { if (pendingExecutionKey) {
closeSnackbar(pendingExecutionKey) dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
} }
showSnackbar(errorMsg, enqueueSnackbar, closeSnackbar) dispatch(enqueueSnackbar(errorMsg))
const executeDataUsedSignatures = safeInstance.methods const executeDataUsedSignatures = safeInstance.methods
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs) .execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)

Some files were not shown because too many files have changed in this diff Show More