Skip to content

Commit

Permalink
chore(portfolio): wip wire up
Browse files Browse the repository at this point in the history
  • Loading branch information
stackchain committed May 4, 2024
1 parent b4259a5 commit 9d30546
Show file tree
Hide file tree
Showing 18 changed files with 392 additions and 46 deletions.
1 change: 1 addition & 0 deletions apps/wallet-mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
"expo-blur": "12.9.2",
"expo-camera": "^13.2.1",
"expo-device": "^5.4.0",
"expo-image": "^1.10.6",
"expo-status-bar": "~1.4.4",
"immer": "^10.0.2",
"jsc-android": "241213.1.0",
Expand Down
21 changes: 9 additions & 12 deletions apps/wallet-mobile/src/components/AmountItem/AmountItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useTheme} from '@yoroi/theme'
import {Portfolio} from '@yoroi/types'
import {Balance} from '@yoroi/types'
import {Swap} from '@yoroi/types'
import * as React from 'react'
import {StyleSheet, View, ViewProps} from 'react-native'
Expand All @@ -8,16 +8,16 @@ import {usePriceImpactRiskTheme} from '../../features/Swap/common/helpers'
import {SwapPriceImpactRisk} from '../../features/Swap/common/types'
import {isEmptyString} from '../../utils'
import {YoroiWallet} from '../../yoroi-wallets/cardano/types'
import {useTokenInfo} from '../../yoroi-wallets/hooks'
import {Quantities} from '../../yoroi-wallets/utils'
import {Boundary, Icon, Spacer, Text, TokenIcon, TokenIconPlaceholder} from '..'
import {PairedBalance} from '../PairedBalance/PairedBalance'
import { infoExtractName } from '@yoroi/portfolio'

