From 3a2100e4eb5fe8a682e100f7896c6f4cacd475a1 Mon Sep 17 00:00:00 2001 From: richardo2016x Date: Tue, 23 Apr 2024 15:49:43 +0800 Subject: [PATCH 1/9] feat: support standalone add token entry. --- _raw/locales/en/messages.json | 7 + src/background/service/preference.ts | 21 +- src/ui/assets/dashboard/tab-history.svg | 7 - src/ui/assets/dashboard/tab-list.svg | 8 - src/ui/assets/dashboard/tab-summary.svg | 5 - src/ui/hooks/useRefState.ts | 19 + src/ui/hooks/useSearchToken.ts | 267 +++++++++++- src/ui/utils/portfolio/project.ts | 6 +- src/ui/views/AddressDetail/GnosisSafeInfo.tsx | 2 +- .../Approval/components/Connect/index.tsx | 2 +- .../components/TypedDataActions/utils.ts | 4 +- .../CommonPopup/AssetList/AddTokenEntry.tsx | 44 ++ .../AssetList/AssetListContainer.tsx | 42 +- .../CustomAssetList/AddCustomTokenPopup.tsx | 388 ++++++++++++++++++ .../views/CommonPopup/AssetList/TokenTabs.tsx | 67 --- .../AssetList/icons/add-entry-cc.svg | 5 + src/utils/chain.ts | 3 +- 17 files changed, 761 insertions(+), 136 deletions(-) delete mode 100644 src/ui/assets/dashboard/tab-history.svg delete mode 100644 src/ui/assets/dashboard/tab-list.svg delete mode 100644 src/ui/assets/dashboard/tab-summary.svg create mode 100644 src/ui/hooks/useRefState.ts create mode 100644 src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx create mode 100644 src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx delete mode 100644 src/ui/views/CommonPopup/AssetList/TokenTabs.tsx create mode 100644 src/ui/views/CommonPopup/AssetList/icons/add-entry-cc.svg diff --git a/_raw/locales/en/messages.json b/_raw/locales/en/messages.json index 0c7151b217e..faf682c1c7c 100644 --- a/_raw/locales/en/messages.json +++ b/_raw/locales/en/messages.json @@ -1107,6 +1107,13 @@ "customDescription": "Custom token added by you will be shown here", "comingSoon": "Coming Soon...", "searchPlaceholder": "Tokens", + "AddMainnetToken": { + "title": "Add Custom Token", + "selectChain": "Select chain", + "searching": "Searching Token", + "tokenAddress": "Token Address", + "notFound": "Token not found" + }, "AddTestnetToken": { "title": "Add Custom Network Token", "selectChain": "Select chain", diff --git a/src/background/service/preference.ts b/src/background/service/preference.ts index ceb90b20a90..effc812dbd6 100644 --- a/src/background/service/preference.ts +++ b/src/background/service/preference.ts @@ -605,19 +605,18 @@ class PreferenceService { getCustomizedToken = () => { return this.store.customizedToken || []; }; + hasCustomizedToken = (token: Token) => { + return !!this.store.customizedToken?.find( + (item) => + isSameAddress(item.address, token.address) && item.chain === token.chain + ); + }; addCustomizedToken = (token: Token) => { - if ( - !this.store.customizedToken?.find( - (item) => - isSameAddress(item.address, token.address) && - item.chain === token.chain - ) - ) { - this.store.customizedToken = [ - ...(this.store.customizedToken || []), - token, - ]; + if (this.hasCustomizedToken(token)) { + throw new Error('Token already added'); } + + this.store.customizedToken = [...(this.store.customizedToken || []), token]; }; removeCustomizedToken = (token: Token) => { this.store.customizedToken = this.store.customizedToken?.filter( diff --git a/src/ui/assets/dashboard/tab-history.svg b/src/ui/assets/dashboard/tab-history.svg deleted file mode 100644 index 99480a5458c..00000000000 --- a/src/ui/assets/dashboard/tab-history.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/ui/assets/dashboard/tab-list.svg b/src/ui/assets/dashboard/tab-list.svg deleted file mode 100644 index 491f6e329ec..00000000000 --- a/src/ui/assets/dashboard/tab-list.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/ui/assets/dashboard/tab-summary.svg b/src/ui/assets/dashboard/tab-summary.svg deleted file mode 100644 index 0b2a9f14545..00000000000 --- a/src/ui/assets/dashboard/tab-summary.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/ui/hooks/useRefState.ts b/src/ui/hooks/useRefState.ts new file mode 100644 index 00000000000..7c55f78b9df --- /dev/null +++ b/src/ui/hooks/useRefState.ts @@ -0,0 +1,19 @@ +import React, { useCallback, useRef, useState } from 'react'; + +export function useRefState(initValue: T | null) { + const stateRef = useRef(initValue) as React.MutableRefObject; + const [, setSpinner] = useState(false); + + const setRefState = useCallback((newState: T, triggerRerender = true) => { + stateRef.current = newState; + if (triggerRerender) { + setSpinner((prev) => !prev); + } + }, []); + + return { + state: stateRef.current, + stateRef, + setRefState, + } as const; +} diff --git a/src/ui/hooks/useSearchToken.ts b/src/ui/hooks/useSearchToken.ts index bf2106d4fbb..58ce6b5ac1d 100644 --- a/src/ui/hooks/useSearchToken.ts +++ b/src/ui/hooks/useSearchToken.ts @@ -1,12 +1,273 @@ -import { useEffect, useRef, useState, useCallback } from 'react'; +import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import { useWallet } from '../utils/WalletContext'; import { TokenItem } from '@rabby-wallet/rabby-api/dist/types'; -import { DisplayedToken } from '../utils/portfolio/project'; +import { + DisplayedToken, + encodeProjectTokenId, +} from '../utils/portfolio/project'; import { AbstractPortfolioToken } from '../utils/portfolio/types'; -import { useRabbySelector } from 'ui/store'; +import { useRabbyDispatch, useRabbySelector } from 'ui/store'; import { isSameAddress } from '../utils'; import { requestOpenApiWithChainId } from '../utils/openapi'; import { findChainByServerID } from '@/utils/chain'; +import { Chain } from '@debank/common'; +import useDebounceValue from './useDebounceValue'; +import { useRefState } from './useRefState'; + +function isSearchInputWeb3Address(q: string) { + return q.length === 42 && q.toLowerCase().startsWith('0x'); +} + +export function useIsTokenAddedLocally(token?: TokenItem | null) { + const { customize, blocked } = useRabbySelector( + (state) => state.account.tokens + ); + + const addedInfo = useMemo(() => { + if (!token) return { onCustomize: false, onBlocked: false, isLocal: false }; + + const onCustomize = !!customize.find( + (t) => t.id === encodeProjectTokenId(token) + ); + const onBlocked = + !onCustomize && + !!blocked.find( + (t) => t.chain && token.chain && isSameAddress(t.id, token.id) + ); + + return { + onCustomize, + onBlocked, + isLocal: onCustomize || onBlocked, + }; + }, [customize, token?.id]); + + return addedInfo; +} + +export function varyTokensByLocal< + T extends TokenItem[] | AbstractPortfolioToken[] +>( + tokenList: T, + input: { + customize?: AbstractPortfolioToken[]; + blocked?: AbstractPortfolioToken[]; + } +) { + const { customize = [], blocked = [] } = input; + + const varied = { + remote: [] as TokenItem[], + local: [] as TokenItem[], + }; + const localMap = {} as Record; + const wholeList = customize.concat(blocked); + for (let i = 0; i < wholeList.length; i++) { + const item = wholeList[i]; + localMap[`${item.chain}-${item.id}`] = item; + } + + tokenList.forEach((token) => { + const matched = localMap[`${token.chain}-${token.id}`]; + if (matched) { + varied.local.push(matched); + } else { + varied.remote.push(token); + } + }); + + return varied; +} + +export function useVaryTokensByLocal< + T extends TokenItem[] | AbstractPortfolioToken[] +>(tokenList: T) { + const { customize, blocked } = useRabbySelector( + (state) => state.account.tokens + ); + + const varied = useMemo(() => { + return varyTokensByLocal(tokenList, { customize, blocked }); + }, [customize, blocked]); + + return varied; +} + +export function useOperateCustomToken() { + const dispatch = useRabbyDispatch(); + + const addToken = useCallback(async (tokenWithAmount: TokenItem) => { + if (!tokenWithAmount) return; + + if (tokenWithAmount.is_core) { + return dispatch.account.addBlockedToken( + new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken + ); + } else { + return dispatch.account.addCustomizeToken( + new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken + ); + } + }, []); + + const removeToken = useCallback(async (tokenWithAmount: TokenItem) => { + if (!tokenWithAmount) return; + + if (tokenWithAmount?.is_core) { + return dispatch.account.removeBlockedToken( + new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken + ); + } else { + return dispatch.account.removeCustomizeToken( + new DisplayedToken(tokenWithAmount) as AbstractPortfolioToken + ); + } + }, []); + + return { + addToken, + removeToken, + }; +} + +/** eslint-enable react-hooks/exhaustive-deps */ +export function useFindCustomToken(input?: { + // address: string, + chainServerId?: Chain['serverId']; + isTestnet?: boolean; + autoSearch?: boolean; +}) { + const { + // address, + // chainServerId: _propchainServerId, + isTestnet = false, + autoSearch, + } = input || {}; + + const wallet = useWallet(); + const [{ tokenList, portfolioTokenList }, setLists] = useState<{ + tokenList: TokenItem[]; + portfolioTokenList: AbstractPortfolioToken[]; + }>({ + tokenList: [], + portfolioTokenList: [], + }); + const [isLoading, setIsLoading] = useState(false); + const { + state: searchKeyword, + setRefState: setSearchKeyword, + stateRef: skRef, + } = useRefState(''); + const debouncedSearchKeyword = useDebounceValue(searchKeyword, 150); + const { customize, blocked } = useRabbySelector( + (state) => state.account.tokens + ); + + const searchCustomToken = useCallback( + async ( + opt: { + address?: string; + q?: string; + chainServerId?: Chain['serverId']; + } = {} + ) => { + const { address, q = debouncedSearchKeyword, chainServerId } = opt || {}; + + const lists: { + tokenList: TokenItem[]; + portfolioTokenList: AbstractPortfolioToken[]; + } = { + tokenList: [], + portfolioTokenList: [], + }; + if (!address) return lists; + + const chainItem = !chainServerId + ? null + : findChainByServerID(chainServerId); + if (isTestnet || chainItem?.isTestnet) { + return; + } + + setIsLoading(true); + + try { + if (isSearchInputWeb3Address(q)) { + lists.tokenList = await requestOpenApiWithChainId( + (ctx) => ctx.openapi.searchToken(address, q, chainServerId, true), + { + isTestnet: !!isTestnet || !!chainItem?.isTestnet, + wallet, + } + ); + } else { + lists.tokenList = await requestOpenApiWithChainId( + (ctx) => ctx.openapi.searchToken(address, q, chainServerId), + { + isTestnet: !!isTestnet || !!chainItem?.isTestnet, + wallet, + } + ); + } + + if (q === skRef.current) { + // const reg = new RegExp(debouncedSearchKeyword, 'i'); + // const matchCustomTokens = customize.filter((token) => { + // return ( + // reg.test(token.name) || + // reg.test(token.symbol) || + // reg.test(token.display_symbol || '') + // ); + // }); + + lists.portfolioTokenList = [ + ...(lists.tokenList.map( + (item) => new DisplayedToken(item) + ) as AbstractPortfolioToken[]), + // ...matchCustomTokens, + ].filter((item) => { + const isBlocked = !!blocked.find((b) => + isSameAddress(b.id, item.id) + ); + return !isBlocked; + }); + } + setLists(lists); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + + return lists; + }, + [debouncedSearchKeyword, blocked, isTestnet] + ); + + useEffect(() => { + if (autoSearch) { + searchCustomToken(); + } + }, [autoSearch, searchCustomToken]); + + const resetSearchResult = useCallback(() => { + setLists({ + tokenList: [], + portfolioTokenList: [], + }); + }, []); + + return { + searchCustomToken, + debouncedSearchKeyword, + setSearchKeyword, + resetSearchResult, + tokenList, + foundTokenList: portfolioTokenList, + isLoading, + }; +} +/** eslint-disable react-hooks/exhaustive-deps */ const useSearchToken = ( address: string | undefined, diff --git a/src/ui/utils/portfolio/project.ts b/src/ui/utils/portfolio/project.ts index 00b653832da..5ba0ccae7c4 100644 --- a/src/ui/utils/portfolio/project.ts +++ b/src/ui/utils/portfolio/project.ts @@ -297,6 +297,10 @@ class DisplayedPortfolio implements AbstractPortfolio { // static createFromHistory(h: PortfolioItem) {} } +export function encodeProjectTokenId(token: PortfolioItemToken) { + return token.id + token.chain; +} + export class DisplayedToken implements AbstractPortfolioToken { [immerable] = true; id: string; @@ -332,7 +336,7 @@ export class DisplayedToken implements AbstractPortfolioToken { constructor(token: PortfolioItemToken) { this._tokenId = token.id; this.amount = token.amount || 0; - this.id = token.id + token.chain; + this.id = encodeProjectTokenId(token); this.chain = token.chain; this.logo_url = token.logo_url; this.price = token.price || 0; diff --git a/src/ui/views/AddressDetail/GnosisSafeInfo.tsx b/src/ui/views/AddressDetail/GnosisSafeInfo.tsx index 94cc109d357..b1c0b7c2e57 100644 --- a/src/ui/views/AddressDetail/GnosisSafeInfo.tsx +++ b/src/ui/views/AddressDetail/GnosisSafeInfo.tsx @@ -50,7 +50,7 @@ export const GnonisSafeInfo = ({ const wallet = useWallet(); const [activeData, setActiveData] = useState< | { - chain?: Chain; + chain?: Chain | null; data: BasicSafeInfo; } | undefined diff --git a/src/ui/views/Approval/components/Connect/index.tsx b/src/ui/views/Approval/components/Connect/index.tsx index 02fbaa93f91..a9a041c799d 100644 --- a/src/ui/views/Approval/components/Connect/index.tsx +++ b/src/ui/views/Approval/components/Connect/index.tsx @@ -491,7 +491,7 @@ const Connect = ({ params: { icon, origin } }: ConnectProps) => { account!.address, origin ); - let targetChain: Chain | undefined; + let targetChain: Chain | null | undefined; for (let i = 0; i < recommendChains.length; i++) { targetChain = findChain({ serverId: recommendChains[i].id, diff --git a/src/ui/views/Approval/components/TypedDataActions/utils.ts b/src/ui/views/Approval/components/TypedDataActions/utils.ts index 85bd63c32d5..50de4571fe8 100644 --- a/src/ui/views/Approval/components/TypedDataActions/utils.ts +++ b/src/ui/views/Approval/components/TypedDataActions/utils.ts @@ -691,7 +691,7 @@ export const fetchRequireData = async ( sender: string, wallet: WalletControllerType ): Promise => { - let chain: Chain | undefined; + let chain: Chain | null | undefined; if (actionData.chainId) { chain = findChain({ id: Number(actionData.chainId), @@ -928,7 +928,7 @@ export const formatSecurityEngineCtx = async ({ requireData: TypedDataRequireData; wallet: WalletControllerType; }): Promise => { - let chain: Chain | undefined; + let chain: Chain | null | undefined; if (actionData?.chainId) { chain = findChain({ id: Number(actionData.chainId), diff --git a/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx b/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx new file mode 100644 index 00000000000..7715c2e3781 --- /dev/null +++ b/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +import { ReactComponent as RcAddEntryCC } from './icons/add-entry-cc.svg'; +import clsx from 'clsx'; +import { AddCustomTokenPopup } from './CustomAssetList/AddCustomTokenPopup'; + +export default function AddTokenEntry({ + onConfirm, +}: { + onConfirm: React.ComponentProps['onConfirm']; +}) { + const [isShowAddModal, setIsShowAddModal] = React.useState(false); + + return ( + <> +
{ + setIsShowAddModal(true); + }} + > + + Add Tokens +
+ + { + setIsShowAddModal(false); + }} + onConfirm={() => { + setIsShowAddModal(false); + // refreshAsync(); + onConfirm?.(); + }} + /> + + ); +} diff --git a/src/ui/views/CommonPopup/AssetList/AssetListContainer.tsx b/src/ui/views/CommonPopup/AssetList/AssetListContainer.tsx index 29396e893d3..1af513f7eea 100644 --- a/src/ui/views/CommonPopup/AssetList/AssetListContainer.tsx +++ b/src/ui/views/CommonPopup/AssetList/AssetListContainer.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { TokenSearchInput } from './TokenSearchInput'; -import { TokenTabEnum, TokenTabs } from './TokenTabs'; +import AddTokenEntry from './AddTokenEntry'; import { useRabbySelector } from '@/ui/store'; import { TokenList } from './TokenList'; import useSortTokens from 'ui/hooks/useSortTokens'; @@ -48,9 +48,7 @@ export const AssetListContainer: React.FC = ({ blockedTokens, customizeTokens, } = useQueryProjects(currentAccount?.address, false, visible, isTestnet); - const [activeTab, setActiveTab] = React.useState( - TokenTabEnum.List - ); + const isEmptyAssets = !isTokensLoading && !tokenList.length && @@ -98,7 +96,6 @@ export const AssetListContainer: React.FC = ({ React.useEffect(() => { if (!visible) { - setActiveTab(TokenTabEnum.List); setSearch(''); inputRef.current?.setValue(''); inputRef.current?.focus(); @@ -132,40 +129,27 @@ export const AssetListContainer: React.FC = ({ }} className={isFocus || search ? 'w-[360px]' : 'w-[160px]'} /> - {isFocus || search ? null : ( - - )} + {isFocus || search ? null : {}} />} {isTokensLoading || isSearching ? ( ) : (
- {(activeTab === TokenTabEnum.List || search) && ( - - )} - {activeTab === TokenTabEnum.Summary && !search && ( - - )} - {activeTab === TokenTabEnum.History && !search && } +
)}
{isPortfoliosLoading ? ( diff --git a/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx b/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx new file mode 100644 index 00000000000..45dfc6a720d --- /dev/null +++ b/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx @@ -0,0 +1,388 @@ +/** eslint-enable react-hooks/exhaustive-deps */ +import IconUnknown from '@/ui/assets/token-default.svg'; +import { Popup } from '@/ui/component'; +import ChainSelectorModal from '@/ui/component/ChainSelector/Modal'; +import { TooltipWithMagnetArrow } from '@/ui/component/Tooltip/TooltipWithMagnetArrow'; +import { formatAmount, useWallet } from '@/ui/utils'; +import { findChain, getChainList } from '@/utils/chain'; +import { CHAINS_ENUM } from '@debank/common'; +import { useRequest, useSetState } from 'ahooks'; +import { Button, Form, Input, Spin, message } from 'antd'; +import { useForm } from 'antd/lib/form/Form'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { Loading3QuartersOutlined } from '@ant-design/icons'; +import { ReactComponent as RcIconDown } from '@/ui/assets/dashboard/portfolio/cc-down.svg'; +import { ReactComponent as RcIconCheck } from '@/ui/assets/dashboard/portfolio/cc-check.svg'; +import { ReactComponent as RcIconChecked } from '@/ui/assets/dashboard/portfolio/cc-checked.svg'; +import clsx from 'clsx'; +import { useThemeMode } from '@/ui/hooks/usePreference'; +import { + useOperateCustomToken, + useFindCustomToken, + useIsTokenAddedLocally, +} from '@/ui/hooks/useSearchToken'; + +interface Props { + visible?: boolean; + onClose?(): void; + onConfirm?(): void; +} + +const Wraper = styled.div` + .ant-form-item { + margin-bottom: 16px; + } + .ant-form-item-label > label { + color: var(--r-neutral-body, #3e495e); + font-size: 13px; + line-height: 16px; + } + + .ant-input { + height: 52px; + width: 100%; + margin-left: auto; + margin-right: auto; + background: transparent; + border: 1px solid var(--r-neutral-line, #d3d8e0); + border-radius: 6px; + + color: var(--r-neutral-title1, #192945); + font-size: 15px; + font-weight: 500; + + &:focus { + border-color: var(--r-blue-default, #7084ff); + } + } + .ant-input[disabled] { + background: var(--r-neutral-card2, #f2f4f7); + border-color: transparent; + &:hover { + border-color: transparent; + } + } + .ant-form-item-has-error .ant-input, + .ant-form-item-has-error .ant-input:hover { + border: 1px solid var(--r-red-default, #e34935); + } + + .ant-form-item-explain.ant-form-item-explain-error { + color: var(--r-red-default, #e34935); + font-size: 13px; + line-height: 16px; + min-height: 16px; + } +`; + +const Footer = styled.div` + height: 76px; + border-top: 0.5px solid var(--r-neutral-line, rgba(255, 255, 255, 0.1)); + background: var(--r-neutral-card-1, rgba(255, 255, 255, 0.06)); + padding: 16px 20px; + display: flex; + justify-content: space-between; + gap: 16px; + width: 100%; + position: absolute; + left: 0; + right: 0; + bottom: 0; +`; + +/** + * @description now this popup only server mainnet chains' token list + */ +export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { + const wallet = useWallet(); + const [chainSelectorState, setChainSelectorState] = useSetState<{ + visible: boolean; + chain: CHAINS_ENUM | null; + }>({ + visible: false, + chain: getChainList('mainnet')?.[0]?.enum || null, + }); + + const chain = findChain({ enum: chainSelectorState.chain }); + const [tokenId, setTokenId] = useState(''); + + const [checked, setChecked] = useState(false); + const { t } = useTranslation(); + const [form] = useForm(); + + const { resetSearchResult, searchCustomToken } = useFindCustomToken(); + + const { + data, + runAsync: doSearch, + loading: isSearchingToken, + error, + } = useRequest( + async () => { + if (!chain?.id || !tokenId) { + return null; + } + + const currentAccount = await wallet.getCurrentAccount(); + setChecked(false); + form.setFields([ + { + name: 'address', + errors: [], + }, + ]); + + return searchCustomToken({ + address: currentAccount!.address, + chainServerId: chain.serverId, + q: tokenId, + }); + }, + { + manual: true, + + onError: (e) => { + form.setFields([ + { + name: 'address', + errors: [t('page.dashboard.assets.AddMainnetToken.notFound')], + }, + ]); + }, + } + ); + + useEffect(() => { + if (tokenId) { + doSearch(); + } + }, [chain?.serverId, tokenId]); + + const { tokenList } = data || {}; + const token = useMemo(() => tokenList?.[0], [tokenList]); + // const { isLocal: isLocalToken } = useIsTokenAddedLocally(token); + + const { addToken } = useOperateCustomToken(); + + const { runAsync: runAddToken, loading: isSubmitting } = useRequest( + async () => { + if (!token || !chain?.id || !tokenId) { + return null; + } + return addToken(token); + }, + { + manual: true, + } + ); + + const handleConfirm = useCallback(async () => { + try { + await runAddToken(); + onConfirm?.(); + } catch (e) { + message.error(e?.message); + } + }, [runAddToken, onConfirm]); + + useEffect(() => { + if (!visible) { + setChainSelectorState({ + visible: false, + chain: getChainList('mainnet')?.[0]?.enum || null, + }); + resetSearchResult(); + setTokenId(''); + setChecked(false); + form.resetFields(); + } + }, [visible, resetSearchResult]); + + const { isDarkTheme } = useThemeMode(); + + return ( + <> + + {t('page.dashboard.assets.AddMainnetToken.title')} +
+ } + maskStyle={ + isDarkTheme + ? { + backgroundColor: 'transparent', + } + : undefined + } + > + +
+ +
{ + setChainSelectorState({ + visible: true, + }); + }} + > + {!chain ? ( +
+
+ {t('page.dashboard.assets.AddMainnetToken.selectChain')} +
+
+ +
+
+ ) : ( +
+ +
+ {chain?.name} +
+
+ +
+
+ )} +
+
+ + { + setTokenId(e.target.value); + }} + autoComplete="off" + /> + + {isSearchingToken ? ( +
+ {' '} + {t('page.dashboard.assets.AddMainnetToken.searching')} +
+ ) : ( + <> + {token && !error ? ( + +
{ + // if (isLocalToken) return; + setChecked((v) => !v); + }} + className={clsx( + 'flex items-center gap-[12px] rounded-[6px] cursor-pointer', + 'bg-r-neutral-card2 min-h-[52px] px-[16px] py-[14px]', + 'border-[1px] border-transparent', + checked && 'border-rabby-blue-default' + // isLocalToken && 'opacity-60 cursor-not-allowed' + )} + > +
+ + + {chain?.name} + +
+
+ {formatAmount(token.amount || 0)} {token.symbol} +
+ {checked ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ ) : null} + + )} +
+
+ + +
+
+ + { + setChainSelectorState({ + visible: false, + }); + }} + onChange={(value) => { + setChainSelectorState({ + visible: false, + chain: value, + }); + }} + /> + + ); +}; diff --git a/src/ui/views/CommonPopup/AssetList/TokenTabs.tsx b/src/ui/views/CommonPopup/AssetList/TokenTabs.tsx deleted file mode 100644 index d02a2bff906..00000000000 --- a/src/ui/views/CommonPopup/AssetList/TokenTabs.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Radio } from 'antd'; -import React from 'react'; -import { ReactComponent as TabHistorySVG } from '@/ui/assets/dashboard/tab-history.svg'; -import { ReactComponent as TabListSVG } from '@/ui/assets/dashboard/tab-list.svg'; -import { ReactComponent as TabSummarySVG } from '@/ui/assets/dashboard/tab-summary.svg'; -import styled from 'styled-components'; - -export enum TokenTabEnum { - List = 'list', - Summary = 'summary', - History = 'history', -} - -const TabsStyled = styled(Radio.Group)` - background: var(--r-neutral-card-2, #f2f4f7); - border-radius: 6px; - padding: 2px; - - .ant-radio { - display: none; - - & + span { - padding: 0; - } - } - - .ant-radio-wrapper { - border-radius: 4px; - padding: 6px 8px; - margin-right: 0; - color: var(--r-neutral-foot, #6a7587); - - &:after { - display: none; - } - } - - .ant-radio-wrapper-checked { - background: var(--r-neutral-card-1, #fff); - color: var(--r-neutral-title-1, rgba(247, 250, 252, 1)); - } -`; - -export interface Props { - activeTab?: TokenTabEnum; - onTabChange?: (tab: TokenTabEnum) => void; -} - -export const TokenTabs: React.FC = ({ - activeTab = TokenTabEnum.List, - onTabChange, -}) => { - return ( -
- , value: TokenTabEnum.List }, - { label: , value: TokenTabEnum.Summary }, - { label: , value: TokenTabEnum.History }, - ]} - value={activeTab} - onChange={(e) => onTabChange?.(e.target.value)} - defaultValue={TokenTabEnum.List} - /> -
- ); -}; diff --git a/src/ui/views/CommonPopup/AssetList/icons/add-entry-cc.svg b/src/ui/views/CommonPopup/AssetList/icons/add-entry-cc.svg new file mode 100644 index 00000000000..9fe8f2cb8d9 --- /dev/null +++ b/src/ui/views/CommonPopup/AssetList/icons/add-entry-cc.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/utils/chain.ts b/src/utils/chain.ts index a421f414967..e4c1434c0a0 100644 --- a/src/utils/chain.ts +++ b/src/utils/chain.ts @@ -70,7 +70,7 @@ export const findChain = (params: { serverId?: string | null; hex?: string | null; networkId?: string | null; -}) => { +}): Chain | null | undefined => { const { enum: chainEnum, id, serverId, hex, networkId } = params; if (chainEnum && chainEnum.startsWith('CUSTOM_')) { return findChain({ @@ -85,6 +85,7 @@ export const findChain = (params: { item.hex === hex || item.network === networkId ); + return chain; }; From c544827797fca0231d3c065328a6312db2eef5e9 Mon Sep 17 00:00:00 2001 From: richardo2016x Date: Tue, 23 Apr 2024 15:51:16 +0800 Subject: [PATCH 2/9] build: shellscript executable. --- scripts/pack-debug.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/pack-debug.sh diff --git a/scripts/pack-debug.sh b/scripts/pack-debug.sh old mode 100644 new mode 100755 From dc3fc35e94ae3347f532315d2e7c47ac128706fc Mon Sep 17 00:00:00 2001 From: richardo2016x Date: Tue, 23 Apr 2024 18:56:49 +0800 Subject: [PATCH 3/9] feat: adjust operation. --- _raw/locales/en/messages.json | 2 +- src/ui/models/account.ts | 4 + .../CommonPopup/AssetList/AddTokenEntry.tsx | 100 +++++++++++------- .../AssetList/AssetListContainer.tsx | 15 +-- .../CustomAssetList/AddCustomTokenPopup.tsx | 36 ++++--- .../AssetList/CustomizedButton.tsx | 8 +- .../views/CommonPopup/AssetList/TokenList.tsx | 9 +- .../AssetList/components/TokenButton.tsx | 39 +++++-- .../TokenDetailPopup/CustomizedButton.tsx | 2 +- .../TokenDetailPopup/TokenDetail.tsx | 4 +- 10 files changed, 142 insertions(+), 77 deletions(-) diff --git a/_raw/locales/en/messages.json b/_raw/locales/en/messages.json index faf682c1c7c..236979992cb 100644 --- a/_raw/locales/en/messages.json +++ b/_raw/locales/en/messages.json @@ -1103,7 +1103,7 @@ "blockDescription": "Token blocked by you will be shown here", "unfoldChain": "Unfold 1 chain", "unfoldChainPlural": "Unfold {{moreLen}} chains", - "customLinkText": "Search address to add custom token", + "customButtonText": "Add custom token", "customDescription": "Custom token added by you will be shown here", "comingSoon": "Coming Soon...", "searchPlaceholder": "Tokens", diff --git a/src/ui/models/account.ts b/src/ui/models/account.ts index f86f52168de..1a54594d965 100644 --- a/src/ui/models/account.ts +++ b/src/ui/models/account.ts @@ -326,6 +326,8 @@ export const account = createModel()({ ? store.account.testnetTokens.list : store.account.tokens.list; setTokenList(tokenList.filter((item) => item.id !== token.id)); + + return token; }, async removeBlockedToken(token: AbstractPortfolioToken, store) { @@ -354,6 +356,8 @@ export const account = createModel()({ : store.account.tokens.list; setTokenList([...tokenList, token]); } + + return token; }, async triggerFetchBalanceOnBackground( diff --git a/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx b/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx index 7715c2e3781..811e3513847 100644 --- a/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx +++ b/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx @@ -1,44 +1,66 @@ -import React from 'react'; +import React, { useImperativeHandle } from 'react'; import { ReactComponent as RcAddEntryCC } from './icons/add-entry-cc.svg'; import clsx from 'clsx'; import { AddCustomTokenPopup } from './CustomAssetList/AddCustomTokenPopup'; +import { TokenItem } from '@rabby-wallet/rabby-api/dist/types'; +import { TokenDetailPopup } from '@/ui/views/Dashboard/components/TokenDetailPopup'; -export default function AddTokenEntry({ - onConfirm, -}: { - onConfirm: React.ComponentProps['onConfirm']; -}) { - const [isShowAddModal, setIsShowAddModal] = React.useState(false); - - return ( - <> -
{ - setIsShowAddModal(true); - }} - > - - Add Tokens -
- - { - setIsShowAddModal(false); - }} - onConfirm={() => { - setIsShowAddModal(false); - // refreshAsync(); - onConfirm?.(); - }} - /> - - ); -} +export type AddTokenEntryInst = { + startAddToken: () => void; +}; +const AddTokenEntry = React.forwardRef( + function AddTokenEntryPorto(_, ref) { + const [isShowAddModal, setIsShowAddModal] = React.useState(false); + + useImperativeHandle(ref, () => ({ + startAddToken: () => { + setIsShowAddModal(true); + }, + })); + + const [focusingToken, setFocusingToken] = React.useState( + null + ); + + return ( + <> +
{ + setIsShowAddModal(true); + }} + > + + Add Tokens +
+ + { + setIsShowAddModal(false); + }} + onConfirm={(addedToken) => { + setIsShowAddModal(false); + setFocusingToken(addedToken?.token || null); + // refreshAsync(); + }} + /> + + setFocusingToken(null)} + /> + + ); + } +); + +export default AddTokenEntry; diff --git a/src/ui/views/CommonPopup/AssetList/AssetListContainer.tsx b/src/ui/views/CommonPopup/AssetList/AssetListContainer.tsx index 1af513f7eea..3e61c9003d2 100644 --- a/src/ui/views/CommonPopup/AssetList/AssetListContainer.tsx +++ b/src/ui/views/CommonPopup/AssetList/AssetListContainer.tsx @@ -1,8 +1,8 @@ import React, { useMemo } from 'react'; import { TokenSearchInput } from './TokenSearchInput'; -import AddTokenEntry from './AddTokenEntry'; +import AddTokenEntry, { AddTokenEntryInst } from './AddTokenEntry'; import { useRabbySelector } from '@/ui/store'; -import { TokenList } from './TokenList'; +import { HomeTokenList } from './TokenList'; import useSortTokens from 'ui/hooks/useSortTokens'; import useSearchToken from '@/ui/hooks/useSearchToken'; import { @@ -12,8 +12,6 @@ import { import ProtocolList from './ProtocolList'; import { useQueryProjects } from 'ui/utils/portfolio'; import { Input } from 'antd'; -import { SummaryList } from './SummaryList'; -import { HistoryList } from './HisotryList'; import { useFilterProtocolList } from './useFilterProtocolList'; interface Props { @@ -103,6 +101,8 @@ export const AssetListContainer: React.FC = ({ } }, [visible]); + const addTokenEntryRef = React.useRef(null); + if (isTokensLoading && !hasTokens) { return ; } @@ -129,15 +129,18 @@ export const AssetListContainer: React.FC = ({ }} className={isFocus || search ? 'w-[360px]' : 'w-[160px]'} /> - {isFocus || search ? null : {}} />} + {isFocus || search ? null : } {isTokensLoading || isSearching ? ( ) : (
- { + addTokenEntryRef.current?.startAddToken(); + }} isSearch={!!search} isNoResults={isNoResults} blockedTokens={blockedTokens} diff --git a/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx b/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx index 45dfc6a720d..780ccd2201f 100644 --- a/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx +++ b/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx @@ -23,11 +23,18 @@ import { useFindCustomToken, useIsTokenAddedLocally, } from '@/ui/hooks/useSearchToken'; +import { TokenItem } from '@rabby-wallet/rabby-api/dist/types'; +import { AbstractPortfolioToken } from '@/ui/utils/portfolio/types'; interface Props { visible?: boolean; onClose?(): void; - onConfirm?(): void; + onConfirm?: ( + addedInfo: { + token: TokenItem; + portofolioToken?: AbstractPortfolioToken | null; + } | null + ) => void; } const Wraper = styled.div` @@ -112,14 +119,13 @@ export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { const { t } = useTranslation(); const [form] = useForm(); - const { resetSearchResult, searchCustomToken } = useFindCustomToken(); - const { - data, - runAsync: doSearch, - loading: isSearchingToken, - error, - } = useRequest( + tokenList, + resetSearchResult, + searchCustomToken, + } = useFindCustomToken(); + + const { runAsync: doSearch, loading: isSearchingToken, error } = useRequest( async () => { if (!chain?.id || !tokenId) { return null; @@ -134,7 +140,7 @@ export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { }, ]); - return searchCustomToken({ + await searchCustomToken({ address: currentAccount!.address, chainServerId: chain.serverId, q: tokenId, @@ -160,7 +166,6 @@ export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { } }, [chain?.serverId, tokenId]); - const { tokenList } = data || {}; const token = useMemo(() => tokenList?.[0], [tokenList]); // const { isLocal: isLocalToken } = useIsTokenAddedLocally(token); @@ -171,7 +176,12 @@ export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { if (!token || !chain?.id || !tokenId) { return null; } - return addToken(token); + const portofolioToken = (await addToken(token)) || null; + + return { + token, + portofolioToken, + }; }, { manual: true, @@ -180,8 +190,8 @@ export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { const handleConfirm = useCallback(async () => { try { - await runAddToken(); - onConfirm?.(); + const addedInfo = await runAddToken(); + onConfirm?.(addedInfo); } catch (e) { message.error(e?.message); } diff --git a/src/ui/views/CommonPopup/AssetList/CustomizedButton.tsx b/src/ui/views/CommonPopup/AssetList/CustomizedButton.tsx index e37bc4f29fa..94ecfad370f 100644 --- a/src/ui/views/CommonPopup/AssetList/CustomizedButton.tsx +++ b/src/ui/views/CommonPopup/AssetList/CustomizedButton.tsx @@ -5,12 +5,12 @@ import useSortToken from '@/ui/hooks/useSortTokens'; import { useTranslation } from 'react-i18next'; interface Props { - onClickLink: () => void; + onClickButton: () => void; isTestnet: boolean; } export const CustomizedButton: React.FC = ({ - onClickLink, + onClickButton, isTestnet, }) => { const { customize } = useRabbySelector((store) => @@ -22,10 +22,10 @@ export const CustomizedButton: React.FC = ({ return ( ); }; diff --git a/src/ui/views/CommonPopup/AssetList/TokenList.tsx b/src/ui/views/CommonPopup/AssetList/TokenList.tsx index 4f16b6c62dc..ef7873123c1 100644 --- a/src/ui/views/CommonPopup/AssetList/TokenList.tsx +++ b/src/ui/views/CommonPopup/AssetList/TokenList.tsx @@ -13,15 +13,17 @@ export interface Props { list?: TokenItemProps['item'][]; isSearch: boolean; onFocusInput: () => void; + onOpenAddEntryPopup: () => void; isNoResults?: boolean; blockedTokens?: TokenItemProps['item'][]; customizeTokens?: TokenItemProps['item'][]; isTestnet: boolean; } -export const TokenList: React.FC = ({ +export const HomeTokenList = ({ list, onFocusInput, + onOpenAddEntryPopup, isSearch, isNoResults, blockedTokens, @@ -68,7 +70,10 @@ export const TokenList: React.FC = ({
{!isSearch && hasList && (
- +
)} diff --git a/src/ui/views/CommonPopup/AssetList/components/TokenButton.tsx b/src/ui/views/CommonPopup/AssetList/components/TokenButton.tsx index cd74152589c..7c79c5ad50f 100644 --- a/src/ui/views/CommonPopup/AssetList/components/TokenButton.tsx +++ b/src/ui/views/CommonPopup/AssetList/components/TokenButton.tsx @@ -7,12 +7,15 @@ import { ReactComponent as EmptySVG } from '@/ui/assets/dashboard/empty.svg'; import { TokenTable } from './TokenTable'; import { useCommonPopupView } from '@/ui/utils'; import { useTranslation } from 'react-i18next'; +import { Button } from 'antd'; export interface Props { label: string; - onClickLink: () => void; + onClickButton?: () => void; tokens?: AbstractPortfolioToken[]; + onClickLink?: () => void; linkText?: string; + buttonText?: string; description?: string; hiddenSubTitle?: boolean; } @@ -22,6 +25,8 @@ export const TokenButton: React.FC = ({ tokens, onClickLink, linkText, + onClickButton, + buttonText, description, hiddenSubTitle, }) => { @@ -32,8 +37,13 @@ export const TokenButton: React.FC = ({ const handleClickLink = React.useCallback(() => { setVisible(false); - onClickLink(); - }, []); + onClickLink?.(); + }, [onClickLink]); + + const handleClickButton = React.useCallback(() => { + setVisible(false); + onClickButton?.(); + }, [onClickButton]); React.useEffect(() => { if (!commonPopupVisible) { @@ -78,12 +88,23 @@ export const TokenButton: React.FC = ({
{description}
-
- {linkText} -
+ {linkText && ( +
+ {linkText} +
+ )} + {buttonText && ( + + )}
} /> diff --git a/src/ui/views/Dashboard/components/TokenDetailPopup/CustomizedButton.tsx b/src/ui/views/Dashboard/components/TokenDetailPopup/CustomizedButton.tsx index bdbc6b4acab..554957d1948 100644 --- a/src/ui/views/Dashboard/components/TokenDetailPopup/CustomizedButton.tsx +++ b/src/ui/views/Dashboard/components/TokenDetailPopup/CustomizedButton.tsx @@ -29,7 +29,7 @@ const SwitchStyled = styled(Switch)` } `; -export const CustomizedButton: React.FC = ({ +export const CustomizedSwitch: React.FC = ({ selected, onOpen, onClose, diff --git a/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx b/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx index 101a487cfd3..e470c34eaad 100644 --- a/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx +++ b/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx @@ -24,7 +24,7 @@ import { CHAINS } from 'consts'; import { ellipsisOverflowedText } from 'ui/utils'; import { getTokenSymbol } from '@/ui/utils/token'; import { SWAP_SUPPORT_CHAINS } from '@/constant'; -import { CustomizedButton } from './CustomizedButton'; +import { CustomizedSwitch } from './CustomizedButton'; import { BlockedButton } from './BlockedButton'; import { useRabbySelector } from '@/ui/store'; import { TooltipWithMagnetArrow } from '@/ui/component/Tooltip/TooltipWithMagnetArrow'; @@ -218,7 +218,7 @@ const TokenDetail = ({ onClose={() => removeToken(tokenWithAmount)} /> ) : ( - addToken(tokenWithAmount)} onClose={() => removeToken(tokenWithAmount)} From 5e4fc456bcb44f694798de7c5ec9b5fadf477dd6 Mon Sep 17 00:00:00 2001 From: richardo2016x Date: Wed, 24 Apr 2024 11:35:30 +0800 Subject: [PATCH 4/9] chore: i18n. --- _raw/locales/en/messages.json | 3 ++- src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/_raw/locales/en/messages.json b/_raw/locales/en/messages.json index 236979992cb..a67967efa8e 100644 --- a/_raw/locales/en/messages.json +++ b/_raw/locales/en/messages.json @@ -1125,7 +1125,8 @@ "add": "Token", "addTestnet": "Network" }, - "noTestnetAssets": "No Custom Network Assets" + "noTestnetAssets": "No Custom Network Assets", + "addTokenEntryText": "Add Tokens" }, "hd": { "howToConnectLedger": "How to Connect Ledger", diff --git a/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx b/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx index 811e3513847..215cb3fbc31 100644 --- a/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx +++ b/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx @@ -5,12 +5,14 @@ import clsx from 'clsx'; import { AddCustomTokenPopup } from './CustomAssetList/AddCustomTokenPopup'; import { TokenItem } from '@rabby-wallet/rabby-api/dist/types'; import { TokenDetailPopup } from '@/ui/views/Dashboard/components/TokenDetailPopup'; +import { useTranslation } from 'react-i18next'; export type AddTokenEntryInst = { startAddToken: () => void; }; const AddTokenEntry = React.forwardRef( function AddTokenEntryPorto(_, ref) { + const { t } = useTranslation(); const [isShowAddModal, setIsShowAddModal] = React.useState(false); useImperativeHandle(ref, () => ({ @@ -37,7 +39,7 @@ const AddTokenEntry = React.forwardRef( }} > - Add Tokens + {t('page.dashboard.assets.addTokenEntryText')} Date: Fri, 26 Apr 2024 17:41:52 +0800 Subject: [PATCH 5/9] fix: style & behaviors. --- _raw/locales/en/messages.json | 4 +- .../CommonPopup/AssetList/AddTokenEntry.tsx | 39 ++++-- .../CustomAssetList/AddCustomTokenPopup.tsx | 13 ++ .../AddCustomTestnetTokenPopup.tsx | 4 + .../CustomTestnetTokenDetail.tsx | 8 +- .../AssetList/CustomizedButton.tsx | 4 +- .../AssetList/components/TokenButton.tsx | 116 +++++++++++------- .../TokenDetailPopup/TokenDetail.tsx | 8 +- .../components/TokenDetailPopup/index.tsx | 2 +- 9 files changed, 135 insertions(+), 63 deletions(-) diff --git a/_raw/locales/en/messages.json b/_raw/locales/en/messages.json index a67967efa8e..385913085cd 100644 --- a/_raw/locales/en/messages.json +++ b/_raw/locales/en/messages.json @@ -1112,6 +1112,7 @@ "selectChain": "Select chain", "searching": "Searching Token", "tokenAddress": "Token Address", + "tokenAddressPlaceholder": "Token Address", "notFound": "Token not found" }, "AddTestnetToken": { @@ -1119,6 +1120,7 @@ "selectChain": "Select chain", "searching": "Searching Token", "tokenAddress": "Token Address", + "tokenAddressPlaceholder": "Token Address", "notFound": "Token not found" }, "TestnetAssetListContainer": { @@ -1126,7 +1128,7 @@ "addTestnet": "Network" }, "noTestnetAssets": "No Custom Network Assets", - "addTokenEntryText": "Add Tokens" + "addTokenEntryText": "Token" }, "hd": { "howToConnectLedger": "How to Connect Ledger", diff --git a/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx b/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx index 215cb3fbc31..6b11ed25be7 100644 --- a/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx +++ b/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx @@ -4,14 +4,19 @@ import { ReactComponent as RcAddEntryCC } from './icons/add-entry-cc.svg'; import clsx from 'clsx'; import { AddCustomTokenPopup } from './CustomAssetList/AddCustomTokenPopup'; import { TokenItem } from '@rabby-wallet/rabby-api/dist/types'; -import { TokenDetailPopup } from '@/ui/views/Dashboard/components/TokenDetailPopup'; import { useTranslation } from 'react-i18next'; +import { SpecialTokenListPopup } from './components/TokenButton'; +import { useRabbySelector } from '@/ui/store'; +import useSortToken from '@/ui/hooks/useSortTokens'; +type Props = { + onConfirm?: React.ComponentProps['onConfirm']; +}; export type AddTokenEntryInst = { startAddToken: () => void; }; -const AddTokenEntry = React.forwardRef( - function AddTokenEntryPorto(_, ref) { +const AddTokenEntry = React.forwardRef( + function AddTokenEntryPorto({ onConfirm }, ref) { const { t } = useTranslation(); const [isShowAddModal, setIsShowAddModal] = React.useState(false); @@ -21,8 +26,15 @@ const AddTokenEntry = React.forwardRef( }, })); - const [focusingToken, setFocusingToken] = React.useState( - null + // const [focusingToken, setFocusingToken] = React.useState( + // null + // ); + + const { customize } = useRabbySelector((store) => store.account.tokens); + const tokens = useSortToken(customize); + + const [showCustomizedTokens, setShowCustomizedTokens] = React.useState( + false ); return ( @@ -30,7 +42,9 @@ const AddTokenEntry = React.forwardRef(
( }} onConfirm={(addedToken) => { setIsShowAddModal(false); - setFocusingToken(addedToken?.token || null); + setShowCustomizedTokens(true); + + // setFocusingToken(addedToken?.token || null); // refreshAsync(); }} /> - setFocusingToken(null)} + /> */} + + setShowCustomizedTokens(false)} /> ); diff --git a/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx b/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx index 780ccd2201f..ce02be3759c 100644 --- a/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx +++ b/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx @@ -144,6 +144,15 @@ export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { address: currentAccount!.address, chainServerId: chain.serverId, q: tokenId, + }).then((lists) => { + if (!lists?.tokenList.length) { + form.setFields([ + { + name: 'address', + errors: [t('page.dashboard.assets.AddMainnetToken.notFound')], + }, + ]); + } }); }, { @@ -289,6 +298,9 @@ export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { name="address" > { setTokenId(e.target.value); }} @@ -378,6 +390,7 @@ export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { { setTokenId(e.target.value); }} @@ -343,6 +346,7 @@ export const AddCustomTestnetTokenPopup = ({ { setChainSelectorState({ diff --git a/src/ui/views/CommonPopup/AssetList/CustomTestnetTokenDetailPopup/CustomTestnetTokenDetail.tsx b/src/ui/views/CommonPopup/AssetList/CustomTestnetTokenDetailPopup/CustomTestnetTokenDetail.tsx index 77ed8ce6a67..652349d87b1 100644 --- a/src/ui/views/CommonPopup/AssetList/CustomTestnetTokenDetailPopup/CustomTestnetTokenDetail.tsx +++ b/src/ui/views/CommonPopup/AssetList/CustomTestnetTokenDetailPopup/CustomTestnetTokenDetail.tsx @@ -159,9 +159,7 @@ export const CustomTestnetTokenDetail = ({ type="primary" size="large" disabled - style={{ - width: 114, - }} + className="w-[114px] h-[36px] py-[0]" > {t('page.dashboard.tokenDetail.swap')} @@ -171,7 +169,7 @@ export const CustomTestnetTokenDetail = ({ type="primary" ghost size="large" - className="w-[114px] rabby-btn-ghost" + className="w-[114px] h-[36px] py-[0] rabby-btn-ghost" onClick={goToSend} > {t('page.dashboard.tokenDetail.send')} @@ -180,7 +178,7 @@ export const CustomTestnetTokenDetail = ({ type="primary" ghost size="large" - className="w-[114px] rabby-btn-ghost" + className="w-[114px] h-[36px] py-[0] rabby-btn-ghost" onClick={goToReceive} > {t('page.dashboard.tokenDetail.receive')} diff --git a/src/ui/views/CommonPopup/AssetList/CustomizedButton.tsx b/src/ui/views/CommonPopup/AssetList/CustomizedButton.tsx index 94ecfad370f..49254caa404 100644 --- a/src/ui/views/CommonPopup/AssetList/CustomizedButton.tsx +++ b/src/ui/views/CommonPopup/AssetList/CustomizedButton.tsx @@ -4,10 +4,10 @@ import { useRabbySelector } from '@/ui/store'; import useSortToken from '@/ui/hooks/useSortTokens'; import { useTranslation } from 'react-i18next'; -interface Props { +type Props = { onClickButton: () => void; isTestnet: boolean; -} +}; export const CustomizedButton: React.FC = ({ onClickButton, diff --git a/src/ui/views/CommonPopup/AssetList/components/TokenButton.tsx b/src/ui/views/CommonPopup/AssetList/components/TokenButton.tsx index 7c79c5ad50f..8a23ca98760 100644 --- a/src/ui/views/CommonPopup/AssetList/components/TokenButton.tsx +++ b/src/ui/views/CommonPopup/AssetList/components/TokenButton.tsx @@ -9,7 +9,9 @@ import { useCommonPopupView } from '@/ui/utils'; import { useTranslation } from 'react-i18next'; import { Button } from 'antd'; -export interface Props { +interface TokenButtonPopupProps { + visible?: boolean; + onClose?: () => void; label: string; onClickButton?: () => void; tokens?: AbstractPortfolioToken[]; @@ -20,6 +22,68 @@ export interface Props { hiddenSubTitle?: boolean; } +export function SpecialTokenListPopup({ + visible, + onClose, + label, + tokens, + onClickLink, + linkText, + onClickButton, + buttonText, + description, + hiddenSubTitle, +}: TokenButtonPopupProps) { + const { t } = useTranslation(); + const len = tokens?.length ?? 0; + + return ( + + {!hiddenSubTitle && ( +
+ {t('page.dashboard.assets.tokenButton.subTitle')} +
+ )} + + +
{description}
+ {linkText && ( +
+ {linkText} +
+ )} + {buttonText && ( + + )} +
+ } + /> + + ); +} + +export type Props = TokenButtonPopupProps; + export const TokenButton: React.FC = ({ label, tokens, @@ -33,7 +97,6 @@ export const TokenButton: React.FC = ({ const { visible: commonPopupVisible } = useCommonPopupView(); const [visible, setVisible] = React.useState(false); const len = tokens?.length ?? 0; - const { t } = useTranslation(); const handleClickLink = React.useCallback(() => { setVisible(false); @@ -68,47 +131,18 @@ export const TokenButton: React.FC = ({ - setVisible(false)} - title={`${len} ${label}`} - isSupportDarkMode - > - {!hiddenSubTitle && ( -
- {t('page.dashboard.assets.tokenButton.subTitle')} -
- )} - - -
{description}
- {linkText && ( -
- {linkText} -
- )} - {buttonText && ( - - )} - - } - /> -
+ label={label} + tokens={tokens} + onClickLink={handleClickLink} + linkText={linkText} + onClickButton={handleClickButton} + buttonText={buttonText} + description={description} + hiddenSubTitle={hiddenSubTitle} + /> ); }; diff --git a/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx b/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx index e470c34eaad..3b1f4fe7529 100644 --- a/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx +++ b/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx @@ -269,9 +269,7 @@ const TokenDetail = ({ size="large" onClick={goToSwap} disabled={!tokenSupportSwap} - style={{ - width: 114, - }} + className="w-[114px] h-[36px] py-[0]" > {t('page.dashboard.tokenDetail.swap')} @@ -281,7 +279,7 @@ const TokenDetail = ({ type="primary" ghost size="large" - className="w-[114px] rabby-btn-ghost" + className="w-[114px] h-[36px] py-[0] rabby-btn-ghost" onClick={goToSend} > {t('page.dashboard.tokenDetail.send')} @@ -290,7 +288,7 @@ const TokenDetail = ({ type="primary" ghost size="large" - className="w-[114px] rabby-btn-ghost" + className="w-[114px] h-[36px] py-[0] rabby-btn-ghost" onClick={goToReceive} > {t('page.dashboard.tokenDetail.receive')} diff --git a/src/ui/views/Dashboard/components/TokenDetailPopup/index.tsx b/src/ui/views/Dashboard/components/TokenDetailPopup/index.tsx index 68ba8e029e2..7985a93edc3 100644 --- a/src/ui/views/Dashboard/components/TokenDetailPopup/index.tsx +++ b/src/ui/views/Dashboard/components/TokenDetailPopup/index.tsx @@ -98,7 +98,7 @@ export const TokenDetailPopup = ({ onClose={onClose} canClickToken={canClickToken} hideOperationButtons={hideOperationButtons} - > + /> )} ); From be32bdce1ce2521a44d5f23e13b6c10466e695b6 Mon Sep 17 00:00:00 2001 From: richardo2016x Date: Fri, 26 Apr 2024 18:10:30 +0800 Subject: [PATCH 6/9] style: tuning. --- src/ui/assets/open-external-cc.svg | 8 +++++--- src/ui/views/CommonPopup/AssetList/ProtocolList.tsx | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ui/assets/open-external-cc.svg b/src/ui/assets/open-external-cc.svg index fd0bcf5c7ed..77c42109219 100644 --- a/src/ui/assets/open-external-cc.svg +++ b/src/ui/assets/open-external-cc.svg @@ -1,3 +1,5 @@ - - - + + + + + \ No newline at end of file diff --git a/src/ui/views/CommonPopup/AssetList/ProtocolList.tsx b/src/ui/views/CommonPopup/AssetList/ProtocolList.tsx index b7b881b37a5..a70d72ab8b8 100644 --- a/src/ui/views/CommonPopup/AssetList/ProtocolList.tsx +++ b/src/ui/views/CommonPopup/AssetList/ProtocolList.tsx @@ -65,7 +65,6 @@ const ProtocolItemWrapper = styled.div` font-size: 13px; line-height: 15px; color: var(--r-neutral-title-1, #192945); - margin-left: 8px; } .net-worth { font-weight: 500; @@ -134,7 +133,7 @@ const ProtocolItem = ({ isShowChainTooltip={true} />
{ evt.stopPropagation(); openInTab(protocol.site_url, false); From 568c4cbc02198cece382630063b67ae0a0b8aa9c Mon Sep 17 00:00:00 2001 From: richardo2016x Date: Fri, 26 Apr 2024 18:46:27 +0800 Subject: [PATCH 7/9] feat: robust change. --- src/ui/hooks/useSearchToken.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ui/hooks/useSearchToken.ts b/src/ui/hooks/useSearchToken.ts index 58ce6b5ac1d..b186dd680a1 100644 --- a/src/ui/hooks/useSearchToken.ts +++ b/src/ui/hooks/useSearchToken.ts @@ -201,13 +201,13 @@ export function useFindCustomToken(input?: { } ); } else { - lists.tokenList = await requestOpenApiWithChainId( - (ctx) => ctx.openapi.searchToken(address, q, chainServerId), - { - isTestnet: !!isTestnet || !!chainItem?.isTestnet, - wallet, - } - ); + // lists.tokenList = await requestOpenApiWithChainId( + // (ctx) => ctx.openapi.searchToken(address, q, chainServerId), + // { + // isTestnet: !!isTestnet || !!chainItem?.isTestnet, + // wallet, + // } + // ); } if (q === skRef.current) { From 5be1b6fdc5ecc02a5f1ed74539ea9b91c52e03e6 Mon Sep 17 00:00:00 2001 From: richardo2016x Date: Fri, 26 Apr 2024 19:09:24 +0800 Subject: [PATCH 8/9] feat: filter out core token, style tuning. --- src/ui/hooks/useSearchToken.ts | 3 ++- .../CommonPopup/AssetList/AddTokenEntry.tsx | 5 +++++ .../CustomAssetList/AddCustomTokenPopup.tsx | 22 ++++++++++++++++++- .../AddCustomTestnetTokenPopup.tsx | 17 ++++++++++++-- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/ui/hooks/useSearchToken.ts b/src/ui/hooks/useSearchToken.ts index b186dd680a1..6de8e52469d 100644 --- a/src/ui/hooks/useSearchToken.ts +++ b/src/ui/hooks/useSearchToken.ts @@ -199,7 +199,8 @@ export function useFindCustomToken(input?: { isTestnet: !!isTestnet || !!chainItem?.isTestnet, wallet, } - ); + // filter out core tokens + ).then((res) => res.filter((item) => !item.is_core)); } else { // lists.tokenList = await requestOpenApiWithChainId( // (ctx) => ctx.openapi.searchToken(address, q, chainServerId), diff --git a/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx b/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx index 6b11ed25be7..ea3403006a9 100644 --- a/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx +++ b/src/ui/views/CommonPopup/AssetList/AddTokenEntry.tsx @@ -79,6 +79,11 @@ const AddTokenEntry = React.forwardRef( { + setShowCustomizedTokens(true); + }} tokens={tokens} visible={showCustomizedTokens} onClose={() => setShowCustomizedTokens(false)} diff --git a/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx b/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx index ce02be3759c..4df4f17bada 100644 --- a/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx +++ b/src/ui/views/CommonPopup/AssetList/CustomAssetList/AddCustomTokenPopup.tsx @@ -9,7 +9,13 @@ import { CHAINS_ENUM } from '@debank/common'; import { useRequest, useSetState } from 'ahooks'; import { Button, Form, Input, Spin, message } from 'antd'; import { useForm } from 'antd/lib/form/Form'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { Loading3QuartersOutlined } from '@ant-design/icons'; @@ -63,6 +69,11 @@ const Wraper = styled.div` &:focus { border-color: var(--r-blue-default, #7084ff); } + + &::placeholder { + font-size: 14px; + font-weight: 400; + } } .ant-input[disabled] { background: var(--r-neutral-card2, #f2f4f7); @@ -219,6 +230,13 @@ export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { } }, [visible, resetSearchResult]); + const inputRef = useRef(null); + useEffect(() => { + if (visible) { + inputRef.current?.focus(); + } + }, [visible]); + const { isDarkTheme } = useThemeMode(); return ( @@ -298,6 +316,8 @@ export const AddCustomTokenPopup = ({ visible, onClose, onConfirm }: Props) => { name="address" > (null); + useEffect(() => { + if (visible) { + inputRef.current?.focus(); + } + }, [visible]); + const { isDarkTheme } = useThemeMode(); return ( @@ -256,6 +267,8 @@ export const AddCustomTestnetTokenPopup = ({ name="address" > Date: Fri, 26 Apr 2024 19:24:30 +0800 Subject: [PATCH 9/9] style: tuning. --- .../CustomTestnetTokenDetail.tsx | 9 ++++++--- .../components/TokenDetailPopup/TokenDetail.tsx | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ui/views/CommonPopup/AssetList/CustomTestnetTokenDetailPopup/CustomTestnetTokenDetail.tsx b/src/ui/views/CommonPopup/AssetList/CustomTestnetTokenDetailPopup/CustomTestnetTokenDetail.tsx index 652349d87b1..0aab86fd99a 100644 --- a/src/ui/views/CommonPopup/AssetList/CustomTestnetTokenDetailPopup/CustomTestnetTokenDetail.tsx +++ b/src/ui/views/CommonPopup/AssetList/CustomTestnetTokenDetailPopup/CustomTestnetTokenDetail.tsx @@ -159,7 +159,10 @@ export const CustomTestnetTokenDetail = ({ type="primary" size="large" disabled - className="w-[114px] h-[36px] py-[0]" + className="wrapped-button w-[114px]" + style={{ + width: 114, + }} > {t('page.dashboard.tokenDetail.swap')} @@ -169,7 +172,7 @@ export const CustomTestnetTokenDetail = ({ type="primary" ghost size="large" - className="w-[114px] h-[36px] py-[0] rabby-btn-ghost" + className="w-[114px] rabby-btn-ghost" onClick={goToSend} > {t('page.dashboard.tokenDetail.send')} @@ -178,7 +181,7 @@ export const CustomTestnetTokenDetail = ({ type="primary" ghost size="large" - className="w-[114px] h-[36px] py-[0] rabby-btn-ghost" + className="w-[114px] rabby-btn-ghost" onClick={goToReceive} > {t('page.dashboard.tokenDetail.receive')} diff --git a/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx b/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx index 6702baf9300..c2ce5045c84 100644 --- a/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx +++ b/src/ui/views/Dashboard/components/TokenDetailPopup/TokenDetail.tsx @@ -269,7 +269,10 @@ const TokenDetail = ({ size="large" onClick={goToSwap} disabled={!tokenSupportSwap} - className="w-[114px] h-[36px] py-[0]" + className="w-[114px]" + style={{ + width: 114, + }} > {t('page.dashboard.tokenDetail.swap')} @@ -279,7 +282,7 @@ const TokenDetail = ({ type="primary" ghost size="large" - className="w-[114px] h-[36px] py-[0] rabby-btn-ghost" + className="w-[114px] rabby-btn-ghost" onClick={goToSend} > {t('page.dashboard.tokenDetail.send')} @@ -288,7 +291,7 @@ const TokenDetail = ({ type="primary" ghost size="large" - className="w-[114px] h-[36px] py-[0] rabby-btn-ghost" + className="w-[114px] rabby-btn-ghost" onClick={goToReceive} > {t('page.dashboard.tokenDetail.receive')}