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
Cal Leung 2 years ago committed by GitHub
parent 068dad8242
commit 7f695c4eb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 81
      app/components/Base/Alert.js
  2. 99
      app/components/Base/Alert.tsx
  3. 1
      app/components/Base/RemoteImage/__snapshots__/index.test.tsx.snap
  4. 5
      app/components/Base/RemoteImage/index.js
  5. 2
      app/components/Nav/Main/MainNavigator.js
  6. 13
      app/components/Nav/Main/index.js
  7. 16
      app/components/UI/AccountApproval/index.test.tsx
  8. 22
      app/components/UI/AccountInfoCard/index.test.tsx
  9. 10
      app/components/UI/AccountList/index.test.tsx
  10. 8
      app/components/UI/AccountRightButton/index.test.tsx
  11. 4
      app/components/UI/AddCustomNetwork/index.js
  12. 27
      app/components/UI/AddCustomToken/__snapshots__/index.test.tsx.snap
  13. 31
      app/components/UI/AddCustomToken/index.js
  14. 10
      app/components/UI/ApproveTransactionReview/index.js
  15. 20
      app/components/UI/ApproveTransactionReview/index.test.tsx
  16. 2
      app/components/UI/AssetIcon/__snapshots__/index.test.tsx.snap
  17. 3
      app/components/UI/AssetIcon/index.test.tsx
  18. 18
      app/components/UI/AssetIcon/index.tsx
  19. 5
      app/components/UI/AssetList/index.js
  20. 10
      app/components/UI/AssetOverview/index.js
  21. 48
      app/components/UI/AssetSearch/__snapshots__/index.test.tsx.snap
  22. 96
      app/components/UI/AssetSearch/index.js
  23. 20
      app/components/UI/AssetSearch/index.test.tsx
  24. 88
      app/components/UI/AssetSearch/index.tsx
  25. 5
      app/components/UI/BackupAlert/index.test.tsx
  26. 10
      app/components/UI/CollectibleContractOverview/index.test.tsx
  27. 15
      app/components/UI/CollectibleContracts/index.js
  28. 24
      app/components/UI/CollectibleContracts/index.test.tsx
  29. 4
      app/components/UI/CollectibleModal/index.test.tsx
  30. 16
      app/components/UI/CollectibleOverview/index.test.tsx
  31. 67
      app/components/UI/EthInput/SelectableAsset/index.js
  32. 3
      app/components/UI/EthInput/__snapshots__/index.test.tsx.snap
  33. 701
      app/components/UI/EthInput/index.js
  34. 19
      app/components/UI/EthInput/index.test.tsx
  35. 10
      app/components/UI/NetworkList/index.test.tsx
  36. 16
      app/components/UI/OnboardingWizard/Step3/index.test.tsx
  37. 4
      app/components/UI/OptinMetrics/index.test.tsx
  38. 19
      app/components/UI/PaymentRequest/AssetList/__snapshots__/index.test.tsx.snap
  39. 110
      app/components/UI/PaymentRequest/AssetList/index.js
  40. 22
      app/components/UI/PaymentRequest/AssetList/index.test.tsx
  41. 115
      app/components/UI/PaymentRequest/AssetList/index.tsx
  42. 28
      app/components/UI/PaymentRequest/index.js
  43. 10
      app/components/UI/ReceiveRequest/index.test.tsx
  44. 4
      app/components/UI/SearchTokenAutocomplete/__snapshots__/index.test.tsx.snap
  45. 14
      app/components/UI/SignatureRequest/index.test.tsx
  46. 17
      app/components/UI/Swaps/QuotesView.js
  47. 18
      app/components/UI/Swaps/components/ActionAlert.js
  48. 4
      app/components/UI/Swaps/components/TokenImportModal.js
  49. 7
      app/components/UI/Swaps/index.js
  50. 2
      app/components/UI/TokenImage/__snapshots__/index.test.tsx.snap
  51. 35
      app/components/UI/TokenImage/index.js
  52. 13
      app/components/UI/TokenImage/index.test.tsx
  53. 6
      app/components/UI/Tokens/__snapshots__/index.test.tsx.snap
  54. 78
      app/components/UI/Tokens/index.js
  55. 9
      app/components/UI/Tokens/index.test.tsx
  56. 26
      app/components/UI/TransactionEditor/index.test.tsx
  57. 20
      app/components/UI/TransactionElement/TransactionDetails/index.test.tsx
  58. 20
      app/components/UI/TransactionElement/index.test.tsx
  59. 7
      app/components/UI/TransactionElement/utils.js
  60. 14
      app/components/UI/TransactionReview/TransactionReviewData/index.test.tsx
  61. 12
      app/components/UI/TransactionReview/TransactionReviewEIP1559/index.test.tsx
  62. 12
      app/components/UI/TransactionReview/TransactionReviewFeeCard/index.test.tsx
  63. 20
      app/components/UI/TransactionReview/TransactionReviewInformation/index.test.tsx
  64. 20
      app/components/UI/TransactionReview/TransactionReviewSummary/index.test.tsx
  65. 1
      app/components/UI/TransactionReview/__snapshots__/index.test.tsx.snap
  66. 10
      app/components/UI/TransactionReview/index.js
  67. 31
      app/components/UI/TransactionReview/index.test.tsx
  68. 30
      app/components/UI/Transactions/index.test.tsx
  69. 1
      app/components/UI/WatchAssetRequest/index.js
  70. 10
      app/components/Views/AccountBackupStep1/index.test.tsx
  71. 10
      app/components/Views/AddAsset/index.test.tsx
  72. 18
      app/components/Views/Approval/index.test.tsx
  73. 28
      app/components/Views/ApproveView/Approve/index.test.tsx
  74. 10
      app/components/Views/ChoosePassword/index.test.tsx
  75. 4
      app/components/Views/ChoosePasswordSimple/index.test.tsx
  76. 10
      app/components/Views/Collectible/index.test.tsx
  77. 8
      app/components/Views/Entry/index.test.tsx
  78. 10
      app/components/Views/ExtensionSync/index.test.tsx
  79. 221
      app/components/Views/ExtensionSync/index.tsx
  80. 4
      app/components/Views/ImportFromSeed/index.test.tsx
  81. 4
      app/components/Views/LockScreen/index.test.tsx
  82. 16
      app/components/Views/Login/index.test.tsx
  83. 10
      app/components/Views/ManualBackupStep2/index.test.tsx
  84. 4
      app/components/Views/OfflineMode/index.test.tsx
  85. 10
      app/components/Views/ResetPassword/index.test.tsx
  86. 10
      app/components/Views/RevealPrivateCredential/index.test.tsx
  87. 15
      app/components/Views/Send/index.js
  88. 8
      app/components/Views/SendFlow/AddressElement/index.test.tsx
  89. 24
      app/components/Views/SendFlow/AddressList/index.test.tsx
  90. 1
      app/components/Views/SendFlow/Amount/index.js
  91. 40
      app/components/Views/SendFlow/Amount/index.test.tsx
  92. 32
      app/components/Views/SendFlow/Confirm/index.test.tsx
  93. 52
      app/components/Views/SendFlow/ErrorMessage/__snapshots__/index.test.tsx.snap
  94. 4
      app/components/Views/SendFlow/ErrorMessage/index.js
  95. 2
      app/components/Views/SendFlow/ErrorMessage/index.test.tsx
  96. 38
      app/components/Views/SendFlow/SendTo/index.test.tsx
  97. 39
      app/components/Views/SendFlow/WarningMessage/index.js
  98. 41
      app/components/Views/SendFlow/WarningMessage/index.tsx
  99. 8
      app/components/Views/Settings/AdvancedSettings/index.test.tsx
  100. 20
      app/components/Views/Settings/Contacts/ContactForm/index.test.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -8,6 +8,7 @@ exports[`RemoteImage should render correctly 1`] = `
style={Object {}}
>
<SvgCssUri
fill="black"
height="100%"
source={
Object {

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Image, ViewPropTypes } from 'react-native';
import { Image, ViewPropTypes, View } from 'react-native';
import FadeIn from 'react-native-fade-in-image';
// eslint-disable-next-line import/default
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
@ -21,10 +21,11 @@ const RemoteImage = (props) => {
style.height = source.height;
}
}
return (
<ComponentErrorBoundary onError={props.onError} componentLabel="RemoteImage-SVG">
<View style={style}>
<SvgCssUri {...props} uri={source.uri} width={'100%'} height={'100%'} />
<SvgCssUri {...props} uri={source.uri} width={'100%'} height={'100%'} fill={'black'} />
</View>
</ComponentErrorBoundary>
);

@ -72,7 +72,7 @@ const styles = StyleSheet.create({
const WalletTabHome = () => (
<Stack.Navigator initialRouteName={'WalletView'}>
<Stack.Screen name="WalletView" component={Wallet} options={Wallet.navigationOptions} />
<Stack.Screen name="WalletView" component={Wallet} />
<Stack.Screen name="Asset" component={Asset} options={Asset.navigationOptions} />
<Stack.Screen name="AddAsset" component={AddAsset} options={AddAsset.navigationOptions} />

@ -11,7 +11,7 @@ import {
} from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { connect, useSelector } from 'react-redux';
import GlobalAlert from '../../UI/GlobalAlert';
import BackgroundTimer from 'react-native-background-timer';
import Approval from '../../Views/Approval';
@ -39,7 +39,6 @@ import {
} from '../../../util/transactions';
import { BN } from 'ethereumjs-util';
import Logger from '../../../util/Logger';
import contractMap from '@metamask/contract-metadata';
import MessageSign from '../../UI/MessageSign';
import Approve from '../../Views/ApproveView/Approve';
import TransactionTypes from '../../../core/TransactionTypes';
@ -65,6 +64,7 @@ import Analytics from '../../../core/Analytics';
import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics';
import BigNumber from 'bignumber.js';
import { setInfuraAvailabilityBlocked, setInfuraAvailabilityNotBlocked } from '../../../actions/infuraAvailability';
import { getTokenList } from '../../../reducers/tokens';
import { toLowerCaseEquals } from '../../../util/general';
const styles = StyleSheet.create({
@ -93,7 +93,6 @@ const Main = (props) => {
const [showExpandedMessage, setShowExpandedMessage] = useState(false);
const [currentPageTitle, setCurrentPageTitle] = useState('');
const [currentPageUrl, setCurrentPageUrl] = useState('');
const [showRemindLaterModal, setShowRemindLaterModal] = useState(false);
const [skipCheckbox, setSkipCheckbox] = useState(false);
@ -102,6 +101,8 @@ const Main = (props) => {
const lockManager = useRef();
const removeConnectionStatusListener = useRef();
const tokenList = useSelector(getTokenList);
const setTransactionObject = props.setTransactionObject;
const toggleApproveModal = props.toggleApproveModal;
const toggleDappTransactionModal = props.toggleDappTransactionModal;
@ -342,10 +343,7 @@ const Main = (props) => {
let asset = props.tokens.find(({ address }) => toLowerCaseEquals(address, to));
if (!asset) {
// try to lookup contract by lowercased address `to`
const contractMapKey = Object.keys(contractMap).find((key) => toLowerCaseEquals(key, to));
if (contractMapKey) {
asset = contractMap[contractMapKey];
}
asset = tokenList[to];
if (!asset) {
try {
@ -403,6 +401,7 @@ const Main = (props) => {
toggleApproveModal,
toggleDappTransactionModal,
autoSign,
tokenList,
]
);

@ -10,22 +10,22 @@ const initialState = {
engine: {
backgroundState: {
AccountTrackerController: {
accounts: { '0x2': { balance: '0' } }
accounts: { '0x2': { balance: '0' } },
},
NetworkController: {
provider: {
type: ROPSTEN
}
type: ROPSTEN,
},
},
TokensController: {
tokens: []
tokens: [],
},
PreferencesController: {
selectedAddress: '0xe7E125654064EEa56229f273dA586F10DF96B0a1',
identities: { '0xe7E125654064EEa56229f273dA586F10DF96B0a1': { name: 'Account 1' } }
}
}
}
identities: { '0xe7E125654064EEa56229f273dA586F10DF96B0a1': { name: 'Account 1' } },
},
},
},
};
const store = mockStore(initialState);

@ -11,28 +11,28 @@ const initialState = {
AccountTrackerController: {
accounts: {
'0x0': {
balance: 200
}
}
balance: 200,
},
},
},
PreferencesController: {
selectedAddress: '0x0',
identities: {
address: '0x0',
name: 'Account 1'
}
name: 'Account 1',
},
},
CurrencyRateController: {
conversionRate: 10,
currentCurrency: 'inr'
currentCurrency: 'inr',
},
NetworkController: {
provider: {
ticker: 'eth'
}
}
}
}
ticker: 'eth',
},
},
},
},
};
const store = mockStore(initialState);

@ -11,11 +11,11 @@ const store = mockStore({
backgroundState: {
AccountTrackerController: {
accounts: {
[address]: { name: 'account 1', address, balance: 0 }
}
}
}
}
[address]: { name: 'account 1', address, balance: 0 },
},
},
},
},
});
describe('Accounts', () => {

@ -9,10 +9,10 @@ const store = mockStore({
engine: {
backgroundState: {
PreferencesController: {
selectedAddress: '0xe7E125654064EEa56229f273dA586F10DF96B0a1'
}
}
}
selectedAddress: '0xe7E125654064EEa56229f273dA586F10DF96B0a1',
},
},
},
});
describe('AccountRightButton', () => {

@ -7,7 +7,7 @@ import { strings } from '../../../../locales/i18n';
import { colors, fontStyles } from '../../../styles/common';
import Device from '../../../util/device';
import Icon from 'react-native-vector-icons/FontAwesome';
import Alert from '../../Base/Alert';
import Alert, { AlertType } from '../../Base/Alert';
import EvilIcons from 'react-native-vector-icons/EvilIcons';
import Text from '../../Base/Text';
@ -234,7 +234,7 @@ const AddCustomNetwork = ({ customNetworkInformation, currentPageInformation, on
return (
<Alert
type={'warning'}
type={AlertType.Warning}
testID={'error-message-warning'}
style={styles.alertContainer}
renderIcon={() => <EvilIcons name="bell" style={styles.alertIcon} />}

@ -16,7 +16,7 @@ exports[`AddCustomToken should render correctly 1`] = `
confirmButtonMode="normal"
confirmDisabled={true}
confirmTestID="add-custom-asset-confirm-button"
confirmText="ADD TOKEN"
confirmText="IMPORT"
confirmed={false}
onCancelPress={[Function]}
onConfirmPress={[Function]}
@ -25,6 +25,31 @@ exports[`AddCustomToken should render correctly 1`] = `
testID="add-asset-cancel-button"
>
<View>
<WarningMessage
style={
Object {
"marginHorizontal": 20,
"marginTop": 20,
"paddingRight": 0,
}
}
warningMessage={
<React.Fragment>
Anyone can create a token, including creating fake versions of existing tokens. Learn more about
<Text
onPress={[Function]}
style={
Object {
"color": "#037dd6",
}
}
suppressHighlighting={true}
>
scams and security risks.
</Text>
</React.Fragment>
}
/>
<View
style={
Object {

@ -8,6 +8,8 @@ import { isValidAddress } from 'ethereumjs-util';
import ActionView from '../ActionView';
import { isSmartContractAddress } from '../../../util/transactions';
import AnalyticsV2 from '../../../util/analyticsV2';
import WarningMessage from '../../Views/SendFlow/WarningMessage';
import AppConstants from '../../../core/AppConstants';
const styles = StyleSheet.create({
wrapper: {
@ -29,6 +31,7 @@ const styles = StyleSheet.create({
color: colors.red,
...fontStyles.normal,
},
warningContainer: { marginHorizontal: 20, marginTop: 20, paddingRight: 0 },
});
/**
@ -187,6 +190,33 @@ export default class AddCustomToken extends PureComponent {
current && current.focus();
};
renderWarning = () => (
<WarningMessage
style={styles.warningContainer}
warningMessage={
<>
{strings('add_asset.warning_body_description')}
<Text
suppressHighlighting
onPress={() => {
// TODO: This functionality exists in a bunch of other places. We need to unify this into a utils function
this.props.navigation.navigate('Webview', {
screen: 'SimpleWebview',
params: {
url: AppConstants.URLS.SECURITY,
title: strings('add_asset.security_tips'),
},
});
}}
style={{ color: colors.blue }}
>
{strings('add_asset.warning_link')}
</Text>
</>
}
/>
);
render = () => {
const { address, symbol, decimals } = this.state;
return (
@ -202,6 +232,7 @@ export default class AddCustomToken extends PureComponent {
confirmDisabled={!(address && symbol && decimals)}
>
<View>
{this.renderWarning()}
<View style={styles.rowWrapper}>
<Text style={fontStyles.normal}>{strings('token.token_address')}</Text>
<TextInput

@ -6,7 +6,6 @@ import { getApproveNavbar } from '../../UI/Navbar';
import { colors, fontStyles } from '../../../styles/common';
import { connect } from 'react-redux';
import { getHost } from '../../../util/browser';
import contractMap from '@metamask/contract-metadata';
import { safeToChecksumAddress, renderShortAddress } from '../../../util/address';
import Engine from '../../../core/Engine';
import { strings } from '../../../../locales/i18n';
@ -39,6 +38,7 @@ import EditPermission, { MINIMUM_VALUE } from './EditPermission';
import Logger from '../../../util/Logger';
import InfoModal from '../Swaps/components/InfoModal';
import Text from '../../Base/Text';
import { getTokenList } from '../../../reducers/tokens';
import TransactionReviewEIP1559 from '../../UI/TransactionReview/TransactionReviewEIP1559';
import ClipboardManager from '../../../core/ClipboardManager';
@ -237,6 +237,10 @@ class ApproveTransactionReview extends PureComponent {
* If the gas estimations are ready
*/
gasEstimationReady: PropTypes.bool,
/**
* List of tokens from TokenListController
*/
tokenList: PropTypes.object,
};
state = {
@ -264,11 +268,12 @@ class ApproveTransactionReview extends PureComponent {
const {
transaction: { origin, to, gas, gasPrice, data },
conversionRate,
tokenList,
} = this.props;
const { AssetsContractController } = Engine.context;
const host = getHost(this.originIsWalletConnect ? origin.split(WALLET_CONNECT_ORIGIN)[1] : origin);
let tokenSymbol, tokenDecimals;
const contract = contractMap[safeToChecksumAddress(to)];
const contract = tokenList[safeToChecksumAddress(to)];
if (!contract) {
try {
tokenDecimals = await AssetsContractController.getTokenDecimals(to);
@ -734,6 +739,7 @@ const mapStateToProps = (state) => ({
activeTabUrl: getActiveTabUrl(state),
network: state.engine.backgroundState.NetworkController.network,
chainId: state.engine.backgroundState.NetworkController.provider.chainId,
tokenList: getTokenList(state),
});
const mapDispatchToProps = (dispatch) => ({

@ -9,30 +9,30 @@ const initialState = {
engine: {
backgroundState: {
AccountTrackerController: {
accounts: { '0x2': { balance: '0' } }
accounts: { '0x2': { balance: '0' } },
},
CurrencyRateController: {
conversionRate: 5
conversionRate: 5,
},
NetworkController: {
provider: {
ticker: 'ETH',
type: 'ETH'
}
type: 'ETH',
},
},
TokensController: {
tokens: []
}
}
tokens: [],
},
},
},
transaction: {},
settings: {
primaryCurrency: 'fiat'
primaryCurrency: 'fiat',
},
browser: {
activeTab: 1605778647042,
tabs: [{ id: 1605778647042, url: 'https://metamask.github.io/test-dapp/' }]
}
tabs: [{ id: 1605778647042, url: 'https://metamask.github.io/test-dapp/' }],
},
};
const store = mockStore(initialState);

@ -10,7 +10,7 @@ exports[`AssetIcon should render correctly 1`] = `
}
source={
Object {
"uri": "https://raw.githubusercontent.com/metamask/contract-metadata/master/images/metamark.svg",
"uri": "https://s3.amazonaws.com/airswap-token-images/WBTC.png",
}
}
style={

@ -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,8 +1,8 @@
import React, { memo } from 'react';
import { ImageStyle, StyleSheet, StyleProp } from 'react-native';
import { ImageStyle, StyleSheet, StyleProp, ImageSourcePropType } from 'react-native';
import RemoteImage from '../../Base/RemoteImage';
import getAssetLogoPath from '../../../util/assets';
import { colors } from '../../../styles/common';
import staticLogos from 'images/static-logos';
interface Props {
/**
@ -26,15 +26,23 @@ const styles = StyleSheet.create({
},
});
function isUrl(string: string) {
if (/^(http:\/\/|https:\/\/)/.test(string)) {
return true;
}
return false;
}
/**
* PureComponent that provides an asset icon dependent on OS.
*/
// eslint-disable-next-line react/display-name
const AssetIcon = memo((props: Props) => {
if (!props.logo) return null;
const uri = props.watchedAsset ? props.logo : getAssetLogoPath(props.logo);
if (!props.logo || props.logo.substr(0, 4) === 'ipfs') return null;
const style = [styles.logo, props.customStyle];
return <RemoteImage fadeIn placeholderStyle={{ backgroundColor: colors.white }} source={{ uri }} style={style} />;
const source: ImageSourcePropType = isUrl(props.logo) ? { uri: props.logo } : (staticLogos as any)[props.logo];
return <RemoteImage fadeIn placeholderStyle={{ backgroundColor: colors.white }} source={source} style={style} />;
});
export default AssetIcon;

@ -70,8 +70,9 @@ export default class AssetList extends PureComponent {
<Text style={styles.normalText}>{strings('token.no_tokens_found')}</Text>
) : null}
{searchResults.slice(0, 6).map((_, i) => {
const { symbol, name, address, logo } = searchResults[i] || {};
const { symbol, name, address, iconUrl } = searchResults[i] || {};
const isSelected = selectedAsset && selectedAsset.address === address;
return (
<StyledButton
type={isSelected ? 'normal' : 'transparent'}
@ -81,7 +82,7 @@ export default class AssetList extends PureComponent {
testID={'searched-token-result'}
>
<View style={styles.assetListElement}>
<AssetIcon logo={logo} />
<AssetIcon logo={iconUrl} />
<Text style={styles.text}>
{name} ({symbol})
</Text>

@ -201,17 +201,13 @@ class AssetOverview extends PureComponent {
renderLogo = () => {
const {
asset: { address, image, logo, isETH },
asset: { address, image, logo: iconUrl, isETH },
} = this.props;
if (isETH) {
return <NetworkMainAssetLogo biggest style={styles.ethLogo} />;
}
const watchedAsset = image !== undefined;
return logo || image ? (
<AssetIcon watchedAsset={watchedAsset} logo={image || logo} />
) : (
<Identicon address={address} />
);
return iconUrl || image ? <AssetIcon logo={iconUrl || image} /> : <Identicon address={address} />;
};
componentDidMount = async () => {

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

@ -19,10 +19,7 @@ describe('BackupAlert', () => {
const wrapper = shallow(
<Provider store={store}>
<BackupAlert />
</Provider>,
{
context: { store: mockStore(initialState) },
}
</Provider>
);
expect(wrapper.dive()).toMatchSnapshot();
});

@ -9,10 +9,10 @@ const initialState = {
engine: {
backgroundState: {
CollectiblesController: {
collectibles: []
}
}
}
collectibles: [],
},
},
},
};
const store = mockStore(initialState);
@ -26,7 +26,7 @@ describe('CollectibleContractOverview', () => {
symbol: 'symbol',
description: 'description',
address: '0x123',
totalSupply: 1
totalSupply: 1,
}}
/>
</Provider>

@ -2,7 +2,6 @@ import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity, StyleSheet, View, InteractionManager, Image } from 'react-native';
import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { colors, fontStyles } from '../../../styles/common';
import { strings } from '../../../../locales/i18n';
import CollectibleContractElement from '../CollectibleContractElement';
@ -31,17 +30,19 @@ const styles = StyleSheet.create({
justifyContent: 'center',
},
addText: {
fontSize: 15,
fontSize: 14,
color: colors.blue,
...fontStyles.normal,
},
footer: {
flex: 1,
paddingBottom: 30,
alignItems: 'center',
marginTop: 24,
},
emptyContainer: {
flex: 1,
marginBottom: 42,
marginBottom: 18,
justifyContent: 'center',
alignItems: 'center',
},
@ -55,8 +56,9 @@ const styles = StyleSheet.create({
color: colors.grey200,
},
emptyText: {
color: colors.grey200,
color: colors.greyAssetVisibility,
marginBottom: 8,
fontSize: 14,
},
});
@ -81,8 +83,8 @@ const CollectibleContracts = ({ collectibleContracts, collectibles, navigation,
const renderFooter = () => (
<View style={styles.footer} key={'collectible-contracts-footer'}>
<Text style={styles.emptyText}>{strings('wallet.no_collectibles')}</Text>
<TouchableOpacity style={styles.add} onPress={goToAddCollectible} testID={'add-collectible-button'}>
<Icon name="plus" size={16} color={colors.blue} />
<Text style={styles.addText}>{strings('wallet.add_collectibles')}</Text>
</TouchableOpacity>
</View>
@ -153,9 +155,6 @@ const CollectibleContracts = ({ collectibleContracts, collectibles, navigation,
{strings('wallet.learn_more')}
</Text>
</View>
<Text big style={styles.emptyText}>
{strings('wallet.no_collectibles')}
</Text>
</View>
);

@ -7,17 +7,17 @@ import { Provider } from 'react-redux';
const mockStore = configureMockStore();
const initialState = {
collectibles: {
favorites: {}
favorites: {},
},
engine: {
backgroundState: {
NetworkController: {
provider: {
chainId: 1
}
chainId: 1,
},
},
PreferencesController: {
selectedAddress: '0x1'
selectedAddress: '0x1',
},
CollectiblesController: {
collectibleContracts: [
@ -27,20 +27,20 @@ const initialState = {
address: '0x0',
symbol: 'NM',
description: 'description',
totalSupply: 10
}
totalSupply: 10,
},
],
collectibles: [
{
address: '0x0',
tokenId: 10,
name: 'name',
image: 'image'
}
]
}
}
}
image: 'image',
},
],
},
},
},
};
const store = mockStore(initialState);

@ -17,8 +17,8 @@ describe('CollectibleModal', () => {
route={{
params: {
contractName: 'Opensea',
collectible: { name: 'Leopard', tokenId: 6904, address: '0x123' }
}
collectible: { name: 'Leopard', tokenId: 6904, address: '0x123' },
},
}}
/>
</Provider>

@ -7,20 +7,20 @@ import { Provider } from 'react-redux';
const mockStore = configureMockStore();
const initialState = {
collectibles: {
favorites: {}
favorites: {},
},
engine: {
backgroundState: {
NetworkController: {
provider: {
chainId: 1
}
chainId: 1,
},
},
PreferencesController: {
selectedAddress: '0x1'