Feature/2816 token detection mvp (#2901)
* Update AssetDetectionController to use TokenDetectionController * Update redux with token list controller data * Replace contract map with token list from TokenListController. Add typescript support * - Update to use iconUrl from token list - Create token list selectors - Minor refactor into typescript - Update copies * Hook up preferences controller to handle static token list * Use controllers v12.1.0. Set up last bit for token list controller constructor * Remove controllers tgz * Add comments to token reducers * Remove all contract metadata imports * Upgrade nodeify library to fix pbkdf2 library crasher * Remove comment and lock library version * Remove unused field on engine reducer * Add missing blue100 * Change Alert to use enums. Add jest types. Remove comments. Write test for token util. * Handle undefined passed to token util * Update enzyme to support jest diving redux connected components * Fix all unit tests that uses Redux. Update snapshots. * Update app to support latest controllers. * Provide TokensController with provider on initialization. * Clean up code to display asset logos * Create static asset generation script. Generate on build + watch. * Update static logos * Properly format static logos file * Update controller package * Mock static-logos.js for testing * ignored tokens updates * add migration * cleanup * Upgrade controller version * Use latest controller version that includes abort controller polyfill * Use TokenListController types * Remove unused EthInput * Remove commented code in QuotesView.js * No need to set address on tokenlist array * Update controller version * Remove TransactionDirection unused file. ESLint ignore static assets file * Refactor wallet to tsx * Add missing deps array * Add missing tab label * Don't lint generated static logos file. Fix crasher for ipfs logos. * Fix unit test * Update title and cta label on tokens and nft pages * Fix unit test * Fix unit test * Rename asset list extension * Showing icons for payment request flow * Fix showing icon in payments. Fix tests Co-authored-by: Alex <adonesky@gmail.com>pull/3129/head^2
parent
068dad8242
commit
7f695c4eb7
136 changed files with 1952 additions and 2465 deletions
@ -1,81 +0,0 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import { View, StyleSheet, TouchableOpacity } from 'react-native'; |
||||
import { colors } from '../../styles/common'; |
||||
import Text from './Text'; |
||||
|
||||
const styles = StyleSheet.create({ |
||||
base: { |
||||
paddingHorizontal: 12, |
||||
paddingVertical: 12, |
||||
borderWidth: 1, |
||||
borderRadius: 4, |
||||
flexDirection: 'row', |
||||
}, |
||||
baseSmall: { |
||||
paddingVertical: 8, |
||||
}, |
||||
info: { |
||||
backgroundColor: colors.blue100, |
||||
borderColor: colors.blue, |
||||
}, |
||||
warning: { |
||||
backgroundColor: colors.yellow100, |
||||
borderColor: colors.yellow, |
||||
}, |
||||
error: { |
||||
backgroundColor: colors.red000, |
||||
borderColor: colors.red, |
||||
}, |
||||
textInfo: { color: colors.blue, flexShrink: 1, lineHeight: 16.8 }, |
||||
textWarning: { color: colors.yellow700, flexShrink: 1, lineHeight: 16.8 }, |
||||
textError: { color: colors.red, flexShrink: 1, lineHeight: 16.8 }, |
||||
textIconStyle: { marginRight: 12 }, |
||||
iconWrapper: { |
||||
alignItems: 'center', |
||||
}, |
||||
}); |
||||
|
||||
function getStyles(type) { |
||||
switch (type) { |
||||
case 'warning': { |
||||
return [styles.warning, styles.textWarning]; |
||||
} |
||||
case 'error': { |
||||
return [styles.error, styles.textError]; |
||||
} |
||||
case 'info': |
||||
default: { |
||||
return [styles.info, styles.textInfo]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
function Alert({ type = 'info', small, renderIcon, style, onPress, children, ...props }) { |
||||
const [wrapperStyle, textStyle] = useMemo(() => getStyles(type), [type]); |
||||
|
||||
const Wrapper = onPress ? TouchableOpacity : View; |
||||
return ( |
||||
<Wrapper style={[styles.base, small && styles.baseSmall, wrapperStyle, style]} onPress={onPress} {...props}> |
||||
{renderIcon && typeof renderIcon === 'function' && <View style={styles.iconWrapper}>{renderIcon()}</View>} |
||||
{typeof children === 'function' ? ( |
||||
children(textStyle) |
||||
) : ( |
||||
<Text small={small} style={[textStyle, !!renderIcon && styles.textIconStyle]}> |
||||
{children} |
||||
</Text> |
||||
)} |
||||
</Wrapper> |
||||
); |
||||
} |
||||
|
||||
Alert.propTypes = { |
||||
type: PropTypes.oneOf(['info', 'warning', 'error']), |
||||
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), |
||||
small: PropTypes.bool, |
||||
renderIcon: PropTypes.func, |
||||
onPress: PropTypes.func, |
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), |
||||
}; |
||||
|
||||
export default Alert; |
@ -0,0 +1,99 @@ |
||||
import React, { useCallback, ReactNode } from 'react'; |
||||
import { |
||||
View, |
||||
StyleSheet, |
||||
TouchableOpacity, |
||||
ViewStyle, |
||||
StyleProp, |
||||
TouchableOpacityProps, |
||||
ViewProps, |
||||
TextStyle |
||||
} from 'react-native'; |
||||
import { colors } from '../../styles/common'; |
||||
import CustomText from './Text'; |
||||
// TODO: Convert into typescript and correctly type optionals
|
||||
const Text = CustomText as any; |
||||
|
||||
export enum AlertType { |
||||
Info = 'Info', |
||||
Warning = 'Warning', |
||||
Error = 'Error' |
||||
} |
||||
|
||||
type Props = { |
||||
type: AlertType; |
||||
style?: StyleProp<ViewStyle>; |
||||
small?: boolean; |
||||
renderIcon?: () => ReactNode; |
||||
onPress?: () => void; |
||||
children?: ReactNode; |
||||
}; |
||||
|
||||
const Alert = ({ type = AlertType.Info, small, renderIcon, style, onPress, children, ...props }: Props) => { |
||||
const Wrapper: React.ComponentClass<TouchableOpacityProps | ViewProps> = onPress ? TouchableOpacity : View; |
||||
|
||||
const getStyles: (type: AlertType) => [StyleProp<ViewStyle>, StyleProp<TextStyle>] = useCallback(type => { |
||||
switch (type) { |
||||
case AlertType.Warning: { |
||||
return [styles.warning, { ...styles.textWarning, ...styles.baseTextStyle }]; |
||||
} |
||||
case AlertType.Error: { |
||||
return [styles.error, { ...styles.textError, ...styles.baseTextStyle }]; |
||||
} |
||||
case AlertType.Info: |
||||
default: { |
||||
return [styles.info, { ...styles.textInfo, ...styles.baseTextStyle }]; |
||||
} |
||||
} |
||||
}, []); |
||||
|
||||
const [wrapperStyle, textStyle] = getStyles(type); |
||||
|
||||
return ( |
||||
<Wrapper style={[styles.base, small && styles.baseSmall, wrapperStyle, style]} onPress={onPress} {...props}> |
||||
{renderIcon && <View style={styles.iconWrapper}>{renderIcon()}</View>} |
||||
{typeof children === 'function' ? ( |
||||
children(textStyle) |
||||
) : ( |
||||
<Text small={small} style={[textStyle, !!renderIcon && styles.textIconStyle]}> |
||||
{children} |
||||
</Text> |
||||
)} |
||||
</Wrapper> |
||||
); |
||||
}; |
||||
|
||||
const styles = StyleSheet.create({ |
||||
base: { |
||||
paddingHorizontal: 12, |
||||
paddingVertical: 8, |
||||
borderWidth: 1, |
||||
borderRadius: 8, |
||||
flexDirection: 'row' |
||||
}, |
||||
baseSmall: { |
||||
paddingVertical: 8 |
||||
}, |
||||
info: { |
||||
backgroundColor: colors.blue100, |
||||
borderColor: colors.blue |
||||
}, |
||||
warning: { |
||||
backgroundColor: colors.yellow100, |
||||
borderColor: colors.yellowWarningBorder |
||||
}, |
||||
error: { |
||||
backgroundColor: colors.red000, |
||||
borderColor: colors.red |
||||
}, |
||||
baseTextStyle: { fontSize: 14, flex: 1, lineHeight: 17 }, |
||||
textInfo: { color: colors.blue }, |
||||
textWarning: { color: colors.black }, |
||||
textError: { color: colors.red }, |
||||
textIconStyle: { marginRight: 12 }, |
||||
iconWrapper: { |
||||
alignItems: 'center' |
||||
} |
||||
}); |
||||
|
||||
export default Alert; |
@ -1,10 +1,11 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import AssetIcon from './'; |
||||
const sampleLogo = 'https://s3.amazonaws.com/airswap-token-images/WBTC.png'; |
||||
|
||||
describe('AssetIcon', () => { |
||||
it('should render correctly', () => { |
||||
const wrapper = shallow(<AssetIcon logo={'metamark.svg'} />); |
||||
const wrapper = shallow(<AssetIcon logo={sampleLogo} />); |
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
||||
|
@ -1,49 +1,7 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`AssetSearch should render correctly 1`] = ` |
||||
<View |
||||
style={ |
||||
Object { |
||||
"alignItems": "center", |
||||
"borderColor": "#d6d9dc", |
||||
"borderRadius": 4, |
||||
"borderWidth": 1, |
||||
"flex": 1, |
||||
"flexDirection": "row", |
||||
"justifyContent": "center", |
||||
"margin": 20, |
||||
"marginBottom": 0, |
||||
} |
||||
} |
||||
testID="add-searched-token-screen" |
||||
> |
||||
<Icon |
||||
allowFontScaling={false} |
||||
name="search" |
||||
size={22} |
||||
style={ |
||||
Object { |
||||
"padding": 16, |
||||
} |
||||
} |
||||
/> |
||||
<TextInput |
||||
onChangeText={[Function]} |
||||
placeholder="Search Tokens" |
||||
placeholderTextColor="#d6d9dc" |
||||
style={ |
||||
Array [ |
||||
Object { |
||||
"fontFamily": "EuclidCircularB-Regular", |
||||
"fontWeight": "400", |
||||
}, |
||||
Object { |
||||
"width": "85%", |
||||
}, |
||||
] |
||||
} |
||||
testID="input-search-asset" |
||||
value="" |
||||
/> |
||||
</View> |
||||
<Memo() |
||||
onSearch={[Function]} |
||||
/> |
||||
`; |
||||
|
@ -1,96 +0,0 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { TextInput, View, StyleSheet } from 'react-native'; |
||||
import { colors, fontStyles } from '../../../styles/common'; |
||||
import PropTypes from 'prop-types'; |
||||
import { strings } from '../../../../locales/i18n'; |
||||
import contractMap from '@metamask/contract-metadata'; |
||||
import Fuse from 'fuse.js'; |
||||
import Icon from 'react-native-vector-icons/FontAwesome'; |
||||
import { toLowerCaseEquals } from '../../../util/general'; |
||||
|
||||
const styles = StyleSheet.create({ |
||||
searchSection: { |
||||
margin: 20, |
||||
marginBottom: 0, |
||||
flex: 1, |
||||
flexDirection: 'row', |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
borderWidth: 1, |
||||
borderRadius: 4, |
||||
borderColor: colors.grey100, |
||||
}, |
||||
textInput: { |
||||
...fontStyles.normal, |
||||
}, |
||||
icon: { |
||||
padding: 16, |
||||
}, |
||||
}); |
||||
|
||||
const contractList = Object.entries(contractMap) |
||||
.map(([address, tokenData]) => { |
||||
tokenData.address = address; |
||||
return tokenData; |
||||
}) |
||||
.filter((tokenData) => Boolean(tokenData.erc20)); |
||||
|
||||
const fuse = new Fuse(contractList, { |
||||
shouldSort: true, |
||||
threshold: 0.45, |
||||
location: 0, |
||||
distance: 100, |
||||
maxPatternLength: 32, |
||||
minMatchCharLength: 1, |
||||
keys: [ |
||||
{ name: 'name', weight: 0.5 }, |
||||
{ name: 'symbol', weight: 0.5 }, |
||||
], |
||||
}); |
||||
|
||||
/** |
||||
* PureComponent that provides ability to search assets. |
||||
*/ |
||||
export default class AssetSearch extends PureComponent { |
||||
state = { |
||||
searchQuery: '', |
||||
inputWidth: '85%', |
||||
}; |
||||
|
||||
static propTypes = { |
||||
/** |
||||
/* navigation object required to push new views |
||||
*/ |
||||
onSearch: PropTypes.func, |
||||
}; |
||||
|
||||
componentDidMount() { |
||||
setTimeout(() => this.setState({ inputWidth: '86%' }), 100); |
||||
} |
||||
|
||||
handleSearch = (searchQuery) => { |
||||
this.setState({ searchQuery }); |
||||
const fuseSearchResult = fuse.search(searchQuery); |
||||
const addressSearchResult = contractList.filter((token) => toLowerCaseEquals(token.address, searchQuery)); |
||||
const results = [...addressSearchResult, ...fuseSearchResult]; |
||||
this.props.onSearch({ searchQuery, results }); |
||||
}; |
||||
|
||||
render = () => { |
||||
const { searchQuery, inputWidth } = this.state; |
||||
|
||||
return ( |
||||
<View style={styles.searchSection} testID={'add-searched-token-screen'}> |
||||
<Icon name="search" size={22} style={styles.icon} /> |
||||
<TextInput |
||||
style={[styles.textInput, { width: inputWidth }]} |
||||
value={searchQuery} |
||||
placeholder={strings('token.search_tokens_placeholder')} |
||||
placeholderTextColor={colors.grey100} |
||||
onChangeText={this.handleSearch} |
||||
testID={'input-search-asset'} |
||||
/> |
||||
</View> |
||||
); |
||||
}; |
||||
} |
@ -1,10 +1,28 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import AssetSearch from './'; |
||||
import configureMockStore from 'redux-mock-store'; |
||||
import { Provider } from 'react-redux'; |
||||
|
||||
const mockStore = configureMockStore(); |
||||
const initialState = { |
||||
engine: { |
||||
backgroundState: { |
||||
TokenListController: { |
||||
tokenList: {}, |
||||
}, |
||||
}, |
||||
}, |
||||
}; |
||||
const store = mockStore(initialState); |
||||
|
||||
describe('AssetSearch', () => { |
||||
it('should render correctly', () => { |
||||
const wrapper = shallow(<AssetSearch />); |
||||
const wrapper = shallow( |
||||
<Provider store={store}> |
||||
<AssetSearch onSearch={() => null} /> |
||||
</Provider> |
||||
); |
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
}); |
||||
|
@ -0,0 +1,88 @@ |
||||
import React, { memo, useEffect, useState, useCallback } from 'react'; |
||||
import { TextInput, View, StyleSheet } from 'react-native'; |
||||
import { colors, fontStyles } from '../../../styles/common'; |
||||
import { strings } from '../../../../locales/i18n'; |
||||
import Fuse from 'fuse.js'; |
||||
import Icon from 'react-native-vector-icons/FontAwesome'; |
||||
import { toLowerCaseEquals } from '../../../util/general'; |
||||
import { useSelector } from 'react-redux'; |
||||
import { getTokenListArray } from '../../../reducers/tokens'; |
||||
import { TokenListToken } from '@metamask/controllers'; |
||||
|
||||
const styles = StyleSheet.create({ |
||||
searchSection: { |
||||
margin: 20, |
||||
marginBottom: 0, |
||||
flex: 1, |
||||
flexDirection: 'row', |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
borderWidth: 1, |
||||
borderRadius: 4, |
||||
borderColor: colors.grey100 |
||||
}, |
||||
textInput: { |
||||
...fontStyles.normal |
||||
} as StyleSheet.NamedStyles<any>, |
||||
icon: { |
||||
padding: 16 |
||||
} |
||||
}); |
||||
|
||||
const fuse = new Fuse([], { |
||||
shouldSort: true, |
||||
threshold: 0.45, |
||||
location: 0, |
||||
distance: 100, |
||||
maxPatternLength: 32, |
||||
minMatchCharLength: 1, |
||||
keys: [{ name: 'name', weight: 0.5 }, { name: 'symbol', weight: 0.5 }] |
||||
}); |
||||
|
||||
type Props = { |
||||
onSearch: ({ results, searchQuery }: { results: TokenListToken[]; searchQuery: string }) => void; |
||||
}; |
||||
|
||||
const AssetSearch = memo(({ onSearch }: Props) => { |
||||
const [searchQuery, setSearchQuery] = useState(''); |
||||
const [inputWidth, setInputWidth] = useState('85%'); |
||||
const tokenList = useSelector<any, TokenListToken[]>(getTokenListArray); |
||||
|
||||
useEffect(() => { |
||||
setTimeout(() => { |
||||
setInputWidth('86%'); |
||||
}, 100); |
||||
}, []); |
||||
|
||||
// Update fuse list
|
||||
useEffect(() => { |
||||
fuse.setCollection(tokenList); |
||||
}, [tokenList]); |
||||
|
||||
const handleSearch = useCallback( |
||||
(searchQuery: string) => { |
||||
setSearchQuery(searchQuery); |
||||
const fuseSearchResult = fuse.search(searchQuery); |
||||
const addressSearchResult = tokenList.filter(token => toLowerCaseEquals(token.address, searchQuery)); |
||||
const results = [...addressSearchResult, ...fuseSearchResult]; |
||||
onSearch({ searchQuery, results }); |
||||
}, |
||||
[setSearchQuery, onSearch, tokenList] |
||||
); |
||||
|
||||
return ( |
||||
<View style={styles.searchSection} testID={'add-searched-token-screen'}> |
||||
<Icon name="search" size={22} style={styles.icon} /> |
||||
<TextInput |
||||
style={[styles.textInput, { width: inputWidth }]} |
||||
value={searchQuery} |
||||
placeholder={strings('token.search_tokens_placeholder')} |
||||
placeholderTextColor={colors.grey100} |
||||
onChangeText={handleSearch} |
||||
testID={'input-search-asset'} |
||||
/> |
||||
</View> |
||||
); |
||||
}); |
||||
|
||||
export default AssetSearch; |