diff --git a/src/containers/AssetsContainer.tsx b/src/containers/AssetsContainer.tsx index c8e2959b5..195f24998 100644 --- a/src/containers/AssetsContainer.tsx +++ b/src/containers/AssetsContainer.tsx @@ -1,133 +1,26 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { StyleSheet, View, Image, Text, TouchableOpacity, ScrollView, RefreshControl } from 'react-native'; -import { TButtonOutlined } from '../components/atoms/TButton'; -import { TP } from '../components/atoms/THeadings'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useIsFocused } from '@react-navigation/native'; import theme, { commonStyles } from '../utils/theme'; import { AssetsScreenNavigationProp } from '../screens/AssetListingScreen'; -import useWalletStore from '../store/useWalletStore'; import Debug from 'debug'; import { formatCurrencyValue } from '../utils/numbers'; import { capitalizeFirstLetter } from '../utils/strings'; -import { isNetworkError } from '../utils/errors'; -import { assetStorage, connect } from '../utils/StorageManager/setup'; import { ArrowDown, ArrowUp } from 'iconoir-react-native'; import { tokenRegistry } from '../utils/tokenRegistry'; import TSpinner from '../components/atoms/TSpinner'; import useAppSettings from '../hooks/useAppSettings'; -import useUserStore from '../store/userStore'; +import useAssetManager from '../hooks/useAssetManager'; const debug = Debug('tonomy-id:containers:AssetsContainer'); export default function AssetsContainer({ navigation }: { navigation: AssetsScreenNavigationProp['navigation'] }) { const [total, setTotal] = useState(0); - const [isAssetLoading, setAssetLoading] = useState(true); - const [refreshBalance, setRefreshBalance] = useState(false); - const { developerMode } = useAppSettings(); - - const { accountsInitialized, initializeWalletAccount, updateBalance } = useWalletStore(); - const isUpdatingBalances = useRef(false); - const [accounts, setAccounts] = useState< - { network: string; accountName: string; balance: string; usdBalance: number }[] - >([]); - const { user } = useUserStore(); + const { developerMode } = useAppSettings(); + const { isAssetLoading, accounts, onRefresh, refreshBalance } = useAssetManager(); const tokens = useMemo(() => tokenRegistry, []); - const fetchCryptoAssets = useCallback(async () => { - try { - if (!accountsInitialized) await initializeWalletAccount(user); - await connect(); - - for (const { chain, token } of tokens) { - try { - const asset = await assetStorage.findAssetByName(token); - - debug( - `fetchCryptoAssets() fetching asset ${chain.getName()}: ${asset?.accountName}-${asset?.balance}` - ); - let account; - - if (asset) { - account = { - network: capitalizeFirstLetter(chain.getName()), - accountName: asset.accountName, - balance: asset.balance, - usdBalance: asset.usdBalance, - }; - } else { - account = { - network: capitalizeFirstLetter(chain.getName()), - accountName: null, - balance: '0', - usdBalance: 0, - }; - } - - setAccounts((prevAccounts) => { - // find index of the account in the array - const index = prevAccounts.findIndex((acc) => acc.network === account.network); - - if (index !== -1) { - // Update the existing asset - const updatedAccounts = [...prevAccounts]; - - updatedAccounts[index] = account; - return updatedAccounts; - } else { - // Add the new asset - return [...prevAccounts, account]; - } - }); - } catch (error) { - debug(`fetchCryptoAssets() error fetching ${chain.getName()} asset`, error); - } - } - } catch (error) { - console.error('fetchCryptoAssets() error', error); - } - }, [accountsInitialized, initializeWalletAccount, tokens, user]); - - const updateAllBalances = useCallback(async () => { - if (isUpdatingBalances.current) return; // Prevent re-entry if already running - isUpdatingBalances.current = true; - - try { - debug('updateAllBalances()'); - await updateBalance(); - await fetchCryptoAssets(); - setAssetLoading(false); - } catch (error) { - if (isNetworkError(error)) { - debug('updateAllBalances() Error updating account detail network error:'); - } else { - console.error('AssetsContainer() updateAllBalances() error', error); - } - } finally { - isUpdatingBalances.current = false; - } - }, [updateBalance, fetchCryptoAssets]); - - const onRefresh = useCallback(async () => { - try { - setRefreshBalance(true); - await updateAllBalances(); - } finally { - setRefreshBalance(false); - } - }, [updateAllBalances]); - - // updateAllBalances() on mount and every 20 seconds - useEffect(() => { - updateAllBalances(); - - const interval = setInterval(updateAllBalances, 10000); - - return () => clearInterval(interval); - }, [updateAllBalances]); - useEffect(() => { const totalAssetsUSDBalance = accounts.reduce((previousValue, currentValue) => { return previousValue + currentValue.usdBalance; @@ -225,30 +118,47 @@ export default function AssetsContainer({ navigation }: { navigation: AssetsScre )} - {accountData.account ? ( - - - {accountData.balance} - - - ${formatCurrencyValue(accountData.usdBalance ?? 0)} - + {refreshBalance ? ( + + ) : ( - - { - navigation.navigate('CreateEthereumKey', { - requestType: 'createKey', - request: null, - transaction: null, - }); - }} - > - Not connected - Generate key - - + <> + {accountData.account ? ( + + + + {accountData.balance} + + + + ${formatCurrencyValue(accountData.usdBalance ?? 0)} + + + ) : ( + + { + navigation.navigate('CreateEthereumKey', { + requestType: 'createKey', + request: null, + transaction: null, + }); + }} + > + Not connected + Generate key + + + )} + )} @@ -257,8 +167,15 @@ export default function AssetsContainer({ navigation }: { navigation: AssetsScre })} ) : ( - - + + )} diff --git a/src/containers/SelectAssetContainer.tsx b/src/containers/SelectAssetContainer.tsx index 3a295c7d9..53bdb53d2 100644 --- a/src/containers/SelectAssetContainer.tsx +++ b/src/containers/SelectAssetContainer.tsx @@ -1,13 +1,14 @@ -import { ScrollView, StyleSheet, Text, TouchableOpacity, View, Image } from 'react-native'; +import { ScrollView, StyleSheet, Text, TouchableOpacity, View, Image, RefreshControl } from 'react-native'; import { SelectAssetScreenNavigationProp } from '../screens/SelectAssetScreen'; import theme from '../utils/theme'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { assetStorage, connect } from '../utils/StorageManager/setup'; +import { useMemo } from 'react'; import { capitalizeFirstLetter } from '../utils/strings'; import Debug from 'debug'; import { formatCurrencyValue } from '../utils/numbers'; import { TokenRegistryEntry, getKeyOrNullFromChain, tokenRegistry } from '../utils/tokenRegistry'; import useAppSettings from '../hooks/useAppSettings'; +import useAssetManager from '../hooks/useAssetManager'; +import TSpinner from '../components/atoms/TSpinner'; const debug = Debug('tonomy-id:containers:MainContainer'); @@ -18,69 +19,12 @@ const SelectAssetContainer = ({ navigation: SelectAssetScreenNavigationProp['navigation']; type: string; }) => { - const [accounts, setAccounts] = useState< - { network: string; accountName: string | null; balance: string; usdBalance: number }[] - >([]); + const { isAssetLoading, accounts, onRefresh, refreshBalance } = useAssetManager(); const { developerMode } = useAppSettings(); const tokens = useMemo(() => tokenRegistry, []); - const fetchCryptoAssets = useCallback(async () => { - try { - await connect(); - - for (const { chain, token } of tokens) { - const asset = await assetStorage.findAssetByName(token); - - debug(`fetchCryptoAssets() fetching asset for ${chain.getName()}`); - let account; - - if (asset) { - account = { - network: capitalizeFirstLetter(chain.getName()), - accountName: asset.accountName, - balance: asset.balance, - usdBalance: asset.usdBalance, - }; - } else { - account = { - network: capitalizeFirstLetter(chain.getName()), - accountName: null, - balance: '0', - usdBalance: 0, - }; - } - - setAccounts((prevAccounts) => { - // find index of the account in the array - const index = prevAccounts.findIndex((acc) => acc.network === account.network); - - if (index !== -1) { - // Update the existing asset - const updatedAccounts = [...prevAccounts]; - - updatedAccounts[index] = account; - return updatedAccounts; - } else { - // Add the new asset - return [...prevAccounts, account]; - } - }); - } - } catch (error) { - debug('fetchCryptoAssets() error', error); - } - }, [tokens]); - - useEffect(() => { - fetchCryptoAssets(); - - const interval = setInterval(fetchCryptoAssets, 10000); - - return () => clearInterval(interval); - }, [fetchCryptoAssets]); - const findAccountByChain = (chain: string) => { const accountExists = accounts.find((account) => account.network === chain); const balance = accountExists?.balance; @@ -115,62 +59,90 @@ const SelectAssetContainer = ({ return ( - + } + > select a currency to {type} - - {tokens.map((chainObj, index) => { - const chainName = capitalizeFirstLetter(chainObj.chain.getName()); - - const accountData = findAccountByChain(chainName); - - if (chainObj.chain.isTestnet() && !developerMode) { - return null; - } - - return ( - handleOnPress(chainObj)} - > - - - + {!isAssetLoading ? ( + + {tokens.map((chainObj, index) => { + const chainName = capitalizeFirstLetter(chainObj.chain.getName()); + + const accountData = findAccountByChain(chainName); + + if (chainObj.chain.isTestnet() && !developerMode) { + return null; + } + + return ( + handleOnPress(chainObj)} + > + + - {chainObj.token.getSymbol()} - - {chainName} + + {chainObj.token.getSymbol()} + + {chainName} + + {chainObj.chain.isTestnet() && ( + + + Testnet + + + )} - {chainObj.chain.isTestnet() && ( - - - Testnet + {refreshBalance ? ( + + + + ) : ( + + + {accountData.balance} + + + ${formatCurrencyValue(accountData.usdBalance ?? 0)} )} - - - {accountData.balance} - - - ${formatCurrencyValue(accountData.usdBalance ?? 0)} - - - - - ); - })} - + + ); + })} + + ) : ( + + + + )} diff --git a/src/hooks/useAssetManager.ts b/src/hooks/useAssetManager.ts new file mode 100644 index 000000000..05a134c35 --- /dev/null +++ b/src/hooks/useAssetManager.ts @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import useFetchCrytpoAccount from './useFetchCrytpoAccount'; +import useUpdateBalances from './useUpdateBalances'; + +const useAssetManager = () => { + const [isAssetLoading, setAssetLoading] = useState(true); + const [refreshBalance, setRefreshBalance] = useState(false); + const [accounts, setAccounts] = useState< + { network: string; accountName: string; balance: string; usdBalance: number }[] + >([]); + + const { fetchCryptoAssets } = useFetchCrytpoAccount({ + setAccounts, + setAssetLoading, + }); + + const { updateAllBalances, onRefresh } = useUpdateBalances({ + fetchCryptoAssets, + setRefreshBalance, + }); + + return { + isAssetLoading, + refreshBalance, + accounts, + updateAllBalances, + onRefresh, + }; +}; + +export default useAssetManager; diff --git a/src/hooks/useFetchCrytpoAccount.ts b/src/hooks/useFetchCrytpoAccount.ts new file mode 100644 index 000000000..4ebb396ee --- /dev/null +++ b/src/hooks/useFetchCrytpoAccount.ts @@ -0,0 +1,82 @@ +import { useCallback, useMemo } from 'react'; +import { capitalizeFirstLetter } from '../utils/strings'; +import { assetStorage, connect } from '../utils/StorageManager/setup'; +import useWalletStore from '../store/useWalletStore'; +import { tokenRegistry } from '../utils/tokenRegistry'; +import useUserStore from '../store/userStore'; +import Debug from 'debug'; +import { useFocusEffect } from '@react-navigation/native'; + +const debug = Debug('tonomy-id:hooks:useAssets'); + +const useFetchCrytpoAccount = ({ setAccounts, setAssetLoading }) => { + const { accountsInitialized, initializeWalletAccount } = useWalletStore(); + const { user } = useUserStore(); + + const tokens = useMemo(() => tokenRegistry, []); + + const fetchCryptoAssets = useCallback(async () => { + try { + // Ensure accounts are initialized + if (!accountsInitialized) await initializeWalletAccount(user); + await connect(); + + for (const { chain, token } of tokens) { + try { + const asset = await assetStorage.findAssetByName(token); + + debug( + `fetchCryptoAssets() fetching asset ${chain.getName()}: ${asset?.accountName}-${asset?.balance}` + ); + let account; + + if (asset) { + account = { + network: capitalizeFirstLetter(chain.getName()), + accountName: asset.accountName, + balance: asset.balance, + usdBalance: asset.usdBalance, + }; + } else { + account = { + network: capitalizeFirstLetter(chain.getName()), + accountName: null, + balance: '0', + usdBalance: 0, + }; + } + + setAccounts((prevAccounts) => { + // find index of the account in the array + const index = prevAccounts.findIndex((acc) => acc.network === account.network); + + if (index !== -1) { + // Update the existing asset + const updatedAccounts = [...prevAccounts]; + + updatedAccounts[index] = account; + return updatedAccounts; + } else { + // Add the new asset + return [...prevAccounts, account]; + } + }); + } catch (assetError) { + console.error(`Error fetching asset for ${chain.getName()}:`, assetError); + } + } + } catch (fetchError) { + console.error('Error fetching crypto assets:', fetchError); + } + }, [initializeWalletAccount, tokens, user, accountsInitialized, setAccounts]); + + useFocusEffect( + useCallback(() => { + fetchCryptoAssets().then(() => setAssetLoading(false)); + }, [fetchCryptoAssets, setAssetLoading]) + ); + + return { fetchCryptoAssets }; +}; + +export default useFetchCrytpoAccount; diff --git a/src/hooks/useUpdateBalances.ts b/src/hooks/useUpdateBalances.ts new file mode 100644 index 000000000..46fa005ac --- /dev/null +++ b/src/hooks/useUpdateBalances.ts @@ -0,0 +1,55 @@ +import { useCallback, useRef } from 'react'; +import { isNetworkError } from '../utils/errors'; +import useWalletStore from '../store/useWalletStore'; +import Debug from 'debug'; +import { useFocusEffect } from '@react-navigation/native'; + +const debug = Debug('tonomy-id:hooks:useUpdateBalances'); + +const useUpdateBalances = ({ fetchCryptoAssets, setRefreshBalance }) => { + const { updateBalance } = useWalletStore(); + + const isUpdatingBalances = useRef(false); + + const updateAllBalances = useCallback(async () => { + if (isUpdatingBalances.current) return; // Prevent re-entry if already running + isUpdatingBalances.current = true; + + try { + await updateBalance(); + await fetchCryptoAssets(); + } catch (error) { + if (isNetworkError(error)) { + debug('useAsset: Error updating account detail due to network error'); + } else { + console.error('useAsset: updateAllBalances() error', error); + } + } finally { + isUpdatingBalances.current = false; + } + }, [updateBalance, fetchCryptoAssets]); + + const onRefresh = useCallback(async () => { + setRefreshBalance(true); + + try { + await updateAllBalances(); + } finally { + setRefreshBalance(false); + } + }, [updateAllBalances, setRefreshBalance]); + + useFocusEffect( + useCallback(() => { + updateAllBalances(); + + const interval = setInterval(updateAllBalances, 8000); + + return () => clearInterval(interval); + }, [updateAllBalances]) + ); + + return { updateAllBalances, onRefresh }; +}; + +export default useUpdateBalances; diff --git a/src/store/useWalletStore.ts b/src/store/useWalletStore.ts index 870d95b35..b87801f44 100644 --- a/src/store/useWalletStore.ts +++ b/src/store/useWalletStore.ts @@ -183,6 +183,7 @@ const useWalletStore = create((set, get) => ({ const { token } = await getTokenEntryByChain(chain); const balance = await token.getBalance(account); + debug(`updateBalance() ${chain.getName()} balance:`, await balance.toString(4)); await assetStorage.updateAccountBalance(balance); } catch (error) { console.error(`updateBalance() Error fetching balance ${chain.getName()}:`, error);