From ff3a9b1c456f734ddcfa983d3be7a6974bf7055a Mon Sep 17 00:00:00 2001 From: pandablue0809 Date: Tue, 1 Jul 2025 21:42:48 +0900 Subject: [PATCH 1/3] tokenSelector update --- components/UI/TokenSelector.js | 158 ++++++++++++++++++++++----- pages/services/send.js | 29 ++++- styles/components/tokenSelector.scss | 43 ++++++++ 3 files changed, 203 insertions(+), 27 deletions(-) diff --git a/components/UI/TokenSelector.js b/components/UI/TokenSelector.js index e532b10d..f813f598 100644 --- a/components/UI/TokenSelector.js +++ b/components/UI/TokenSelector.js @@ -5,11 +5,11 @@ import { IoMdClose } from 'react-icons/io' import { IoChevronDown } from 'react-icons/io5' import axios from 'axios' import { avatarServer, nativeCurrency, nativeCurrenciesImages, useWidth } from '../../utils' -import { niceCurrency, shortAddress } from '../../utils/format' +import { niceCurrency, shortAddress, amountFormat } from '../../utils/format' const limit = 20 -export default function TokenSelector({ value, onChange, excludeNative = false }) { +export default function TokenSelector({ value, onChange, excludeNative = false, destinationAddress = null }) { const { t } = useTranslation() const width = useWidth() const [isOpen, setIsOpen] = useState(false) @@ -18,6 +18,12 @@ export default function TokenSelector({ value, onChange, excludeNative = false } const [isLoading, setIsLoading] = useState(false) const [searchTimeout, setSearchTimeout] = useState(null) + // Clear search results when destination address changes + useEffect(() => { + setSearchResults([]) + setSearchQuery('') + }, [destinationAddress]) + // Handle search with debounce useEffect(() => { if (!isOpen) { @@ -30,24 +36,66 @@ export default function TokenSelector({ value, onChange, excludeNative = false } const timeout = setTimeout(async () => { if (!searchQuery) { - // do not reload default token list if it's already loaded - // when searched for native currency, we also add the native currency on top, - // so check that it's not that case before canceling the search - if ( - searchResults[0]?.currency === nativeCurrency && - !niceCurrency(searchResults[1]?.currency)?.toLowerCase().startsWith(nativeCurrency.toLowerCase()) - ) - return + // Only apply the early return logic when there's no destination address + // When destination address is provided, we always want to fetch fresh data + if (!destinationAddress) { + // do not reload default token list if it's already loaded + // when searched for native currency, we also add the native currency on top, + // so check that it's not that case before canceling the search + if ( + searchResults[0]?.currency === nativeCurrency && + !niceCurrency(searchResults[1]?.currency)?.toLowerCase().startsWith(nativeCurrency.toLowerCase()) + ) + return + } setIsLoading(true) try { - const response = await axios('v2/trustlines/tokens?limit=' + limit) - const tokens = response.data?.tokens || [] - if (excludeNative) { - setSearchResults(tokens) + let tokens = [] + + if (destinationAddress) { + // Fetch tokens that destination can hold based on trustlines + const response = await axios(`v2/objects/${destinationAddress}?limit=100`) + const objects = response.data?.objects || [] + + // Filter RippleState objects to get trustlines where destination can hold tokens + const trustlines = objects.filter(obj => { + if (obj.LedgerEntryType !== 'RippleState') return false + + if (parseFloat(obj.LowLimit.value) <= 0 && parseFloat(obj.HighLimit.value) <= 0 ) { + return false + } + + return true + }) + + // Convert trustlines to token format + tokens = trustlines.map(tl => ({ + currency: tl.Balance.currency, + issuer: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuer : tl.HighLimit.issuer, + issuerDetails: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuerDetails : tl.HighLimit.issuerDetails, + limit: Math.max(parseFloat(tl.LowLimit.value), parseFloat(tl.HighLimit.value)), + balance: tl.Balance.value + })) + + // Add native currency if not excluded + if (!excludeNative) { + tokens.unshift({ currency: nativeCurrency, limit: null }) + } } else { - setSearchResults([{ currency: nativeCurrency }, ...tokens]) + // Fallback to original behavior if no destination address + const response = await axios('v2/trustlines/tokens?limit=' + limit) + tokens = response.data?.tokens || [] + if (!excludeNative) { + setSearchResults([{ currency: nativeCurrency }, ...tokens]) + } else { + setSearchResults(tokens) + } + setIsLoading(false) + return } + + setSearchResults(tokens) } catch (error) { console.error('Error loading tokens:', error) if (excludeNative) { @@ -63,16 +111,55 @@ export default function TokenSelector({ value, onChange, excludeNative = false } setIsLoading(true) try { - //limit doesn't work with search.. - const response = await axios(`v2/trustlines/tokens/search/${searchQuery}?limit=${limit}`) - const tokens = response.data?.tokens || [] + if (destinationAddress) { + // For destination-specific search, we need to filter the existing trustlines + // This is a simplified approach - in a real implementation you might want to + // implement server-side search for trustlines + const response = await axios(`v2/objects/${destinationAddress}?limit=1000`) + const objects = response.data?.objects || [] + + const trustlines = objects.filter(obj => { + if (obj.LedgerEntryType !== 'RippleState') return false + if (parseFloat(obj.LowLimit.value) <= 0 && parseFloat(obj.HighLimit.value) <= 0) return false + + // Filter by search query + const currency = obj.Balance.currency + const issuerDetails = obj.HighLimit.issuer === destinationAddress ? obj.LowLimit.issuerDetails : obj.HighLimit.issuerDetails || {} + const serviceOrUsername = issuerDetails.service || issuerDetails.username || '' + const issuer = obj.HighLimit.issuer === destinationAddress ? obj.LowLimit.issuer : obj.HighLimit.issuer || '' + + const searchLower = searchQuery.toLowerCase() + return ( + currency.toLowerCase().includes(searchLower) || + serviceOrUsername.toLowerCase().includes(searchLower) || + issuer.toLowerCase().includes(searchLower) + ) + }) + + const tokens = trustlines.map(tl => ({ + currency: tl.Balance.currency, + issuer: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuer : tl.HighLimit.issuer, + issuerDetails: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuerDetails : tl.HighLimit.issuerDetails, + limit: Math.max(parseFloat(tl.LowLimit.value), parseFloat(tl.HighLimit.value)), + balance: tl.Balance.value + })) + + if (!excludeNative && searchQuery.toUpperCase() === nativeCurrency.toUpperCase()) { + tokens.unshift({ currency: nativeCurrency, limit: null }) + } + + setSearchResults(tokens) + } else { + // Fallback to original search behavior + const response = await axios(`v2/trustlines/tokens/search/${searchQuery}?limit=${limit}`) + const tokens = response.data?.tokens || [] - if (!excludeNative && searchQuery.toUpperCase() === nativeCurrency.toUpperCase()) { - // If search for native currency, add it first - tokens.unshift({ currency: nativeCurrency }) - } + if (!excludeNative && searchQuery.toUpperCase() === nativeCurrency.toUpperCase()) { + tokens.unshift({ currency: nativeCurrency }) + } - setSearchResults(tokens) + setSearchResults(tokens) + } } catch (error) { console.error('Error searching tokens:', error) setSearchResults([]) @@ -89,7 +176,7 @@ export default function TokenSelector({ value, onChange, excludeNative = false } } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchQuery, isOpen]) + }, [searchQuery, isOpen, destinationAddress]) const handleSelect = (token) => { onChange(token) @@ -119,6 +206,20 @@ export default function TokenSelector({ value, onChange, excludeNative = false } return niceCurrency(token.currency) } + // Helper to get token limit display + const getTokenLimitDisplay = (token) => { + if (!token.limit || token.currency === nativeCurrency) return null + + return ( +
+ Max: + + {amountFormat({ value: token.limit, currency: token.currency, issuer: token.issuer }, { short: true })} + +
+ ) + } + return (
-

Select Token

+

+ {destinationAddress ? 'Select Token (Destination can hold)' : 'Select Token'} +

setIsOpen(false)} />
@@ -198,6 +301,7 @@ export default function TokenSelector({ value, onChange, excludeNative = false }
{getTokenDisplayName(token)} {width > 1100 ? {token.issuer} : {shortAddress(token.issuer)}} + {getTokenLimitDisplay(token)}
@@ -210,6 +314,10 @@ export default function TokenSelector({ value, onChange, excludeNative = false } ) : searchQuery ? (
{t('general.no-data')}
+ ) : destinationAddress ? ( +
+ No trustlines found for this destination address. +
) : null} diff --git a/pages/services/send.js b/pages/services/send.js index dc9b86f6..65a992b8 100644 --- a/pages/services/send.js +++ b/pages/services/send.js @@ -54,6 +54,27 @@ export default function Send({ setSelectedToken(token) } + // Helper to get maximum amount that can be sent for the selected token + const getMaxAmount = () => { + if (!selectedToken || selectedToken.currency === nativeCurrency) return null + return selectedToken.limit + } + + // Helper to format max amount display + const getMaxAmountDisplay = () => { + const maxAmount = getMaxAmount() + if (!maxAmount) return null + + return ( +
+ Max amount: + + {amountFormat({ value: maxAmount, currency: selectedToken.currency, issuer: selectedToken.issuer }, { short: true })} + +
+ ) + } + useEffect(() => { let queryAddList = [] let queryRemoveList = [] @@ -285,7 +306,10 @@ export default function Send({ placeholder="Destination address" name="destination" hideButton={true} - setValue={setAddress} + setValue={(value) => { + setAddress(value) + setSelectedToken({ currency: nativeCurrency }) + }} setInnerValue={setAddress} rawData={isAddressValid(address) ? { address } : {}} type="address" @@ -342,7 +366,8 @@ export default function Send({
Currency - + + {getMaxAmountDisplay()}
diff --git a/styles/components/tokenSelector.scss b/styles/components/tokenSelector.scss index ce1abe2c..9117552c 100644 --- a/styles/components/tokenSelector.scss +++ b/styles/components/tokenSelector.scss @@ -218,4 +218,47 @@ justify-content: center; overflow: hidden; } + + &-modal-item-limit { + display: flex; + align-items: center; + gap: 4px; + margin-top: 2px; + font-size: 12px; + + &-label { + color: #6b7280; + font-weight: 500; + .dark & { + color: #9ca3af; + } + } + + &-value { + color: var(--accent-link); + font-weight: 500; + } + } +} + +// Max amount display styles for send page +.max-amount-display { + display: flex; + align-items: center; + gap: 6px; + margin-top: 4px; + font-size: 12px; + + .max-amount-label { + color: #6b7280; + font-weight: 500; + .dark & { + color: #9ca3af; + } + } + + .max-amount-value { + color: var(--accent-link); + font-weight: 500; + } } From 2598a5fbac9f0cae1d70878cad12d0d6f59b7694 Mon Sep 17 00:00:00 2001 From: pandablue0809 Date: Wed, 9 Jul 2025 23:06:47 +0900 Subject: [PATCH 2/3] similar code was extracted --- components/UI/TokenSelector.js | 132 +++++++++++++++------------------ 1 file changed, 60 insertions(+), 72 deletions(-) diff --git a/components/UI/TokenSelector.js b/components/UI/TokenSelector.js index f813f598..441b6068 100644 --- a/components/UI/TokenSelector.js +++ b/components/UI/TokenSelector.js @@ -9,6 +9,56 @@ import { niceCurrency, shortAddress, amountFormat } from '../../utils/format' const limit = 20 +// Helper function to fetch and process trustlines for a destination address +const fetchTrustlinesForDestination = async (destinationAddress, searchQuery = '') => { + const response = await axios(`v2/objects/${destinationAddress}?limit=1000`) + const objects = response.data?.objects || [] + + // Filter RippleState objects to get trustlines where destination can hold tokens + const trustlines = objects.filter(obj => { + if (obj.LedgerEntryType !== 'RippleState') return false + if (parseFloat(obj.LowLimit.value) <= 0 && parseFloat(obj.HighLimit.value) <= 0) return false + + // If search query is provided, filter by it + if (searchQuery) { + const currency = obj.Balance.currency + const issuerDetails = obj.HighLimit.issuer === destinationAddress ? obj.LowLimit.issuerDetails : obj.HighLimit.issuerDetails || {} + const serviceOrUsername = issuerDetails.service || issuerDetails.username || '' + const issuer = obj.HighLimit.issuer === destinationAddress ? obj.LowLimit.issuer : obj.HighLimit.issuer || '' + + const searchLower = searchQuery.toLowerCase() + return ( + currency.toLowerCase().includes(searchLower) || + serviceOrUsername.toLowerCase().includes(searchLower) || + issuer.toLowerCase().includes(searchLower) + ) + } + + return true + }) + + // Convert trustlines to token format + return trustlines.map(tl => ({ + currency: tl.Balance.currency, + issuer: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuer : tl.HighLimit.issuer, + issuerDetails: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuerDetails : tl.HighLimit.issuerDetails, + limit: Math.max(parseFloat(tl.LowLimit.value), parseFloat(tl.HighLimit.value)), + balance: tl.Balance.value + })) +} + +// Helper function to add native currency to tokens array if needed +const addNativeCurrencyIfNeeded = (tokens, excludeNative, searchQuery = '') => { + if (excludeNative) return tokens + + const shouldAddNative = !searchQuery || searchQuery.toUpperCase() === nativeCurrency.toUpperCase() + if (shouldAddNative) { + tokens.unshift({ currency: nativeCurrency, limit: null }) + } + + return tokens +} + export default function TokenSelector({ value, onChange, excludeNative = false, destinationAddress = null }) { const { t } = useTranslation() const width = useWidth() @@ -55,33 +105,8 @@ export default function TokenSelector({ value, onChange, excludeNative = false, if (destinationAddress) { // Fetch tokens that destination can hold based on trustlines - const response = await axios(`v2/objects/${destinationAddress}?limit=100`) - const objects = response.data?.objects || [] - - // Filter RippleState objects to get trustlines where destination can hold tokens - const trustlines = objects.filter(obj => { - if (obj.LedgerEntryType !== 'RippleState') return false - - if (parseFloat(obj.LowLimit.value) <= 0 && parseFloat(obj.HighLimit.value) <= 0 ) { - return false - } - - return true - }) - - // Convert trustlines to token format - tokens = trustlines.map(tl => ({ - currency: tl.Balance.currency, - issuer: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuer : tl.HighLimit.issuer, - issuerDetails: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuerDetails : tl.HighLimit.issuerDetails, - limit: Math.max(parseFloat(tl.LowLimit.value), parseFloat(tl.HighLimit.value)), - balance: tl.Balance.value - })) - - // Add native currency if not excluded - if (!excludeNative) { - tokens.unshift({ currency: nativeCurrency, limit: null }) - } + tokens = await fetchTrustlinesForDestination(destinationAddress) + tokens = addNativeCurrencyIfNeeded(tokens, excludeNative) } else { // Fallback to original behavior if no destination address const response = await axios('v2/trustlines/tokens?limit=' + limit) @@ -112,53 +137,16 @@ export default function TokenSelector({ value, onChange, excludeNative = false, setIsLoading(true) try { if (destinationAddress) { - // For destination-specific search, we need to filter the existing trustlines - // This is a simplified approach - in a real implementation you might want to - // implement server-side search for trustlines - const response = await axios(`v2/objects/${destinationAddress}?limit=1000`) - const objects = response.data?.objects || [] - - const trustlines = objects.filter(obj => { - if (obj.LedgerEntryType !== 'RippleState') return false - if (parseFloat(obj.LowLimit.value) <= 0 && parseFloat(obj.HighLimit.value) <= 0) return false - - // Filter by search query - const currency = obj.Balance.currency - const issuerDetails = obj.HighLimit.issuer === destinationAddress ? obj.LowLimit.issuerDetails : obj.HighLimit.issuerDetails || {} - const serviceOrUsername = issuerDetails.service || issuerDetails.username || '' - const issuer = obj.HighLimit.issuer === destinationAddress ? obj.LowLimit.issuer : obj.HighLimit.issuer || '' - - const searchLower = searchQuery.toLowerCase() - return ( - currency.toLowerCase().includes(searchLower) || - serviceOrUsername.toLowerCase().includes(searchLower) || - issuer.toLowerCase().includes(searchLower) - ) - }) - - const tokens = trustlines.map(tl => ({ - currency: tl.Balance.currency, - issuer: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuer : tl.HighLimit.issuer, - issuerDetails: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuerDetails : tl.HighLimit.issuerDetails, - limit: Math.max(parseFloat(tl.LowLimit.value), parseFloat(tl.HighLimit.value)), - balance: tl.Balance.value - })) - - if (!excludeNative && searchQuery.toUpperCase() === nativeCurrency.toUpperCase()) { - tokens.unshift({ currency: nativeCurrency, limit: null }) - } - - setSearchResults(tokens) + // For destination-specific search, filter the existing trustlines + const tokens = await fetchTrustlinesForDestination(destinationAddress, searchQuery) + const tokensWithNative = addNativeCurrencyIfNeeded(tokens, excludeNative, searchQuery) + setSearchResults(tokensWithNative) } else { // Fallback to original search behavior const response = await axios(`v2/trustlines/tokens/search/${searchQuery}?limit=${limit}`) const tokens = response.data?.tokens || [] - - if (!excludeNative && searchQuery.toUpperCase() === nativeCurrency.toUpperCase()) { - tokens.unshift({ currency: nativeCurrency }) - } - - setSearchResults(tokens) + const tokensWithNative = addNativeCurrencyIfNeeded(tokens, excludeNative, searchQuery) + setSearchResults(tokensWithNative) } } catch (error) { console.error('Error searching tokens:', error) @@ -286,7 +274,7 @@ export default function TokenSelector({ value, onChange, excludeNative = false,
{searchResults.map((token, index) => (
handleSelect(token)} > @@ -326,4 +314,4 @@ export default function TokenSelector({ value, onChange, excludeNative = false, )}
) -} +} \ No newline at end of file From e3113bcf4e2290ecab9f37e6ca67543dca32ead4 Mon Sep 17 00:00:00 2001 From: Viacheslav Bakshaev Date: Sun, 13 Jul 2025 09:49:23 +0500 Subject: [PATCH 3/3] move styles to a better place, fix UI jumps --- pages/services/send.js | 43 ++++++++++++++++++++++------ styles/components/tokenSelector.scss | 22 -------------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/pages/services/send.js b/pages/services/send.js index 2cacb345..39a3ad66 100644 --- a/pages/services/send.js +++ b/pages/services/send.js @@ -75,14 +75,40 @@ export default function Send({ const getMaxAmountDisplay = () => { const maxAmount = getMaxAmount() if (!maxAmount) return null - + return ( -
- Max amount: - - {amountFormat({ value: maxAmount, currency: selectedToken.currency, issuer: selectedToken.issuer }, { short: true })} + <> + + (Dest. can accept max{' '} + + {amountFormat( + { value: maxAmount, currency: selectedToken.currency, issuer: selectedToken.issuer }, + { short: true } + )} + + ) -
+ + ) } @@ -422,7 +448,9 @@ export default function Send({
- {t('table.amount')} + + {t('table.amount')} {getMaxAmountDisplay()} + setAmount(e.target.value)} @@ -439,7 +467,6 @@ export default function Send({
Currency - {getMaxAmountDisplay()}
diff --git a/styles/components/tokenSelector.scss b/styles/components/tokenSelector.scss index 9117552c..b5c77bd0 100644 --- a/styles/components/tokenSelector.scss +++ b/styles/components/tokenSelector.scss @@ -240,25 +240,3 @@ } } } - -// Max amount display styles for send page -.max-amount-display { - display: flex; - align-items: center; - gap: 6px; - margin-top: 4px; - font-size: 12px; - - .max-amount-label { - color: #6b7280; - font-weight: 500; - .dark & { - color: #9ca3af; - } - } - - .max-amount-value { - color: var(--accent-link); - font-weight: 500; - } -}