diff --git a/android/app/build.gradle b/android/app/build.gradle index 36acb3d4..f09a69fd 100755 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -5,7 +5,7 @@ import com.android.build.OutputFile def versionMajor = 1 def versionMinor = 0 -def versionRevision = 22 +def versionRevision = 23 def versionBuild = 0 def keystorePropertiesFile = rootProject.file("keystore.properties"); diff --git a/env/main.android.json b/env/main.android.json index 2540c6c6..e9f671c7 100644 --- a/env/main.android.json +++ b/env/main.android.json @@ -1,5 +1,5 @@ { - "APP_VERSION": "1.0.22", + "APP_VERSION": "1.0.23", "ELECTRUM_PROTOCOL_CHANGE": 1.4, "KEY_DERIVATION_VERSION": 1, diff --git a/env/main.ios.json b/env/main.ios.json index 4c9e72e1..cf93259c 100644 --- a/env/main.ios.json +++ b/env/main.ios.json @@ -1,5 +1,5 @@ { - "APP_VERSION": "1.0.22", + "APP_VERSION": "1.0.23", "ELECTRUM_PROTOCOL_CHANGE": 1.4, "KEY_DERIVATION_VERSION": 1, diff --git a/ios/assets/env/main.json b/ios/assets/env/main.json index 4c9e72e1..cf93259c 100644 --- a/ios/assets/env/main.json +++ b/ios/assets/env/main.json @@ -1,5 +1,5 @@ { - "APP_VERSION": "1.0.22", + "APP_VERSION": "1.0.23", "ELECTRUM_PROTOCOL_CHANGE": 1.4, "KEY_DERIVATION_VERSION": 1, diff --git a/ios/verusMobile.xcodeproj/project.pbxproj b/ios/verusMobile.xcodeproj/project.pbxproj index eb32e3be..622298c7 100644 --- a/ios/verusMobile.xcodeproj/project.pbxproj +++ b/ios/verusMobile.xcodeproj/project.pbxproj @@ -745,7 +745,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-webview\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/rn-fetch-blob\"", ); - MARKETING_VERSION = 1.0.22; + MARKETING_VERSION = 1.0.23; PRODUCT_BUNDLE_IDENTIFIER = org.reactjs.native.verusmobile; PRODUCT_NAME = verusmobile; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -817,7 +817,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-webview\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/rn-fetch-blob\"", ); - MARKETING_VERSION = 1.0.22; + MARKETING_VERSION = 1.0.23; PRODUCT_BUNDLE_IDENTIFIER = org.reactjs.native.verusmobile; PRODUCT_NAME = verusmobile; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/ios/verusMobile/Info.plist b/ios/verusMobile/Info.plist index db904dcb..2a642ed7 100755 --- a/ios/verusMobile/Info.plist +++ b/ios/verusMobile/Info.plist @@ -48,16 +48,36 @@ - NSCameraUsageDescription - Verus Mobile needs access to the camera to scan QR codes, and to allow you to add images to your locally encrypted personal profile - NSFaceIDUsageDescription - Enabling Face ID allows you quick and secure access to your account. - NSMicrophoneUsageDescription - Verus Mobile needs access to the camera to scan QR codes - NSPhotoLibraryAddUsageDescription - The user can save QR code invoices to their photo library. - NSPhotoLibraryUsageDescription - The user can use the photo library to add images to their locally encrypted personal profile + NSAppleMusicUsageDescription + The app does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code. + NSBluetoothAlwaysUsageDescription + The app does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code. + NSBluetoothPeripheralUsageDescription + The app does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code. + NSCalendarsUsageDescription + The app does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code. + NSCameraUsageDescription + Verus Mobile needs access to the camera to scan QR codes, and to allow you to add images to your locally encrypted personal profile + NSContactsUsageDescription + The app does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code. + NSFaceIDUsageDescription + Enabling Face ID allows you quick and secure access to your account. + NSLocationAlwaysUsageDescription + The app does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code. + NSLocationWhenInUseUsageDescription + The app does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code. + NSMicrophoneUsageDescription + Verus Mobile needs access to the camera to scan QR codes + NSMotionUsageDescription + The app does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code. + NSPhotoLibraryAddUsageDescription + The user can save QR code invoices to their photo library. + NSPhotoLibraryUsageDescription + The user can use the photo library to add images to their locally encrypted personal profile + NSSiriUsageDescription + The app does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code. + NSSpeechRecognitionUsageDescription + The app does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code. UIAppFonts AntDesign.ttf diff --git a/package.json b/package.json index 2a0057c0..f7905d11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "verusmobile", - "version": "1.0.22", + "version": "1.0.23", "private": true, "scripts": { "postinstall": "./node_modules/.bin/rn-nodeify --hack --install --yarn && npx jetify", diff --git a/src/components/BarcodeReader/BarcodeReader.js b/src/components/BarcodeReader/BarcodeReader.js index 3c49d5de..d0c84980 100644 --- a/src/components/BarcodeReader/BarcodeReader.js +++ b/src/components/BarcodeReader/BarcodeReader.js @@ -108,7 +108,7 @@ const BarcodeReader = props => { }} device={device} codeScanner={codeScanner} - isActive={appStateVisible === 'active'} + isActive={appStateVisible === 'active' && !props.cameraDisabled} {...cameraProps} /> { + const [preventExit, setPreventExit] = useState(false); + const [modalHeight, setModalHeight] = useState(600); // Adjust as needed + + const activeCoinsForUser = useSelector(state => state.coins.activeCoinsForUser); + const [modalTitle, setModalTitle] = useState("Title"); + + useEffect(() => { + // Handle side effects here if necessary + }, []); + + const cancel = () => { + if (!preventExit) { + setVisible(false); + if (onClose) { + onClose(); + } + } + }; + + const showHelpModal = () => { + // Implement your help modal logic here + }; + + return ( + + + + {loading ? : + + ( + + + + {modalTitle} + + + ), + headerStyle: { + height: 52, + }, + }} + > + + {() => ( + , + }} + > + { + e.preventDefault(); + }, + }} + > + {tabProps => ( + + )} + + { + e.preventDefault(); + }, + }} + > + {tabProps => ( + + )} + + {mode === CONVERT_CARD_MODAL_MODES.RECEIVE && ( + { + e.preventDefault(); + }, + }} + > + {tabProps => ( + + )} + + )} + { + e.preventDefault(); + }, + }} + > + {tabProps => ( + + )} + + + )} + + + + } + + + + ); +}; + +export default ConvertCardModal; \ No newline at end of file diff --git a/src/components/SearchableList.js b/src/components/SearchableList.js new file mode 100644 index 00000000..bfc7535d --- /dev/null +++ b/src/components/SearchableList.js @@ -0,0 +1,115 @@ +import React, { useMemo, useState } from 'react'; +import { View, TextInput, FlatList } from 'react-native'; +import { List, Text } from 'react-native-paper'; +import { RenderCircleCoinLogo } from '../utils/CoinData/Graphics'; +import Colors from '../globals/colors'; +import { useNavigation } from '@react-navigation/native'; + +const SearchableList = (props) => { + const [searchQuery, setSearchQuery] = useState(''); + const items = props.items ? props.items : []; + + const navigation = useNavigation(); + + // Filter the data based on the search query + const filteredData = useMemo(() => { + return items.filter(item => + ( + item.title.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description.toLowerCase().includes(searchQuery.toLowerCase()) + ) + ); + }, [searchQuery, items]); + + const handleSelect = (key) => { + if (props.onSelect) props.onSelect(key); + if (props.nextScreen) navigation.navigate(props.nextScreen) + } + + // Render each item in the FlatList + const renderItem = ({ item }) => ( + handleSelect(item.key)} + left={() => ( + + {RenderCircleCoinLogo(item.logo)} + + )} + right={props => + + {item.rightTitle && ( + + {item.rightTitle} + + )} + {item.rightDescription && ( + + {item.rightDescription} + + )} + + } + /> + ); + + return ( + + setSearchQuery(text)} + /> + item.key} + renderItem={renderItem} + ListEmptyComponent={ + + No items found + + } + /> + + ); +}; + +export default SearchableList; \ No newline at end of file diff --git a/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendForm/ConvertOrCrossChainSendForm.js b/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendForm/ConvertOrCrossChainSendForm.js index de4b6ac9..e2f408d7 100644 --- a/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendForm/ConvertOrCrossChainSendForm.js +++ b/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendForm/ConvertOrCrossChainSendForm.js @@ -4,7 +4,7 @@ import { Alert, View, TouchableWithoutFeedback, Keyboard, FlatList, Animated, To import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { TextInput, Button, Divider, Checkbox, List, Text, IconButton } from "react-native-paper"; import { createAlert } from "../../../../actions/actions/alert/dispatchers/alert"; -import { API_SEND, DLIGHT_PRIVATE, ERC20, ETH } from "../../../../utils/constants/intervalConstants"; +import { API_SEND, DLIGHT_PRIVATE, ERC20, ETH, VRPC } from "../../../../utils/constants/intervalConstants"; import { SEND_MODAL_ADVANCED_FORM, SEND_MODAL_AMOUNT_FIELD, @@ -57,6 +57,7 @@ import { useObjectSelector } from "../../../../hooks/useObjectSelector"; const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFormData, navigation }) => { const { height } = Dimensions.get('window'); const sendModal = useObjectSelector(state => state.sendModal); + const allSubWallets = useObjectSelector(state => state.coinMenus.allSubWallets); const activeUser = useObjectSelector(state => state.authentication.activeAccount); const addresses = useObjectSelector(state => selectAddresses(state)); const activeAccount = useObjectSelector(state => state.authentication.activeAccount); @@ -76,9 +77,17 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor [SEND_MODAL_EXPORTTO_FIELD]: "Destination network", [SEND_MODAL_VIA_FIELD]: "Convert via", [SEND_MODAL_CONVERTTO_FIELD]: sendModal.data[SEND_MODAL_IS_PRECONVERT] ? "Preconvert to" : "Convert to", - [SEND_MODAL_MAPPING_FIELD]: "Receive as" + [SEND_MODAL_MAPPING_FIELD]: "Receive as", + [SEND_MODAL_TO_ADDRESS_FIELD]: "Recipient address" } + const CONVERSION_PATH_FIELDS = [ + SEND_MODAL_EXPORTTO_FIELD, + SEND_MODAL_VIA_FIELD, + SEND_MODAL_CONVERTTO_FIELD, + SEND_MODAL_MAPPING_FIELD + ]; + const [searchMode, setSearchMode] = useState(false); const [selectedField, setSelectedField] = useState(""); @@ -529,6 +538,80 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor } }; + const processSelfSuggestionPaths = () => { + const addresses = []; + const seen = new Set(); + + if (activeAccount) { + if (allSubWallets[coinsList.VRSC.id]) { + const vrscKeys = activeAccount.keys[coinsList.VRSC.id]; + + for (const channelId in vrscKeys) { + const [channelName, addr, network] = channelId.split('.'); + + if (channelName === VRPC && !seen.has(addr)) { + const walletId = `SUBWALLET_${channelId}`; + + if (allSubWallets[coinsList.VRSC.id]) { + const wallet = allSubWallets[coinsList.VRSC.id].find(x => x.id === walletId); + + if (wallet) { + seen.add(addr); + addresses.push({ + title: wallet.name, + logoid: coinsList.VRSC.id, + key: addr, + description: addr, + values: { + [SEND_MODAL_TO_ADDRESS_FIELD]: addr + }, + right: "", + keywords: [ + addr, + wallet.name + ], + }) + } + } + } + } + } + + for (const coinId in activeAccount.keys) { + if (activeAccount.keys[coinId] && (activeAccount.keys[coinId][ETH] || activeAccount.keys[coinId][ERC20])) { + const ethAddresses = activeAccount.keys[coinId][ETH] ? + activeAccount.keys[coinId][ETH].addresses : activeAccount.keys[coinId][ERC20].addresses; + + if (ethAddresses && ethAddresses.length > 0) { + const addr = ethAddresses[0]; + const addrTitle = addr.substring(0, 8) + '...' + addr.substring(addr.length - 8); + + if (!seen.has(addr)) { + seen.add(addr); + addresses.push({ + title: addrTitle, + logoid: coinsList.ETH.id, + key: addr, + description: addr, + values: { + [SEND_MODAL_TO_ADDRESS_FIELD]: addr + }, + right: "", + keywords: [ + addr + ] + }) + } + + break; + } + } + } + } + + return addresses; + }; + const fetchSuggestionsBase = async (field) => { if (loadingSuggestions) return; let newSuggestionsBase = [] @@ -547,25 +630,27 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor // mapping: boolean; // bounceback: boolean; // }>} - - const paths = conversionPaths - ? conversionPaths - : await getConversionPaths( - sendModal.coinObj, - sendModal.subWallet.api_channels[API_SEND], - { - src: sendModal.coinObj.currency_id, - }, - ); let flatPaths = [] + + if (CONVERSION_PATH_FIELDS.includes(field)) { + const paths = conversionPaths + ? conversionPaths + : await getConversionPaths( + sendModal.coinObj, + sendModal.subWallet.api_channels[API_SEND], + { + src: sendModal.coinObj.currency_id, + }, + ); - for (const destinationid in paths) { - flatPaths = flatPaths.concat(paths[destinationid]) + for (const destinationid in paths) { + flatPaths = flatPaths.concat(paths[destinationid]) + } + + setConversionPaths(paths) } - setConversionPaths(paths) - switch (field) { case SEND_MODAL_CONVERTTO_FIELD: newSuggestionsBase = await processConverttoSuggestionPaths(flatPaths, sendModal.coinObj); @@ -582,6 +667,9 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor case SEND_MODAL_MAPPING_FIELD: newSuggestionsBase = await processMappingSuggestionPaths(flatPaths, sendModal.coinObj); setSuggestionBase(newSuggestionsBase); + case SEND_MODAL_TO_ADDRESS_FIELD: + newSuggestionsBase = processSelfSuggestionPaths(); + setSuggestionBase(newSuggestionsBase); default: setSuggestionBase(newSuggestionsBase); break; @@ -817,7 +905,7 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor if (addr.endsWith("@")) { const identityRes = await getIdentity(coinObj, activeAccount, channel, addr); - if (identityRes.error) throw new Error("Failed to fetch " + addr); + if (identityRes.error) throw new Error(`Failed to get information about ${addr}. Try using the i-address of this VerusID.`); keyhash = identityRes.result.identity.identityaddress; } else keyhash = addr; @@ -1018,7 +1106,8 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor } right={() => selectedField !== SEND_MODAL_EXPORTTO_FIELD && - selectedField !== SEND_MODAL_MAPPING_FIELD ? ( + selectedField !== SEND_MODAL_MAPPING_FIELD && + selectedField !== SEND_MODAL_TO_ADDRESS_FIELD ? ( updateSendFormData(SEND_MODAL_TO_ADDRESS_FIELD, text) } - onSelfPress={() => setAddressSelf()} + onSelfPress={() => handleFieldFocus(SEND_MODAL_TO_ADDRESS_FIELD)} amountValue={sendModal.data[SEND_MODAL_AMOUNT_FIELD]} onAmountChange={text => updateSendFormData(SEND_MODAL_AMOUNT_FIELD, text) diff --git a/src/containers/Coin/DynamicHeader.js b/src/containers/Coin/DynamicHeader.js index cf757ccf..bab4291d 100644 --- a/src/containers/Coin/DynamicHeader.js +++ b/src/containers/Coin/DynamicHeader.js @@ -38,6 +38,7 @@ import { GestureDetector, Gesture, Directions } from 'react-native-gesture-handl import { useSelector, useDispatch } from 'react-redux'; import Styles from '../../styles'; import { useObjectSelector } from '../../hooks/useObjectSelector'; +import { coinsList } from '../../utils/CoinData/CoinsList'; const DynamicHeader = ({ switchTab }) => { const dispatch = useDispatch(); @@ -144,7 +145,7 @@ const DynamicHeader = ({ switchTab }) => { let mappedCoin = null; - if (activeCoin.mapped_to != null) { + if (activeCoin.mapped_to != null && activeCoin.id !== coinsList.VRSC.id) { try { mappedCoin = CoinDirectory.getBasicCoinObj(activeCoin.mapped_to); } catch (e) { diff --git a/src/containers/Coin/SendCoin/SendCoin.js b/src/containers/Coin/SendCoin/SendCoin.js index ab7e923a..b0b46b5c 100644 --- a/src/containers/Coin/SendCoin/SendCoin.js +++ b/src/containers/Coin/SendCoin/SendCoin.js @@ -43,7 +43,9 @@ const SendCoin = ({ navigation }) => { const generalWalletSettings = useObjectSelector( state => state.settings.generalWalletSettings, ); + const dispatch = useDispatch() + const [cameraDisabled, setCameraDisabled] = useState(generalWalletSettings.enableSendCoinCameraToggle === true); const CONVERT_OR_CROSS_CHAIN_OPTIONS = CONVERSION_DISABLED ? [ { @@ -207,6 +209,10 @@ const SendCoin = ({ navigation }) => { openConvertOrCrossChainSendModal(activeCoin, subWallet, option.data) } + const toggleCameraDisabled = () => { + setCameraDisabled(!cameraDisabled); + } + const openConvertOrCrossChainModal = () => { const { ackedCurrencyDisclaimer } = generalWalletSettings; @@ -261,6 +267,7 @@ By proceeding, you confirm that you've read, understood, and agreed to this. Ens openConvertOrCrossChainModal() } + > + {CONVERSION_DISABLED ? "Send cross-chain" : "Convert or cross-chain"} + + )} + {generalWalletSettings.enableSendCoinCameraToggle && ( + )} diff --git a/src/containers/Convert/Convert.js b/src/containers/Convert/Convert.js index 3f79f9d6..350092fb 100644 --- a/src/containers/Convert/Convert.js +++ b/src/containers/Convert/Convert.js @@ -4,11 +4,839 @@ import Styles from "../../styles"; import { Button } from "react-native-paper"; import Colors from "../../globals/colors"; import ConvertCard from "./ConvertCard/ConvertCard"; +import { useSelector } from "react-redux"; +import { extractLedgerData } from "../../utils/ledger/extractLedgerData"; +import { API_GET_BALANCES, API_SEND, ERC20, ETH, GENERAL, IS_CONVERTABLE_WITH_VRSC_ETH_BRIDGE, IS_PBAAS_ROOT, VRPC } from "../../utils/constants/intervalConstants"; +import { IS_PBAAS_CHAIN, USD } from "../../utils/constants/currencies"; +import BigNumber from 'bignumber.js'; +import ConvertCardModal from "../../components/ConvertCardModal/ConvertCardModal"; +import { CONVERT_CARD_MODAL_MODES } from "../../utils/constants/convert"; +import { normalizeNum } from "../../utils/normalizeNum"; +import { formatCurrency } from "react-native-format-currency"; +import { useObjectSelector } from "../../hooks/useObjectSelector"; +import { CoinDirectory } from "../../utils/CoinData/CoinDirectory"; +import { closeLoadingModal, openLoadingModal } from "../../actions/actionDispatchers"; +import { createAlert } from "../../actions/actions/alert/dispatchers/alert"; +import { getConversionPaths } from "../../utils/api/routers/getConversionPaths"; +import { VETH } from "../../utils/constants/web3Constants"; +import { coinsList } from "../../utils/CoinData/CoinsList"; const Convert = (props) => { - useEffect(() => { + const displayCurrency = useSelector(state => state.settings.generalWalletSettings.displayCurrency || USD); + + const allSubWallets = useObjectSelector(state => state.coinMenus.allSubWallets); + const activeCoinsForUser = useObjectSelector(state => state.coins.activeCoinsForUser); + const rates = useObjectSelector(state => state.ledger.rates); + const balances = useObjectSelector(state => extractLedgerData(state, 'balances', API_GET_BALANCES)); + const activeAccount = useObjectSelector(state => state.authentication.activeAccount); + + const [loading, setLoading] = useState(false); + + const [totalBalances, setTotalBalances] = useState({}); + + const [convertCardModalMode, setConvertCardModalMode] = useState(CONVERT_CARD_MODAL_MODES.SEND); + const [convertCardModalVisible, setConvertCardModalVisible] = useState(false); + + // These key value maps that contain the data that the form needs to execute on a user's selection + const [sourceCurrencyMap, setSourceCurrencyMap] = useState({}); + const [destCurrencyMap, setDestCurrencyMap] = useState({}); + + // These are in display format, with props: title, description, rightTitle, rightDescription, logo, and key + const [sourceCurrencyOptionsList, setSourceCurrencyOptionsList] = useState([]); + const [destCurrencyOptionsList, setDestCurrencyOptionsList] = useState([]); + + const [sourceNetworkOptionsList, setSourceNetworkOptionsList] = useState([]); + const [destNetworkOptionsList, setDestNetworkOptionsList] = useState([]); + + const [destConverterOptionsList, setDestConverterOptionsList] = useState([]); + + const [sourceWalletOptionsList, setSourceWalletOptionsList] = useState([]); + const [destAddressOptionsList, setDestAddressOptionsList] = useState([]); + + const [selectedSourceCurrency, setSelectedSourceCurrency] = useState(null); + const [selectedSourceCoinObj, setSelectedSourceCoinObj] = useState(null); + const [selectedSourceNetwork, setSelectedSourceNetwork] = useState(null); + const [selectedSourceWallet, setSelectedSourceWallet] = useState(null); + + const [selectedDestCurrencyId, setSelectedDestCurrencyId] = useState(null); + const [selectedDestCoinObj, setSelectedDestCoinObj] = useState(null); + const [selectedDestNetworkId, setSelectedDestNetworkId] = useState(null); + + const [selectedConverterId, setSelectedConverterId] = useState(null); + const [selectedDestAddress, setSelectedDestAddress] = useState(null); + + const [sendAmount, setSendAmount] = useState(null); + const [sourceBalance, setSourceBalance] = useState(null); + + const [conversionPaths, setConversionPaths] = useState(null); + + const getTotalBalances = () => { + let coinBalances = {}; + + activeCoinsForUser.map(coinObj => { + coinBalances[coinObj.id] = { + crypto: BigNumber('0'), + rate: null + }; + + allSubWallets[coinObj.id].map(wallet => { + if ( + balances[coinObj.id] != null && + balances[coinObj.id][wallet.id] != null + ) { + const cryptoBalance = coinBalances[coinObj.id].crypto.plus( + balances[coinObj.id] && + balances[coinObj.id][wallet.id] && + balances[coinObj.id][wallet.id].total != null + ? BigNumber(balances[coinObj.id][wallet.id].total) + : null, + ); + + const uniRate = rates[GENERAL] && rates[GENERAL][coinObj.id] ? rates[GENERAL][coinObj.id][displayCurrency] : null + + coinBalances[coinObj.id] = { + crypto: cryptoBalance, + rate: uniRate != null ? BigNumber(uniRate) : null + } + } + }); + }); + + return coinBalances; + }; + + const getSourceCurrencyMap = () => { + const currencyMap = {}; + const usedCoins = []; + + for (const coinObj of activeCoinsForUser.sort(x => (x.mapped_to ? -1 : 1))) { + if (!usedCoins.includes(coinObj.id) && (coinObj.proto === 'vrsc' || coinObj.tags.includes(IS_CONVERTABLE_WITH_VRSC_ETH_BRIDGE))) { + currencyMap[coinObj.currency_id] = [coinObj]; + + if (coinObj.mapped_to != null && coinObj.tags.includes(IS_CONVERTABLE_WITH_VRSC_ETH_BRIDGE)) { + const mappedCoinObj = activeCoinsForUser.find(x => x.id === coinObj.mapped_to); + + if (mappedCoinObj != null) { + usedCoins.push(mappedCoinObj.id) + currencyMap[coinObj.currency_id].push(mappedCoinObj); + } + } + } + } + + return currencyMap; + }; + + const formatFiatValue = (n) => { + const rawFiatDisplayValue = normalizeNum( + n, + 2, + )[3]; + + const [valueFormattedWithSymbol] = formatCurrency({amount: rawFiatDisplayValue, code: displayCurrency}); - }, []) + return valueFormattedWithSymbol; + } + + const getRightText = (coinId, walletIds) => { + let title = '-'; + let description = '-'; + + if (balances[coinId] != null) { + const uniRate = rates[GENERAL] && rates[GENERAL][coinId] ? BigNumber(rates[GENERAL][coinId][displayCurrency]) : null; + + let totalCryptoBalance = BigNumber(0); + + for (const walletId of walletIds) { + const cryptoBalance = balances[coinId][walletId]; + + if (cryptoBalance != null) { + totalCryptoBalance = totalCryptoBalance.plus(BigNumber(cryptoBalance.confirmed)); + } + } + + description = `${Number(totalCryptoBalance.toString())}`; + + if (uniRate != null) { + title = formatFiatValue(Number((totalCryptoBalance.multipliedBy(uniRate)).toString())); + } + } + + return { title, description }; + } + + const getSourceCurrencyOptionsList = () => { + const currencies = []; + + for (const coinObjs of Object.values(sourceCurrencyMap)) { + const rootCoinObj = coinObjs[0]; + const mappedCoinObj = coinObjs.length > 1 ? coinObjs[1] : null; + + const titleCoinObj = mappedCoinObj && mappedCoinObj.display_name.length < rootCoinObj.display_name.length ? + mappedCoinObj + : + rootCoinObj; + + const title = titleCoinObj.display_name; + const description = coinObjs.map(x => x.display_ticker).join(' / '); + + let rightTitle = '-'; + let rightDescription = '-'; + + let totalCryptoBalance = BigNumber(0); + let fiatRate; + + if (totalBalances[rootCoinObj.id]) { + if (totalBalances[rootCoinObj.id].crypto) { + totalCryptoBalance = totalCryptoBalance.plus(totalBalances[rootCoinObj.id].crypto); + } + + if (totalBalances[rootCoinObj.id].rate) fiatRate = totalBalances[rootCoinObj.id].rate; + } + + if (mappedCoinObj && totalBalances[mappedCoinObj.id]) { + if (totalBalances[mappedCoinObj.id].crypto) { + totalCryptoBalance = totalCryptoBalance.plus(totalBalances[mappedCoinObj.id].crypto); + } + + if (!fiatRate && totalBalances[mappedCoinObj.id].rate) fiatRate = totalBalances[mappedCoinObj.id].rate; + } + + const totalFiatBalance = fiatRate != null ? totalCryptoBalance.multipliedBy(fiatRate) : null; + + if (totalFiatBalance != null) { + rightTitle = formatFiatValue(Number(totalFiatBalance.toString())); + } + + rightDescription = `${Number(totalCryptoBalance.toString())}`; + + currencies.push({ + title, + description, + rightDescription, + rightTitle, + key: rootCoinObj.currency_id, + logo: rootCoinObj.id + }); + } + + return currencies; + }; + + const getDestCurrencyOptionsList = () => { + const currencies = []; + + if (conversionPaths) { + for (const destinationCurrencyId in conversionPaths) { + let title = "-"; + let logo; + let aliases = []; + let rightTitle = formatFiatValue(0); + let rightDescription = "-"; + + if (conversionPaths[destinationCurrencyId].length > 0) { + try { + const destCurrencyObj = CoinDirectory.findSimpleCoinObj(destinationCurrencyId); + aliases.push(destCurrencyObj.display_ticker); + + let totalCryptoBalance = BigNumber(0); + let fiatRate; + + if (totalBalances[destCurrencyObj.id]) { + if (totalBalances[destCurrencyObj.id].crypto) { + totalCryptoBalance = totalCryptoBalance.plus(totalBalances[destCurrencyObj.id].crypto); + } + + if (totalBalances[destCurrencyObj.id].rate) fiatRate = totalBalances[destCurrencyObj.id].rate; + } + + if (destCurrencyObj.mapped_to && conversionPaths[destinationCurrencyId].some(x => { + return x.ethdest || (x.exportto && x.exportto.fullyqualifiedname === VETH) + })) { + const mappedCoinObj = CoinDirectory.findSimpleCoinObj(destCurrencyObj.mapped_to); + + if (totalBalances[mappedCoinObj.id]) { + if (totalBalances[mappedCoinObj.id].crypto) { + totalCryptoBalance = totalCryptoBalance.plus(totalBalances[mappedCoinObj.id].crypto); + } + + if (!fiatRate && totalBalances[mappedCoinObj.id].rate) fiatRate = totalBalances[mappedCoinObj.id].rate; + } + + const titleCoinObj = mappedCoinObj && mappedCoinObj.display_name.length < destCurrencyObj.display_name.length ? + mappedCoinObj + : + destCurrencyObj; + + title = titleCoinObj.display_name; + + aliases.push(mappedCoinObj.display_ticker); + + logo = titleCoinObj.id; + } else { + title = destCurrencyObj.display_name; + logo = destCurrencyObj.id; + } + + const totalFiatBalance = fiatRate != null ? totalCryptoBalance.multipliedBy(fiatRate) : null; + + if (totalFiatBalance != null) { + rightTitle = formatFiatValue(Number(totalFiatBalance.toString())); + } + + rightDescription = `${Number(totalCryptoBalance.toString())}`; + + currencies.push({ + title, + description: aliases.join(' / '), + rightDescription, + rightTitle, + key: destinationCurrencyId, + logo + }); + } catch(e) { + title = conversionPaths[destinationCurrencyId][0].ethdest ? + conversionPaths[destinationCurrencyId][0].destination.mapto.fullyqualifiedname + : + conversionPaths[destinationCurrencyId][0].destination.fullyqualifiedname; + + currencies.push({ + title, + description: title, + rightDescription: "0", + rightTitle, + key: destinationCurrencyId, + logo: conversionPaths[destinationCurrencyId][0].ethdest ? + conversionPaths[destinationCurrencyId][0].destination.mapto.currencyid + : + destinationCurrencyId + }); + } + } + } + } + + return currencies; + }; + + const getSourceNetworkOptionsList = () => { + const networks = []; + + if (selectedSourceCurrency) { + for (const alias of selectedSourceCurrency) { + if (alias.proto === 'vrsc') { + if (allSubWallets[alias.id]) { + const networkWallets = {}; + + for (const subWallet of allSubWallets[alias.id]) { + const [channelName, addr, network] = subWallet.channel.split('.'); + + if (channelName === VRPC) { + if (!networkWallets[network]) networkWallets[network] = [subWallet.id]; + else networkWallets[network].push(subWallet.id); + } + } + + for (const network in networkWallets) { + try { + const networkObj = CoinDirectory.getBasicCoinObj(network); + + if (networkObj) { + const rightText = getRightText(alias.id, networkWallets[network]); + + networks.push({ + title: `From ${networkObj.display_name} network`, + description: `as ${alias.display_ticker}`, + logo: networkObj.id, + key: networkObj.id, + rightTitle: rightText.title, + rightDescription: rightText.description + }); + } + } catch(e) { + console.warn(e) + } + } + } + } else if (alias.proto === 'eth' || alias.proto === 'erc20') { + const rightText = getRightText(alias.id, ['MAIN_WALLET']); + + networks.push({ + title: 'From Ethereum network', + description: `as ${alias.display_ticker}`, + logo: alias.testnet ? 'GETH' : 'ETH', + key: alias.testnet ? 'GETH' : 'ETH', + rightTitle: rightText.title, + rightDescription: rightText.description + }); + } + } + } + + return networks; + }; + + const getDestNetworkOptionsList = () => { + const networks = []; + + if (selectedSourceNetwork && selectedSourceCoinObj && selectedDestCurrencyId && conversionPaths[selectedDestCurrencyId]) { + const conversionOptions = conversionPaths[selectedDestCurrencyId]; + + const exportDests = {}; + let hasLocalConversion = false; + + for (const option of conversionOptions) { + let networkName; + let coinAlias; + + let logo; + let key; + let rightTitle = formatFiatValue(0); + let rightDescription = "0"; + + function setRightText (networkId) { + if (selectedDestCoinObj && allSubWallets[selectedDestCoinObj.id]) { + const walletIds = []; + + for (const wallet of allSubWallets[selectedDestCoinObj.id]) { + const [channelName, addr, network] = wallet.channel.split('.'); + if (network === networkId) { + walletIds.push(wallet.id); + } + } + + const rightText = getRightText(selectedDestCoinObj.id, walletIds); + rightTitle = rightText.title; + rightDescription = rightText.description; + } + } + + function pushNetwork() { + networks.push({ + title: `On ${networkName}`, + description: `as ${coinAlias}`, + logo, + key, + rightTitle, + rightDescription + }) + } + + if (option.exportto && !exportDests[option.exportto.currencyid]) { + exportDests[option.exportto.currencyid] = option.exportto; + let exportToEth = false; + + try { + const exportNetworkObj = CoinDirectory.getBasicCoinObj(option.exportto.currencyid); + + if (exportNetworkObj.mapped_to && !((exportNetworkObj.pbaas_options & IS_PBAAS_CHAIN) === IS_PBAAS_CHAIN)) { + const mappedObj = CoinDirectory.getBasicCoinObj(exportNetworkObj.mapped_to); + + networkName = mappedObj.display_name; + logo = mappedObj.id + exportToEth = true; + } else { + networkName = exportNetworkObj.display_name; + logo = exportNetworkObj.id + } + } catch(e) { + networkName = option.exportto.fullyqualifiedname; + logo = 'VRSC' + } + + if (selectedDestCoinObj) { + if (selectedDestCoinObj.mapped_to && exportToEth) { + const mappedObj = CoinDirectory.getBasicCoinObj(selectedDestCoinObj.mapped_to); + + coinAlias = mappedObj.display_ticker; + } else { + coinAlias = selectedDestCoinObj.display_ticker; + } + } else { + coinAlias = option.ethdest ? option.destination.name : option.destination.fullyqualifiedname; + } + + key = option.exportto.currencyid; + + setRightText(option.exportto.currencyid ? option.exportto.currencyid : option.exportto.currencyid); + pushNetwork(); + } else if (!hasLocalConversion) { + hasLocalConversion = true; + + networkName = selectedSourceNetwork.display_name; + + if (option.ethdest) { + if (selectedDestCoinObj && selectedDestCoinObj.mapped_to) { + try { + const mappedObj = CoinDirectory.getBasicCoinObj(selectedDestCoinObj.mapped_to); + coinAlias = mappedObj.display_ticker; + } catch(e) { + coinAlias = option.destination.name; + } + } else { + coinAlias = option.destination.name; + } + } else if (selectedDestCoinObj) { + coinAlias = selectedDestCoinObj.display_ticker; + } else { + coinAlias = option.destination.fullyqualifiedname; + } + + logo = selectedSourceNetwork.id; + key = selectedSourceNetwork.currency_id; + + setRightText(selectedSourceNetwork.currencyid); + pushNetwork(); + } + } + } + + return networks; + }; + + const getDestConverterOptionsList = () => { + const converters = []; + + function pushConverter(title, ticker, logo, key, price) { + converters.push({ + title: title, + description: '', + logo, + key, + rightTitle: price, + rightDescription: `Est. ${ticker} per ${selectedSourceCoinObj.display_ticker}` + }) + } + + if (selectedSourceNetwork && selectedDestCurrencyId && conversionPaths[selectedDestCurrencyId] && selectedDestNetworkId) { + for (let i = 0; i < conversionPaths[selectedDestCurrencyId].length; i++) { + const path = conversionPaths[selectedDestCurrencyId][i]; + + if ((selectedDestNetworkId === selectedSourceNetwork.currency_id && path.exportto == null) || + (path.exportto && (path.exportto.currencyid === selectedDestNetworkId))) { + let title; + let logo; + const ticker = selectedDestCoinObj ? + selectedDestCoinObj.display_ticker : path.ethdest ? + path.mapto.destination.fullyqualifiedname : path.destination.fullyqualifiedname; + + if (path.via) { + try { + const converterObj = CoinDirectory.getBasicCoinObj(path.via.currencyid); + + title = `Via ${converterObj.display_name}`; + logo = converterObj.id; + } catch(e) { + title = `Via ${path.via.fullyqualifiedname}`; + logo = path.via.currencyid; + } + } else { + title = "Direct" + logo = path.ethdest ? path.destination.mapto.currencyid : path.destination.currencyid; + } + + pushConverter(title, ticker, logo, `${selectedDestCurrencyId}:${i}`, normalizeNum(path.price, 8)[3]); + } + } + } + + return converters; + }; + + const getSourceWalletOptionsList = () => { + const wallets = []; + + if (selectedSourceCurrency && selectedSourceNetwork) { + if (selectedSourceCoinObj && allSubWallets[selectedSourceCoinObj.id]) { + for (const wallet of allSubWallets[selectedSourceCoinObj.id]) { + const [channelName, addr, network] = wallet.channel.split('.'); + + if ( + channelName === ERC20 || + channelName === ETH || + (channelName === VRPC && selectedSourceNetwork.currency_id === network) + ) { + const rightText = getRightText(selectedSourceCoinObj.id, [wallet.id]); + + wallets.push({ + title: wallet.name, + description: `as ${selectedSourceCoinObj.display_ticker}`, + logo: selectedSourceNetwork.id, + key: wallet.id, + rightTitle: rightText.title, + rightDescription: rightText.description + }) + } + } + } + } + + return wallets; + }; + + const getDestAddressOptionsList = () => { + const addresses = []; + + if (activeAccount && allSubWallets[coinsList.VRSC.id] && conversionPaths && selectedConverterId && selectedDestNetworkId) { + const [currencyKey, converterIndex] = selectedConverterId.split(':'); + + if (conversionPaths[currencyKey] && conversionPaths[currencyKey][converterIndex]) { + const converter = conversionPaths[currencyKey][converterIndex]; + const currencyAlias = selectedDestCoinObj ? + selectedDestCoinObj.display_ticker : converter.ethdest ? + converter.destination.name : converter.destination.fullyqualifiedname; + const allowEthAddrs = converter.ethdest || (converter.exportto && converter.exportto.fullyqualifiedname === VETH); + + if (!converter.ethdest) { + const vrscKeys = activeAccount.keys[coinsList.VRSC.id]; + + for (const channelId in vrscKeys) { + const [channelName, addr, network] = channelId.split('.'); + let rightTitle = formatFiatValue(0); + let rightDescription = '0'; + + if (channelName === VRPC) { + const walletId = `SUBWALLET_${channelId}`; + + if (allSubWallets[coinsList.VRSC.id]) { + const wallet = allSubWallets[coinsList.VRSC.id].find(x => x.id === walletId); + + if (wallet) { + if (selectedDestCoinObj) { + const rightText = getRightText(selectedDestCoinObj.id, [walletId]); + rightTitle = rightText.title; + rightDescription = rightText.description; + } + + addresses.push({ + title: wallet.name, + description: `as ${currencyAlias}`, + logo: selectedDestNetworkId, + key: addr, + rightTitle, + rightDescription + }) + } + } + } + } + } + + if (allowEthAddrs) { + for (const coinId in activeAccount.keys) { + if (activeAccount.keys[coinId] && (activeAccount.keys[coinId][ETH] || activeAccount.keys[coinId][ERC20])) { + const ethAddresses = activeAccount.keys[coinId][ETH] ? + activeAccount.keys[coinId][ETH].addresses : activeAccount.keys[coinId][ERC20].addresses; + + if (ethAddresses && ethAddresses.length > 0) { + const addr = ethAddresses[0]; + const addrTitle = addr.substring(0, 8) + '...' + addr.substring(addr.length - 8); + + let rightTitle = formatFiatValue(0); + let rightDescription = "0"; + + if (selectedDestCoinObj) { + const rightText = getRightText(selectedDestCoinObj.id, ['MAIN_WALLET']); + rightTitle = rightText.title; + rightDescription = rightText.description; + } + + addresses.push({ + title: addrTitle, + description: `as ${currencyAlias}`, + logo: coinsList.ETH.id, + key: addr, + rightTitle, + rightDescription + }) + + break; + } + } + } + } + } + } + + return addresses; + }; + + const getSourceBalance = () => { + return balances[selectedSourceCoinObj.id][selectedSourceWallet.id].confirmed; + } + + const fetchConversionPaths = async () => { + try { + const paths = await getConversionPaths( + selectedSourceCoinObj, + selectedSourceWallet.api_channels[API_SEND], + { + src: selectedSourceCoinObj.currency_id, + }, + ); + + const processedPaths = {}; + const bounceBacks = {}; + + for (const destId in paths) { + processedPaths[destId] = paths[destId].filter(x => { + if (x.bounceback && x.ethdest) bounceBacks[x.destination.mapto.currencyid] = x; + + return !x.mapping && !x.bounceback; + }); + } + + for (const destId in bounceBacks) { + if (processedPaths[destId]) { + processedPaths[destId].push(bounceBacks[destId]) + } else { + processedPaths[destId] = [bounceBacks[destId]] + } + } + + setConversionPaths(processedPaths) + } catch(e) { + console.warn(e); + + createAlert("Error", "Error fetching conversion options. Try going into the wallet for the coin you want to send, and converting through the send tab.") + } + } + + const handleCurrencySelection = (key) => { + if (convertCardModalMode === CONVERT_CARD_MODAL_MODES.SEND) { + setSelectedSourceCurrency(sourceCurrencyMap[key]); + } else { + setSelectedDestCurrencyId(key); + } + } + + const handleNetworkSelection = (key) => { + try { + if (convertCardModalMode === CONVERT_CARD_MODAL_MODES.SEND) { + const networkObj = CoinDirectory.getBasicCoinObj(key); + + setSelectedSourceNetwork(networkObj); + } else { + setSelectedDestNetworkId(key); + } + } catch (e) { + console.warn(e) + } + } + + const handleAddressSelection = (key) => { + try { + if (convertCardModalMode === CONVERT_CARD_MODAL_MODES.SEND) { + if (selectedSourceCoinObj) { + const subWallets = allSubWallets[selectedSourceCoinObj.id]; + + if (subWallets) { + const selectedWallet = subWallets.find(x => x.id === key); + + setSelectedSourceWallet(selectedWallet); + setConvertCardModalVisible(false); + } + } + } else { + setSelectedDestAddress(key); + setConvertCardModalVisible(false); + } + } catch (e) { + console.warn(e) + } + } + + const handleConverterSelection = (key) => { + setSelectedConverterId(key); + } + + const handleSendSelectPressed = () => { + setSelectedSourceCoinObj(null); + setSelectedDestCoinObj(null); + setSelectedSourceWallet(null); + setSelectedSourceNetwork(null); + setSelectedSourceCurrency(null); + setConvertCardModalMode(CONVERT_CARD_MODAL_MODES.SEND); + setConvertCardModalVisible(true); + } + + const handleDestSelectPressed = async () => { + setSelectedDestCurrencyId(null); + + setLoading(true); + setConvertCardModalVisible(true); + setConvertCardModalMode(CONVERT_CARD_MODAL_MODES.RECEIVE); + await fetchConversionPaths(); + setLoading(false); + } + + useEffect(() => { + setTotalBalances(getTotalBalances()); + }, [allSubWallets, activeCoinsForUser, balances, displayCurrency, rates]); + + useEffect(() => { + if (selectedSourceCoinObj && + selectedSourceWallet && + balances[selectedSourceCoinObj.id] && + balances[selectedSourceCoinObj.id][selectedSourceWallet.id]) { + setSourceBalance(getSourceBalance()) + } + }, [selectedSourceCoinObj, selectedSourceWallet, balances]) + + useEffect(() => { + setSourceCurrencyMap(getSourceCurrencyMap()); + }, [activeCoinsForUser]); + + useEffect(() => { + setSourceCurrencyOptionsList(getSourceCurrencyOptionsList()); + }, [sourceCurrencyMap, totalBalances]); + + useEffect(() => { + if (conversionPaths != null && totalBalances != null && selectedSourceCoinObj != null) { + setDestCurrencyOptionsList(getDestCurrencyOptionsList()); + } + }, [conversionPaths, totalBalances, selectedSourceCoinObj]); + + useEffect(() => { + setSourceNetworkOptionsList(getSourceNetworkOptionsList()); + }, [selectedSourceCurrency, totalBalances]); + + useEffect(() => { + setDestNetworkOptionsList(getDestNetworkOptionsList()); + }, [selectedDestCurrencyId, selectedSourceCoinObj, selectedDestCoinObj, conversionPaths]); + + useEffect(() => { + setDestConverterOptionsList(getDestConverterOptionsList()); + }, [selectedDestCurrencyId, selectedSourceCoinObj, selectedDestNetworkId, conversionPaths]); + + useEffect(() => { + setDestAddressOptionsList(getDestAddressOptionsList()); + }, [activeAccount, allSubWallets, conversionPaths, selectedConverterId, selectedDestCurrencyId, selectedSourceCoinObj, selectedDestNetworkId]); + + useEffect(() => { + if (selectedSourceCurrency && selectedSourceNetwork) { + const sourceCoinObj = selectedSourceCurrency.find(x => { + const networkProtocol = x.proto === 'erc20' ? 'eth' : x.proto; + return networkProtocol === selectedSourceNetwork.proto + }); + + setSelectedSourceCoinObj(sourceCoinObj); + } else setSelectedSourceCoinObj(null); + }, [selectedSourceCurrency, selectedSourceNetwork]); + + useEffect(() => { + if (selectedDestCurrencyId) { + try { + setSelectedDestCoinObj(CoinDirectory.findSimpleCoinObj(selectedDestCurrencyId)); + } catch(e) { + setSelectedDestCoinObj(null); + } + } else setSelectedDestCoinObj(null); + }, [selectedDestCurrencyId]); + + useEffect(() => { + setSourceWalletOptionsList(getSourceWalletOptionsList()); + }, [selectedSourceCoinObj, totalBalances]); return ( { justifyContent: 'space-between' }} > + setConvertCardModalVisible(false)} + mode={convertCardModalMode} + totalBalances={totalBalances} + currencies={convertCardModalMode === CONVERT_CARD_MODAL_MODES.SEND ? sourceCurrencyOptionsList : destCurrencyOptionsList} + networks={convertCardModalMode === CONVERT_CARD_MODAL_MODES.SEND ? sourceNetworkOptionsList : destNetworkOptionsList} + converters={convertCardModalMode === CONVERT_CARD_MODAL_MODES.SEND ? null : destConverterOptionsList} + addresses={convertCardModalMode === CONVERT_CARD_MODAL_MODES.SEND ? sourceWalletOptionsList : destAddressOptionsList} + onSelectCurrency={(currencyId) => handleCurrencySelection(currencyId)} + onSelectNetwork={(networkId) => handleNetworkSelection(networkId)} + onSelectAddress={(addressKey) => handleAddressSelection(addressKey)} + onSelectConverter={(converterId) => handleConverterSelection(converterId)} + setVisible={setConvertCardModalVisible} + loading={loading} + /> - + setSendAmount(sourceBalance ? sourceBalance.toString() : 0)} + /> + setSendAmount(sourceBalance ? sourceBalance.toString() : 0)} + /> { + setCardActive(false); + + if (onSelectPressed) onSelectPressed(); + } + return ( setAmount(x)} mode="outlined" keyboardType="numeric" + disabled={!setAmount} /> {!cardActive ? : - + {RenderCircleCoinLogo(coinObj.id)} - + } - {!cardActive ? '' : `${balance == null ? balance : '-'} ${coinObj.display_ticker}`} + {!cardActive ? '' : `${balance != null ? balance : '-'} ${coinObj.display_ticker}`}