Skip to content

Commit d2995dc

Browse files
Separate Cash,Savings,Invest
1 parent 18665ed commit d2995dc

File tree

12 files changed

+565
-260
lines changed

12 files changed

+565
-260
lines changed

apps/next/pages/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const Page: NextPageWithLayout<InferGetServerSidePropsType<typeof getServ
4949
<title>Send</title>
5050
</Head>
5151
{session ? (
52-
<HomeLayout TopNav={<TopNav header="Home" showLogo={true} />}>
52+
<HomeLayout TopNav={<TopNav header="Home" showLogo={true} backFunction="router" />}>
5353
<HomeScreen />
5454
</HomeLayout>
5555
) : (

packages/app/components/TopNav.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,23 +101,16 @@ export function TopNav({
101101
showOnGtLg = false,
102102
hideRightActions = false,
103103
}: TopNavProps) {
104-
const [queryParams, setRootParams] = useRootScreenParams()
104+
const [queryParams] = useRootScreenParams()
105105
const path = usePathname()
106106
const parts = path.split('/').filter(Boolean)
107107
const { push, back } = useRouter()
108108
const media = useMedia()
109-
const { coin: selectedCoin } = useCoinFromTokenParam()
110109
const { profile } = useUser()
111110

112-
const hasSelectedCoin = Boolean(selectedCoin)
113-
114111
const handleBack = () => {
115112
// pop to the base path if subroute. e.g. /account/settings/edit-profile -> /account
116113
// else, go to home page
117-
if (hasSelectedCoin) {
118-
setRootParams({ ...queryParams, token: undefined })
119-
return
120-
}
121114
if (backFunction === 'router') {
122115
back()
123116
return
@@ -169,7 +162,7 @@ export function TopNav({
169162
)
170163
case media.gtLg && !showOnGtLg:
171164
return null
172-
case hasSelectedCoin:
165+
case Boolean(queryParams.token):
173166
return (
174167
<XStack ai="center" f={1}>
175168
<Button onPress={handleBack}>
@@ -181,9 +174,6 @@ export function TopNav({
181174
/>
182175
</ButtonOg.Icon>
183176
</Button>
184-
<Paragraph size={'$8'} col={'$color10'}>
185-
Balance
186-
</Paragraph>
187177
</XStack>
188178
)
189179
case !isSubRoute:

packages/app/features/home/HomeQuickActions.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ const Trade = () => {
113113
height={'auto'}
114114
>
115115
<Theme name="green">
116-
<IconSwap
117-
size={'$1'}
116+
<IconPlus
117+
size={'$1.5'}
118118
$theme-dark={{ color: '$primary' }}
119119
$theme-light={{ color: '$color12' }}
120120
/>
@@ -127,7 +127,7 @@ const Trade = () => {
127127
numberOfLines={1}
128128
ellipsizeMode="tail"
129129
>
130-
Trade
130+
Invest
131131
</ButtonText>
132132
</YStack>
133133
</QuickActionButton>

packages/app/features/home/TokenBalanceList.tsx renamed to packages/app/features/home/InvestmentBalanceList.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Link, type LinkProps, Paragraph, XStack, YStack } from '@my/ui'
1+
import { Link, LinkableButton, type LinkProps, Paragraph, XStack, YStack } from '@my/ui'
22

33
import { IconCoin } from 'app/components/icons/IconCoin'
44
import type { CoinWithBalance } from 'app/data/coins'
55

66
import { useCoins } from 'app/provider/coins'
7-
import { useRootScreenParams } from 'app/routers/params'
7+
88
import formatAmount from 'app/utils/formatAmount'
99
import { Fragment } from 'react'
1010
import { useHoverStyles } from 'app/utils/useHoverStyles'
@@ -13,26 +13,24 @@ import { useTokenPrices } from 'app/utils/useTokenPrices'
1313
import { type MarketData, useMultipleTokensMarketData } from 'app/utils/coin-gecko'
1414
import { useThemeSetting } from '@tamagui/next-theme'
1515
import { useIsPriceHidden } from 'app/features/home/utils/useIsPriceHidden'
16+
import { IconPlus } from 'app/components/icons'
1617

17-
export const TokenBalanceList = () => {
18-
const { coins, isLoading } = useCoins()
19-
const [{ token: tokenParam }] = useRootScreenParams()
18+
export const InvestmentsBalanceList = () => {
19+
const { investmentCoins, isLoading } = useCoins()
2020
const hoverStyles = useHoverStyles()
2121
const { data: tokensMarketData, isLoading: isLoadingTokensMarketData } =
22-
useMultipleTokensMarketData(coins?.map((c) => c.coingeckoTokenId) || [])
22+
useMultipleTokensMarketData(investmentCoins?.map((c) => c.coingeckoTokenId) || [])
2323

2424
if (isLoading) return null
2525

26-
return coins.map((coin) => (
26+
return investmentCoins.map((coin) => (
2727
<Fragment key={`token-balance-list-${coin.label}`}>
2828
<TokenBalanceItem
2929
coin={coin}
3030
jc={'space-between'}
3131
ai={'center'}
3232
p={'$3.5'}
3333
br={'$4'}
34-
disabled={tokenParam !== undefined && tokenParam !== coin.token}
35-
disabledStyle={{ opacity: 0.5 }}
3634
href={{
3735
pathname: '/',
3836
query: { token: coin.token },
@@ -141,3 +139,15 @@ const TokenBalance = ({
141139
</Paragraph>
142140
)
143141
}
142+
143+
export const AddInvestmentLink = () => {
144+
const hoverStyles = useHoverStyles()
145+
146+
return (
147+
<LinkableButton circular href="/trade" p="$2" size="$5" hoverStyle={hoverStyles}>
148+
<LinkableButton.Icon>
149+
<IconPlus size="$5" color="$color10" />
150+
</LinkableButton.Icon>
151+
</LinkableButton>
152+
)
153+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { Button, Card, Paragraph, Spinner, Theme, ThemeName, XStack, YStack } from '@my/ui'
2+
import formatAmount from 'app/utils/formatAmount'
3+
4+
import { ChevronLeft, ChevronRight } from '@tamagui/lucide-icons'
5+
6+
import { useIsPriceHidden } from './utils/useIsPriceHidden'
7+
8+
import { useSendAccountBalances } from 'app/utils/useSendAccountBalances'
9+
import { investmentCoins } from 'app/data/coins'
10+
11+
import { useRootScreenParams } from 'app/routers/params'
12+
import { useMultipleTokensMarketData } from 'app/utils/coin-gecko'
13+
import { useMemo } from 'react'
14+
import { IconError } from 'app/components/icons'
15+
import { useCoins } from 'app/provider/coins'
16+
17+
export const InvestmentsBalanceCard = () => {
18+
const [queryParams, setParams] = useRootScreenParams()
19+
const isInvestmentCoin = investmentCoins.some(
20+
(coin) => coin.token.toLowerCase() === queryParams.token
21+
)
22+
const isInvestmentsScreen = queryParams.token === 'investments'
23+
24+
const toggleSubScreen = () =>
25+
setParams(
26+
{ ...queryParams, token: queryParams.token === 'investments' ? undefined : 'investments' },
27+
{ webBehavior: 'push' }
28+
)
29+
30+
const { isPriceHidden } = useIsPriceHidden()
31+
32+
const { dollarBalances, isLoading } = useSendAccountBalances()
33+
const dollarTotal = Object.entries(dollarBalances ?? {})
34+
.filter(([address]) =>
35+
investmentCoins.some((coin) => coin.token.toLowerCase() === address.toLowerCase())
36+
)
37+
.reduce((total, [, balance]) => total + balance, 0)
38+
39+
const formattedBalance = formatAmount(dollarTotal, 9, 0)
40+
41+
return (
42+
<Card py="$5" px="$4" w={'100%'} jc="space-between" onPress={toggleSubScreen}>
43+
<YStack jc={'center'} gap={'$5'} w={'100%'}>
44+
<YStack w={'100%'} gap={'$2.5'} jc="space-between">
45+
<XStack ai={'center'} jc={'space-between'} gap="$2.5" width={'100%'}>
46+
<Paragraph fontSize={'$7'} fontWeight="400">
47+
Invest
48+
</Paragraph>
49+
<Button
50+
chromeless
51+
backgroundColor="transparent"
52+
hoverStyle={{ backgroundColor: 'transparent' }}
53+
pressStyle={{
54+
backgroundColor: 'transparent',
55+
borderColor: 'transparent',
56+
}}
57+
focusStyle={{ backgroundColor: 'transparent' }}
58+
p={0}
59+
height={'auto'}
60+
>
61+
<Button.Icon>
62+
{isInvestmentCoin || isInvestmentsScreen ? (
63+
<ChevronLeft
64+
size={'$1.5'}
65+
color={'$lightGrayTextField'}
66+
$theme-light={{ color: '$darkGrayTextField' }}
67+
$lg={{ display: 'none' }}
68+
/>
69+
) : (
70+
<ChevronRight
71+
size={'$1.5'}
72+
color={'$primary'}
73+
$theme-light={{ color: '$color12' }}
74+
/>
75+
)}
76+
</Button.Icon>
77+
</Button>
78+
</XStack>
79+
</YStack>
80+
<Paragraph fontSize={'$10'} fontWeight={'600'} color={'$color12'}>
81+
{(() => {
82+
switch (true) {
83+
case isPriceHidden:
84+
return '///////'
85+
case isLoading || !dollarBalances:
86+
return <Spinner size={'large'} />
87+
default:
88+
return `$${formattedBalance}`
89+
}
90+
})()}
91+
</Paragraph>
92+
<InvestmentsAggregate />
93+
</YStack>
94+
</Card>
95+
)
96+
}
97+
98+
function InvestmentsAggregate() {
99+
const tokenIds = useCoins()
100+
.investmentCoins.filter((c) => c?.balance && c.balance > 0n)
101+
.map((c) => c.coingeckoTokenId)
102+
103+
const { data: marketData, isLoading, isError } = useMultipleTokensMarketData(tokenIds)
104+
const { investmentsDailyAggregate, sign } = useMemo(() => {
105+
if (!marketData?.length) return { investmentsDailyAggregate: 0, sign: null } as const
106+
107+
// Simple average of percentage changes
108+
const aggregatePercentage =
109+
marketData.reduce((total, coin) => {
110+
// Use 0 if market_cap_change_percentage_24h is null or undefined
111+
return total + (coin?.market_cap_change_percentage_24h ?? 0)
112+
}, 0) / marketData.length
113+
114+
return {
115+
investmentsDailyAggregate: Number(aggregatePercentage.toFixed(2)),
116+
sign: aggregatePercentage > 0 ? 'positive' : aggregatePercentage < 0 ? 'negative' : 'zero',
117+
} as const
118+
}, [marketData])
119+
if (tokenIds.length === 0)
120+
return (
121+
<XStack gap="$2" ai="center">
122+
<Paragraph color="$color10" $gtXs={{ fontSize: 14 }} fontSize={12}>
123+
Add popular crypto assets to your portfolio
124+
</Paragraph>
125+
</XStack>
126+
)
127+
128+
if (isLoading) return <Spinner size="small" />
129+
130+
if (isError)
131+
return (
132+
<XStack gap="$2" ai="center">
133+
<Paragraph color="$color10" $gtXs={{ fontSize: 14 }} fontSize={12}>
134+
Failed to load market data
135+
</Paragraph>
136+
<IconError size="$1.5" color={'$error'} />
137+
</XStack>
138+
)
139+
140+
if (sign === 'positive')
141+
return (
142+
<Theme name={'green_active'}>
143+
<Paragraph fontSize={'$4'}>+{investmentsDailyAggregate}%</Paragraph>
144+
</Theme>
145+
)
146+
if (sign === 'negative')
147+
return (
148+
<Theme name={'red_active'}>
149+
<Paragraph fontSize={'$4'}>{investmentsDailyAggregate}%</Paragraph>
150+
</Theme>
151+
)
152+
return (
153+
<Paragraph fontSize={'$4'} color="$color10">
154+
{investmentsDailyAggregate}%
155+
</Paragraph>
156+
)
157+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Button, Card, Link, Paragraph, Spinner, XStack, YStack } from '@my/ui'
2+
import formatAmount from 'app/utils/formatAmount'
3+
4+
import { ChevronRight } from '@tamagui/lucide-icons'
5+
import { useMemo } from 'react'
6+
import { useSendEarnBalances, useVaultConvertSharesToAssets } from '../earn/hooks'
7+
import { useIsPriceHidden } from './utils/useIsPriceHidden'
8+
import { formatUnits } from 'viem'
9+
10+
export const SavingsBalanceCard = () => {
11+
const { isPriceHidden } = useIsPriceHidden()
12+
const { data: balances, isLoading } = useSendEarnBalances()
13+
// Extract vaults and shares from balances for conversion
14+
const vaults =
15+
balances
16+
?.filter((balance) => balance.shares > 0n && balance.log_addr !== null)
17+
.map((balance) => balance.log_addr) || []
18+
19+
const shares =
20+
balances
21+
?.filter((balance) => balance.shares > 0n && balance.log_addr !== null)
22+
.map((balance) => balance.shares) || []
23+
24+
// Use the hook to get current asset values based on onchain rate
25+
const currentAssets = useVaultConvertSharesToAssets({ vaults, shares })
26+
27+
const totalAssets = useMemo(
28+
() => formatUSDCValue(currentAssets.data?.reduce((sum, assets) => sum + assets, 0n) ?? 0n),
29+
[currentAssets.data]
30+
)
31+
32+
return (
33+
<Card py="$5" px="$4" w={'100%'} jc="space-between">
34+
<Link href={'/earn'}>
35+
<YStack jc={'center'} gap={'$5'} w={'100%'}>
36+
<YStack w={'100%'} gap={'$2.5'} jc="space-between">
37+
<XStack ai={'center'} jc={'space-between'} gap="$2.5" width={'100%'}>
38+
<Paragraph fontSize={'$7'} fontWeight="400">
39+
Savings
40+
</Paragraph>
41+
<Button
42+
chromeless
43+
backgroundColor="transparent"
44+
hoverStyle={{ backgroundColor: 'transparent' }}
45+
pressStyle={{
46+
backgroundColor: 'transparent',
47+
borderColor: 'transparent',
48+
}}
49+
focusStyle={{ backgroundColor: 'transparent' }}
50+
p={0}
51+
height={'auto'}
52+
>
53+
<Button.Icon>
54+
<ChevronRight
55+
size={'$1.5'}
56+
color={'$primary'}
57+
$theme-light={{ color: '$color12' }}
58+
/>
59+
</Button.Icon>
60+
</Button>
61+
</XStack>
62+
</YStack>
63+
<Paragraph fontSize={'$10'} fontWeight={'600'} color={'$color12'}>
64+
{(() => {
65+
switch (true) {
66+
case isPriceHidden:
67+
return '///////'
68+
case isLoading || !balances:
69+
return <Spinner size={'large'} />
70+
default:
71+
return `$${totalAssets}`
72+
}
73+
})()}
74+
</Paragraph>
75+
<Paragraph fontSize={'$4'} color={'$color10'}>
76+
Up to 12% interest
77+
</Paragraph>
78+
</YStack>
79+
</Link>
80+
</Card>
81+
)
82+
}
83+
84+
const formatUSDCValue = (value: bigint): string => {
85+
const valueInUSDC = Number(formatUnits(value, 6))
86+
return formatAmount(valueInUSDC, 9, 0)
87+
}

0 commit comments

Comments
 (0)