Merge branch 'development' into address-book-v2

This commit is contained in:
Daniel Sanchez 2021-05-18 17:56:33 +02:00
commit b59dd8ab87
33 changed files with 876 additions and 1335 deletions

View File

@ -163,19 +163,19 @@
"@gnosis.pm/safe-contracts": "1.1.1-dev.2", "@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#0e4fcd6", "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#0e4fcd6",
"@gnosis.pm/util-contracts": "2.0.6", "@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid-singleton": "5.49.0", "@ledgerhq/hw-transport-node-hid-singleton": "5.51.1",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.11.0", "@material-ui/icons": "^4.11.0",
"@material-ui/lab": "4.0.0-alpha.57", "@material-ui/lab": "4.0.0-alpha.57",
"@openzeppelin/contracts": "3.1.0", "@openzeppelin/contracts": "3.1.0",
"@sentry/react": "^6.2.1", "@sentry/react": "^6.3.5",
"@sentry/tracing": "^6.2.1", "@sentry/tracing": "^6.3.5",
"@truffle/contract": "^4.3.0", "@truffle/contract": "^4.3.0",
"@unstoppabledomains/resolution": "^1.17.0", "@unstoppabledomains/resolution": "^1.17.0",
"async-sema": "^3.1.0", "async-sema": "^3.1.0",
"axios": "0.21.1", "axios": "0.21.1",
"bignumber.js": "9.0.1", "bignumber.js": "9.0.1",
"bnc-onboard": "~1.22.0", "bnc-onboard": "~1.25.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"connected-react-router": "6.8.0", "connected-react-router": "6.8.0",
"currency-flags": "2.1.2", "currency-flags": "2.1.2",
@ -240,21 +240,22 @@
"@storybook/preset-create-react-app": "^3.1.5", "@storybook/preset-create-react-app": "^3.1.5",
"@storybook/react": "^5.3.19", "@storybook/react": "^5.3.19",
"@testing-library/jest-dom": "^5.11.10", "@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6", "@testing-library/react": "^11.2.7",
"@typechain/web3-v1": "^2.2.0", "@typechain/web3-v1": "^2.2.0",
"@types/history": "4.6.2", "@types/history": "4.6.2",
"@types/jest": "^26.0.22", "@types/jest": "^26.0.22",
"@types/js-cookie": "^2.2.6",
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/lodash.memoize": "^4.1.6", "@types/lodash.memoize": "^4.1.6",
"@types/node": "^14.14.37", "@types/node": "^14.14.45",
"@types/react": "^16.14.5", "@types/react": "^16.14.5",
"@types/react-dom": "^16.9.12", "@types/react-dom": "^16.9.13",
"@types/react-redux": "^7.1.11", "@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6", "@types/react-router-dom": "^5.1.6",
"@types/redux-actions": "^2.6.1", "@types/redux-actions": "^2.6.1",
"@types/styled-components": "^5.1.9", "@types/styled-components": "^5.1.9",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.22.0", "@typescript-eslint/parser": "^4.23.0",
"concurrently": "^6.0.0", "concurrently": "^6.0.0",
"coveralls": "^3.1.0", "coveralls": "^3.1.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -263,21 +264,22 @@
"electron": "^9.4.0", "electron": "^9.4.0",
"electron-builder": "22.10.5", "electron-builder": "22.10.5",
"electron-notarize": "1.0.0", "electron-notarize": "1.0.0",
"eslint": "^7.17.0", "eslint": "^7.26.0",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.23.0",
"eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.21.5", "eslint-plugin-react": "^7.23.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "~4.3.8", "husky": "~4.3.8",
"lint-staged": "^10.5.2", "lint-staged": "^10.5.2",
"patch-package": "^6.4.6", "patch-package": "^6.4.7",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.2.0", "prettier": "^2.2.0",
"redux-mock-store": "^1.5.4", "redux-mock-store": "^1.5.4",
"sass": "^1.32.0", "sass": "^1.32.0",
"typechain": "^4.0.0", "typechain": "^4.0.0",
"typescript": "4.2.3", "typescript": "4.2.4",
"wait-on": "^5.3.0" "wait-on": "^5.3.0"
} }
} }

View File

