Skip to content

Commit 026635a

Browse files
committed
feat: simplify token widget, manage searching for address
1 parent c4b26cb commit 026635a

File tree

4 files changed

+115
-136
lines changed

4 files changed

+115
-136
lines changed

web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/AddCustomTokenTab.tsx

-59
This file was deleted.

web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/TokensTab.tsx

-30
This file was deleted.

web/src/pages/NewTransaction/Terms/Payment/GeneralTransaction/TokenAndAmount/TokenSelector/index.tsx

+104-38
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ import React, { useState, useEffect, useRef } from "react";
22
import styled from "styled-components";
33
import Skeleton from "react-loading-skeleton";
44
import { useClickAway } from "react-use";
5-
import { Tabs } from "@kleros/ui-components-library";
5+
import { Searchbar } from "@kleros/ui-components-library";
66
import { Alchemy } from "alchemy-sdk";
77
import { useAccount, useNetwork } from "wagmi";
88
import { useNewTransactionContext } from "context/NewTransactionContext";
99
import { initializeTokens } from "utils/initializeTokens";
1010
import { alchemyConfig } from "utils/alchemyConfig";
1111
import { Overlay } from "components/Overlay";
12-
import TokensTab from "./TokensTab";
13-
import AddCustomTokenTab from "./AddCustomTokenTab";
1412
import { StyledModal } from "pages/MyTransactions/Modal/StyledModal";
1513
import { useLocalStorage } from "hooks/useLocalStorage";
1614

@@ -48,15 +46,25 @@ const DropdownArrow = styled.span`
4846
margin-left: 8px;
4947
`;
5048

51-
export const Item = styled.div`
49+
export const Item = styled.div<{ selected: boolean }>`
5250
display: flex;
5351
align-items: center;
5452
gap: 8px;
55-
padding: 8px;
53+
padding: 10px 16px;
5654
cursor: pointer;
5755
&:hover {
58-
background: ${({ theme }) => theme.mediumBlue};
56+
background: ${({ theme }) => theme.lightBlue};
5957
}
58+
${({ selected, theme }) =>
59+
selected &&
60+
`
61+
background: ${theme.mediumBlue};
62+
border-left: 3px solid ${theme.primaryBlue};
63+
padding-left: 13px;
64+
&:hover {
65+
background: ${theme.mediumBlue};
66+
}
67+
`}
6068
`;
6169

6270
export const TokenLogo = styled.img`
@@ -81,13 +89,40 @@ const StyledLogoSkeleton = styled(Skeleton)`
8189
border-radius: 50%;
8290
`;
8391

84-
const TokenSelector: React.FC = () => {
92+
const ReStyledModal = styled(StyledModal)`
93+
display: flex;
94+
width: 500px;
95+
`;
96+
97+
const StyledSearchbar = styled(Searchbar)`
98+
width: 100%;
99+
input {
100+
font-size: 16px;
101+
}
102+
`;
103+
104+
const ItemsContainer = styled.div`
105+
display: flex;
106+
width: 100%;
107+
flex-direction: column;
108+
margin-top: 24px;
109+
`;
110+
111+
const StyledP = styled.p`
112+
display: flex;
113+
align-self: flex-start;
114+
font-weight: 600;
115+
margin: 0;
116+
margin-bottom: 28px;
117+
`;
118+
119+
const TokenSelector = () => {
85120
const { address } = useAccount();
86121
const { chain } = useNetwork();
87122
const { sendingToken, setSendingToken } = useNewTransactionContext();
88-
const [tokens, setTokens] = useLocalStorage<any[]>("tokens", []);
123+
const [tokens, setTokens] = useLocalStorage("tokens", []);
124+
const [filteredTokens, setFilteredTokens] = useState([]);
89125
const [isOpen, setIsOpen] = useState(false);
90-
const [activeTab, setActiveTab] = useState("tokens");
91126
const [searchQuery, setSearchQuery] = useState("");
92127
const containerRef = useRef(null);
93128
const [loading, setLoading] = useState(true);
@@ -101,20 +136,56 @@ const TokenSelector: React.FC = () => {
101136
}, [address, chain]);
102137

103138
useEffect(() => {
104-
if (tokens.length > 0) {
139+
if (tokens?.length > 0) {
105140
const nativeToken = tokens.find((token) => token.address === "native");
106141
setSendingToken(JSON.parse(localStorage.getItem("selectedToken")) || nativeToken);
107142
}
108-
}, [tokens]);
143+
}, [tokens, setSendingToken]);
109144

110145
const handleSelectToken = (token) => {
111146
setSendingToken(token);
112147
localStorage.setItem("selectedToken", JSON.stringify(token));
113148
setIsOpen(false);
114149
};
115150