export type AmountItemProps = {
wallet: YoroiWallet
amount: Portfolio.Token.Amount
amount: Balance.Amount
style?: ViewProps['style']
isPrivacyOff?: boolean
status?: string
inWallet?: boolean
variant?: 'swap'
priceImpactRisk?: SwapPriceImpactRisk
Expand All @@ -35,12 +35,13 @@ export const AmountItem = ({
orderType,
}: AmountItemProps) => {
const {styles, colors} = useStyles()
const {quantity, tokenId} = amount
const tokenInfo = useTokenInfo({wallet, tokenId})

const {info, quantity} = amount
const isPrimary = info.nature === 'primary'
const name = infoExtractName(info)
const isPrimary = tokenInfo.id === wallet.primaryTokenInfo.id
const name = tokenInfo.ticker ?? tokenInfo.name
const nameLabel = isEmptyString(name) ? '-' : name
const detail = isPrimary ? info.: tokenInfo.fingerprint
const detail = isPrimary ? tokenInfo.description : tokenInfo.fingerprint

const formattedQuantity = Quantities.format(quantity, tokenInfo.decimals ?? 0)
const showSwapDetails = !isPrimary && variant === 'swap'
Expand Down Expand Up @@ -87,10 +88,6 @@ export const AmountItem = ({
</Text>
</View>
)}

{isPrimary && variant !== 'swap' && (
<PairedBalance isPrivacyOff={isPrivacyOff} amount={{quantity, tokenId: tokenInfo.id}} />
)}
</Right>
</View>
)
Expand Down
103 changes: 103 additions & 0 deletions apps/wallet-mobile/src/components/TokenAmountItem/AmountIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {isString} from '@yoroi/common'
import {useTheme} from '@yoroi/theme'
import {Chain, Portfolio} from '@yoroi/types'
import {Image} from 'expo-image'
import React from 'react'
import {StyleSheet, View} from 'react-native'

import {Icon} from '../Icon'

const headers = {
Accept: 'image/webp',
} as const

const blurhash =
'|rF?hV%2WCj[ayj[a|j[az_NaeWBj@ayfRayfQfQM{M|azj[azf6fQfQfQIpWXofj[ayj[j[fQayWCoeoeaya}j[ayfQa{oLj?j[WVj[ayayj[fQoff7azayj[ayj[j[ayofayayayj[fQj[ayayj[ayfjj[j[ayjuayj['

type AmountIconProps = {
info: Portfolio.Token.Info
isMainnet?: boolean
size: 'sm' | 'md'
}
export const AmountIcon = React.memo(({info, isMainnet, size = 'md'}: AmountIconProps) => {
const {styles} = useStyles()

if (info.nature === Portfolio.Token.Nature.Primary) return <PrimaryIcon size={size} />

if (isString(info.icon) && info.icon.length > 0) {
return (
<Image
source={{uri: `data:image/png;base64,${info.icon}`}}
style={[size === 'sm' ? styles.iconSmall : styles.iconMedium]}
placeholder={blurhash}
/>
)
}

const [policy, name] = info.id.split('.')
const network = isMainnet ? Chain.Network.Mainnet : Chain.Network.Preprod
const uri = `https://${network}.processed-media.yoroiwallet.com/${policy}/${name}?width=64&height=64&kind=metadata&fit=cover`

return (
<Image source={{uri, headers}} contentFit="cover" style={[size === 'sm' ? styles.iconSmall : styles.iconMedium]} />
)
})

const PrimaryIcon = ({size = 'md'}: {size?: 'sm' | 'md'}) => {
const {styles} = useStyles()
return (
<View style={[size === 'sm' ? styles.iconSmall : styles.iconMedium, styles.primary]}>
<Icon.Cardano color="white" height={size === 'sm' ? 20 : 35} width={size === 'sm' ? 20 : 35} />
</View>
)
}

export const TokenIconPlaceholder = ({size = 'md'}: {size?: 'sm' | 'md'}) => {
const {styles, colors} = useStyles()
return (
<View style={[styles.iconMedium, styles.placeholder, size === 'sm' && styles.placeholderSmall]}>
<Icon.Tokens color={colors.icon} size={35} />
</View>
)
}

const useStyles = () => {
const {theme} = useTheme()
const {color} = theme
const styles = StyleSheet.create({
primary: {
backgroundColor: color.primary[600],
},
iconMedium: {
backgroundColor: 'transparent',
width: 40,
height: 40,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
iconSmall: {
backgroundColor: 'transparent',
width: 24,
height: 24,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
placeholder: {
backgroundColor: color.gray[100],
},
placeholderSmall: {
width: 24,
height: 24,
},
})

const colors = {
icon: color.gray[600],
}

return {styles, colors}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {storiesOf} from '@storybook/react-native'
import {tokenMocks} from '@yoroi/portfolio'
import React from 'react'
import {Text, View} from 'react-native'

import {QueryProvider} from '../../../.storybook/decorators'
import {SelectedWalletProvider} from '../../features/WalletManager/Context'
import {mocks} from '../../yoroi-wallets/mocks'
import {Spacer} from '..'
import {AmountItem} from './AmountItem'

const primaryAmount = tokenMocks.primaryETH.balance
const secondaryAmount = tokenMocks.nftCryptoKitty.balance

storiesOf('AmountItem', module).add('Gallery', () => (
<QueryProvider>
<SelectedWalletProvider wallet={mocks.wallet}>
<View style={{flex: 1, justifyContent: 'center', padding: 16}}>
<Text>Fungible primary token</Text>

<AmountItem
privacyPlaceholder="-"
wallet={mocks.wallet}
amount={primaryAmount}
style={{backgroundColor: 'white', padding: 16, borderRadius: 8}}
/>

<Spacer height={40} />

<Text>Fungible non-primary token</Text>

<AmountItem
privacyPlaceholder="-"
wallet={mocks.wallet}
amount={secondaryAmount}
style={{backgroundColor: 'white', padding: 16, borderRadius: 8}}
/>
</View>
</SelectedWalletProvider>
</QueryProvider>
))
178 changes: 178 additions & 0 deletions apps/wallet-mobile/src/components/TokenAmountItem/AmountItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {amountFormatter, infoExtractName} from '@yoroi/portfolio'
import {useTheme} from '@yoroi/theme'
import {Portfolio} from '@yoroi/types'
import {Swap} from '@yoroi/types'
import * as React from 'react'
import {StyleSheet, View, ViewProps} from 'react-native'

import {usePriceImpactRiskTheme} from '../../features/Swap/common/helpers'
import {SwapPriceImpactRisk} from '../../features/Swap/common/types'
import {YoroiWallet} from '../../yoroi-wallets/cardano/types'
import {Icon, Spacer, Text} from '..'
import {PairedBalance} from '../PairedBalance/PairedBalance'
import {AmountIcon} from './AmountIcon'

export type AmountItemProps = {
wallet: YoroiWallet
amount: Portfolio.Token.Amount
privacyPlaceholder: string
style?: ViewProps['style']
isPrivacyOff?: boolean
inWallet?: boolean
variant?: 'swap'
priceImpactRisk?: SwapPriceImpactRisk
orderType?: Swap.OrderType
isMainnet?: boolean
}

export const AmountItem = ({
isMainnet,
isPrivacyOff,
privacyPlaceholder,
style,
amount,
inWallet,
variant,
priceImpactRisk,
orderType,
}: AmountItemProps) => {
const {styles, colors} = useStyles()
const priceImpactRiskTheme = usePriceImpactRiskTheme(priceImpactRisk ?? 'none')

const {info} = amount
const isPrimary = info.nature === 'primary'
const name = infoExtractName(info)
const detail = isPrimary ? info.description : info.fingerprint

const formattedQuantity = !isPrivacyOff ? amountFormatter()(amount) : privacyPlaceholder

const showSwapDetails = !isPrimary && variant === 'swap'
const priceImpactRiskTextColor = orderType === 'market' ? priceImpactRiskTheme.text : colors.text

return (
<View style={[style, styles.container]} testID="assetItem">
<Left>
<AmountIcon info={amount.info} size={variant === 'swap' ? 'sm' : 'md'} isMainnet={isMainnet} />
</Left>

<Middle>
<View style={styles.row}>
<Text numberOfLines={1} ellipsizeMode="middle" style={styles.name} testID="tokenInfoText">
{name}
</Text>

{showSwapDetails && (
<>
<Spacer width={4} />

{inWallet && <Icon.Portfolio size={22} color={colors.icon} />}
</>
)}
</View>

<Text numberOfLines={1} ellipsizeMode="middle" style={styles.detail} testID="tokenFingerprintText">
{detail}
</Text>
</Middle>

<Right>
{info.type !== Portfolio.Token.Type.NFT && variant !== 'swap' && (
<View style={styles.row} testID="tokenAmountText">
{priceImpactRisk === 'moderate' && <Icon.Info size={24} color={priceImpactRiskTextColor} />}

{priceImpactRisk === 'high' && <Icon.Warning size={24} color={priceImpactRiskTextColor} />}

<Text style={[styles.quantity, {color: priceImpactRiskTextColor}]}>{formattedQuantity}</Text>
</View>
)}

{isPrimary && variant !== 'swap' && (
<PairedBalance isPrivacyOff={isPrivacyOff} amount={amount} privacyPlaceholder={privacyPlaceholder} />
)}
</Right>
</View>
)
}

const Left = ({style, ...props}: ViewProps) => <View style={style} {...props} />
const Middle = ({style, ...props}: ViewProps) => (
<View style={[style, {flex: 1, justifyContent: 'center', paddingHorizontal: 8}]} {...props} />
)
const Right = ({style, ...props}: ViewProps) => <View style={style} {...props} />

export const AmountItemPlaceholder = ({style}: ViewProps) => {
const {colors} = useStyles()
return (
<View
style={[
style,
{
display: 'flex',
flexDirection: 'row',
gap: 12,
height: 56,
},
]}
>
<View
style={{
backgroundColor: colors.background,
borderRadius: 8,
flexGrow: 3,
}}
/>

<View
style={{
backgroundColor: colors.background,
borderRadius: 8,
flexGrow: 1,
}}
/>
</View>
)
}

const useStyles = () => {
const {theme} = useTheme()
const {color, typography} = theme
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
name: {
color: color.gray[900],
fontSize: 16,
lineHeight: 22,
fontWeight: '500',
fontFamily: 'Rubik-Medium',
},
detail: {
color: color.gray[600],
fontSize: 12,
lineHeight: 18,
maxWidth: 140,
},
quantity: {
color: color.gray[900],
...typography['body-1-l-regular'],
textAlign: 'right',
flexGrow: 1,
},
row: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
})

const colors = {
text: color.gray[900],
background: color.gray[200],
icon: color.secondary[600],
}

return {styles, colors}
}

0 comments on commit 9d30546

Please sign in to comment.