mirror of
https://github.com/status-im/safe-react.git
synced 2025-02-26 16:30:34 +00:00
Merge pull request #53 from gnosis/feature/WA-232-remove-custom-token
WA-232 - Feature: remove custom ERC20 tokens
This commit is contained in:
commit
614fa659de
@ -16,8 +16,8 @@ import { type SelectorProps } from '~/routes/tokens/container/selector'
|
|||||||
import { type Actions } from '~/routes/tokens/container/actions'
|
import { type Actions } from '~/routes/tokens/container/actions'
|
||||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||||
import AddToken from '~/routes/tokens/component/AddToken'
|
import AddToken from '~/routes/tokens/component/AddToken'
|
||||||
|
import RemoveToken from '~/routes/tokens/component/RemoveToken'
|
||||||
import TokenComponent from './Token'
|
import TokenComponent from './Token'
|
||||||
// import RemoveToken from '~/routes/tokens/component/RemoveToken'
|
|
||||||
|
|
||||||
const safeIcon = require('~/routes/safe/component/Safe/assets/gnosis_safe.svg')
|
const safeIcon = require('~/routes/safe/component/Safe/assets/gnosis_safe.svg')
|
||||||
|
|
||||||
@ -49,11 +49,23 @@ class TokenLayout extends React.PureComponent<TokenProps, State> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
onReset = () => {
|
||||||
onRemoveToken = () => {
|
this.setState({ component: undefined })
|
||||||
this.setState({ component: <RemoveToken /> })
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
onRemoveToken = (token: Token) => {
|
||||||
|
const { safeAddress, removeToken } = this.props
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
component: <RemoveToken
|
||||||
|
token={token}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
removeTokenAction={removeToken}
|
||||||
|
onReset={this.onReset}
|
||||||
|
/>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onEnableToken = (token: Token) => {
|
onEnableToken = (token: Token) => {
|
||||||
const { enableToken, safe } = this.props
|
const { enableToken, safe } = this.props
|
||||||
const safeAddress = safe.get('address')
|
const safeAddress = safe.get('address')
|
||||||
@ -83,6 +95,7 @@ class TokenLayout extends React.PureComponent<TokenProps, State> {
|
|||||||
token={token}
|
token={token}
|
||||||
onDisableToken={this.onDisableToken}
|
onDisableToken={this.onDisableToken}
|
||||||
onEnableToken={this.onEnableToken}
|
onEnableToken={this.onEnableToken}
|
||||||
|
onRemove={this.onRemoveToken}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</MuiList>
|
</MuiList>
|
||||||
|
38
src/routes/tokens/component/RemoveToken/Review.jsx
Normal file
38
src/routes/tokens/component/RemoveToken/Review.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Bold from '~/components/layout/Bold'
|
||||||
|
import Heading from '~/components/layout/Heading'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string,
|
||||||
|
funds: string,
|
||||||
|
symbol: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormProps = {
|
||||||
|
submitting: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinnerStyle = {
|
||||||
|
minHeight: '50px',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Review = ({ name, funds, symbol }: Props) => ({ submitting }: FormProps) => (
|
||||||
|
<Block>
|
||||||
|
<Heading tag="h2">Remove CUSTOM ERC 20 Token</Heading>
|
||||||
|
<Paragraph align="left">
|
||||||
|
<Bold>You are about to remove the custom token: </Bold> {name}
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph align="left">
|
||||||
|
<Bold>{`You have ${funds} ${symbol} in your wallet`}</Bold>
|
||||||
|
</Paragraph>
|
||||||
|
<Block style={spinnerStyle}>
|
||||||
|
{ submitting && <CircularProgress size={50} /> }
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Review
|
73
src/routes/tokens/component/RemoveToken/index.jsx
Normal file
73
src/routes/tokens/component/RemoveToken/index.jsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import { type Token } from '~/routes/tokens/store/model/token'
|
||||||
|
import Stepper from '~/components/Stepper'
|
||||||
|
import RemoveTokenAction from '~/routes/tokens/store/actions/removeToken'
|
||||||
|
import Review from '~/routes/tokens/component/RemoveToken/Review'
|
||||||
|
|
||||||
|
const getSteps = () => [
|
||||||
|
'Review remove token operation',
|
||||||
|
]
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
token: Token,
|
||||||
|
safeAddress: string,
|
||||||
|
removeTokenAction: typeof RemoveTokenAction,
|
||||||
|
onReset: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
done: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REMOVE_TOKEN_RESET_BUTTON_TEXT = 'RESET'
|
||||||
|
|
||||||
|
export const removeToken = async (safeAddress: string, token: Token, removeTokenAction: typeof RemoveTokenAction) =>
|
||||||
|
removeTokenAction(safeAddress, token)
|
||||||
|
|
||||||
|
class RemoveToken extends React.PureComponent<Props, State> {
|
||||||
|
state = {
|
||||||
|
done: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveReset = () => {
|
||||||
|
this.setState({ done: false }, this.props.onReset())
|
||||||
|
}
|
||||||
|
|
||||||
|
executeRemoveOperation = async () => {
|
||||||
|
try {
|
||||||
|
const { token, safeAddress, removeTokenAction } = this.props
|
||||||
|
await removeToken(safeAddress, token, removeTokenAction)
|
||||||
|
this.setState({ done: true })
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ done: false })
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('Error while removing owner ' + error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { done } = this.state
|
||||||
|
const { token } = this.props
|
||||||
|
const finishedButton = <Stepper.FinishButton title={REMOVE_TOKEN_RESET_BUTTON_TEXT} />
|
||||||
|
const steps = getSteps()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Stepper
|
||||||
|
finishedTransaction={done}
|
||||||
|
finishedButton={finishedButton}
|
||||||
|
onSubmit={this.executeRemoveOperation}
|
||||||
|
steps={steps}
|
||||||
|
onReset={this.onRemoveReset}
|
||||||
|
>
|
||||||
|
<Stepper.Page name={token.get('name')} symbol={token.get('symbol')} funds={token.get('funds')}>
|
||||||
|
{ Review }
|
||||||
|
</Stepper.Page>
|
||||||
|
</Stepper>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemoveToken
|
@ -9,13 +9,13 @@ import CardContent from '@material-ui/core/CardContent'
|
|||||||
import CardMedia from '@material-ui/core/CardMedia'
|
import CardMedia from '@material-ui/core/CardMedia'
|
||||||
import Typography from '@material-ui/core/Typography'
|
import Typography from '@material-ui/core/Typography'
|
||||||
import { isEther } from '~/utils/tokens'
|
import { isEther } from '~/utils/tokens'
|
||||||
// import Delete from '@material-ui/icons/Delete'
|
import Delete from '@material-ui/icons/Delete'
|
||||||
// import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import { type WithStyles } from '~/theme/mui'
|
import { type WithStyles } from '~/theme/mui'
|
||||||
|
|
||||||
type Props = WithStyles & {
|
type Props = WithStyles & {
|
||||||
token: Token,
|
token: Token,
|
||||||
onRemoveToken: (balance: Token)=> void,
|
onRemove: (token: Token)=> void,
|
||||||
onEnableToken: (token: Token) => void,
|
onEnableToken: (token: Token) => void,
|
||||||
onDisableToken: (token: Token) => void,
|
onDisableToken: (token: Token) => void,
|
||||||
}
|
}
|
||||||
@ -42,12 +42,12 @@ const styles = () => ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
class TokenComponent extends React.Component<Props, State> {
|
class TokenComponent extends React.PureComponent<Props, State> {
|
||||||
state = {
|
state = {
|
||||||
checked: this.props.token.get('status'),
|
checked: this.props.token.get('status'),
|
||||||
}
|
}
|
||||||
|
|
||||||
// onRemoveClick = () => this.props.onRemoveToken(this.props.token)
|
onRemoveClick = () => this.props.onRemove(this.props.token)
|
||||||
|
|
||||||
handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
|
handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
const { checked } = e.target
|
const { checked } = e.target
|
||||||
@ -75,16 +75,14 @@ class TokenComponent extends React.Component<Props, State> {
|
|||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
{symbol}
|
{symbol}
|
||||||
</Typography>
|
{ token.get('removable') &&
|
||||||
</CardContent>
|
|
||||||
</Block>
|
|
||||||
{/*
|
|
||||||
<Block className={classes.controls}>
|
|
||||||
<IconButton aria-label="Delete" onClick={this.onRemoveClick}>
|
<IconButton aria-label="Delete" onClick={this.onRemoveClick}>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
</Block>
|
</Block>
|
||||||
*/}
|
|
||||||
<CardMedia
|
<CardMedia
|
||||||
className={classes.cover}
|
className={classes.cover}
|
||||||
image={token.get('logoUrl')}
|
image={token.get('logoUrl')}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import addToken from '~/routes/tokens/store/actions/addToken'
|
import addToken from '~/routes/tokens/store/actions/addToken'
|
||||||
|
import removeToken from '~/routes/tokens/store/actions/removeToken'
|
||||||
import enableToken from '~/routes/tokens/store/actions/enableToken'
|
import enableToken from '~/routes/tokens/store/actions/enableToken'
|
||||||
import disableToken from '~/routes/tokens/store/actions/disableToken'
|
import disableToken from '~/routes/tokens/store/actions/disableToken'
|
||||||
import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens'
|
import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
@ -8,10 +9,12 @@ export type Actions = {
|
|||||||
enableToken: typeof enableToken,
|
enableToken: typeof enableToken,
|
||||||
disableToken: typeof disableToken,
|
disableToken: typeof disableToken,
|
||||||
addToken: typeof addToken,
|
addToken: typeof addToken,
|
||||||
|
removeToken: typeof removeToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
addToken,
|
addToken,
|
||||||
|
removeToken,
|
||||||
enableToken,
|
enableToken,
|
||||||
disableToken,
|
disableToken,
|
||||||
fetchTokens,
|
fetchTokens,
|
||||||
|
@ -22,7 +22,7 @@ class TokensView extends React.PureComponent<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
tokens, addresses, safe, safeAddress, disableToken, enableToken, addToken,
|
tokens, addresses, safe, safeAddress, disableToken, enableToken, addToken, removeToken,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -35,6 +35,7 @@ class TokensView extends React.PureComponent<Props> {
|
|||||||
disableToken={disableToken}
|
disableToken={disableToken}
|
||||||
enableToken={enableToken}
|
enableToken={enableToken}
|
||||||
addToken={addToken}
|
addToken={addToken}
|
||||||
|
removeToken={removeToken}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
20
src/routes/tokens/store/actions/removeToken.js
Normal file
20
src/routes/tokens/store/actions/removeToken.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
import { type Token } from '~/routes/tokens/store/model/token'
|
||||||
|
|
||||||
|
export const REMOVE_TOKEN = 'REMOVE_TOKEN'
|
||||||
|
|
||||||
|
type RemoveTokenProps = {
|
||||||
|
safeAddress: string,
|
||||||
|
token: Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeToken = createAction(
|
||||||
|
REMOVE_TOKEN,
|
||||||
|
(safeAddress: string, token: Token): RemoveTokenProps => ({
|
||||||
|
safeAddress,
|
||||||
|
token,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default removeToken
|
@ -2,11 +2,12 @@
|
|||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import { handleActions, type ActionType } from 'redux-actions'
|
import { handleActions, type ActionType } from 'redux-actions'
|
||||||
import addToken, { ADD_TOKEN } from '~/routes/tokens/store/actions/addToken'
|
import addToken, { ADD_TOKEN } from '~/routes/tokens/store/actions/addToken'
|
||||||
|
import removeToken, { REMOVE_TOKEN } from '~/routes/tokens/store/actions/removeToken'
|
||||||
import addTokens, { ADD_TOKENS } from '~/routes/tokens/store/actions/addTokens'
|
import addTokens, { ADD_TOKENS } from '~/routes/tokens/store/actions/addTokens'
|
||||||
import { type Token } from '~/routes/tokens/store/model/token'
|
import { type Token } from '~/routes/tokens/store/model/token'
|
||||||
import disableToken, { DISABLE_TOKEN } from '~/routes/tokens/store/actions/disableToken'
|
import disableToken, { DISABLE_TOKEN } from '~/routes/tokens/store/actions/disableToken'
|
||||||
import enableToken, { ENABLE_TOKEN } from '~/routes/tokens/store/actions/enableToken'
|
import enableToken, { ENABLE_TOKEN } from '~/routes/tokens/store/actions/enableToken'
|
||||||
import { setActiveTokenAddresses, getActiveTokenAddresses, setToken } from '~/utils/localStorage/tokens'
|
import { setActiveTokenAddresses, getActiveTokenAddresses, setToken, removeTokenFromStorage } from '~/utils/localStorage/tokens'
|
||||||
import { ensureOnce } from '~/utils/singleton'
|
import { ensureOnce } from '~/utils/singleton'
|
||||||
import { calculateActiveErc20TokensFrom } from '~/utils/tokens'
|
import { calculateActiveErc20TokensFrom } from '~/utils/tokens'
|
||||||
|
|
||||||
@ -16,6 +17,12 @@ export type State = Map<string, Map<string, Token>>
|
|||||||
|
|
||||||
const setTokensOnce = ensureOnce(setActiveTokenAddresses)
|
const setTokensOnce = ensureOnce(setActiveTokenAddresses)
|
||||||
|
|
||||||
|
const removeFromActiveTokens = (safeAddress: string, tokenAddress: string) => {
|
||||||
|
const activeTokens = getActiveTokenAddresses(safeAddress)
|
||||||
|
const index = activeTokens.indexOf(tokenAddress)
|
||||||
|
setActiveTokenAddresses(safeAddress, activeTokens.delete(index))
|
||||||
|
}
|
||||||
|
|
||||||
export default handleActions({
|
export default handleActions({
|
||||||
[ADD_TOKENS]: (state: State, action: ActionType<typeof addTokens>): State => {
|
[ADD_TOKENS]: (state: State, action: ActionType<typeof addTokens>): State => {
|
||||||
const { safeAddress, tokens } = action.payload
|
const { safeAddress, tokens } = action.payload
|
||||||
@ -40,12 +47,18 @@ export default handleActions({
|
|||||||
setToken(safeAddress, token)
|
setToken(safeAddress, token)
|
||||||
return state.setIn([safeAddress, tokenAddress], token)
|
return state.setIn([safeAddress, tokenAddress], token)
|
||||||
},
|
},
|
||||||
|
[REMOVE_TOKEN]: (state: State, action: ActionType<typeof removeToken>): State => {
|
||||||
|
const { safeAddress, token } = action.payload
|
||||||
|
|
||||||
|
const tokenAddress = token.get('address')
|
||||||
|
removeFromActiveTokens(safeAddress, tokenAddress)
|
||||||
|
removeTokenFromStorage(safeAddress, token)
|
||||||
|
return state.removeIn([safeAddress, tokenAddress])
|
||||||
|
},
|
||||||
[DISABLE_TOKEN]: (state: State, action: ActionType<typeof disableToken>): State => {
|
[DISABLE_TOKEN]: (state: State, action: ActionType<typeof disableToken>): State => {
|
||||||
const { address, safeAddress } = action.payload
|
const { address, safeAddress } = action.payload
|
||||||
|
|
||||||
const activeTokens = getActiveTokenAddresses(safeAddress)
|
removeFromActiveTokens(safeAddress, address)
|
||||||
const index = activeTokens.indexOf(address)
|
|
||||||
setActiveTokenAddresses(safeAddress, activeTokens.delete(index))
|
|
||||||
|
|
||||||
return state.setIn([safeAddress, address, 'status'], false)
|
return state.setIn([safeAddress, address, 'status'], false)
|
||||||
},
|
},
|
||||||
|
83
src/test/tokens.dom.removing.test.js
Normal file
83
src/test/tokens.dom.removing.test.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// @flow
|
||||||
|
import * as TestUtils from 'react-dom/test-utils'
|
||||||
|
import { getWeb3 } from '~/wallets/getWeb3'
|
||||||
|
import { promisify } from '~/utils/promisify'
|
||||||
|
import { getFirstTokenContract, getSecondTokenContract } from '~/test/utils/tokenMovements'
|
||||||
|
import { aNewStore } from '~/store'
|
||||||
|
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
||||||
|
import { travelToTokens } from '~/test/builder/safe.dom.utils'
|
||||||
|
import * as fetchTokensModule from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
|
import * as enhancedFetchModule from '~/utils/fetch'
|
||||||
|
import { TOKEN_ADRESS_PARAM } from '~/routes/tokens/component/AddToken/FirstPage'
|
||||||
|
import { TOKEN_NAME_PARAM, TOKEN_SYMBOL_PARAM, TOKEN_DECIMALS_PARAM, TOKEN_LOGO_URL_PARAM } from '~/routes/tokens/component/AddToken/SecondPage'
|
||||||
|
import addToken from '~/routes/tokens/store/actions/addToken'
|
||||||
|
import { addTokenFnc } from '~/routes/tokens/component/AddToken'
|
||||||
|
import { sleep } from '~/utils/timer'
|
||||||
|
import TokenComponent from '~/routes/tokens/component/Token'
|
||||||
|
import { testToken } from '~/test/builder/tokens.dom.utils'
|
||||||
|
|
||||||
|
describe('DOM > Feature > Add new ERC 20 Tokens', () => {
|
||||||
|
let web3
|
||||||
|
let accounts
|
||||||
|
let firstErc20Token
|
||||||
|
let secondErc20Token
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
web3 = getWeb3()
|
||||||
|
accounts = await promisify(cb => web3.eth.getAccounts(cb))
|
||||||
|
firstErc20Token = await getFirstTokenContract(web3, accounts[0])
|
||||||
|
secondErc20Token = await getSecondTokenContract(web3, accounts[0])
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
enhancedFetchModule.enhancedFetch = jest.fn()
|
||||||
|
enhancedFetchModule.enhancedFetch.mockImplementation(() => Promise.resolve([
|
||||||
|
{
|
||||||
|
address: firstErc20Token.address,
|
||||||
|
name: 'First Token Example',
|
||||||
|
symbol: 'FTE',
|
||||||
|
decimals: 18,
|
||||||
|
logoUrl: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
|
||||||
|
},
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('remove custom ERC 20 tokens', async () => {
|
||||||
|
// GIVEN
|
||||||
|
const store = aNewStore()
|
||||||
|
const safeAddress = await aMinedSafe(store)
|
||||||
|
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
[TOKEN_ADRESS_PARAM]: secondErc20Token.address,
|
||||||
|
[TOKEN_NAME_PARAM]: 'Custom ERC20 Token',
|
||||||
|
[TOKEN_SYMBOL_PARAM]: 'CTS',
|
||||||
|
[TOKEN_DECIMALS_PARAM]: '10',
|
||||||
|
[TOKEN_LOGO_URL_PARAM]: 'https://example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
const customAddTokensFn: any = (...args) => store.dispatch(addToken(...args))
|
||||||
|
await addTokenFnc(values, customAddTokensFn, safeAddress)
|
||||||
|
const TokensDom = travelToTokens(store, safeAddress)
|
||||||
|
await sleep(400)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
const buttons = TestUtils.scryRenderedDOMComponentsWithTag(TokensDom, 'button')
|
||||||
|
expect(buttons.length).toBe(2)
|
||||||
|
const removeUserButton = buttons[0]
|
||||||
|
expect(removeUserButton.getAttribute('aria-label')).toBe('Delete')
|
||||||
|
TestUtils.Simulate.click(removeUserButton)
|
||||||
|
await sleep(400)
|
||||||
|
|
||||||
|
const form = TestUtils.findRenderedDOMComponentWithTag(TokensDom, 'form')
|
||||||
|
// submit it
|
||||||
|
TestUtils.Simulate.submit(form)
|
||||||
|
TestUtils.Simulate.submit(form)
|
||||||
|
|
||||||
|
await sleep(400)
|
||||||
|
|
||||||
|
const tokens = TestUtils.scryRenderedComponentsWithType(TokensDom, TokenComponent)
|
||||||
|
expect(tokens.length).toBe(2)
|
||||||
|
testToken(tokens[0].props.token, 'FTE', false)
|
||||||
|
testToken(tokens[1].props.token, 'ETH', true)
|
||||||
|
})
|
||||||
|
})
|
97
src/test/tokens.redux.remove.test.js
Normal file
97
src/test/tokens.redux.remove.test.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// @flow
|
||||||
|
import { getWeb3 } from '~/wallets/getWeb3'
|
||||||
|
import { type Match } from 'react-router-dom'
|
||||||
|
import { promisify } from '~/utils/promisify'
|
||||||
|
import { getFirstTokenContract, getSecondTokenContract } from '~/test/utils/tokenMovements'
|
||||||
|
import { aNewStore } from '~/store'
|
||||||
|
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
||||||
|
import { travelToSafe } from '~/test/builder/safe.dom.utils'
|
||||||
|
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
|
||||||
|
import { testToken } from '~/test/builder/tokens.dom.utils'
|
||||||
|
import * as fetchTokensModule from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
|
import * as enhancedFetchModule from '~/utils/fetch'
|
||||||
|
import { TOKEN_ADRESS_PARAM } from '~/routes/tokens/component/AddToken/FirstPage'
|
||||||
|
import { TOKEN_NAME_PARAM, TOKEN_DECIMALS_PARAM, TOKEN_SYMBOL_PARAM, TOKEN_LOGO_URL_PARAM } from '~/routes/tokens/component/AddToken/SecondPage'
|
||||||
|
import addToken from '~/routes/tokens/store/actions/addToken'
|
||||||
|
import { addTokenFnc } from '~/routes/tokens/component/AddToken'
|
||||||
|
import { activeTokensSelector, tokenListSelector } from '~/routes/tokens/store/selectors'
|
||||||
|
import removeTokenAction from '~/routes/tokens/store/actions/removeToken'
|
||||||
|
import { makeToken } from '~/routes/tokens/store/model/token'
|
||||||
|
import { removeToken } from '~/routes/tokens/component/RemoveToken'
|
||||||
|
|
||||||
|
describe('DOM > Feature > Add new ERC 20 Tokens', () => {
|
||||||
|
let web3
|
||||||
|
let accounts
|
||||||
|
let firstErc20Token
|
||||||
|
let secondErc20Token
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
web3 = getWeb3()
|
||||||
|
accounts = await promisify(cb => web3.eth.getAccounts(cb))
|
||||||
|
firstErc20Token = await getFirstTokenContract(web3, accounts[0])
|
||||||
|
secondErc20Token = await getSecondTokenContract(web3, accounts[0])
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
enhancedFetchModule.enhancedFetch = jest.fn()
|
||||||
|
enhancedFetchModule.enhancedFetch.mockImplementation(() => Promise.resolve([
|
||||||
|
{
|
||||||
|
address: firstErc20Token.address,
|
||||||
|
name: 'First Token Example',
|
||||||
|
symbol: 'FTE',
|
||||||
|
decimals: 18,
|
||||||
|
logoUrl: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
|
||||||
|
},
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
|
||||||
|
const checkTokensOf = (store: Store, safeAddress: string) => {
|
||||||
|
const match: Match = buildMathPropsFrom(safeAddress)
|
||||||
|
const activeTokenList = activeTokensSelector(store.getState(), { match })
|
||||||
|
expect(activeTokenList.count()).toBe(1)
|
||||||
|
testToken(activeTokenList.get(0), 'ETH', true)
|
||||||
|
|
||||||
|
const tokenList = tokenListSelector(store.getState(), { match })
|
||||||
|
expect(tokenList.count()).toBe(2)
|
||||||
|
testToken(tokenList.get(0), 'FTE', false)
|
||||||
|
testToken(tokenList.get(1), 'ETH', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
it('removes custom ERC 20 including page reload', async () => {
|
||||||
|
// GIVEN
|
||||||
|
const store = aNewStore()
|
||||||
|
const safeAddress = await aMinedSafe(store)
|
||||||
|
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
[TOKEN_ADRESS_PARAM]: secondErc20Token.address,
|
||||||
|
[TOKEN_NAME_PARAM]: 'Custom ERC20 Token',
|
||||||
|
[TOKEN_SYMBOL_PARAM]: 'CTS',
|
||||||
|
[TOKEN_DECIMALS_PARAM]: '10',
|
||||||
|
[TOKEN_LOGO_URL_PARAM]: 'https://example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
const customAddTokensFn: any = (...args) => store.dispatch(addToken(...args))
|
||||||
|
await addTokenFnc(values, customAddTokensFn, safeAddress)
|
||||||
|
|
||||||
|
const token = makeToken({
|
||||||
|
address: secondErc20Token.address,
|
||||||
|
name: 'Custom ERC20 Token',
|
||||||
|
symbol: 'CTS',
|
||||||
|
decimals: 10,
|
||||||
|
logoUrl: 'https://example.com',
|
||||||
|
status: true,
|
||||||
|
removable: true,
|
||||||
|
})
|
||||||
|
const customRemoveTokensFnc: any = (...args) => store.dispatch(removeTokenAction(...args))
|
||||||
|
await removeToken(safeAddress, token, customRemoveTokensFnc)
|
||||||
|
checkTokensOf(store, safeAddress)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
const reloadedStore = aNewStore()
|
||||||
|
await reloadedStore.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
||||||
|
travelToSafe(reloadedStore, safeAddress) // reload
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
checkTokensOf(reloadedStore, safeAddress)
|
||||||
|
})
|
||||||
|
})
|
@ -51,3 +51,17 @@ export const setToken = (safeAddress: string, token: Token) => {
|
|||||||
console.log('Error adding token in localstorage')
|
console.log('Error adding token in localstorage')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const removeTokenFromStorage = (safeAddress: string, token: Token) => {
|
||||||
|
const data: List<TokenProps> = getTokens(safeAddress)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const index = data.indexOf(token)
|
||||||
|
const serializedState = JSON.stringify(data.remove(index))
|
||||||
|
const key = getTokensKey(safeAddress)
|
||||||
|
localStorage.setItem(key, serializedState)
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('Error removing token in localstorage')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user