Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
5 changes: 1 addition & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',

Expand Down Expand Up @@ -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',
Expand Down
97 changes: 55 additions & 42 deletions src/components/scenes/EditTokenScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,9 +46,9 @@ interface Props extends EdgeAppSceneProps<'editToken'> {
wallet: EdgeCurrencyWallet
}

function EditTokenSceneComponent(props: Props) {
const EditTokenSceneComponent: React.FC<Props> = props => {
const { navigation, route, wallet } = props
const { tokenId } = route.params
const { deferEnable, tokenId } = route.params

const theme = useTheme()
const styles = getStyles(theme)
Expand All @@ -61,7 +67,7 @@ function EditTokenSceneComponent(props: Props) {
return (multiplier.length - 1).toString()
})

const emptyNetworkLocation = () => {
const emptyNetworkLocation = (): Map<string, string> => {
const out = new Map<string, string>()
for (const item of customTokenTemplate) {
const value = route.params.networkLocation?.[item.key]
Expand Down Expand Up @@ -90,7 +96,8 @@ function EditTokenSceneComponent(props: Props) {
if (tokenId == null) return
await Airship.show<'ok' | 'cancel' | undefined>(bridge => (
<ButtonsModal
// @ts-expect-error
// @ts-expect-error ButtonsModal's bridge generic cannot infer the
// union of button keys from this inline `buttons` object.
bridge={bridge}
title={lstrings.string_delete}
message={lstrings.edittoken_delete_prompt}
Expand Down Expand Up @@ -196,21 +203,26 @@ function EditTokenSceneComponent(props: Props) {
}

// Check if custom token input conflicts with custom tokens.
if (currencyConfig.customTokens[newTokenId] != null) {
const isNewCustomToken = currencyConfig.customTokens[newTokenId] == null
if (isNewCustomToken) {
await currencyConfig.addCustomToken(customTokenInput)
} else {
// Always override changes to custom tokens
// TODO: Fine for if they are on this scene intentionally modifying a
// custom token, but maybe warn about this override if they are trying
// to add a new custom token with the same contract address as an
// existing custom token
await currencyConfig.changeCustomToken(newTokenId, customTokenInput)
} else {
await currencyConfig.addCustomToken(customTokenInput)
}

await wallet.changeEnabledTokenIds([
...wallet.enabledTokenIds,
newTokenId
])
// A brand-new custom token shows up in the caller's token list, so the
// caller can enable it as part of its own save flow:
if (!isNewCustomToken || deferEnable !== true) {
await wallet.changeEnabledTokenIds([
...wallet.enabledTokenIds,
newTokenId
])
}
logActivity(
`Add Custom Token: ${account.username} -- ${getWalletName(wallet)} -- ${
wallet.type
Expand All @@ -220,7 +232,7 @@ function EditTokenSceneComponent(props: Props) {
}
})

const autoCompleteToken = async (searchString: string) => {
const autoCompleteToken = async (searchString: string): Promise<void> => {
if (
// Ignore autocomplete if it's already loading
isAutoCompleteTokenLoading.current ||
Expand Down Expand Up @@ -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 (
<FilledTextInput
key={item.key}
aroundRem={0.5}
autoCapitalize="none"
autoCorrect={false}
autoFocus={false}
placeholder={translateDescription(item.displayName)}
keyboardType={item.type === 'number' ? 'numeric' : 'default'}
value={location.get(item.key) ?? ''}
onChangeText={value => {
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<React.ReactElement | null> => {
return customTokenTemplate
.sort((a, b) => (a.key === 'contractAddress' ? -1 : 1))
.map(item => {
if (item.type === 'nativeAmount') return null
return (
<FilledTextInput
key={item.key}
aroundRem={0.5}
autoCapitalize="none"
autoCorrect={false}
autoFocus={false}
placeholder={translateDescription(item.displayName)}
keyboardType={item.type === 'number' ? 'numeric' : 'default'}
value={location.get(item.key) ?? ''}
onChangeText={value => {
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 (
<SceneWrapper avoidKeyboard>
Expand Down
43 changes: 38 additions & 5 deletions src/components/scenes/ManageTokensScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,17 @@ const ManageTokensSceneComponent: React.FC<Props> = 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
Expand Down Expand Up @@ -123,9 +127,37 @@ const ManageTokensSceneComponent: React.FC<Props> = 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) => {
Expand Down Expand Up @@ -200,6 +232,7 @@ const ManageTokensSceneComponent: React.FC<Props> = props => {
// Goes to the add token scene:
const handleAdd = useHandler(() => {
navigation.navigate('editToken', {
deferEnable: true,
walletId: wallet.id
})
})
Expand Down
12 changes: 10 additions & 2 deletions src/components/themed/ManageTokensRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ export const ManageTokensRowComponent: React.FC<Props> = props => {
})

return (
<Pressable style={styles.row} onPress={handleToggle}>
<Pressable
style={styles.row}
testID={`manageTokensRow_${token.currencyCode}`}
onPress={handleToggle}
>
<CryptoIcon
marginRem={[0, 0.5, 0, 0]} // We don't need left margins because there's no border. This component effectively is the left "border"
sizeRem={2}
Expand All @@ -96,7 +100,11 @@ export const ManageTokensRowComponent: React.FC<Props> = props => {
<EdgeText style={styles.displayName}>{token.displayName}</EdgeText>
</View>
{!isCustom ? null : (
<EdgeTouchableOpacity style={styles.editIcon} onPress={handleEdit}>
<EdgeTouchableOpacity
style={styles.editIcon}
testID={`manageTokensRow_${token.currencyCode}_edit`}
onPress={handleEdit}
>
<FontAwesomeIcon
color={theme.iconTappable}
name="edit"
Expand Down
1 change: 1 addition & 0 deletions src/components/themed/WalletListCurrencyRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ const WalletListCurrencyRowComponent: React.FC<Props> = props => {
onLongPress={handleLongPress}
onPress={handlePress}
paddingRem={0.5}
testID={`walletListRow_${displayCurrencyCode}_${walletName}`}
gradientBackground={{
colors: [primaryColor, '#00000000'],
start: { x: 0, y: 0 },
Expand Down
Loading