Skip to content

Commit

Permalink
feat(feedV2): generic claim reward type (#6198)
Browse files Browse the repository at this point in the history
### Description

Introduce new generic type for the TX feed for claim reward.

`CLAIM_REWARD` replaces `EARN_CLAIM_REWARD`

Why?

1. This allows directly supporting more claim reward TXs, not only for
Earn but also GoodDollar, etc.
2. We can do this now without too much effort and keep a "simple"
backend implementation for the new `getWalletTransactions`

Similar to what was done in
#6189

This is just for the new feed API with Zerion. 

### Test plan

- Tests pass

### Related issues

- Related to RET-1204
- Also see this Slack
[thread](https://valora-app.slack.com/archives/C029Z1QMD7B/p1729847488933829)
for context

### Backwards compatibility

Yes

### Network scalability

If a new NetworkId and/or Network are added in the future, the changes
in this PR will:

- [x] Continue to work without code changes, OR trigger a compilation
error (guaranteeing we find it when a new network is added)
  • Loading branch information
jeanregisser authored Oct 31, 2024
1 parent ef81adc commit 06c96c3
Show file tree
Hide file tree
Showing 12 changed files with 692 additions and 2 deletions.
9 changes: 8 additions & 1 deletion locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2288,7 +2288,10 @@
"depositSubtitle_noTxAppName": "to unknown pool",
"withdrawTitle": "Withdrew",
"withdrawSubtitle": "from {{txAppName}} Pool",
"withdrawSubtitle_noTxAppName": "from unknown app"
"withdrawSubtitle_noTxAppName": "from unknown app",
"claimRewardTitle": "Collected",
"claimRewardSubtitle": "from {{txAppName}} Pool",
"claimRewardSubtitle_noTxAppName": "from unknown app"
},
"transactionDetails": {
"descriptionLabel": "Details",
Expand All @@ -2300,6 +2303,10 @@
"withdrawSubtitle": "Withdrew {{tokenSymbol}} from {{txAppName}} Pool",
"withdrawSubtitle_noTxAppName": "Withdrew {{tokenSymbol}} from unknown app",
"withdrawDetails": "Amount Withdrawn",
"claimRewardTitle": "Collected",
"claimRewardSubtitle": "Collected {{tokenSymbol}} from {{txAppName}} Pool",
"claimRewardSubtitle_noTxAppName": "Collected {{tokenSymbol}} from unknown app",
"claimRewardDetails": "Amount Collected",
"swap": "Swap",
"network": "Network",
"fees": "Fees"
Expand Down
162 changes: 162 additions & 0 deletions src/transactions/feed/ClaimRewardFeedItem.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { fireEvent, render, within } from '@testing-library/react-native'
import React from 'react'
import { Provider } from 'react-redux'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { HomeEvents } from 'src/analytics/Events'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import ClaimRewardFeedItem from 'src/transactions/feed/ClaimRewardFeedItem'
import { NetworkId } from 'src/transactions/types'
import { createMockStore } from 'test/utils'
import {
mockAaveArbUsdcAddress,
mockAaveArbUsdcTokenId,
mockArbArbTokenId,
mockArbUsdcTokenId,
mockClaimRewardTransaction,
} from 'test/values'

jest.mock('src/statsig')
jest
.mocked(getFeatureGate)
.mockImplementation((featureGateName) => featureGateName === StatsigFeatureGates.SHOW_POSITIONS)

const store = createMockStore({
tokens: {
tokenBalances: {
[mockArbUsdcTokenId]: {
tokenId: mockArbUsdcTokenId,
symbol: 'USDC',
priceUsd: '1',
priceFetchedAt: Date.now(),
networkId: NetworkId['arbitrum-sepolia'],
},
[mockArbArbTokenId]: {
tokenId: mockArbArbTokenId,
symbol: 'ARB',
priceUsd: '0.9898',
priceFetchedAt: Date.now(),
networkId: NetworkId['arbitrum-sepolia'],
},
[mockAaveArbUsdcTokenId]: {
networkId: NetworkId['arbitrum-sepolia'],
address: mockAaveArbUsdcAddress,
tokenId: mockAaveArbUsdcTokenId,
symbol: 'aArbSepUSDC',
priceUsd: '1',
priceFetchedAt: Date.now(),
},
},
},
positions: {
positions: [
{
type: 'app-token',
networkId: NetworkId['arbitrum-sepolia'],
address: '0x460b97bd498e1157530aeb3086301d5225b91216',
tokenId: 'arbitrum-sepolia:0x460b97bd498e1157530aeb3086301d5225b91216',
positionId: 'arbitrum-sepolia:0x460b97bd498e1157530aeb3086301d5225b91216',
appId: 'aave',
appName: 'Aave',
symbol: 'aArbSepUSDC',
decimals: 6,
displayProps: {
title: 'USDC',
description: 'Supplied (APY: 1.92%)',
imageUrl: 'https://raw.githubusercontent.com/valora-inc/dapp-list/main/assets/aave.png',
},
dataProps: {
yieldRates: [
{
percentage: 1.9194202601763743,
label: 'Earnings APY',
tokenId: 'arbitrum-sepolia:0x75faf114eafb1bdbe2f0316df893fd58ce46aa4d',
},
],
earningItems: [],
depositTokenId: 'arbitrum-sepolia:0x75faf114eafb1bdbe2f0316df893fd58ce46aa4d',
withdrawTokenId: 'arbitrum-sepolia:0x460b97bd498e1157530aeb3086301d5225b91216',
},
tokens: [
{
tokenId: 'arbitrum-sepolia:0x75faf114eafb1bdbe2f0316df893fd58ce46aa4d',
networkId: NetworkId['arbitrum-sepolia'],
address: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
symbol: 'USDC',
decimals: 6,
priceUsd: '0',
type: 'base-token',
balance: '0',
},
],
pricePerShare: ['1'],
priceUsd: '0.999',
balance: '10',
supply: '190288.768509',
availableShortcutIds: ['deposit', 'withdraw'],
},
],
earnPositionIds: ['arbitrum-sepolia:0x460b97bd498e1157530aeb3086301d5225b91216'],
},
})

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

it('should render correctly', () => {
const { getByText, getByTestId } = render(
<Provider store={store}>
<ClaimRewardFeedItem transaction={mockClaimRewardTransaction} />
</Provider>
)

expect(getByText('transactionFeed.claimRewardTitle')).toBeTruthy()
expect(getByText('transactionFeed.claimRewardSubtitle, {"txAppName":"Aave"}')).toBeTruthy()
expect(
within(getByTestId('ClaimRewardFeedItem/amount-crypto')).getByText('+1.50 ARB')
).toBeTruthy()
expect(within(getByTestId('ClaimRewardFeedItem/amount-local')).getByText('₱1.97')).toBeTruthy()
})

it('should display when app name is not available', () => {
const { getByText } = render(
<Provider store={store}>
<ClaimRewardFeedItem transaction={{ ...mockClaimRewardTransaction, appName: undefined }} />
</Provider>
)

expect(getByText('transactionFeed.claimRewardSubtitle, {"context":"noTxAppName"}')).toBeTruthy()
})

it('should navigate correctly on tap', () => {
const { getByTestId } = render(
<Provider store={store}>
<ClaimRewardFeedItem transaction={mockClaimRewardTransaction} />
</Provider>
)

fireEvent.press(
getByTestId(`ClaimRewardFeedItem/${mockClaimRewardTransaction.transactionHash}`)
)
expect(navigate).toHaveBeenCalledWith(Screens.TransactionDetailsScreen, {
transaction: mockClaimRewardTransaction,
})
})

it('should fire analytic event on tap', () => {
const { getByTestId } = render(
<Provider store={store}>
<ClaimRewardFeedItem transaction={mockClaimRewardTransaction} />
</Provider>
)

fireEvent.press(
getByTestId(`ClaimRewardFeedItem/${mockClaimRewardTransaction.transactionHash}`)
)
expect(AppAnalytics.track).toHaveBeenCalledWith(HomeEvents.transaction_feed_item_select)
})
})
143 changes: 143 additions & 0 deletions src/transactions/feed/ClaimRewardFeedItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import BigNumber from 'bignumber.js'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { HomeEvents } from 'src/analytics/Events'
import TokenDisplay from 'src/components/TokenDisplay'
import Touchable from 'src/components/Touchable'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import Colors from 'src/styles/colors'
import { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import variables from 'src/styles/variables'
import TransactionFeedItemImage from 'src/transactions/feed/TransactionFeedItemImage'
import { ClaimReward } from 'src/transactions/types'

interface DescriptionProps {
transaction: ClaimReward
}

function Description({ transaction }: DescriptionProps) {
const { t } = useTranslation()
const txAppName = transaction.appName
const title = t('transactionFeed.claimRewardTitle')
const subtitle = t('transactionFeed.claimRewardSubtitle', {
context: !txAppName ? 'noTxAppName' : undefined,
txAppName,
})

return (
<View style={styles.contentContainer}>
<Text style={styles.title} testID={'ClaimRewardFeedItem/title'} numberOfLines={1}>
{title}
</Text>
<Text style={styles.subtitle} testID={'ClaimRewardFeedItem/subtitle'} numberOfLines={1}>
{subtitle}
</Text>
</View>
)
}

interface AmountDisplayProps {
transaction: ClaimReward
isLocal: boolean
}

function AmountDisplay({ transaction, isLocal }: AmountDisplayProps) {
const amountValue = new BigNumber(transaction.amount.value)
const tokenId = transaction.amount.tokenId

const textStyle = isLocal ? styles.amountSubtitle : [styles.amountTitle, { color: Colors.accent }]

return (
<TokenDisplay
amount={amountValue}
localAmount={transaction.amount.localAmount}
tokenId={tokenId}
showLocalAmount={isLocal}
showSymbol={true}
showExplicitPositiveSign={!isLocal}
hideSign={!!isLocal}
style={textStyle}
testID={`ClaimRewardFeedItem/amount-${isLocal ? 'local' : 'crypto'}`}
/>
)
}

interface AmountProps {
transaction: ClaimReward
}

function Amount({ transaction }: AmountProps) {
return (
<View style={styles.amountContainer}>
<AmountDisplay transaction={transaction} isLocal={false} />
<AmountDisplay transaction={transaction} isLocal={true} />
</View>
)
}

interface Props {
transaction: ClaimReward
}

export default function ClaimRewardFeedItem({ transaction }: Props) {
return (
<Touchable
testID={`ClaimRewardFeedItem/${transaction.transactionHash}`}
onPress={() => {
// TODO: we'll add the type in a subsequent PR
AppAnalytics.track(HomeEvents.transaction_feed_item_select)
navigate(Screens.TransactionDetailsScreen, { transaction })
}}
>
<View style={styles.container}>
<TransactionFeedItemImage
status={transaction.status}
transactionType={transaction.type}
networkId={transaction.networkId}
/>
<Description transaction={transaction} />
<Amount transaction={transaction} />
</View>
</Touchable>
)
}

const styles = StyleSheet.create({
container: {
flexDirection: 'row',
paddingVertical: Spacing.Small12,
paddingHorizontal: variables.contentPadding,
alignItems: 'center',
},
contentContainer: {
flex: 1,
paddingHorizontal: variables.contentPadding,
},
title: {
...typeScale.labelMedium,
color: Colors.black,
},
subtitle: {
...typeScale.bodySmall,
color: Colors.gray4,
},
amountContainer: {
maxWidth: '50%',
},
amountTitle: {
...typeScale.labelMedium,
color: Colors.black,
flexWrap: 'wrap',
textAlign: 'right',
},
amountSubtitle: {
...typeScale.bodySmall,
color: Colors.gray4,
flexWrap: 'wrap',
textAlign: 'right',
},
})
Loading

0 comments on commit 06c96c3

Please sign in to comment.