diff --git a/app/components/UI/AssetIcon/index.js b/app/components/UI/AssetIcon/index.js index 776152b0332..f5e142abf24 100644 --- a/app/components/UI/AssetIcon/index.js +++ b/app/components/UI/AssetIcon/index.js @@ -18,16 +18,20 @@ const styles = StyleSheet.create({ // eslint-disable-next-line react/display-name const AssetIcon = React.memo(props => { if (!props.logo) return null; - const uri = getAssetLogoPath(props.logo); + const uri = props.watchedAsset ? props.logo : getAssetLogoPath(props.logo); const style = [styles.logo, props.customStyle]; return ; }); AssetIcon.propTypes = { /** - * String of the asset icon + * String of the asset icon to be searched in contractMap */ logo: PropTypes.string, + /** + * Whether logo has to be fetched from eth-contract-metadata + */ + watchedAsset: PropTypes.bool, /** * Custom style to apply to image */ diff --git a/app/components/UI/AssetOverview/index.js b/app/components/UI/AssetOverview/index.js index e6993db38b5..78f65532112 100644 --- a/app/components/UI/AssetOverview/index.js +++ b/app/components/UI/AssetOverview/index.js @@ -92,12 +92,17 @@ class AssetOverview extends Component { renderLogo = () => { const { - asset: { address, logo, symbol } + asset: { address, image, logo, symbol } } = this.props; if (symbol === 'ETH') { return ; } - return logo ? : ; + const watchedAsset = image !== undefined; + return logo || image ? ( + + ) : ( + + ); }; render() { diff --git a/app/components/UI/TokenImage/index.js b/app/components/UI/TokenImage/index.js index 8ae5aa8f8a4..b72868660e6 100644 --- a/app/components/UI/TokenImage/index.js +++ b/app/components/UI/TokenImage/index.js @@ -50,11 +50,12 @@ export default class TokenElement extends Component { asset.logo = contractMap[checksumAddress].logo; } } - + // When image is defined, is coming from a token added by watchAsset, so it has to be handled alone + const watchedAsset = asset.image !== undefined; return ( - {asset.logo ? ( - + {asset.logo || asset.image ? ( + ) : ( )} diff --git a/app/components/UI/WatchAssetRequest/__snapshots__/index.test.js.snap b/app/components/UI/WatchAssetRequest/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..a6d281a27f5 --- /dev/null +++ b/app/components/UI/WatchAssetRequest/__snapshots__/index.test.js.snap @@ -0,0 +1,212 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WatchAssetRequest should render correctly 1`] = ` + + + + Add Suggested Token + + + + + + + Would you like to add this token? + + + + + + + Token + + + + + + + + + TKN + + + + + + + + Balance + + + + + 0 + + TKN + + + + + + + +`; diff --git a/app/components/UI/WatchAssetRequest/index.js b/app/components/UI/WatchAssetRequest/index.js new file mode 100644 index 00000000000..07b3b4471e2 --- /dev/null +++ b/app/components/UI/WatchAssetRequest/index.js @@ -0,0 +1,189 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Platform, StyleSheet, View, Text } from 'react-native'; +import { colors, fontStyles } from '../../../styles/common'; +import { strings } from '../../../../locales/i18n'; +import { connect } from 'react-redux'; +import ActionView from '../ActionView'; +import { renderFromTokenMinimalUnit } from '../../../util/number'; +import TokenImage from '../../UI/TokenImage'; +import DeviceSize from '../../../util/DeviceSize'; +import Engine from '../../../core/Engine'; + +const styles = StyleSheet.create({ + root: { + backgroundColor: colors.white, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + paddingBottom: DeviceSize.isIphoneX() ? 20 : 0, + minHeight: Platform.OS === 'ios' ? '50%' : '60%' + }, + title: { + textAlign: 'center', + fontSize: 18, + marginVertical: 12, + marginHorizontal: 20, + color: colors.fontPrimary, + ...fontStyles.bold + }, + text: { + ...fontStyles.normal, + fontSize: 16, + paddingTop: 25, + paddingHorizontal: 10 + }, + tokenInformation: { + flexDirection: 'row', + marginHorizontal: 40, + flex: 1, + alignItems: 'flex-start', + marginVertical: 30 + }, + tokenInfo: { + flex: 1, + flexDirection: 'column' + }, + infoTitleWrapper: { + alignItems: 'center' + }, + infoTitle: { + ...fontStyles.bold + }, + infoBalance: { + alignItems: 'center' + }, + infoToken: { + alignItems: 'center' + }, + token: { + flexDirection: 'row' + }, + identicon: { + paddingVertical: 10 + }, + signText: { + ...fontStyles.normal, + fontSize: 16 + }, + addMessage: { + flexDirection: 'row', + margin: 20 + }, + children: { + alignItems: 'center', + borderTopColor: colors.lightGray, + borderTopWidth: 1 + } +}); + +/** + * Component that renders watch asset content + */ +class WatchAssetRequest extends Component { + static propTypes = { + /** + * Callback triggered when this message signature is rejected + */ + onCancel: PropTypes.func, + /** + * Callback triggered when this message signature is approved + */ + onConfirm: PropTypes.func, + /** + * Token object + */ + suggestedAssetMeta: PropTypes.object, + /** + * Object containing token balances in the format address => balance + */ + contractBalances: PropTypes.object + }; + + componentWillUnmount = async () => { + const { AssetsController } = Engine.context; + const { suggestedAssetMeta } = this.props; + await AssetsController.rejectWatchAsset(suggestedAssetMeta.id); + }; + + onConfirm = async () => { + const { onConfirm, suggestedAssetMeta } = this.props; + const { AssetsController } = Engine.context; + await AssetsController.acceptWatchAsset(suggestedAssetMeta.id); + onConfirm && onConfirm(); + }; + + render() { + const { + suggestedAssetMeta: { asset }, + contractBalances + } = this.props; + const balance = + asset.address in contractBalances + ? renderFromTokenMinimalUnit(contractBalances[asset.address], asset.decimals) + : '0'; + return ( + + + + {strings('watch_asset_request.title')} + + + + + + {strings('watch_asset_request.message')} + + + + + + {strings('watch_asset_request.token')} + + + + + + + + {asset.symbol} + + + + + + + {strings('watch_asset_request.balance')} + + + + + {balance} {asset.symbol} + + + + + + + + ); + } +} + +const mapStateToProps = state => ({ + contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances +}); + +export default connect(mapStateToProps)(WatchAssetRequest); diff --git a/app/components/UI/WatchAssetRequest/index.test.js b/app/components/UI/WatchAssetRequest/index.test.js new file mode 100644 index 00000000000..9b8c2545128 --- /dev/null +++ b/app/components/UI/WatchAssetRequest/index.test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import WatchAssetRequest from './'; +import configureMockStore from 'redux-mock-store'; +import { BN } from 'ethereumjs-util'; + +const mockStore = configureMockStore(); + +describe('WatchAssetRequest', () => { + it('should render correctly', () => { + const initialState = { + engine: { + backgroundState: { + TokenBalancesController: { + contractBalances: { '0x2': new BN(0) } + } + } + } + }; + + const wrapper = shallow( + , + { + context: { store: mockStore(initialState) } + } + ); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/Views/Browser/__snapshots__/index.test.js.snap b/app/components/Views/Browser/__snapshots__/index.test.js.snap index d07f5b4a215..102960a8edd 100644 --- a/app/components/Views/Browser/__snapshots__/index.test.js.snap +++ b/app/components/Views/Browser/__snapshots__/index.test.js.snap @@ -326,5 +326,52 @@ exports[`Browser should render correctly 1`] = ` goToFilePhishingIssue={[Function]} /> + + + `; diff --git a/app/components/Views/Browser/index.js b/app/components/Views/Browser/index.js index 0ef270041f2..7bf5b28850d 100644 --- a/app/components/Views/Browser/index.js +++ b/app/components/Views/Browser/index.js @@ -53,6 +53,7 @@ import AppConstants from '../../../core/AppConstants'; import SearchApi from 'react-native-search-api'; import DeeplinkManager from '../../../core/DeeplinkManager'; import Branch from 'react-native-branch'; +import WatchAssetRequest from '../../UI/WatchAssetRequest'; const HOMEPAGE_URL = 'about:blank'; const SUPPORTED_TOP_LEVEL_DOMAINS = ['eth', 'test']; @@ -350,7 +351,9 @@ export class Browser extends Component { clampedScroll, contentHeight: 0, forwardEnabled: false, - forceReload: false + forceReload: false, + suggestedAssetMeta: undefined, + watchAsset: false }; } @@ -445,6 +448,15 @@ export class Browser extends Component { }); }); return promise; + }, + wallet_watchAsset: async ({ params }) => { + const { + options: { address, decimals, image, symbol }, + type + } = params; + const { AssetsController } = Engine.context; + const suggestionResult = await AssetsController.watchAsset({ address, symbol, decimals, image }, type); + return suggestionResult.result; } }); @@ -521,6 +533,10 @@ export class Browser extends Component { Engine.context.TypedMessageManager.hub.on('unapprovedMessage', messageParams => { this.setState({ signMessage: true, signMessageParams: messageParams, signType: 'typed' }); }); + + Engine.context.AssetsController.hub.on('pendingSuggestedAsset', suggestedAssetMeta => { + this.setState({ watchAsset: true, suggestedAssetMeta }); + }); this.loadUrl(); Branch.subscribe(this.handleDeeplinks); @@ -619,6 +635,7 @@ export class Browser extends Component { // Remove all Engine listeners Engine.context.PersonalMessageManager.hub.removeAllListeners(); Engine.context.TypedMessageManager.hub.removeAllListeners(); + Engine.context.AssetsController.hub.removeAllListeners(); Engine.context.TransactionController.hub.removeListener('unapprovedTransaction', this.onUnapprovedTransaction); Engine.context.TransactionController.hub.removeListener('networkChange', this.reload); if (Platform.OS === 'ios') { @@ -1451,6 +1468,35 @@ export class Browser extends Component { ); }; + onCancelWatchAsset = () => { + this.setState({ watchAsset: false }); + }; + + renderWatchAssetModal = () => { + const { watchAsset, suggestedAssetMeta } = this.state; + return ( + + + + ); + }; + onAccountsConfirm = () => { const { approveHost, selectedAddress } = this.props; this.setState({ showApprovalDialog: false }); @@ -1604,6 +1650,7 @@ export class Browser extends Component { {this.renderSigningModal()} {this.renderApprovalModal()} {this.renderPhishingModal()} + {this.renderWatchAssetModal()} {this.renderOptions()} {Platform.OS === 'ios' ? this.renderBottomBar(canGoBack, canGoForward) : null} diff --git a/app/util/assets.js b/app/util/assets.js index 866f6c8422f..e61374b1d56 100644 --- a/app/util/assets.js +++ b/app/util/assets.js @@ -1,3 +1,8 @@ +/** + * Utility function to return corresponding eth-contract-metadata logo + * + * @param {string} logo - Logo path from eth-contract-metadata + */ export default function getAssetLogoPath(logo) { if (!logo) return; const path = 'https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/'; diff --git a/locales/en.json b/locales/en.json index 75a8bb46a83..646eb884197 100644 --- a/locales/en.json +++ b/locales/en.json @@ -454,6 +454,14 @@ "balance_title": "Balance:", "message": "Message:" }, + "watch_asset_request": { + "title": "Add Suggested Token", + "cancel": "CANCEL", + "add": "ADD TOKEN", + "message": "Would you like to add this token?", + "token": "Token", + "balance": "Balance" + }, "unit": { "eth": "ETH", "negative": "-", diff --git a/locales/es.json b/locales/es.json index b1e0c9873c9..e7963a37a00 100644 --- a/locales/es.json +++ b/locales/es.json @@ -451,6 +451,14 @@ "balance_title": "Balance:", "message": "Mensaje:" }, + "watch_asset_request": { + "title": "Agregar Token Sugerido", + "cancel": "CANCELar", + "add": "AGREGAR TOKEN", + "message": "Te gustarĂ­a agregar este token?", + "token": "Token", + "balance": "Balance" + }, "unit": { "eth": "ETH", "negative": "-", diff --git a/package-lock.json b/package-lock.json index 01824f5e42b..749aa9e8589 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6254,6 +6254,14 @@ } } }, + "eth-method-registry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eth-method-registry/-/eth-method-registry-1.1.0.tgz", + "integrity": "sha512-jGbbGYd19XJCtoGFtUD2qJYWefKCCbFcu7F/AQ5sJXvqTIVAHnFn3paaV2zhN5t7iyKYp1qxc+ugOky+72xcbg==", + "requires": { + "ethjs": "^0.3.0" + } + }, "eth-phishing-detect": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/eth-phishing-detect/-/eth-phishing-detect-1.1.13.tgz", diff --git a/package.json b/package.json index 2c477aea1dc..8dc415a0c3d 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "ethjs-unit": "0.1.6", "events": "3.0.0", "fuse.js": "3.4.4", - "gaba": "^1.0.0-beta.68", + "gaba": "1.0.0-beta.68", "https-browserify": "0.0.1", "jsc-android": "236355.1.1", "multihashes": "0.4.14",