Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(assets): add token details screen #4266

Merged
merged 9 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1844,5 +1844,18 @@
"notificationCenterSpotlight": {
"message": "Introducing a new way to claim rewards, view alerts, and see updates in one place",
"cta": "Got it"
},
"tokenDetails": {
"yourBalance": "Your balance",
"learnMore": "Learn more about {{tokenName}}",
"priceUnavailable": "Price Unavailable",
"priceDeltaSuffix": "Today",
"actions": {
"send": "Send",
"swap": "Swap",
"add": "Add",
"withdraw": "Withdraw",
"more": "More"
}
}
}
2 changes: 2 additions & 0 deletions src/analytics/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,8 @@ export enum AssetsEvents {
view_dapp_positions = 'view_dapp_positions',
tap_asset = 'tap_asset',
tap_claim_rewards = 'tap_claim_rewards',
tap_token_details_action = 'tap_token_details_action',
tap_token_details_learn_more = 'tap_token_details_learn_more',
}

export enum NftEvents {
Expand Down
21 changes: 15 additions & 6 deletions src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { NotificationReceiveState } from 'src/notifications/types'
import { AdventureCardName } from 'src/onboarding/types'
import { RecipientType } from 'src/recipients/recipient'
import { Field } from 'src/swap/types'
import { TokenDetailsActionName } from 'src/tokens/types'
import { NetworkId } from 'src/transactions/types'
import { AnalyticsCurrency, CiCoCurrency, Currency } from 'src/utils/currencies'
import { Awaited } from 'src/utils/typescript'
Expand Down Expand Up @@ -1320,6 +1321,14 @@ interface TokenBottomSheetEventsProperties {
}
}

export interface TokenProperties {
symbol: string
address: string | null
balanceUsd: number
networkId: NetworkId
tokenId: string
}

interface AssetsEventsProperties {
[AssetsEvents.show_asset_balance_info]: undefined
[AssetsEvents.view_wallet_assets]: undefined
Expand All @@ -1342,16 +1351,16 @@ interface AssetsEventsProperties {
description: string
balanceUsd: number
}
| {
| ({
assetType: 'token'
address?: string
networkId: NetworkId
tokenId: string
title: string // Example: 'cUSD'
description: string
balanceUsd: number
}
} & TokenProperties)
[AssetsEvents.tap_claim_rewards]: undefined
[AssetsEvents.tap_token_details_action]: {
action: TokenDetailsActionName
} & TokenProperties
[AssetsEvents.tap_token_details_learn_more]: TokenProperties
}

