Merge branch 'development' into upgrade-web3

This commit is contained in:
Daniel Sanchez 2021-05-18 11:25:03 +02:00 committed by GitHub
commit d5184b83f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 850 additions and 1303 deletions

View File

@ -161,21 +161,21 @@
"@gnosis.pm/safe-apps-sdk": "1.0.3",
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#b281238",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#4864ebb",
"@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/icons": "^4.11.0",
"@material-ui/lab": "4.0.0-alpha.57",
"@openzeppelin/contracts": "3.1.0",
"@sentry/react": "^6.2.1",
"@sentry/tracing": "^6.2.1",
"@sentry/react": "^6.3.5",
"@sentry/tracing": "^6.3.5",
"@truffle/contract": "^4.3.0",
"@unstoppabledomains/resolution": "^1.17.0",
"async-sema": "^3.1.0",
"axios": "0.21.1",
"bignumber.js": "9.0.1",
"bnc-onboard": "~1.22.0",
"bnc-onboard": "~1.25.0",
"classnames": "^2.2.6",
"connected-react-router": "6.8.0",
"currency-flags": "2.1.2",
@ -239,21 +239,22 @@
"@storybook/preset-create-react-app": "^3.1.5",
"@storybook/react": "^5.3.19",
"@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",
"@types/history": "4.6.2",
"@types/jest": "^26.0.22",
"@types/js-cookie": "^2.2.6",
"@types/lodash.get": "^4.4.6",
"@types/lodash.memoize": "^4.1.6",
"@types/node": "^14.14.37",
"@types/node": "^14.14.45",
"@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-router-dom": "^5.1.6",
"@types/redux-actions": "^2.6.1",
"@types/styled-components": "^5.1.9",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"concurrently": "^6.0.0",
"coveralls": "^3.1.0",
"cross-env": "^7.0.3",
@ -262,21 +263,22 @@
"electron": "^9.4.0",
"electron-builder": "22.10.5",
"electron-notarize": "1.0.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint": "^7.26.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.23.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "~4.3.8",
"lint-staged": "^10.5.2",
"patch-package": "^6.4.6",
"patch-package": "^6.4.7",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.2.0",
"redux-mock-store": "^1.5.4",
"sass": "^1.32.0",
"typechain": "^4.0.0",
"typescript": "4.2.3",
"typescript": "4.2.4",
"wait-on": "^5.3.0"
}
}

View File

