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": "1.0.3",
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2", "@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-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", "@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",
@ -239,21 +239,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",
@ -262,21 +263,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 () => {
try {
onLoading(true)
const appInfo = await getAppInfoFromUrl(debouncedValue) const appInfo = await getAppInfoFromUrl(debouncedValue)
onAppInfo({ ...appInfo }) 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

@ -12,7 +12,7 @@ import {
safeNameSelector, safeNameSelector,
} from 'src/logic/safe/store/selectors' } from 'src/logic/safe/store/selectors'
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'
@ -74,6 +74,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,
@ -166,6 +167,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

@ -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

@ -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

@ -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(
(page) => {
if (!analyticsAllowed || !analyticsLoaded) { if (!analyticsAllowed || !analyticsLoaded) {
return 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}`))
}

1764
yarn.lock

File diff suppressed because it is too large Load Diff