@ -27,7 +27,7 @@ What you need to install globally:
yarn global add truffle ganache-cli yarn global add truffle ganache-cli
``` ```
We use [yarn](https://yarnpkg.com) in our infrastacture, so we decided to go with yarn in the README We use [yarn](https://yarnpkg.com) in our infrastructure, so we decided to go with yarn in the README
### Installing and running ### Installing and running
@ -146,7 +146,7 @@ Please read [CONTRIBUTING.md](https://gist.github.com/PurpleBooth/b24679402957c6
## Versioning ## Versioning
We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/gnosis/gnosis-team-safe/tags). We use [SemVer](https://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/gnosis/gnosis-team-safe/tags).
## License ## License

View File

@ -10,7 +10,7 @@ import { openCookieBanner } from 'src/logic/cookies/store/actions/openCookieBann
import { cookieBannerOpen } from 'src/logic/cookies/store/selectors' import { cookieBannerOpen } from 'src/logic/cookies/store/selectors'
import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils' import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils'
import { mainFontFamily, md, primary, screenSm } from 'src/theme/variables' import { mainFontFamily, md, primary, screenSm } from 'src/theme/variables'
import { loadGoogleAnalytics } from 'src/utils/googleAnalytics' import { loadGoogleAnalytics, removeCookies } from 'src/utils/googleAnalytics'
import { closeIntercom, isIntercomLoaded, loadIntercom } from 'src/utils/intercom' import { closeIntercom, isIntercomLoaded, loadIntercom } from 'src/utils/intercom'
import AlertRedIcon from './assets/alert-red.svg' import AlertRedIcon from './assets/alert-red.svg'
import IntercomIcon from './assets/intercom.png' import IntercomIcon from './assets/intercom.png'
@ -160,6 +160,11 @@ const CookiesBanner = (): ReactElement => {
await saveCookie(COOKIES_KEY, newState, expDays) await saveCookie(COOKIES_KEY, newState, expDays)
setShowAnalytics(localAnalytics) setShowAnalytics(localAnalytics)
setShowIntercom(localIntercom) setShowIntercom(localIntercom)
if (!localAnalytics) {
removeCookies()
}
if (!localIntercom && isIntercomLoaded()) { if (!localIntercom && isIntercomLoaded()) {
closeIntercom() closeIntercom()
} }

View File

@ -1,22 +1,22 @@
import { Text } from '@gnosis.pm/safe-react-components' import { EthHashInfo } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import styled from 'styled-components'
const Wrapper = styled.div` type Props = {
display: flex; address: string
align-items: center; iconUrl?: string
` iconUrlFallback?: string
const Icon = styled.img` text?: string
max-width: 15px; }
max-height: 15px;
margin-right: 9px;
`
type Props = { iconUrl: string | null | undefined; text?: string } export const CustomIconText = ({ address, iconUrl, text, iconUrlFallback }: Props): ReactElement => (
<EthHashInfo
export const CustomIconText = ({ iconUrl, text }: Props): ReactElement => ( hash={address}
<Wrapper> showHash={false}
{iconUrl && <Icon alt={text} src={iconUrl} />} avatarSize="sm"
{text && <Text size="xl">{text}</Text>} showAvatar
</Wrapper> customAvatar={iconUrl || undefined}
customAvatarFallback={iconUrlFallback}
name={text}
textSize="xl"
/>
) )

View File

@ -62,9 +62,6 @@ const AddressInput = ({
} catch (err) { } catch (err) {
console.error('Failed to resolve address for ENS name: ', err) console.error('Failed to resolve address for ENS name: ', err)
} }
} else {
const formattedAddress = checksumAddress(address)
fieldMutator(formattedAddress)
} }
}} }}
</OnChange> </OnChange>

View File

