mirror of
https://github.com/status-im/safe-react.git
synced 2025-02-22 06:28:11 +00:00
Merge branch 'development' into upgrade-web3
This commit is contained in:
commit
d5184b83f3
36
package.json
36
package.json
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
|
@ -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()
|
||||||
|
@ -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' : ''
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
@ -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 })
|
||||||
|
@ -136,7 +136,7 @@ type BaseCustom = {
|
|||||||
toInfo?: AddressInfo
|
toInfo?: AddressInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
type Custom = BaseCustom & {
|
export type Custom = BaseCustom & {
|
||||||
methodName: string | null
|
methodName: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 />
|
||||||
|
@ -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) => {
|
||||||
|
@ -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"
|
||||||
|
@ -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,
|
||||||
|
@ -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} />
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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}>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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}`))
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user