Skip to content

Commit 502a6c8

Browse files
CP-10649: Add custom token by network (#2768)
1 parent 7ae310a commit 502a6c8

File tree

15 files changed

+616
-305
lines changed

15 files changed

+616
-305
lines changed

packages/core-mobile/app/new/common/hooks/useAddCustomToken.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import { useDispatch, useSelector } from 'react-redux'
22
import { isAddress } from 'ethers'
33
import { addCustomToken as addCustomTokenAction } from 'store/customToken'
4-
import { useState, useEffect, useMemo } from 'react'
4+
import { useState, useEffect, useMemo, useCallback } from 'react'
55
import { Network } from '@avalabs/core-chains-sdk'
66
import Logger from 'utils/Logger'
77
import AnalyticsService from 'services/analytics/AnalyticsService'
88
import TokenService from 'services/token/TokenService'
9-
import { useNetworks } from 'hooks/networks/useNetworks'
109
import {
1110
NetworkContractToken,
1211
TokenType,
1312
TokenWithBalanceERC20
1413
} from '@avalabs/vm-module-types'
1514
import { useNetworkContractTokens } from 'hooks/networks/useNetworkContractTokens'
16-
import { selectTokensWithBalance } from 'store/balance'
15+
import { selectTokensWithBalanceByNetwork } from 'store/balance'
16+
import {
17+
useSelectedNetwork,
18+
useTokenAddress
19+
} from 'features/tokenManagement/store'
1720

1821
enum AddressValidationStatus {
1922
Valid,
@@ -67,15 +70,18 @@ type CustomToken = {
6770
}
6871

6972
const useAddCustomToken = (callback: () => void): CustomToken => {
70-
const { activeNetwork } = useNetworks()
71-
const tokens = useNetworkContractTokens(activeNetwork)
72-
const [tokenAddress, setTokenAddress] = useState('')
73+
const [tokenAddress, setTokenAddress] = useTokenAddress()
7374
const [errorMessage, setErrorMessage] = useState('')
7475
const [token, setToken] = useState<NetworkContractToken>()
7576
const dispatch = useDispatch()
76-
const chainId = activeNetwork.chainId
7777
const [isLoading, setIsLoading] = useState(false)
78-
const tokensWithBalance = useSelector(selectTokensWithBalance)
78+
const [selectedNetwork] = useSelectedNetwork()
79+
const chainId = selectedNetwork?.chainId
80+
81+
const tokens = useNetworkContractTokens(selectedNetwork)
82+
const tokensWithBalance = useSelector(
83+
selectTokensWithBalanceByNetwork(chainId)
84+
)
7985

8086
const tokenAddresses = useMemo(
8187
() => [
@@ -109,6 +115,20 @@ const useAddCustomToken = (callback: () => void): CustomToken => {
109115
)
110116

111117
useEffect(() => {
118+
if (tokenAddress === '') {
119+
setErrorMessage('')
120+
setToken(undefined)
121+
}
122+
}, [tokenAddress])
123+
124+
useEffect(() => {
125+
if (selectedNetwork === undefined) {
126+
if (tokenAddress) {
127+
setErrorMessage('Please select a network.')
128+
}
129+
return
130+
}
131+
112132
const validationStatus = validateAddress(tokenAddress, tokenAddresses)
113133
switch (validationStatus) {
114134
case AddressValidationStatus.Invalid:
@@ -121,7 +141,7 @@ const useAddCustomToken = (callback: () => void): CustomToken => {
121141
break
122142
case AddressValidationStatus.Valid:
123143
setIsLoading(true)
124-
fetchTokenData(activeNetwork, tokenAddress)
144+
fetchTokenData(selectedNetwork, tokenAddress)
125145
.then(t => {
126146
setToken(t)
127147
setErrorMessage('')
@@ -141,10 +161,10 @@ const useAddCustomToken = (callback: () => void): CustomToken => {
141161
setErrorMessage('')
142162
setToken(undefined)
143163
}
144-
}, [activeNetwork, tokenAddress, tokenAddresses, tokens])
164+
}, [selectedNetwork, tokenAddress, tokenAddresses])
145165

146-
const addCustomToken = (): void => {
147-
if (token) {
166+
const addCustomToken = useCallback((): void => {
167+
if (token && chainId) {
148168
dispatch(addCustomTokenAction({ chainId, token }))
149169
setTokenAddress('')
150170
callback()
@@ -153,7 +173,7 @@ const useAddCustomToken = (callback: () => void): CustomToken => {
153173
address: token.address
154174
})
155175
}
156-
}
176+
}, [token, chainId, dispatch, setTokenAddress, callback])
157177

158178
return {
159179
tokenAddress,
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import {
2+
Button,
3+
Icons,
4+
Text,
5+
TouchableOpacity,
6+
useTheme,
7+
View
8+
} from '@avalabs/k2-alpine'
9+
import { showSnackbar } from 'common/utils/toast'
10+
import React, { useCallback } from 'react'
11+
import useAddCustomToken from 'common/hooks/useAddCustomToken'
12+
import { LocalTokenWithBalance } from 'store/balance'
13+
import { useRouter } from 'expo-router'
14+
import { LogoWithNetwork } from 'features/portfolio/assets/components/LogoWithNetwork'
15+
import { LoadingState } from 'common/components/LoadingState'
16+
import { ScrollScreen } from 'common/components/ScrollScreen'
17+
import { TokenLogo } from 'common/components/TokenLogo'
18+
import { TextInput } from 'react-native'
19+
import { useSelectedNetwork } from '../store'
20+
21+
export const AddCustomTokenScreen = (): JSX.Element => {
22+
const {
23+
theme: { colors }
24+
} = useTheme()
25+
const [selectedNetwork, setSelectedNetwork] = useSelectedNetwork()
26+
const { canGoBack, back, navigate } = useRouter()
27+
28+
const showSuccess = useCallback(() => {
29+
showSnackbar('Added!')
30+
setSelectedNetwork(undefined)
31+
canGoBack() && back()
32+
}, [setSelectedNetwork, canGoBack, back])
33+
34+
const {
35+
tokenAddress,
36+
setTokenAddress,
37+
errorMessage,
38+
token,
39+
addCustomToken,
40+
isLoading
41+
} = useAddCustomToken(showSuccess)
42+
43+
// only enable button if we have token and no error message
44+
const disabled = !!(errorMessage || !token || isLoading)
45+
46+
const goToScanQrCode = useCallback((): void => {
47+
// @ts-ignore TODO: make routes typesafe
48+
navigate('/tokenManagement/scanQrCode')
49+
}, [navigate])
50+
51+
const goToSelectNetwork = useCallback((): void => {
52+
// @ts-ignore TODO: make routes typesafe
53+
navigate('/selectCustomTokenNetwork')
54+
}, [navigate])
55+
56+
const renderToken = useCallback((): JSX.Element | undefined => {
57+
if (isLoading) {
58+
return <LoadingState sx={{ flex: 1 }} />
59+
}
60+
61+
if (!token) {
62+
return undefined
63+
}
64+
65+
return (
66+
<View sx={{ marginTop: 32 }}>
67+
<View
68+
style={{ justifyContent: 'center', alignItems: 'center', gap: 16 }}>
69+
<LogoWithNetwork
70+
token={token as LocalTokenWithBalance}
71+
outerBorderColor={colors.$surfacePrimary}
72+
/>
73+
<Text variant="heading6">{token.name}</Text>
74+
</View>
75+
<Button
76+
disabled={disabled}
77+
size="large"
78+
type="primary"
79+
style={{ margin: 16 }}
80+
onPress={addCustomToken}>
81+
Add
82+
</Button>
83+
</View>
84+
)
85+
}, [token, colors.$surfacePrimary, addCustomToken, disabled, isLoading])
86+
87+
const renderTokenAddress = useCallback(() => {
88+
return (
89+
<>
90+
<View
91+
sx={{
92+
backgroundColor: colors.$surfaceSecondary,
93+
borderRadius: 12,
94+
padding: 16,
95+
flexDirection: 'row',
96+
justifyContent: 'space-between'
97+
}}>
98+
<View sx={{ width: '90%' }}>
99+
<Text
100+
variant="body2"
101+
sx={{
102+
fontSize: 11,
103+
lineHeight: 14,
104+
color: colors.$textSecondary
105+
}}>
106+
Token contract address
107+
</Text>
108+
<TextInput
109+
onChangeText={setTokenAddress}
110+
numberOfLines={2}
111+
multiline
112+
value={tokenAddress}
113+
style={{
114+
color: colors.$textPrimary,
115+
fontSize: 15,
116+
lineHeight: 20
117+
}}
118+
/>
119+
</View>
120+
<TouchableOpacity onPress={goToScanQrCode} hitSlop={16}>
121+
<Icons.Custom.QRCodeScanner
122+
color={colors.$textPrimary}
123+
width={20}
124+
height={20}
125+
/>
126+
</TouchableOpacity>
127+
</View>
128+
129+
{errorMessage.length > 0 && (
130+
<View
131+
sx={{
132+
flexDirection: 'row',
133+
alignItems: 'center'
134+
}}>
135+
<Icons.Action.Info color={colors.$textDanger} />
136+
<Text
137+
variant="subtitle1"
138+
sx={{ color: colors.$textDanger, marginLeft: 8 }}>
139+
{errorMessage}
140+
</Text>
141+
</View>
142+
)}
143+
</>
144+
)
145+
}, [
146+
colors.$surfaceSecondary,
147+
colors.$textSecondary,
148+
colors.$textPrimary,
149+
colors.$textDanger,
150+
setTokenAddress,
151+
tokenAddress,
152+
goToScanQrCode,
153+
errorMessage
154+
])
155+
156+
const renderNetwork = useCallback((): JSX.Element => {
157+
return (
158+
<TouchableOpacity
159+
onPress={goToSelectNetwork}
160+
sx={{
161+
backgroundColor: colors.$surfaceSecondary,
162+
paddingHorizontal: 16,
163+
paddingVertical: 10,
164+
justifyContent: 'space-between',
165+
flexDirection: 'row',
166+
borderRadius: 12,
167+
alignItems: 'center'
168+
}}>
169+
<Text variant="body2" sx={{ fontSize: 16, lineHeight: 22 }}>
170+
Network
171+
</Text>
172+
{selectedNetwork ? (
173+
<View sx={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
174+
<TokenLogo logoUri={selectedNetwork.logoUri} size={24} />
175+
<Text
176+
variant="body2"
177+
sx={{
178+
fontSize: 16,
179+
lineHeight: 22,
180+
color: colors.$textSecondary
181+
}}>
182+
{selectedNetwork.chainName}
183+
</Text>
184+
<View sx={{ marginHorizontal: 8 }}>
185+
<Icons.Navigation.ChevronRightV2 />
186+
</View>
187+
</View>
188+
) : (
189+
<View sx={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
190+
<Text
191+
variant="body2"
192+
sx={{
193+
fontSize: 16,
194+
lineHeight: 22,
195+
color: colors.$textSecondary
196+
}}>
197+
Select
198+
</Text>
199+
<View sx={{ marginHorizontal: 8 }}>
200+
<Icons.Navigation.ChevronRightV2 />
201+
</View>
202+
</View>
203+
)}
204+
</TouchableOpacity>
205+
)
206+
}, [
207+
colors.$surfaceSecondary,
208+
colors.$textSecondary,
209+
goToSelectNetwork,
210+
selectedNetwork
211+
])
212+
213+
return (
214+
<ScrollScreen
215+
title={`Add a custom\ntoken`}
216+
contentContainerStyle={{ padding: 16 }}
217+
shouldAvoidKeyboard
218+
isModal>
219+
<View sx={{ gap: 10, marginTop: 24 }}>
220+
{renderNetwork()}
221+
{selectedNetwork && renderTokenAddress()}
222+
{renderToken()}
223+
</View>
224+
</ScrollScreen>
225+
)
226+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Text, View } from '@avalabs/k2-alpine'
2+
import { useHeaderHeight } from '@react-navigation/elements'
3+
import { QrCodeScanner } from 'common/components/QrCodeScanner'
4+
import { useRouter } from 'expo-router'
5+
import React from 'react'
6+
import { useTokenAddress } from '../store'
7+
8+
export const ScanQrCodeScreen = (): JSX.Element => {
9+
const { dismiss } = useRouter()
10+
const headerHeight = useHeaderHeight()
11+
const [tokenAddress, setTokenAddress] = useTokenAddress()
12+
13+
const handleOnSuccess = (address: string): void => {
14+
if (tokenAddress) return
15+
setTokenAddress(address)
16+
dismiss()
17+
}
18+
19+
return (
20+
<View
21+
sx={{ paddingHorizontal: 16, paddingTop: headerHeight + 16, flex: 1 }}>
22+
<Text variant="heading2">Scan a QR code</Text>
23+
<QrCodeScanner
24+
onSuccess={handleOnSuccess}
25+
vibrate={true}
26+
sx={{
27+
height: '80%',
28+
width: '100%',
29+
marginTop: 21
30+
}}
31+
/>
32+
</View>
33+
)
34+
}

0 commit comments

Comments
 (0)