@ -130,7 +130,7 @@ describe('Forms > Validators', () => {
}) })
describe('mustBeEthereumAddress validator', () => { describe('mustBeEthereumAddress validator', () => {
const MUST_BE_ETH_ADDRESS_ERR_MSG = 'Input must be a valid Ethereum address, ENS or Unstoppable domain' const MUST_BE_ETH_ADDRESS_ERR_MSG = 'Must be a valid address, ENS or Unstoppable domain'
it('Returns undefined for a valid ethereum address', async () => { it('Returns undefined for a valid ethereum address', async () => {
expect(await mustBeEthereumAddress('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe')).toBeUndefined() expect(await mustBeEthereumAddress('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe')).toBeUndefined()

View File

@ -68,7 +68,7 @@ export const mustBeEthereumAddress = memoize(
const startsWith0x = address?.startsWith('0x') const startsWith0x = address?.startsWith('0x')
const isAddress = getWeb3().utils.isAddress(address) const isAddress = getWeb3().utils.isAddress(address)
const errorMessage = `Input must be a valid Ethereum address${ const errorMessage = `Must be a valid address${
isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : '' isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : ''
}` }`

View File

@ -4,9 +4,10 @@ import { getNetworkName } from 'src/config'
const PREFIX = `v1_${getNetworkName()}` const PREFIX = `v1_${getNetworkName()}`
export const loadFromCookie = async (key) => { export const loadFromCookie = async (key: string, withoutPrefix = false): Promise<undefined | Record<string, any>> => {
const prefix = withoutPrefix ? '' : `${PREFIX}__`
try { try {
const stringifiedValue = await Cookies.get(`${PREFIX}__${key}`) const stringifiedValue = await Cookies.get(`${prefix}${key}`)
if (stringifiedValue === null || stringifiedValue === undefined) { if (stringifiedValue === null || stringifiedValue === undefined) {
return undefined return undefined
} }
@ -18,7 +19,7 @@ export const loadFromCookie = async (key) => {
} }
} }
export const saveCookie = async (key, value, expirationDays) => { export const saveCookie = async (key: string, value: Record<string, any>, expirationDays: number): Promise<void> => {
try { try {
const stringifiedValue = JSON.stringify(value) const stringifiedValue = JSON.stringify(value)
const expiration = expirationDays ? { expires: expirationDays } : undefined const expiration = expirationDays ? { expires: expirationDays } : undefined
@ -27,3 +28,5 @@ export const saveCookie = async (key, value, expirationDays) => {
console.error(`Failed to save ${key} in cookies:`, err) console.error(`Failed to save ${key} in cookies:`, err)
} }
} }
export const removeCookie = (key: string, path: string, domain: string): void => Cookies.remove(key, { path, domain })

View File

@ -136,7 +136,7 @@ type BaseCustom = {
toInfo?: AddressInfo toInfo?: AddressInfo
} }
type Custom = BaseCustom & { export type Custom = BaseCustom & {
methodName: string | null methodName: string | null
} }

View File

@ -28,22 +28,34 @@ export const appUrlResolver = createDecorator({
}, },
}) })
export const AppInfoUpdater = ({ onAppInfo }: { onAppInfo: (appInfo: SafeApp) => void }): null => { type AppInfoUpdaterProps = {
onAppInfo: (appInfo: SafeApp) => void
onLoading: (isLoading: boolean) => void
}
export const AppInfoUpdater = ({ onAppInfo, onLoading }: AppInfoUpdaterProps): null => {
const { const {
input: { value: appUrl }, input: { value: appUrl },
} = useField('appUrl', { subscription: { value: true } }) } = useField('appUrl', { subscription: { value: true } })
const debouncedValue = useDebounce(appUrl, 500) const debouncedValue = useDebounce(appUrl, 500)
React.useEffect(() => { React.useEffect(() => {
const updateAppInfo = async () => { const updateAppInfo = async () => {
const appInfo = await getAppInfoFromUrl(debouncedValue) try {
onAppInfo({ ...appInfo }) onLoading(true)
const appInfo = await getAppInfoFromUrl(debouncedValue)
onAppInfo({ ...appInfo })
onLoading(false)
} catch (error) {
onLoading(false)
}
} }
if (isValidURL(debouncedValue)) { if (isValidURL(debouncedValue)) {
updateAppInfo() updateAppInfo()
} }
}, [debouncedValue, onAppInfo]) }, [debouncedValue, onAppInfo, onLoading])
return null return null
} }
@ -55,7 +67,15 @@ const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => {
const validate = !visited?.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList)) const validate = !visited?.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList))
return ( return (
<Field label="App URL" name="appUrl" placeholder="App URL" type="text" component={TextField} validate={validate} /> <Field
label="App URL"
name="appUrl"
placeholder="App URL"
type="text"
component={TextField}
validate={validate}
autoComplete="off"
/>
) )
} }

View File

@ -1,4 +1,4 @@
import { TextField } from '@gnosis.pm/safe-react-components' import { TextField, Loader } from '@gnosis.pm/safe-react-components'
import React, { useState, ReactElement } from 'react' import React, { useState, ReactElement } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -24,6 +24,7 @@ const StyledTextFileAppName = styled(TextField)`
` `
const AppInfo = styled.div` const AppInfo = styled.div`
display: flex;
margin: 36px 0 24px 0; margin: 36px 0 24px 0;
img { img {
@ -41,6 +42,18 @@ const AppDocsInfo = styled.div`
} }
` `
const WrapperLoader = styled.div`
height: 55px;
width: 65px;
display: flex;
align-items: center;
justify-content: center;
`
const StyledLoader = styled(Loader)`
margin-right: 15px;
`
interface AddAppFormValues { interface AddAppFormValues {
appUrl: string appUrl: string
agreementAccepted: boolean agreementAccepted: boolean
@ -62,6 +75,7 @@ const AddApp = ({ appList, closeModal }: AddAppProps): ReactElement => {
const [appInfo, setAppInfo] = useState<SafeApp>(APP_INFO) const [appInfo, setAppInfo] = useState<SafeApp>(APP_INFO)
const history = useHistory() const history = useHistory()
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
const [isLoading, setIsLoading] = useState(false)
const handleSubmit = () => { const handleSubmit = () => {
const newAppList = [ const newAppList = [
@ -93,14 +107,26 @@ const AddApp = ({ appList, closeModal }: AddAppProps): ReactElement => {
<Icon size="sm" type="externalLink" color="primary" /> <Icon size="sm" type="externalLink" color="primary" />
</Link> </Link>
</AppDocsInfo> </AppDocsInfo>
<AppUrl appList={appList} /> <AppUrl appList={appList} />
{/* Fetch app from url and return a SafeApp */} {/* Fetch app from url and return a SafeApp */}
<AppInfoUpdater onAppInfo={setAppInfo} /> <AppInfoUpdater onAppInfo={setAppInfo} onLoading={setIsLoading} />
<AppInfo> <AppInfo>
<Img alt="Token image" height={55} src={appInfo.iconUrl} /> {isLoading ? (
<StyledTextFileAppName label="App name" readOnly value={appInfo.name} onChange={() => {}} /> <WrapperLoader>
<StyledLoader size="sm" />
</WrapperLoader>
) : (
<Img alt="Token image" width={55} src={appInfo.iconUrl} />
)}
<StyledTextFileAppName
label="App name"
readOnly
value={isLoading ? 'Loading...' : appInfo.name}
onChange={() => {}}
/>
</AppInfo> </AppInfo>
<AppAgreement /> <AppAgreement />

View File

@ -9,7 +9,7 @@ import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@
import { safeEthBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { safeEthBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
import { grantedSelector } from 'src/routes/safe/container/selector' import { grantedSelector } from 'src/routes/safe/container/selector'
import { getNetworkName, getTxServiceUrl } from 'src/config' import { getNetworkId, getNetworkName, getTxServiceUrl } from 'src/config'
import { SAFELIST_ADDRESS } from 'src/routes/routes' import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { isSameURL } from 'src/utils/url' import { isSameURL } from 'src/utils/url'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
@ -71,6 +71,7 @@ type Props = {
} }
const NETWORK_NAME = getNetworkName() const NETWORK_NAME = getNetworkName()
const NETWORK_ID = getNetworkId()
const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = { const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = {
isOpen: false, isOpen: false,
@ -163,6 +164,7 @@ const AppFrame = ({ appUrl }: Props): ReactElement => {
communicator?.on('getSafeInfo', () => ({ communicator?.on('getSafeInfo', () => ({
safeAddress, safeAddress,
network: NETWORK_NAME, network: NETWORK_NAME,
chainId: NETWORK_ID,
})) }))
communicator?.on('rpcCall', async (msg) => { communicator?.on('rpcCall', async (msg) => {

View File

@ -91,7 +91,7 @@ const ChooseTxType = ({
className={classes.firstButton} className={classes.firstButton}
color="primary" color="primary"
minHeight={52} minHeight={52}
minWidth={260} minWidth={240}
onClick={() => setActiveScreen('sendFunds')} onClick={() => setActiveScreen('sendFunds')}
variant="contained" variant="contained"
testId="modal-send-funds-btn" testId="modal-send-funds-btn"
@ -104,7 +104,7 @@ const ChooseTxType = ({
className={classes.firstButton} className={classes.firstButton}
color="primary" color="primary"
minHeight={52} minHeight={52}
minWidth={260} minWidth={240}
onClick={() => setActiveScreen('sendCollectible')} onClick={() => setActiveScreen('sendCollectible')}
variant="contained" variant="contained"
testId="modal-send-collectible-btn" testId="modal-send-collectible-btn"
@ -122,7 +122,7 @@ const ChooseTxType = ({
color="primary" color="primary"
disabled={disableContractInteraction} disabled={disableContractInteraction}
minHeight={52} minHeight={52}
minWidth={260} minWidth={240}
onClick={() => setActiveScreen('contractInteraction')} onClick={() => setActiveScreen('contractInteraction')}
variant="outlined" variant="outlined"
testId="modal-contract-interaction-btn" testId="modal-contract-interaction-btn"

View File

@ -1,5 +1,5 @@
import { createStyles, makeStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { lg, md, sm } from 'src/theme/variables' import { lg, md, sm, xs } from 'src/theme/variables'
export const useStyles = makeStyles( export const useStyles = makeStyles(
createStyles({ createStyles({
@ -7,10 +7,11 @@ export const useStyles = makeStyles(
padding: `${md} ${lg}`, padding: `${md} ${lg}`,
justifyContent: 'space-between', justifyContent: 'space-between',
boxSizing: 'border-box', boxSizing: 'border-box',
maxHeight: '75px', height: '74px',
}, },
manage: { manage: {
fontSize: lg, fontSize: lg,
marginTop: `${xs}`,
}, },
disclaimer: { disclaimer: {
marginBottom: `-${md}`, marginBottom: `-${md}`,
@ -24,17 +25,13 @@ export const useStyles = makeStyles(
closeIcon: { closeIcon: {
height: '35px', height: '35px',
width: '35px', width: '35px',
marginBottom: `-${xs}`,
}, },
buttonColumn: { buttonColumn: {
margin: '16px 0 44px 0', margin: '52px 0 44px 0',
'& > button': {
fontSize: md,
fontFamily: 'Averta',
},
}, },
firstButton: { firstButton: {
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)', marginBottom: 12,
marginBottom: 15,
}, },
iconSmall: { iconSmall: {
fontSize: 16, fontSize: 16,

View File

@ -1,4 +1,4 @@
import { Loader, Text, theme, Title } from '@gnosis.pm/safe-react-components' import { Text, theme, Title } from '@gnosis.pm/safe-react-components'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -28,16 +28,6 @@ const NoModuleLegend = (): React.ReactElement => (
</InfoText> </InfoText>
) )
const LoadingModules = (): React.ReactElement => {
const classes = useStyles()
return (
<Block className={classes.container}>
<Loader size="md" />
</Block>
)
}
export const Advanced = (): React.ReactElement => { export const Advanced = (): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const nonce = useSelector(safeNonceSelector) const nonce = useSelector(safeNonceSelector)
@ -83,13 +73,7 @@ export const Advanced = (): React.ReactElement => {
. .
</InfoText> </InfoText>
{!moduleData ? ( {!moduleData || !moduleData.length ? <NoModuleLegend /> : <ModulesTable moduleData={moduleData} />}
<NoModuleLegend />
) : moduleData?.length === 0 ? (
<LoadingModules />
) : (
<ModulesTable moduleData={moduleData} />
)}
</Block> </Block>
</> </>
) )

View File

@ -116,9 +116,16 @@ export const AddOwnerModal = ({ isOpen, onClose }: Props): React.ReactElement =>
title="Add owner to Safe" title="Add owner to Safe"
> >
<> <>
{activeScreen === 'selectOwner' && <OwnerForm onClose={onClose} onSubmit={ownerSubmitted} />} {activeScreen === 'selectOwner' && (
<OwnerForm initialValues={values} onClose={onClose} onSubmit={ownerSubmitted} />
)}
{activeScreen === 'selectThreshold' && ( {activeScreen === 'selectThreshold' && (
<ThresholdForm onClickBack={onClickBack} onClose={onClose} onSubmit={thresholdSubmitted} /> <ThresholdForm
onClickBack={onClickBack}
initialValues={{ threshold: values.threshold }}
onClose={onClose}
onSubmit={thresholdSubmitted}
/>
)} )}
{activeScreen === 'reviewAddOwner' && ( {activeScreen === 'reviewAddOwner' && (
<ReviewAddOwner onClickBack={onClickBack} onClose={onClose} onSubmit={onAddOwner} values={values} /> <ReviewAddOwner onClickBack={onClickBack} onClose={onClose} onSubmit={onAddOwner} values={values} />

View File

@ -26,6 +26,8 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { safeOwnersAddressesListSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { safeOwnersAddressesListSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { OwnerValues } from '../..'
export const ADD_OWNER_NAME_INPUT_TEST_ID = 'add-owner-name-input' export const ADD_OWNER_NAME_INPUT_TEST_ID = 'add-owner-name-input'
export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid' export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid'
export const ADD_OWNER_NEXT_BTN_TEST_ID = 'add-owner-next-btn' export const ADD_OWNER_NEXT_BTN_TEST_ID = 'add-owner-next-btn'
@ -41,9 +43,10 @@ const useStyles = makeStyles(styles)
type OwnerFormProps = { type OwnerFormProps = {
onClose: () => void onClose: () => void
onSubmit: (values) => void onSubmit: (values) => void
initialValues?: OwnerValues
} }
export const OwnerForm = ({ onClose, onSubmit }: OwnerFormProps): React.ReactElement => { export const OwnerForm = ({ onClose, onSubmit, initialValues }: OwnerFormProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const handleSubmit = (values) => { const handleSubmit = (values) => {
onSubmit(values) onSubmit(values)
@ -65,7 +68,14 @@ export const OwnerForm = ({ onClose, onSubmit }: OwnerFormProps): React.ReactEle
</IconButton> </IconButton>
</Row> </Row>
<Hairline /> <Hairline />
<GnoForm formMutators={formMutators} onSubmit={handleSubmit}> <GnoForm
formMutators={formMutators}
initialValues={{
ownerName: initialValues?.ownerName,
ownerAddress: initialValues?.ownerAddress,
}}
onSubmit={handleSubmit}
>
{(...args) => { {(...args) => {
const mutators = args[3] const mutators = args[3]

View File

@ -27,13 +27,18 @@ type SubmitProps = {
threshold: number threshold: number
} }
type ThresholdValues = {
threshold: string
}
type Props = { type Props = {
onClickBack: () => void onClickBack: () => void
onClose: () => void onClose: () => void
onSubmit: (values: SubmitProps) => void onSubmit: (values: SubmitProps) => void
initialValues: ThresholdValues
} }
export const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactElement => { export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: Props): ReactElement => {
const classes = useStyles() const classes = useStyles()
const threshold = useSelector(safeThresholdSelector) as number const threshold = useSelector(safeThresholdSelector) as number
const owners = useSelector(safeOwnersSelector) const owners = useSelector(safeOwnersSelector)
@ -54,7 +59,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactE
</IconButton> </IconButton>
</Row> </Row>
<Hairline /> <Hairline />
<GnoForm initialValues={{ threshold: threshold.toString() }} onSubmit={handleSubmit}> <GnoForm initialValues={{ threshold: initialValues.threshold || threshold.toString() }} onSubmit={handleSubmit}>
{() => ( {() => (
<> <>
<Block className={classes.formContainer}> <Block className={classes.formContainer}>

View File

@ -117,7 +117,12 @@ export const RemoveOwnerModal = ({
<CheckOwner onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} /> <CheckOwner onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
)} )}
{activeScreen === 'selectThreshold' && ( {activeScreen === 'selectThreshold' && (
<ThresholdForm onClickBack={onClickBack} onClose={onClose} onSubmit={thresholdSubmitted} /> <ThresholdForm
onClickBack={onClickBack}
initialValues={{ threshold: values.threshold }}
onClose={onClose}
onSubmit={thresholdSubmitted}
/>
)} )}
{activeScreen === 'reviewRemoveOwner' && ( {activeScreen === 'reviewRemoveOwner' && (
<ReviewRemoveOwnerModal <ReviewRemoveOwnerModal

View File

@ -24,13 +24,18 @@ export const REMOVE_OWNER_THRESHOLD_NEXT_BTN_TEST_ID = 'remove-owner-threshold-n
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
type ThresholdValues = {
threshold: string
}
type Props = { type Props = {
onClickBack: () => void onClickBack: () => void
onClose: () => void onClose: () => void
onSubmit: (txParameters: TxParameters) => void onSubmit: (txParameters: TxParameters) => void
initialValues: ThresholdValues
} }
export const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactElement => { export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: Props): ReactElement => {
const classes = useStyles() const classes = useStyles()
const owners = useSelector(safeOwnersSelector) const owners = useSelector(safeOwnersSelector)
const threshold = useSelector(safeThresholdSelector) as number const threshold = useSelector(safeThresholdSelector) as number
@ -51,7 +56,10 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactE
</IconButton> </IconButton>
</Row> </Row>
<Hairline /> <Hairline />
<GnoForm initialValues={{ threshold: defaultThreshold.toString() }} onSubmit={handleSubmit}> <GnoForm
initialValues={{ threshold: initialValues.threshold || defaultThreshold.toString() }}
onSubmit={handleSubmit}
>
{() => { {() => {
const numOptions = owners && owners.size > 1 ? owners.size - 1 : 1 const numOptions = owners && owners.size > 1 ? owners.size - 1 : 1

View File

@ -17,7 +17,7 @@ import { OwnerForm } from 'src/routes/safe/components/Settings/ManageOwners/Repl
import { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review' import { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
type OwnerValues = { export type OwnerValues = {
newOwnerAddress: string newOwnerAddress: string
newOwnerName: string newOwnerName: string
} }
@ -131,7 +131,13 @@ export const ReplaceOwnerModal = ({
> >
<> <>
{activeScreen === 'checkOwner' && ( {activeScreen === 'checkOwner' && (
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} /> <OwnerForm
onClose={onClose}
onSubmit={ownerSubmitted}
initialValues={values}
ownerAddress={ownerAddress}
ownerName={ownerName}
/>
)} )}
{activeScreen === 'reviewReplaceOwner' && ( {activeScreen === 'reviewReplaceOwner' && (
<ReviewReplaceOwnerModal <ReviewReplaceOwnerModal

View File

@ -32,6 +32,8 @@ export const REPLACE_OWNER_NAME_INPUT_TEST_ID = 'replace-owner-name-input'
export const REPLACE_OWNER_ADDRESS_INPUT_TEST_ID = 'replace-owner-address-testid' export const REPLACE_OWNER_ADDRESS_INPUT_TEST_ID = 'replace-owner-address-testid'
export const REPLACE_OWNER_NEXT_BTN_TEST_ID = 'replace-owner-next-btn' export const REPLACE_OWNER_NEXT_BTN_TEST_ID = 'replace-owner-next-btn'
import { OwnerValues } from '../..'
const formMutators = { const formMutators = {
setOwnerAddress: (args, state, utils) => { setOwnerAddress: (args, state, utils) => {
utils.changeValue(state, 'ownerAddress', () => args[0]) utils.changeValue(state, 'ownerAddress', () => args[0])
@ -50,9 +52,16 @@ type OwnerFormProps = {
onSubmit: (values: NewOwnerProps) => void onSubmit: (values: NewOwnerProps) => void
ownerAddress: string ownerAddress: string
ownerName: string ownerName: string
initialValues?: OwnerValues
} }
export const OwnerForm = ({ onClose, onSubmit, ownerAddress, ownerName }: OwnerFormProps): ReactElement => { export const OwnerForm = ({
onClose,
onSubmit,
ownerAddress,
ownerName,
initialValues,
}: OwnerFormProps): ReactElement => {
const classes = useStyles() const classes = useStyles()
const handleSubmit = (values: NewOwnerProps) => { const handleSubmit = (values: NewOwnerProps) => {
@ -75,7 +84,14 @@ export const OwnerForm = ({ onClose, onSubmit, ownerAddress, ownerName }: OwnerF
</IconButton> </IconButton>
</Row> </Row>
<Hairline /> <Hairline />
<GnoForm formMutators={formMutators} onSubmit={handleSubmit}> <GnoForm
formMutators={formMutators}
onSubmit={handleSubmit}
initialValues={{
ownerName: initialValues?.newOwnerName,
ownerAddress: initialValues?.newOwnerAddress,
}}
>
{(...args) => { {(...args) => {
const mutators = args[3] const mutators = args[3]

View File

@ -4,6 +4,7 @@ import React, { ReactElement } from 'react'
import { useField } from 'react-final-form' import { useField } from 'react-final-form'
import styled from 'styled-components' import styled from 'styled-components'
import { getNetworkName } from 'src/config'
import { Field } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/Amount' import { Field } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/Amount'
// TODO: propose refactor in safe-react-components based on this requirements // TODO: propose refactor in safe-react-components based on this requirements
@ -81,17 +82,30 @@ const ResetTimeOptions = styled.div`
grid-area: resetTimeOption; grid-area: resetTimeOption;
` `
export const RESET_TIME_OPTIONS = [ const RESET_TIME_OPTIONS = [
{ label: '1 day', value: '1' }, { label: '1 day', value: '1440' }, // 1 day x 24h x 60min
{ label: '1 week', value: '7' }, { label: '1 week', value: '10080' }, // 7 days x 24h x 60min
{ label: '1 month', value: '30' }, { label: '1 month', value: '43200' }, // 30 days x 24h x 60min
] ]
const RINKEBY_RESET_TIME_OPTIONS = [
{ label: '5 minutes', value: '5' },
{ label: '30 minutes', value: '30' },
{ label: '1 hour', value: '60' },
]
export const getResetTimeOptions = (): RadioButtonOption[] => {
const currentNetwork = getNetworkName().toLowerCase()
return currentNetwork !== 'rinkeby' ? RESET_TIME_OPTIONS : RINKEBY_RESET_TIME_OPTIONS
}
const ResetTime = (): ReactElement => { const ResetTime = (): ReactElement => {
const { const {
input: { value: withResetTime }, input: { value: withResetTime },
} = useField('withResetTime', { subscription: { value: true } }) } = useField('withResetTime', { subscription: { value: true } })
const resetTimeOptions = getResetTimeOptions()
const switchExplanation = withResetTime ? 'choose reset time period' : 'one time' const switchExplanation = withResetTime ? 'choose reset time period' : 'one time'
return ( return (
@ -104,11 +118,7 @@ const ResetTime = (): ReactElement => {
</ResetTimeToggle> </ResetTimeToggle>
{withResetTime && ( {withResetTime && (
<ResetTimeOptions> <ResetTimeOptions>
<SafeRadioButtons <SafeRadioButtons groupName="resetTime" initialValue={resetTimeOptions[0].value} options={resetTimeOptions} />
groupName="resetTime"
initialValue={RESET_TIME_OPTIONS[0].value}
options={RESET_TIME_OPTIONS}
/>
</ResetTimeOptions> </ResetTimeOptions>
)} )}
</> </>

View File

@ -20,7 +20,7 @@ import { MultiSendTx } from 'src/logic/safe/utils/upgradeSafe'
import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { sameAddress } from 'src/logic/wallets/ethAddresses' import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' import { getResetTimeOptions } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime'
import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay'
import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style' import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style'
import { safeParamAddressFromStateSelector, safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors' import { safeParamAddressFromStateSelector, safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors'
@ -104,7 +104,7 @@ const calculateSpendingLimitsTxData = (
beneficiary: values.beneficiary, beneficiary: values.beneficiary,
token: values.token, token: values.token,
spendingLimitInWei: toTokenUnit(values.amount, txToken.decimals), spendingLimitInWei: toTokenUnit(values.amount, txToken.decimals),
resetTimeMin: values.withResetTime ? +values.resetTime * 60 * 24 : 0, resetTimeMin: values.withResetTime ? +values.resetTime : 0,
resetBaseMin: values.withResetTime ? startTime : 0, resetBaseMin: values.withResetTime ? startTime : 0,
} }
@ -209,13 +209,13 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
} }
const resetTimeLabel = useMemo( const resetTimeLabel = useMemo(
() => (values.withResetTime ? RESET_TIME_OPTIONS.find(({ value }) => value === values.resetTime)?.label : ''), () => (values.withResetTime ? getResetTimeOptions().find(({ value }) => value === values.resetTime)?.label : ''),
[values.resetTime, values.withResetTime], [values.resetTime, values.withResetTime],
) )
const previousResetTime = (existentSpendingLimit: SpendingLimit) => const previousResetTime = (existentSpendingLimit: SpendingLimit) =>
RESET_TIME_OPTIONS.find(({ value }) => value === (+existentSpendingLimit.resetTimeMin / 60 / 24).toString()) getResetTimeOptions().find(({ value }) => value === (+existentSpendingLimit.resetTimeMin).toString())?.label ??
?.label ?? 'One-time spending limit' 'One-time spending limit'
const closeEditModalCallback = (txParameters: TxParameters) => { const closeEditModalCallback = (txParameters: TxParameters) => {
const oldGasPrice = Number(gasPriceFormatted) const oldGasPrice = Number(gasPriceFormatted)

View File

@ -18,7 +18,7 @@ import { TxParametersDetail } from 'src/routes/safe/components/Transactions/help
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants'
import { RESET_TIME_OPTIONS } from './FormFields/ResetTime' import { getResetTimeOptions } from './FormFields/ResetTime'
import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay' import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay'
import { SpendingLimitTable } from './LimitsTable/dataFetcher' import { SpendingLimitTable } from './LimitsTable/dataFetcher'
import { useStyles } from './style' import { useStyles } from './style'
@ -91,7 +91,7 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
} }
const resetTimeLabel = const resetTimeLabel =
RESET_TIME_OPTIONS.find(({ value }) => +value === +spendingLimit.resetTime.resetTimeMin / 24 / 60)?.label ?? '' getResetTimeOptions().find(({ value }) => +value === +spendingLimit.resetTime.resetTimeMin)?.label ?? ''
const closeEditModalCallback = (txParameters: TxParameters) => { const closeEditModalCallback = (txParameters: TxParameters) => {
const oldGasPrice = Number(gasPriceFormatted) const oldGasPrice = Number(gasPriceFormatted)

View File

@ -1,9 +1,8 @@
import { EthHashInfo } from '@gnosis.pm/safe-react-components' import { EthHashInfo } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { useSelector } from 'react-redux'
import { getExplorerInfo } from 'src/config'
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors' import { getExplorerInfo } from 'src/config'
import { useKnownAddress } from './hooks/useKnownAddress'
type Props = { type Props = {
address: string address: string
@ -12,7 +11,7 @@ type Props = {
} }
export const AddressInfo = ({ address, name, avatarUrl }: Props): ReactElement | null => { export const AddressInfo = ({ address, name, avatarUrl }: Props): ReactElement | null => {
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, address)) const toInfo = useKnownAddress(address, { name, image: avatarUrl })
if (address === '') { if (address === '') {
return null return null
@ -21,9 +20,9 @@ export const AddressInfo = ({ address, name, avatarUrl }: Props): ReactElement |
return ( return (
<EthHashInfo <EthHashInfo
hash={address} hash={address}
name={recipientName === 'UNKNOWN' ? name : recipientName} name={toInfo.name}
showAvatar showAvatar
customAvatar={avatarUrl} customAvatar={toInfo.image}
showCopyBtn showCopyBtn
explorerUrl={getExplorerInfo(address)} explorerUrl={getExplorerInfo(address)}
/> />

View File

@ -6,7 +6,7 @@ import styled from 'styled-components'
import useTokenInfo from 'src/logic/safe/hooks/useTokenInfo' import useTokenInfo from 'src/logic/safe/hooks/useTokenInfo'
import { DataDecoded } from 'src/logic/safe/store/models/types/gateway.d' import { DataDecoded } from 'src/logic/safe/store/models/types/gateway.d'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' import { getResetTimeOptions } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime'
import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay'
const SET_ALLOWANCE = 'setAllowance' const SET_ALLOWANCE = 'setAllowance'
@ -35,7 +35,7 @@ export const ModifySpendingLimitDetails = ({ data }: { data: DataDecoded }): Rea
) )
const resetTimeLabel = React.useMemo( const resetTimeLabel = React.useMemo(
() => RESET_TIME_OPTIONS.find(({ value }) => +value === +resetTimeMin / 24 / 60)?.label ?? '', () => getResetTimeOptions().find(({ value }) => +value === +resetTimeMin)?.label ?? '',
[resetTimeMin], [resetTimeMin],
) )

View File

@ -23,7 +23,7 @@ import { TokenTransferAmount } from './TokenTransferAmount'
import { TxsInfiniteScrollContext } from './TxsInfiniteScroll' import { TxsInfiniteScrollContext } from './TxsInfiniteScroll'
import { TxLocationContext } from './TxLocationProvider' import { TxLocationContext } from './TxLocationProvider'
import { CalculatedVotes } from './TxQueueCollapsed' import { CalculatedVotes } from './TxQueueCollapsed'
import { isCancelTxDetails } from './utils' import { getTxTo, isCancelTxDetails } from './utils'
const TxInfo = ({ info }: { info: AssetInfo }) => { const TxInfo = ({ info }: { info: AssetInfo }) => {
if (isTokenTransferAsset(info)) { if (isTokenTransferAsset(info)) {
@ -114,6 +114,7 @@ export const TxCollapsed = ({
}: TxCollapsedProps): ReactElement => { }: TxCollapsedProps): ReactElement => {
const { txLocation } = useContext(TxLocationContext) const { txLocation } = useContext(TxLocationContext)
const { ref, lastItemId } = useContext(TxsInfiniteScrollContext) const { ref, lastItemId } = useContext(TxsInfiniteScrollContext)
const toAddress = getTxTo(transaction)
const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : '' const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : ''
const onChainRejection = const onChainRejection =
@ -127,7 +128,12 @@ export const TxCollapsed = ({
const txCollapsedType = ( const txCollapsedType = (
<div className={'tx-type' + willBeReplaced + onChainRejection}> <div className={'tx-type' + willBeReplaced + onChainRejection}>
<CustomIconText iconUrl={type.icon} text={type.text} /> <CustomIconText
address={toAddress || '0x'}
iconUrl={type.icon}
iconUrlFallback={type.fallbackIcon}
text={type.text}
/>
</div> </div>
) )

View File

@ -0,0 +1,21 @@
import { useSelector } from 'react-redux'
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
type AddressInfo = { name: string | undefined; image: string | undefined }
type UseKnownAddressResponse = AddressInfo & { isAddressBook: boolean }
export const useKnownAddress = (address: string, addressInfo: AddressInfo): UseKnownAddressResponse => {
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, address))
const isInAddressBook = recipientName !== 'UNKNOWN'
return isInAddressBook
? {
name: recipientName,
image: undefined,
isAddressBook: true,
}
: { ...addressInfo, isAddressBook: false }
}

View File

@ -1,22 +1,31 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Transaction } from 'src/logic/safe/store/models/types/gateway.d' import { Transaction, Custom } from 'src/logic/safe/store/models/types/gateway.d'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import CustomTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/custom.svg' import CustomTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/custom.svg'
import CircleCrossRed from 'src/routes/safe/components/Transactions/TxList/assets/circle-cross-red.svg' import CircleCrossRed from 'src/routes/safe/components/Transactions/TxList/assets/circle-cross-red.svg'
import IncomingTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/incoming.svg' import IncomingTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/incoming.svg'
import OutgoingTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/outgoing.svg' import OutgoingTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/outgoing.svg'
import SettingsTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/settings.svg' import SettingsTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/settings.svg'
import { getTxTo } from 'src/routes/safe/components/Transactions/TxList/utils'
import { useKnownAddress } from './useKnownAddress'
export type TxTypeProps = { export type TxTypeProps = {
icon: string | null icon?: string
text: string fallbackIcon?: string
text?: string
} }
export const useTransactionType = (tx: Transaction): TxTypeProps => { export const useTransactionType = (tx: Transaction): TxTypeProps => {
const [type, setType] = useState<TxTypeProps>({ icon: CustomTxIcon, text: 'Contract interaction' }) const [type, setType] = useState<TxTypeProps>({ icon: CustomTxIcon, text: 'Contract interaction' })
const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector)
const toAddress = getTxTo(tx)
// Fixed casting because known address only works for Custom tx
const knownAddress = useKnownAddress(toAddress || '0x', {
name: (tx.txInfo as Custom)?.toInfo?.name,
image: (tx.txInfo as Custom)?.toInfo?.logoUri || undefined,
})
useEffect(() => { useEffect(() => {
switch (tx.txInfo.type) { switch (tx.txInfo.type) {
@ -51,16 +60,15 @@ export const useTransactionType = (tx: Transaction): TxTypeProps => {
} }
const toInfo = tx.txInfo.toInfo const toInfo = tx.txInfo.toInfo
if (toInfo) { setType({
setType({ icon: toInfo.logoUri, text: toInfo.name }) icon: knownAddress.isAddressBook ? CustomTxIcon : knownAddress.image || CustomTxIcon,
break fallbackIcon: knownAddress.isAddressBook ? undefined : CustomTxIcon,
} text: toInfo ? knownAddress.name : 'Contract interaction',
})
setType({ icon: CustomTxIcon, text: 'Contract interaction' })
break break
} }
} }
}, [tx, safeAddress]) }, [tx, safeAddress, knownAddress.name, knownAddress.image, knownAddress.isAddressBook])
return type return type
} }

View File

@ -96,3 +96,20 @@ export const isCancelTxDetails = (txInfo: Transaction['txInfo']): boolean =>
export const addressInList = (list: string[] = []) => (address: string): boolean => export const addressInList = (list: string[] = []) => (address: string): boolean =>
list.some((ownerAddress) => sameAddress(ownerAddress, address)) list.some((ownerAddress) => sameAddress(ownerAddress, address))
export const getTxTo = (tx: Transaction): string | undefined => {
switch (tx.txInfo.type) {
case 'Transfer': {
return tx.txInfo.recipient
}
case 'SettingsChange': {
return undefined
}
case 'Custom': {
return tx.txInfo.to
}
case 'Creation': {
return tx.txInfo.factory || undefined
}
}
}

View File

@ -4,10 +4,16 @@ import { getNetworkInfo } from 'src/config'
import { getGoogleAnalyticsTrackingID } from 'src/config' import { getGoogleAnalyticsTrackingID } from 'src/config'
import { COOKIES_KEY } from 'src/logic/cookies/model/cookie' import { COOKIES_KEY } from 'src/logic/cookies/model/cookie'
import { loadFromCookie } from 'src/logic/cookies/utils' import { loadFromCookie, removeCookie } from 'src/logic/cookies/utils'
export const SAFE_NAVIGATION_EVENT = 'Safe Navigation' export const SAFE_NAVIGATION_EVENT = 'Safe Navigation'
export const COOKIES_LIST = [
{ name: '_ga', path: '/' },
{ name: '_gat', path: '/' },
{ name: '_gid', path: '/' },
]
let analyticsLoaded = false let analyticsLoaded = false
export const loadGoogleAnalytics = (): void => { export const loadGoogleAnalytics = (): void => {
if (analyticsLoaded) { if (analyticsLoaded) {
@ -50,12 +56,15 @@ export const useAnalytics = (): UseAnalyticsResponse => {
fetchCookiesFromStorage() fetchCookiesFromStorage()
}, []) }, [])
const trackPage = (page) => { const trackPage = useCallback(
if (!analyticsAllowed || !analyticsLoaded) { (page) => {
return if (!analyticsAllowed || !analyticsLoaded) {
} return
ReactGA.pageview(page) }
} ReactGA.pageview(page)
},
[analyticsAllowed],
)
const trackEvent = useCallback( const trackEvent = useCallback(
(event: EventArgs) => { (event: EventArgs) => {
@ -69,3 +78,9 @@ export const useAnalytics = (): UseAnalyticsResponse => {
return { trackPage, trackEvent } return { trackPage, trackEvent }
} }
// we remove GA cookies manually as react-ga does not provides a utility for it.
export const removeCookies = (): void => {
const subDomain = location.host.split('.').slice(-2).join('.')
COOKIES_LIST.forEach((cookie) => removeCookie(cookie.name, cookie.path, `.${subDomain}`))
}

1758
yarn.lock

File diff suppressed because it is too large Load Diff