Skip to content

Commit deb0249

Browse files
authored
[PE-6005] Add mobile wallet UI (#11890)
1 parent b1bdf67 commit deb0249

File tree

13 files changed

+413
-103
lines changed

13 files changed

+413
-103
lines changed

packages/common/src/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ export * from './content'
4444
export * from './chats'
4545
export * from './useSupporterPrompt'
4646
export * from './useRemixCountdown'
47+
export * from './useFormattedUSDCBalance'
48+
export * from './useFormattedAudioBalance'
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useMemo } from 'react'
2+
3+
import { AUDIO } from '@audius/fixed-decimal'
4+
5+
import { useTokenPrice } from '../api'
6+
import { TOKEN_LISTING_MAP } from '../store'
7+
import { formatWei, isNullOrUndefined } from '../utils'
8+
9+
import { useTotalBalanceWithFallback } from './useAudioBalance'
10+
11+
// AUDIO token address from Jupiter
12+
const AUDIO_TOKEN_ID = TOKEN_LISTING_MAP.AUDIO.address
13+
14+
type UseFormattedAudioBalanceReturn = {
15+
audioBalance: ReturnType<typeof useTotalBalanceWithFallback>
16+
audioBalanceFormatted: string
17+
isAudioBalanceLoading: boolean
18+
audioPrice: string | null
19+
audioDollarValue: string
20+
isAudioPriceLoading: boolean
21+
}
22+
23+
export const useFormattedAudioBalance = (): UseFormattedAudioBalanceReturn => {
24+
const audioBalance = useTotalBalanceWithFallback()
25+
const audioBalanceFormatted = formatWei(audioBalance, true, 0)
26+
const isAudioBalanceLoading = isNullOrUndefined(audioBalance)
27+
28+
const { data: audioPriceData, isPending: isAudioPriceLoading } =
29+
useTokenPrice(AUDIO_TOKEN_ID)
30+
const audioPrice = audioPriceData?.price || null
31+
32+
// Calculate dollar value of user's AUDIO balance
33+
const audioDollarValue = useMemo(() => {
34+
if (!audioPrice || !audioBalance) return '$0.00'
35+
36+
const priceNumber = parseFloat(audioPrice)
37+
const balanceValue = parseFloat(AUDIO(audioBalance).toString())
38+
const totalValue = priceNumber * balanceValue
39+
40+
return `$${totalValue.toFixed(2)} ($${parseFloat(audioPrice).toFixed(4)})`
41+
}, [audioBalance, audioPrice])
42+
43+
return {
44+
audioBalance,
45+
audioBalanceFormatted,
46+
isAudioBalanceLoading,
47+
audioPrice,
48+
audioDollarValue,
49+
isAudioPriceLoading
50+
}
51+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useMemo } from 'react'
2+
3+
import { USDC } from '@audius/fixed-decimal'
4+
import BN from 'bn.js'
5+
6+
import { BNUSDC, Status } from '../models'
7+
8+
import { useUSDCBalance } from './useUSDCBalance'
9+
10+
type UseFormattedUSDCBalanceReturn = {
11+
balance: BNUSDC | null
12+
balanceFormatted: string
13+
balanceCents: number
14+
usdcValue: ReturnType<typeof USDC>
15+
isLoading: boolean
16+
status: Status
17+
}
18+
19+
export const useFormattedUSDCBalance = (): UseFormattedUSDCBalanceReturn => {
20+
const { data: balance, status: balanceStatus } = useUSDCBalance()
21+
const usdcValue = USDC(balance ?? new BN(0)).floor(2)
22+
const balanceCents = Number(usdcValue.floor(2).toString()) * 100
23+
const balanceFormatted = useMemo(() => {
24+
return balanceStatus === Status.LOADING
25+
? '0.00'
26+
: usdcValue.toFixed(2).replace('$', '')
27+
}, [usdcValue, balanceStatus])
28+
29+
const isLoading = balanceStatus === Status.LOADING
30+
31+
return {
32+
balance,
33+
balanceFormatted,
34+
balanceCents,
35+
usdcValue,
36+
isLoading,
37+
status: balanceStatus
38+
}
39+
}