116-
const filteredTokens =
117-
tokens && tokens.filter((token) => token?.symbol?.toLowerCase().includes(searchQuery?.toLowerCase()));
151+
const handleSearch = async (query) => {
152+
setSearchQuery(query);
153+
154+
if (!query) {
155+
setFilteredTokens(tokens);
156+
return;
157+
}
158+
159+
const isAddress = query.startsWith("0x") && query.length === 42;
160+
if (isAddress) {
161+
try {
162+
const metadata = await alchemyInstance.core.getTokenMetadata(query.toLowerCase());
163+
const resultToken = {
164+
symbol: metadata.symbol,
165+
address: query.toLowerCase(),
166+
logo: metadata.logo || "https://via.placeholder.com/24",
167+
};
168+
169+
const updatedTokens = [...tokens, resultToken];
170+
const uniqueTokens = Array.from(new Set(updatedTokens.map((a) => a.address))).map((address) => {
171+
return updatedTokens.find((a) => a.address === address);
172+
});
173+
174+
setFilteredTokens([resultToken]);
175+
setTokens(uniqueTokens);
176+
localStorage.setItem("tokens", JSON.stringify(uniqueTokens));
177+
} catch (error) {
178+
console.error("Error fetching token info:", error);
179+
}
180+
} else {
181+
const filteredTokens = tokens.filter((token) => token.symbol.toLowerCase().includes(query.toLowerCase()));
182+
setFilteredTokens(filteredTokens);
183+
}
184+
};
185+
186+
const tokensToDisplay = searchQuery
187+
? filteredTokens
188+
: [sendingToken, ...tokens.filter((token) => token.address !== sendingToken?.address)];
118189

119190
return (
120191
<TokenSelectorWrapper>
@@ -126,38 +197,33 @@ const TokenSelector: React.FC = () => {
126197
) : (
127198
sendingToken && <TokenLogo src={sendingToken.logo} alt={`${sendingToken.symbol} logo`} />
128199
)}
129-
{loading ? <Skeleton width={40} height={16} /> : sendingToken ? sendingToken.symbol : "Select a token"}
200+
{loading ? <Skeleton width={40} height={16} /> : sendingToken?.symbol}
130201
</DropdownContent>
131202
<DropdownArrow />
132203
</DropdownButton>
133204
{isOpen && (
134205
<>
135206
<Overlay />
136-
<StyledModal ref={containerRef}>
137-
<Tabs
138-
items={[
139-
{ text: "Tokens", value: "tokens" },
140-
{ text: "Add Custom Token", value: "addCustomToken" },
141-
]}
142-
callback={setActiveTab}
143-
currentValue={activeTab}
207+
<ReStyledModal ref={containerRef}>
208+
<StyledP>Select a token</StyledP>
209+
<StyledSearchbar
210+
placeholder="Search by name or paste address"
211+
value={searchQuery}
212+
onChange={(e) => handleSearch(e.target.value)}
144213
/>
145-
{activeTab === "tokens" && (
146-
<TokensTab
147-
searchQuery={searchQuery}
148-
setSearchQuery={setSearchQuery}
149-
filteredTokens={filteredTokens}
150-
handleSelectToken={handleSelectToken}
151-
/>
152-
)}
153-
{activeTab === "addCustomToken" && (
154-
<AddCustomTokenTab
155-
setTokens={setTokens}
156-
setActiveTab={setActiveTab}
157-
alchemyInstance={alchemyInstance}
158-
/>
159-
)}
160-
</StyledModal>
214+
<ItemsContainer>
215+
{tokensToDisplay?.map((token) => (
216+
<Item
217+
key={token.address}
218+
onClick={() => handleSelectToken(token)}
219+
selected={sendingToken?.address === token.address}
220+
>
221+
<TokenLogo src={token.logo} alt={`${token?.symbol} logo`} />
222+
<TokenLabel>{token.symbol}</TokenLabel>
223+
</Item>
224+
))}
225+
</ItemsContainer>
226+
</ReStyledModal>
161227
</>
162228
)}
163229
</Container>

web/src/utils/initializeTokens.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ export const initializeTokens = async (address: string, setTokens, setLoading, c
77
setLoading(true);
88
const nativeToken = fetchNativeToken(chain);
99
const balances = await alchemyInstance.core.getTokenBalances(address);
10-
const tokenList = balances.tokenBalances.map(async (token) => {
11-
const tokenInfo = await fetchTokenInfo(token.contractAddress, alchemyInstance);
12-
return {
13-
symbol: tokenInfo.symbol,
14-
address: token.contractAddress,
15-
logo: tokenInfo.logo,
16-
};
17-
});
18-
const allTokens = [nativeToken, ...(await Promise.all(tokenList))];
10+
const tokenList = await Promise.all(
11+
balances.tokenBalances.map(async (token) => {
12+
const tokenInfo = await fetchTokenInfo(token.contractAddress, alchemyInstance);
13+
return {
14+
symbol: tokenInfo.symbol,
15+
address: token.contractAddress,
16+
logo: tokenInfo.logo,
17+
};
18+
})
19+
);
20+
const allTokens = [nativeToken, ...tokenList];
1921
const customTokens = JSON.parse(localStorage.getItem("tokens")) || [];
2022
const combinedTokens = [
2123
...allTokens,

0 commit comments

Comments
 (0)