diff --git a/CHANGELOG.md b/CHANGELOG.md index cfba92d8c57..5daba512d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ - fixed: Improve the unstake error experience by replacing the popup alert and generic "unknown error occurred" with the real error in the scene's error field, and showing a clear message when the wallet lacks the balance to cover the unstaking network fee. - fixed: Tapping Max on the Sell scene no longer briefly shows the entered fiat amount in the crypto field while the max is being calculated. - fixed: An info card no longer disappears into an empty gap when the carousel's card list shrinks. A card's position comes entirely from an animated transform keyed on its index, and that transform is not re-applied when a surviving card shifts slots, so dropping a card left the ones after it parked a full card-width off-screen. The carousel now remounts a card whose slot changes. Reproduces wherever the list shrinks after mount - most visibly when a `noBalance` card is filtered out as balances finish loading. +- fixed: The Manage Tokens Save button now enables after a custom token is added, so the addition can be applied without also toggling one of the default tokens. +- fixed: A newly added custom token now appears with the enabled tokens at the top of Manage Tokens, instead of only turning up through a name search. ## 4.50.2 (2026-08-06) diff --git a/eslint.config.mjs b/eslint.config.mjs index e817da3e660..38e47c44237 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -268,7 +268,6 @@ export default [ 'src/components/scenes/DuressModeHowToScene.tsx', 'src/components/scenes/DuressModeSettingScene.tsx', - 'src/components/scenes/EditTokenScene.tsx', 'src/components/scenes/ExtraTabScene.tsx', 'src/components/scenes/Fio/FioAddressListScene.tsx', @@ -296,8 +295,6 @@ export default [ 'src/components/scenes/Loans/LoanManageScene.tsx', 'src/components/scenes/Loans/LoanStatusScene.tsx', - 'src/components/scenes/ManageTokensScene.tsx', - 'src/components/scenes/NotificationCenterScene.tsx', 'src/components/scenes/NotificationScene.tsx', @@ -359,7 +356,7 @@ export default [ 'src/components/themed/LineTextDivider.tsx', 'src/components/themed/MainButton.tsx', - 'src/components/themed/ManageTokensRow.tsx', + 'src/components/themed/MenuTabs.tsx', 'src/components/themed/ModalParts.tsx', 'src/components/themed/PinDots.tsx', diff --git a/src/components/scenes/EditTokenScene.tsx b/src/components/scenes/EditTokenScene.tsx index 80f59be8984..4fdd2200433 100644 --- a/src/components/scenes/EditTokenScene.tsx +++ b/src/components/scenes/EditTokenScene.tsx @@ -26,6 +26,12 @@ import { SceneHeader } from '../themed/SceneHeader' export interface EditTokenParams { currencyCode?: string + + /** True when the caller enables the new token itself, so this scene must + * only create it. ManageTokensScene sets this so its Save button is what + * applies the addition. Ignored when editing an existing token. */ + deferEnable?: boolean + displayName?: string multiplier?: string networkLocation?: JsonObject @@ -40,9 +46,9 @@ interface Props extends EdgeAppSceneProps<'editToken'> { wallet: EdgeCurrencyWallet } -function EditTokenSceneComponent(props: Props) { +const EditTokenSceneComponent: React.FC = props => { const { navigation, route, wallet } = props - const { tokenId } = route.params + const { deferEnable, tokenId } = route.params const theme = useTheme() const styles = getStyles(theme) @@ -61,7 +67,7 @@ function EditTokenSceneComponent(props: Props) { return (multiplier.length - 1).toString() }) - const emptyNetworkLocation = () => { + const emptyNetworkLocation = (): Map => { const out = new Map() for (const item of customTokenTemplate) { const value = route.params.networkLocation?.[item.key] @@ -90,7 +96,8 @@ function EditTokenSceneComponent(props: Props) { if (tokenId == null) return await Airship.show<'ok' | 'cancel' | undefined>(bridge => ( { + const autoCompleteToken = async (searchString: string): Promise => { if ( // Ignore autocomplete if it's already loading isAutoCompleteTokenLoading.current || @@ -266,36 +278,37 @@ function EditTokenSceneComponent(props: Props) { } } - const renderCustomTokenTemplateRows = () => { - return customTokenTemplate - .sort((a, b) => (a.key === 'contractAddress' ? -1 : 1)) - .map(item => { - if (item.type === 'nativeAmount') return null - return ( - { - setLocation(location => { - const out = new Map(location) - out.set(item.key, value.replace(/\s/g, '')) - return out - }) - - if (item.key === 'contractAddress') { - autoCompleteToken(value).catch(() => {}) - } - }} - /> - ) - }) - } + const renderCustomTokenTemplateRows = + (): Array => { + return customTokenTemplate + .sort((a, b) => (a.key === 'contractAddress' ? -1 : 1)) + .map(item => { + if (item.type === 'nativeAmount') return null + return ( + { + setLocation(location => { + const out = new Map(location) + out.set(item.key, value.replace(/\s/g, '')) + return out + }) + + if (item.key === 'contractAddress') { + autoCompleteToken(value).catch(() => {}) + } + }} + /> + ) + }) + } return ( diff --git a/src/components/scenes/ManageTokensScene.tsx b/src/components/scenes/ManageTokensScene.tsx index 02512622e7e..08f41bea8cd 100644 --- a/src/components/scenes/ManageTokensScene.tsx +++ b/src/components/scenes/ManageTokensScene.tsx @@ -76,13 +76,17 @@ const ManageTokensSceneComponent: React.FC = props => { () => new Set(enabledTokenIds) ) - // Baseline for sorting (fixed on mount, never changes): - const sortingBaselineSet = React.useMemo( - () => new Set(enabledTokenIds), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + // Baseline for sorting. Fixed on mount so toggling a row never re-orders the + // list, except that custom tokens added during this session join it, so they + // show up with the enabled tokens at the top: + const [sortingBaselineSet, setSortingBaselineSet] = React.useState( + () => new Set(enabledTokenIds) ) + // Custom tokens we have already accounted for. The Add Token scene only + // creates brand-new custom tokens, leaving us to enable them: + const seenCustomTokenIdsRef = React.useRef(new Set(Object.keys(customTokens))) + // Check if there are unsaved changes: const hasUnsavedChanges = React.useMemo(() => { if (pendingEnabledTokenIds.size !== baselineSet.size) return true @@ -123,9 +127,37 @@ const ManageTokensSceneComponent: React.FC = props => { for (const tokenId of toAdd) next.add(tokenId) return next }) + // ...and sort them in with the rest of the enabled tokens: + setSortingBaselineSet(prev => { + const next = new Set(prev) + for (const tokenId of toAdd) next.add(tokenId) + return next + }) } }, [enabledTokenIds, baselineSet]) + // Treat custom tokens created during this session as pending additions, so + // the Save button applies them and they sort in with the enabled tokens: + React.useEffect(() => { + const currentCustomTokenIds = Object.keys(customTokens) + const addedTokenIds = currentCustomTokenIds.filter( + tokenId => !seenCustomTokenIdsRef.current.has(tokenId) + ) + seenCustomTokenIdsRef.current = new Set(currentCustomTokenIds) + if (addedTokenIds.length === 0) return + + setPendingEnabledTokenIds(prev => { + const next = new Set(prev) + for (const tokenId of addedTokenIds) next.add(tokenId) + return next + }) + setSortingBaselineSet(prev => { + const next = new Set(prev) + for (const tokenId of addedTokenIds) next.add(tokenId) + return next + }) + }, [customTokens]) + // Sort the token list (only re-sort when allTokens changes, not on toggle): const sortedTokenIds = React.useMemo(() => { return Object.keys(allTokens).sort((id1, id2) => { @@ -200,6 +232,7 @@ const ManageTokensSceneComponent: React.FC = props => { // Goes to the add token scene: const handleAdd = useHandler(() => { navigation.navigate('editToken', { + deferEnable: true, walletId: wallet.id }) }) diff --git a/src/components/themed/ManageTokensRow.tsx b/src/components/themed/ManageTokensRow.tsx index 36cf7555bf1..f59c30247e6 100644 --- a/src/components/themed/ManageTokensRow.tsx +++ b/src/components/themed/ManageTokensRow.tsx @@ -83,7 +83,11 @@ export const ManageTokensRowComponent: React.FC = props => { }) return ( - + = props => { {token.displayName} {!isCustom ? null : ( - + = props => { onLongPress={handleLongPress} onPress={handlePress} paddingRem={0.5} + testID={`walletListRow_${displayCurrencyCode}_${walletName}`} gradientBackground={{ colors: [primaryColor, '#00000000'], start: { x: 0, y: 0 },