packages/common/src/messages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './trackPage'
99
export * from './notifications'
1010
export * from './buySell'
1111
export * from './remixes'
12+
export * from './walletMessages'
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const walletMessages = {
2+
// CashWallet messages
3+
cashBalance: 'Cash Balance',
4+
withdraw: 'Withdraw',
5+
addFunds: 'Add Cash',
6+
cashBalanceTooltip:
7+
'Your cash balance is stored as USDC in your built-in wallet',
8+
usdc: 'USDC',
9+
builtInWallet: 'Built-In Wallet',
10+
payoutWallet: 'Payout Wallet',
11+
transactionHistory: 'Transaction History',
12+
13+
// YourCoins messages
14+
yourCoins: 'Your Coins',
15+
loading: '-- $AUDIO',
16+
dollarZero: '$0.00',
17+
loadingPrice: '$0.00 (loading...)',
18+
buySell: 'Buy/Sell'
19+
}

packages/mobile/src/screens/app-drawer-screen/left-nav-drawer/useNavConfig.tsx

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useMemo, type ReactNode } from 'react'
33
import {
44
useAccountHasClaimableRewards,
55
useChallengeCooldownSchedule,
6+
useFeatureFlag,
67
useSelectTierInfo,
78
useTotalBalanceWithFallback,
89
useUSDCBalance
@@ -31,12 +32,13 @@ import {
3132
IconCloudUpload,
3233
IconUser,
3334
IconGift,
35+
IconWallet,
3436
useTheme,
3537
NotificationCount
3638
} from '@audius/harmony-native'
3739
import LogoUSDC from 'app/assets/images/logoUSDC.svg'
3840
import { IconAudioBadge } from 'app/components/audio-rewards'
39-
import { useFeatureFlag, useRemoteVar } from 'app/hooks/useRemoteConfig'
41+
import { useRemoteVar } from 'app/hooks/useRemoteConfig'
4042
import type { AppTabScreenParamList } from 'app/screens/app-screen'
4143
import { make } from 'app/services/analytics'
4244
import { env } from 'app/services/env'
@@ -52,6 +54,7 @@ const messages = {
5254
upload: 'Upload',
5355
settings: 'Settings',
5456
featureFlags: 'Feature Flags',
57+
wallet: 'Wallet',
5558
usdcDollarSign: (balance: string) => `$${balance}`
5659
}
5760

@@ -70,6 +73,9 @@ export const useNavConfig = () => {
7073
const { isEnabled: isFeatureFlagAccessEnabled } = useFeatureFlag(
7174
FeatureFlags.FEATURE_FLAG_ACCESS
7275
)
76+
const { isEnabled: isWalletUIUpdateEnabled } = useFeatureFlag(
77+
FeatureFlags.WALLET_UI_UPDATE
78+
)
7379
const challengeRewardIds = useRemoteVar(StringKeys.CHALLENGE_REWARD_IDS)
7480
const hasClaimableRewards = useAccountHasClaimableRewards(challengeRewardIds)
7581
const hasUnreadMessages = useSelector(getHasUnreadMessages)
@@ -111,31 +117,45 @@ export const useNavConfig = () => {
111117
unreadMessagesCount > 0 ? (
112118
<NotificationCount count={unreadMessagesCount} />
113119
) : undefined
114-
},
115-
{
116-
icon: IconCrown,
117-
label: messages.audio,
118-
to: 'AudioScreen',
119-
rightIcon: (
120-
<BalancePill
121-
balance={audioBalanceFormatted}
122-
icon={<IconAudioBadge tier={tier} showNoTier size='m' />}
123-
isLoading={isAudioBalanceLoading}
124-
/>
125-
)
126-
},
127-
{
128-
icon: IconDonate,
129-
label: messages.usdc,
130-
to: 'PayAndEarnScreen',
131-
rightIcon: (
132-
<BalancePill
133-
balance={messages.usdcDollarSign(usdcBalanceFormatted)}
134-
icon={<LogoUSDC height={spacing.unit5} width={spacing.unit5} />}
135-
isLoading={isUSDCBalanceLoading}
136-
/>
137-
)
138-
},
120+
}
121+
]
122+
123+
if (isWalletUIUpdateEnabled) {
124+
items.push({
125+
icon: IconWallet,
126+
label: messages.wallet,
127+
to: 'wallet'
128+
})
129+
} else {
130+
items.push(
131+
{
132+
icon: IconCrown,
133+
label: messages.audio,
134+
to: 'AudioScreen',
135+
rightIcon: (
136+
<BalancePill
137+
balance={audioBalanceFormatted}
138+
icon={<IconAudioBadge tier={tier} showNoTier size='m' />}
139+
isLoading={isAudioBalanceLoading}
140+
/>
141+
)
142+
},
143+
{
144+
icon: IconDonate,
145+
label: messages.usdc,
146+
to: 'PayAndEarnScreen',
147+
rightIcon: (
148+
<BalancePill
149+
balance={messages.usdcDollarSign(usdcBalanceFormatted)}
150+
icon={<LogoUSDC height={spacing.unit5} width={spacing.unit5} />}
151+
isLoading={isUSDCBalanceLoading}
152+
/>
153+
)
154+
}
155+
)
156+
}
157+
158+
items.push(
139159
{
140160
icon: IconGift,
141161
label: messages.rewards,
@@ -155,7 +175,7 @@ export const useNavConfig = () => {
155175
label: messages.settings,
156176
to: 'SettingsScreen'
157177
}
158-
]
178+
)
159179

