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",