interface NftsEventsProperties {
Expand Down
2 changes: 2 additions & 0 deletions src/analytics/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,8 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
[AssetsEvents.view_dapp_positions]: `When a user taps on the "Dapp Positions" segmented control or tab`,
[AssetsEvents.tap_asset]: `When a user taps on an asset`,
[AssetsEvents.tap_claim_rewards]: `When a user taps on the "Claim Rewards" button`,
[AssetsEvents.tap_token_details_action]: `When a user taps one of the actions on the token details screen`,
[AssetsEvents.tap_token_details_learn_more]: `When a user taps the learn more link on the token details screen`,
[NftEvents.nft_error_screen_open]: `When the high level error screen is mounted`,
[NftEvents.nft_media_load]: `When attempting to load NFT media`,
[NftEvents.nft_gallery_screen_open]: `When the gallery screen is mounted`,
Expand Down
6 changes: 4 additions & 2 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { ReactNode, useCallback } from 'react'
import { ActivityIndicator, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'
import Touchable from 'src/components/Touchable'
import colors, { Colors } from 'src/styles/colors'
import fontStyles from 'src/styles/fonts'
import fontStyles, { typeScale } from 'src/styles/fonts'
import { vibrateInformative } from 'src/styles/hapticFeedback'

const BUTTON_TAP_DEBOUNCE_TIME = 300 // milliseconds
Expand Down Expand Up @@ -42,6 +42,7 @@ export interface ButtonProps {
testID?: string
touchableStyle?: StyleProp<ViewStyle>
iconMargin?: number
fontStyle?: typeof typeScale
}

export default React.memo(function Button(props: ButtonProps) {
Expand All @@ -60,6 +61,7 @@ export default React.memo(function Button(props: ButtonProps) {
loadingColor,
touchableStyle,
iconMargin = 4,
fontStyle = fontStyles.regular600,
} = props

// Debounce onPress event so that it is called once on trigger and
Expand Down Expand Up @@ -103,7 +105,7 @@ export default React.memo(function Button(props: ButtonProps) {
maxFontSizeMultiplier={1}
accessibilityLabel={accessibilityLabel}
style={{
...fontStyles.regular600,
...fontStyle,
color: textColor,
marginLeft: icon && iconPositionLeft ? iconMargin : 0,
marginRight: icon && !iconPositionLeft ? iconMargin : 0,
Expand Down
3 changes: 0 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,6 @@ export const WALLET_BALANCE_UPPER_BOUND = new BigNumber('1e10')

export const TIME_UNTIL_TOKEN_INFO_BECOMES_STALE = 12 * ONE_HOUR_IN_MILLIS

// The amount of time
export const TIME_OF_SUPPORTED_UNSYNC_HISTORICAL_PRICES = ONE_HOUR_IN_MILLIS

export const DEFAULT_FORNO_URL =
DEFAULT_TESTNET === 'mainnet'
? 'https://forno.celo.org/'
Expand Down
15 changes: 15 additions & 0 deletions src/icons/ArrowRightThick.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react'
import Svg, { Path } from 'react-native-svg'
import colors from 'src/styles/colors'

interface Props {
color?: colors
}

const ArrowRightThick = ({ color = colors.gray3 }: Props) => (
<Svg width={24} height={24} viewBox="0 0 24 24" fill="none">
<Path d="M14 18L12.6 16.55L16.15 13H4V11H16.15L12.6 7.45L14 6L20 12L14 18Z" fill={color} />
</Svg>
)

export default React.memo(ArrowRightThick)
15 changes: 15 additions & 0 deletions src/icons/DataDown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react'
import Svg, { Path } from 'react-native-svg'
import colors from 'src/styles/colors'

interface Props {
color?: colors
}

const DataDown = ({ color = colors.warning }: Props) => (
<Svg width={10} height={6} viewBox="0 0 10 6" fill="none">
<Path d="M5 6L10 -9.53674e-07L4.29138e-07 -7.94466e-08L5 6Z" fill={color} />
</Svg>
)

export default React.memo(DataDown)
15 changes: 15 additions & 0 deletions src/icons/DataUp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react'
import Svg, { Path } from 'react-native-svg'
import colors from 'src/styles/colors'

interface Props {
color?: colors
}

const DataUp = ({ color = colors.greenUI }: Props) => (
<Svg width={10} height={6} viewBox="0 0 10 6" fill="none">
<Path d="M5 0L0 6H10L5 0Z" fill={color} />
</Svg>
)

export default React.memo(DataUp)
19 changes: 19 additions & 0 deletions src/icons/quick-actions/More.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react'
import Svg, { Path } from 'react-native-svg'
import Colors from 'src/styles/colors'

interface Props {
color: Colors
}

const QuickActionsMore = ({ color }: Props) => (
<Svg width={25} height={24} viewBox="0 0 25 24" fill="none">
<Path
d="M4.83325 12C4.83325 11.1761 5.50939 10.5 6.33325 10.5C7.15711 10.5 7.83325 11.1761 7.83325 12C7.83325 12.8239 7.15711 13.5 6.33325 13.5C5.50939 13.5 4.83325 12.8239 4.83325 12ZM16.8333 12C16.8333 11.1761 17.5094 10.5 18.3333 10.5C19.1571 10.5 19.8333 11.1761 19.8333 12C19.8333 12.8239 19.1571 13.5 18.3333 13.5C17.5094 13.5 16.8333 12.8239 16.8333 12ZM10.8333 12C10.8333 11.1761 11.5094 10.5 12.3333 10.5C13.1571 10.5 13.8333 11.1761 13.8333 12C13.8333 12.8239 13.1571 13.5 12.3333 13.5C11.5094 13.5 10.8333 12.8239 10.8333 12Z"
fill={color}
stroke={color}
/>
</Svg>
)

export default React.memo(QuickActionsMore)
6 changes: 6 additions & 0 deletions src/navigator/Navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import SwapReviewScreen from 'src/swap/SwapReviewScreen'
import SwapScreen from 'src/swap/SwapScreen'
import AssetsScreen from 'src/tokens/Assets'
import TokenBalancesScreen from 'src/tokens/TokenBalances'
import TokenDetailsScreen from 'src/tokens/TokenDetails'
import TransactionDetailsScreen from 'src/transactions/feed/TransactionDetailsScreen'
import Logger from 'src/utils/Logger'
import { ExtractProps } from 'src/utils/typescript'
Expand Down Expand Up @@ -560,6 +561,11 @@ const assetScreens = (Navigator: typeof Stack) => (
component={AssetsScreen}
options={AssetsScreen.navigationOptions}
/>
<Navigator.Screen
name={Screens.TokenDetails}
component={TokenDetailsScreen}
options={TokenDetailsScreen.navigationOptions}
/>
</>
)

Expand Down
1 change: 1 addition & 0 deletions src/navigator/Screens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export enum Screens {
SwapExecuteScreen = 'SwapExecuteScreen',
SwapReviewScreen = 'SwapReviewScreen',
TokenBalances = 'TokenBalances',
TokenDetails = 'TokenDetails',
TransactionDetailsScreen = 'TransactionDetailsScreen',
UpgradeScreen = 'UpgradeScreen',
ValidateRecipientAccount = 'ValidateRecipientAccount',
Expand Down
1 change: 1 addition & 0 deletions src/navigator/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export type StackParamList = {
[Screens.SwapExecuteScreen]: undefined
[Screens.SwapReviewScreen]: undefined
[Screens.SwapScreenWithBack]: { fromTokenId: string } | undefined
[Screens.TokenDetails]: { tokenId: string }
[Screens.TransactionDetailsScreen]: {
transaction: TokenTransaction
}
Expand Down
2 changes: 1 addition & 1 deletion src/styles/fonts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const typeScale = StyleSheet.create({
lineHeight: 28,
},
labelLarge: {
fontFamily: Inter.Bold,
fontFamily: Inter.Medium,
fontSize: 18,
lineHeight: 28,
},
Expand Down
15 changes: 3 additions & 12 deletions src/tokens/AssetItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import { Image, StyleSheet, Text, View } from 'react-native'
import { TouchableWithoutFeedback } from 'react-native-gesture-handler'
import { AssetsEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import LegacyTokenDisplay from 'src/components/LegacyTokenDisplay'
import PercentageIndicator from 'src/components/PercentageIndicator'
import TokenDisplay from 'src/components/TokenDisplay'
import LegacyTokenDisplay from 'src/components/LegacyTokenDisplay'
import { TIME_OF_SUPPORTED_UNSYNC_HISTORICAL_PRICES } from 'src/config'
import { Position } from 'src/positions/types'
import Colors from 'src/styles/colors'
import fontStyles from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import { TokenBalance } from 'src/tokens/slice'
import { isHistoricalPriceUpdated } from 'src/tokens/utils'
import { Currency } from 'src/utils/currencies'
import { ONE_DAY_IN_MILLIS } from 'src/utils/time'

export const PositionItem = ({ position }: { position: Position }) => {
const balanceInDecimal =
Expand Down Expand Up @@ -85,14 +84,6 @@ export const TokenBalanceItem = ({
token: TokenBalance
showPriceChangeIndicatorInBalances: boolean
}) => {
const isHistoricalPriceUpdated = () => {
return (
token.historicalPricesUsd?.lastDay &&
TIME_OF_SUPPORTED_UNSYNC_HISTORICAL_PRICES >
Math.abs(token.historicalPricesUsd.lastDay.at - (Date.now() - ONE_DAY_IN_MILLIS))
)
}

const onPress = () => {
ValoraAnalytics.track(AssetsEvents.tap_asset, {
assetType: 'token',
Expand Down Expand Up @@ -130,7 +121,7 @@ export const TokenBalanceItem = ({
<View style={styles.tokenContainer}>
{showPriceChangeIndicatorInBalances &&
token.historicalPricesUsd &&
isHistoricalPriceUpdated() && (
isHistoricalPriceUpdated(token) && (
<PercentageIndicator
testID={`percentageIndicator:${token.symbol}`}
comparedValue={token.historicalPricesUsd.lastDay.price}
Expand Down
23 changes: 23 additions & 0 deletions src/tokens/Assets.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { fireEvent, render } from '@testing-library/react-native'
import * as React from 'react'
import { Provider } from 'react-redux'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { getFeatureGate } from 'src/statsig'
Expand Down Expand Up @@ -81,6 +82,10 @@ const storeWithPositionsAndClaimableRewards = {
}

describe('AssetsScreen', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('renders tokens and collectibles tabs when positions is disabled', () => {
jest.mocked(getFeatureGate).mockReturnValue(false)
const store = createMockStore(storeWithPositions)
Expand Down Expand Up @@ -198,6 +203,24 @@ describe('AssetsScreen', () => {
expect(queryAllByTestId('PositionItem')).toHaveLength(0)
})

it('clicking a token navigates to the token details screen and fires analytics event', () => {
jest.mocked(getFeatureGate).mockReturnValue(false)
const store = createMockStore(storeWithPositions)

const { getAllByTestId } = render(
<Provider store={store}>
<MockedNavigator component={AssetsScreen} />
</Provider>
)

expect(getAllByTestId('TokenBalanceItem')).toHaveLength(2)

fireEvent.press(getAllByTestId('TokenBalanceItem')[0])
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.TokenDetails, { tokenId: mockCusdTokenId })
expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1)
})

it('hides claim rewards if feature gate is false', () => {
jest
.mocked(getFeatureGate)
Expand Down
21 changes: 19 additions & 2 deletions src/tokens/Assets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ import {
useTotalTokenBalance,
} from 'src/tokens/hooks'
import { TokenBalance } from 'src/tokens/slice'
import { getSupportedNetworkIdsForTokenBalances, sortByUsdBalance } from 'src/tokens/utils'
import {
getSupportedNetworkIdsForTokenBalances,
getTokenAnalyticsProps,
sortByUsdBalance,
} from 'src/tokens/utils'

const DEVICE_WIDTH_BREAKPOINT = 340

Expand Down Expand Up @@ -279,7 +283,20 @@ function AssetsScreen({ navigation, route }: Props) {
if (assetIsPosition(item)) {
return <PositionItem position={item} />
}
return <TokenBalanceItem token={item} />
return (
<TokenBalanceItem
token={item}
onPress={() => {
navigate(Screens.TokenDetails, { tokenId: item.tokenId })
ValoraAnalytics.track(AssetsEvents.tap_asset, {
...getTokenAnalyticsProps(item),
title: item.symbol,
description: item.name,
assetType: 'token',
})
}}
/>
)
}

const tabBarItems = useMemo(() => {
Expand Down
Loading