160180
if (env.ENVIRONMENT === 'staging' || isFeatureFlagAccessEnabled) {
161181
items.push({
@@ -177,7 +197,8 @@ export const useNavConfig = () => {
177197
usdcBalanceFormatted,
178198
isUSDCBalanceLoading,
179199
hasClaimableRewards,
180-
isFeatureFlagAccessEnabled
200+
isFeatureFlagAccessEnabled,
201+
isWalletUIUpdateEnabled
181202
])
182203

183204
return {

packages/mobile/src/screens/app-screen/AppTabScreen.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
TopSupportersScreen,
5858
SupportingUsersScreen
5959
} from 'app/screens/user-list-screen'
60+
import { WalletScreen } from 'app/screens/wallet-screen'
6061

6162
import { useAppScreenOptions } from './useAppScreenOptions'
6263

@@ -108,6 +109,7 @@ export type AppTabScreenParamList = {
108109
PayAndEarnScreen: undefined
109110
AudioScreen: undefined
110111
RewardsScreen: undefined
112+
wallet: undefined
111113
Upload: {
112114
initialMetadata?: Partial<TrackMetadataForUpload>
113115
}
@@ -226,6 +228,7 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => {
226228
<Stack.Screen name='PayAndEarnScreen' component={PayAndEarnScreen} />
227229
<Stack.Screen name='AudioScreen' component={AudioScreen} />
228230
<Stack.Screen name='RewardsScreen' component={RewardsScreen} />
231+
<Stack.Screen name='wallet' component={WalletScreen} />
229232

230233
<Stack.Group>
231234
<Stack.Screen name='EditProfile' component={EditProfileScreen} />
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react'
2+
3+
import { IconWallet } from '@audius/harmony-native'
4+
import { ScrollView, Screen, ScreenContent } from 'app/components/core'
5+
import { makeStyles } from 'app/styles'
6+
7+
import { CashWallet } from './components/CashWallet'
8+
import { YourCoins } from './components/YourCoins'
9+
10+
const messages = {
11+
title: 'WALLET'
12+
}
13+
14+
const useStyles = makeStyles(({ spacing }) => ({
15+
root: {
16+
paddingVertical: spacing(8),
17+
paddingHorizontal: spacing(3)
18+
},
19+
container: {
20+
gap: spacing(4)
21+
}
22+
}))
23+
24+
export const WalletScreen = () => {
25+
const styles = useStyles()
26+
return (
27+
<Screen
28+
url='/wallet'
29+
variant='secondary'
30+
title={messages.title}
31+
style={styles.root}
32+
icon={IconWallet}
33+
>
34+
<ScreenContent>
35+
<ScrollView contentContainerStyle={styles.container}>
36+
<CashWallet />
37+
<YourCoins />
38+
</ScrollView>
39+
</ScreenContent>
40+
</Screen>
41+
)
42+
}

0 commit comments

Comments
 (0)