@ -27,7 +27,7 @@ What you need to install globally:
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
@ -146,7 +146,7 @@ Please read [CONTRIBUTING.md](https://gist.github.com/PurpleBooth/b24679402957c6
## 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

View File

@ -10,7 +10,7 @@ import { openCookieBanner } from 'src/logic/cookies/store/actions/openCookieBann
import { cookieBannerOpen } from 'src/logic/cookies/store/selectors'
import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils'
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 AlertRedIcon from './assets/alert-red.svg'
import IntercomIcon from './assets/intercom.png'
@ -160,6 +160,11 @@ const CookiesBanner = (): ReactElement => {
await saveCookie(COOKIES_KEY, newState, expDays)
setShowAnalytics(localAnalytics)
setShowIntercom(localIntercom)
if (!localAnalytics) {
removeCookies()
}
if (!localIntercom && isIntercomLoaded()) {
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 styled from 'styled-components'
const Wrapper = styled.div`
display: flex;
align-items: center;
`
const Icon = styled.img`
max-width: 15px;
max-height: 15px;
margin-right: 9px;
`
type Props = {
address: string
iconUrl?: string
iconUrlFallback?: string
text?: string
}
type Props = { iconUrl: string | null | undefined; text?: string }
export const CustomIconText = ({ iconUrl, text }: Props): ReactElement => (
<Wrapper>
{iconUrl && <Icon alt={text} src={iconUrl} />}
{text && <Text size="xl">{text}</Text>}
</Wrapper>
export const CustomIconText = ({ address, iconUrl, text, iconUrlFallback }: Props): ReactElement => (
<EthHashInfo
hash={address}
showHash={false}
avatarSize="sm"
showAvatar
customAvatar={iconUrl || undefined}
customAvatarFallback={iconUrlFallback}
name={text}
textSize="xl"
/>
)

View File

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

View File

@ -130,7 +130,7 @@ describe('Forms > Validators', () => {
})
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 () => {
expect(await mustBeEthereumAddress('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe')).toBeUndefined()

View File

@ -68,7 +68,7 @@ export const mustBeEthereumAddress = memoize(
const startsWith0x = address?.startsWith('0x')
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' : ''
}`

View File

@ -4,9 +4,10 @@ import { getNetworkName } from 'src/config'
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 {
const stringifiedValue = await Cookies.get(`${PREFIX}__${key}`)
const stringifiedValue = await Cookies.get(`${prefix}${key}`)
if (stringifiedValue === null || stringifiedValue === 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 {
const stringifiedValue = JSON.stringify(value)
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)
}
}
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
}
type Custom = BaseCustom & {
export type Custom = BaseCustom & {
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 {
input: { value: appUrl },
} = useField('appUrl', { subscription: { value: true } })
const debouncedValue = useDebounce(appUrl, 500)
React.useEffect(() => {
const updateAppInfo = async () => {
const appInfo = await getAppInfoFromUrl(debouncedValue)
onAppInfo({ ...appInfo })
try {
onLoading(true)
const appInfo = await getAppInfoFromUrl(debouncedValue)
onAppInfo({ ...appInfo })
onLoading(false)
} catch (error) {
onLoading(false)
}
}
if (isValidURL(debouncedValue)) {
updateAppInfo()
}
}, [debouncedValue, onAppInfo])
}, [debouncedValue, onAppInfo, onLoading])
return null
}
@ -55,7 +67,15 @@ const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => {
const validate = !visited?.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList))
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 styled from 'styled-components'
@ -24,6 +24,7 @@ const StyledTextFileAppName = styled(TextField)`
`
const AppInfo = styled.div`
display: flex;
margin: 36px 0 24px 0;
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 {
appUrl: string
agreementAccepted: boolean
@ -62,6 +75,7 @@ const AddApp = ({ appList, closeModal }: AddAppProps): ReactElement => {
const [appInfo, setAppInfo] = useState<SafeApp>(APP_INFO)
const history = useHistory()
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
const [isLoading, setIsLoading] = useState(false)
const handleSubmit = () => {
const newAppList = [
@ -93,14 +107,26 @@ const AddApp = ({ appList, closeModal }: AddAppProps): ReactElement => {
<Icon size="sm" type="externalLink" color="primary" />
</Link>
</AppDocsInfo>
<AppUrl appList={appList} />
{/* Fetch app from url and return a SafeApp */}
<AppInfoUpdater onAppInfo={setAppInfo} />
<AppInfoUpdater onAppInfo={setAppInfo} onLoading={setIsLoading} />
<AppInfo>
<Img alt="Token image" height={55} src={appInfo.iconUrl} />
<StyledTextFileAppName label="App name" readOnly value={appInfo.name} onChange={() => {}} />
{isLoading ? (
<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>
<AppAgreement />

View File

@ -12,7 +12,7 @@ import {
safeNameSelector,
} from 'src/logic/safe/store/selectors'
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 { isSameURL } from 'src/utils/url'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
@ -74,6 +74,7 @@ type Props = {
}
const NETWORK_NAME = getNetworkName()
const NETWORK_ID = getNetworkId()
const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = {
isOpen: false,
@ -166,6 +167,7 @@ const AppFrame = ({ appUrl }: Props): ReactElement => {
communicator?.on('getSafeInfo', () => ({
safeAddress,
network: NETWORK_NAME,
chainId: NETWORK_ID,
}))
communicator?.on('rpcCall', async (msg) => {

View File

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

View File

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

View File

@ -116,9 +116,16 @@ export const AddOwnerModal = ({ isOpen, onClose }: Props): React.ReactElement =>
title="Add owner to Safe"
>
<>
{activeScreen === 'selectOwner' && <OwnerForm onClose={onClose} onSubmit={ownerSubmitted} />}
{activeScreen === 'selectOwner' && (
<OwnerForm initialValues={values} onClose={onClose} onSubmit={ownerSubmitted} />
)}
{activeScreen === 'selectThreshold' && (
<ThresholdForm onClickBack={onClickBack} onClose={onClose} onSubmit={thresholdSubmitted} />
<ThresholdForm
onClickBack={onClickBack}
initialValues={{ threshold: values.threshold }}
onClose={onClose}
onSubmit={thresholdSubmitted}
/>
)}
{activeScreen === 'reviewAddOwner' && (
<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 { 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_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid'
export const ADD_OWNER_NEXT_BTN_TEST_ID = 'add-owner-next-btn'
@ -41,9 +43,10 @@ const useStyles = makeStyles(styles)
type OwnerFormProps = {
onClose: () => 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 handleSubmit = (values) => {
onSubmit(values)
@ -65,7 +68,14 @@ export const OwnerForm = ({ onClose, onSubmit }: OwnerFormProps): React.ReactEle
</IconButton>
</Row>
<Hairline />
<GnoForm formMutators={formMutators} onSubmit={handleSubmit}>
<GnoForm
formMutators={formMutators}
initialValues={{
ownerName: initialValues?.ownerName,
ownerAddress: initialValues?.ownerAddress,
}}
onSubmit={handleSubmit}
>
{(...args) => {
const mutators = args[3]

View File

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

View File

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

View File

@ -24,13 +24,18 @@ export const REMOVE_OWNER_THRESHOLD_NEXT_BTN_TEST_ID = 'remove-owner-threshold-n
const useStyles = makeStyles(styles)
type ThresholdValues = {
threshold: string
}
type Props = {
onClickBack: () => void
onClose: () => 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 owners = useSelector(safeOwnersSelector)
const threshold = useSelector(safeThresholdSelector) as number
@ -51,7 +56,10 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactE
</IconButton>
</Row>
<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

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 { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
type OwnerValues = {
export type OwnerValues = {
newOwnerAddress: string
newOwnerName: string
}
@ -131,7 +131,13 @@ export const ReplaceOwnerModal = ({
>
<>
{activeScreen === 'checkOwner' && (
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
<OwnerForm
onClose={onClose}
onSubmit={ownerSubmitted}
initialValues={values}
ownerAddress={ownerAddress}
ownerName={ownerName}
/>
)}
{activeScreen === 'reviewReplaceOwner' && (
<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_NEXT_BTN_TEST_ID = 'replace-owner-next-btn'
import { OwnerValues } from '../..'
const formMutators = {
setOwnerAddress: (args, state, utils) => {
utils.changeValue(state, 'ownerAddress', () => args[0])
@ -50,9 +52,16 @@ type OwnerFormProps = {
onSubmit: (values: NewOwnerProps) => void
ownerAddress: 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 handleSubmit = (values: NewOwnerProps) => {
@ -75,7 +84,14 @@ export const OwnerForm = ({ onClose, onSubmit, ownerAddress, ownerName }: OwnerF
</IconButton>
</Row>
<Hairline />
<GnoForm formMutators={formMutators} onSubmit={handleSubmit}>
<GnoForm
formMutators={formMutators}
onSubmit={handleSubmit}
initialValues={{
ownerName: initialValues?.newOwnerName,
ownerAddress: initialValues?.newOwnerAddress,
}}
>
{(...args) => {
const mutators = args[3]

View File

@ -1,9 +1,8 @@
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
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 = {
address: string
@ -12,7 +11,7 @@ type Props = {
}
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 === '') {
return null
@ -21,9 +20,9 @@ export const AddressInfo = ({ address, name, avatarUrl }: Props): ReactElement |
return (
<EthHashInfo
hash={address}
name={recipientName === 'UNKNOWN' ? name : recipientName}
name={toInfo.name}
showAvatar
customAvatar={avatarUrl}
customAvatar={toInfo.image}
showCopyBtn
explorerUrl={getExplorerInfo(address)}
/>

View File

@ -23,7 +23,7 @@ import { TokenTransferAmount } from './TokenTransferAmount'
import { TxsInfiniteScrollContext } from './TxsInfiniteScroll'
import { TxLocationContext } from './TxLocationProvider'
import { CalculatedVotes } from './TxQueueCollapsed'
import { isCancelTxDetails } from './utils'
import { getTxTo, isCancelTxDetails } from './utils'
const TxInfo = ({ info }: { info: AssetInfo }) => {
if (isTokenTransferAsset(info)) {
@ -114,6 +114,7 @@ export const TxCollapsed = ({
}: TxCollapsedProps): ReactElement => {
const { txLocation } = useContext(TxLocationContext)
const { ref, lastItemId } = useContext(TxsInfiniteScrollContext)
const toAddress = getTxTo(transaction)
const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : ''
const onChainRejection =
@ -127,7 +128,12 @@ export const TxCollapsed = ({
const txCollapsedType = (
<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>
)

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 { 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 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 IncomingTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/incoming.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 { getTxTo } from 'src/routes/safe/components/Transactions/TxList/utils'
import { useKnownAddress } from './useKnownAddress'
export type TxTypeProps = {
icon: string | null
text: string
icon?: string
fallbackIcon?: string
text?: string
}
export const useTransactionType = (tx: Transaction): TxTypeProps => {
const [type, setType] = useState<TxTypeProps>({ icon: CustomTxIcon, text: 'Contract interaction' })
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(() => {
switch (tx.txInfo.type) {
@ -51,16 +60,15 @@ export const useTransactionType = (tx: Transaction): TxTypeProps => {
}
const toInfo = tx.txInfo.toInfo
if (toInfo) {
setType({ icon: toInfo.logoUri, text: toInfo.name })
break
}
setType({ icon: CustomTxIcon, text: 'Contract interaction' })
setType({
icon: knownAddress.isAddressBook ? CustomTxIcon : knownAddress.image || CustomTxIcon,
fallbackIcon: knownAddress.isAddressBook ? undefined : CustomTxIcon,
text: toInfo ? knownAddress.name : 'Contract interaction',
})
break
}
}
}, [tx, safeAddress])
}, [tx, safeAddress, knownAddress.name, knownAddress.image, knownAddress.isAddressBook])
return type
}

View File

@ -96,3 +96,20 @@ export const isCancelTxDetails = (txInfo: Transaction['txInfo']): boolean =>
export const addressInList = (list: string[] = []) => (address: string): boolean =>
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 { 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 COOKIES_LIST = [
{ name: '_ga', path: '/' },
{ name: '_gat', path: '/' },
{ name: '_gid', path: '/' },
]
let analyticsLoaded = false
export const loadGoogleAnalytics = (): void => {
if (analyticsLoaded) {
@ -50,12 +56,15 @@ export const useAnalytics = (): UseAnalyticsResponse => {
fetchCookiesFromStorage()
}, [])
const trackPage = (page) => {
if (!analyticsAllowed || !analyticsLoaded) {
return
}
ReactGA.pageview(page)
}
const trackPage = useCallback(
(page) => {
if (!analyticsAllowed || !analyticsLoaded) {
return
}
ReactGA.pageview(page)
},
[analyticsAllowed],
)
const trackEvent = useCallback(
(event: EventArgs) => {
@ -69,3 +78,9 @@ export const useAnalytics = (): UseAnalyticsResponse => {
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}`))
}

1764
yarn.lock

File diff suppressed because it is too large Load Diff