Merge pull request #1291 from gnosis/release-2.11.0
Gnosis Safe Multisig - Public Release v2.11.0
|
@ -0,0 +1,8 @@
|
||||||
|
module.exports = {
|
||||||
|
stories: ['../src/**/*.stories.tsx'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/preset-create-react-app',
|
||||||
|
'@storybook/addon-actions',
|
||||||
|
'@storybook/addon-links',
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
<style>
|
||||||
|
html, body, #root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -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>
|
||||||
|
))
|
38
package.json
|
@ -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",
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
Before Width: | Height: | Size: 337 B After Width: | Height: | Size: 337 B |
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 345 B |
Before Width: | Height: | Size: 324 B After Width: | Height: | Size: 324 B |
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 391 B |
|
@ -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
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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()
|
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 228 B |
After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 300 B After Width: | Height: | Size: 300 B |
Before Width: | Height: | Size: 237 B After Width: | Height: | Size: 237 B |
Before Width: | Height: | Size: 314 B After Width: | Height: | Size: 314 B |
|
@ -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}
|
|
@ -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'
|
|
@ -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>
|
|
@ -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">
|
|
@ -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'
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 519 B After Width: | Height: | Size: 519 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 767 B After Width: | Height: | Size: 767 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -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
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)
|
|
@ -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
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)
|
|
@ -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
|
|
@ -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 }
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
||||||
import { fetchProvider, removeProvider } from 'src/logic/wallets/store/actions'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
fetchProvider,
|
|
||||||
removeProvider,
|
|
||||||
}
|
|
|
@ -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 |
|
@ -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)
|
|
|
@ -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))
|
|
|
@ -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,
|
|
||||||
})
|
|
|
@ -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
|
|
@ -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
|
|
@ -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} />
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -23,7 +23,7 @@ const useStyles = makeStyles({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const DefaultBadge = () => {
|
const DefaultBadge = (): React.ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
Before Width: | Height: | Size: 482 B After Width: | Height: | Size: 482 B |
|
@ -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
|
|
@ -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)
|
|
@ -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)),
|
|
@ -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
|
|
|
@ -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'
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
.frame {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
|
@ -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),
|
|
||||||
)
|
|
|
@ -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]
|
||||||
|
|
|
@ -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] })
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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>
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
@ -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>()
|
|
@ -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>()
|
|
@ -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
|
|
@ -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])
|
||||||
}
|
}
|
|
@ -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 () => {
|
|
@ -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
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
@ -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)
|