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",
|
||||
"version": "2.10.2",
|
||||
"version": "2.11.0",
|
||||
"description": "Allowing crypto users manage funds in a safer way",
|
||||
"website": "https://github.com/gnosis/safe-react#readme",
|
||||
"bugs": {
|
||||
|
@ -40,11 +40,14 @@
|
|||
"start": "react-app-rewired start",
|
||||
"test": "NODE_ENV=test && react-app-rewired test --env=jsdom",
|
||||
"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": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged --allow-empty"
|
||||
"pre-commit": "lint-staged --allow-empty",
|
||||
"pre-push": "tsc"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
|
@ -161,8 +164,9 @@
|
|||
]
|
||||
},
|
||||
"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-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",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.19.1",
|
||||
"@material-ui/core": "4.11.0",
|
||||
|
@ -172,7 +176,7 @@
|
|||
"async-sema": "^3.1.0",
|
||||
"axios": "0.19.2",
|
||||
"bignumber.js": "9.0.0",
|
||||
"bnc-onboard": "1.10.3",
|
||||
"bnc-onboard": "1.11.1",
|
||||
"classnames": "^2.2.6",
|
||||
"concurrently": "^5.2.0",
|
||||
"connected-react-router": "6.8.0",
|
||||
|
@ -227,22 +231,27 @@
|
|||
"web3-utils": "^1.2.11"
|
||||
},
|
||||
"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/react": "10.4.7",
|
||||
"@testing-library/user-event": "12.0.17",
|
||||
"@testing-library/react": "10.4.8",
|
||||
"@testing-library/user-event": "12.1.0",
|
||||
"@typechain/web3-v1": "^1.0.0",
|
||||
"@types/history": "4.6.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/jest": "^26.0.9",
|
||||
"@types/lodash.memoize": "^4.1.6",
|
||||
"@types/node": "14.0.27",
|
||||
"@types/react": "^16.9.43",
|
||||
"@types/node": "14.6.0",
|
||||
"@types/react": "^16.9.47",
|
||||
"@types/react-dom": "^16.9.6",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/styled-components": "^5.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "3.7.1",
|
||||
"@typescript-eslint/parser": "3.7.1",
|
||||
"autoprefixer": "9.8.5",
|
||||
"@types/styled-components": "^5.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "3.9.1",
|
||||
"@typescript-eslint/parser": "3.9.1",
|
||||
"autoprefixer": "9.8.6",
|
||||
"cross-env": "^7.0.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
|
@ -262,6 +271,7 @@
|
|||
"node-sass": "^4.14.1",
|
||||
"prettier": "2.0.5",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"react-docgen-typescript-loader": "^3.7.2",
|
||||
"truffle": "5.1.36",
|
||||
"typechain": "^2.0.0",
|
||||
"typescript": "3.9.7",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const os = require('os');
|
||||
const { dialog } = require('electron');
|
||||
const log = require('electron-log');
|
||||
const settings = require('electron-settings').default;
|
||||
const settings = require('electron-settings');
|
||||
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import Col from 'src/components/layout/Col'
|
|||
import Hairline from 'src/components/layout/Hairline'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
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 { 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 Footer = () => {
|
||||
const Footer = (): React.ReactElement => {
|
||||
const date = new Date()
|
||||
const classes = useStyles()
|
||||
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 { Link } from 'react-router-dom'
|
||||
|
||||
import NetworkLabel from './NetworkLabel'
|
||||
import Provider from './Provider'
|
||||
import SafeListHeader from './SafeListHeader'
|
||||
|
||||
import Spacer from 'src/components/Spacer'
|
||||
import openHoc from 'src/components/hoc/OpenHoc'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Divider from 'src/components/layout/Divider'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { border, headerHeight, md, screenSm, sm } from 'src/theme/variables'
|
||||
|
@ -41,11 +38,12 @@ const styles = () => ({
|
|||
zIndex: 1301,
|
||||
},
|
||||
logo: {
|
||||
flexBasis: '95px',
|
||||
flexBasis: '114px',
|
||||
flexShrink: '0',
|
||||
flexGrow: '0',
|
||||
maxWidth: '55px',
|
||||
padding: sm,
|
||||
marginTop: '4px',
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
maxWidth: 'none',
|
||||
paddingLeft: md,
|
||||
|
@ -61,13 +59,9 @@ const Layout = openHoc(({ classes, clickAway, open, providerDetails, providerInf
|
|||
<Row className={classes.summary}>
|
||||
<Col className={classes.logo} middle="xs" start="xs">
|
||||
<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>
|
||||
</Col>
|
||||
<Divider />
|
||||
<SafeListHeader />
|
||||
<Divider />
|
||||
<NetworkLabel />
|
||||
<Spacer />
|
||||
<Provider
|
||||
info={providerInfo}
|
|
@ -2,7 +2,7 @@ import { withStyles } from '@material-ui/core/styles'
|
|||
import * as React from 'react'
|
||||
|
||||
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 Paragraph from 'src/components/layout/Paragraph'
|
||||
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 classNames from 'classnames'
|
||||
import * as React from 'react'
|
||||
import { EthHashInfo, Identicon } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import EtherscanBtn from 'src/components/EtherscanBtn'
|
||||
import CircleDot from 'src/components/Header/components/CircleDot'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
|
||||
import Spacer from 'src/components/Spacer'
|
||||
import Block from 'src/components/layout/Block'
|
||||
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 Paragraph from 'src/components/layout/Paragraph'
|
||||
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 { upperFirst } from 'src/utils/css'
|
||||
|
||||
|
@ -93,8 +90,6 @@ const styles = () => ({
|
|||
|
||||
const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard, provider, userAddress }) => {
|
||||
const status = connected ? 'Connected' : 'Connection error'
|
||||
const address = userAddress ? shortVersionOf(userAddress, 4) : 'Address not available'
|
||||
const identiconAddress = userAddress || 'random'
|
||||
const color = connected ? 'primary' : 'warning'
|
||||
|
||||
return (
|
||||
|
@ -102,20 +97,16 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
|
|||
<Block className={classes.container}>
|
||||
<Row align="center" className={classes.identicon} margin="md">
|
||||
{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" />
|
||||
)}
|
||||
</Row>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} noMargin size="sm">
|
||||
{address}
|
||||
</Paragraph>
|
||||
{userAddress && (
|
||||
<>
|
||||
<CopyBtn content={userAddress} increaseZindex />
|
||||
<EtherscanBtn increaseZindex type="address" value={userAddress} />
|
||||
</>
|
||||
{userAddress ? (
|
||||
<EthHashInfo hash={userAddress} showCopyBtn showEtherscanBtn shortenHash={4} network={network} />
|
||||
) : (
|
||||
'Address not available'
|
||||
)}
|
||||
</Block>
|
||||
</Block>
|
|
@ -1,12 +1,11 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import * as React from 'react'
|
||||
import { EthHashInfo, Text } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import NetworkLabel from '../NetworkLabel'
|
||||
import CircleDot from 'src/components/Header/components/CircleDot'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import CircleDot from 'src/components/AppLayout/Header/components/CircleDot'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
|
||||
import WalletIcon from '../WalletIcon'
|
||||
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 classes = useStyles()
|
||||
const cutAddress = connected ? shortVersionOf(userAddress, 4) : 'Connection Error'
|
||||
const color = connected ? 'primary' : 'warning'
|
||||
const identiconAddress = userAddress || 'random'
|
||||
|
||||
const addressColor = connected ? 'text' : 'warning'
|
||||
return (
|
||||
<>
|
||||
{!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}
|
||||
</Paragraph>
|
||||
<div className={classes.providerContainer}>
|
||||
{connected && <Identicon address={identiconAddress} className={classes.identicon} diameter={10} />}
|
||||
<Paragraph className={classes.address} color={color} noMargin size="xs">
|
||||
{connected ? (
|
||||
<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}
|
||||
</Paragraph>
|
||||
</Paragraph> */}
|
||||
</div>
|
||||
</Col>
|
||||
<Col className={classes.networkLabel} layout="column" start="sm">
|
|
@ -1,7 +1,7 @@
|
|||
import { withStyles } from '@material-ui/core/styles'
|
||||
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 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 {
|
||||
className?: 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 classes = useStyles()
|
||||
const customClasses = increaseZindex ? { popper: classes.increasedPopperZindex } : {}
|
||||
const customClasses = increaseZIndex ? { popper: classes.increasedPopperZindex } : {}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
|
|
@ -17,7 +17,7 @@ interface EtherscanLinkProps {
|
|||
className?: string
|
||||
cut?: number
|
||||
knownAddress?: boolean
|
||||
type?: 'tx' | 'address'
|
||||
type: 'tx' | 'address'
|
||||
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`
|
||||
border-bottom: solid 2px rgb(232, 231, 230);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
|
|
@ -5,8 +5,8 @@ import React from 'react'
|
|||
import { Provider } from 'react-redux'
|
||||
import { ThemeProvider } from 'styled-components'
|
||||
|
||||
import Loader from '../Loader'
|
||||
import PageFrame from '../layout/PageFrame'
|
||||
import Loader from 'src/components/Loader'
|
||||
import App from 'src/components/App'
|
||||
|
||||
import AppRoutes from 'src/routes'
|
||||
import { history, store } from 'src/store'
|
||||
|
@ -16,13 +16,11 @@ import { wrapInSuspense } from 'src/utils/wrapInSuspense'
|
|||
import './index.module.scss'
|
||||
import './OnboardCustom.module.scss'
|
||||
|
||||
const Root = () => (
|
||||
const Root = (): React.ReactElement => (
|
||||
<ThemeProvider theme={styledTheme}>
|
||||
<Provider store={store}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageFrame>{wrapInSuspense(<AppRoutes />, <Loader />)}</PageFrame>
|
||||
</ConnectedRouter>
|
||||
<ConnectedRouter history={history}>{<App>{wrapInSuspense(<AppRoutes />, <Loader />)}</App>}</ConnectedRouter>
|
||||
</MuiThemeProvider>
|
||||
</Provider>
|
||||
</ThemeProvider>
|
||||
|
|
|
@ -23,7 +23,7 @@ const useStyles = makeStyles({
|
|||
},
|
||||
})
|
||||
|
||||
const DefaultBadge = () => {
|
||||
const DefaultBadge = (): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
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 Row from 'src/components/layout/Row'
|
||||
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'
|
||||
|
||||
const { useEffect, useMemo, useState } = React
|
||||
|
||||
export const SidebarContext = React.createContext({
|
||||
export const SafeListSidebarContext = React.createContext({
|
||||
isOpen: false,
|
||||
toggleSidebar: () => {},
|
||||
})
|
||||
|
@ -35,7 +35,7 @@ const filterBy = (filter, safes) =>
|
|||
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 [filter, setFilter] = useState('')
|
||||
const classes = useSidebarStyles()
|
||||
|
@ -74,7 +74,7 @@ const Sidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeActi
|
|||
const filteredSafes = useMemo(() => filterBy(filter, safes), [safes, filter])
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={{ isOpen, toggleSidebar }}>
|
||||
<SafeListSidebarContext.Provider value={{ isOpen, toggleSidebar }}>
|
||||
<Drawer
|
||||
classes={{ paper: classes.sidebarPaper }}
|
||||
className={classes.sidebar}
|
||||
|
@ -119,7 +119,7 @@ const Sidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeActi
|
|||
/>
|
||||
</Drawer>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
</SafeListSidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -130,4 +130,4 @@ export default connect(
|
|||
currentSafe: safeParamAddressFromStateSelector(state),
|
||||
}),
|
||||
{ setDefaultSafeAction: setDefaultSafe },
|
||||
)(Sidebar)
|
||||
)(SafeListSidebar)
|
|
@ -1,6 +1,6 @@
|
|||
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) =>
|
||||
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(
|
||||
(address: string): ValidatorReturnType => {
|
||||
const startsWith0x = address.startsWith('0x')
|
||||
const startsWith0x = address?.startsWith('0x')
|
||||
const isAddress = getWeb3().utils.isAddress(address)
|
||||
|
||||
return startsWith0x && isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name'
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex-direction: column;
|
||||
padding: 96px 200px 0px 200px;
|
||||
padding: 12px 0 0 0;
|
||||
}
|
||||
|
||||
@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) =>
|
||||
`safes/${safeAddress}/incoming-transfers/`
|
||||
|
||||
export const getAllTransactionsUriFrom = (safeAddress: string): string =>
|
||||
`safes/${safeAddress}/all-transactions/`
|
||||
|
||||
export const getSafeCreationTxUri = (safeAddress) => `safes/${safeAddress}/creation/`
|
||||
|
||||
export const getRelayUrl = () => getConfig()[RELAY_API_URL]
|
||||
|
|
|
@ -5,8 +5,8 @@ import ReactDOM from 'react-dom'
|
|||
import Root from 'src/components/Root'
|
||||
import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage'
|
||||
import loadActiveTokens from 'src/logic/tokens/store/actions/loadActiveTokens'
|
||||
import loadDefaultSafe from 'src/routes/safe/store/actions/loadDefaultSafe'
|
||||
import loadSafesFromStorage from 'src/routes/safe/store/actions/loadSafesFromStorage'
|
||||
import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe'
|
||||
import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
|
||||
import { store } from 'src/store'
|
||||
|
||||
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
|
||||
|
|
|
@ -3,7 +3,7 @@ import { List } from 'immutable'
|
|||
import { loadAddressBook } from 'src/logic/addressBook/store/actions/loadAddressBook'
|
||||
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
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) => {
|
||||
try {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { createSelector } from 'reselect'
|
|||
|
||||
import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
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 =>
|
||||
state[ADDRESS_BOOK_REDUCER_ID].get('addressBook')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { List } from 'immutable'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
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'
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/O
|
|||
|
||||
import { AppReduxState } from 'src/store'
|
||||
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 nftTokensSelector = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
CurrencyReducerMap,
|
||||
CurrencyValuesState,
|
||||
} 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 { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
|
|
|
@ -2,14 +2,14 @@ import { IconButton } from '@material-ui/core'
|
|||
import { Close as IconClose } from '@material-ui/icons'
|
||||
import * as React from 'react'
|
||||
|
||||
import { NOTIFICATIONS } from './notificationTypes'
|
||||
import { Notification, NOTIFICATIONS } from './notificationTypes'
|
||||
|
||||
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { getAppInfoFromOrigin } from 'src/routes/safe/components/Apps/utils'
|
||||
import { store } from 'src/store'
|
||||
|
||||
const setNotificationOrigin = (notification, origin) => {
|
||||
const setNotificationOrigin = (notification: Notification, origin: string): Notification => {
|
||||
if (!origin) {
|
||||
return notification
|
||||
}
|
||||
|
@ -18,8 +18,9 @@ const setNotificationOrigin = (notification, origin) => {
|
|||
return { ...notification, message: `${appInfo.name}: ${notification.message}` }
|
||||
}
|
||||
|
||||
const getStandardTxNotificationsQueue = (origin) => {
|
||||
return {
|
||||
const getStandardTxNotificationsQueue = (
|
||||
origin: string,
|
||||
): Record<string, Record<string, Notification> | Notification> => ({
|
||||
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
|
||||
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
|
||||
afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin),
|
||||
|
@ -28,8 +29,7 @@ const getStandardTxNotificationsQueue = (origin) => {
|
|||
moreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG, origin),
|
||||
},
|
||||
afterExecutionError: setNotificationOrigin(NOTIFICATIONS.TX_FAILED_MSG, origin),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const waitingTransactionNotificationsQueue = {
|
||||
beforeExecution: null,
|
||||
|
@ -40,7 +40,7 @@ const waitingTransactionNotificationsQueue = {
|
|||
afterExecutionError: null,
|
||||
}
|
||||
|
||||
const getConfirmationTxNotificationsQueue = (origin) => {
|
||||
const getConfirmationTxNotificationsQueue = (origin: string) => {
|
||||
return {
|
||||
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_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 {
|
||||
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
|
||||
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
|
||||
|
@ -199,9 +199,13 @@ export const getNotificationsFromTxType: any = (txType, origin) => {
|
|||
return notificationsQueue
|
||||
}
|
||||
|
||||
export const enhanceSnackbarForAction: any = (notification, key, onClick) => ({
|
||||
export const enhanceSnackbarForAction = (
|
||||
notification: Notification,
|
||||
key?: number | string,
|
||||
onClick?: () => void,
|
||||
): Notification => ({
|
||||
...notification,
|
||||
key,
|
||||
key: key || notification.key,
|
||||
options: {
|
||||
...notification.options,
|
||||
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 { capitalize } from 'src/utils/css'
|
||||
|
||||
|
@ -9,7 +11,50 @@ export const INFO = 'info'
|
|||
const shortDuration = 5000
|
||||
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
|
||||
CONNECT_WALLET_MSG: {
|
||||
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 { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
|
||||
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 => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
|
@ -4,11 +4,11 @@ import { useDispatch } from 'react-redux'
|
|||
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
||||
import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe'
|
||||
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||
import fetchLatestMasterContractVersion from 'src/routes/safe/store/actions/fetchLatestMasterContractVersion'
|
||||
import fetchSafe from 'src/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
|
||||
import fetchSafeCreationTx from 'src/routes/safe/store/actions/fetchSafeCreationTx'
|
||||
import { Dispatch } from 'src/routes/safe/store/actions/types.d'
|
||||
import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion'
|
||||
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
|
||||
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
|
||||
import fetchSafeCreationTx from 'src/logic/safe/store/actions/fetchSafeCreationTx'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
|
||||
export const useLoadSafe = (safeAddress: string): void => {
|
||||
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 fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||
import fetchEtherBalance from 'src/routes/safe/store/actions/fetchEtherBalance'
|
||||
import { checkAndUpdateSafe } from 'src/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
|
||||
import fetchEtherBalance from 'src/logic/safe/store/actions/fetchEtherBalance'
|
||||
import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
||||
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
|
||||
import { TIMEOUT } from 'src/utils/constants'
|
||||
|
||||
export const useSafeScheduledUpdates = (safeAddress: string): void => {
|
||||
|
@ -36,11 +36,11 @@ export const useSafeScheduledUpdates = (safeAddress: string): void => {
|
|||
|
||||
if (safeAddress) {
|
||||
fetchSafeData(safeAddress)
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
clearTimeout(timer.current)
|
||||
}
|
||||
}
|
||||
}, [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 { 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', () => {
|
||||
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 { List, Map } from 'immutable'
|
||||
import { WithSnackbarProps } from 'notistack'
|
||||
import { batch } from 'react-redux'
|
||||
import semverSatisfies from 'semver/functions/satisfies'
|
||||
import { ThunkAction } from 'redux-thunk'
|
||||
|
||||
import { onboardUser } from 'src/components/ConnectButton'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { getNotificationsFromTxType, showSnackbar } from 'src/logic/notifications'
|
||||
import { getNotificationsFromTxType } from 'src/logic/notifications'
|
||||
import {
|
||||
CALL,
|
||||
getApprovalTransaction,
|
||||
|
@ -22,21 +21,23 @@ import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
|||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
import { addOrUpdateCancellationTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
|
||||
import { addOrUpdateTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||
import { removeCancellationTransaction } from 'src/routes/safe/store/actions/transactions/removeCancellationTransaction'
|
||||
import { removeTransaction } from 'src/routes/safe/store/actions/transactions/removeTransaction'
|
||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
|
||||
import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
|
||||
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 {
|
||||
generateSafeTxHash,
|
||||
mockTransaction,
|
||||
TxToMock,
|
||||
} from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/routes/safe/store/actions/utils'
|
||||
} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
|
||||
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
|
||||
import { makeConfirmation } from '../models/confirmation'
|
||||
import fetchTransactions from './transactions/fetchTransactions'
|
||||
import { safeTransactionsSelector } from 'src/routes/safe/store/selectors'
|
||||
import { Transaction, TransactionStatus, TxArgs } from 'src/routes/safe/store/models/types/transaction'
|
||||
import { safeTransactionsSelector } from 'src/logic/safe/store/selectors'
|
||||
import { Transaction, TransactionStatus, TxArgs } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { AnyAction } from 'redux'
|
||||
import { PayableTx } from 'src/types/contracts/types.d'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
@ -95,7 +96,7 @@ export const storeTx = async (
|
|||
}
|
||||
}
|
||||
|
||||
interface CreateTransaction extends WithSnackbarProps {
|
||||
interface CreateTransactionArgs {
|
||||
navigateToTransactionsTab?: boolean
|
||||
notifiedTransaction: string
|
||||
operation?: number
|
||||
|
@ -108,23 +109,22 @@ interface CreateTransaction extends WithSnackbarProps {
|
|||
}
|
||||
|
||||
type CreateTransactionAction = ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction>
|
||||
type ConfirmEventHandler = (safeTxHash: string) => void
|
||||
|
||||
const createTransaction = ({
|
||||
const createTransaction = (
|
||||
{
|
||||
safeAddress,
|
||||
to,
|
||||
valueInWei,
|
||||
txData = EMPTY_DATA,
|
||||
notifiedTransaction,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
txNonce,
|
||||
operation = CALL,
|
||||
navigateToTransactionsTab = true,
|
||||
origin = null,
|
||||
}: CreateTransaction): CreateTransactionAction => async (
|
||||
dispatch: Dispatch,
|
||||
getState: () => AppReduxState,
|
||||
): Promise<void> => {
|
||||
}: CreateTransactionArgs,
|
||||
onUserConfirm?: ConfirmEventHandler,
|
||||
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => {
|
||||
const state = getState()
|
||||
|
||||
if (navigateToTransactionsTab) {
|
||||
|
@ -149,7 +149,7 @@ const createTransaction = ({
|
|||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
|
||||
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
|
||||
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
|
||||
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
|
||||
|
||||
let pendingExecutionKey
|
||||
|
||||
|
@ -179,17 +179,20 @@ const createTransaction = ({
|
|||
const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
|
||||
|
||||
if (signature) {
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
await saveTxToHistory({ ...txArgs, signature, origin })
|
||||
showSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
|
||||
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
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 }
|
||||
|
||||
// if not set owner management tests will fail on ganache
|
||||
|
@ -201,7 +204,7 @@ const createTransaction = ({
|
|||
...txArgs,
|
||||
confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
|
||||
value: txArgs.valueInWei,
|
||||
safeTxHash: generateSafeTxHash(safeAddress, txArgs),
|
||||
safeTxHash,
|
||||
submissionDate: new Date().toISOString(),
|
||||
}
|
||||
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
|
||||
|
@ -209,11 +212,12 @@ const createTransaction = ({
|
|||
await tx
|
||||
.send(sendParams)
|
||||
.once('transactionHash', async (hash) => {
|
||||
onUserConfirm?.(safeTxHash)
|
||||
try {
|
||||
txHash = hash
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
|
||||
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
|
||||
|
||||
await Promise.all([
|
||||
saveTxToHistory({ ...txArgs, txHash, origin }),
|
||||
|
@ -233,21 +237,21 @@ const createTransaction = ({
|
|||
}
|
||||
})
|
||||
.on('error', (error) => {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
|
||||
console.error('Tx error: ', error)
|
||||
})
|
||||
.then(async (receipt) => {
|
||||
if (pendingExecutionKey) {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
}
|
||||
|
||||
showSnackbar(
|
||||
dispatch(
|
||||
enqueueSnackbar(
|
||||
isExecution
|
||||
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
|
||||
: notificationsQueue.afterExecution.moreConfirmationsNeeded,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
),
|
||||
)
|
||||
|
||||
const toStoreTx = isExecution
|
||||
|
@ -283,13 +287,13 @@ const createTransaction = ({
|
|||
: notificationsQueue.afterExecutionError.message
|
||||
|
||||
console.error(`Error creating the TX: `, err)
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
if (pendingExecutionKey) {
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
}
|
||||
|
||||
showSnackbar(errorMsg, enqueueSnackbar, closeSnackbar)
|
||||
dispatch(enqueueSnackbar(errorMsg))
|
||||
|
||||
const executeDataUsedSignatures = safeInstance.methods
|
||||
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
|