1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-23 03:36:18 +02:00
metamask-extension/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js
Daniel c3b79bb358
Show custom tokens in Swaps, add a custom token in Swaps (#11200)
* Show custom tokens in Swaps

* Add messages for adding a custom token in Swaps

* Add the first version of importing custom tokens in swaps

* Fix lint rules

* Create a new component: ImportToken

* Remove a pointer cursor from regular heading

* Fix a CSS issue for tokens with long names

* Update a comment

* Don’t return a custom token if it doesn’t have symbol or decimals

* Only search by contract address if nothing was found

* Track “Token Imported” event

* Fix unit tests

* Import tracking for “Token Imported”, increase token icon font size

* Disable token import for Source Token

* Update logic and content for notifications, update tests

* Do not hide a dropdown placeholder on click, so a user can click on a link

* Update a key name

* Update styling for the “danger” type notification in Swaps

* Show either a warning or danger notification based on token verification occurences

* Remove testnets from SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP

* Use the “shouldSearchForImports” prop

* Create a new function for handling token import: “onOpenImportTokenModalClick”

* Filter token duplicities before iterating over tokens

* Use “address” instead of “symbol” for checking uniqueness

* Trigger Build

* Use a new API (/token) to get token data for importing in Swaps

* Temporarily decrese Jest threshold for functions
2021-06-03 13:38:37 -02:30

129 lines
3.8 KiB
JavaScript

import React, { useState, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import Fuse from 'fuse.js';
import log from 'loglevel';
import InputAdornment from '@material-ui/core/InputAdornment';
import TextField from '../../../../components/ui/text-field';
import { usePrevious } from '../../../../hooks/usePrevious';
import { isValidHexAddress } from '../../../../../shared/modules/hexstring-utils';
import { fetchToken } from '../../swaps.util';
import { getCurrentChainId } from '../../../../selectors/selectors';
const renderAdornment = () => (
<InputAdornment position="start" style={{ marginRight: '12px' }}>
<img src="images/search.svg" width="17" height="17" alt="" />
</InputAdornment>
);
export default function ListItemSearch({
onSearch,
error,
listToSearch = [],
fuseSearchKeys,
searchPlaceholderText,
defaultToAll,
shouldSearchForImports,
}) {
const fuseRef = useRef();
const [searchQuery, setSearchQuery] = useState('');
const chainId = useSelector(getCurrentChainId);
/**
* Search a custom token for import based on a contract address.
* @param {String} contractAddress
*/
const handleSearchTokenForImport = async (contractAddress) => {
setSearchQuery(contractAddress);
try {
const token = await fetchToken(contractAddress, chainId);
if (token) {
token.primaryLabel = token.symbol;
token.secondaryLabel = token.name;
token.notImported = true;
onSearch({
searchQuery: contractAddress,
results: [token],
});
return;
}
} catch (e) {
log.error('Token not found, show 0 results.', e);
}
onSearch({
searchQuery: contractAddress,
results: [], // No token for import found.
});
};
const handleSearch = async (newSearchQuery) => {
const trimmedNewSearchQuery = newSearchQuery.trim();
const validHexAddress = isValidHexAddress(trimmedNewSearchQuery);
const fuseSearchResult = fuseRef.current.search(newSearchQuery);
const results =
defaultToAll && newSearchQuery === '' ? listToSearch : fuseSearchResult;
if (shouldSearchForImports && results.length === 0 && validHexAddress) {
await handleSearchTokenForImport(trimmedNewSearchQuery);
return;
}
setSearchQuery(newSearchQuery);
onSearch({
searchQuery: newSearchQuery,
results,
});
};
useEffect(() => {
if (!fuseRef.current) {
fuseRef.current = new Fuse(listToSearch, {
shouldSort: true,
threshold: 0.45,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: fuseSearchKeys,
});
}
}, [fuseSearchKeys, listToSearch]);
const previousListToSearch = usePrevious(listToSearch ?? []);
useEffect(() => {
if (
fuseRef.current &&
searchQuery &&
previousListToSearch !== listToSearch
) {
fuseRef.current.setCollection(listToSearch);
const fuseSearchResult = fuseRef.current.search(searchQuery);
onSearch({ searchQuery, results: fuseSearchResult });
}
}, [listToSearch, searchQuery, onSearch, previousListToSearch]);
return (
<TextField
data-testid="search-list-items"
className="searchable-item-list__search"
placeholder={searchPlaceholderText}
type="text"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
error={error}
fullWidth
startAdornment={renderAdornment()}
autoComplete="off"
autoFocus
/>
);
}
ListItemSearch.propTypes = {
onSearch: PropTypes.func,
error: PropTypes.string,
listToSearch: PropTypes.array.isRequired,
fuseSearchKeys: PropTypes.arrayOf(PropTypes.object).isRequired,
searchPlaceholderText: PropTypes.string,
defaultToAll: PropTypes.bool,
shouldSearchForImports: PropTypes.bool,
};