Skip to content

Commit

Permalink
Hide chart when data not available, prefetch ada prices
Browse files Browse the repository at this point in the history
  • Loading branch information
jorbuedo committed Sep 30, 2024
1 parent 6aad749 commit af47146
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 121 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {invalid} from '@yoroi/common'
import {invalid, isRight} from '@yoroi/common'
import {Portfolio} from '@yoroi/types'
import {produce} from 'immer'
import * as React from 'react'
import {useQuery} from 'react-query'

import {useSelectedNetwork} from '../../WalletManager/common/hooks/useSelectedNetwork'

export const PortfolioDetailsTab = {
Performance: 'Performance',
Expand Down Expand Up @@ -32,13 +36,15 @@ const defaultActions: PortfolioActions = {
} as const

const defaultState: PortfolioState = {
isTokenHistoryApiAvailable: false,
isPrimaryTokenActive: false,
detailsTab: PortfolioDetailsTab.Overview,
listTab: PortfolioListTab.Wallet,
dappsTab: PortfolioDappsTab.LiquidityPool,
} as const

type PortfolioState = {
isTokenHistoryApiAvailable: boolean
isPrimaryTokenActive: boolean
detailsTab: PortfolioDetailsTab
listTab: PortfolioListTab
Expand All @@ -57,6 +63,22 @@ export const PortfolioProvider = ({
children: React.ReactNode
initialState?: Partial<PortfolioState>
}) => {
const {
networkManager: {tokenManager},
} = useSelectedNetwork()
const {data} = useQuery({
queryKey: ['isTokenHistoryApiAvailable'],
initialData: () => false,
queryFn: async () => {
const response = await tokenManager.api.tokenHistory(
'279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f.534e454b',
Portfolio.Token.HistoryPeriod.OneDay,
)
if (isRight(response)) return true
return false
},
})
const isTokenHistoryApiAvailable = data ?? false
const [portfolioState, dispatch] = React.useReducer(portfolioReducer, {...defaultState, ...initialState})

const actions = React.useRef<PortfolioActions>({
Expand All @@ -76,8 +98,8 @@ export const PortfolioProvider = ({
}).current

const context = React.useMemo<PortfolioState & PortfolioActions>(
() => ({...portfolioState, ...actions}),
[actions, portfolioState],
() => ({...portfolioState, ...actions, isTokenHistoryApiAvailable}),
[actions, portfolioState, isTokenHistoryApiAvailable],
)

return <PortfolioContext.Provider value={context}>{children}</PortfolioContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import {Chain, Portfolio} from '@yoroi/types'
import {useQuery, UseQueryOptions} from 'react-query'

import {supportedCurrencies, time} from '../../../../kernel/constants'
import {features} from '../../../../kernel/features'
import {useLanguage} from '../../../../kernel/i18n'
import {logger} from '../../../../kernel/logger/logger'
import {fetchPtPriceActivity} from '../../../../yoroi-wallets/cardano/usePrimaryTokenActivity'
import {delay} from '../../../../yoroi-wallets/utils/timeUtils'
import {useCurrencyPairing} from '../../../Settings/Currency/CurrencyContext'
import {useSelectedNetwork} from '../../../WalletManager/common/hooks/useSelectedNetwork'
import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet'
import {networkConfigs} from '../../../WalletManager/network-manager/network-manager'
import {priceChange} from '../helpers/priceChange'
import {usePortfolioTokenDetailParams} from './useNavigateTo'

export const TOKEN_CHART_INTERVAL = {
export const TokenChartInterval = {
DAY: '24 H',
WEEK: '1 W',
MONTH: '1 M',
Expand All @@ -24,7 +24,7 @@ export const TOKEN_CHART_INTERVAL = {
ALL: 'ALL',
} as const

export type TokenChartInterval = (typeof TOKEN_CHART_INTERVAL)[keyof typeof TOKEN_CHART_INTERVAL]
export type TokenChartInterval = (typeof TokenChartInterval)[keyof typeof TokenChartInterval]

type TokenChartData = {
label: string
Expand All @@ -36,22 +36,33 @@ type TokenChartData = {
const getTimestamps = (timeInterval: TokenChartInterval) => {
const now = Date.now()
const [from, resolution] = {
[TOKEN_CHART_INTERVAL.DAY]: [now - time.oneDay, 96],
[TOKEN_CHART_INTERVAL.WEEK]: [now - time.oneWeek, 168],
[TOKEN_CHART_INTERVAL.MONTH]: [now - time.oneMonth, 180],
[TOKEN_CHART_INTERVAL.SIX_MONTHS]: [now - time.sixMonths, 180],
[TOKEN_CHART_INTERVAL.YEAR]: [now - time.oneYear, 365],
[TOKEN_CHART_INTERVAL.ALL]: [new Date('2018').getTime(), 256],
}[timeInterval]
[TokenChartInterval.DAY]: [now - time.oneDay, 96],
[TokenChartInterval.WEEK]: [now - time.oneWeek, 168],
[TokenChartInterval.MONTH]: [now - time.oneMonth, 180],
[TokenChartInterval.SIX_MONTHS]: [now - time.sixMonths, 180],
[TokenChartInterval.YEAR]: [now - time.oneYear, 365],
[TokenChartInterval.ALL]: [new Date('2018').getTime(), 256],
}[timeInterval ?? TokenChartInterval.DAY]

const step = (now - from) / resolution
return Array.from({length: resolution}, (_, i) => from + Math.round(step * i))
}

const ptTicker = networkConfigs[Chain.Network.Mainnet].primaryTokenInfo.ticker

export const ptPriceQueryFn = async ({queryKey}: {queryKey: ['ptPriceHistory', TokenChartInterval]}) => {
const response = await fetchPtPriceActivity(getTimestamps(queryKey[1]))
if (isRight(response) && !response.value.data.error) {
const tickers = response.value.data.tickers
if (tickers.length === 0) return null
return tickers
}
logger.error('Failed to fetch token chart data for PT')
return null
}

export const useGetPortfolioTokenChart = (
timeInterval = TOKEN_CHART_INTERVAL.DAY as TokenChartInterval,
timeInterval = TokenChartInterval.DAY as TokenChartInterval,
options: UseQueryOptions<
TokenChartData[] | null,
Error,
Expand All @@ -70,45 +81,50 @@ export const useGetPortfolioTokenChart = (
const {currency} = useCurrencyPairing()
const {languageCode} = useLanguage()

const ptQuery = useQuery({
const ptPriceQuery = useQuery<
Awaited<ReturnType<typeof ptPriceQueryFn>>,
Error,
Awaited<ReturnType<typeof ptPriceQueryFn>>,
['ptPriceHistory', TokenChartInterval]
>({
enabled: tokenInfo && isPrimaryToken(tokenInfo.info),
staleTime: time.halfHour,
cacheTime: time.oneHour,
retryDelay: time.oneSecond,
optimisticResults: true,
refetchInterval: time.halfHour,
useErrorBoundary: true,
refetchOnMount: false,
queryKey: ['ptPriceHistory', timeInterval],
queryFn: ptPriceQueryFn,
})

const ptQuery = useQuery({
enabled: tokenInfo && isPrimaryToken(tokenInfo.info),
staleTime: time.oneMinute,
...options,
queryKey: ['useGetPortfolioTokenChart', tokenInfo?.info.id ?? '', timeInterval, currency],
queryKey: ['useGetPortfolioTokenChart', '.', timeInterval, currency],
queryFn: async () => {
const response = await fetchPtPriceActivity(getTimestamps(timeInterval))
if (isRight(response)) {
if (response.value.data.error) throw new Error(response.value.data.error)

const tickers = response.value.data.tickers
if (tickers.length === 0) return null

const validCurrency = currency === ptTicker ? supportedCurrencies.USD : currency ?? supportedCurrencies.USD

const initialPrice = tickers[0].prices[validCurrency]
const records = tickers
.map((ticker) => {
const value = ticker.prices[validCurrency]
if (value === undefined) return undefined
const {changePercent, changeValue} = priceChange(initialPrice, value)
const label = new Date(ticker.timestamp).toLocaleString(languageCode, {
dateStyle: 'short',
timeStyle: 'short',
})
return {label, value, changePercent, changeValue}
// force queryFn to be async, otherwise it takes longer and doesn't show isFetching
await delay(0)

const tickers = ptPriceQuery.data ?? []
if (tickers.length === 0) return null

const validCurrency = currency === ptTicker ? supportedCurrencies.USD : currency ?? supportedCurrencies.USD

const initialPrice = tickers[0].prices[validCurrency]
const records = tickers
.map((ticker) => {
const value = ticker.prices[validCurrency]
if (value === undefined) return undefined
const {changePercent, changeValue} = priceChange(initialPrice, value)
const label = new Date(ticker.timestamp).toLocaleString(languageCode, {
dateStyle: 'short',
timeStyle: 'short',
})
.filter(Boolean) as TokenChartData[]
return {label, value, changePercent, changeValue}
})
.filter(Boolean) as TokenChartData[]

return records
}
logger.error('Failed to fetch token chart data for PT')
return null
return records
},
})

Expand All @@ -119,8 +135,6 @@ export const useGetPortfolioTokenChart = (
...options,
queryKey: ['useGetPortfolioTokenChart', tokenInfo?.info.id ?? '', timeInterval],
queryFn: async () => {
if (!features.portfolioSecondaryCharts) return null

const response = await tokenManager.api.tokenHistory(tokenId, chartIntervalToHistoryPeriod(timeInterval))
if (isRight(response)) {
const prices = response.value.data.prices
Expand Down Expand Up @@ -153,10 +167,10 @@ export const useGetPortfolioTokenChart = (

const chartIntervalToHistoryPeriod = (i: TokenChartInterval): Portfolio.Token.HistoryPeriod =>
({
[TOKEN_CHART_INTERVAL.DAY]: Portfolio.Token.HistoryPeriod.OneDay,
[TOKEN_CHART_INTERVAL.WEEK]: Portfolio.Token.HistoryPeriod.OneWeek,
[TOKEN_CHART_INTERVAL.MONTH]: Portfolio.Token.HistoryPeriod.OneMonth,
[TOKEN_CHART_INTERVAL.SIX_MONTHS]: Portfolio.Token.HistoryPeriod.SixMonth,
[TOKEN_CHART_INTERVAL.YEAR]: Portfolio.Token.HistoryPeriod.OneYear,
[TOKEN_CHART_INTERVAL.ALL]: Portfolio.Token.HistoryPeriod.All,
[TokenChartInterval.DAY]: Portfolio.Token.HistoryPeriod.OneDay,
[TokenChartInterval.WEEK]: Portfolio.Token.HistoryPeriod.OneWeek,
[TokenChartInterval.MONTH]: Portfolio.Token.HistoryPeriod.OneMonth,
[TokenChartInterval.SIX_MONTHS]: Portfolio.Token.HistoryPeriod.SixMonth,
[TokenChartInterval.YEAR]: Portfolio.Token.HistoryPeriod.OneYear,
[TokenChartInterval.ALL]: Portfolio.Token.HistoryPeriod.All,
}[i] ?? Portfolio.Token.HistoryPeriod.OneDay)
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ import {useTheme} from '@yoroi/theme'
import React, {useCallback, useState} from 'react'
import {StyleSheet, View} from 'react-native'

import {
type TokenChartInterval,
TOKEN_CHART_INTERVAL,
useGetPortfolioTokenChart,
} from '../../../common/hooks/useGetPortfolioTokenChart'
import {TokenChartInterval, useGetPortfolioTokenChart} from '../../../common/hooks/useGetPortfolioTokenChart'
import {ChartPlaceholder} from './ChartPlaceholder'
import {PortfolioTokenChartSkeleton} from './PortfolioTokenChartSkeleton'
import {TokenChart} from './TokenChart'
Expand All @@ -18,7 +14,7 @@ export const PortfolioTokenChart = () => {

const [selectedIndex, setSelectedIndex] = useState(0)

const [timeInterval, setTimeInterval] = useState<TokenChartInterval>(TOKEN_CHART_INTERVAL.DAY)
const [timeInterval, setTimeInterval] = useState<TokenChartInterval>(TokenChartInterval.DAY)

const {data, isFetching} = useGetPortfolioTokenChart(timeInterval)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'

import {Text} from '../../../../../components/Text'
import {type TokenChartInterval, TOKEN_CHART_INTERVAL} from '../../../common/hooks/useGetPortfolioTokenChart'
import {TokenChartInterval} from '../../../common/hooks/useGetPortfolioTokenChart'

interface Props {
timeInterval: TokenChartInterval
Expand All @@ -21,7 +21,7 @@ export const TokenChartToolbar = ({timeInterval, disabled, onChange}: Props) =>

return (
<View style={styles.chartToolbar}>
{Object.values(TOKEN_CHART_INTERVAL).map((itv) => (
{Object.values(TokenChartInterval).map((itv) => (
<TouchableOpacity
onPress={() => handleChange(itv)}
style={[itv === timeInterval ? styles.chartToolbarItemActive : {}, styles.chartToolbarItem]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {Text} from '../../../../../components/Text'
import {Tooltip} from '../../../../../components/Tooltip/Tooltip'
import {useCurrencyPairing} from '../../../../Settings/Currency/CurrencyContext'
import {formatPriceChange} from '../../../common/helpers/priceChange'
import {TOKEN_CHART_INTERVAL, TokenChartInterval} from '../../../common/hooks/useGetPortfolioTokenChart'
import {TokenChartInterval} from '../../../common/hooks/useGetPortfolioTokenChart'
import {useStrings} from '../../../common/hooks/useStrings'
import {PnlTag} from '../../../common/PnlTag/PnlTag'

Expand Down Expand Up @@ -35,17 +35,17 @@ export const TokenPerformance = ({tokenPerformance, timeInterval}: Props) => {

const intervalLabel = React.useMemo(() => {
switch (timeInterval) {
case TOKEN_CHART_INTERVAL.DAY:
case TokenChartInterval.DAY:
return strings._24_hours
case TOKEN_CHART_INTERVAL.WEEK:
case TokenChartInterval.WEEK:
return strings._1_week
case TOKEN_CHART_INTERVAL.MONTH:
case TokenChartInterval.MONTH:
return strings._1_month
case TOKEN_CHART_INTERVAL.SIX_MONTHS:
case TokenChartInterval.SIX_MONTHS:
return strings._6_months
case TOKEN_CHART_INTERVAL.YEAR:
case TokenChartInterval.YEAR:
return strings._1_year
case TOKEN_CHART_INTERVAL.ALL:
case TokenChartInterval.ALL:
return strings.all_time
default:
return strings._24_hours
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {isPrimaryTokenInfo} from '@yoroi/portfolio'
import {useTheme} from '@yoroi/theme'
import {App} from '@yoroi/types'
import * as React from 'react'
Expand All @@ -21,16 +22,16 @@ import {PortfolioTokenBalance} from './PortfolioTokenBalance/PortfolioTokenBalan
import {PortfolioTokenChart} from './PortfolioTokenChart/PortfolioTokenChart'
import {PortfolioTokenInfo} from './PortfolioTokenInfo/PortfolioTokenInfo'

const HEADER_HEIGHT = 304

export const PortfolioTokenDetailsScreen = () => {
const strings = useStrings()
const {detailsTab, setDetailsTab} = usePortfolio()
const {detailsTab, setDetailsTab, isTokenHistoryApiAvailable} = usePortfolio()
const {track} = useMetrics()
const [isStickyTab, setIsStickyTab] = React.useState(false)
const {id: tokenId} = usePortfolioTokenDetailParams()
const {wallet} = useSelectedWallet()
const tokenInfo = wallet.balances.records.get(tokenId)?.info
const isPrimaryToken = isPrimaryTokenInfo(tokenInfo)
const HEADER_HEIGHT = isPrimaryToken || isTokenHistoryApiAvailable ? 304 : 85
const {styles} = useStyles(HEADER_HEIGHT)

if (!tokenInfo) throwLoggedError(new App.Errors.InvalidState('Token info not found, invalid state'))
Expand Down Expand Up @@ -91,9 +92,13 @@ export const PortfolioTokenDetailsScreen = () => {

<Spacer height={16} />

<PortfolioTokenChart />
{(isPrimaryToken || isTokenHistoryApiAvailable) && (
<>
<PortfolioTokenChart />

<Spacer height={16} />
<Spacer height={16} />
</>
)}
</Animated.View>

<Animated.View>{renderTabs}</Animated.View>
Expand Down
1 change: 0 additions & 1 deletion apps/wallet-mobile/src/kernel/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export const features = {
showProdPoolsInDev: isDev,
moderatingNftsEnabled: false,
poolTransition: true,
portfolioSecondaryCharts: isDev,
portfolioPerformance: false,
portfolioNews: false,
portfolioExport: false,
Expand Down
12 changes: 12 additions & 0 deletions apps/wallet-mobile/src/kernel/query-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import {freeze} from 'immer'
import {QueryClient} from 'react-query'

import {ptPriceQueryFn, TokenChartInterval} from '../features/Portfolio/common/hooks/useGetPortfolioTokenChart'

const queryClient = new QueryClient()

Object.values(TokenChartInterval).forEach((TokenChartInterval) => {
queryClient.prefetchQuery<
Awaited<ReturnType<typeof ptPriceQueryFn>>,
Error,
Awaited<ReturnType<typeof ptPriceQueryFn>>,
['ptPriceHistory', TokenChartInterval]
>({queryKey: ['ptPriceHistory', TokenChartInterval], queryFn: ptPriceQueryFn})
})

const keyToPersist = 'persist'
/*
const queryPersistorStorageKey = 'react-query-persistor'
Expand Down
Loading

0 comments on commit af47146

Please sign in to comment.