Skip to content

Commit

Permalink
Add buttons to CoinRankingDetailsScene
Browse files Browse the repository at this point in the history
Jon-edge committed Jan 30, 2025
1 parent a1adba3 commit 1d75f69
Showing 2 changed files with 291 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

- added: `NotificationCenterScene`
- added: Search bar to `EarnScene`
- added: Buy, Sell, Earn, and Trade buttons to `CoinRankingDetailsScene`
- added: Price chart to `TransactionListScene`
- added: Add Unizen DEX
- changed: `TransactionListScene` split into two scenes: `TransactionListScene` and `TransactionListScene2`
300 changes: 290 additions & 10 deletions src/components/scenes/CoinRankingDetailsScene.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import { useIsFocused } from '@react-navigation/native'
import * as React from 'react'
import { View } from 'react-native'
import { ActivityIndicator, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import Feather from 'react-native-vector-icons/Feather'
import Ionicons from 'react-native-vector-icons/Ionicons'

import { createWallet, getUniqueWalletName } from '../../actions/CreateWalletActions'
import { getFirstOpenInfo } from '../../actions/FirstOpenActions'
import { updateStakingState } from '../../actions/scene/StakingActions'
import { Fontello } from '../../assets/vector/index'
import { WalletListModal, WalletListResult } from '../../components/modals/WalletListModal'
import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants'
import { useAsyncValue } from '../../hooks/useAsyncValue'
import { formatFiatString } from '../../hooks/useFiatText'
import { useHandler } from '../../hooks/useHandler'
import { useWatch } from '../../hooks/useWatch'
import { toLocaleDate, toPercentString } from '../../locales/intl'
import { lstrings } from '../../locales/strings'
import { defaultWalletStakingState } from '../../reducers/StakingReducer'
import { getDefaultFiat } from '../../selectors/SettingsSelectors'
import { asCoinRankingData, CoinRankingData, CoinRankingDataPercentChange } from '../../types/coinrankTypes'
import { useSelector } from '../../types/reactRedux'
import { EdgeAppSceneProps } from '../../types/routerTypes'
import { useDispatch, useSelector } from '../../types/reactRedux'
import { EdgeAppSceneProps, NavigationBase } from '../../types/routerTypes'
import { EdgeAsset } from '../../types/types'
import { CryptoAmount } from '../../util/CryptoAmount'
import { fetchRates } from '../../util/network'
import { formatLargeNumberString as formatLargeNumber } from '../../util/utils'
import { getPluginFromPolicy } from '../../util/stakeUtils'
import { getUkCompliantString } from '../../util/ukComplianceUtils'
import { DECIMAL_PRECISION, formatLargeNumberString as formatLargeNumber } from '../../util/utils'
import { IconButton } from '../buttons/IconButton'
import { SwipeChart } from '../charts/SwipeChart'
import { EdgeAnim, fadeInLeft } from '../common/EdgeAnim'
import { SceneWrapper } from '../common/SceneWrapper'
import { Airship, showError } from '../services/AirshipInstance'
import { cacheStyles, Theme, useTheme } from '../services/ThemeContext'
import { EdgeText } from '../themed/EdgeText'
import { COINGECKO_SUPPORTED_FIATS } from './CoinRankingScene'
@@ -81,9 +98,19 @@ const COLUMN_RIGHT_DATA_KEYS: Array<keyof CoinRankingData> = [
const CoinRankingDetailsSceneComponent = (props: Props) => {
const theme = useTheme()
const styles = getStyles(theme)
const dispatch = useDispatch()
const { route, navigation } = props
const { assetId, fiatCurrencyCode, coinRankingData: initCoinRankingData } = route.params

const account = useSelector(state => state.core.account)
const exchangeRates = useSelector(state => state.exchangeRates)
const walletStakingStateMap = useSelector(state => state.staking.walletStakingMap ?? defaultWalletStakingState)

const currencyWallets = useWatch(account, 'currencyWallets')
const isFocused = useIsFocused()

const [countryCode] = useAsyncValue(async () => (await getFirstOpenInfo()).countryCode)

// In case the user changes their default fiat while viewing this scene, we
// want to go back since the parent scene handles fetching data.
const defaultFiat = useSelector(state => getDefaultFiat(state))
@@ -106,19 +133,58 @@ const CoinRankingDetailsSceneComponent = (props: Props) => {

const coinRankingData = fetchedCoinRankingData ?? initCoinRankingData

const { currencyCode, currencyName } = coinRankingData ?? {}
const currencyCodeUppercase = currencyCode?.toUpperCase() ?? ''
const { currencyCode: coinRankingCurrencyCode, currencyName } = coinRankingData ?? {}
// `coinRankingCurrencyCode` is lowercase and that breaks a lot of our utility
// calls
const currencyCode = coinRankingCurrencyCode?.toUpperCase() ?? ''

/** Loosely Equivalent EdgeAssets for the CoinGecko coin on this scene */
const edgeAssets = React.useMemo<EdgeAsset[]>(() => {
if (coinRankingData == null) return []

const out = []
// Search for mainnet coins:
for (const pluginId of Object.keys(account.currencyConfig)) {
const config = account.currencyConfig[pluginId]
if (config.currencyInfo.currencyCode.toLowerCase() === currencyCode.toLowerCase()) out.push({ tokenId: null, pluginId })
}
// Search for tokens:
for (const pluginId of Object.keys(account.currencyConfig)) {
const config = account.currencyConfig[pluginId]
for (const tokenId of Object.keys(config.allTokens)) {
const token = config.allTokens[tokenId]
if (token.currencyCode.toLowerCase() === currencyCode.toLowerCase()) out.push({ tokenId, pluginId })
}
}
return out
}, [account.currencyConfig, coinRankingData, currencyCode])

const isFocused = useIsFocused()
const initFiat = React.useState<string>(fiatCurrencyCode)[0]

/** Find all wallets that can hold this asset */
const matchingWallets = React.useMemo(
() => Object.values(currencyWallets).filter(wallet => edgeAssets.some(asset => asset.pluginId === wallet.currencyInfo.pluginId)),
[edgeAssets, currencyWallets]
)

/** Check if all the stake plugins are loaded for this asset type */
const isStakingLoading = matchingWallets.some(wallet => walletStakingStateMap[wallet.id] == null || walletStakingStateMap[wallet.id].isLoading)

React.useEffect(() => {
if (isFocused && initFiat !== supportedFiat) {
// Take this stale scene off the stack
navigation.pop()
// Force a refresh & refetch
navigation.navigate('coinRanking')

// Update staking state:
if (coinRankingData != null && matchingWallets.length > 0) {
matchingWallets.forEach(wallet => {
dispatch(updateStakingState(currencyCode, wallet)).catch(err => showError(err))
})
}
}
}, [supportedFiat, initFiat, isFocused, navigation])
}, [coinRankingData, currencyCode, dispatch, edgeAssets, initFiat, isFocused, matchingWallets, navigation, supportedFiat])

const imageUrlObject = React.useMemo(
() => ({
@@ -214,15 +280,217 @@ const CoinRankingDetailsSceneComponent = (props: Props) => {
return rows
}

/**
* Returns a WalletListResult to use for the button navigation. Returns the
* wallet the user chose from the wallet picker, automatically selects a
* single wallet, or undefined if the user was presented with the wallet
* picker but dismissed it.
*/
const chooseWalletListResult = async (): Promise<Extract<WalletListResult, { type: 'wallet' }> | undefined> => {
// No compatible assets. Shouldn't happen since buttons are blocked from
// handlers anyway, if there's no edgeAssets
if (edgeAssets.length === 0) return

// If no wallet exists, auto create one.
// Only do this if there is only one possible match that we know of
if (matchingWallets.length === 0 && edgeAssets.length === 1) {
const walletName = getUniqueWalletName(account, edgeAssets[0].pluginId)
const targetWallet = await createWallet(account, {
name: walletName,
walletType: `wallet:${edgeAssets[0].pluginId}`,
fiatCurrencyCode: fiatCurrencyCode
})
if (edgeAssets[0].tokenId != null) {
await targetWallet.changeEnabledTokenIds([...targetWallet.enabledTokenIds, edgeAssets[0].tokenId])
}
return {
type: 'wallet',
tokenId: edgeAssets[0].tokenId,
walletId: targetWallet.id
}
}

// If only one wallet, auto-select it
if (matchingWallets.length === 1) {
return {
type: 'wallet',
tokenId: edgeAssets[0].tokenId,
walletId: matchingWallets[0].id
}
}

// Else, If multiple wallets, show picker. Tokens also can be added here.
const result = await Airship.show<WalletListResult>(bridge => (
<WalletListModal
bridge={bridge}
navigation={navigation as NavigationBase}
headerTitle={lstrings.select_wallet_to_send_from}
allowedAssets={edgeAssets}
showCreateWallet
/>
))
// User aborted the flow. Callers will also noop.
if (result?.type !== 'wallet') return
return result
}

const handleBuyPress = useHandler(async () => {
if (edgeAssets.length === 0) return
const forcedWalletResult = await chooseWalletListResult()
if (forcedWalletResult == null) return

navigation.navigate('edgeTabs', {
screen: 'buyTab',
params: {
screen: 'pluginListBuy',
params: {
forcedWalletResult
}
}
})
})

const handleSellPress = useHandler(async () => {
if (edgeAssets.length === 0) return
const forcedWalletResult = await chooseWalletListResult()
if (forcedWalletResult == null) return

navigation.navigate('edgeTabs', {
screen: 'sellTab',
params: {
screen: 'pluginListSell',
params: {
forcedWalletResult
}
}
})
})

const handleTradePress = useHandler(async () => {
if (edgeAssets.length === 0) return

const walletListResult = await chooseWalletListResult()
if (walletListResult == null) return

const { walletId, tokenId } = walletListResult

// Find the wallet with highest USD value to use as source (swap from)
// TODO: Include token balances in this sort
const sourceWallet = Object.values(currencyWallets)
.filter(wallet => wallet.id !== walletId)
.sort((a, b) => {
const aCryptoAmount = new CryptoAmount({
currencyConfig: a.currencyConfig,
tokenId: null,
nativeAmount: a.balanceMap.get(null) ?? '0'
})
const aDollarValue = parseFloat(aCryptoAmount.displayDollarValue(exchangeRates, DECIMAL_PRECISION))

const bCryptoAmount = new CryptoAmount({
currencyConfig: b.currencyConfig,
tokenId: null,
nativeAmount: b.balanceMap.get(null) ?? '0'
})
const bDollarValue = parseFloat(bCryptoAmount.displayDollarValue(exchangeRates, DECIMAL_PRECISION))

return bDollarValue - aDollarValue
})[0]

// Navigate to the swap scene
navigation.navigate('edgeTabs', {
screen: 'swapTab',
params: {
screen: 'swapCreate',
params: {
fromWalletId: sourceWallet.id,
toWalletId: walletId,
toTokenId: tokenId
}
}
})
})

const isStakingAvailable = (): boolean => {
if (countryCode == null || edgeAssets.length === 0) return false

// Special case for FIO because it uses it's own staking plugin
const isStakingSupported = currencyCode === 'FIO' || edgeAssets.some(asset => SPECIAL_CURRENCY_INFO[asset.pluginId]?.isStakingSupported === true)
return isStakingSupported
}

const handleStakePress = useHandler(async () => {
const walletListResult = await chooseWalletListResult()
if (walletListResult == null) return
const { walletId } = walletListResult

const walletStakingState = walletStakingStateMap[walletId] ?? defaultWalletStakingState
const { stakePlugins } = walletStakingState
const stakePolicies = Object.values(walletStakingState.stakePolicies)

// Handle FIO staking
if (currencyCode === 'FIO') {
navigation.push('fioStakingOverview', {
tokenId: null,
walletId
})
return
}

// Handle StakePlugin staking
if (stakePlugins != null && stakePolicies != null) {
if (stakePolicies.length > 1) {
navigation.push('stakeOptions', {
walletId,
currencyCode
})
} else if (stakePolicies.length === 1) {
const [stakePolicy] = stakePolicies
const { stakePolicyId } = stakePolicy
const stakePlugin = getPluginFromPolicy(stakePlugins, stakePolicy, {
pluginId: currencyWallets[walletId].currencyInfo.pluginId,
currencyCode
})
if (stakePlugin != null)
navigation.push('stakeOverview', {
stakePlugin,
walletId,
stakePolicyId
})
}
}
})

return (
<SceneWrapper hasTabs hasNotifications scroll>
{coinRankingData != null ? (
<View style={styles.container}>
<EdgeAnim style={styles.titleContainer} enter={fadeInLeft}>
<FastImage style={styles.icon} source={imageUrlObject} />
<EdgeText style={styles.title}>{`${currencyName} (${currencyCodeUppercase})`}</EdgeText>
<EdgeText style={styles.title}>{`${currencyName} (${currencyCode})`}</EdgeText>
</EdgeAnim>
<SwipeChart assetId={coinRankingData.assetId} currencyCode={currencyCodeUppercase} fiatCurrencyCode={initFiat} />
<SwipeChart assetId={coinRankingData.assetId} currencyCode={currencyCode} fiatCurrencyCode={initFiat} />
{edgeAssets.length <= 0 ? null : (
<View style={styles.buttonsContainer}>
<IconButton label={lstrings.title_buy} onPress={handleBuyPress}>
<Fontello name="buy" size={theme.rem(2)} color={theme.primaryText} />
</IconButton>
<IconButton label={lstrings.title_sell} onPress={handleSellPress}>
<Fontello name="sell" size={theme.rem(2)} color={theme.primaryText} />
</IconButton>
{!isStakingAvailable() ? null : (
<IconButton label={getUkCompliantString(countryCode, 'stake_earn_button_label')} onPress={handleStakePress}>
{isStakingLoading ? (
<ActivityIndicator color={theme.primaryText} style={styles.buttonLoader} />
) : (
<Feather name="percent" size={theme.rem(2)} color={theme.primaryText} />
)}
</IconButton>
)}
<IconButton label={lstrings.trade_currency} onPress={handleTradePress}>
<Ionicons name="swap-horizontal" size={theme.rem(2)} color={theme.primaryText} />
</IconButton>
</View>
)}
<View style={styles.columns}>
<View style={styles.column}>{renderRows(coinRankingData, COLUMN_LEFT_DATA_KEYS)}</View>
<View style={styles.column}>{renderRows(coinRankingData, COLUMN_RIGHT_DATA_KEYS)}</View>
@@ -273,6 +541,18 @@ const getStyles = cacheStyles((theme: Theme) => {
margin: theme.rem(0.5),
flexDirection: 'row',
alignItems: 'center'
},
buttonsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
marginVertical: theme.rem(1),
paddingHorizontal: theme.rem(1)
},
buttonLoader: {
justifyContent: 'center',
alignItems: 'center',
minHeight: theme.rem(2)
}
}
})

0 comments on commit 1d75f69

Please sign in to comment.