mirror of
https://github.com/status-im/safe-react.git
synced 2025-02-19 21:18:09 +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-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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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"
|
||||
/>
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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' : ''
|
||||
}`
|
||||
|
||||
|
@ -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 })
|
||||
|
@ -136,7 +136,7 @@ type BaseCustom = {
|
||||
toInfo?: AddressInfo
|
||||
}
|
||||
|
||||
type Custom = BaseCustom & {
|
||||
export type Custom = BaseCustom & {
|
||||
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 {
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 />
|
||||
|
@ -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) => {
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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} />
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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}>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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)}
|
||||
/>
|
||||
|
@ -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>
|
||||
)
|
||||
|
||||
|
@ -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 { 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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}`))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user