From 5e24cfc93e5a685a3dbae6248d1da9d5cc319ad1 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 30 Jul 2020 17:02:20 -0300 Subject: [PATCH 01/69] fix: Card component requires `className` instead of `styles` --- src/routes/safe/components/Apps/index.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index afea195525..64fbeaf6d1 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -1,4 +1,5 @@ import { Card, FixedDialog, FixedIcon, IconText, Loader, Menu, Text, Title } from '@gnosis.pm/safe-react-components' +import { createStyles, makeStyles } from '@material-ui/core/styles' import { withSnackbar } from 'notistack' import React, { useCallback, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -23,6 +24,12 @@ import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { isSameHref } from 'src/utils/url' import { SafeApp, StoredSafeApp } from './types' +const useStyles = makeStyles( + createStyles({ + card: { marginBottom: '24px' }, + }), +) + const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY' const APPS_LEGAL_DISCLAIMER_STORAGE_KEY = 'APPS_LEGAL_DISCLAIMER_STORAGE_KEY' @@ -64,6 +71,7 @@ const operations = { } function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) { + const classes = useStyles() const [appList, setAppList] = useState>([]) const [legalDisclaimerAccepted, setLegalDisclaimerAccepted] = useState(false) const [selectedApp, setSelectedApp] = useState() @@ -419,7 +427,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) { */} ) : ( - + No Apps Enabled From f669a3a4928b633470801818d2c9d38a9bdbe3ab Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 30 Jul 2020 17:02:40 -0300 Subject: [PATCH 02/69] update `safe-react-components` --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 40f5fa81bd..db3bd3511f 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ }, "dependencies": { "@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#2b4635b", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid": "5.19.1", "@material-ui/core": "4.11.0", diff --git a/yarn.lock b/yarn.lock index 5228fddaa6..3ae950c673 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1337,9 +1337,9 @@ solc "0.5.14" truffle "^5.1.21" -"@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#2b4635b": version "0.2.0" - resolved "https://github.com/gnosis/safe-react-components.git#7bb55de2d8a2b6f192466043dc643387fb42dbf6" + resolved "https://github.com/gnosis/safe-react-components.git#2b4635b22bebfca0260486f1ae7a9a35b02d71c2" dependencies: classnames "^2.2.6" polished "3.6.5" From 2583e57c6f58f35b6d345738995f73dcf31f4335 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 30 Jul 2020 17:03:11 -0300 Subject: [PATCH 03/69] change `Spending Limit` icon from `allowances` to `fuelIndicator` --- src/routes/safe/components/Settings/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Settings/index.tsx b/src/routes/safe/components/Settings/index.tsx index 78c0a05fb8..36010cd4ab 100644 --- a/src/routes/safe/components/Settings/index.tsx +++ b/src/routes/safe/components/Settings/index.tsx @@ -122,7 +122,7 @@ const Settings: React.FC = () => { From df6547a99b7e15328d9c20dd1b75811fc47ade76 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 6 Aug 2020 21:28:04 -0300 Subject: [PATCH 04/69] add types to `TokenSelectField` component - marked `isValid` as optional, with default `true` value - marked `initialValue` as optional - migrated to hooks for material-ui styles --- .../SendFunds/TokenSelectField/index.tsx | 81 ++++++++++++------- .../SendFunds/TokenSelectField/style.ts | 6 +- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx index d33974ed40..c87f53052f 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx @@ -1,8 +1,10 @@ import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' import MenuItem from '@material-ui/core/MenuItem' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' +import { List } from 'immutable' import React from 'react' +import { Token } from 'src/logic/tokens/store/model/token' import { selectStyles, selectedTokenStyles } from './style' @@ -15,7 +17,15 @@ import Paragraph from 'src/components/layout/Paragraph' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' -const SelectedToken = ({ classes, tokenAddress, tokens }) => { +const useSelectedTokenStyles = makeStyles(selectedTokenStyles) + +interface SelectTokenProps { + tokenAddress: string + tokens: List +} + +const SelectedToken = ({ tokenAddress, tokens }: SelectTokenProps): React.ReactElement => { + const classes = useSelectedTokenStyles() const token = tokens.find(({ address }) => address === tokenAddress) return ( @@ -28,7 +38,7 @@ const SelectedToken = ({ classes, tokenAddress, tokens }) => { ) : ( @@ -39,32 +49,43 @@ const SelectedToken = ({ classes, tokenAddress, tokens }) => { ) } -const SelectedTokenStyled = withStyles(selectedTokenStyles)(SelectedToken) -const TokenSelectField = ({ classes, initialValue, isValid, tokens }) => ( - } - validate={required} - > - {tokens.map((token) => ( - - - {token.name} - - - - ))} - -) +const useSelectStyles = makeStyles(selectStyles) + +interface TokenSelectFieldProps { + initialValue?: string + isValid?: boolean + tokens: List +} + +const TokenSelectField = ({ initialValue, isValid = true, tokens }: TokenSelectFieldProps): React.ReactElement => { + const classes = useSelectStyles() + + return ( + } + validate={required} + > + {tokens.map((token) => ( + + + {token.name} + + + + ))} + + ) +} -export default withStyles(selectStyles)(TokenSelectField) +export default TokenSelectField diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/style.ts b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/style.ts index 561dd1691d..f5bd385d93 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/style.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/style.ts @@ -1,6 +1,8 @@ +import { createStyles } from '@material-ui/core' + import { sm } from 'src/theme/variables' -export const selectedTokenStyles = () => ({ +export const selectedTokenStyles = createStyles({ container: { minHeight: '55px', padding: 0, @@ -16,7 +18,7 @@ export const selectedTokenStyles = () => ({ }, }) -export const selectStyles = () => ({ +export const selectStyles = createStyles({ selectMenu: { paddingRight: 0, }, From 7616e8f79f1b141e2caf32ed1a34fd02a2a541a2 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 6 Aug 2020 21:30:33 -0300 Subject: [PATCH 05/69] refactor `AddressBookInput` - migrated to hooks for material-ui styles - make `label` parametrized and optional, with default value - make `setIsValidAddress` optional --- .../screens/AddressBookInput/index.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx index 6079daefc4..29ea088105 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx @@ -1,6 +1,5 @@ import MuiTextField from '@material-ui/core/TextField' -import { withStyles } from '@material-ui/core/styles' -import makeStyles from '@material-ui/core/styles/makeStyles' +import { makeStyles } from '@material-ui/core/styles' import Autocomplete from '@material-ui/lab/Autocomplete' import { List } from 'immutable' import React, { useEffect, useState } from 'react' @@ -24,7 +23,8 @@ export interface AddressBookProps { setSelectedEntry: ( entry: { address?: string; name?: string } | React.SetStateAction<{ address: string; name: string }>, ) => void - setIsValidAddress: (valid?: boolean) => void + setIsValidAddress?: (valid?: boolean) => void + label?: string } const useStyles = makeStyles(styles) @@ -67,11 +67,12 @@ interface FilteredAddressBookEntry { const AddressBookInput = ({ fieldMutator, isCustomTx, + label = 'Recipient', pristine, recipientAddress, setIsValidAddress, setSelectedEntry, -}: AddressBookProps) => { +}: AddressBookProps): React.ReactElement => { const classes = useStyles() const addressBook = useSelector(getAddressBook) const [isValidForm, setIsValidForm] = useState(true) @@ -90,7 +91,7 @@ const AddressBookInput = ({ if (inputTouched && !normalizedAddress) { setIsValidForm(false) setValidationText('Required') - setIsValidAddress(false) + setIsValidAddress?.(false) return } if (normalizedAddress) { @@ -121,7 +122,7 @@ const AddressBookInput = ({ setIsValidForm(isValidText === undefined) setValidationText(isValidText) fieldMutator(resolvedAddress) - setIsValidAddress(isValidText === undefined) + setIsValidAddress?.(isValidText === undefined) } useEffect(() => { @@ -201,7 +202,7 @@ const AddressBookInput = ({ }, className: statusClasses, }} - label={!isValidForm ? validationText : 'Recipient'} + label={!isValidForm ? validationText : label} onChange={(event) => { setInputTouched(true) onAddressInputChanged(event.target.value) @@ -232,4 +233,4 @@ const AddressBookInput = ({ ) } -export default withStyles(styles as any)(AddressBookInput) +export default AddressBookInput From 32e5d50a21072a81bd98da594478ece5a8771cb4 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 6 Aug 2020 21:32:59 -0300 Subject: [PATCH 06/69] add types to `GnoModal` component - migrated to hooks for material-ui styles --- src/components/Modal/index.tsx | 88 +++++++++++++-------- src/routes/safe/components/Layout/index.tsx | 4 +- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 9a7f23e916..94ff2e8981 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -1,52 +1,70 @@ import Modal from '@material-ui/core/Modal' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles, createStyles } from '@material-ui/core/styles' import cn from 'classnames' import * as React from 'react' import { sm } from 'src/theme/variables' -const styles = () => ({ - root: { - alignItems: 'center', - justifyContent: 'center', - display: 'flex', - overflowY: 'scroll', - }, - paper: { - position: 'absolute', - top: '120px', - width: '500px', - height: '530px', - borderRadius: sm, - backgroundColor: '#ffffff', - boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)', - '&:focus': { - outline: 'none', +const useStyles = makeStyles( + createStyles({ + root: { + alignItems: 'center', + justifyContent: 'center', + display: 'flex', + overflowY: 'scroll', }, - display: 'flex', - flexDirection: 'column', - }, -}) + paper: { + position: 'absolute', + top: '120px', + width: '500px', + height: '530px', + borderRadius: sm, + backgroundColor: '#ffffff', + boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)', + '&:focus': { + outline: 'none', + }, + display: 'flex', + flexDirection: 'column', + }, + }), +) + +interface GnoModalProps { + children: React.ReactNode + description: string + // type copied from Material-UI Modal's `close` prop + handleClose?: { + bivarianceHack(event: Record, reason: 'backdropClick' | 'escapeKeyDown'): void + }['bivarianceHack'] + modalClassName?: string + open: boolean + paperClassName?: string + title: string +} const GnoModal = ({ children, - classes, description, handleClose, modalClassName, open, paperClassName, title, -}: any) => ( - -
{children}
-
-) +}: GnoModalProps): React.ReactElement => { + const classes = useStyles() + + return ( + +
{children}
+
+ ) +} -export default withStyles(styles as any)(GnoModal) +export default GnoModal diff --git a/src/routes/safe/components/Layout/index.tsx b/src/routes/safe/components/Layout/index.tsx index 2473eda00c..3b8974103a 100644 --- a/src/routes/safe/components/Layout/index.tsx +++ b/src/routes/safe/components/Layout/index.tsx @@ -35,8 +35,8 @@ const AddressBookTable = React.lazy(() => import('src/routes/safe/components/Add interface Props extends RouteComponentProps { sendFunds: Record showReceive: boolean - onShow: (value: string) => void - onHide: (value: string) => void + onShow: (value: string) => () => void + onHide: (value: string) => () => void showSendFunds: (value: string) => void hideSendFunds: () => void } From d0b2c7d5301945838a2f831a2461e9e597973094 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 6 Aug 2020 21:34:19 -0300 Subject: [PATCH 07/69] update `safe-react-components` --- package.json | 2 +- yarn.lock | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 31c549ca9d..ed79858c14 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ }, "dependencies": { "@gnosis.pm/safe-contracts": "1.1.1-dev.2", - "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#2b4635b", + "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#4df39fe", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid": "5.19.1", "@material-ui/core": "4.11.0", diff --git a/yarn.lock b/yarn.lock index 117189ded8..6e0416c878 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1337,9 +1337,9 @@ solc "0.5.14" truffle "^5.1.21" -"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#2b4635b": +"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#4df39fe": version "0.2.0" - resolved "https://github.com/gnosis/safe-react-components.git#2b4635b22bebfca0260486f1ae7a9a35b02d71c2" + resolved "https://github.com/gnosis/safe-react-components.git#4df39fea2665000bb07bacba19e6f7631cca2de3" dependencies: classnames "^2.2.6" polished "3.6.5" @@ -2327,11 +2327,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.28.tgz#0e36d718a29355ee51cec83b42d921299200f6d9" integrity sha512-dzjES1Egb4c1a89C7lKwQh8pwjYmlOAG9dW1pBgxEk57tMrLnssOfEthz8kdkNaBd7lIqQx7APm5+mZ619IiCQ== -"@types/node@^10.12.18": - version "10.17.28" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.28.tgz#0e36d718a29355ee51cec83b42d921299200f6d9" - integrity sha512-dzjES1Egb4c1a89C7lKwQh8pwjYmlOAG9dW1pBgxEk57tMrLnssOfEthz8kdkNaBd7lIqQx7APm5+mZ619IiCQ== - "@types/node@^10.3.2": version "10.17.27" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.27.tgz#391cb391c75646c8ad2a7b6ed3bbcee52d1bdf19" From cea0817eb5e02228179b99294b34a8f5427199ec Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 6 Aug 2020 21:34:46 -0300 Subject: [PATCH 08/69] WIP: New Spending Limit form --- .../Settings/SpendingLimit/index.tsx | 425 +++++++++++++++++- .../Settings/SpendingLimit/style.ts | 3 + 2 files changed, 416 insertions(+), 12 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 26251d949e..ab96ceef72 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -1,18 +1,33 @@ -import { Text, Title } from '@gnosis.pm/safe-react-components' +import { Button, EthHashInfo, Icon, RadioButtons, Text, TextField, Title } from '@gnosis.pm/safe-react-components' +import { FormControlLabel, hexToRgb, Switch as SwitchMui } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' +import { Mutator } from 'final-form' import React from 'react' +import { useField, useForm, useFormState } from 'react-final-form' +import { useSelector } from 'react-redux' + +import GnoField from 'src/components/forms/Field' +import GnoForm from 'src/components/forms/GnoForm' +import { composeValidators, minValue, mustBeFloat, required } from 'src/components/forms/validator' +import Block from 'src/components/layout/Block' +import Col from 'src/components/layout/Col' +import Img from 'src/components/layout/Img' +import Row from 'src/components/layout/Row' +import GnoModal from 'src/components/Modal' +import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' +import { getNetwork } from 'src/config' +import { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' +import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' +import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' import styled from 'styled-components' -import { styles } from './style' -import Beneficiary from './assets/beneficiary.svg' import AssetAmount from './assets/asset-amount.svg' +import Beneficiary from './assets/beneficiary.svg' import Time from './assets/time.svg' -import Block from 'src/components/layout/Block' -import Img from 'src/components/layout/Img' -import Row from 'src/components/layout/Row' -import Col from 'src/components/layout/Col' -import Button from 'src/components/layout/Button' +import { styles } from './style' const useStyles = makeStyles(styles) @@ -41,8 +56,394 @@ const SpendingLimitStep = styled.div` max-width: 164px; ` +// +// TODO: refactor and split into components for better readability +// + +const Field = styled(GnoField)` + margin: 8px 0; + width: 100%; +` + +/** + * beneficiary - START + */ +const KEYCODES = { + TAB: 9, + SHIFT: 16, +} +const SelectAddressRow = (): React.ReactElement => { + const classes = useStyles() + + const { mutators } = useForm() + + const [selectedEntry, setSelectedEntry] = React.useState<{ address: string; name: string } | null>({ + address: '', + name: '', + }) + + const [pristine, setPristine] = React.useState(true) + React.useMemo(() => { + if (selectedEntry === null && pristine) { + setPristine(false) + } + }, [selectedEntry, pristine]) + + const addressBook = useSelector(getAddressBook) + const handleScan = (value, closeQrModal) => { + let scannedAddress = value + + if (scannedAddress.startsWith('ethereum:')) { + scannedAddress = scannedAddress.replace('ethereum:', '') + } + const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : '' + mutators?.setBeneficiary?.(scannedAddress) + setSelectedEntry({ + name: scannedName, + address: scannedAddress, + }) + closeQrModal() + } + + return ( + + {!!selectedEntry?.address ? ( + +
{ + if (![KEYCODES.TAB, KEYCODES.SHIFT].includes(e.keyCode)) { + setSelectedEntry(null) + } + }} + onClick={() => { + setSelectedEntry(null) + }} + > + +
+ + ) : ( + <> + + + + + + + + )} +
+ ) +} +/** + * beneficiary - END + */ + +/** + * token + */ +const TokenSelectRow = (): React.ReactElement => { + const tokens = useSelector(extendedSafeTokensSelector) + // const { + // input: { value: tokenAddress }, + // meta: { pristine, dirty }, + // } = useField('token', { subscription: { value: true, pristine: true, dirty: true } }) + // const { } = useFormState() + // const setRed = dirty && !pristine && !tokenAddress + + return ( + + + + + + ) +} + +/** + * amount + */ +const AmountSetRow = (): React.ReactElement => { + const classes = useStyles() + + const { + input: { value: tokenAddress }, + meta: { pristine: tokenAddressPristine }, + } = useField('token', { subscription: { value: true, pristine: true } }) + const tokens = useSelector(extendedSafeTokensSelector) + const selectedTokenRecord = tokens.find((token) => token.address === tokenAddress) + const validate = !tokenAddressPristine && composeValidators(required, mustBeFloat, minValue(0, false)) + + return ( + + + + + + ) +} + +/** + * resetTime - START + */ +// TODO: propose refactor in safe-react-components based on this requirements +const SpendingLimitRadioButtons = styled(RadioButtons)` + & .MuiRadio-colorPrimary.Mui-checked { + color: ${({ theme }) => theme.colors.primary}; + } +` +const StyledSwitch = styled(({ ...rest }) => )` + && { + .MuiIconButton-label, + .MuiSwitch-colorSecondary { + color: ${({ theme }) => theme.colors.icon}; + } + + .MuiSwitch-colorSecondary.Mui-checked .MuiIconButton-label { + color: ${({ theme }) => theme.colors.primary}; + } + + .MuiSwitch-colorSecondary.Mui-checked:hover { + background-color: ${({ theme }) => hexToRgb(`${theme.colors.primary}03`)}; + } + + .Mui-checked + .MuiSwitch-track { + background-color: ${({ theme }) => theme.colors.primaryLight}; + } + } +` +interface RadioButtonOption { + label: string + value: string +} +interface RadioButtonProps { + options: RadioButtonOption[] + initialValue: string + groupName: string +} +const SafeRadioButtons = ({ options, initialValue, groupName }: RadioButtonProps): React.ReactElement => ( + + {({ input: { name, value, onChange } }) => ( + + )} + +) +const Switch = ({ label, name }: { label: string; name: string }): React.ReactElement => ( + ( + + )} + /> + } + /> +) +const RESET_TIME_OPTIONS = [ + { label: '1 day', value: '1' }, + { label: '1 week', value: '7' }, + { label: '1 month', value: '30' }, +] +const ResetTime = (): React.ReactElement => { + const { + input: { value: withResetTime }, + } = useField('withResetTime', { subscription: { value: true } }) + + return ( + <> + + + + Set a reset-time to have the allowance automatically refill after a defined time-period. + + + + + + + + + {withResetTime && ( + + + + + + )} + + ) +} +/** + * resetTime - END + */ + +const canSubmit = ({ dirty, invalid, submitting, dirtyFieldsSinceLastSubmit, pristine, values }): boolean => { + return !( + submitting || + invalid || + pristine || + !dirty || + (values.token && !values.amount) || + (values.withResetTime && !values.resetTime) || + !dirtyFieldsSinceLastSubmit + ) +} +const SpendingLimitFormButtons = ({ onClose }: { onClose: () => void }): React.ReactElement => { + const formState = useFormState() + + return ( + <> + + + + + ) +} + +/** + * New Spending Limit Form - START + */ +// TODO: propose refactor in safe-react-components based on this requirements +const TitleSection = styled.div` + display: flex; + justify-content: space-between; + padding: 16px 24px; + border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; +` +const StyledButton = styled.button` + background: none; + border: none; + padding: 5px; + width: 26px; + height: 26px; + + span { + margin-right: 0; + } + + :hover { + background: ${({ theme }) => theme.colors.separator}; + border-radius: 16px; + cursor: pointer; + } +` +const FormContainer = styled.div` + padding: 24px; +` +const FooterSection = styled.div` + border-top: 2px solid ${({ theme }) => theme.colors.separator}; + padding: 16px 24px; +` +const FooterWrapper = styled.div` + display: flex; + justify-content: space-around; +` +const formMutators: Record> = { + setBeneficiary: (args, state, utils) => { + utils.changeValue(state, 'beneficiary', () => args[0]) + }, +} +const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boolean }): React.ReactElement => { + const classes = useStyles() + + const handleSubmit = (values) => { + console.log(values) + } + + return ( + + + + New Spending Limit + + + + + + + + + {() => ( + <> + + + + + + + + + + + + + + )} + + + ) +} +/** + * New Spending Limit Form - END + */ + const SpendingLimit = (): React.ReactElement => { const classes = useStyles() + const [showNewSpendingLimitModal, setShowNewSpendingLimitModal] = React.useState(false) + + const openNewSpendingLimitModal = () => { + setShowNewSpendingLimitModal(true) + } + + const closeNewSpendingLimitModal = () => { + setShowNewSpendingLimitModal(false) + } return ( <> @@ -95,16 +496,16 @@ const SpendingLimit = (): React.ReactElement => { + {showNewSpendingLimitModal && } ) } diff --git a/src/routes/safe/components/Settings/SpendingLimit/style.ts b/src/routes/safe/components/Settings/SpendingLimit/style.ts index 43cb78c445..b01a6cf360 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/style.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/style.ts @@ -125,4 +125,7 @@ export const styles = createStyles({ maxWidth: 'calc(100% - 30px)', overflow: 'hidden', }, + amountInput: { + width: '100% !important', + }, }) From 83a047887079a2e05cd869ca3aec901261ba98e8 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 6 Aug 2020 21:51:36 -0300 Subject: [PATCH 09/69] fix compatibility with new `safe-react-components` version --- src/routes/safe/components/Apps/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index 4495bf9492..19e0e68244 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -1,4 +1,5 @@ import { Card, FixedIcon, IconText, Loader, Menu, Title } from '@gnosis.pm/safe-react-components' +import { makeStyles, createStyles } from '@material-ui/core/styles' import { withSnackbar } from 'notistack' import React, { useCallback, useEffect, useState, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -23,6 +24,8 @@ import { } from 'src/routes/safe/store/selectors' import { isSameHref } from 'src/utils/url' +const useStyles = makeStyles(createStyles({ card: { marginBottom: '24px' } })) + const StyledIframe = styled.iframe` padding: 15px; box-sizing: border-box; @@ -60,6 +63,7 @@ const operations = { } function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) { + const classes = useStyles() const { appList, loadingAppList, onAppToggle, onAppAdded } = useAppList() const [appIsLoading, setAppIsLoading] = useState(true) @@ -271,7 +275,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) { {getContent()} ) : ( - + No Apps Enabled From 66c1e85d930de3da0d5c7b53676f883490691c73 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 7 Aug 2020 10:19:53 -0300 Subject: [PATCH 10/69] update `safe-react-components` version --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ed79858c14..27b5976288 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ }, "dependencies": { "@gnosis.pm/safe-contracts": "1.1.1-dev.2", - "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#4df39fe", + "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#45c746a", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid": "5.19.1", "@material-ui/core": "4.11.0", diff --git a/yarn.lock b/yarn.lock index a5a185d928..065aba1167 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1337,9 +1337,9 @@ solc "0.5.14" truffle "^5.1.21" -"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#4df39fe": +"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#45c746a": version "0.2.0" - resolved "https://github.com/gnosis/safe-react-components.git#4df39fea2665000bb07bacba19e6f7631cca2de3" + resolved "https://github.com/gnosis/safe-react-components.git#45c746a12661b9c38e839e76022b6a0a92285db7" dependencies: classnames "^2.2.6" polished "3.6.5" @@ -18784,7 +18784,6 @@ websocket@^1.0.31: dependencies: debug "^2.2.0" es5-ext "^0.10.50" - gulp "^4.0.2" nan "^2.14.0" typedarray-to-buffer "^3.1.5" yaeti "^0.0.6" From debf1cea762adb42afde780e97aa471a91f42f6f Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 10 Aug 2020 21:45:57 -0300 Subject: [PATCH 11/69] fix styles for `TokenSelectField`'s placeholder --- .../SendModal/screens/SendFunds/TokenSelectField/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx index c87f53052f..02b50b561b 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx @@ -1,3 +1,4 @@ +import { Text } from '@gnosis.pm/safe-react-components' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' import MenuItem from '@material-ui/core/MenuItem' @@ -12,7 +13,6 @@ import Field from 'src/components/forms/Field' import SelectField from 'src/components/forms/SelectField' import { required } from 'src/components/forms/validator' import Img from 'src/components/layout/Img' -import Paragraph from 'src/components/layout/Paragraph' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' @@ -42,9 +42,9 @@ const SelectedToken = ({ tokenAddress, tokens }: SelectTokenProps): React.ReactE /> ) : ( - + Select an asset* - + )} ) From 7c109f032ddb9298525b9bcaafa4fae1cfdff9e7 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 10 Aug 2020 21:47:22 -0300 Subject: [PATCH 12/69] WIP: add review screen Also: - fixed styles - handled form state while going back and forth between create and review - had to change `Review` Button as submitting wasn't triggered on the first click. --- .../Settings/SpendingLimit/index.tsx | 294 ++++++++++++++---- 1 file changed, 229 insertions(+), 65 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index ab96ceef72..0a593fef14 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -1,6 +1,16 @@ -import { Button, EthHashInfo, Icon, RadioButtons, Text, TextField, Title } from '@gnosis.pm/safe-react-components' +import { + Button, + EthHashInfo, + Icon, + IconText, + RadioButtons, + Text, + TextField, + Title, +} from '@gnosis.pm/safe-react-components' import { FormControlLabel, hexToRgb, Switch as SwitchMui } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' +import { Skeleton } from '@material-ui/lab' import { Mutator } from 'final-form' import React from 'react' import { useField, useForm, useFormState } from 'react-final-form' @@ -10,6 +20,7 @@ import GnoField from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' import { composeValidators, minValue, mustBeFloat, required } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' +import GnoButton from 'src/components/layout/Button' import Col from 'src/components/layout/Col' import Img from 'src/components/layout/Img' import Row from 'src/components/layout/Row' @@ -18,8 +29,10 @@ import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import { getNetwork } from 'src/config' import { getAddressBook } from 'src/logic/addressBook/store/selectors' import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { Token } from 'src/logic/tokens/store/model/token' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' +import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' import styled from 'styled-components' @@ -75,19 +88,29 @@ const KEYCODES = { const SelectAddressRow = (): React.ReactElement => { const classes = useStyles() + const { initialValues } = useFormState() const { mutators } = useForm() + React.useEffect(() => { + if (initialValues?.beneficiary) { + mutators?.setBeneficiary?.(initialValues.beneficiary) + } + }, [initialValues, mutators]) const [selectedEntry, setSelectedEntry] = React.useState<{ address: string; name: string } | null>({ - address: '', + address: initialValues?.beneficiary || '', name: '', }) - const [pristine, setPristine] = React.useState(true) + const [pristine, setPristine] = React.useState(!initialValues?.beneficiary) React.useMemo(() => { - if (selectedEntry === null && pristine) { - setPristine(false) + if (selectedEntry === null) { + mutators?.setBeneficiary?.('') + + if (pristine) { + setPristine(false) + } } - }, [selectedEntry, pristine]) + }, [selectedEntry, mutators, pristine]) const addressBook = useSelector(getAddressBook) const handleScan = (value, closeQrModal) => { @@ -160,12 +183,6 @@ const SelectAddressRow = (): React.ReactElement => { */ const TokenSelectRow = (): React.ReactElement => { const tokens = useSelector(extendedSafeTokensSelector) - // const { - // input: { value: tokenAddress }, - // meta: { pristine, dirty }, - // } = useField('token', { subscription: { value: true, pristine: true, dirty: true } }) - // const { } = useFormState() - // const setRed = dirty && !pristine && !tokenAddress return ( @@ -184,18 +201,22 @@ const AmountSetRow = (): React.ReactElement => { const { input: { value: tokenAddress }, - meta: { pristine: tokenAddressPristine }, - } = useField('token', { subscription: { value: true, pristine: true } }) + } = useField('token', { subscription: { value: true } }) + const { + meta: { touched, visited }, + } = useField('amount', { subscription: { touched: true, visited: true } }) const tokens = useSelector(extendedSafeTokensSelector) const selectedTokenRecord = tokens.find((token) => token.address === tokenAddress) - const validate = !tokenAddressPristine && composeValidators(required, mustBeFloat, minValue(0, false)) + const validate = (touched || visited) && composeValidators(required, mustBeFloat, minValue(0, false)) + + console.log({ validate }) return ( { * resetTime - END */ -const canSubmit = ({ dirty, invalid, submitting, dirtyFieldsSinceLastSubmit, pristine, values }): boolean => { +const YetAnotherButton = styled(GnoButton)` + &.Mui-disabled { + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.white}; + opacity: 0.5; + } +` +const canReview = ({ invalid, submitting, dirtyFieldsSinceLastSubmit, values }): boolean => { return !( submitting || invalid || - pristine || - !dirty || + !values.beneficiary || (values.token && !values.amount) || + // TODO: review the next validation, as resetTime has a default value, this check looks unnecessary (values.withResetTime && !values.resetTime) || !dirtyFieldsSinceLastSubmit ) } -const SpendingLimitFormButtons = ({ onClose }: { onClose: () => void }): React.ReactElement => { - const formState = useFormState() +interface NewSpendingLimitProps { + initialValues?: Record + onCancel: () => void + onReview: (values) => void +} +/** + * New Spending Limit Form - START + */ +const NewSpendingLimit = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): React.ReactElement => ( + <> + + + New Spending Limit{' '} + <Text size="lg" color="secondaryLight"> + 1 of 2 + </Text> + + + + + + + + + {(...args) => ( + <> + + + + + + + + + + + + + Review + + + + + )} + + +) + +const StyledImage = styled.img` + width: 32px; + height: 32px; + object-fit: contain; + margin: 0 8px 0 0; +` +const StyledImageName = styled.div` + display: flex; + align-items: center; +` +interface ReviewSpendingLimitProps { + onBack: () => void + onClose: () => void + onSubmit: () => void + txToken: Token | null + values: Record +} +const ReviewSpendingLimit = ({ + onBack, + onClose, + onSubmit, + txToken, + values, +}: ReviewSpendingLimitProps): React.ReactElement => { + const addressBook = useSelector(getAddressBook) return ( <> - + + + New Spending Limit{' '} + <Text size="lg" color="secondaryLight"> + 2 of 2 + </Text> + + + + + + - + + + + Beneficiary + + + + + + Amount + + {txToken !== null ? ( + + + + {values.amount} {txToken.symbol} + + + ) : ( + + )} + + + + Reset Time + + {values.withResetTime ? ( + + value === values.resetTime).label} + textSize="lg" + /> + + ) : ( + + + {/* TODO: review message */} + One-time spending limit allowance + + + )} + + + + + + + + + + ) } - -/** - * New Spending Limit Form - START - */ // TODO: propose refactor in safe-react-components based on this requirements const TitleSection = styled.div` display: flex; @@ -382,8 +556,20 @@ const formMutators: Record> = const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boolean }): React.ReactElement => { const classes = useStyles() - const handleSubmit = (values) => { - console.log(values) + const [step, setStep] = React.useState<'create' | 'review'>('create') + const [values, setValues] = React.useState() + const tokens = useSelector(extendedSafeTokensSelector) + const [txToken, setTxToken] = React.useState(null) + const handleReview = (values) => { + setValues(values) + if (values.token) { + setTxToken(tokens.find((token) => token.address === values.token)) + } + setStep('review') + } + const handleSubmit = () => { + // TODO: here we do the magic. FINALLY!!!! + console.log({ values }) } return ( @@ -394,38 +580,16 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole description="set rules for specific beneficiaries to access funds from this Safe without having to collect all signatures" paperClassName={classes.modal} > - - - New Spending Limit - - - - - - - - - {() => ( - <> - - - - - - - - - - - - - - )} - + {step === 'create' && } + {step === 'review' && ( + setStep('create')} + onClose={close} + onSubmit={handleSubmit} + txToken={txToken} + values={values} + /> + )} ) } From b8540533a7fb70830cb8bc30074492725a3f0240 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 11 Aug 2020 16:26:21 -0300 Subject: [PATCH 13/69] WIP: fix styles/layout - also refactored `ScanQRWrapper` so it uses the specified icon --- .../ScanQRModal/ScanQRWrapper/index.tsx | 32 +-- .../Settings/SpendingLimit/index.tsx | 270 +++++++++--------- 2 files changed, 156 insertions(+), 146 deletions(-) diff --git a/src/components/ScanQRModal/ScanQRWrapper/index.tsx b/src/components/ScanQRModal/ScanQRWrapper/index.tsx index 9982e6c400..1a4cbca479 100644 --- a/src/components/ScanQRModal/ScanQRWrapper/index.tsx +++ b/src/components/ScanQRModal/ScanQRWrapper/index.tsx @@ -1,20 +1,24 @@ +import { Icon } from '@gnosis.pm/safe-react-components' import { makeStyles } from '@material-ui/core/styles' -import { useState } from 'react' -import * as React from 'react' +import React from 'react' -import QRIcon from 'src/assets/icons/qrcode.svg' import ScanQRModal from 'src/components/ScanQRModal' -import Img from 'src/components/layout/Img' const useStyles = makeStyles({ qrCodeBtn: { cursor: 'pointer', + border: 'none', + backgroundColor: 'transparent', }, }) -export const ScanQRWrapper = (props) => { +interface ScanQRWrapperProps { + handleScan: (value: string, closeQRCallback: () => void) => void +} + +export const ScanQRWrapper = ({ handleScan }: ScanQRWrapperProps): React.ReactElement => { const classes = useStyles() - const [qrModalOpen, setQrModalOpen] = useState(false) + const [qrModalOpen, setQrModalOpen] = React.useState(false) const openQrModal = () => { setQrModalOpen(true) @@ -25,22 +29,14 @@ export const ScanQRWrapper = (props) => { } const onScanFinished = (value) => { - props.handleScan(value, closeQrModal) + handleScan(value, closeQrModal) } return ( <> - Scan QR { - openQrModal() - }} - role="button" - src={QRIcon} - testId="qr-icon" - /> + {qrModalOpen && } ) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 0a593fef14..d5aaa332db 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -15,6 +15,7 @@ import { Mutator } from 'final-form' import React from 'react' import { useField, useForm, useFormState } from 'react-final-form' import { useSelector } from 'react-redux' +import styled from 'styled-components' import GnoField from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' @@ -34,7 +35,6 @@ import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/scre import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' -import styled from 'styled-components' import AssetAmount from './assets/asset-amount.svg' import Beneficiary from './assets/beneficiary.svg' @@ -68,7 +68,6 @@ const SpendingLimitStep = styled.div` min-width: 120px; max-width: 164px; ` - // // TODO: refactor and split into components for better readability // @@ -85,9 +84,13 @@ const KEYCODES = { TAB: 9, SHIFT: 16, } +const BeneficiaryInput = styled.div` + grid-area: beneficiaryInput; +` +const BeneficiaryScan = styled.div` + grid-area: beneficiaryScan; +` const SelectAddressRow = (): React.ReactElement => { - const classes = useStyles() - const { initialValues } = useFormState() const { mutators } = useForm() React.useEffect(() => { @@ -128,50 +131,44 @@ const SelectAddressRow = (): React.ReactElement => { closeQrModal() } - return ( - - {!!selectedEntry?.address ? ( - -
{ - if (![KEYCODES.TAB, KEYCODES.SHIFT].includes(e.keyCode)) { - setSelectedEntry(null) - } - }} - onClick={() => { - setSelectedEntry(null) - }} - > - -
- - ) : ( - <> - - - - - - - - )} -
+ return !!selectedEntry?.address ? ( + { + if (![KEYCODES.TAB, KEYCODES.SHIFT].includes(e.keyCode)) { + setSelectedEntry(null) + } + }} + onClick={() => { + setSelectedEntry(null) + }} + > + + + ) : ( + <> + + + + + + + ) } /** @@ -181,21 +178,28 @@ const SelectAddressRow = (): React.ReactElement => { /** * token */ +const TokenInput = styled.div` + grid-area: tokenInput; +` const TokenSelectRow = (): React.ReactElement => { const tokens = useSelector(extendedSafeTokensSelector) return ( - - - - - + + + ) } /** * amount */ +const AmountInput = styled.div` + grid-area: amountInput; +` +const GnoTextField = styled(TextField)` + margin: 0; +` const AmountSetRow = (): React.ReactElement => { const classes = useStyles() @@ -209,23 +213,19 @@ const AmountSetRow = (): React.ReactElement => { const selectedTokenRecord = tokens.find((token) => token.address === tokenAddress) const validate = (touched || visited) && composeValidators(required, mustBeFloat, minValue(0, false)) - console.log({ validate }) - return ( - - - - - + + + ) } @@ -293,6 +293,15 @@ const RESET_TIME_OPTIONS = [ { label: '1 week', value: '7' }, { label: '1 month', value: '30' }, ] +const ResetTimeLabel = styled.div` + grid-area: resetTimeLabel; +` +const ResetTimeToggle = styled.div` + grid-area: resetTimeToggle; +` +const ResetTimeOptions = styled.div` + grid-area: resetTimeOption; +` const ResetTime = (): React.ReactElement => { const { input: { value: withResetTime }, @@ -300,28 +309,20 @@ const ResetTime = (): React.ReactElement => { return ( <> - - - - Set a reset-time to have the allowance automatically refill after a defined time-period. - - - - - - - - + + Set a reset-time to have the allowance automatically refill after a defined time-period. + + + + {withResetTime && ( - - - - - + + + )} ) @@ -356,6 +357,52 @@ interface NewSpendingLimitProps { /** * New Spending Limit Form - START */ +const TitleSection = styled.div` + display: flex; + justify-content: space-between; + padding: 16px 24px; + border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; +` +const StyledButton = styled.button` + background: none; + border: none; + padding: 5px; + width: 26px; + height: 26px; + + span { + margin-right: 0; + } + + :hover { + background: ${({ theme }) => theme.colors.separator}; + border-radius: 16px; + cursor: pointer; + } +` +const FormContainer = styled.div` + padding: 24px; + align-items: center; + display: grid; + grid-template-columns: 4fr 1fr; + grid-template-rows: 6fr; + gap: 16px 8px; + grid-template-areas: + 'beneficiaryInput beneficiaryScan' + 'tokenInput .' + 'amountInput .' + 'resetTimeLabel resetTimeLabel' + 'resetTimeToggle resetTimeToggle' + 'resetTimeOption resetTimeOption'; +` +const FooterSection = styled.div` + border-top: 2px solid ${({ theme }) => theme.colors.separator}; + padding: 16px 24px; +` +const FooterWrapper = styled.div` + display: flex; + justify-content: space-around; +` const NewSpendingLimit = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): React.ReactElement => ( <> @@ -383,7 +430,7 @@ const NewSpendingLimit = ({ initialValues, onCancel, onReview }: NewSpendingLimi - @@ -428,6 +475,7 @@ const ReviewSpendingLimit = ({ txToken, values, }: ReviewSpendingLimitProps): React.ReactElement => { + const classes = useStyles() const addressBook = useSelector(getAddressBook) return ( @@ -445,7 +493,7 @@ const ReviewSpendingLimit = ({ - + Beneficiary @@ -497,11 +545,11 @@ const ReviewSpendingLimit = ({
)} - + - @@ -514,40 +562,6 @@ const ReviewSpendingLimit = ({ ) } // TODO: propose refactor in safe-react-components based on this requirements -const TitleSection = styled.div` - display: flex; - justify-content: space-between; - padding: 16px 24px; - border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; -` -const StyledButton = styled.button` - background: none; - border: none; - padding: 5px; - width: 26px; - height: 26px; - - span { - margin-right: 0; - } - - :hover { - background: ${({ theme }) => theme.colors.separator}; - border-radius: 16px; - cursor: pointer; - } -` -const FormContainer = styled.div` - padding: 24px; -` -const FooterSection = styled.div` - border-top: 2px solid ${({ theme }) => theme.colors.separator}; - padding: 16px 24px; -` -const FooterWrapper = styled.div` - display: flex; - justify-content: space-around; -` const formMutators: Record> = { setBeneficiary: (args, state, utils) => { utils.changeValue(state, 'beneficiary', () => args[0]) From 3c9543d27bc58b6618a9e6edb21d648f951ffcb8 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 13 Aug 2020 10:58:25 -0300 Subject: [PATCH 14/69] disable Spending Limit button for non-owners --- src/routes/safe/components/Settings/SpendingLimit/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index d5aaa332db..1db9e81a41 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -34,7 +34,7 @@ import { Token } from 'src/logic/tokens/store/model/token' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' -import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' +import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' import AssetAmount from './assets/asset-amount.svg' import Beneficiary from './assets/beneficiary.svg' @@ -613,6 +613,7 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole const SpendingLimit = (): React.ReactElement => { const classes = useStyles() + const granted = useSelector(grantedSelector) const [showNewSpendingLimitModal, setShowNewSpendingLimitModal] = React.useState(false) const openNewSpendingLimitModal = () => { @@ -674,6 +675,7 @@ const SpendingLimit = (): React.ReactElement => { + + + Review + + + + + )} + + +) + +export default NewSpendingLimit diff --git a/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx b/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx new file mode 100644 index 0000000000..ee9d4bc35c --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx @@ -0,0 +1,113 @@ +import { RadioButtons, Text } from '@gnosis.pm/safe-react-components' +import { FormControlLabel, hexToRgb, Switch as SwitchMui } from '@material-ui/core' +import React from 'react' +import { useField } from 'react-final-form' +import { Field } from 'src/routes/safe/components/Settings/SpendingLimit/Amount' +import styled from 'styled-components' + +// TODO: propose refactor in safe-react-components based on this requirements +const SpendingLimitRadioButtons = styled(RadioButtons)` + & .MuiRadio-colorPrimary.Mui-checked { + color: ${({ theme }) => theme.colors.primary}; + } +` +const StyledSwitch = styled(({ ...rest }) => )` + && { + .MuiIconButton-label, + .MuiSwitch-colorSecondary { + color: ${({ theme }) => theme.colors.icon}; + } + + .MuiSwitch-colorSecondary.Mui-checked .MuiIconButton-label { + color: ${({ theme }) => theme.colors.primary}; + } + + .MuiSwitch-colorSecondary.Mui-checked:hover { + background-color: ${({ theme }) => hexToRgb(`${theme.colors.primary}03`)}; + } + + .Mui-checked + .MuiSwitch-track { + background-color: ${({ theme }) => theme.colors.primaryLight}; + } + } +` + +interface RadioButtonOption { + label: string + value: string +} + +interface RadioButtonProps { + options: RadioButtonOption[] + initialValue: string + groupName: string +} + +const SafeRadioButtons = ({ options, initialValue, groupName }: RadioButtonProps): React.ReactElement => ( + + {({ input: { name, value, onChange } }) => ( + + )} + +) + +const Switch = ({ label, name }: { label: string; name: string }): React.ReactElement => ( + ( + + )} + /> + } + /> +) + +const ResetTimeLabel = styled.div` + grid-area: resetTimeLabel; +` + +const ResetTimeToggle = styled.div` + grid-area: resetTimeToggle; +` + +const ResetTimeOptions = styled.div` + grid-area: resetTimeOption; +` + +export const RESET_TIME_OPTIONS = [ + { label: '1 day', value: '1' }, + { label: '1 week', value: '7' }, + { label: '1 month', value: '30' }, +] + +const ResetTime = (): React.ReactElement => { + const { + input: { value: withResetTime }, + } = useField('withResetTime', { subscription: { value: true } }) + + return ( + <> + + Set a reset-time to have the allowance automatically refill after a defined time-period. + + + + + {withResetTime && ( + + + + )} + + ) +} + +export default ResetTime diff --git a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx new file mode 100644 index 0000000000..2ffeb2bcb0 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx @@ -0,0 +1,162 @@ +import { Button, EthHashInfo, Icon, IconText, Text, Title } from '@gnosis.pm/safe-react-components' +import { Skeleton } from '@material-ui/lab' +import React from 'react' +import { useSelector } from 'react-redux' +import Block from 'src/components/layout/Block' +import Col from 'src/components/layout/Col' +import Row from 'src/components/layout/Row' +import { getNetwork } from 'src/config' +import { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { Token } from 'src/logic/tokens/store/model/token' +import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' +import { + FooterSection, + FooterWrapper, + StyledButton, + TitleSection, +} from 'src/routes/safe/components/Settings/SpendingLimit/index' +import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/ResetTime' +import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' +import styled from 'styled-components' + +const StyledImage = styled.img` + width: 32px; + height: 32px; + object-fit: contain; + margin: 0 8px 0 0; +` + +const StyledImageName = styled.div` + display: flex; + align-items: center; +` + +interface ReviewSpendingLimitProps { + onBack: () => void + onClose: () => void + onSubmit: () => void + txToken: Token | null + values: Record + existentSpendingLimit?: Record +} + +const ReviewSpendingLimit = ({ + onBack, + onClose, + onSubmit, + txToken, + values, + existentSpendingLimit, +}: ReviewSpendingLimitProps): React.ReactElement => { + const classes = useStyles() + const addressBook = useSelector(getAddressBook) + + return ( + <> + + + New Spending Limit{' '} + <Text size="lg" color="secondaryLight"> + 2 of 2 + </Text> + + + + + + + + + + + Beneficiary + + + + + + Amount + + {txToken !== null ? ( + <> + + + + {values.amount} {txToken.symbol} + + + {existentSpendingLimit && ( + + Previous Amount: {existentSpendingLimit.amount} + + )} + + ) : ( + + )} + + + + Reset Time + + {values.withResetTime ? ( + + value === values.resetTime).label} + textSize="lg" + /> + + ) : ( + + + {/* TODO: review message */} + One-time spending limit allowance + + + )} + {existentSpendingLimit && ( + + + Previous Reset Time:{' '} + {RESET_TIME_OPTIONS.find( + ({ value }) => value === (+existentSpendingLimit.resetTimeMin / 60 / 24).toString(), + )?.label ?? 'One-time spending limit allowance'} + + + )} + + + {existentSpendingLimit && ( + + You are about to replace an existent spending limit + + )} + + + + + + + + + + + ) +} + +export default ReviewSpendingLimit diff --git a/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps.tsx b/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps.tsx new file mode 100644 index 0000000000..45382385d1 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps.tsx @@ -0,0 +1,79 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React from 'react' +import styled from 'styled-components' + +import Img from 'src/components/layout/Img' +import AssetAmount from 'src/routes/safe/components/Settings/SpendingLimit/assets/asset-amount.svg' +import Beneficiary from 'src/routes/safe/components/Settings/SpendingLimit/assets/beneficiary.svg' +import Time from 'src/routes/safe/components/Settings/SpendingLimit/assets/time.svg' + +const StepWrapper = styled.div` + display: flex; + justify-content: space-around; + margin-top: 20px; + max-width: 720px; + text-align: center; +` + +const Step = styled.div` + width: 24%; + min-width: 120px; + max-width: 164px; +` + +const StepsLine = styled.div` + height: 2px; + flex: 1; + background: #d4d5d3; + margin: 46px 0; +` + +const SpendingLimitSteps = (): React.ReactElement => ( + + + Select Beneficiary + + + Select Beneficiary + + + + Choose an account that will benefit from this allowance. + + + + The beneficiary does not have to be an owner of this Safe + + + + + + + Select asset and amount + + + Select asset and amount + + + + You can set a spending limit for any asset stored in your Safe + + + + + + + Select time + + + Select time + + + + You can choose to set a one-time spending limit or to have it automatically refill after a defined time-period + + + +) + +export default SpendingLimitSteps diff --git a/src/routes/safe/components/Settings/SpendingLimit/TokenSelect.tsx b/src/routes/safe/components/Settings/SpendingLimit/TokenSelect.tsx new file mode 100644 index 0000000000..276a84e666 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/TokenSelect.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' +import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' +import styled from 'styled-components' + +const TokenInput = styled.div` + grid-area: tokenInput; +` + +const TokenSelect = (): React.ReactElement => { + const tokens = useSelector(extendedSafeTokensSelector) + + return ( + + + + ) +} + +export default TokenSelect diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 66a5f55c07..8c86167051 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -1,379 +1,42 @@ import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' -import { - Button, - EthHashInfo, - Icon, - IconText, - RadioButtons, - Text, - TextField, - Title, -} from '@gnosis.pm/safe-react-components' -import { FormControlLabel, hexToRgb, Switch as SwitchMui } from '@material-ui/core' -import { makeStyles } from '@material-ui/core/styles' -import { Skeleton } from '@material-ui/lab' +import { Button, Text, Title } from '@gnosis.pm/safe-react-components' import { BigNumber } from 'bignumber.js' -import { Mutator } from 'final-form' import { useSnackbar } from 'notistack' import React from 'react' -import { useField, useForm, useFormState } from 'react-final-form' import { useDispatch, useSelector } from 'react-redux' +import styled from 'styled-components' -import GnoField from 'src/components/forms/Field' -import GnoForm from 'src/components/forms/GnoForm' -import { composeValidators, minValue, mustBeFloat, required } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' -import GnoButton from 'src/components/layout/Button' import Col from 'src/components/layout/Col' -import Img from 'src/components/layout/Img' import Row from 'src/components/layout/Row' import GnoModal from 'src/components/Modal' -import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' -import { getNetwork } from 'src/config' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromAdbk } from 'src/logic/addressBook/utils' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' -import { Token } from 'src/logic/tokens/store/model/token' import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3' import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' -import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' -import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' -import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' + +import NewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit' +import ReviewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit' +import SpendingLimitSteps from 'src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps' import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' -import { safeModulesSelector, safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' +import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' -import styled from 'styled-components' - -import AssetAmount from './assets/asset-amount.svg' -import Beneficiary from './assets/beneficiary.svg' -import Time from './assets/time.svg' -import { styles } from './style' - -const useStyles = makeStyles(styles) +import { useStyles } from './style' const InfoText = styled(Text)` margin-top: 16px; ` -const Steps = styled.div` - display: flex; - justify-content: space-around; - margin-top: 20px; - max-width: 720px; - text-align: center; -` - -const StepsLine = styled.div` - height: 2px; - flex: 1; - background: #d4d5d3; - margin: 46px 0; -` - -const SpendingLimitStep = styled.div` - width: 24%; - min-width: 120px; - max-width: 164px; -` -// -// TODO: refactor and split into components for better readability -// - -const Field = styled(GnoField)` - margin: 8px 0; - width: 100%; -` - -/** - * beneficiary - START - */ -const KEYCODES = { - TAB: 9, - SHIFT: 16, -} -const BeneficiaryInput = styled.div` - grid-area: beneficiaryInput; -` -const BeneficiaryScan = styled.div` - grid-area: beneficiaryScan; -` -const SelectAddressRow = (): React.ReactElement => { - const { initialValues } = useFormState() - const { mutators } = useForm() - React.useEffect(() => { - if (initialValues?.beneficiary) { - mutators?.setBeneficiary?.(initialValues.beneficiary) - } - }, [initialValues, mutators]) - - const [selectedEntry, setSelectedEntry] = React.useState<{ address: string; name: string } | null>({ - address: initialValues?.beneficiary || '', - name: '', - }) - - const [pristine, setPristine] = React.useState(!initialValues?.beneficiary) - React.useMemo(() => { - if (selectedEntry === null) { - mutators?.setBeneficiary?.('') - - if (pristine) { - setPristine(false) - } - } - }, [selectedEntry, mutators, pristine]) - - const addressBook = useSelector(getAddressBook) - const handleScan = (value, closeQrModal) => { - let scannedAddress = value - - if (scannedAddress.startsWith('ethereum:')) { - scannedAddress = scannedAddress.replace('ethereum:', '') - } - const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : '' - mutators?.setBeneficiary?.(scannedAddress) - setSelectedEntry({ - name: scannedName, - address: scannedAddress, - }) - closeQrModal() - } - - return !!selectedEntry?.address ? ( - { - if (![KEYCODES.TAB, KEYCODES.SHIFT].includes(e.keyCode)) { - setSelectedEntry(null) - } - }} - onClick={() => { - setSelectedEntry(null) - }} - > - - - ) : ( - <> - - - - - - - - ) -} -/** - * beneficiary - END - */ - -/** - * token - */ -const TokenInput = styled.div` - grid-area: tokenInput; -` -const TokenSelectRow = (): React.ReactElement => { - const tokens = useSelector(extendedSafeTokensSelector) - - return ( - - - - ) -} - -/** - * amount - */ -const AmountInput = styled.div` - grid-area: amountInput; -` -const GnoTextField = styled(TextField)` - margin: 0; -` -const AmountSetRow = (): React.ReactElement => { - const classes = useStyles() - - const { - input: { value: tokenAddress }, - } = useField('token', { subscription: { value: true } }) - const { - meta: { touched, visited }, - } = useField('amount', { subscription: { touched: true, visited: true } }) - const tokens = useSelector(extendedSafeTokensSelector) - const selectedTokenRecord = tokens.find((token) => token.address === tokenAddress) - const validate = (touched || visited) && composeValidators(required, mustBeFloat, minValue(0, false)) - - return ( - - - - ) -} - -/** - * resetTime - START - */ -// TODO: propose refactor in safe-react-components based on this requirements -const SpendingLimitRadioButtons = styled(RadioButtons)` - & .MuiRadio-colorPrimary.Mui-checked { - color: ${({ theme }) => theme.colors.primary}; - } -` -const StyledSwitch = styled(({ ...rest }) => )` - && { - .MuiIconButton-label, - .MuiSwitch-colorSecondary { - color: ${({ theme }) => theme.colors.icon}; - } - - .MuiSwitch-colorSecondary.Mui-checked .MuiIconButton-label { - color: ${({ theme }) => theme.colors.primary}; - } - - .MuiSwitch-colorSecondary.Mui-checked:hover { - background-color: ${({ theme }) => hexToRgb(`${theme.colors.primary}03`)}; - } - - .Mui-checked + .MuiSwitch-track { - background-color: ${({ theme }) => theme.colors.primaryLight}; - } - } -` -interface RadioButtonOption { - label: string - value: string -} -interface RadioButtonProps { - options: RadioButtonOption[] - initialValue: string - groupName: string -} -const SafeRadioButtons = ({ options, initialValue, groupName }: RadioButtonProps): React.ReactElement => ( - - {({ input: { name, value, onChange } }) => ( - - )} - -) -const Switch = ({ label, name }: { label: string; name: string }): React.ReactElement => ( - ( - - )} - /> - } - /> -) -const RESET_TIME_OPTIONS = [ - { label: '1 day', value: '1' }, - { label: '1 week', value: '7' }, - { label: '1 month', value: '30' }, -] -const ResetTimeLabel = styled.div` - grid-area: resetTimeLabel; -` -const ResetTimeToggle = styled.div` - grid-area: resetTimeToggle; -` -const ResetTimeOptions = styled.div` - grid-area: resetTimeOption; -` -const ResetTime = (): React.ReactElement => { - const { - input: { value: withResetTime }, - } = useField('withResetTime', { subscription: { value: true } }) - - return ( - <> - - Set a reset-time to have the allowance automatically refill after a defined time-period. - - - - - {withResetTime && ( - - - - )} - - ) -} -/** - * resetTime - END - */ - -const YetAnotherButton = styled(GnoButton)` - &.Mui-disabled { - background-color: ${({ theme }) => theme.colors.primary}; - color: ${({ theme }) => theme.colors.white}; - opacity: 0.5; - } -` -const canReview = ({ invalid, submitting, dirtyFieldsSinceLastSubmit, values }): boolean => { - return !( - submitting || - invalid || - !values.beneficiary || - (values.token && !values.amount) || - // TODO: review the next validation, as resetTime has a default value, this check looks unnecessary - (values.withResetTime && !values.resetTime) || - !dirtyFieldsSinceLastSubmit - ) -} -interface NewSpendingLimitProps { - initialValues?: Record - onCancel: () => void - onReview: (values) => void -} -/** - * New Spending Limit Form - START - */ -const TitleSection = styled.div` +export const TitleSection = styled.div` display: flex; justify-content: space-between; padding: 16px 24px; border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; ` -const StyledButton = styled.button` + +export const StyledButton = styled.button` background: none; border: none; padding: 5px; @@ -390,218 +53,16 @@ const StyledButton = styled.button` cursor: pointer; } ` -const FormContainer = styled.div` - padding: 24px; - align-items: center; - display: grid; - grid-template-columns: 4fr 1fr; - grid-template-rows: 6fr; - gap: 16px 8px; - grid-template-areas: - 'beneficiaryInput beneficiaryScan' - 'tokenInput .' - 'amountInput .' - 'resetTimeLabel resetTimeLabel' - 'resetTimeToggle resetTimeToggle' - 'resetTimeOption resetTimeOption'; -` -const FooterSection = styled.div` + +export const FooterSection = styled.div` border-top: 2px solid ${({ theme }) => theme.colors.separator}; padding: 16px 24px; ` -const FooterWrapper = styled.div` + +export const FooterWrapper = styled.div` display: flex; justify-content: space-around; ` -const NewSpendingLimit = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): React.ReactElement => ( - <> - - - New Spending Limit{' '} - <Text size="lg" color="secondaryLight"> - 1 of 2 - </Text> - - - - - - - - - {(...args) => ( - <> - - - - - - - - - - - - - Review - - - - - )} - - -) - -const StyledImage = styled.img` - width: 32px; - height: 32px; - object-fit: contain; - margin: 0 8px 0 0; -` -const StyledImageName = styled.div` - display: flex; - align-items: center; -` -interface ReviewSpendingLimitProps { - onBack: () => void - onClose: () => void - onSubmit: () => void - txToken: Token | null - values: Record - existentSpendingLimit?: Record -} -const ReviewSpendingLimit = ({ - onBack, - onClose, - onSubmit, - txToken, - values, - existentSpendingLimit, -}: ReviewSpendingLimitProps): React.ReactElement => { - const classes = useStyles() - const addressBook = useSelector(getAddressBook) - - return ( - <> - - - New Spending Limit{' '} - <Text size="lg" color="secondaryLight"> - 2 of 2 - </Text> - - - - - - - - - - - Beneficiary - - - - - - Amount - - {txToken !== null ? ( - <> - - - - {values.amount} {txToken.symbol} - - - {existentSpendingLimit && ( - - Previous Amount: {existentSpendingLimit.amount} - - )} - - ) : ( - - )} - - - - Reset Time - - {values.withResetTime ? ( - - value === values.resetTime).label} - textSize="lg" - /> - - ) : ( - - - {/* TODO: review message */} - One-time spending limit allowance - - - )} - {existentSpendingLimit && ( - - - Previous Reset Time:{' '} - {RESET_TIME_OPTIONS.find( - ({ value }) => value === (+existentSpendingLimit.resetTimeMin / 60 / 24).toString(), - )?.label ?? 'One-time spending limit allowance'} - - - )} - - - {existentSpendingLimit && ( - - You are about to replace an existent spending limit - - )} - - - - - - - - - - - ) -} -// TODO: propose refactor in safe-react-components based on this requirements -const formMutators: Record> = { - setBeneficiary: (args, state, utils) => { - utils.changeValue(state, 'beneficiary', () => args[0]) - }, -} const requestModuleData = (safeAddress: string): Promise => { const batch = new web3ReadOnly.BatchRequest() @@ -691,7 +152,6 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole const classes = useStyles() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const modules = useSelector(safeModulesSelector) const { enqueueSnackbar, closeSnackbar } = useSnackbar() const dispatch = useDispatch() @@ -735,7 +195,6 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole } const handleSubmit = async (values: Record) => { - // TODO: here we do the magic. FINALLY!!!! const [enabledModules, delegates] = await requestModuleData(safeAddress) const isSpendingLimitEnabled = enabledModules?.array?.some((module) => module.toLowerCase() === SPENDING_LIMIT_MODULE_ADDRESS.toLowerCase()) ?? @@ -786,8 +245,6 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole .encodeABI(), }) - console.log({ modules, enabledModules, delegates, values }) - await sendTransactions( dispatch, safeAddress, @@ -821,46 +278,6 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole ) } -/** - * New Spending Limit Form - END - */ - -const SpendingLimitSteps = (): React.ReactElement => ( - - - Select Beneficiary - - Select Beneficiary - - - Choose an account that will benefit from this allowance. - - - The beneficiary does not have to be an owner of this Safe - - - - - Select asset and amount - - Select asset and amount - - - You can set a spending limit for any asset stored in your Safe - - - - - Select time - - Select time - - - You can choose to set a one-time spending limit or to have it automatically refill after a defined time-period - - - -) const SpendingLimit = (): React.ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) diff --git a/src/routes/safe/components/Settings/SpendingLimit/style.ts b/src/routes/safe/components/Settings/SpendingLimit/style.ts index b01a6cf360..c098eb109f 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/style.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/style.ts @@ -1,4 +1,4 @@ -import { createStyles } from '@material-ui/core' +import { createStyles, makeStyles } from '@material-ui/core' import { background, boldFont, @@ -13,119 +13,121 @@ import { xl, } from 'src/theme/variables' -export const styles = createStyles({ - title: { - padding: lg, - paddingBottom: 0, - }, - hide: { - '&:hover': { - backgroundColor: '#fff3e2', - }, - '&:hover $actions': { - visibility: 'initial', - }, - }, - actions: { - justifyContent: 'flex-end', - visibility: 'hidden', - minWidth: '100px', - }, - noBorderBottom: { - '& > td': { - borderBottom: 'none', - }, - }, - annotation: { - paddingLeft: lg, - }, - ownersText: { - color: secondaryText, - '& b': { - color: fontColor, - }, - }, - container: { - padding: lg, - }, - actionButton: { - fontWeight: boldFont, - marginRight: sm, - }, - buttonRow: { - padding: lg, - position: 'absolute', - left: 0, - bottom: 0, - boxSizing: 'border-box', - width: '100%', - justifyContent: 'flex-end', - borderTop: `2px solid ${border}`, - }, - modifyBtn: { - height: xl, - fontSize: smallFontSize, - }, - removeModuleIcon: { - marginLeft: lg, - cursor: 'pointer', - }, - modalHeading: { - boxSizing: 'border-box', - justifyContent: 'space-between', - maxHeight: '75px', - padding: `${sm} ${lg}`, - }, - modalContainer: { - minHeight: '369px', - }, - modalManage: { - fontSize: lg, - }, - modalClose: { - height: '35px', - width: '35px', - }, - modalButtonRow: { - height: '84px', - justifyContent: 'center', - }, - modalButtonRemove: { - color: '#fff', - backgroundColor: error, - height: '42px', - }, - modalName: { - textOverflow: 'ellipsis', - overflow: 'hidden', - }, - modalUserName: { - whiteSpace: 'nowrap', - }, - modalOwner: { - backgroundColor: background, - padding: md, - alignItems: 'center', - }, - modalUser: { - justifyContent: 'left', - }, - modalDescription: { - padding: md, - }, - modalOpen: { - paddingLeft: sm, - width: 'auto', - '&:hover': { +export const useStyles = makeStyles( + createStyles({ + title: { + padding: lg, + paddingBottom: 0, + }, + hide: { + '&:hover': { + backgroundColor: '#fff3e2', + }, + '&:hover $actions': { + visibility: 'initial', + }, + }, + actions: { + justifyContent: 'flex-end', + visibility: 'hidden', + minWidth: '100px', + }, + noBorderBottom: { + '& > td': { + borderBottom: 'none', + }, + }, + annotation: { + paddingLeft: lg, + }, + ownersText: { + color: secondaryText, + '& b': { + color: fontColor, + }, + }, + container: { + padding: lg, + }, + actionButton: { + fontWeight: boldFont, + marginRight: sm, + }, + buttonRow: { + padding: lg, + position: 'absolute', + left: 0, + bottom: 0, + boxSizing: 'border-box', + width: '100%', + justifyContent: 'flex-end', + borderTop: `2px solid ${border}`, + }, + modifyBtn: { + height: xl, + fontSize: smallFontSize, + }, + removeModuleIcon: { + marginLeft: lg, cursor: 'pointer', }, - }, - modal: { - height: 'auto', - maxWidth: 'calc(100% - 30px)', - overflow: 'hidden', - }, - amountInput: { - width: '100% !important', - }, -}) + modalHeading: { + boxSizing: 'border-box', + justifyContent: 'space-between', + maxHeight: '75px', + padding: `${sm} ${lg}`, + }, + modalContainer: { + minHeight: '369px', + }, + modalManage: { + fontSize: lg, + }, + modalClose: { + height: '35px', + width: '35px', + }, + modalButtonRow: { + height: '84px', + justifyContent: 'center', + }, + modalButtonRemove: { + color: '#fff', + backgroundColor: error, + height: '42px', + }, + modalName: { + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + modalUserName: { + whiteSpace: 'nowrap', + }, + modalOwner: { + backgroundColor: background, + padding: md, + alignItems: 'center', + }, + modalUser: { + justifyContent: 'left', + }, + modalDescription: { + padding: md, + }, + modalOpen: { + paddingLeft: sm, + width: 'auto', + '&:hover': { + cursor: 'pointer', + }, + }, + modal: { + height: 'auto', + maxWidth: 'calc(100% - 30px)', + overflow: 'hidden', + }, + amountInput: { + width: '100% !important', + }, + }), +) From 9be7cd739c486305007954074ca721e863434216 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 18 Aug 2020 18:21:35 -0300 Subject: [PATCH 20/69] fix reference after merge --- src/routes/safe/components/Settings/SpendingLimit/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 8c86167051..c7311a2938 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -12,14 +12,14 @@ import Row from 'src/components/layout/Row' import GnoModal from 'src/components/Modal' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3' -import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' +import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' import NewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit' import ReviewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit' import SpendingLimitSteps from 'src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps' import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' -import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' From b4656700c0c399f4f424ecf49488994e80fcf7f5 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 18 Aug 2020 18:27:43 -0300 Subject: [PATCH 21/69] add `REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS` as .env variable --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index cb172be891..d755ccf010 100644 --- a/.env.example +++ b/.env.example @@ -30,3 +30,6 @@ REACT_APP_APP_VERSION=$npm_package_version # For Apps REACT_APP_GNOSIS_APPS_URL=https://safe-apps.staging.gnosisdev.com + +# Contracts Addresses +REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS= From 91826212cbbed94faf300f5e1fd24f846ab2bd3f Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 18 Aug 2020 18:52:08 -0300 Subject: [PATCH 22/69] set return type for array map --- src/routes/safe/components/Settings/SpendingLimit/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index c7311a2938..172163eeea 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -115,7 +115,7 @@ const requestAllowancesByDelegatesAndTokens = async ( let whenRequestValues = [] - tokensByDelegate.map(([delegate, tokens]) => { + tokensByDelegate.map(([delegate, tokens]): void => { whenRequestValues = tokens.map((token) => generateBatchRequests({ abi: SpendingLimitModule.abi, From e0a62aec0895ef6b029bdb7dacb1e8879326a009 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 18 Aug 2020 19:03:18 -0300 Subject: [PATCH 23/69] add return value --- src/routes/safe/components/Settings/SpendingLimit/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 172163eeea..6c4d7a3328 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -125,6 +125,7 @@ const requestAllowancesByDelegatesAndTokens = async ( context: { delegate, token }, }), ) + return }) batch.execute() From 1000ffa0870bdaaf1d7a661c1e4bed3e6ef97f4f Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 18 Aug 2020 19:14:27 -0300 Subject: [PATCH 24/69] replace map with for..of loop --- src/routes/safe/components/Settings/SpendingLimit/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 6c4d7a3328..e0e354468c 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -115,7 +115,7 @@ const requestAllowancesByDelegatesAndTokens = async ( let whenRequestValues = [] - tokensByDelegate.map(([delegate, tokens]): void => { + for (const [delegate, tokens] of tokensByDelegate) { whenRequestValues = tokens.map((token) => generateBatchRequests({ abi: SpendingLimitModule.abi, @@ -125,8 +125,7 @@ const requestAllowancesByDelegatesAndTokens = async ( context: { delegate, token }, }), ) - return - }) + } batch.execute() From b81938af618ce9ce6fe6e2c01663a235a07e9c58 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 19 Aug 2020 11:07:48 -0300 Subject: [PATCH 25/69] move MODULE_ADDRESS constant to .env.example --- .env.example | 3 ++- src/utils/constants.ts | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index d755ccf010..23a68730e6 100644 --- a/.env.example +++ b/.env.example @@ -32,4 +32,5 @@ REACT_APP_APP_VERSION=$npm_package_version REACT_APP_GNOSIS_APPS_URL=https://safe-apps.staging.gnosisdev.com # Contracts Addresses -REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS= +REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS=0x9e9Bf12b5a66c0f0A7435835e0365477E121B110 + diff --git a/src/utils/constants.ts b/src/utils/constants.ts index cb84da136b..e640cfb597 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -14,5 +14,4 @@ export const OPENSEA_API_KEY = process.env.REACT_APP_OPENSEA_API_KEY || '' export const COLLECTIBLES_SOURCE = process.env.REACT_APP_COLLECTIBLES_SOURCE || 'OpenSea' export const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000 export const ETHERSCAN_API_KEY = process.env.REACT_APP_ETHERSCAN_API_KEY -export const SPENDING_LIMIT_MODULE_ADDRESS = - process.env.REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS || '0x9e9Bf12b5a66c0f0A7435835e0365477E121B110' +export const SPENDING_LIMIT_MODULE_ADDRESS = process.env.REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS From f2d333a3d32b645f237559a9a1405e2fe6c1ca62 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 19 Aug 2020 11:56:37 -0300 Subject: [PATCH 26/69] replace native button with src Button component --- .../ScanQRModal/ScanQRWrapper/index.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/ScanQRModal/ScanQRWrapper/index.tsx b/src/components/ScanQRModal/ScanQRWrapper/index.tsx index 1a4cbca479..63b8572fbf 100644 --- a/src/components/ScanQRModal/ScanQRWrapper/index.tsx +++ b/src/components/ScanQRModal/ScanQRWrapper/index.tsx @@ -1,4 +1,4 @@ -import { Icon } from '@gnosis.pm/safe-react-components' +import { Button, Icon } from '@gnosis.pm/safe-react-components' import { makeStyles } from '@material-ui/core/styles' import React from 'react' @@ -8,7 +8,8 @@ const useStyles = makeStyles({ qrCodeBtn: { cursor: 'pointer', border: 'none', - backgroundColor: 'transparent', + padding: '0 !important', + minWidth: '40px', }, }) @@ -34,9 +35,16 @@ export const ScanQRWrapper = ({ handleScan }: ScanQRWrapperProps): React.ReactEl return ( <> - + {qrModalOpen && } ) From c06f9035d4a44ac3af557fbf4bb9f4e413362bf4 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 19 Aug 2020 12:00:59 -0300 Subject: [PATCH 27/69] remove unneeded button styles --- src/components/ScanQRModal/ScanQRWrapper/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/ScanQRModal/ScanQRWrapper/index.tsx b/src/components/ScanQRModal/ScanQRWrapper/index.tsx index 63b8572fbf..e03654e410 100644 --- a/src/components/ScanQRModal/ScanQRWrapper/index.tsx +++ b/src/components/ScanQRModal/ScanQRWrapper/index.tsx @@ -6,8 +6,6 @@ import ScanQRModal from 'src/components/ScanQRModal' const useStyles = makeStyles({ qrCodeBtn: { - cursor: 'pointer', - border: 'none', padding: '0 !important', minWidth: '40px', }, From 54b45ffcaf28c709c7f23e1035abab35d19fbc81 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 19 Aug 2020 17:02:08 -0300 Subject: [PATCH 28/69] rename `TextField` to `SRCTextField`, and `GnoTextField` to `TextField` --- .../safe/components/Settings/SpendingLimit/Amount.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/Amount.tsx b/src/routes/safe/components/Settings/SpendingLimit/Amount.tsx index 54f95475cd..9927cab2eb 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/Amount.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/Amount.tsx @@ -1,4 +1,4 @@ -import { TextField } from '@gnosis.pm/safe-react-components' +import { TextField as SRCTextField } from '@gnosis.pm/safe-react-components' import React from 'react' import { useField } from 'react-final-form' import { useSelector } from 'react-redux' @@ -18,7 +18,7 @@ const AmountInput = styled.div` grid-area: amountInput; ` -const GnoTextField = styled(TextField)` +const TextField = styled(SRCTextField)` margin: 0; ` @@ -41,7 +41,7 @@ export const Amount = (): React.ReactElement => { return ( Date: Wed, 19 Aug 2020 17:09:13 -0300 Subject: [PATCH 29/69] extract inline event handlers --- .../SpendingLimit/BeneficiarySelect.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx b/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx index bb7fdae8c9..ba66c6b258 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx @@ -65,19 +65,23 @@ const BeneficiarySelect = (): React.ReactElement => { closeQrModal() } + const handleOnKeyDown = (e: React.KeyboardEvent): void => { + if (![KEYCODES.TAB, KEYCODES.SHIFT].includes(e.keyCode)) { + setSelectedEntry(null) + } + } + + const handleOnClick = () => { + setSelectedEntry(null) + } + return selectedEntry !== null && selectedEntry.address ? ( { - if (![KEYCODES.TAB, KEYCODES.SHIFT].includes(e.keyCode)) { - setSelectedEntry(null) - } - }} - onClick={() => { - setSelectedEntry(null) - }} + onKeyDown={handleOnKeyDown} + onClick={handleOnClick} > Date: Wed, 19 Aug 2020 17:13:15 -0300 Subject: [PATCH 30/69] extract utility functions --- .../SpendingLimit/BeneficiarySelect.tsx | 6 +- .../Settings/SpendingLimit/index.tsx | 102 +++--------------- .../Settings/SpendingLimit/utils.ts | 98 +++++++++++++++++ 3 files changed, 111 insertions(+), 95 deletions(-) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/utils.ts diff --git a/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx b/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx index ba66c6b258..b814f366bd 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx @@ -9,11 +9,7 @@ import { getNetwork } from 'src/config' import { getAddressBook } from 'src/logic/addressBook/store/selectors' import { getNameFromAdbk } from 'src/logic/addressBook/utils' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' - -const KEYCODES = { - TAB: 9, - SHIFT: 16, -} +import { KEYCODES } from 'src/routes/safe/components/Settings/SpendingLimit/utils' const BeneficiaryInput = styled.div` grid-area: beneficiaryInput; diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index e0e354468c..d0b3144f91 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -1,27 +1,32 @@ -import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import { Button, Text, Title } from '@gnosis.pm/safe-react-components' -import { BigNumber } from 'bignumber.js' import { useSnackbar } from 'notistack' import React from 'react' import { useDispatch, useSelector } from 'react-redux' -import styled from 'styled-components' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' import GnoModal from 'src/components/Modal' -import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' -import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' +import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3' +import { getWeb3 } from 'src/logic/wallets/getWeb3' import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' import NewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit' import ReviewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit' import SpendingLimitSteps from 'src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps' +import { + currentMinutes, + fromTokenUnit, + requestAllowancesByDelegatesAndTokens, + requestModuleData, + requestTokensByDelegate, + toTokenUnit, +} from 'src/routes/safe/components/Settings/SpendingLimit/utils' import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' +import styled from 'styled-components' import { useStyles } from './style' @@ -64,90 +69,6 @@ export const FooterWrapper = styled.div` justify-content: space-around; ` -const requestModuleData = (safeAddress: string): Promise => { - const batch = new web3ReadOnly.BatchRequest() - - const requests = [ - { - abi: GnosisSafeSol.abi, - address: safeAddress, - methods: [{ method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] }], - batch, - }, - { - abi: SpendingLimitModule.abi, - address: SPENDING_LIMIT_MODULE_ADDRESS, - methods: [{ method: 'getDelegates', args: [safeAddress, 0, 100] }], - batch, - }, - ] - - const whenRequestsValues = requests.map(generateBatchRequests) - - batch.execute() - - return Promise.all(whenRequestsValues).then(([modules, delegates]) => [modules[0], delegates[0]]) -} - -const requestTokensByDelegate = async (safeAddress: string, delegates: string[]): Promise => { - const batch = new web3ReadOnly.BatchRequest() - - const whenRequestValues = delegates.map((delegateAddress: string) => - generateBatchRequests({ - abi: SpendingLimitModule.abi, - address: SPENDING_LIMIT_MODULE_ADDRESS, - methods: [{ method: 'getTokens', args: [safeAddress, delegateAddress] }], - batch, - context: delegateAddress, - }), - ) - - batch.execute() - - return Promise.all(whenRequestValues) -} - -const requestAllowancesByDelegatesAndTokens = async ( - safeAddress: string, - tokensByDelegate: [string, string[]][], -): Promise => { - const batch = new web3ReadOnly.BatchRequest() - - let whenRequestValues = [] - - for (const [delegate, tokens] of tokensByDelegate) { - whenRequestValues = tokens.map((token) => - generateBatchRequests({ - abi: SpendingLimitModule.abi, - address: SPENDING_LIMIT_MODULE_ADDRESS, - methods: [{ method: 'getTokenAllowance', args: [safeAddress, delegate, token] }], - batch, - context: { delegate, token }, - }), - ) - } - - batch.execute() - - return Promise.all(whenRequestValues).then((allowances) => - allowances.map(([{ delegate, token }, [amount, spent, resetTimeMin, lastResetMin, nonce]]) => ({ - delegate, - token, - amount, - spent, - resetTimeMin, - lastResetMin, - nonce, - })), - ) -} - -const fromTokenUnit = (amount: string, decimals: string | number): string => - new BigNumber(amount).times(`1e-${decimals}`).toFixed() -const toTokenUnit = (amount: string, decimals: string | number): string => - new BigNumber(amount).times(`1e${decimals}`).toFixed() -const currentMinutes = () => Math.floor(Date.now() / (1000 * 60)) - const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boolean }): React.ReactElement => { const classes = useStyles() @@ -278,6 +199,7 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole ) } + const SpendingLimit = (): React.ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) diff --git a/src/routes/safe/components/Settings/SpendingLimit/utils.ts b/src/routes/safe/components/Settings/SpendingLimit/utils.ts new file mode 100644 index 0000000000..b5990dc9a4 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/utils.ts @@ -0,0 +1,98 @@ +import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' +import { BigNumber } from 'bignumber.js' +import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' +import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' +import { web3ReadOnly } from 'src/logic/wallets/getWeb3' +import SpendingLimitModule from 'src/utils/AllowanceModule.json' +import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' + +export const KEYCODES = { + TAB: 9, + SHIFT: 16, +} + +export const fromTokenUnit = (amount: string, decimals: string | number): string => + new BigNumber(amount).times(`1e-${decimals}`).toFixed() + +export const toTokenUnit = (amount: string, decimals: string | number): string => + new BigNumber(amount).times(`1e${decimals}`).toFixed() + +export const currentMinutes = () => Math.floor(Date.now() / (1000 * 60)) + +export const requestModuleData = (safeAddress: string): Promise => { + const batch = new web3ReadOnly.BatchRequest() + + const requests = [ + { + abi: GnosisSafeSol.abi, + address: safeAddress, + methods: [{ method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] }], + batch, + }, + { + abi: SpendingLimitModule.abi, + address: SPENDING_LIMIT_MODULE_ADDRESS, + methods: [{ method: 'getDelegates', args: [safeAddress, 0, 100] }], + batch, + }, + ] + + const whenRequestsValues = requests.map(generateBatchRequests) + + batch.execute() + + return Promise.all(whenRequestsValues).then(([modules, delegates]) => [modules[0], delegates[0]]) +} + +export const requestTokensByDelegate = async (safeAddress: string, delegates: string[]): Promise => { + const batch = new web3ReadOnly.BatchRequest() + + const whenRequestValues = delegates.map((delegateAddress: string) => + generateBatchRequests({ + abi: SpendingLimitModule.abi, + address: SPENDING_LIMIT_MODULE_ADDRESS, + methods: [{ method: 'getTokens', args: [safeAddress, delegateAddress] }], + batch, + context: delegateAddress, + }), + ) + + batch.execute() + + return Promise.all(whenRequestValues) +} + +export const requestAllowancesByDelegatesAndTokens = async ( + safeAddress: string, + tokensByDelegate: [string, string[]][], +): Promise => { + const batch = new web3ReadOnly.BatchRequest() + + let whenRequestValues = [] + + for (const [delegate, tokens] of tokensByDelegate) { + whenRequestValues = tokens.map((token) => + generateBatchRequests({ + abi: SpendingLimitModule.abi, + address: SPENDING_LIMIT_MODULE_ADDRESS, + methods: [{ method: 'getTokenAllowance', args: [safeAddress, delegate, token] }], + batch, + context: { delegate, token }, + }), + ) + } + + batch.execute() + + return Promise.all(whenRequestValues).then((allowances) => + allowances.map(([{ delegate, token }, [amount, spent, resetTimeMin, lastResetMin, nonce]]) => ({ + delegate, + token, + amount, + spent, + resetTimeMin, + lastResetMin, + nonce, + })), + ) +} From 9781dee5a22c0945c7723300b7874e230f2a1d36 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 19 Aug 2020 17:23:43 -0300 Subject: [PATCH 31/69] add TODO for `YetAnotherButton` --- .../safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx index c13074d1ed..71ed700fde 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx @@ -39,7 +39,6 @@ const YetAnotherButton = styled(GnoButton)` } ` -// TODO: propose refactor in safe-react-components based on this requirements const formMutators: Record> = { setBeneficiary: (args, state, utils) => { utils.changeValue(state, 'beneficiary', () => args[0]) @@ -95,6 +94,7 @@ const NewSpendingLimit = ({ initialValues, onCancel, onReview }: NewSpendingLimi Cancel + {/* TODO: replace this with safe-react-components button. This is used as "submit" SRC Button does not triggers submission up until the 2nd click */} Date: Wed, 19 Aug 2020 17:28:00 -0300 Subject: [PATCH 32/69] add TODO for StyledSwitch --- src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx b/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx index ee9d4bc35c..208ee54c32 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx @@ -11,6 +11,8 @@ const SpendingLimitRadioButtons = styled(RadioButtons)` color: ${({ theme }) => theme.colors.primary}; } ` + +// TODO: add `name` and `value` to SRC Switch, as they're required for a better RFF integration const StyledSwitch = styled(({ ...rest }) => )` && { .MuiIconButton-label, From 30f3c2f6e030a53b046ecc5323d277f22bc77164 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 19 Aug 2020 16:32:34 -0300 Subject: [PATCH 33/69] WIP: listing Spending Limits --- .../SpendingLimit/ReviewSpendingLimit.tsx | 5 +- .../Settings/SpendingLimit/dataFetcher.ts | 73 ++++++++ .../Settings/SpendingLimit/index.tsx | 164 +++++++++++++++++- .../Settings/SpendingLimit/style.ts | 1 - 4 files changed, 233 insertions(+), 10 deletions(-) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts diff --git a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx index 2ffeb2bcb0..cfd49b619e 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx @@ -15,7 +15,8 @@ import { FooterWrapper, StyledButton, TitleSection, -} from 'src/routes/safe/components/Settings/SpendingLimit/index' + SpendingLimit, +} from 'src/routes/safe/components/Settings/SpendingLimit' import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/ResetTime' import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' import styled from 'styled-components' @@ -38,7 +39,7 @@ interface ReviewSpendingLimitProps { onSubmit: () => void txToken: Token | null values: Record - existentSpendingLimit?: Record + existentSpendingLimit?: SpendingLimit } const ReviewSpendingLimit = ({ diff --git a/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts b/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts new file mode 100644 index 0000000000..c1a0a36fdb --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts @@ -0,0 +1,73 @@ +import { formatRelative } from 'date-fns' +import { List } from 'immutable' +import { TableColumn } from 'src/components/Table/types.d' +import { SpendingLimit } from '.' + +export const SPENDING_LIMIT_TABLE_BENEFICIARY_ID = 'beneficiary' +export const SPENDING_LIMIT_TABLE_SPENT_ID = 'spent' +export const SPENDING_LIMIT_TABLE_RESET_TIME_ID = 'resetTime' +export const SPENDING_LIMIT_TABLE_ACTION_ID = 'action' + +export type SpendingLimitTable = { + [SPENDING_LIMIT_TABLE_BENEFICIARY_ID]: string + [SPENDING_LIMIT_TABLE_SPENT_ID]: Record + [SPENDING_LIMIT_TABLE_RESET_TIME_ID]: string +} + +const relativeTime = (baseTimeMin: string, resetTimeMin: string): string => { + const baseTimeSeconds = +baseTimeMin * 60 + const resetTimeSeconds = +resetTimeMin * 60 + const nextResetTimeMilliseconds = (baseTimeSeconds + resetTimeSeconds) * 1000 + return formatRelative(nextResetTimeMilliseconds, Date.now()) +} + +export const getSpendingLimitData = (spendingLimits: SpendingLimit[] | null): SpendingLimitTable[] | undefined => { + return spendingLimits?.map((spendingLimit) => ({ + [SPENDING_LIMIT_TABLE_BENEFICIARY_ID]: spendingLimit.delegate, + [SPENDING_LIMIT_TABLE_SPENT_ID]: { + spent: spendingLimit.spent, + amount: spendingLimit.amount, + tokenAddress: spendingLimit.token, + }, + [SPENDING_LIMIT_TABLE_RESET_TIME_ID]: relativeTime(spendingLimit.lastResetMin, spendingLimit.resetTimeMin), + })) +} + +export const generateColumns = (): List => { + const beneficiaryColumn: TableColumn = { + align: 'left', + custom: false, + disablePadding: false, + id: SPENDING_LIMIT_TABLE_BENEFICIARY_ID, + label: 'Beneficiary', + order: false, + } + + const spentColumn: TableColumn = { + align: 'left', + custom: false, + disablePadding: false, + id: SPENDING_LIMIT_TABLE_SPENT_ID, + label: 'Spent', + order: false, + } + + const resetColumn: TableColumn = { + align: 'left', + custom: false, + disablePadding: false, + id: SPENDING_LIMIT_TABLE_RESET_TIME_ID, + label: 'Reset Time', + order: false, + } + + const actionsColumn: TableColumn = { + custom: true, + disablePadding: false, + id: SPENDING_LIMIT_TABLE_ACTION_ID, + label: '', + order: false, + } + + return List([beneficiaryColumn, spentColumn, resetColumn, actionsColumn]) +} diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index d0b3144f91..cac847c814 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -1,7 +1,21 @@ import { Button, Text, Title } from '@gnosis.pm/safe-react-components' +import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' +import { Button, EthHashInfo, Text, Title } from '@gnosis.pm/safe-react-components' +import TableContainer from '@material-ui/core/TableContainer' +import { BigNumber } from 'bignumber.js' +import cn from 'classnames' import { useSnackbar } from 'notistack' import React from 'react' import { useDispatch, useSelector } from 'react-redux' +import { TableCell, TableRow } from 'src/components/layout/Table' +import Table from 'src/components/Table' +import { getNetwork } from 'src/config' +import { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { formatAmount } from 'src/logic/tokens/utils/formatAmount' +import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' +import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' +import styled from 'styled-components' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' @@ -26,9 +40,17 @@ import { import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' -import styled from 'styled-components' +import { + generateColumns, + getSpendingLimitData, + SPENDING_LIMIT_TABLE_BENEFICIARY_ID, + SPENDING_LIMIT_TABLE_RESET_TIME_ID, + SPENDING_LIMIT_TABLE_SPENT_ID, + SpendingLimitTable, +} from './dataFetcher' import { useStyles } from './style' +import SpendingLimitSteps from './SpendingLimitSteps' const InfoText = styled(Text)` margin-top: 16px; @@ -69,6 +91,16 @@ export const FooterWrapper = styled.div` justify-content: space-around; ` +export interface SpendingLimit { + delegate: string + token: string + amount: string + spent: string + resetTimeMin: string + lastResetMin: string + nonce: string +} + const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boolean }): React.ReactElement => { const classes = useStyles() @@ -80,7 +112,7 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole const [values, setValues] = React.useState() const tokens = useSelector(extendedSafeTokensSelector) const [txToken, setTxToken] = React.useState(null) - const [existentSpendingLimit, setExistentSpendingLimit] = React.useState() + const [existentSpendingLimit, setExistentSpendingLimit] = React.useState() const handleReview = async (values) => { const selectedToken = tokens.find((token) => token.address === values.token) @@ -200,21 +232,61 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole ) } +const StyledImage = styled.img` + width: 32px; + height: 32px; + object-fit: contain; + margin: 0 8px 0 0; +` + +const StyledImageName = styled.div` + display: flex; + align-items: center; +` + +const TableActionButton = styled(Button)` + background-color: transparent; + padding: 0; + + &:hover { + background-color: transparent; + } +` + const SpendingLimit = (): React.ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) + const tokens = useSelector(extendedSafeTokensSelector) + const addressBook = useSelector(getAddressBook) const [showNewSpendingLimitModal, setShowNewSpendingLimitModal] = React.useState(false) // TODO: Refactor `delegates` for better performance. This is just to verify allowance works const safeAddress = useSelector(safeParamAddressFromStateSelector) - const [delegates, setDelegates] = React.useState({ results: [], next: '' }) + const [spendingLimits, setSpendingLimits] = React.useState() + const [spendingLimitData, setSpendingLimitData] = React.useState() React.useEffect(() => { const doRequestData = async () => { const [, delegates] = await requestModuleData(safeAddress) - setDelegates(delegates) + const tokensByDelegate = await requestTokensByDelegate(safeAddress, delegates.results) + const allowances = await requestAllowancesByDelegatesAndTokens(safeAddress, tokensByDelegate) + setSpendingLimits(allowances) + setSpendingLimitData(getSpendingLimitData(allowances)) } doRequestData() - }, [safeAddress]) + }, [safeAddress, tokens]) + + const [cut, setCut] = React.useState(undefined) + const { width } = useWindowDimensions() + React.useEffect(() => { + if (width <= 1024) { + setCut(4) + } else { + setCut(8) + } + }, [width]) + + const columns = generateColumns() + const autoColumns = columns.filter(({ custom }) => !custom) const openNewSpendingLimitModal = () => { setShowNewSpendingLimitModal(true) @@ -224,6 +296,19 @@ const SpendingLimit = (): React.ReactElement => { setShowNewSpendingLimitModal(false) } + const humanReadableSpent = (spent: string, amount: string, tokenAddress: string): React.ReactElement => { + const token = tokens.find((token) => token.address === tokenAddress) + const formattedSpent = formatAmount(fromTokenUnit(spent, token.decimals)).toString() + const formattedAmount = formatAmount(fromTokenUnit(amount, token.decimals)).toString() + + return ( + + {width > 1024 && } + {`${formattedSpent} of ${formattedAmount} ${token.symbol}`} + + ) + } + return ( <> @@ -234,8 +319,73 @@ const SpendingLimit = (): React.ReactElement => { You can set rules for specific beneficiaries to access funds from this Safe without having to collect all signatures. - {delegates?.results?.length ? ( - delegates.results.map((delegate) =>
{delegate}
) + {spendingLimits?.length && spendingLimitData?.length ? ( + + + {(sortedData) => + sortedData.map((row, index) => ( + = 3 && index === sortedData.size - 1 && classes.noBorderBottom)} + data-testid="spending-limit-table-row" + key={index} + tabIndex={-1} + > + {autoColumns.map((column, index) => { + const columnId = column.id + const rowElement = row[columnId] + + return ( + + {columnId === SPENDING_LIMIT_TABLE_BENEFICIARY_ID && ( + + )} + + {columnId === SPENDING_LIMIT_TABLE_SPENT_ID && + humanReadableSpent(rowElement.spent, rowElement.amount, rowElement.tokenAddress)} + + {columnId === SPENDING_LIMIT_TABLE_RESET_TIME_ID && {rowElement}} + + ) + })} + + + {granted && ( + console.log({ row })} + data-testid="remove-action" + > + {null} + + )} + + + + )) + } +
+
) : ( )} diff --git a/src/routes/safe/components/Settings/SpendingLimit/style.ts b/src/routes/safe/components/Settings/SpendingLimit/style.ts index c098eb109f..35b2f9c20c 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/style.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/style.ts @@ -30,7 +30,6 @@ export const useStyles = makeStyles( actions: { justifyContent: 'flex-end', visibility: 'hidden', - minWidth: '100px', }, noBorderBottom: { '& > td': { From 8df6201a57a03a96c5ed2f19a3fa3c778d253a05 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 19 Aug 2020 17:53:36 -0300 Subject: [PATCH 34/69] cleanup imports --- .../Settings/SpendingLimit/Amount.tsx | 3 +- .../SpendingLimit/BeneficiarySelect.tsx | 3 +- .../SpendingLimit/NewSpendingLimit.tsx | 19 ++++----- .../Settings/SpendingLimit/ResetTime.tsx | 3 +- .../SpendingLimit/ReviewSpendingLimit.tsx | 19 ++++----- .../SpendingLimit/SpendingLimitSteps.tsx | 6 +-- .../Settings/SpendingLimit/dataFetcher.ts | 6 ++- .../Settings/SpendingLimit/index.tsx | 39 +++++++------------ .../Settings/SpendingLimit/utils.ts | 13 ++++++- 9 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/Amount.tsx b/src/routes/safe/components/Settings/SpendingLimit/Amount.tsx index 9927cab2eb..aeb1a407ff 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/Amount.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/Amount.tsx @@ -2,11 +2,12 @@ import { TextField as SRCTextField } from '@gnosis.pm/safe-react-components' import React from 'react' import { useField } from 'react-final-form' import { useSelector } from 'react-redux' +import styled from 'styled-components' import GnoField from 'src/components/forms/Field' import { composeValidators, minValue, mustBeFloat, required } from 'src/components/forms/validator' import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' -import styled from 'styled-components' + import { useStyles } from './style' export const Field = styled(GnoField)` diff --git a/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx b/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx index b814f366bd..a1346e0f5f 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx @@ -9,7 +9,8 @@ import { getNetwork } from 'src/config' import { getAddressBook } from 'src/logic/addressBook/store/selectors' import { getNameFromAdbk } from 'src/logic/addressBook/utils' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' -import { KEYCODES } from 'src/routes/safe/components/Settings/SpendingLimit/utils' + +import { KEYCODES } from './utils' const BeneficiaryInput = styled.div` grid-area: beneficiaryInput; diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx index 71ed700fde..b5276a05a3 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx @@ -1,19 +1,16 @@ import { Button, Icon, Text, Title } from '@gnosis.pm/safe-react-components' import { Mutator } from 'final-form' import React from 'react' +import styled from 'styled-components' + import GnoForm from 'src/components/forms/GnoForm' import GnoButton from 'src/components/layout/Button' -import { Amount } from 'src/routes/safe/components/Settings/SpendingLimit/Amount' -import BeneficiarySelect from 'src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect' -import { - TitleSection, - StyledButton, - FooterSection, - FooterWrapper, -} from 'src/routes/safe/components/Settings/SpendingLimit/index' -import ResetTime from 'src/routes/safe/components/Settings/SpendingLimit/ResetTime' -import TokenSelect from 'src/routes/safe/components/Settings/SpendingLimit/TokenSelect' -import styled from 'styled-components' + +import { Amount } from './Amount' +import BeneficiarySelect from './BeneficiarySelect' +import { TitleSection, StyledButton, FooterSection, FooterWrapper } from '.' +import ResetTime from './ResetTime' +import TokenSelect from './TokenSelect' const FormContainer = styled.div` padding: 24px; diff --git a/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx b/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx index 208ee54c32..d436044a8a 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx @@ -2,9 +2,10 @@ import { RadioButtons, Text } from '@gnosis.pm/safe-react-components' import { FormControlLabel, hexToRgb, Switch as SwitchMui } from '@material-ui/core' import React from 'react' import { useField } from 'react-final-form' -import { Field } from 'src/routes/safe/components/Settings/SpendingLimit/Amount' import styled from 'styled-components' +import { Field } from './Amount' + // TODO: propose refactor in safe-react-components based on this requirements const SpendingLimitRadioButtons = styled(RadioButtons)` & .MuiRadio-colorPrimary.Mui-checked { diff --git a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx index cfd49b619e..9fe9cbfca4 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx @@ -2,6 +2,8 @@ import { Button, EthHashInfo, Icon, IconText, Text, Title } from '@gnosis.pm/saf import { Skeleton } from '@material-ui/lab' import React from 'react' import { useSelector } from 'react-redux' +import styled from 'styled-components' + import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' @@ -10,16 +12,11 @@ import { getAddressBook } from 'src/logic/addressBook/store/selectors' import { getNameFromAdbk } from 'src/logic/addressBook/utils' import { Token } from 'src/logic/tokens/store/model/token' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' -import { - FooterSection, - FooterWrapper, - StyledButton, - TitleSection, - SpendingLimit, -} from 'src/routes/safe/components/Settings/SpendingLimit' -import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/ResetTime' -import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' -import styled from 'styled-components' + +import { FooterSection, FooterWrapper, StyledButton, TitleSection } from '.' +import { RESET_TIME_OPTIONS } from './ResetTime' +import { useStyles } from './style' +import { SpendingLimitRow } from './utils' const StyledImage = styled.img` width: 32px; @@ -39,7 +36,7 @@ interface ReviewSpendingLimitProps { onSubmit: () => void txToken: Token | null values: Record - existentSpendingLimit?: SpendingLimit + existentSpendingLimit?: SpendingLimitRow } const ReviewSpendingLimit = ({ diff --git a/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps.tsx b/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps.tsx index 45382385d1..e1372d43c8 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps.tsx @@ -3,9 +3,9 @@ import React from 'react' import styled from 'styled-components' import Img from 'src/components/layout/Img' -import AssetAmount from 'src/routes/safe/components/Settings/SpendingLimit/assets/asset-amount.svg' -import Beneficiary from 'src/routes/safe/components/Settings/SpendingLimit/assets/beneficiary.svg' -import Time from 'src/routes/safe/components/Settings/SpendingLimit/assets/time.svg' +import AssetAmount from './assets/asset-amount.svg' +import Beneficiary from './assets/beneficiary.svg' +import Time from './assets/time.svg' const StepWrapper = styled.div` display: flex; diff --git a/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts b/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts index c1a0a36fdb..df32a80df2 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts @@ -1,7 +1,9 @@ import { formatRelative } from 'date-fns' import { List } from 'immutable' + import { TableColumn } from 'src/components/Table/types.d' -import { SpendingLimit } from '.' + +import { SpendingLimitRow } from './utils' export const SPENDING_LIMIT_TABLE_BENEFICIARY_ID = 'beneficiary' export const SPENDING_LIMIT_TABLE_SPENT_ID = 'spent' @@ -21,7 +23,7 @@ const relativeTime = (baseTimeMin: string, resetTimeMin: string): string => { return formatRelative(nextResetTimeMilliseconds, Date.now()) } -export const getSpendingLimitData = (spendingLimits: SpendingLimit[] | null): SpendingLimitTable[] | undefined => { +export const getSpendingLimitData = (spendingLimits: SpendingLimitRow[] | null): SpendingLimitTable[] | undefined => { return spendingLimits?.map((spendingLimit) => ({ [SPENDING_LIMIT_TABLE_BENEFICIARY_ID]: spendingLimit.delegate, [SPENDING_LIMIT_TABLE_SPENT_ID]: { diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index cac847c814..bb00cb812c 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -1,12 +1,11 @@ -import { Button, Text, Title } from '@gnosis.pm/safe-react-components' -import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import { Button, EthHashInfo, Text, Title } from '@gnosis.pm/safe-react-components' import TableContainer from '@material-ui/core/TableContainer' -import { BigNumber } from 'bignumber.js' import cn from 'classnames' import { useSnackbar } from 'notistack' import React from 'react' import { useDispatch, useSelector } from 'react-redux' +import styled from 'styled-components' + import { TableCell, TableRow } from 'src/components/layout/Table' import Table from 'src/components/Table' import { getNetwork } from 'src/config' @@ -15,7 +14,6 @@ import { getNameFromAdbk } from 'src/logic/addressBook/utils' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' -import styled from 'styled-components' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' @@ -24,11 +22,14 @@ import GnoModal from 'src/components/Modal' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { getWeb3 } from 'src/logic/wallets/getWeb3' - import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' -import NewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit' -import ReviewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit' -import SpendingLimitSteps from 'src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps' +import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' +import SpendingLimitModule from 'src/utils/AllowanceModule.json' +import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' + +import NewSpendingLimit from './NewSpendingLimit' +import ReviewSpendingLimit from './ReviewSpendingLimit' +import SpendingLimitSteps from './SpendingLimitSteps' import { currentMinutes, fromTokenUnit, @@ -36,11 +37,8 @@ import { requestModuleData, requestTokensByDelegate, toTokenUnit, -} from 'src/routes/safe/components/Settings/SpendingLimit/utils' -import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' -import SpendingLimitModule from 'src/utils/AllowanceModule.json' -import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' - + SpendingLimitRow, +} from './utils' import { generateColumns, getSpendingLimitData, @@ -50,7 +48,6 @@ import { SpendingLimitTable, } from './dataFetcher' import { useStyles } from './style' -import SpendingLimitSteps from './SpendingLimitSteps' const InfoText = styled(Text)` margin-top: 16px; @@ -91,16 +88,6 @@ export const FooterWrapper = styled.div` justify-content: space-around; ` -export interface SpendingLimit { - delegate: string - token: string - amount: string - spent: string - resetTimeMin: string - lastResetMin: string - nonce: string -} - const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boolean }): React.ReactElement => { const classes = useStyles() @@ -112,7 +99,7 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole const [values, setValues] = React.useState() const tokens = useSelector(extendedSafeTokensSelector) const [txToken, setTxToken] = React.useState(null) - const [existentSpendingLimit, setExistentSpendingLimit] = React.useState() + const [existentSpendingLimit, setExistentSpendingLimit] = React.useState() const handleReview = async (values) => { const selectedToken = tokens.find((token) => token.address === values.token) @@ -262,7 +249,7 @@ const SpendingLimit = (): React.ReactElement => { // TODO: Refactor `delegates` for better performance. This is just to verify allowance works const safeAddress = useSelector(safeParamAddressFromStateSelector) - const [spendingLimits, setSpendingLimits] = React.useState() + const [spendingLimits, setSpendingLimits] = React.useState() const [spendingLimitData, setSpendingLimitData] = React.useState() React.useEffect(() => { const doRequestData = async () => { diff --git a/src/routes/safe/components/Settings/SpendingLimit/utils.ts b/src/routes/safe/components/Settings/SpendingLimit/utils.ts index b5990dc9a4..4a0111d531 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/utils.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/utils.ts @@ -1,5 +1,6 @@ import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import { BigNumber } from 'bignumber.js' + import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' import { web3ReadOnly } from 'src/logic/wallets/getWeb3' @@ -62,10 +63,20 @@ export const requestTokensByDelegate = async (safeAddress: string, delegates: st return Promise.all(whenRequestValues) } +export type SpendingLimitRow = { + delegate: string + token: string + amount: string + spent: string + resetTimeMin: string + lastResetMin: string + nonce: string +} + export const requestAllowancesByDelegatesAndTokens = async ( safeAddress: string, tokensByDelegate: [string, string[]][], -): Promise => { +): Promise => { const batch = new web3ReadOnly.BatchRequest() let whenRequestValues = [] From 68d81819fec9321d534fe9751063698d6c5e3f5e Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 19 Aug 2020 20:55:15 -0300 Subject: [PATCH 35/69] fix delegates retrieval - used for..of for better readability --- .../Settings/SpendingLimit/utils.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/utils.ts b/src/routes/safe/components/Settings/SpendingLimit/utils.ts index b5990dc9a4..aaea6ec1b3 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/utils.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/utils.ts @@ -68,18 +68,20 @@ export const requestAllowancesByDelegatesAndTokens = async ( ): Promise => { const batch = new web3ReadOnly.BatchRequest() - let whenRequestValues = [] + const whenRequestValues = [] for (const [delegate, tokens] of tokensByDelegate) { - whenRequestValues = tokens.map((token) => - generateBatchRequests({ - abi: SpendingLimitModule.abi, - address: SPENDING_LIMIT_MODULE_ADDRESS, - methods: [{ method: 'getTokenAllowance', args: [safeAddress, delegate, token] }], - batch, - context: { delegate, token }, - }), - ) + for (const token of tokens) { + whenRequestValues.push( + generateBatchRequests({ + abi: SpendingLimitModule.abi, + address: SPENDING_LIMIT_MODULE_ADDRESS, + methods: [{ method: 'getTokenAllowance', args: [safeAddress, delegate, token] }], + batch, + context: { delegate, token }, + }), + ) + } } batch.execute() From d90b029cc7cbb490fb910dbb52f9081692330804 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 19 Aug 2020 21:00:02 -0300 Subject: [PATCH 36/69] fix logic for one-time allowance --- src/routes/safe/components/Settings/SpendingLimit/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index d0b3144f91..039b97d544 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -161,7 +161,7 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole values.token, toTokenUnit(values.amount, txToken.decimals), values.withResetTime ? +values.resetTime * 60 * 24 : 0, - startTime, + values.withResetTime ? startTime : 0, ) .encodeABI(), }) From 6661611658bdb3f4ce51dd6a32051d5c5cb47c26 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 19 Aug 2020 21:08:20 -0300 Subject: [PATCH 37/69] set message for one-time 'resetTime' --- .../safe/components/Settings/SpendingLimit/dataFetcher.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts b/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts index df32a80df2..25626a5f43 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts @@ -17,9 +17,14 @@ export type SpendingLimitTable = { } const relativeTime = (baseTimeMin: string, resetTimeMin: string): string => { + if (resetTimeMin === '0') { + return 'One-time' + } + const baseTimeSeconds = +baseTimeMin * 60 const resetTimeSeconds = +resetTimeMin * 60 const nextResetTimeMilliseconds = (baseTimeSeconds + resetTimeSeconds) * 1000 + return formatRelative(nextResetTimeMilliseconds, Date.now()) } From 72ab5f9d2b1e8b3a5d5ea453e8d178c3aa65512d Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 20 Aug 2020 10:31:02 -0300 Subject: [PATCH 38/69] support ETH Spending Limit --- src/routes/safe/components/Settings/SpendingLimit/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 039b97d544..f5e9b5c961 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -9,6 +9,8 @@ import Row from 'src/components/layout/Row' import GnoModal from 'src/components/Modal' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { getWeb3 } from 'src/logic/wallets/getWeb3' import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' @@ -158,7 +160,7 @@ const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boole data: spendingLimit.methods .setAllowance( values.beneficiary, - values.token, + values.token === ETH_ADDRESS ? ZERO_ADDRESS : values.token, toTokenUnit(values.amount, txToken.decimals), values.withResetTime ? +values.resetTime * 60 * 24 : 0, values.withResetTime ? startTime : 0, From c49f5f85764e9ff5f0adf39537fb3793eb716c2c Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 20 Aug 2020 11:00:51 -0300 Subject: [PATCH 39/69] display ETH allowance --- src/routes/safe/components/Settings/SpendingLimit/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 7d0a66c03c..f7e222f848 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -286,7 +286,8 @@ const SpendingLimit = (): React.ReactElement => { } const humanReadableSpent = (spent: string, amount: string, tokenAddress: string): React.ReactElement => { - const token = tokens.find((token) => token.address === tokenAddress) + const safeTokenAddress = tokenAddress === ZERO_ADDRESS ? ETH_ADDRESS : tokenAddress + const token = tokens.find((token) => token.address === safeTokenAddress) const formattedSpent = formatAmount(fromTokenUnit(spent, token.decimals)).toString() const formattedAmount = formatAmount(fromTokenUnit(amount, token.decimals)).toString() @@ -309,7 +310,7 @@ const SpendingLimit = (): React.ReactElement => { signatures. {spendingLimits?.length && spendingLimitData?.length ? ( - + Date: Thu, 20 Aug 2020 12:12:50 -0300 Subject: [PATCH 40/69] add back SPENDING_LIMIT_MODULE_ADDRESS default value in `constants` file this is due to the lack of global variable in the build --- src/utils/constants.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e640cfb597..cb84da136b 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -14,4 +14,5 @@ export const OPENSEA_API_KEY = process.env.REACT_APP_OPENSEA_API_KEY || '' export const COLLECTIBLES_SOURCE = process.env.REACT_APP_COLLECTIBLES_SOURCE || 'OpenSea' export const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000 export const ETHERSCAN_API_KEY = process.env.REACT_APP_ETHERSCAN_API_KEY -export const SPENDING_LIMIT_MODULE_ADDRESS = process.env.REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS +export const SPENDING_LIMIT_MODULE_ADDRESS = + process.env.REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS || '0x9e9Bf12b5a66c0f0A7435835e0365477E121B110' From 74761a524ffe5e459bfad8121801e847ad2129f6 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 20 Aug 2020 16:00:44 -0300 Subject: [PATCH 41/69] WIP add "delete" spending limit functionality --- .../RemoveSpendingLimitModal.tsx | 247 ++++++++++++++++++ .../Settings/SpendingLimit/dataFetcher.ts | 27 +- .../Settings/SpendingLimit/index.tsx | 80 ++++-- .../Settings/SpendingLimit/utils.ts | 30 ++- 4 files changed, 350 insertions(+), 34 deletions(-) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx new file mode 100644 index 0000000000..cd5a0c44f7 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx @@ -0,0 +1,247 @@ +import { Button, EthHashInfo, Icon, IconText, Text, Title } from '@gnosis.pm/safe-react-components' +import { Skeleton } from '@material-ui/lab' +import { useSnackbar } from 'notistack' +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { fromTokenUnit } from 'src/routes/safe/components/Settings/SpendingLimit/utils' +import styled from 'styled-components' + +import Block from 'src/components/layout/Block' +import Col from 'src/components/layout/Col' +import Row from 'src/components/layout/Row' +import Modal from 'src/components/Modal' +import { getNetwork } from 'src/config' +import { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import createTransaction from 'src/logic/safe/store/actions/createTransaction' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' +import { Token } from 'src/logic/tokens/store/model/token' +import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { getWeb3 } from 'src/logic/wallets/getWeb3' +import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' +import { + FooterSection, + FooterWrapper, + StyledButton, + TitleSection, +} from 'src/routes/safe/components/Settings/SpendingLimit' +import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/ResetTime' +import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' +import SpendingLimitModule from 'src/utils/AllowanceModule.json' +import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' +import { ResetTimeInfo as ResetTimeInfoType, SpendingLimitTable } from './dataFetcher' + +import { useStyles } from './style' + +const StyledImage = styled.img` + width: 32px; + height: 32px; + object-fit: contain; + margin: 0 8px 0 0; +` + +const StyledImageName = styled.div` + display: flex; + align-items: center; +` + +interface GenericInfoProps { + title?: string + children: React.ReactNode +} + +const GenericInfo = ({ title, children }: GenericInfoProps): React.ReactElement => ( + <> + {title && ( + + {title} + + )} + {children ?? } + +) + +interface AddressInfoProps { + title?: string + address: string +} + +const AddressInfo = ({ title, address }: AddressInfoProps): React.ReactElement => { + const addressBook = useSelector(getAddressBook) + + return ( + + {addressBook && ( + + )} + + ) +} + +interface TokenInfoProps { + title?: string + amount: string + address: string +} + +const TokenInfo = ({ title, amount, address }: TokenInfoProps): React.ReactElement => { + const tokens = useSelector(extendedSafeTokensSelector) + const [token, setToken] = React.useState() + + React.useEffect(() => { + if (tokens) { + const tokenAddress = address === ZERO_ADDRESS ? ETH_ADDRESS : address + const foundToken = tokens.find((token) => token.address === tokenAddress) + setToken(foundToken ?? null) + } + }, [address, tokens]) + + return ( + + {token && ( + + + + {fromTokenUnit(amount, token.decimals)} {token.symbol} + + + )} + {token === null && No token info} + + ) +} + +interface ResetTimeInfoProps { + title?: string + resetTime: ResetTimeInfoType +} + +const ResetTimeInfo = ({ title, resetTime }: ResetTimeInfoProps): React.ReactElement => { + return ( + + {resetTime.resetTimeMin !== '0' ? ( + + +value === +resetTime.resetTimeMin / 24 / 60).label} + textSize="lg" + /> + + ) : ( + + One-time spending limit allowance + + )} + + ) +} + +interface RemoveSpendingLimitModalProps { + onClose: () => void + spendingLimit: SpendingLimitTable + open: boolean +} + +const RemoveSpendingLimitModal = ({ + onClose, + spendingLimit, + open, +}: RemoveSpendingLimitModalProps): React.ReactElement => { + const classes = useStyles() + + const safeAddress = useSelector(safeParamAddressFromStateSelector) + + const { enqueueSnackbar, closeSnackbar } = useSnackbar() + const dispatch = useDispatch() + + const removeSelectedSpendingLimit = async (): Promise => { + try { + const web3 = getWeb3() + const spendingLimitContract = new web3.eth.Contract(SpendingLimitModule.abi as any, SPENDING_LIMIT_MODULE_ADDRESS) + const { + beneficiary, + spent: { tokenAddress }, + } = spendingLimit + + // TODO: replace with a proper way to remove allowances. + // as we don't have a current way to remove an allowance, we tweak it by setting its `amount` and `resetTimeMin` to 0 + // This is directly related to `discardZeroAllowance` + const txData = spendingLimitContract.methods + .setAllowance(beneficiary, tokenAddress === ETH_ADDRESS ? ZERO_ADDRESS : tokenAddress, 0, 0, 0) + .encodeABI() + + dispatch( + createTransaction({ + safeAddress, + to: SPENDING_LIMIT_MODULE_ADDRESS, + valueInWei: '0', + txData, + notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, + enqueueSnackbar, + closeSnackbar, + }), + ) + } catch (e) { + console.error( + `failed to remove spending limit ${spendingLimit.beneficiary} -> ${spendingLimit.spent.tokenAddress}`, + e.message, + ) + } + } + + return ( + + + + Remove Spending Limit + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default RemoveSpendingLimitModal diff --git a/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts b/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts index 25626a5f43..2564289a09 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts @@ -10,10 +10,22 @@ export const SPENDING_LIMIT_TABLE_SPENT_ID = 'spent' export const SPENDING_LIMIT_TABLE_RESET_TIME_ID = 'resetTime' export const SPENDING_LIMIT_TABLE_ACTION_ID = 'action' +export type SpentTableInfo = { + spent: string + amount: string + tokenAddress: string +} + +export type ResetTimeInfo = { + relativeTime: string + lastResetMin: string + resetTimeMin: string +} + export type SpendingLimitTable = { [SPENDING_LIMIT_TABLE_BENEFICIARY_ID]: string - [SPENDING_LIMIT_TABLE_SPENT_ID]: Record - [SPENDING_LIMIT_TABLE_RESET_TIME_ID]: string + [SPENDING_LIMIT_TABLE_SPENT_ID]: SpentTableInfo + [SPENDING_LIMIT_TABLE_RESET_TIME_ID]: ResetTimeInfo } const relativeTime = (baseTimeMin: string, resetTimeMin: string): string => { @@ -28,17 +40,20 @@ const relativeTime = (baseTimeMin: string, resetTimeMin: string): string => { return formatRelative(nextResetTimeMilliseconds, Date.now()) } -export const getSpendingLimitData = (spendingLimits: SpendingLimitRow[] | null): SpendingLimitTable[] | undefined => { - return spendingLimits?.map((spendingLimit) => ({ +export const getSpendingLimitData = (spendingLimits: SpendingLimitRow[] | null): SpendingLimitTable[] | undefined => + spendingLimits?.map((spendingLimit) => ({ [SPENDING_LIMIT_TABLE_BENEFICIARY_ID]: spendingLimit.delegate, [SPENDING_LIMIT_TABLE_SPENT_ID]: { spent: spendingLimit.spent, amount: spendingLimit.amount, tokenAddress: spendingLimit.token, }, - [SPENDING_LIMIT_TABLE_RESET_TIME_ID]: relativeTime(spendingLimit.lastResetMin, spendingLimit.resetTimeMin), + [SPENDING_LIMIT_TABLE_RESET_TIME_ID]: { + relativeTime: relativeTime(spendingLimit.lastResetMin, spendingLimit.resetTimeMin), + lastResetMin: spendingLimit.lastResetMin, + resetTimeMin: spendingLimit.resetTimeMin, + }, })) -} export const generateColumns = (): List => { const beneficiaryColumn: TableColumn = { diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index f7e222f848..dff3a6fbaf 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -14,6 +14,7 @@ import { getNameFromAdbk } from 'src/logic/addressBook/utils' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' +import { Token } from 'src/logic/tokens/store/model/token' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' @@ -31,6 +32,7 @@ import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import NewSpendingLimit from './NewSpendingLimit' import ReviewSpendingLimit from './ReviewSpendingLimit' +import RemoveSpendingLimitModal from './RemoveSpendingLimitModal' import SpendingLimitSteps from './SpendingLimitSteps' import { currentMinutes, @@ -242,12 +244,49 @@ const TableActionButton = styled(Button)` } ` +type SpentInfo = { + token: Token + spent: string + amount: string +} + +interface HumanReadableSpentProps { + spent: string + amount: string + tokenAddress: string +} + +const HumanReadableSpent = ({ spent, amount, tokenAddress }: HumanReadableSpentProps): React.ReactElement => { + const tokens = useSelector(extendedSafeTokensSelector) + const { width } = useWindowDimensions() + const [spentInfo, setSpentInfo] = React.useState() + + React.useEffect(() => { + if (tokens) { + const safeTokenAddress = tokenAddress === ZERO_ADDRESS ? ETH_ADDRESS : tokenAddress + const token = tokens.find((token) => token.address === safeTokenAddress) + const formattedSpent = formatAmount(fromTokenUnit(spent, token.decimals)).toString() + const formattedAmount = formatAmount(fromTokenUnit(amount, token.decimals)).toString() + + setSpentInfo({ token, spent: formattedSpent, amount: formattedAmount }) + } + }, [amount, spent, tokenAddress, tokens]) + + return spentInfo ? ( + + {width > 1024 && ( + + )} + {`${spentInfo.spent} of ${spentInfo.amount} ${spentInfo.token.symbol}`} + + ) : null +} + const SpendingLimit = (): React.ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) const tokens = useSelector(extendedSafeTokensSelector) const addressBook = useSelector(getAddressBook) - const [showNewSpendingLimitModal, setShowNewSpendingLimitModal] = React.useState(false) // TODO: Refactor `delegates` for better performance. This is just to verify allowance works const safeAddress = useSelector(safeParamAddressFromStateSelector) @@ -277,26 +316,26 @@ const SpendingLimit = (): React.ReactElement => { const columns = generateColumns() const autoColumns = columns.filter(({ custom }) => !custom) + const [showNewSpendingLimitModal, setShowNewSpendingLimitModal] = React.useState(false) const openNewSpendingLimitModal = () => { setShowNewSpendingLimitModal(true) } - const closeNewSpendingLimitModal = () => { setShowNewSpendingLimitModal(false) } - const humanReadableSpent = (spent: string, amount: string, tokenAddress: string): React.ReactElement => { - const safeTokenAddress = tokenAddress === ZERO_ADDRESS ? ETH_ADDRESS : tokenAddress - const token = tokens.find((token) => token.address === safeTokenAddress) - const formattedSpent = formatAmount(fromTokenUnit(spent, token.decimals)).toString() - const formattedAmount = formatAmount(fromTokenUnit(amount, token.decimals)).toString() - - return ( - - {width > 1024 && } - {`${formattedSpent} of ${formattedAmount} ${token.symbol}`} - - ) + const [showRemoveSpendingLimitModal, setShowRemoveSpendingLimitModal] = React.useState(false) + const [selectedRow, setSelectedRow] = React.useState(null) + const openRemoveSpendingLimitModal = (row: SpendingLimitTable) => { + setSelectedRow(row) + setShowRemoveSpendingLimitModal(true) + } + const closeRemoveSpendingLimitModal = () => { + setShowRemoveSpendingLimitModal(false) + setSelectedRow(null) + } + const handleDeleteSpendingLimit = (row: SpendingLimitTable): void => { + openRemoveSpendingLimitModal(row) } return ( @@ -348,10 +387,10 @@ const SpendingLimit = (): React.ReactElement => { /> )} - {columnId === SPENDING_LIMIT_TABLE_SPENT_ID && - humanReadableSpent(rowElement.spent, rowElement.amount, rowElement.tokenAddress)} - - {columnId === SPENDING_LIMIT_TABLE_RESET_TIME_ID && {rowElement}} + {columnId === SPENDING_LIMIT_TABLE_SPENT_ID && } + {columnId === SPENDING_LIMIT_TABLE_RESET_TIME_ID && ( + {rowElement.relativeTime} + )} ) })} @@ -363,7 +402,7 @@ const SpendingLimit = (): React.ReactElement => { iconType="delete" color="error" variant="outlined" - onClick={() => console.log({ row })} + onClick={() => handleDeleteSpendingLimit(row)} data-testid="remove-action" > {null} @@ -396,6 +435,9 @@ const SpendingLimit = (): React.ReactElement => { {showNewSpendingLimitModal && } + {showRemoveSpendingLimitModal && ( + + )} ) } diff --git a/src/routes/safe/components/Settings/SpendingLimit/utils.ts b/src/routes/safe/components/Settings/SpendingLimit/utils.ts index 02673ca48f..811a49b437 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/utils.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/utils.ts @@ -73,6 +73,16 @@ export type SpendingLimitRow = { nonce: string } +/** + * TODO: This is helpfully a temp function that attempts to filter out "deleted" allowances + * As there's no way to remove an Allowance with the current code, it's "deleted" by setting its `amount` to 0 + * along with `resetTimeMin` to 0 as well + * @param {SpendingLimitRow} allowance + * @returns boolean + */ +const discardZeroAllowance = ({ amount, resetTimeMin }: SpendingLimitRow): boolean => + !(amount === '0' && resetTimeMin === '0') + export const requestAllowancesByDelegatesAndTokens = async ( safeAddress: string, tokensByDelegate: [string, string[]][], @@ -98,14 +108,16 @@ export const requestAllowancesByDelegatesAndTokens = async ( batch.execute() return Promise.all(whenRequestValues).then((allowances) => - allowances.map(([{ delegate, token }, [amount, spent, resetTimeMin, lastResetMin, nonce]]) => ({ - delegate, - token, - amount, - spent, - resetTimeMin, - lastResetMin, - nonce, - })), + allowances + .map(([{ delegate, token }, [amount, spent, resetTimeMin, lastResetMin, nonce]]) => ({ + delegate, + token, + amount, + spent, + resetTimeMin, + lastResetMin, + nonce, + })) + .filter(discardZeroAllowance), ) } From 8be577d167790069382fbf65c488878665676ff2 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 20 Aug 2020 22:16:48 -0300 Subject: [PATCH 42/69] refactor: store SpendingLimit in the store - also, refactored components for better readability --- src/logic/contracts/safeContracts.ts | 8 + src/logic/safe/store/actions/fetchSafe.ts | 41 ++- src/logic/safe/store/models/safe.ts | 12 + src/logic/safe/store/selectors/index.ts | 2 + .../SpendingLimit/ReviewSpendingLimit.tsx | 262 +++++++++++++----- .../SpendingLimit/SpendingLimitModal.tsx | 86 ++++++ .../Settings/SpendingLimit/index.tsx | 199 ++----------- .../Settings/SpendingLimit/utils.ts | 3 +- src/routes/safe/components/Settings/index.tsx | 4 +- 9 files changed, 371 insertions(+), 246 deletions(-) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/SpendingLimitModal.tsx diff --git a/src/logic/contracts/safeContracts.ts b/src/logic/contracts/safeContracts.ts index d2992add06..b533212044 100644 --- a/src/logic/contracts/safeContracts.ts +++ b/src/logic/contracts/safeContracts.ts @@ -1,9 +1,11 @@ +import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import { AbiItem } from 'web3-utils' import contract from 'truffle-contract' import Web3 from 'web3' import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json' import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json' +import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { ensureOnce } from 'src/utils/singleton' import memoize from 'lodash.memoize' import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3' @@ -37,7 +39,13 @@ const createProxyFactoryContract = (web3: Web3, networkId: number): GnosisSafePr return proxyFactory } +const createSpendingLimitContract = () => { + const web3 = getWeb3() + return new web3.eth.Contract(SpendingLimitModule.abi as AbiItem[], SPENDING_LIMIT_MODULE_ADDRESS) +} + export const getGnosisSafeContract = memoize(createGnosisSafeContract) +export const getSpendingLimitContract = memoize(createSpendingLimitContract) const getCreateProxyFactoryContract = memoize(createProxyFactoryContract) const instantiateMasterCopies = async () => { diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index 2a7c82ef6f..41682910ee 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -11,11 +11,17 @@ import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner' import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner' import updateSafe from 'src/logic/safe/store/actions/updateSafe' import { makeOwner } from 'src/logic/safe/store/models/owner' +import { + requestAllowancesByDelegatesAndTokens, + requestTokensByDelegate, +} from 'src/routes/safe/components/Settings/SpendingLimit/utils' import { checksumAddress } from 'src/utils/checksumAddress' -import { ModulePair, SafeOwner } from 'src/logic/safe/store/models/safe' +import { ModulePair, SafeOwner, SpendingLimit } from 'src/logic/safe/store/models/safe' import { Dispatch } from 'redux' -import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' + +import { getSpendingLimitContract, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' +import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' const buildOwnersFrom = ( safeOwners, @@ -83,8 +89,22 @@ export const buildSafe = async (safeAdd: string, safeName: string, latestMasterC return safe } +const getSpendingLimits = async (modules, safeAddress: string): Promise => { + const isSpendingLimitEnabled = + modules?.array?.some((module) => module.toLowerCase() === SPENDING_LIMIT_MODULE_ADDRESS.toLowerCase()) ?? false + + if (isSpendingLimitEnabled) { + const delegates = await getSpendingLimitContract().methods.getDelegates(safeAddress, 0, 100).call() + const tokensByDelegate = await requestTokensByDelegate(safeAddress, delegates.results) + return requestAllowancesByDelegatesAndTokens(safeAddress, tokensByDelegate) + } + + return null +} + export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch): Promise => { const safeAddress = checksumAddress(safeAdd) + // Check if the owner's safe did change and update them const safeParams = [ 'getThreshold', @@ -93,15 +113,21 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch // TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled { method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] }, ] + + const safeInfo = generateBatchRequests({ + abi: GnosisSafeSol.abi, + address: safeAddress, + methods: safeParams, + } as any) + const [[remoteThreshold, remoteNonce, remoteOwners, modules], localSafe] = await Promise.all([ - generateBatchRequests({ - abi: GnosisSafeSol.abi, - address: safeAddress, - methods: safeParams, - } as any), + safeInfo, getLocalSafe(safeAddress), ]) + // request SpendingLimit info + const spendingLimits = await getSpendingLimits(modules, safeAddress) + // Converts from [ { address, ownerName} ] to address array const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : undefined @@ -109,6 +135,7 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch updateSafe({ address: safeAddress, modules: buildModulesLinkedList(modules?.array, modules?.next), + spendingLimits, nonce: Number(remoteNonce), threshold: Number(remoteThreshold), }), diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index 0311d3be2c..d8296329ba 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -7,6 +7,16 @@ export type SafeOwner = { export type ModulePair = [string, string] +export type SpendingLimit = { + delegate: string + token: string + amount: string + spent: string + resetTimeMin: string + lastResetMin: string + nonce: string +} + export type SafeRecordProps = { name: string address: string @@ -14,6 +24,7 @@ export type SafeRecordProps = { ethBalance: string owners: List modules: ModulePair[] | null + spendingLimits: SpendingLimit[] | null activeTokens: Set activeAssets: Set blacklistedTokens: Set @@ -34,6 +45,7 @@ const makeSafe = Record({ ethBalance: '0', owners: List([]), modules: [], + spendingLimits: [], activeTokens: Set(), activeAssets: Set(), blacklistedTokens: Set(), diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index 9c6d3f9c59..f133288d46 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -208,6 +208,8 @@ export const safeModulesSelector = createSelector(safeSelector, safeFieldSelecto export const safeFeaturesEnabledSelector = createSelector(safeSelector, safeFieldSelector('featuresEnabled')) +export const safeSpendingLimitsSelector = createSelector(safeSelector, safeFieldSelector('spendingLimits')) + export const getActiveTokensAddressesForAllSafes = createSelector(safesListSelector, (safes) => { const addresses = Set().withMutations((set) => { safes.forEach((safe) => { diff --git a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx index 2ffeb2bcb0..6454e5d1e2 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx @@ -1,14 +1,20 @@ import { Button, EthHashInfo, Icon, IconText, Text, Title } from '@gnosis.pm/safe-react-components' -import { Skeleton } from '@material-ui/lab' +import { useSnackbar } from 'notistack' import React from 'react' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' import { getNetwork } from 'src/config' import { getAddressBook } from 'src/logic/addressBook/store/selectors' import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { getGnosisSafeInstanceAt, getSpendingLimitContract } from 'src/logic/contracts/safeContracts' +import { SpendingLimit } from 'src/logic/safe/store/models/safe' +import { safeParamAddressFromStateSelector, safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors' import { Token } from 'src/logic/tokens/store/model/token' +import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import { FooterSection, @@ -18,6 +24,8 @@ import { } from 'src/routes/safe/components/Settings/SpendingLimit/index' import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/ResetTime' import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' +import { currentMinutes, fromTokenUnit, toTokenUnit } from 'src/routes/safe/components/Settings/SpendingLimit/utils' +import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import styled from 'styled-components' const StyledImage = styled.img` @@ -35,23 +43,187 @@ const StyledImageName = styled.div` interface ReviewSpendingLimitProps { onBack: () => void onClose: () => void - onSubmit: () => void txToken: Token | null values: Record existentSpendingLimit?: Record } -const ReviewSpendingLimit = ({ - onBack, - onClose, - onSubmit, - txToken, - values, - existentSpendingLimit, -}: ReviewSpendingLimitProps): React.ReactElement => { - const classes = useStyles() +interface GenericInfoProps { + title?: string + children: React.ReactNode +} +const GenericInfo = ({ title, children }: GenericInfoProps): React.ReactElement => ( + <> + {title && ( + + {title} + + )} + {children} + +) + +interface AddressInfoProps { + address: string + title?: string +} +const AddressInfo = ({ address, title }: AddressInfoProps): React.ReactElement => { const addressBook = useSelector(getAddressBook) + return ( + + + + ) +} + +interface TokenInfoProps { + amount: string + title?: string + token: Token +} +const TokenInfo = ({ amount, title, token }: TokenInfoProps): React.ReactElement => { + return ( + + + + + {amount} {token.symbol} + + + + ) +} + +interface ResetTimeInfoProps { + title?: string + label?: string +} +const ResetTimeInfo = ({ title, label }: ResetTimeInfoProps): React.ReactElement => { + return ( + + {label ? ( + + + + ) : ( + + + {/* TODO: review message */} + One-time spending limit allowance + + + )} + + ) +} + +const ReviewSpendingLimit = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): React.ReactElement => { + const classes = useStyles() + + const { enqueueSnackbar, closeSnackbar } = useSnackbar() + const dispatch = useDispatch() + + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const spendingLimits = useSelector(safeSpendingLimitsSelector) + + // undefined: before setting a value + // null: if no previous value + // SpendingLimit: if previous value exists + const [existentSpendingLimit, setExistentSpendingLimit] = React.useState() + React.useEffect(() => { + const checkExistence = async () => { + // if `delegate` already exist, check what tokens were delegated to the _beneficiary_ `getTokens(safe, delegate)` + const currentDelegate = spendingLimits.find( + ({ delegate, token }) => + delegate.toLowerCase() === values.beneficiary.toLowerCase() && + token.toLowerCase() === values.token.toLowerCase(), + ) + + // let the user know that is about to replace an existent allowance + if (currentDelegate !== undefined) { + setExistentSpendingLimit({ + ...currentDelegate, + amount: fromTokenUnit(currentDelegate.amount, txToken.decimals), + }) + } else { + setExistentSpendingLimit(null) + } + } + + checkExistence() + }, [spendingLimits, txToken.decimals, values.beneficiary, values.token]) + + const handleSubmit = async () => { + const spendingLimitContract = getSpendingLimitContract() + const isSpendingLimitEnabled = spendingLimits !== null + + const transactions = [] + + // is spendingLimit module enabled? -> if not, create the tx to enable it, and encode it + if (!isSpendingLimitEnabled) { + const safeInstance = await getGnosisSafeInstanceAt(safeAddress) + transactions.push({ + to: safeAddress, + value: 0, + data: safeInstance.methods.enableModule(SPENDING_LIMIT_MODULE_ADDRESS).encodeABI(), + }) + } + + // does `delegate` already exist? (`getDelegates`, previously queried to build the table with allowances (??)) + // ^ - shall we rely on this or query the list of delegates once again? + const isDelegateAlreadyAdded = + spendingLimits.some(({ delegate }) => delegate.toLowerCase() === values?.beneficiary.toLowerCase()) ?? false + + // if `delegate` does not exist, add it by calling `addDelegate(beneficiary)` + if (!isDelegateAlreadyAdded && values?.beneficiary) { + transactions.push({ + to: SPENDING_LIMIT_MODULE_ADDRESS, + value: 0, + data: spendingLimitContract.methods.addDelegate(values?.beneficiary).encodeABI(), + }) + } + + // prepare the setAllowance tx + const startTime = currentMinutes() - 30 + transactions.push({ + to: SPENDING_LIMIT_MODULE_ADDRESS, + value: 0, + data: spendingLimitContract.methods + .setAllowance( + values.beneficiary, + values.token === ETH_ADDRESS ? ZERO_ADDRESS : values.token, + toTokenUnit(values.amount, txToken.decimals), + values.withResetTime ? +values.resetTime * 60 * 24 : 0, + values.withResetTime ? startTime : 0, + ) + .encodeABI(), + }) + + await sendTransactions( + dispatch, + safeAddress, + transactions, + enqueueSnackbar, + closeSnackbar, + JSON.stringify({ name: 'Spending Limit', message: 'New Allowance' }), + ) + .then(onClose) + .catch(console.error) + } + + const resetTimeLabel = values.withResetTime + ? RESET_TIME_OPTIONS.find(({ value }) => value === values.resetTime).label + : '' + return ( <> @@ -69,62 +241,18 @@ const ReviewSpendingLimit = ({ - - Beneficiary - - + - - Amount - - {txToken !== null ? ( - <> - - - - {values.amount} {txToken.symbol} - - - {existentSpendingLimit && ( - - Previous Amount: {existentSpendingLimit.amount} - - )} - - ) : ( - + + {existentSpendingLimit && ( + + Previous Amount: {existentSpendingLimit.amount} + )} - - Reset Time - - {values.withResetTime ? ( - - value === values.resetTime).label} - textSize="lg" - /> - - ) : ( - - - {/* TODO: review message */} - One-time spending limit allowance - - - )} + {existentSpendingLimit && ( @@ -150,7 +278,13 @@ const ReviewSpendingLimit = ({ Back - diff --git a/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitModal.tsx new file mode 100644 index 0000000000..05a765ffb6 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitModal.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import GnoModal from 'src/components/Modal' +import { Token } from 'src/logic/tokens/store/model/token' +import NewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit' +import ReviewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit' +import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' +import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' + +const CREATE = 'CREATE' as const +const REVIEW = 'REVIEW' as const + +type Step = typeof CREATE | typeof REVIEW + +type SpendingLimitModalReducerState = { + step: Step + values: Record | null + txToken: Token | null +} + +const newSpendingLimitReducer = (state: SpendingLimitModalReducerState, action) => { + const { type, newState } = action + + switch (type) { + case CREATE: { + return { + ...state, + step: CREATE, + } + } + + case REVIEW: { + return { + ...newState, + step: REVIEW, + } + } + } +} + +const useSpendingLimit = (initialStep: Step) => { + const [state, dispatch] = React.useReducer(newSpendingLimitReducer, { + step: initialStep, + values: null, + txToken: null, + }) + + const create = React.useCallback(() => dispatch({ type: CREATE }), []) + const review = React.useCallback((newState) => dispatch({ type: REVIEW, newState }), []) + + return [state, { create, review }] +} + +interface SpendingLimitModalProps { + close: () => void + open: boolean +} + +const SpendingLimitModal = ({ close, open }: SpendingLimitModalProps): React.ReactElement => { + const classes = useStyles() + + const tokens = useSelector(extendedSafeTokensSelector) + const [{ step, txToken, values }, { create, review }] = useSpendingLimit(CREATE) + + const handleReview = async (values) => { + review({ + values, + txToken: tokens.find((token) => token.address === values.token), + }) + } + + return ( + + {step === CREATE && } + {step === REVIEW && } + + ) +} + +export default SpendingLimitModal diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index f5e9b5c961..fc2b07df91 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -1,33 +1,15 @@ import { Button, Text, Title } from '@gnosis.pm/safe-react-components' -import { useSnackbar } from 'notistack' import React from 'react' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' -import GnoModal from 'src/components/Modal' -import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' -import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { getWeb3 } from 'src/logic/wallets/getWeb3' - -import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' -import NewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit' -import ReviewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit' -import SpendingLimitSteps from 'src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps' -import { - currentMinutes, - fromTokenUnit, - requestAllowancesByDelegatesAndTokens, - requestModuleData, - requestTokensByDelegate, - toTokenUnit, -} from 'src/routes/safe/components/Settings/SpendingLimit/utils' -import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' -import SpendingLimitModule from 'src/utils/AllowanceModule.json' -import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' +import SpendingLimitModal from './SpendingLimitModal' +import SpendingLimitSteps from './SpendingLimitSteps' +import { requestModuleData } from './utils' +import { grantedSelector } from 'src/routes/safe/container/selector' import styled from 'styled-components' import { useStyles } from './style' @@ -71,138 +53,7 @@ export const FooterWrapper = styled.div` justify-content: space-around; ` -const NewSpendingLimitModal = ({ close, open }: { close: () => void; open: boolean }): React.ReactElement => { - const classes = useStyles() - - const safeAddress = useSelector(safeParamAddressFromStateSelector) - const { enqueueSnackbar, closeSnackbar } = useSnackbar() - const dispatch = useDispatch() - - const [step, setStep] = React.useState<'create' | 'review'>('create') - const [values, setValues] = React.useState() - const tokens = useSelector(extendedSafeTokensSelector) - const [txToken, setTxToken] = React.useState(null) - const [existentSpendingLimit, setExistentSpendingLimit] = React.useState() - - const handleReview = async (values) => { - const selectedToken = tokens.find((token) => token.address === values.token) - - setValues(values) - setTxToken(selectedToken) - - const checkExistence = async () => { - const [, delegates] = await requestModuleData(safeAddress) - const tokensByDelegate = await requestTokensByDelegate(safeAddress, delegates.results) - const allowances = await requestAllowancesByDelegatesAndTokens(safeAddress, tokensByDelegate) - - // if `delegate` already exist, check what tokens were delegated to the _beneficiary_ `getTokens(safe, delegate)` - const currentDelegate = allowances.find( - ({ delegate, token }) => - delegate.toLowerCase() === values.beneficiary.toLowerCase() && - token.toLowerCase() === values.token.toLowerCase(), - ) - - // let the user know that is about to replace an existent allowance - if (currentDelegate !== undefined) { - setExistentSpendingLimit({ - ...currentDelegate, - amount: fromTokenUnit(currentDelegate.amount, selectedToken.decimals), - }) - } else { - setExistentSpendingLimit(undefined) - } - } - - await checkExistence() - setStep('review') - } - - const handleSubmit = async (values: Record) => { - const [enabledModules, delegates] = await requestModuleData(safeAddress) - const isSpendingLimitEnabled = - enabledModules?.array?.some((module) => module.toLowerCase() === SPENDING_LIMIT_MODULE_ADDRESS.toLowerCase()) ?? - false - const transactions = [] - - // is spendingLimit module enabled? -> if not, create the tx to enable it, and encode it - if (!isSpendingLimitEnabled) { - const safeInstance = await getGnosisSafeInstanceAt(safeAddress) - transactions.push({ - to: safeAddress, - value: 0, - data: safeInstance.methods.enableModule(SPENDING_LIMIT_MODULE_ADDRESS).encodeABI(), - }) - } - - // does `delegate` already exist? (`getDelegates`, previously queried to build the table with allowances (??)) - // ^ - shall we rely on this or query the list of delegates once again? - const isDelegateAlreadyAdded = - delegates.results.some((delegate) => delegate.toLowerCase() === values?.beneficiary.toLowerCase()) ?? false - - // if `delegate` does not exist, add it by calling `addDelegate(beneficiary)` - if (!isDelegateAlreadyAdded) { - const web3 = getWeb3() - const spendingLimit = new web3.eth.Contract(SpendingLimitModule.abi as any, SPENDING_LIMIT_MODULE_ADDRESS) - transactions.push({ - to: SPENDING_LIMIT_MODULE_ADDRESS, - value: 0, - data: spendingLimit.methods.addDelegate(values?.beneficiary).encodeABI(), - }) - } - - // prepare the setAllowance tx - const web3 = getWeb3() - const spendingLimit = new web3.eth.Contract(SpendingLimitModule.abi as any, SPENDING_LIMIT_MODULE_ADDRESS) - const startTime = currentMinutes() - 30 - transactions.push({ - to: SPENDING_LIMIT_MODULE_ADDRESS, - value: 0, - data: spendingLimit.methods - .setAllowance( - values.beneficiary, - values.token === ETH_ADDRESS ? ZERO_ADDRESS : values.token, - toTokenUnit(values.amount, txToken.decimals), - values.withResetTime ? +values.resetTime * 60 * 24 : 0, - values.withResetTime ? startTime : 0, - ) - .encodeABI(), - }) - - await sendTransactions( - dispatch, - safeAddress, - transactions, - enqueueSnackbar, - closeSnackbar, - JSON.stringify({ name: 'Spending Limit', message: 'New Allowance' }), - ) - .then(close) - .catch(console.error) - } - return ( - - {step === 'create' && } - {step === 'review' && ( - setStep('create')} - onClose={close} - onSubmit={() => handleSubmit(values)} - txToken={txToken} - values={values} - existentSpendingLimit={existentSpendingLimit} - /> - )} - - ) -} - -const SpendingLimit = (): React.ReactElement => { +const SpendingLimitSettings = (): React.ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) const [showNewSpendingLimitModal, setShowNewSpendingLimitModal] = React.useState(false) @@ -242,24 +93,28 @@ const SpendingLimit = (): React.ReactElement => { )} - - - - - - {showNewSpendingLimitModal && } + + {granted && ( + <> + + + + + + {showNewSpendingLimitModal && } + + )} ) } -export default SpendingLimit +export default SpendingLimitSettings diff --git a/src/routes/safe/components/Settings/SpendingLimit/utils.ts b/src/routes/safe/components/Settings/SpendingLimit/utils.ts index aaea6ec1b3..bd83cacac7 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/utils.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/utils.ts @@ -2,6 +2,7 @@ import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe. import { BigNumber } from 'bignumber.js' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' +import { SpendingLimit } from 'src/logic/safe/store/models/safe' import { web3ReadOnly } from 'src/logic/wallets/getWeb3' import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' @@ -65,7 +66,7 @@ export const requestTokensByDelegate = async (safeAddress: string, delegates: st export const requestAllowancesByDelegatesAndTokens = async ( safeAddress: string, tokensByDelegate: [string, string[]][], -): Promise => { +): Promise => { const batch = new web3ReadOnly.BatchRequest() const whenRequestValues = [] diff --git a/src/routes/safe/components/Settings/index.tsx b/src/routes/safe/components/Settings/index.tsx index 6745b9fb31..1046f90a32 100644 --- a/src/routes/safe/components/Settings/index.tsx +++ b/src/routes/safe/components/Settings/index.tsx @@ -7,7 +7,7 @@ import { useState } from 'react' import { useSelector } from 'react-redux' import Advanced from './Advanced' -import SpendingLimit from './SpendingLimit' +import SpendingLimitSettings from './SpendingLimit' import ManageOwners from './ManageOwners' import { RemoveSafeModal } from './RemoveSafeModal' import SafeDetails from './SafeDetails' @@ -145,7 +145,7 @@ const Settings: React.FC = () => { {menuOptionIndex === 1 && } {menuOptionIndex === 2 && } {menuOptionIndex === 3 && } - {menuOptionIndex === 4 && } + {menuOptionIndex === 4 && } {menuOptionIndex === 5 && } From 157f539e1585e32401a10b5ab5d9f1e39274f04a Mon Sep 17 00:00:00 2001 From: fernandomg Date: Thu, 20 Aug 2020 22:19:08 -0300 Subject: [PATCH 43/69] refactor: convert `scannedAddress` to a oneliner --- .../Settings/SpendingLimit/BeneficiarySelect.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx b/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx index b814f366bd..d78c49a15e 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx @@ -43,12 +43,7 @@ const BeneficiarySelect = (): React.ReactElement => { const addressBook = useSelector(getAddressBook) const handleScan = (value, closeQrModal) => { - let scannedAddress = value - - if (scannedAddress.startsWith('ethereum:')) { - scannedAddress = scannedAddress.replace('ethereum:', '') - } - + const scannedAddress = value.startsWith('ethereum:') ? value.replace('ethereum:', '') : value const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : '' mutators?.setBeneficiary?.(scannedAddress) From f3e74a779e351ebf3b119c48e439e54496b30eb1 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 21 Aug 2020 09:07:07 -0300 Subject: [PATCH 44/69] fix typo --- src/routes/safe/components/Settings/SpendingLimit/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/utils.ts b/src/routes/safe/components/Settings/SpendingLimit/utils.ts index 811a49b437..3bb5b824fd 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/utils.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/utils.ts @@ -74,7 +74,7 @@ export type SpendingLimitRow = { } /** - * TODO: This is helpfully a temp function that attempts to filter out "deleted" allowances + * TODO: This is hopefully a temp function that attempts to filter out "deleted" allowances * As there's no way to remove an Allowance with the current code, it's "deleted" by setting its `amount` to 0 * along with `resetTimeMin` to 0 as well * @param {SpendingLimitRow} allowance From 0335f5f6eceff02426bff22e8b17d757f7bf6159 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 21 Aug 2020 09:34:24 -0300 Subject: [PATCH 45/69] refactor: extract common data display components --- .../Settings/SpendingLimit/AddressInfo.tsx | 34 ++++++ .../Settings/SpendingLimit/DataDisplay.tsx | 20 ++++ .../Settings/SpendingLimit/ResetTimeInfo.tsx | 32 +++++ .../SpendingLimit/ReviewSpendingLimit.tsx | 109 ++---------------- .../Settings/SpendingLimit/TokenInfo.tsx | 40 +++++++ 5 files changed, 135 insertions(+), 100 deletions(-) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/AddressInfo.tsx create mode 100644 src/routes/safe/components/Settings/SpendingLimit/DataDisplay.tsx create mode 100644 src/routes/safe/components/Settings/SpendingLimit/ResetTimeInfo.tsx create mode 100644 src/routes/safe/components/Settings/SpendingLimit/TokenInfo.tsx diff --git a/src/routes/safe/components/Settings/SpendingLimit/AddressInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/AddressInfo.tsx new file mode 100644 index 0000000000..ce69009819 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/AddressInfo.tsx @@ -0,0 +1,34 @@ +import { EthHashInfo } from '@gnosis.pm/safe-react-components' +import React from 'react' +import { useSelector } from 'react-redux' + +import { getNetwork } from 'src/config' +import { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { getNameFromAdbk } from 'src/logic/addressBook/utils' + +import DataDisplay from './DataDisplay' + +interface AddressInfoProps { + address: string + title?: string +} + +const AddressInfo = ({ address, title }: AddressInfoProps): React.ReactElement => { + const addressBook = useSelector(getAddressBook) + + return ( + + + + ) +} + +export default AddressInfo diff --git a/src/routes/safe/components/Settings/SpendingLimit/DataDisplay.tsx b/src/routes/safe/components/Settings/SpendingLimit/DataDisplay.tsx new file mode 100644 index 0000000000..669ea560b4 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/DataDisplay.tsx @@ -0,0 +1,20 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React from 'react' + +interface GenericInfoProps { + title?: string + children: React.ReactNode +} + +const DataDisplay = ({ title, children }: GenericInfoProps): React.ReactElement => ( + <> + {title && ( + + {title} + + )} + {children} + +) + +export default DataDisplay diff --git a/src/routes/safe/components/Settings/SpendingLimit/ResetTimeInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/ResetTimeInfo.tsx new file mode 100644 index 0000000000..3629cf87db --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/ResetTimeInfo.tsx @@ -0,0 +1,32 @@ +import { IconText, Text } from '@gnosis.pm/safe-react-components' +import React from 'react' + +import Row from 'src/components/layout/Row' + +import DataDisplay from './DataDisplay' + +interface ResetTimeInfoProps { + title?: string + label?: string +} + +const ResetTimeInfo = ({ title, label }: ResetTimeInfoProps): React.ReactElement => { + return ( + + {label ? ( + + + + ) : ( + + + {/* TODO: review message */} + One-time spending limit allowance + + + )} + + ) +} + +export default ResetTimeInfo diff --git a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx index 70d9d73de6..908bc4676a 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx @@ -1,15 +1,11 @@ -import { Button, EthHashInfo, Icon, IconText, Text, Title } from '@gnosis.pm/safe-react-components' +import { Button, Icon, Text, Title } from '@gnosis.pm/safe-react-components' import { useSnackbar } from 'notistack' import React from 'react' import { useDispatch, useSelector } from 'react-redux' -import styled from 'styled-components' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' -import { getNetwork } from 'src/config' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromAdbk } from 'src/logic/addressBook/utils' import { getGnosisSafeInstanceAt, getSpendingLimitContract } from 'src/logic/contracts/safeContracts' import { SpendingLimit } from 'src/logic/safe/store/models/safe' import { safeParamAddressFromStateSelector, safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors' @@ -17,26 +13,16 @@ import { Token } from 'src/logic/tokens/store/model/token' import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' -import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import { FooterSection, FooterWrapper, StyledButton, TitleSection } from '.' +import AddressInfo from './AddressInfo' import { RESET_TIME_OPTIONS } from './ResetTime' +import ResetTimeInfo from './ResetTimeInfo' import { useStyles } from './style' +import TokenInfo from './TokenInfo' import { currentMinutes, fromTokenUnit, SpendingLimitRow, toTokenUnit } from './utils' -const StyledImage = styled.img` - width: 32px; - height: 32px; - object-fit: contain; - margin: 0 8px 0 0; -` - -const StyledImageName = styled.div` - display: flex; - align-items: center; -` - interface ReviewSpendingLimitProps { onBack: () => void onClose: () => void @@ -45,84 +31,6 @@ interface ReviewSpendingLimitProps { existentSpendingLimit?: SpendingLimitRow } -interface GenericInfoProps { - title?: string - children: React.ReactNode -} -const GenericInfo = ({ title, children }: GenericInfoProps): React.ReactElement => ( - <> - {title && ( - - {title} - - )} - {children} - -) - -interface AddressInfoProps { - address: string - title?: string -} -const AddressInfo = ({ address, title }: AddressInfoProps): React.ReactElement => { - const addressBook = useSelector(getAddressBook) - - return ( - - - - ) -} - -interface TokenInfoProps { - amount: string - title?: string - token: Token -} -const TokenInfo = ({ amount, title, token }: TokenInfoProps): React.ReactElement => { - return ( - - - - - {amount} {token.symbol} - - - - ) -} - -interface ResetTimeInfoProps { - title?: string - label?: string -} -const ResetTimeInfo = ({ title, label }: ResetTimeInfoProps): React.ReactElement => { - return ( - - {label ? ( - - - - ) : ( - - - {/* TODO: review message */} - One-time spending limit allowance - - - )} - - ) -} - const ReviewSpendingLimit = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): React.ReactElement => { const classes = useStyles() @@ -221,6 +129,10 @@ const ReviewSpendingLimit = ({ onBack, onClose, txToken, values }: ReviewSpendin ? RESET_TIME_OPTIONS.find(({ value }) => value === values.resetTime).label : '' + const previousResetTime = (previousSpendingLimit: SpendingLimit) => + RESET_TIME_OPTIONS.find(({ value }) => value === (+previousSpendingLimit.resetTimeMin / 60 / 24).toString()) + ?.label ?? 'One-time spending limit allowance' + return ( <> @@ -253,10 +165,7 @@ const ReviewSpendingLimit = ({ onBack, onClose, txToken, values }: ReviewSpendin {existentSpendingLimit && ( - Previous Reset Time:{' '} - {RESET_TIME_OPTIONS.find( - ({ value }) => value === (+existentSpendingLimit.resetTimeMin / 60 / 24).toString(), - )?.label ?? 'One-time spending limit allowance'} + Previous Reset Time: {previousResetTime(existentSpendingLimit)} )} diff --git a/src/routes/safe/components/Settings/SpendingLimit/TokenInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/TokenInfo.tsx new file mode 100644 index 0000000000..14d5e93e21 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/TokenInfo.tsx @@ -0,0 +1,40 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React from 'react' +import styled from 'styled-components' + +import { Token } from 'src/logic/tokens/store/model/token' +import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' + +import DataDisplay from './DataDisplay' + +const StyledImage = styled.img` + width: 32px; + height: 32px; + object-fit: contain; + margin: 0 8px 0 0; +` +const StyledImageName = styled.div` + display: flex; + align-items: center; +` + +interface TokenInfoProps { + amount: string + title?: string + token: Token +} + +const TokenInfo = ({ amount, title, token }: TokenInfoProps): React.ReactElement => { + return ( + + + + + {amount} {token.symbol} + + + + ) +} + +export default TokenInfo From 69d035c3f3349119e0fae9a38909d6f0dd2e3f61 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 21 Aug 2020 09:34:48 -0300 Subject: [PATCH 46/69] refactor: extract spending limit table --- .../Settings/SpendingLimit/LimitsTable.tsx | 200 ++++++++++++++++++ .../Settings/SpendingLimit/index.tsx | 199 +---------------- 2 files changed, 206 insertions(+), 193 deletions(-) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx new file mode 100644 index 0000000000..778e96002b --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx @@ -0,0 +1,200 @@ +import { Button, EthHashInfo, Text } from '@gnosis.pm/safe-react-components' +import TableContainer from '@material-ui/core/TableContainer' +import cn from 'classnames' +import React from 'react' +import { useSelector } from 'react-redux' +import styled from 'styled-components' + +import Row from 'src/components/layout/Row' +import { TableCell, TableRow } from 'src/components/layout/Table' +import Table from 'src/components/Table' +import { getNetwork } from 'src/config' +import { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { Token } from 'src/logic/tokens/store/model/token' +import { formatAmount } from 'src/logic/tokens/utils/formatAmount' +import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' +import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' +import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' + +import { + generateColumns, + SPENDING_LIMIT_TABLE_BENEFICIARY_ID, + SPENDING_LIMIT_TABLE_RESET_TIME_ID, + SPENDING_LIMIT_TABLE_SPENT_ID, + SpendingLimitTable, +} from './dataFetcher' +import RemoveSpendingLimitModal from './RemoveSpendingLimitModal' +import { useStyles } from './style' +import { fromTokenUnit } from './utils' + +const StyledImage = styled.img` + width: 32px; + height: 32px; + object-fit: contain; + margin: 0 8px 0 0; +` +const StyledImageName = styled.div` + display: flex; + align-items: center; +` +const TableActionButton = styled(Button)` + background-color: transparent; + padding: 0; + + &:hover { + background-color: transparent; + } +` +type SpentInfo = { + token: Token + spent: string + amount: string +} + +interface HumanReadableSpentProps { + spent: string + amount: string + tokenAddress: string +} + +const HumanReadableSpent = ({ spent, amount, tokenAddress }: HumanReadableSpentProps): React.ReactElement => { + const tokens = useSelector(extendedSafeTokensSelector) + const { width } = useWindowDimensions() + const [spentInfo, setSpentInfo] = React.useState() + + React.useEffect(() => { + if (tokens) { + const safeTokenAddress = tokenAddress === ZERO_ADDRESS ? ETH_ADDRESS : tokenAddress + const token = tokens.find((token) => token.address === safeTokenAddress) + const formattedSpent = formatAmount(fromTokenUnit(spent, token.decimals)).toString() + const formattedAmount = formatAmount(fromTokenUnit(amount, token.decimals)).toString() + + setSpentInfo({ token, spent: formattedSpent, amount: formattedAmount }) + } + }, [amount, spent, tokenAddress, tokens]) + + return spentInfo ? ( + + {width > 1024 && ( + + )} + {`${spentInfo.spent} of ${spentInfo.amount} ${spentInfo.token.symbol}`} + + ) : null +} + +interface SpendingLimitTableProps { + data?: SpendingLimitTable[] +} + +const LimitsTable = ({ data }: SpendingLimitTableProps): React.ReactElement => { + const classes = useStyles() + const granted = useSelector(grantedSelector) + const addressBook = useSelector(getAddressBook) + + const columns = generateColumns() + const autoColumns = columns.filter(({ custom }) => !custom) + + const [cut, setCut] = React.useState(undefined) + const { width } = useWindowDimensions() + React.useEffect(() => { + if (width <= 1024) { + setCut(4) + } else { + setCut(8) + } + }, [width]) + + const [showRemoveSpendingLimitModal, setShowRemoveSpendingLimitModal] = React.useState(false) + const [selectedRow, setSelectedRow] = React.useState(null) + const openRemoveSpendingLimitModal = (row: SpendingLimitTable) => { + setSelectedRow(row) + setShowRemoveSpendingLimitModal(true) + } + const closeRemoveSpendingLimitModal = () => { + setShowRemoveSpendingLimitModal(false) + setSelectedRow(null) + } + const handleDeleteSpendingLimit = (row: SpendingLimitTable): void => { + openRemoveSpendingLimitModal(row) + } + + return ( + <> + +
+ {(sortedData) => + sortedData.map((row, index) => ( + = 3 && index === sortedData.size - 1 && classes.noBorderBottom)} + data-testid="spending-limit-table-row" + key={index} + tabIndex={-1} + > + {autoColumns.map((column, index) => { + const columnId = column.id + const rowElement = row[columnId] + + return ( + + {columnId === SPENDING_LIMIT_TABLE_BENEFICIARY_ID && ( + + )} + + {columnId === SPENDING_LIMIT_TABLE_SPENT_ID && } + {columnId === SPENDING_LIMIT_TABLE_RESET_TIME_ID && ( + {rowElement.relativeTime} + )} + + ) + })} + + + {granted && ( + handleDeleteSpendingLimit(row)} + data-testid="remove-action" + > + {null} + + )} + + + + )) + } +
+
+ {showRemoveSpendingLimitModal && ( + + )} + + ) +} + +export default LimitsTable diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 744930b328..312e77e2e3 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -1,6 +1,4 @@ -import { Button, EthHashInfo, Text, Title } from '@gnosis.pm/safe-react-components' -import TableContainer from '@material-ui/core/TableContainer' -import cn from 'classnames' +import { Button, Text, Title } from '@gnosis.pm/safe-react-components' import React from 'react' import { useSelector } from 'react-redux' import styled from 'styled-components' @@ -8,39 +6,15 @@ import styled from 'styled-components' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' -import { TableCell, TableRow } from 'src/components/layout/Table' -import Table from 'src/components/Table' -import { getNetwork } from 'src/config' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromAdbk } from 'src/logic/addressBook/utils' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -import { Token } from 'src/logic/tokens/store/model/token' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' -import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' -import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' -import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' -import { - generateColumns, - getSpendingLimitData, - SPENDING_LIMIT_TABLE_BENEFICIARY_ID, - SPENDING_LIMIT_TABLE_RESET_TIME_ID, - SPENDING_LIMIT_TABLE_SPENT_ID, - SpendingLimitTable, -} from './dataFetcher' -import RemoveSpendingLimitModal from './RemoveSpendingLimitModal' +import { getSpendingLimitData, SpendingLimitTable } from './dataFetcher' +import LimitsTable from './LimitsTable' import SpendingLimitModal from './SpendingLimitModal' import SpendingLimitSteps from './SpendingLimitSteps' import { useStyles } from './style' -import { - fromTokenUnit, - requestAllowancesByDelegatesAndTokens, - requestModuleData, - requestTokensByDelegate, - SpendingLimitRow, -} from './utils' +import { requestAllowancesByDelegatesAndTokens, requestModuleData, requestTokensByDelegate } from './utils' const InfoText = styled(Text)` margin-top: 16px; @@ -81,99 +55,24 @@ export const FooterWrapper = styled.div` justify-content: space-around; ` -const StyledImage = styled.img` - width: 32px; - height: 32px; - object-fit: contain; - margin: 0 8px 0 0; -` - -const StyledImageName = styled.div` - display: flex; - align-items: center; -` - -const TableActionButton = styled(Button)` - background-color: transparent; - padding: 0; - - &:hover { - background-color: transparent; - } -` - -type SpentInfo = { - token: Token - spent: string - amount: string -} - -interface HumanReadableSpentProps { - spent: string - amount: string - tokenAddress: string -} - -const HumanReadableSpent = ({ spent, amount, tokenAddress }: HumanReadableSpentProps): React.ReactElement => { - const tokens = useSelector(extendedSafeTokensSelector) - const { width } = useWindowDimensions() - const [spentInfo, setSpentInfo] = React.useState() - - React.useEffect(() => { - if (tokens) { - const safeTokenAddress = tokenAddress === ZERO_ADDRESS ? ETH_ADDRESS : tokenAddress - const token = tokens.find((token) => token.address === safeTokenAddress) - const formattedSpent = formatAmount(fromTokenUnit(spent, token.decimals)).toString() - const formattedAmount = formatAmount(fromTokenUnit(amount, token.decimals)).toString() - - setSpentInfo({ token, spent: formattedSpent, amount: formattedAmount }) - } - }, [amount, spent, tokenAddress, tokens]) - - return spentInfo ? ( - - {width > 1024 && ( - - )} - {`${spentInfo.spent} of ${spentInfo.amount} ${spentInfo.token.symbol}`} - - ) : null -} - const SpendingLimitSettings = (): React.ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) const tokens = useSelector(extendedSafeTokensSelector) - const addressBook = useSelector(getAddressBook) // TODO: Refactor `delegates` for better performance. This is just to verify allowance works const safeAddress = useSelector(safeParamAddressFromStateSelector) - const [spendingLimits, setSpendingLimits] = React.useState() - const [spendingLimitData, setSpendingLimitData] = React.useState() + const [spendingLimitData, setSpendingLimitData] = React.useState() React.useEffect(() => { const doRequestData = async () => { const [, delegates] = await requestModuleData(safeAddress) const tokensByDelegate = await requestTokensByDelegate(safeAddress, delegates.results) const allowances = await requestAllowancesByDelegatesAndTokens(safeAddress, tokensByDelegate) - setSpendingLimits(allowances) setSpendingLimitData(getSpendingLimitData(allowances)) } doRequestData() }, [safeAddress, tokens]) - const [cut, setCut] = React.useState(undefined) - const { width } = useWindowDimensions() - React.useEffect(() => { - if (width <= 1024) { - setCut(4) - } else { - setCut(8) - } - }, [width]) - - const columns = generateColumns() - const autoColumns = columns.filter(({ custom }) => !custom) - const [showNewSpendingLimitModal, setShowNewSpendingLimitModal] = React.useState(false) const openNewSpendingLimitModal = () => { setShowNewSpendingLimitModal(true) @@ -182,20 +81,6 @@ const SpendingLimitSettings = (): React.ReactElement => { setShowNewSpendingLimitModal(false) } - const [showRemoveSpendingLimitModal, setShowRemoveSpendingLimitModal] = React.useState(false) - const [selectedRow, setSelectedRow] = React.useState(null) - const openRemoveSpendingLimitModal = (row: SpendingLimitTable) => { - setSelectedRow(row) - setShowRemoveSpendingLimitModal(true) - } - const closeRemoveSpendingLimitModal = () => { - setShowRemoveSpendingLimitModal(false) - setSelectedRow(null) - } - const handleDeleteSpendingLimit = (row: SpendingLimitTable): void => { - openRemoveSpendingLimitModal(row) - } - return ( <> @@ -206,76 +91,7 @@ const SpendingLimitSettings = (): React.ReactElement => { You can set rules for specific beneficiaries to access funds from this Safe without having to collect all signatures. - {spendingLimits?.length && spendingLimitData?.length ? ( - - - {(sortedData) => - sortedData.map((row, index) => ( - = 3 && index === sortedData.size - 1 && classes.noBorderBottom)} - data-testid="spending-limit-table-row" - key={index} - tabIndex={-1} - > - {autoColumns.map((column, index) => { - const columnId = column.id - const rowElement = row[columnId] - - return ( - - {columnId === SPENDING_LIMIT_TABLE_BENEFICIARY_ID && ( - - )} - - {columnId === SPENDING_LIMIT_TABLE_SPENT_ID && } - {columnId === SPENDING_LIMIT_TABLE_RESET_TIME_ID && ( - {rowElement.relativeTime} - )} - - ) - })} - - - {granted && ( - handleDeleteSpendingLimit(row)} - data-testid="remove-action" - > - {null} - - )} - - - - )) - } -
-
- ) : ( - - )} + {spendingLimitData?.length ? : }
{granted && ( @@ -297,9 +113,6 @@ const SpendingLimitSettings = (): React.ReactElement => { {showNewSpendingLimitModal && } )} - {showRemoveSpendingLimitModal && ( - - )} ) } From ed87c7e96b2f3c6609ef0ab0a5c72afd149690f6 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 21 Aug 2020 09:57:07 -0300 Subject: [PATCH 47/69] refactor: use common data display components --- .../RemoveSpendingLimitModal.tsx | 162 ++++-------------- .../SpendingLimit/ReviewSpendingLimit.tsx | 2 +- 2 files changed, 31 insertions(+), 133 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx index cd5a0c44f7..0eea238393 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx @@ -1,18 +1,11 @@ -import { Button, EthHashInfo, Icon, IconText, Text, Title } from '@gnosis.pm/safe-react-components' -import { Skeleton } from '@material-ui/lab' +import { Button, Icon, Title } from '@gnosis.pm/safe-react-components' import { useSnackbar } from 'notistack' import React from 'react' import { useDispatch, useSelector } from 'react-redux' -import { fromTokenUnit } from 'src/routes/safe/components/Settings/SpendingLimit/utils' -import styled from 'styled-components' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' -import Row from 'src/components/layout/Row' import Modal from 'src/components/Modal' -import { getNetwork } from 'src/config' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromAdbk } from 'src/logic/addressBook/utils' import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' @@ -20,132 +13,18 @@ import { Token } from 'src/logic/tokens/store/model/token' import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { getWeb3 } from 'src/logic/wallets/getWeb3' -import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' -import { - FooterSection, - FooterWrapper, - StyledButton, - TitleSection, -} from 'src/routes/safe/components/Settings/SpendingLimit' -import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/ResetTime' import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' -import { ResetTimeInfo as ResetTimeInfoType, SpendingLimitTable } from './dataFetcher' +import { FooterSection, FooterWrapper, StyledButton, TitleSection } from '.' +import AddressInfo from './AddressInfo' +import { SpendingLimitTable } from './dataFetcher' +import { RESET_TIME_OPTIONS } from './ResetTime' +import ResetTimeInfo from './ResetTimeInfo' import { useStyles } from './style' - -const StyledImage = styled.img` - width: 32px; - height: 32px; - object-fit: contain; - margin: 0 8px 0 0; -` - -const StyledImageName = styled.div` - display: flex; - align-items: center; -` - -interface GenericInfoProps { - title?: string - children: React.ReactNode -} - -const GenericInfo = ({ title, children }: GenericInfoProps): React.ReactElement => ( - <> - {title && ( - - {title} - - )} - {children ?? } - -) - -interface AddressInfoProps { - title?: string - address: string -} - -const AddressInfo = ({ title, address }: AddressInfoProps): React.ReactElement => { - const addressBook = useSelector(getAddressBook) - - return ( - - {addressBook && ( - - )} - - ) -} - -interface TokenInfoProps { - title?: string - amount: string - address: string -} - -const TokenInfo = ({ title, amount, address }: TokenInfoProps): React.ReactElement => { - const tokens = useSelector(extendedSafeTokensSelector) - const [token, setToken] = React.useState() - - React.useEffect(() => { - if (tokens) { - const tokenAddress = address === ZERO_ADDRESS ? ETH_ADDRESS : address - const foundToken = tokens.find((token) => token.address === tokenAddress) - setToken(foundToken ?? null) - } - }, [address, tokens]) - - return ( - - {token && ( - - - - {fromTokenUnit(amount, token.decimals)} {token.symbol} - - - )} - {token === null && No token info} - - ) -} - -interface ResetTimeInfoProps { - title?: string - resetTime: ResetTimeInfoType -} - -const ResetTimeInfo = ({ title, resetTime }: ResetTimeInfoProps): React.ReactElement => { - return ( - - {resetTime.resetTimeMin !== '0' ? ( - - +value === +resetTime.resetTimeMin / 24 / 60).label} - textSize="lg" - /> - - ) : ( - - One-time spending limit allowance - - )} - - ) -} +import TokenInfo from './TokenInfo' +import { fromTokenUnit } from './utils' interface RemoveSpendingLimitModalProps { onClose: () => void @@ -160,8 +39,18 @@ const RemoveSpendingLimitModal = ({ }: RemoveSpendingLimitModalProps): React.ReactElement => { const classes = useStyles() - const safeAddress = useSelector(safeParamAddressFromStateSelector) + const tokens = useSelector(extendedSafeTokensSelector) + const [tokenInfo, setTokenInfo] = React.useState() + React.useEffect(() => { + if (tokens) { + const tokenAddress = + spendingLimit.spent.tokenAddress === ZERO_ADDRESS ? ETH_ADDRESS : spendingLimit.spent.tokenAddress + const foundToken = tokens.find((token) => token.address === tokenAddress) + setTokenInfo(foundToken) + } + }, [spendingLimit.spent.tokenAddress, tokens]) + const safeAddress = useSelector(safeParamAddressFromStateSelector) const { enqueueSnackbar, closeSnackbar } = useSnackbar() const dispatch = useDispatch() @@ -200,6 +89,9 @@ const RemoveSpendingLimitModal = ({ } } + const resetTimeLabel = + RESET_TIME_OPTIONS.find(({ value }) => +value === +spendingLimit.resetTime.resetTimeMin / 24 / 60)?.label ?? '' + return ( - + {tokenInfo && ( + + )} - +
diff --git a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx index 908bc4676a..4f4c26edb1 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx @@ -126,7 +126,7 @@ const ReviewSpendingLimit = ({ onBack, onClose, txToken, values }: ReviewSpendin } const resetTimeLabel = values.withResetTime - ? RESET_TIME_OPTIONS.find(({ value }) => value === values.resetTime).label + ? RESET_TIME_OPTIONS.find(({ value }) => value === values.resetTime)?.label : '' const previousResetTime = (previousSpendingLimit: SpendingLimit) => From dacfc4a2f153b66b4970bef0d01cc6bdea626aaf Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 21 Aug 2020 10:44:42 -0300 Subject: [PATCH 48/69] refactor: reorg files and rename components --- .../SpendingLimit/{ => FormFields}/Amount.tsx | 6 ++++-- .../Beneficiary.tsx} | 6 +++--- .../SpendingLimit/{ => FormFields}/ResetTime.tsx | 2 +- .../{TokenSelect.tsx => FormFields/Token.tsx} | 4 ++-- .../Settings/SpendingLimit/FormFields/index.ts | 6 ++++++ .../{ => InfoDisplay}/AddressInfo.tsx | 0 .../{ => InfoDisplay}/DataDisplay.tsx | 0 .../{ => InfoDisplay}/ResetTimeInfo.tsx | 0 .../SpendingLimit/{ => InfoDisplay}/TokenInfo.tsx | 0 .../Settings/SpendingLimit/InfoDisplay/index.ts | 6 ++++++ .../Settings/SpendingLimit/LimitsTable.tsx | 4 ++-- .../{NewSpendingLimit.tsx => NewLimit.tsx} | 13 +++++-------- .../{SpendingLimitModal.tsx => NewLimitModal.tsx} | 12 ++++++------ ...{ReviewSpendingLimit.tsx => NewLimitReview.tsx} | 10 ++++------ .../{SpendingLimitSteps.tsx => NewLimitSteps.tsx} | 4 ++-- ...SpendingLimitModal.tsx => RemoveLimitModal.tsx} | 14 ++++---------- .../components/Settings/SpendingLimit/index.tsx | 8 ++++---- 17 files changed, 49 insertions(+), 46 deletions(-) rename src/routes/safe/components/Settings/SpendingLimit/{ => FormFields}/Amount.tsx (90%) rename src/routes/safe/components/Settings/SpendingLimit/{BeneficiarySelect.tsx => FormFields/Beneficiary.tsx} (94%) rename src/routes/safe/components/Settings/SpendingLimit/{ => FormFields}/ResetTime.tsx (97%) rename src/routes/safe/components/Settings/SpendingLimit/{TokenSelect.tsx => FormFields/Token.tsx} (87%) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/FormFields/index.ts rename src/routes/safe/components/Settings/SpendingLimit/{ => InfoDisplay}/AddressInfo.tsx (100%) rename src/routes/safe/components/Settings/SpendingLimit/{ => InfoDisplay}/DataDisplay.tsx (100%) rename src/routes/safe/components/Settings/SpendingLimit/{ => InfoDisplay}/ResetTimeInfo.tsx (100%) rename src/routes/safe/components/Settings/SpendingLimit/{ => InfoDisplay}/TokenInfo.tsx (100%) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/index.ts rename src/routes/safe/components/Settings/SpendingLimit/{NewSpendingLimit.tsx => NewLimit.tsx} (88%) rename src/routes/safe/components/Settings/SpendingLimit/{SpendingLimitModal.tsx => NewLimitModal.tsx} (77%) rename src/routes/safe/components/Settings/SpendingLimit/{ReviewSpendingLimit.tsx => NewLimitReview.tsx} (95%) rename src/routes/safe/components/Settings/SpendingLimit/{SpendingLimitSteps.tsx => NewLimitSteps.tsx} (95%) rename src/routes/safe/components/Settings/SpendingLimit/{RemoveSpendingLimitModal.tsx => RemoveLimitModal.tsx} (93%) diff --git a/src/routes/safe/components/Settings/SpendingLimit/Amount.tsx b/src/routes/safe/components/Settings/SpendingLimit/FormFields/Amount.tsx similarity index 90% rename from src/routes/safe/components/Settings/SpendingLimit/Amount.tsx rename to src/routes/safe/components/Settings/SpendingLimit/FormFields/Amount.tsx index aeb1a407ff..9b4a935f68 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/Amount.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/FormFields/Amount.tsx @@ -8,7 +8,7 @@ import GnoField from 'src/components/forms/Field' import { composeValidators, minValue, mustBeFloat, required } from 'src/components/forms/validator' import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' -import { useStyles } from './style' +import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' export const Field = styled(GnoField)` margin: 8px 0; @@ -23,7 +23,7 @@ const TextField = styled(SRCTextField)` margin: 0; ` -export const Amount = (): React.ReactElement => { +const Amount = (): React.ReactElement => { const classes = useStyles() const { @@ -54,3 +54,5 @@ export const Amount = (): React.ReactElement => {
) } + +export default Amount diff --git a/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx b/src/routes/safe/components/Settings/SpendingLimit/FormFields/Beneficiary.tsx similarity index 94% rename from src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx rename to src/routes/safe/components/Settings/SpendingLimit/FormFields/Beneficiary.tsx index dc5151a7b3..5b519a3f40 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/BeneficiarySelect.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/FormFields/Beneficiary.tsx @@ -10,7 +10,7 @@ import { getAddressBook } from 'src/logic/addressBook/store/selectors' import { getNameFromAdbk } from 'src/logic/addressBook/utils' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' -import { KEYCODES } from './utils' +import { KEYCODES } from 'src/routes/safe/components/Settings/SpendingLimit/utils' const BeneficiaryInput = styled.div` grid-area: beneficiaryInput; @@ -20,7 +20,7 @@ const BeneficiaryScan = styled.div` grid-area: beneficiaryScan; ` -const BeneficiarySelect = (): React.ReactElement => { +const Beneficiary = (): React.ReactElement => { const { initialValues } = useFormState() const { mutators } = useForm() @@ -102,4 +102,4 @@ const BeneficiarySelect = (): React.ReactElement => { ) } -export default BeneficiarySelect +export default Beneficiary diff --git a/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx b/src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime.tsx similarity index 97% rename from src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx rename to src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime.tsx index d436044a8a..46c5c033b3 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/ResetTime.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime.tsx @@ -4,7 +4,7 @@ import React from 'react' import { useField } from 'react-final-form' import styled from 'styled-components' -import { Field } from './Amount' +import { Field } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/Amount' // TODO: propose refactor in safe-react-components based on this requirements const SpendingLimitRadioButtons = styled(RadioButtons)` diff --git a/src/routes/safe/components/Settings/SpendingLimit/TokenSelect.tsx b/src/routes/safe/components/Settings/SpendingLimit/FormFields/Token.tsx similarity index 87% rename from src/routes/safe/components/Settings/SpendingLimit/TokenSelect.tsx rename to src/routes/safe/components/Settings/SpendingLimit/FormFields/Token.tsx index 276a84e666..381229c4ff 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/TokenSelect.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/FormFields/Token.tsx @@ -8,7 +8,7 @@ const TokenInput = styled.div` grid-area: tokenInput; ` -const TokenSelect = (): React.ReactElement => { +const Token = (): React.ReactElement => { const tokens = useSelector(extendedSafeTokensSelector) return ( @@ -18,4 +18,4 @@ const TokenSelect = (): React.ReactElement => { ) } -export default TokenSelect +export default Token diff --git a/src/routes/safe/components/Settings/SpendingLimit/FormFields/index.ts b/src/routes/safe/components/Settings/SpendingLimit/FormFields/index.ts new file mode 100644 index 0000000000..555ac53768 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/FormFields/index.ts @@ -0,0 +1,6 @@ +import Amount from './Amount' +import Beneficiary from './Beneficiary' +import ResetTime from './ResetTime' +import Token from './Token' + +export { Amount, Beneficiary, ResetTime, Token } diff --git a/src/routes/safe/components/Settings/SpendingLimit/AddressInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx similarity index 100% rename from src/routes/safe/components/Settings/SpendingLimit/AddressInfo.tsx rename to src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx diff --git a/src/routes/safe/components/Settings/SpendingLimit/DataDisplay.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/DataDisplay.tsx similarity index 100% rename from src/routes/safe/components/Settings/SpendingLimit/DataDisplay.tsx rename to src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/DataDisplay.tsx diff --git a/src/routes/safe/components/Settings/SpendingLimit/ResetTimeInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/ResetTimeInfo.tsx similarity index 100% rename from src/routes/safe/components/Settings/SpendingLimit/ResetTimeInfo.tsx rename to src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/ResetTimeInfo.tsx diff --git a/src/routes/safe/components/Settings/SpendingLimit/TokenInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/TokenInfo.tsx similarity index 100% rename from src/routes/safe/components/Settings/SpendingLimit/TokenInfo.tsx rename to src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/TokenInfo.tsx diff --git a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/index.ts b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/index.ts new file mode 100644 index 0000000000..2d6b7eb13d --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/index.ts @@ -0,0 +1,6 @@ +import AddressInfo from './AddressInfo' +import ResetTimeInfo from './ResetTimeInfo' +import TokenInfo from './TokenInfo' + +export { AddressInfo, ResetTimeInfo, TokenInfo } +export default './DataDisplay' diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx index 778e96002b..e1853b2e29 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx @@ -26,7 +26,7 @@ import { SPENDING_LIMIT_TABLE_SPENT_ID, SpendingLimitTable, } from './dataFetcher' -import RemoveSpendingLimitModal from './RemoveSpendingLimitModal' +import RemoveLimitModal from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal' import { useStyles } from './style' import { fromTokenUnit } from './utils' @@ -191,7 +191,7 @@ const LimitsTable = ({ data }: SpendingLimitTableProps): React.ReactElement => { {showRemoveSpendingLimitModal && ( - + )} ) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimit.tsx similarity index 88% rename from src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx rename to src/routes/safe/components/Settings/SpendingLimit/NewLimit.tsx index b5276a05a3..ef80663aec 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimit.tsx @@ -6,11 +6,8 @@ import styled from 'styled-components' import GnoForm from 'src/components/forms/GnoForm' import GnoButton from 'src/components/layout/Button' -import { Amount } from './Amount' -import BeneficiarySelect from './BeneficiarySelect' import { TitleSection, StyledButton, FooterSection, FooterWrapper } from '.' -import ResetTime from './ResetTime' -import TokenSelect from './TokenSelect' +import { Amount, Beneficiary, ResetTime, Token } from './FormFields' const FormContainer = styled.div` padding: 24px; @@ -60,7 +57,7 @@ const canReview = ({ invalid, submitting, dirtyFieldsSinceLastSubmit, values }): ) } -const NewSpendingLimit = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): React.ReactElement => ( +const NewLimit = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): React.ReactElement => ( <> @@ -79,8 +76,8 @@ const NewSpendingLimit = ({ initialValues, onCancel, onReview }: NewSpendingLimi {(...args) => ( <> <FormContainer> - <BeneficiarySelect /> - <TokenSelect /> + <Beneficiary /> + <Token /> <Amount /> <ResetTime /> </FormContainer> @@ -109,4 +106,4 @@ const NewSpendingLimit = ({ initialValues, onCancel, onReview }: NewSpendingLimi </> ) -export default NewSpendingLimit +export default NewLimit diff --git a/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx similarity index 77% rename from src/routes/safe/components/Settings/SpendingLimit/SpendingLimitModal.tsx rename to src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx index 05a765ffb6..620e4abed4 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx @@ -2,8 +2,8 @@ import React from 'react' import { useSelector } from 'react-redux' import GnoModal from 'src/components/Modal' import { Token } from 'src/logic/tokens/store/model/token' -import NewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/NewSpendingLimit' -import ReviewSpendingLimit from 'src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit' +import NewLimit from 'src/routes/safe/components/Settings/SpendingLimit/NewLimit' +import NewLimitReview from 'src/routes/safe/components/Settings/SpendingLimit/NewLimitReview' import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' @@ -56,7 +56,7 @@ interface SpendingLimitModalProps { open: boolean } -const SpendingLimitModal = ({ close, open }: SpendingLimitModalProps): React.ReactElement => { +const NewLimitModal = ({ close, open }: SpendingLimitModalProps): React.ReactElement => { const classes = useStyles() const tokens = useSelector(extendedSafeTokensSelector) @@ -77,10 +77,10 @@ const SpendingLimitModal = ({ close, open }: SpendingLimitModalProps): React.Rea description="set rules for specific beneficiaries to access funds from this Safe without having to collect all signatures" paperClassName={classes.modal} > - {step === CREATE && <NewSpendingLimit initialValues={values} onCancel={close} onReview={handleReview} />} - {step === REVIEW && <ReviewSpendingLimit onBack={create} onClose={close} txToken={txToken} values={values} />} + {step === CREATE && <NewLimit initialValues={values} onCancel={close} onReview={handleReview} />} + {step === REVIEW && <NewLimitReview onBack={create} onClose={close} txToken={txToken} values={values} />} </GnoModal> ) } -export default SpendingLimitModal +export default NewLimitModal diff --git a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitReview.tsx similarity index 95% rename from src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx rename to src/routes/safe/components/Settings/SpendingLimit/NewLimitReview.tsx index 4f4c26edb1..16b8002425 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/ReviewSpendingLimit.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitReview.tsx @@ -16,11 +16,9 @@ import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import { FooterSection, FooterWrapper, StyledButton, TitleSection } from '.' -import AddressInfo from './AddressInfo' -import { RESET_TIME_OPTIONS } from './ResetTime' -import ResetTimeInfo from './ResetTimeInfo' +import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay' +import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' import { useStyles } from './style' -import TokenInfo from './TokenInfo' import { currentMinutes, fromTokenUnit, SpendingLimitRow, toTokenUnit } from './utils' interface ReviewSpendingLimitProps { @@ -31,7 +29,7 @@ interface ReviewSpendingLimitProps { existentSpendingLimit?: SpendingLimitRow } -const ReviewSpendingLimit = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): React.ReactElement => { +const NewLimitReview = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): React.ReactElement => { const classes = useStyles() const { enqueueSnackbar, closeSnackbar } = useSnackbar() @@ -199,4 +197,4 @@ const ReviewSpendingLimit = ({ onBack, onClose, txToken, values }: ReviewSpendin ) } -export default ReviewSpendingLimit +export default NewLimitReview diff --git a/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx similarity index 95% rename from src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps.tsx rename to src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx index e1372d43c8..0e5b2e9566 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/SpendingLimitSteps.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps.tsx @@ -28,7 +28,7 @@ const StepsLine = styled.div` margin: 46px 0; ` -const SpendingLimitSteps = (): React.ReactElement => ( +const NewLimitSteps = (): React.ReactElement => ( <StepWrapper> <Step> <Img alt="Select Beneficiary" title="Beneficiary" height={96} src={Beneficiary} /> @@ -76,4 +76,4 @@ const SpendingLimitSteps = (): React.ReactElement => ( </StepWrapper> ) -export default SpendingLimitSteps +export default NewLimitSteps diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx similarity index 93% rename from src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx rename to src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 0eea238393..3170172bfb 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveSpendingLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -18,12 +18,10 @@ import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import { FooterSection, FooterWrapper, StyledButton, TitleSection } from '.' -import AddressInfo from './AddressInfo' import { SpendingLimitTable } from './dataFetcher' -import { RESET_TIME_OPTIONS } from './ResetTime' -import ResetTimeInfo from './ResetTimeInfo' +import { AddressInfo, TokenInfo, ResetTimeInfo } from './InfoDisplay' +import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' import { useStyles } from './style' -import TokenInfo from './TokenInfo' import { fromTokenUnit } from './utils' interface RemoveSpendingLimitModalProps { @@ -32,11 +30,7 @@ interface RemoveSpendingLimitModalProps { open: boolean } -const RemoveSpendingLimitModal = ({ - onClose, - spendingLimit, - open, -}: RemoveSpendingLimitModalProps): React.ReactElement => { +const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitModalProps): React.ReactElement => { const classes = useStyles() const tokens = useSelector(extendedSafeTokensSelector) @@ -142,4 +136,4 @@ const RemoveSpendingLimitModal = ({ ) } -export default RemoveSpendingLimitModal +export default RemoveLimitModal diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 312e77e2e3..e45f4b0063 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -11,8 +11,8 @@ import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/con import { getSpendingLimitData, SpendingLimitTable } from './dataFetcher' import LimitsTable from './LimitsTable' -import SpendingLimitModal from './SpendingLimitModal' -import SpendingLimitSteps from './SpendingLimitSteps' +import NewLimitModal from 'src/routes/safe/components/Settings/SpendingLimit/NewLimitModal' +import NewLimitSteps from 'src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps' import { useStyles } from './style' import { requestAllowancesByDelegatesAndTokens, requestModuleData, requestTokensByDelegate } from './utils' @@ -91,7 +91,7 @@ const SpendingLimitSettings = (): React.ReactElement => { You can set rules for specific beneficiaries to access funds from this Safe without having to collect all signatures. </InfoText> - {spendingLimitData?.length ? <LimitsTable data={spendingLimitData} /> : <SpendingLimitSteps />} + {spendingLimitData?.length ? <LimitsTable data={spendingLimitData} /> : <NewLimitSteps />} </Block> {granted && ( @@ -110,7 +110,7 @@ const SpendingLimitSettings = (): React.ReactElement => { </Button> </Col> </Row> - {showNewSpendingLimitModal && <SpendingLimitModal close={closeNewSpendingLimitModal} open={true} />} + {showNewSpendingLimitModal && <NewLimitModal close={closeNewSpendingLimitModal} open={true} />} </> )} </> From 767ecb628cfedfa071e0f659f518cee47f87d2bd Mon Sep 17 00:00:00 2001 From: fernandomg <fernando.greco@gmail.com> Date: Fri, 21 Aug 2020 12:32:52 -0300 Subject: [PATCH 49/69] refactor: move token discovery into hook --- .../Settings/SpendingLimit/NewLimitModal.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx index 620e4abed4..2ff0de4198 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx @@ -15,6 +15,7 @@ type Step = typeof CREATE | typeof REVIEW type SpendingLimitModalReducerState = { step: Step values: Record<string, string> | null + tokens: Token[] txToken: Token | null } @@ -31,7 +32,9 @@ const newSpendingLimitReducer = (state: SpendingLimitModalReducerState, action) case REVIEW: { return { + ...state, ...newState, + txToken: state.tokens.find((token) => token.address === newState.values.token) ?? null, step: REVIEW, } } @@ -39,9 +42,12 @@ const newSpendingLimitReducer = (state: SpendingLimitModalReducerState, action) } const useSpendingLimit = (initialStep: Step) => { + const tokens = useSelector(extendedSafeTokensSelector) + const [state, dispatch] = React.useReducer(newSpendingLimitReducer, { step: initialStep, values: null, + tokens: tokens ?? [], txToken: null, }) @@ -59,14 +65,10 @@ interface SpendingLimitModalProps { const NewLimitModal = ({ close, open }: SpendingLimitModalProps): React.ReactElement => { const classes = useStyles() - const tokens = useSelector(extendedSafeTokensSelector) const [{ step, txToken, values }, { create, review }] = useSpendingLimit(CREATE) const handleReview = async (values) => { - review({ - values, - txToken: tokens.find((token) => token.address === values.token), - }) + review({ values }) } return ( From 4b17b1748b769f700bf602fb4e880daa49ebfa38 Mon Sep 17 00:00:00 2001 From: fernandomg <fernando.greco@gmail.com> Date: Fri, 21 Aug 2020 14:13:12 -0300 Subject: [PATCH 50/69] fix: adjust amount to token's decimals - also fixed how `toTokenUnit` converts values to tokenUnit --- .../Settings/SpendingLimit/NewLimitReview.tsx | 4 +-- .../Settings/SpendingLimit/utils.ts | 25 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitReview.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitReview.tsx index 16b8002425..3580db0bb9 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitReview.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitReview.tsx @@ -19,7 +19,7 @@ import { FooterSection, FooterWrapper, StyledButton, TitleSection } from '.' import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay' import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' import { useStyles } from './style' -import { currentMinutes, fromTokenUnit, SpendingLimitRow, toTokenUnit } from './utils' +import { adjustAmountToToken, currentMinutes, fromTokenUnit, SpendingLimitRow, toTokenUnit } from './utils' interface ReviewSpendingLimitProps { onBack: () => void @@ -151,7 +151,7 @@ const NewLimitReview = ({ onBack, onClose, txToken, values }: ReviewSpendingLimi <AddressInfo address={values.beneficiary} title="Beneficiary" /> </Col> <Col margin="lg"> - <TokenInfo amount={values.amount} title="Amount" token={txToken} /> + <TokenInfo amount={adjustAmountToToken(values.amount, txToken.decimals)} title="Amount" token={txToken} /> {existentSpendingLimit && ( <Text size="lg" color="error"> Previous Amount: {existentSpendingLimit.amount} diff --git a/src/routes/safe/components/Settings/SpendingLimit/utils.ts b/src/routes/safe/components/Settings/SpendingLimit/utils.ts index 3bb5b824fd..a6352fc637 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/utils.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/utils.ts @@ -15,10 +15,29 @@ export const KEYCODES = { export const fromTokenUnit = (amount: string, decimals: string | number): string => new BigNumber(amount).times(`1e-${decimals}`).toFixed() -export const toTokenUnit = (amount: string, decimals: string | number): string => - new BigNumber(amount).times(`1e${decimals}`).toFixed() +export const toTokenUnit = (amount: string, decimals: string | number): string => { + const amountBN = new BigNumber(amount).times(`1e${decimals}`) + const [, amountDecimalPlaces] = amount.split('.') -export const currentMinutes = () => Math.floor(Date.now() / (1000 * 60)) + if (amountDecimalPlaces?.length >= +decimals) { + return amountBN.toFixed(0, BigNumber.ROUND_DOWN) + } + + return amountBN.toFixed() +} + +export const adjustAmountToToken = (amount: string, decimals: string | number): string => { + const amountBN = new BigNumber(amount) + const [, amountDecimalPlaces] = amount.split('.') + + if (amountDecimalPlaces?.length >= 18) { + return amountBN.toFixed(+decimals, BigNumber.ROUND_DOWN) + } + + return amountBN.toFixed() +} + +export const currentMinutes = (): number => Math.floor(Date.now() / (1000 * 60)) export const requestModuleData = (safeAddress: string): Promise<any[]> => { const batch = new web3ReadOnly.BatchRequest() From a42a33c8127f74ff13730a5f4168b3877dba51dd Mon Sep 17 00:00:00 2001 From: fernandomg <fernando.greco@gmail.com> Date: Fri, 21 Aug 2020 15:46:50 -0300 Subject: [PATCH 51/69] add typings and comments to the NewLimitModal component and helper functions --- .../Settings/SpendingLimit/NewLimitModal.tsx | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx index 2ff0de4198..a7392c644f 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx @@ -1,5 +1,7 @@ +import { List } from 'immutable' import React from 'react' import { useSelector } from 'react-redux' + import GnoModal from 'src/components/Modal' import { Token } from 'src/logic/tokens/store/model/token' import NewLimit from 'src/routes/safe/components/Settings/SpendingLimit/NewLimit' @@ -12,15 +14,20 @@ const REVIEW = 'REVIEW' as const type Step = typeof CREATE | typeof REVIEW -type SpendingLimitModalReducerState = { - step: Step - values: Record<string, string> | null - tokens: Token[] - txToken: Token | null +type State = { + step?: Step + values?: Record<string, string> | null + txToken?: Token | null +} + +type Action = { + type: Step + newState?: State + tokens?: List<Token> } -const newSpendingLimitReducer = (state: SpendingLimitModalReducerState, action) => { - const { type, newState } = action +const newLimitModalReducer = (state: State, action: Action) => { + const { type, newState, tokens } = action switch (type) { case CREATE: { @@ -34,26 +41,33 @@ const newSpendingLimitReducer = (state: SpendingLimitModalReducerState, action) return { ...state, ...newState, - txToken: state.tokens.find((token) => token.address === newState.values.token) ?? null, + // we lookup into the list of tokens for the selected token info + txToken: tokens.find((token) => token.address === newState.values.token) ?? null, step: REVIEW, } } } } -const useSpendingLimit = (initialStep: Step) => { +type ActionCallback = (state?: State) => void +type NewLimitModalHook = [State, { create: ActionCallback; review: ActionCallback }] + +const useNewLimitModal = (initialStep: Step): NewLimitModalHook => { + // globally stored tokens const tokens = useSelector(extendedSafeTokensSelector) - const [state, dispatch] = React.useReducer(newSpendingLimitReducer, { + // setup the reducer with initial values + const [state, dispatch] = React.useReducer(newLimitModalReducer, { step: initialStep, values: null, - tokens: tokens ?? [], txToken: null, }) - const create = React.useCallback(() => dispatch({ type: CREATE }), []) - const review = React.useCallback((newState) => dispatch({ type: REVIEW, newState }), []) + // define actions + const create = React.useCallback<ActionCallback>(() => dispatch({ type: CREATE }), []) + const review = React.useCallback<ActionCallback>((newState) => dispatch({ type: REVIEW, newState, tokens }), [tokens]) + // returns state and dispatch return [state, { create, review }] } @@ -65,9 +79,11 @@ interface SpendingLimitModalProps { const NewLimitModal = ({ close, open }: SpendingLimitModalProps): React.ReactElement => { const classes = useStyles() - const [{ step, txToken, values }, { create, review }] = useSpendingLimit(CREATE) + // state and dispatch + const [{ step, txToken, values }, { create, review }] = useNewLimitModal(CREATE) const handleReview = async (values) => { + // if form is valid, we update the state to REVIEW and sets values review({ values }) } From 9acdf05491b5d3ea412019e440a56e37dc8dc9d2 Mon Sep 17 00:00:00 2001 From: fernandomg <fernando.greco@gmail.com> Date: Fri, 21 Aug 2020 16:55:32 -0300 Subject: [PATCH 52/69] refactor: reorganize Modal and related components --- .../Settings/SpendingLimit/Modal/index.tsx | 102 ++++++++++++++++ .../Settings/SpendingLimit/NewLimit.tsx | 109 ------------------ .../SpendingLimit/NewLimitModal/Create.tsx | 96 +++++++++++++++ .../Review.tsx} | 65 +++++------ .../index.tsx} | 19 ++- .../Settings/SpendingLimit/index.tsx | 39 +------ 6 files changed, 237 insertions(+), 193 deletions(-) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/Modal/index.tsx delete mode 100644 src/routes/safe/components/Settings/SpendingLimit/NewLimit.tsx create mode 100644 src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Create.tsx rename src/routes/safe/components/Settings/SpendingLimit/{NewLimitReview.tsx => NewLimitModal/Review.tsx} (82%) rename src/routes/safe/components/Settings/SpendingLimit/{NewLimitModal.tsx => NewLimitModal/index.tsx} (79%) diff --git a/src/routes/safe/components/Settings/SpendingLimit/Modal/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/Modal/index.tsx new file mode 100644 index 0000000000..2837d00ac2 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/Modal/index.tsx @@ -0,0 +1,102 @@ +import { Icon, Text, Title } from '@gnosis.pm/safe-react-components' +import React from 'react' +import styled from 'styled-components' + +import GnoModal from 'src/components/Modal' +import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' + +const TitleSection = styled.div` + display: flex; + justify-content: space-between; + padding: 16px 24px; + border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; +` + +const StyledButton = styled.button` + background: none; + border: none; + padding: 5px; + width: 26px; + height: 26px; + + span { + margin-right: 0; + } + + :hover { + background: ${({ theme }) => theme.colors.separator}; + border-radius: 16px; + cursor: pointer; + } +` + +const FooterSection = styled.div` + border-top: 2px solid ${({ theme }) => theme.colors.separator}; + padding: 16px 24px; +` + +const FooterWrapper = styled.div` + display: flex; + justify-content: space-around; +` + +export interface TopBarProps { + title: string + titleNote?: string + onClose: () => void +} + +const TopBar = ({ title, titleNote, onClose }: TopBarProps): React.ReactElement => ( + <TitleSection> + <Title size="xs" withoutMargin> + {title} + {titleNote && ( + <> + {' '} + <Text size="lg" color="secondaryLight"> + {titleNote} + </Text> + </> + )} + + + + + + +) + +interface FooterProps { + children: React.ReactNodeArray +} + +const Footer = ({ children }: FooterProps): React.ReactElement => ( + + {children} + +) + +export interface ModalProps { + children: React.ReactNode + description: string + handleClose: () => void + open: boolean + title: string +} + +// TODO: this is a potential proposal for `safe-react-components` Modal +// By being able to combine components for better flexibility, this way Buttons ban be part of the form body +const Modal = ({ children, ...props }: ModalProps): React.ReactElement => { + const classes = useStyles() + + return ( + + {children} + + ) +} + +Modal.TopBar = TopBar +Modal.Footer = Footer + +export default Modal diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimit.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimit.tsx deleted file mode 100644 index ef80663aec..0000000000 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimit.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { Button, Icon, Text, Title } from '@gnosis.pm/safe-react-components' -import { Mutator } from 'final-form' -import React from 'react' -import styled from 'styled-components' - -import GnoForm from 'src/components/forms/GnoForm' -import GnoButton from 'src/components/layout/Button' - -import { TitleSection, StyledButton, FooterSection, FooterWrapper } from '.' -import { Amount, Beneficiary, ResetTime, Token } from './FormFields' - -const FormContainer = styled.div` - padding: 24px; - align-items: center; - display: grid; - grid-template-columns: 4fr 1fr; - grid-template-rows: 6fr; - gap: 16px 8px; - grid-template-areas: - 'beneficiaryInput beneficiaryScan' - 'tokenInput .' - 'amountInput .' - 'resetTimeLabel resetTimeLabel' - 'resetTimeToggle resetTimeToggle' - 'resetTimeOption resetTimeOption'; -` - -const YetAnotherButton = styled(GnoButton)` - &.Mui-disabled { - background-color: ${({ theme }) => theme.colors.primary}; - color: ${({ theme }) => theme.colors.white}; - opacity: 0.5; - } -` - -const formMutators: Record> = { - setBeneficiary: (args, state, utils) => { - utils.changeValue(state, 'beneficiary', () => args[0]) - }, -} - -interface NewSpendingLimitProps { - initialValues?: Record - onCancel: () => void - onReview: (values) => void -} - -const canReview = ({ invalid, submitting, dirtyFieldsSinceLastSubmit, values }): boolean => { - return !( - submitting || - invalid || - !values.beneficiary || - (values.token && !values.amount) || - // TODO: review the next validation, as resetTime has a default value, this check looks unnecessary - (values.withResetTime && !values.resetTime) || - !dirtyFieldsSinceLastSubmit - ) -} - -const NewLimit = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): React.ReactElement => ( - <> - - - New Spending Limit{' '} - <Text size="lg" color="secondaryLight"> - 1 of 2 - </Text> - - - - - - - - - {(...args) => ( - <> - - - - - - - - - - - - {/* TODO: replace this with safe-react-components button. This is used as "submit" SRC Button does not triggers submission up until the 2nd click */} - - Review - - - - - )} - - -) - -export default NewLimit diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Create.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Create.tsx new file mode 100644 index 0000000000..d604302727 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Create.tsx @@ -0,0 +1,96 @@ +import { Button } from '@gnosis.pm/safe-react-components' +import { Mutator } from 'final-form' +import React from 'react' +import styled from 'styled-components' + +import GnoForm from 'src/components/forms/GnoForm' +import GnoButton from 'src/components/layout/Button' + +import { Amount, Beneficiary, ResetTime, Token } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields' +import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal' + +const FormContainer = styled.div` + padding: 24px; + align-items: center; + display: grid; + grid-template-columns: 4fr 1fr; + grid-template-rows: 6fr; + gap: 16px 8px; + grid-template-areas: + 'beneficiaryInput beneficiaryScan' + 'tokenInput .' + 'amountInput .' + 'resetTimeLabel resetTimeLabel' + 'resetTimeToggle resetTimeToggle' + 'resetTimeOption resetTimeOption'; +` + +const YetAnotherButton = styled(GnoButton)` + &.Mui-disabled { + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.white}; + opacity: 0.5; + } +` + +const formMutators: Record> = { + setBeneficiary: (args, state, utils) => { + utils.changeValue(state, 'beneficiary', () => args[0]) + }, +} + +interface NewSpendingLimitProps { + initialValues?: Record + onCancel: () => void + onReview: (values) => void +} + +const canReview = ({ + invalid, + submitting, + dirtyFieldsSinceLastSubmit, + values: { beneficiary, token, amount }, +}): boolean => !(submitting || invalid || !beneficiary || !token || !amount || !dirtyFieldsSinceLastSubmit) + +const Create = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): React.ReactElement => { + return ( + <> + + + + {(...args) => { + return ( + <> + + + + + + + + + + + {/* TODO: replace this with safe-react-components button. */} + {/* This is used as "submit" SRC Button does not triggers submission up until the 2nd click */} + + Review + + + + ) + }} + + + ) +} + +export default Create diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitReview.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx similarity index 82% rename from src/routes/safe/components/Settings/SpendingLimit/NewLimitReview.tsx rename to src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 3580db0bb9..2eb22ddf91 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitReview.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -1,4 +1,4 @@ -import { Button, Icon, Text, Title } from '@gnosis.pm/safe-react-components' +import { Button, Text } from '@gnosis.pm/safe-react-components' import { useSnackbar } from 'notistack' import React from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -15,11 +15,17 @@ import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' -import { FooterSection, FooterWrapper, StyledButton, TitleSection } from '.' -import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay' import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' -import { useStyles } from './style' -import { adjustAmountToToken, currentMinutes, fromTokenUnit, SpendingLimitRow, toTokenUnit } from './utils' +import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' +import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal' +import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' +import { + adjustAmountToToken, + currentMinutes, + fromTokenUnit, + SpendingLimitRow, + toTokenUnit, +} from 'src/routes/safe/components/Settings/SpendingLimit/utils' interface ReviewSpendingLimitProps { onBack: () => void @@ -29,7 +35,7 @@ interface ReviewSpendingLimitProps { existentSpendingLimit?: SpendingLimitRow } -const NewLimitReview = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): React.ReactElement => { +const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): React.ReactElement => { const classes = useStyles() const { enqueueSnackbar, closeSnackbar } = useSnackbar() @@ -133,18 +139,7 @@ const NewLimitReview = ({ onBack, onClose, txToken, values }: ReviewSpendingLimi return ( <> - - - New Spending Limit{' '} - <Text size="lg" color="secondaryLight"> - 2 of 2 - </Text> - - - - - - + @@ -176,25 +171,23 @@ const NewLimitReview = ({ onBack, onClose, txToken, values }: ReviewSpendingLimi )} - - - - - - - + + + + + ) } -export default NewLimitReview +export default Review diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx similarity index 79% rename from src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx rename to src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx index a7392c644f..97d4623166 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx @@ -2,12 +2,12 @@ import { List } from 'immutable' import React from 'react' import { useSelector } from 'react-redux' -import GnoModal from 'src/components/Modal' import { Token } from 'src/logic/tokens/store/model/token' -import NewLimit from 'src/routes/safe/components/Settings/SpendingLimit/NewLimit' -import NewLimitReview from 'src/routes/safe/components/Settings/SpendingLimit/NewLimitReview' -import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' +import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal' + +import Create from './Create' +import Review from './Review' const CREATE = 'CREATE' as const const REVIEW = 'REVIEW' as const @@ -77,8 +77,6 @@ interface SpendingLimitModalProps { } const NewLimitModal = ({ close, open }: SpendingLimitModalProps): React.ReactElement => { - const classes = useStyles() - // state and dispatch const [{ step, txToken, values }, { create, review }] = useNewLimitModal(CREATE) @@ -88,16 +86,15 @@ const NewLimitModal = ({ close, open }: SpendingLimitModalProps): React.ReactEle } return ( - - {step === CREATE && } - {step === REVIEW && } - + {step === CREATE && } + {step === REVIEW && } + ) } diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index e45f4b0063..88039ee969 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -11,8 +11,8 @@ import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/con import { getSpendingLimitData, SpendingLimitTable } from './dataFetcher' import LimitsTable from './LimitsTable' -import NewLimitModal from 'src/routes/safe/components/Settings/SpendingLimit/NewLimitModal' -import NewLimitSteps from 'src/routes/safe/components/Settings/SpendingLimit/NewLimitSteps' +import NewLimitModal from './NewLimitModal' +import NewLimitSteps from './NewLimitSteps' import { useStyles } from './style' import { requestAllowancesByDelegatesAndTokens, requestModuleData, requestTokensByDelegate } from './utils' @@ -20,41 +20,6 @@ const InfoText = styled(Text)` margin-top: 16px; ` -export const TitleSection = styled.div` - display: flex; - justify-content: space-between; - padding: 16px 24px; - border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; -` - -export const StyledButton = styled.button` - background: none; - border: none; - padding: 5px; - width: 26px; - height: 26px; - - span { - margin-right: 0; - } - - :hover { - background: ${({ theme }) => theme.colors.separator}; - border-radius: 16px; - cursor: pointer; - } -` - -export const FooterSection = styled.div` - border-top: 2px solid ${({ theme }) => theme.colors.separator}; - padding: 16px 24px; -` - -export const FooterWrapper = styled.div` - display: flex; - justify-content: space-around; -` - const SpendingLimitSettings = (): React.ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) From 6aa4b5956b95eb44b907fc8512e718c3ee94324c Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 21 Aug 2020 19:17:50 -0300 Subject: [PATCH 53/69] refactor: use Modal component for RemoveLimitModal --- .../SpendingLimit/RemoveLimitModal.tsx | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 3170172bfb..c57a804ee5 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -1,11 +1,10 @@ -import { Button, Icon, Title } from '@gnosis.pm/safe-react-components' +import { Button } from '@gnosis.pm/safe-react-components' import { useSnackbar } from 'notistack' import React from 'react' import { useDispatch, useSelector } from 'react-redux' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' -import Modal from 'src/components/Modal' import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' @@ -17,10 +16,10 @@ import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' -import { FooterSection, FooterWrapper, StyledButton, TitleSection } from '.' import { SpendingLimitTable } from './dataFetcher' import { AddressInfo, TokenInfo, ResetTimeInfo } from './InfoDisplay' -import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' +import { RESET_TIME_OPTIONS } from './FormFields/ResetTime' +import Modal from './Modal' import { useStyles } from './style' import { fromTokenUnit } from './utils' @@ -88,21 +87,12 @@ const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitM return ( - - - Remove Spending Limit - - - - - - + @@ -122,16 +112,14 @@ const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitM - - - - - - + + + + ) } From 37daea172659294e4d51ac6c45b2ff919449e3ab Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 21 Aug 2020 19:24:04 -0300 Subject: [PATCH 54/69] refactor: retrieve allowances data from store - also make container grow with the content --- .../Settings/SpendingLimit/index.tsx | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 88039ee969..147d025040 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -6,15 +6,14 @@ import styled from 'styled-components' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' -import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' +import { safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors' +import { grantedSelector } from 'src/routes/safe/container/selector' -import { getSpendingLimitData, SpendingLimitTable } from './dataFetcher' +import { getSpendingLimitData } from './dataFetcher' import LimitsTable from './LimitsTable' import NewLimitModal from './NewLimitModal' import NewLimitSteps from './NewLimitSteps' import { useStyles } from './style' -import { requestAllowancesByDelegatesAndTokens, requestModuleData, requestTokensByDelegate } from './utils' const InfoText = styled(Text)` margin-top: 16px; @@ -23,20 +22,8 @@ const InfoText = styled(Text)` const SpendingLimitSettings = (): React.ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) - const tokens = useSelector(extendedSafeTokensSelector) - - // TODO: Refactor `delegates` for better performance. This is just to verify allowance works - const safeAddress = useSelector(safeParamAddressFromStateSelector) - const [spendingLimitData, setSpendingLimitData] = React.useState() - React.useEffect(() => { - const doRequestData = async () => { - const [, delegates] = await requestModuleData(safeAddress) - const tokensByDelegate = await requestTokensByDelegate(safeAddress, delegates.results) - const allowances = await requestAllowancesByDelegatesAndTokens(safeAddress, tokensByDelegate) - setSpendingLimitData(getSpendingLimitData(allowances)) - } - doRequestData() - }, [safeAddress, tokens]) + const allowances = useSelector(safeSpendingLimitsSelector) + const spendingLimitData = getSpendingLimitData(allowances) const [showNewSpendingLimitModal, setShowNewSpendingLimitModal] = React.useState(false) const openNewSpendingLimitModal = () => { @@ -48,7 +35,7 @@ const SpendingLimitSettings = (): React.ReactElement => { return ( <> - + Spending Limit From 2298533f721a9be2a804497fc6acb67aec5a5d7b Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 21 Aug 2020 20:20:10 -0300 Subject: [PATCH 55/69] refactor: reword "one-time" legend --- .../Settings/SpendingLimit/InfoDisplay/ResetTimeInfo.tsx | 5 +---- .../Settings/SpendingLimit/NewLimitModal/Review.tsx | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/ResetTimeInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/ResetTimeInfo.tsx index 3629cf87db..e12c717411 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/ResetTimeInfo.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/ResetTimeInfo.tsx @@ -19,10 +19,7 @@ const ResetTimeInfo = ({ title, label }: ResetTimeInfoProps): React.ReactElement
) : ( - - {/* TODO: review message */} - One-time spending limit allowance - + One-time spending limit )} diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 2eb22ddf91..b3ce3c1a33 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -135,7 +135,7 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): const previousResetTime = (previousSpendingLimit: SpendingLimit) => RESET_TIME_OPTIONS.find(({ value }) => value === (+previousSpendingLimit.resetTimeMin / 60 / 24).toString()) - ?.label ?? 'One-time spending limit allowance' + ?.label ?? 'One-time spending limit' return ( <> From 0de201bcceb174341bde9c50959481dea8f25119 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 21 Aug 2020 20:48:55 -0300 Subject: [PATCH 56/69] refactor: reorg code and use AddressInfo to display address info --- .../SpendingLimit/InfoDisplay/AddressInfo.tsx | 4 +- .../Settings/SpendingLimit/LimitsTable.tsx | 62 +++++++------------ 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx index ce69009819..90c20e3265 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx @@ -10,10 +10,11 @@ import DataDisplay from './DataDisplay' interface AddressInfoProps { address: string + cut?: number title?: string } -const AddressInfo = ({ address, title }: AddressInfoProps): React.ReactElement => { +const AddressInfo = ({ address, cut, title }: AddressInfoProps): React.ReactElement => { const addressBook = useSelector(getAddressBook) return ( @@ -26,6 +27,7 @@ const AddressInfo = ({ address, title }: AddressInfoProps): React.ReactElement = showIdenticon textSize="lg" network={getNetwork()} + shortenHash={cut} /> ) diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx index e1853b2e29..fade436f54 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx @@ -1,4 +1,4 @@ -import { Button, EthHashInfo, Text } from '@gnosis.pm/safe-react-components' +import { Button, Text } from '@gnosis.pm/safe-react-components' import TableContainer from '@material-ui/core/TableContainer' import cn from 'classnames' import React from 'react' @@ -8,9 +8,6 @@ import styled from 'styled-components' import Row from 'src/components/layout/Row' import { TableCell, TableRow } from 'src/components/layout/Table' import Table from 'src/components/Table' -import { getNetwork } from 'src/config' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromAdbk } from 'src/logic/addressBook/utils' import { Token } from 'src/logic/tokens/store/model/token' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' @@ -26,7 +23,8 @@ import { SPENDING_LIMIT_TABLE_SPENT_ID, SpendingLimitTable, } from './dataFetcher' -import RemoveLimitModal from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal' +import { AddressInfo } from './InfoDisplay' +import RemoveLimitModal from './RemoveLimitModal' import { useStyles } from './style' import { fromTokenUnit } from './utils' @@ -86,6 +84,20 @@ const HumanReadableSpent = ({ spent, amount, tokenAddress }: HumanReadableSpentP ) : null } +const useWidthState = (width: number): number | null => { + const [cut, setCut] = React.useState(null) + + React.useEffect(() => { + if (width <= 1024) { + setCut(4) + } else { + setCut(8) + } + }, [width]) + + return cut +} + interface SpendingLimitTableProps { data?: SpendingLimitTable[] } @@ -93,34 +105,14 @@ interface SpendingLimitTableProps { const LimitsTable = ({ data }: SpendingLimitTableProps): React.ReactElement => { const classes = useStyles() const granted = useSelector(grantedSelector) - const addressBook = useSelector(getAddressBook) const columns = generateColumns() const autoColumns = columns.filter(({ custom }) => !custom) - const [cut, setCut] = React.useState(undefined) const { width } = useWindowDimensions() - React.useEffect(() => { - if (width <= 1024) { - setCut(4) - } else { - setCut(8) - } - }, [width]) + const cut = useWidthState(width) - const [showRemoveSpendingLimitModal, setShowRemoveSpendingLimitModal] = React.useState(false) const [selectedRow, setSelectedRow] = React.useState(null) - const openRemoveSpendingLimitModal = (row: SpendingLimitTable) => { - setSelectedRow(row) - setShowRemoveSpendingLimitModal(true) - } - const closeRemoveSpendingLimitModal = () => { - setShowRemoveSpendingLimitModal(false) - setSelectedRow(null) - } - const handleDeleteSpendingLimit = (row: SpendingLimitTable): void => { - openRemoveSpendingLimitModal(row) - } return ( <> @@ -150,18 +142,8 @@ const LimitsTable = ({ data }: SpendingLimitTableProps): React.ReactElement => { return ( {columnId === SPENDING_LIMIT_TABLE_BENEFICIARY_ID && ( - + )} - {columnId === SPENDING_LIMIT_TABLE_SPENT_ID && } {columnId === SPENDING_LIMIT_TABLE_RESET_TIME_ID && ( {rowElement.relativeTime} @@ -177,7 +159,7 @@ const LimitsTable = ({ data }: SpendingLimitTableProps): React.ReactElement => { iconType="delete" color="error" variant="outlined" - onClick={() => handleDeleteSpendingLimit(row)} + onClick={() => setSelectedRow(row)} data-testid="remove-action" > {null} @@ -190,8 +172,8 @@ const LimitsTable = ({ data }: SpendingLimitTableProps): React.ReactElement => { } - {showRemoveSpendingLimitModal && ( - + {selectedRow !== null && ( + setSelectedRow(null)} spendingLimit={selectedRow} open={true} /> )} ) From de07e5c191c218d84ce2aae9cde7ddfe7fa930c1 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 21 Aug 2020 21:13:38 -0300 Subject: [PATCH 57/69] refactor: reorg LimitsTable and split into components for better readability --- .../LimitsTable/SpentVsAmount.tsx | 78 +++++++++++++++++++ .../index.tsx} | 71 +++-------------- 2 files changed, 88 insertions(+), 61 deletions(-) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx rename src/routes/safe/components/Settings/SpendingLimit/{LimitsTable.tsx => LimitsTable/index.tsx} (64%) diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx new file mode 100644 index 0000000000..231f062c2e --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx @@ -0,0 +1,78 @@ +import { Text } from '@gnosis.pm/safe-react-components' +import React from 'react' +import { useSelector } from 'react-redux' +import styled from 'styled-components' + +import { Token } from 'src/logic/tokens/store/model/token' +import { formatAmount } from 'src/logic/tokens/utils/formatAmount' +import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' +import { fromTokenUnit } from 'src/routes/safe/components/Settings/SpendingLimit/utils' +import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' +import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' + +const StyledImage = styled.img` + width: 32px; + height: 32px; + object-fit: contain; + margin: 0 8px 0 0; +` + +const StyledImageName = styled.div` + display: flex; + align-items: center; +` + +const useSelectedToken = (address: string): Token => { + const tokens = useSelector(extendedSafeTokensSelector) + const [token, setToken] = React.useState(null) + + React.useEffect(() => { + if (tokens) { + const tokenAddress = address === ZERO_ADDRESS ? ETH_ADDRESS : address + setToken(tokens.find((token) => token.address === tokenAddress) ?? null) + } + }, [address, tokens]) + + return token +} + +type FormattedAmountsProps = { amount: string; spent: string; token?: Token } + +type FormattedAmounts = { amount: string; spent: string } + +const useFormattedAmounts = ({ amount, spent, token }: FormattedAmountsProps): FormattedAmounts => { + const [formattedAmounts, setFormattedAmounts] = React.useState(null) + + React.useEffect(() => { + if (token) { + const formattedSpent = formatAmount(fromTokenUnit(spent, token.decimals)).toString() + const formattedAmount = formatAmount(fromTokenUnit(amount, token.decimals)).toString() + setFormattedAmounts({ amount: formattedAmount, spent: formattedSpent }) + } + }, [amount, spent, token]) + + return formattedAmounts +} + +interface SpentVsAmountProps { + amount: string + spent: string + tokenAddress: string +} + +const SpentVsAmount = ({ amount, spent, tokenAddress }: SpentVsAmountProps): React.ReactElement => { + const { width } = useWindowDimensions() + const token = useSelectedToken(tokenAddress) + const spentInfo = useFormattedAmounts({ amount, spent, token }) + + return spentInfo ? ( + + {width > 1024 && } + {`${spentInfo.spent} of ${spentInfo.amount} ${token.symbol}`} + + ) : null +} + +export default SpentVsAmount diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx similarity index 64% rename from src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx rename to src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx index fade436f54..8ea2d0467b 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx @@ -8,13 +8,6 @@ import styled from 'styled-components' import Row from 'src/components/layout/Row' import { TableCell, TableRow } from 'src/components/layout/Table' import Table from 'src/components/Table' -import { Token } from 'src/logic/tokens/store/model/token' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' -import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' -import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' -import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' -import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' import { generateColumns, @@ -22,22 +15,15 @@ import { SPENDING_LIMIT_TABLE_RESET_TIME_ID, SPENDING_LIMIT_TABLE_SPENT_ID, SpendingLimitTable, -} from './dataFetcher' -import { AddressInfo } from './InfoDisplay' -import RemoveLimitModal from './RemoveLimitModal' -import { useStyles } from './style' -import { fromTokenUnit } from './utils' - -const StyledImage = styled.img` - width: 32px; - height: 32px; - object-fit: contain; - margin: 0 8px 0 0; -` -const StyledImageName = styled.div` - display: flex; - align-items: center; -` +} from 'src/routes/safe/components/Settings/SpendingLimit/dataFetcher' +import { AddressInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' +import RemoveLimitModal from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal' +import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' +import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' +import { grantedSelector } from 'src/routes/safe/container/selector' + +import SpentVsAmount from './SpentVsAmount' + const TableActionButton = styled(Button)` background-color: transparent; padding: 0; @@ -46,43 +32,6 @@ const TableActionButton = styled(Button)` background-color: transparent; } ` -type SpentInfo = { - token: Token - spent: string - amount: string -} - -interface HumanReadableSpentProps { - spent: string - amount: string - tokenAddress: string -} - -const HumanReadableSpent = ({ spent, amount, tokenAddress }: HumanReadableSpentProps): React.ReactElement => { - const tokens = useSelector(extendedSafeTokensSelector) - const { width } = useWindowDimensions() - const [spentInfo, setSpentInfo] = React.useState() - - React.useEffect(() => { - if (tokens) { - const safeTokenAddress = tokenAddress === ZERO_ADDRESS ? ETH_ADDRESS : tokenAddress - const token = tokens.find((token) => token.address === safeTokenAddress) - const formattedSpent = formatAmount(fromTokenUnit(spent, token.decimals)).toString() - const formattedAmount = formatAmount(fromTokenUnit(amount, token.decimals)).toString() - - setSpentInfo({ token, spent: formattedSpent, amount: formattedAmount }) - } - }, [amount, spent, tokenAddress, tokens]) - - return spentInfo ? ( - - {width > 1024 && ( - - )} - {`${spentInfo.spent} of ${spentInfo.amount} ${spentInfo.token.symbol}`} - - ) : null -} const useWidthState = (width: number): number | null => { const [cut, setCut] = React.useState(null) @@ -144,7 +93,7 @@ const LimitsTable = ({ data }: SpendingLimitTableProps): React.ReactElement => { {columnId === SPENDING_LIMIT_TABLE_BENEFICIARY_ID && ( )} - {columnId === SPENDING_LIMIT_TABLE_SPENT_ID && } + {columnId === SPENDING_LIMIT_TABLE_SPENT_ID && } {columnId === SPENDING_LIMIT_TABLE_RESET_TIME_ID && ( {rowElement.relativeTime} )} From 81c1dd0b8e78eeb5c89f49e5261962062b66d953 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Sun, 23 Aug 2020 11:24:36 -0300 Subject: [PATCH 58/69] refactor: use `useMemo` whenever is possible - also cleaned dup code, and used `useToken` custom hook --- .../LimitsTable/SpentVsAmount.tsx | 35 +++++-------------- .../SpendingLimit/LimitsTable/index.tsx | 16 +-------- .../SpendingLimit/NewLimitModal/Review.tsx | 18 ++++++---- .../SpendingLimit/RemoveLimitModal.tsx | 14 ++------ .../Settings/SpendingLimit/hooks/useToken.tsx | 21 +++++++++++ .../Settings/SpendingLimit/index.tsx | 2 +- 6 files changed, 45 insertions(+), 61 deletions(-) create mode 100644 src/routes/safe/components/Settings/SpendingLimit/hooks/useToken.tsx diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx index 231f062c2e..ef572c282e 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx @@ -1,16 +1,13 @@ import { Text } from '@gnosis.pm/safe-react-components' import React from 'react' -import { useSelector } from 'react-redux' import styled from 'styled-components' import { Token } from 'src/logic/tokens/store/model/token' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' -import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' -import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' +import useToken from 'src/routes/safe/components/Settings/SpendingLimit/hooks/useToken' import { fromTokenUnit } from 'src/routes/safe/components/Settings/SpendingLimit/utils' import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' -import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' const StyledImage = styled.img` width: 32px; @@ -24,36 +21,20 @@ const StyledImageName = styled.div` align-items: center; ` -const useSelectedToken = (address: string): Token => { - const tokens = useSelector(extendedSafeTokensSelector) - const [token, setToken] = React.useState(null) - - React.useEffect(() => { - if (tokens) { - const tokenAddress = address === ZERO_ADDRESS ? ETH_ADDRESS : address - setToken(tokens.find((token) => token.address === tokenAddress) ?? null) - } - }, [address, tokens]) - - return token -} - type FormattedAmountsProps = { amount: string; spent: string; token?: Token } type FormattedAmounts = { amount: string; spent: string } const useFormattedAmounts = ({ amount, spent, token }: FormattedAmountsProps): FormattedAmounts => { - const [formattedAmounts, setFormattedAmounts] = React.useState(null) - - React.useEffect(() => { + return React.useMemo(() => { if (token) { const formattedSpent = formatAmount(fromTokenUnit(spent, token.decimals)).toString() const formattedAmount = formatAmount(fromTokenUnit(amount, token.decimals)).toString() - setFormattedAmounts({ amount: formattedAmount, spent: formattedSpent }) + return { amount: formattedAmount, spent: formattedSpent } } - }, [amount, spent, token]) - return formattedAmounts + return null + }, [amount, spent, token]) } interface SpentVsAmountProps { @@ -64,12 +45,14 @@ interface SpentVsAmountProps { const SpentVsAmount = ({ amount, spent, tokenAddress }: SpentVsAmountProps): React.ReactElement => { const { width } = useWindowDimensions() - const token = useSelectedToken(tokenAddress) + const showIcon = React.useMemo(() => width > 1024, [width]) + + const token = useToken(tokenAddress) const spentInfo = useFormattedAmounts({ amount, spent, token }) return spentInfo ? ( - {width > 1024 && } + {showIcon && } {`${spentInfo.spent} of ${spentInfo.amount} ${token.symbol}`} ) : null diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx index 8ea2d0467b..ea6e673cb7 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx @@ -33,20 +33,6 @@ const TableActionButton = styled(Button)` } ` -const useWidthState = (width: number): number | null => { - const [cut, setCut] = React.useState(null) - - React.useEffect(() => { - if (width <= 1024) { - setCut(4) - } else { - setCut(8) - } - }, [width]) - - return cut -} - interface SpendingLimitTableProps { data?: SpendingLimitTable[] } @@ -59,7 +45,7 @@ const LimitsTable = ({ data }: SpendingLimitTableProps): React.ReactElement => { const autoColumns = columns.filter(({ custom }) => !custom) const { width } = useWindowDimensions() - const cut = useWidthState(width) + const cut = React.useMemo(() => (width <= 1024 ? 4 : 8), [width]) const [selectedRow, setSelectedRow] = React.useState(null) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index b3ce3c1a33..14f22e2512 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -129,13 +129,17 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): .catch(console.error) } - const resetTimeLabel = values.withResetTime - ? RESET_TIME_OPTIONS.find(({ value }) => value === values.resetTime)?.label - : '' + const resetTimeLabel = React.useMemo( + () => (values.withResetTime ? RESET_TIME_OPTIONS.find(({ value }) => value === values.resetTime)?.label : ''), + [values.resetTime, values.withResetTime], + ) - const previousResetTime = (previousSpendingLimit: SpendingLimit) => - RESET_TIME_OPTIONS.find(({ value }) => value === (+previousSpendingLimit.resetTimeMin / 60 / 24).toString()) - ?.label ?? 'One-time spending limit' + const previousResetTime = React.useMemo( + () => + RESET_TIME_OPTIONS.find(({ value }) => value === (+existentSpendingLimit.resetTimeMin / 60 / 24).toString()) + ?.label ?? 'One-time spending limit', + [existentSpendingLimit.resetTimeMin], + ) return ( <> @@ -158,7 +162,7 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): {existentSpendingLimit && ( - Previous Reset Time: {previousResetTime(existentSpendingLimit)} + Previous Reset Time: {previousResetTime} )} diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index c57a804ee5..461f948ce3 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -8,11 +8,10 @@ import Col from 'src/components/layout/Col' import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import { Token } from 'src/logic/tokens/store/model/token' import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { getWeb3 } from 'src/logic/wallets/getWeb3' -import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' +import useToken from 'src/routes/safe/components/Settings/SpendingLimit/hooks/useToken' import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' @@ -32,16 +31,7 @@ interface RemoveSpendingLimitModalProps { const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitModalProps): React.ReactElement => { const classes = useStyles() - const tokens = useSelector(extendedSafeTokensSelector) - const [tokenInfo, setTokenInfo] = React.useState() - React.useEffect(() => { - if (tokens) { - const tokenAddress = - spendingLimit.spent.tokenAddress === ZERO_ADDRESS ? ETH_ADDRESS : spendingLimit.spent.tokenAddress - const foundToken = tokens.find((token) => token.address === tokenAddress) - setTokenInfo(foundToken) - } - }, [spendingLimit.spent.tokenAddress, tokens]) + const tokenInfo = useToken(spendingLimit.spent.tokenAddress) const safeAddress = useSelector(safeParamAddressFromStateSelector) const { enqueueSnackbar, closeSnackbar } = useSnackbar() diff --git a/src/routes/safe/components/Settings/SpendingLimit/hooks/useToken.tsx b/src/routes/safe/components/Settings/SpendingLimit/hooks/useToken.tsx new file mode 100644 index 0000000000..27b4b38098 --- /dev/null +++ b/src/routes/safe/components/Settings/SpendingLimit/hooks/useToken.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { Token } from 'src/logic/tokens/store/model/token' +import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' + +const useToken = (address: string): Token => { + const tokens = useSelector(extendedSafeTokensSelector) + + return React.useMemo(() => { + if (tokens) { + const tokenAddress = address === ZERO_ADDRESS ? ETH_ADDRESS : address + return tokens.find((token) => token.address === tokenAddress) ?? null + } + + return null + }, [address, tokens]) +} + +export default useToken diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index 147d025040..a53277879d 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -35,7 +35,7 @@ const SpendingLimitSettings = (): React.ReactElement => { return ( <> - + Spending Limit From 28347f8d03c361fa7affb7c997bf727337738319 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 24 Aug 2020 10:30:55 -0300 Subject: [PATCH 59/69] refactor: move `Allowance.json` into `src/logic/contracts/artifacts` directory - also reorganized imports in affected files --- .../contracts/artifacts}/AllowanceModule.json | 0 src/logic/contracts/safeContracts.ts | 24 ++++++++++--------- .../SpendingLimit/RemoveLimitModal.tsx | 4 ++-- .../Settings/SpendingLimit/utils.ts | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) rename src/{utils => logic/contracts/artifacts}/AllowanceModule.json (100%) diff --git a/src/utils/AllowanceModule.json b/src/logic/contracts/artifacts/AllowanceModule.json similarity index 100% rename from src/utils/AllowanceModule.json rename to src/logic/contracts/artifacts/AllowanceModule.json diff --git a/src/logic/contracts/safeContracts.ts b/src/logic/contracts/safeContracts.ts index b533212044..164a563d13 100644 --- a/src/logic/contracts/safeContracts.ts +++ b/src/logic/contracts/safeContracts.ts @@ -1,19 +1,21 @@ -import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' -import { AbiItem } from 'web3-utils' -import contract from 'truffle-contract' -import Web3 from 'web3' -import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json' import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json' -import SpendingLimitModule from 'src/utils/AllowanceModule.json' -import { ensureOnce } from 'src/utils/singleton' +import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json' import memoize from 'lodash.memoize' -import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3' -import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions' -import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import contract from 'truffle-contract' +import Web3 from 'web3' +import { AbiItem } from 'web3-utils' + import { isProxyCode } from 'src/logic/contracts/historicProxyCode' -import { GnosisSafeProxyFactory } from 'src/types/contracts/GnosisSafeProxyFactory.d'; +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions' +import { getNetworkIdFrom, getWeb3 } from 'src/logic/wallets/getWeb3' import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' +import { GnosisSafeProxyFactory } from 'src/types/contracts/GnosisSafeProxyFactory.d' +import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' +import { ensureOnce } from 'src/utils/singleton' + +import SpendingLimitModule from './artifacts/AllowanceModule.json' export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001' export const MULTI_SEND_ADDRESS = '0x8d29be29923b68abfdd21e541b9374737b49cdad' diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 461f948ce3..3ce050992d 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -5,6 +5,7 @@ import { useDispatch, useSelector } from 'react-redux' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' +import SpendingLimitModule from 'src/logic/contracts/artifacts/AllowanceModule.json' import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' @@ -12,12 +13,11 @@ import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { getWeb3 } from 'src/logic/wallets/getWeb3' import useToken from 'src/routes/safe/components/Settings/SpendingLimit/hooks/useToken' -import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import { SpendingLimitTable } from './dataFetcher' -import { AddressInfo, TokenInfo, ResetTimeInfo } from './InfoDisplay' import { RESET_TIME_OPTIONS } from './FormFields/ResetTime' +import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay' import Modal from './Modal' import { useStyles } from './style' import { fromTokenUnit } from './utils' diff --git a/src/routes/safe/components/Settings/SpendingLimit/utils.ts b/src/routes/safe/components/Settings/SpendingLimit/utils.ts index a6352fc637..66ce0290ad 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/utils.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/utils.ts @@ -1,10 +1,10 @@ import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import { BigNumber } from 'bignumber.js' +import SpendingLimitModule from 'src/logic/contracts/artifacts/AllowanceModule.json' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' import { web3ReadOnly } from 'src/logic/wallets/getWeb3' -import SpendingLimitModule from 'src/utils/AllowanceModule.json' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' export const KEYCODES = { From 330e1deb9f7fce1051533daf7df2236e3a2411c9 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 24 Aug 2020 10:45:23 -0300 Subject: [PATCH 60/69] refactor: moved `dataFetcher` into `/LimitsTable` - also reorganized imports in affected files --- .../SpendingLimit/{ => LimitsTable}/dataFetcher.ts | 2 +- .../Settings/SpendingLimit/LimitsTable/index.tsx | 13 ++++++------- .../Settings/SpendingLimit/RemoveLimitModal.tsx | 2 +- .../components/Settings/SpendingLimit/index.tsx | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) rename src/routes/safe/components/Settings/SpendingLimit/{ => LimitsTable}/dataFetcher.ts (96%) diff --git a/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/dataFetcher.ts similarity index 96% rename from src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts rename to src/routes/safe/components/Settings/SpendingLimit/LimitsTable/dataFetcher.ts index 2564289a09..dd1cfb1d9c 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/dataFetcher.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/dataFetcher.ts @@ -3,7 +3,7 @@ import { List } from 'immutable' import { TableColumn } from 'src/components/Table/types.d' -import { SpendingLimitRow } from './utils' +import { SpendingLimitRow } from 'src/routes/safe/components/Settings/SpendingLimit/utils' export const SPENDING_LIMIT_TABLE_BENEFICIARY_ID = 'beneficiary' export const SPENDING_LIMIT_TABLE_SPENT_ID = 'spent' diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx index ea6e673cb7..df2967cd2c 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx @@ -8,6 +8,11 @@ import styled from 'styled-components' import Row from 'src/components/layout/Row' import { TableCell, TableRow } from 'src/components/layout/Table' import Table from 'src/components/Table' +import { AddressInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' +import RemoveLimitModal from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal' +import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' +import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' +import { grantedSelector } from 'src/routes/safe/container/selector' import { generateColumns, @@ -15,13 +20,7 @@ import { SPENDING_LIMIT_TABLE_RESET_TIME_ID, SPENDING_LIMIT_TABLE_SPENT_ID, SpendingLimitTable, -} from 'src/routes/safe/components/Settings/SpendingLimit/dataFetcher' -import { AddressInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' -import RemoveLimitModal from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal' -import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' -import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' -import { grantedSelector } from 'src/routes/safe/container/selector' - +} from './dataFetcher' import SpentVsAmount from './SpentVsAmount' const TableActionButton = styled(Button)` diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 3ce050992d..853f524588 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -15,9 +15,9 @@ import { getWeb3 } from 'src/logic/wallets/getWeb3' import useToken from 'src/routes/safe/components/Settings/SpendingLimit/hooks/useToken' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' -import { SpendingLimitTable } from './dataFetcher' import { RESET_TIME_OPTIONS } from './FormFields/ResetTime' import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay' +import { SpendingLimitTable } from './LimitsTable/dataFetcher' import Modal from './Modal' import { useStyles } from './style' import { fromTokenUnit } from './utils' diff --git a/src/routes/safe/components/Settings/SpendingLimit/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/index.tsx index a53277879d..b11eb350dc 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/index.tsx @@ -9,8 +9,8 @@ import Row from 'src/components/layout/Row' import { safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors' import { grantedSelector } from 'src/routes/safe/container/selector' -import { getSpendingLimitData } from './dataFetcher' import LimitsTable from './LimitsTable' +import { getSpendingLimitData } from './LimitsTable/dataFetcher' import NewLimitModal from './NewLimitModal' import NewLimitSteps from './NewLimitSteps' import { useStyles } from './style' From bdee93959a5a783cbbee7192da4b7bd72f16e921 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 24 Aug 2020 11:56:56 -0300 Subject: [PATCH 61/69] refactor: move away from Apps' `sendTransactions` method. Using custom multiSend txs - Added notifications messages (this is extremely verbose, not sure if I should only use the Settings messages) --- .../notifications/notificationBuilder.tsx | 30 +++++++++++ src/logic/notifications/notificationTypes.ts | 50 +++++++++++++++++++ .../safe/transactions/notifiedTransactions.ts | 2 + .../SpendingLimit/NewLimitModal/Review.tsx | 31 ++++++++---- .../SpendingLimit/RemoveLimitModal.tsx | 2 +- 5 files changed, 103 insertions(+), 12 deletions(-) diff --git a/src/logic/notifications/notificationBuilder.tsx b/src/logic/notifications/notificationBuilder.tsx index a3bd0978a3..3bcf7d6778 100644 --- a/src/logic/notifications/notificationBuilder.tsx +++ b/src/logic/notifications/notificationBuilder.tsx @@ -99,6 +99,28 @@ const settingsChangeTxNotificationsQueue = { afterExecutionError: NOTIFICATIONS.SETTINGS_CHANGE_FAILED_MSG, } +const newSpendingLimitTxNotificationsQueue = { + beforeExecution: NOTIFICATIONS.SIGN_NEW_SPENDING_LIMIT_MSG, + pendingExecution: NOTIFICATIONS.NEW_SPENDING_LIMIT_PENDING_MSG, + afterRejection: NOTIFICATIONS.NEW_SPENDING_LIMIT_REJECTED_MSG, + afterExecution: { + noMoreConfirmationsNeeded: NOTIFICATIONS.NEW_SPENDING_LIMIT_EXECUTED_MSG, + moreConfirmationsNeeded: NOTIFICATIONS.NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG, + }, + afterExecutionError: NOTIFICATIONS.NEW_SPENDING_LIMIT_FAILED_MSG, +} + +const removeSpendingLimitTxNotificationsQueue = { + beforeExecution: NOTIFICATIONS.SIGN_REMOVE_SPENDING_LIMIT_MSG, + pendingExecution: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_PENDING_MSG, + afterRejection: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_REJECTED_MSG, + afterExecution: { + noMoreConfirmationsNeeded: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_EXECUTED_MSG, + moreConfirmationsNeeded: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG, + }, + afterExecutionError: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_FAILED_MSG, +} + const defaultNotificationsQueue = { beforeExecution: NOTIFICATIONS.SIGN_TX_MSG, pendingExecution: NOTIFICATIONS.TX_PENDING_MSG, @@ -166,6 +188,14 @@ export const getNotificationsFromTxType: any = (txType, origin) => { notificationsQueue = settingsChangeTxNotificationsQueue break } + case TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX: { + notificationsQueue = newSpendingLimitTxNotificationsQueue + break + } + case TX_NOTIFICATION_TYPES.REMOVE_SPENDING_LIMIT_TX: { + notificationsQueue = removeSpendingLimitTxNotificationsQueue + break + } case TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX: { notificationsQueue = safeNameChangeNotificationsQueue break diff --git a/src/logic/notifications/notificationTypes.ts b/src/logic/notifications/notificationTypes.ts index 30c31eed0c..6ca65bdd38 100644 --- a/src/logic/notifications/notificationTypes.ts +++ b/src/logic/notifications/notificationTypes.ts @@ -147,6 +147,56 @@ export const NOTIFICATIONS = { options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, }, + // Spending Limit + SIGN_NEW_SPENDING_LIMIT_MSG: { + message: 'Please sign the new Spending Limit tx', + options: { variant: INFO, persist: true }, + }, + NEW_SPENDING_LIMIT_PENDING_MSG: { + message: 'New Spending Limit tx pending', + options: { variant: INFO, persist: true }, + }, + NEW_SPENDING_LIMIT_REJECTED_MSG: { + message: 'New Spending Limit tx rejected', + options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, + }, + NEW_SPENDING_LIMIT_EXECUTED_MSG: { + message: 'New Spending Limit tx successfully executed', + options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, + }, + NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: { + message: 'New Spending Limit tx successfully created. More confirmations needed to execute', + options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, + }, + NEW_SPENDING_LIMIT_FAILED_MSG: { + message: 'New Spending Limit tx failed', + options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, + }, + SIGN_REMOVE_SPENDING_LIMIT_MSG: { + message: 'Please sign the remove Spending Limit tx', + options: { variant: INFO, persist: true }, + }, + REMOVE_SPENDING_LIMIT_PENDING_MSG: { + message: 'Remove Spending Limit tx pending', + options: { variant: INFO, persist: true }, + }, + REMOVE_SPENDING_LIMIT_REJECTED_MSG: { + message: 'Remove Spending Limit tx rejected', + options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, + }, + REMOVE_SPENDING_LIMIT_EXECUTED_MSG: { + message: 'Remove Spending Limit tx successfully executed', + options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, + }, + REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: { + message: 'Remove Spending Limit tx successfully created. More confirmations needed to execute', + options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, + }, + REMOVE_SPENDING_LIMIT_FAILED_MSG: { + message: 'Remove Spending Limit tx failed', + options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, + }, + // Network RINKEBY_VERSION_MSG: { message: "Rinkeby Version: Don't send Mainnet assets to this Safe", diff --git a/src/logic/safe/transactions/notifiedTransactions.ts b/src/logic/safe/transactions/notifiedTransactions.ts index 83700fd98e..79867eddd1 100644 --- a/src/logic/safe/transactions/notifiedTransactions.ts +++ b/src/logic/safe/transactions/notifiedTransactions.ts @@ -4,6 +4,8 @@ export const TX_NOTIFICATION_TYPES: any = { CANCELLATION_TX: 'CANCELLATION_TX', WAITING_TX: 'WAITING_TX', SETTINGS_CHANGE_TX: 'SETTINGS_CHANGE_TX', + NEW_SPENDING_LIMIT_TX: 'NEW_SPENDING_LIMIT_TX', + REMOVE_SPENDING_LIMIT_TX: 'REMOVE_SPENDING_LIMIT_TX', SAFE_NAME_CHANGE_TX: 'SAFE_NAME_CHANGE_TX', OWNER_NAME_CHANGE_TX: 'OWNER_NAME_CHANGE_TX', ADDRESSBOOK_NEW_ENTRY: 'ADDRESSBOOK_NEW_ENTRY', diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 14f22e2512..2f17b1d563 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -6,13 +6,20 @@ import { useDispatch, useSelector } from 'react-redux' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' -import { getGnosisSafeInstanceAt, getSpendingLimitContract } from 'src/logic/contracts/safeContracts' +import { + getGnosisSafeInstanceAt, + getSpendingLimitContract, + MULTI_SEND_ADDRESS, +} from 'src/logic/contracts/safeContracts' +import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { SpendingLimit } from 'src/logic/safe/store/models/safe' import { safeParamAddressFromStateSelector, safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors' +import { DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' +import { getEncodedMultiSendCallData } from 'src/logic/safe/utils/upgradeSafe' import { Token } from 'src/logic/tokens/store/model/token' import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import sendTransactions from 'src/routes/safe/components/Apps/sendTransactions' +import { getWeb3 } from 'src/logic/wallets/getWeb3' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' @@ -117,16 +124,18 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): .encodeABI(), }) - await sendTransactions( - dispatch, - safeAddress, - transactions, - enqueueSnackbar, - closeSnackbar, - JSON.stringify({ name: 'Spending Limit', message: 'New Allowance' }), + dispatch( + createTransaction({ + safeAddress, + to: MULTI_SEND_ADDRESS, + valueInWei: '0', + txData: getEncodedMultiSendCallData(transactions, getWeb3()), + notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX, + enqueueSnackbar, + closeSnackbar, + operation: DELEGATE_CALL, + }), ) - .then(onClose) - .catch(console.error) } const resetTimeLabel = React.useMemo( diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 853f524588..57e9644f24 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -59,7 +59,7 @@ const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitM to: SPENDING_LIMIT_MODULE_ADDRESS, valueInWei: '0', txData, - notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, + notifiedTransaction: TX_NOTIFICATION_TYPES.REMOVE_SPENDING_LIMIT_TX, enqueueSnackbar, closeSnackbar, }), From d5bec404f96bdea602a42d4e177134b7f59dc279 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 24 Aug 2020 11:57:51 -0300 Subject: [PATCH 62/69] refactor: undo `previousResetTime` as a memoized value --- .../Settings/SpendingLimit/NewLimitModal/Review.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 2f17b1d563..66e420b450 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -143,12 +143,9 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): [values.resetTime, values.withResetTime], ) - const previousResetTime = React.useMemo( - () => - RESET_TIME_OPTIONS.find(({ value }) => value === (+existentSpendingLimit.resetTimeMin / 60 / 24).toString()) - ?.label ?? 'One-time spending limit', - [existentSpendingLimit.resetTimeMin], - ) + const previousResetTime = (existentSpendingLimit: SpendingLimit) => + RESET_TIME_OPTIONS.find(({ value }) => value === (+existentSpendingLimit.resetTimeMin / 60 / 24).toString()) + ?.label ?? 'One-time spending limit' return ( <> @@ -171,7 +168,7 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): {existentSpendingLimit && ( - Previous Reset Time: {previousResetTime} + Previous Reset Time: {previousResetTime(existentSpendingLimit)} )} From b7f60837bdd01c6365bd7046ca874ef76bb72eaf Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 24 Aug 2020 12:26:06 -0300 Subject: [PATCH 63/69] feature: use `multiSend` txs only when needed --- .../SpendingLimit/NewLimitModal/Review.tsx | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 66e420b450..62f2ab0e9d 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -110,7 +110,7 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): // prepare the setAllowance tx const startTime = currentMinutes() - 30 - transactions.push({ + const setAllowanceTx = { to: SPENDING_LIMIT_MODULE_ADDRESS, value: 0, data: spendingLimitContract.methods @@ -122,20 +122,37 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): values.withResetTime ? startTime : 0, ) .encodeABI(), - }) - - dispatch( - createTransaction({ - safeAddress, - to: MULTI_SEND_ADDRESS, - valueInWei: '0', - txData: getEncodedMultiSendCallData(transactions, getWeb3()), - notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX, - enqueueSnackbar, - closeSnackbar, - operation: DELEGATE_CALL, - }), - ) + } + + // if there's no tx for enable module or adding a delegate, then we avoid using multiSend Tx + if (transactions.length === 0) { + dispatch( + createTransaction({ + safeAddress, + to: SPENDING_LIMIT_MODULE_ADDRESS, + valueInWei: '0', + txData: setAllowanceTx.data, + notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX, + enqueueSnackbar, + closeSnackbar, + }), + ) + } else { + transactions.push(setAllowanceTx) + + dispatch( + createTransaction({ + safeAddress, + to: MULTI_SEND_ADDRESS, + valueInWei: '0', + txData: getEncodedMultiSendCallData(transactions, getWeb3()), + notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX, + enqueueSnackbar, + closeSnackbar, + operation: DELEGATE_CALL, + }), + ) + } } const resetTimeLabel = React.useMemo( From 2fab251fc918bb94a8a2f921da58b9116af4f304 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 24 Aug 2020 12:29:29 -0300 Subject: [PATCH 64/69] fix: add support for ETH allowance modification --- .../components/Settings/SpendingLimit/NewLimitModal/Review.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 62f2ab0e9d..f1466cb561 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -61,7 +61,7 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): const currentDelegate = spendingLimits.find( ({ delegate, token }) => delegate.toLowerCase() === values.beneficiary.toLowerCase() && - token.toLowerCase() === values.token.toLowerCase(), + token.toLowerCase() === (values.token === ETH_ADDRESS ? ZERO_ADDRESS : values.token.toLowerCase()), ) // let the user know that is about to replace an existent allowance From a35be6d77db715b81dd2a5889b6a9b9dab02c95f Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 24 Aug 2020 12:37:07 -0300 Subject: [PATCH 65/69] fix: support non existent `spendingLimits` --- .../Settings/SpendingLimit/NewLimitModal/Review.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index f1466cb561..62d438f4e4 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -58,7 +58,7 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): React.useEffect(() => { const checkExistence = async () => { // if `delegate` already exist, check what tokens were delegated to the _beneficiary_ `getTokens(safe, delegate)` - const currentDelegate = spendingLimits.find( + const currentDelegate = spendingLimits?.find( ({ delegate, token }) => delegate.toLowerCase() === values.beneficiary.toLowerCase() && token.toLowerCase() === (values.token === ETH_ADDRESS ? ZERO_ADDRESS : values.token.toLowerCase()), @@ -97,7 +97,7 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): // does `delegate` already exist? (`getDelegates`, previously queried to build the table with allowances (??)) // ^ - shall we rely on this or query the list of delegates once again? const isDelegateAlreadyAdded = - spendingLimits.some(({ delegate }) => delegate.toLowerCase() === values?.beneficiary.toLowerCase()) ?? false + spendingLimits?.some(({ delegate }) => delegate.toLowerCase() === values?.beneficiary.toLowerCase()) ?? false // if `delegate` does not exist, add it by calling `addDelegate(beneficiary)` if (!isDelegateAlreadyAdded && values?.beneficiary) { From 5550ca9c842a54928146979d265dfaf9f8730996 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 25 Aug 2020 09:27:34 -0300 Subject: [PATCH 66/69] fix: set fixed length for addresses --- .../Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx | 2 +- .../Settings/SpendingLimit/LimitsTable/index.tsx | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx index 90c20e3265..1abeb5960a 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx @@ -14,7 +14,7 @@ interface AddressInfoProps { title?: string } -const AddressInfo = ({ address, cut, title }: AddressInfoProps): React.ReactElement => { +const AddressInfo = ({ address, cut = 4, title }: AddressInfoProps): React.ReactElement => { const addressBook = useSelector(getAddressBook) return ( diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx index df2967cd2c..7502088996 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/index.tsx @@ -11,7 +11,6 @@ import Table from 'src/components/Table' import { AddressInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' import RemoveLimitModal from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal' import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' -import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' import { grantedSelector } from 'src/routes/safe/container/selector' import { @@ -43,9 +42,6 @@ const LimitsTable = ({ data }: SpendingLimitTableProps): React.ReactElement => { const columns = generateColumns() const autoColumns = columns.filter(({ custom }) => !custom) - const { width } = useWindowDimensions() - const cut = React.useMemo(() => (width <= 1024 ? 4 : 8), [width]) - const [selectedRow, setSelectedRow] = React.useState(null) return ( @@ -75,9 +71,7 @@ const LimitsTable = ({ data }: SpendingLimitTableProps): React.ReactElement => { return ( - {columnId === SPENDING_LIMIT_TABLE_BENEFICIARY_ID && ( - - )} + {columnId === SPENDING_LIMIT_TABLE_BENEFICIARY_ID && } {columnId === SPENDING_LIMIT_TABLE_SPENT_ID && } {columnId === SPENDING_LIMIT_TABLE_RESET_TIME_ID && ( {rowElement.relativeTime} From e8c8c34847b85fa5c7c99255773ca72f89c89dde Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 26 Aug 2020 15:31:59 +0200 Subject: [PATCH 67/69] Update React import --- src/components/Modal/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 94ff2e8981..a758c041e7 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -1,7 +1,7 @@ import Modal from '@material-ui/core/Modal' import { makeStyles, createStyles } from '@material-ui/core/styles' import cn from 'classnames' -import * as React from 'react' +import React from 'react' import { sm } from 'src/theme/variables' From 073a065025b2cc33e610bbbc5fae15247a919775 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 26 Aug 2020 12:25:13 -0300 Subject: [PATCH 68/69] fix: avoid `UNKNOWN` as contact name --- .../Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx index 1abeb5960a..eae0f7016d 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx @@ -3,8 +3,7 @@ import React from 'react' import { useSelector } from 'react-redux' import { getNetwork } from 'src/config' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors' import DataDisplay from './DataDisplay' @@ -15,13 +14,13 @@ interface AddressInfoProps { } const AddressInfo = ({ address, cut = 4, title }: AddressInfoProps): React.ReactElement => { - const addressBook = useSelector(getAddressBook) + const name = useSelector((state) => getNameFromAddressBook(state, address)) return ( Date: Fri, 28 Aug 2020 20:20:56 -0300 Subject: [PATCH 69/69] fixes after merge --- src/logic/notifications/notificationTypes.ts | 12 ++++++++++++ src/logic/safe/store/actions/fetchSafe.ts | 1 + src/routes/safe/components/Layout/index.tsx | 0 .../SpendingLimit/LimitsTable/SpentVsAmount.tsx | 2 +- .../Settings/SpendingLimit/NewLimitModal/Review.tsx | 6 ------ .../Settings/SpendingLimit/RemoveLimitModal.tsx | 4 ---- 6 files changed, 14 insertions(+), 11 deletions(-) delete mode 100644 src/routes/safe/components/Layout/index.tsx diff --git a/src/logic/notifications/notificationTypes.ts b/src/logic/notifications/notificationTypes.ts index eb4cc3942f..9a21310b71 100644 --- a/src/logic/notifications/notificationTypes.ts +++ b/src/logic/notifications/notificationTypes.ts @@ -46,6 +46,18 @@ const NOTIFICATION_IDS = { 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', + SIGN_NEW_SPENDING_LIMIT_MSG: 'SIGN_NEW_SPENDING_LIMIT_MSG', + NEW_SPENDING_LIMIT_PENDING_MSG: 'NEW_SPENDING_LIMIT_PENDING_MSG', + NEW_SPENDING_LIMIT_REJECTED_MSG: 'NEW_SPENDING_LIMIT_REJECTED_MSG', + NEW_SPENDING_LIMIT_EXECUTED_MSG: 'NEW_SPENDING_LIMIT_EXECUTED_MSG', + NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: 'NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG', + NEW_SPENDING_LIMIT_FAILED_MSG: 'NEW_SPENDING_LIMIT_FAILED_MSG', + SIGN_REMOVE_SPENDING_LIMIT_MSG: 'SIGN_REMOVE_SPENDING_LIMIT_MSG', + REMOVE_SPENDING_LIMIT_PENDING_MSG: 'REMOVE_SPENDING_LIMIT_PENDING_MSG', + REMOVE_SPENDING_LIMIT_REJECTED_MSG: 'REMOVE_SPENDING_LIMIT_REJECTED_MSG', + REMOVE_SPENDING_LIMIT_EXECUTED_MSG: 'REMOVE_SPENDING_LIMIT_EXECUTED_MSG', + REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: 'REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG', + REMOVE_SPENDING_LIMIT_FAILED_MSG: 'REMOVE_SPENDING_LIMIT_FAILED_MSG', RINKEBY_VERSION_MSG: 'RINKEBY_VERSION_MSG', WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG', ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS', diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index 9cdc1e6055..22fc29635c 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -89,6 +89,7 @@ export const buildSafe = async ( currentVersion, needsUpdate, featuresEnabled, + spendingLimits: null, balances: Map(), latestIncomingTxBlock: null, activeAssets: Set(), diff --git a/src/routes/safe/components/Layout/index.tsx b/src/routes/safe/components/Layout/index.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx index ef572c282e..bcac4da0c4 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/LimitsTable/SpentVsAmount.tsx @@ -7,7 +7,7 @@ import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import useToken from 'src/routes/safe/components/Settings/SpendingLimit/hooks/useToken' import { fromTokenUnit } from 'src/routes/safe/components/Settings/SpendingLimit/utils' -import { useWindowDimensions } from 'src/routes/safe/container/hooks/useWindowDimensions' +import { useWindowDimensions } from 'src/logic/hooks/useWindowDimensions' const StyledImage = styled.img` width: 32px; diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 62d438f4e4..ecdef8370f 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -1,5 +1,4 @@ import { Button, Text } from '@gnosis.pm/safe-react-components' -import { useSnackbar } from 'notistack' import React from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -45,7 +44,6 @@ interface ReviewSpendingLimitProps { const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): React.ReactElement => { const classes = useStyles() - const { enqueueSnackbar, closeSnackbar } = useSnackbar() const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) @@ -133,8 +131,6 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): valueInWei: '0', txData: setAllowanceTx.data, notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX, - enqueueSnackbar, - closeSnackbar, }), ) } else { @@ -147,8 +143,6 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): valueInWei: '0', txData: getEncodedMultiSendCallData(transactions, getWeb3()), notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX, - enqueueSnackbar, - closeSnackbar, operation: DELEGATE_CALL, }), ) diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 57e9644f24..0a1cd7bd1d 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -1,5 +1,4 @@ import { Button } from '@gnosis.pm/safe-react-components' -import { useSnackbar } from 'notistack' import React from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -34,7 +33,6 @@ const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitM const tokenInfo = useToken(spendingLimit.spent.tokenAddress) const safeAddress = useSelector(safeParamAddressFromStateSelector) - const { enqueueSnackbar, closeSnackbar } = useSnackbar() const dispatch = useDispatch() const removeSelectedSpendingLimit = async (): Promise => { @@ -60,8 +58,6 @@ const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitM valueInWei: '0', txData, notifiedTransaction: TX_NOTIFICATION_TYPES.REMOVE_SPENDING_LIMIT_TX, - enqueueSnackbar, - closeSnackbar, }), ) } catch (e) {