Skip to content

Commit

Permalink
feat(earn): dispatch withdraw start on pressing collect (#5430)
Browse files Browse the repository at this point in the history
### Description

Dispatches the redux action on pressing the collect earnings button and
fires analytics event

### Test plan

Unit tests

### Related issues

- Part of ACT-1180

### 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
satish-ravi committed May 18, 2024
1 parent e8472fd commit da2b95c
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 21 deletions.
1 change: 1 addition & 0 deletions src/analytics/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -691,4 +691,5 @@ export enum EarnEvents {
earn_view_pools_press = 'earn_view_pools_press',
earn_exit_pool_press = 'earn_exit_pool_press',
earn_feed_item_select = 'earn_feed_item_select',
earn_collect_earnings_press = 'earn_collect_earnings_press',
}
8 changes: 8 additions & 0 deletions src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
RewardsScreenOrigin,
} from 'src/consumerIncentives/analyticsEventsTracker'
import { DappSection } from 'src/dapps/types'
import { SerializableRewardsInfo } from 'src/earn/types'
import { ProviderSelectionAnalyticsData } from 'src/fiatExchanges/types'
import { CICOFlow, FiatExchangeFlow, PaymentMethod } from 'src/fiatExchanges/utils'
import { HomeActionName, NotificationBannerCTATypes, NotificationType } from 'src/home/types'
Expand Down Expand Up @@ -1608,6 +1609,13 @@ interface EarnEventsProperties {
[EarnEvents.earn_feed_item_select]: {
origin: 'EarnDeposit' | 'EarnWithdraw' | 'EarnClaimReward'
}
[EarnEvents.earn_collect_earnings_press]: {
tokenId: string
amount: string
networkId: NetworkId
providerId: string
rewards: SerializableRewardsInfo[]
}
}

export type AnalyticsPropertiesList = AppEventsProperties &
Expand Down
1 change: 1 addition & 0 deletions src/analytics/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
[EarnEvents.earn_view_pools_press]: `When the user taps on the view pools button from token details`,
[EarnEvents.earn_exit_pool_press]: `When the user taps on the exit pool button from the earn card in discover tab`,
[EarnEvents.earn_feed_item_select]: `When the users taps on an earn transaction feed item`,
[EarnEvents.earn_collect_earnings_press]: `When the user taps on the collect earnings button in the collect screen`,

// Legacy event docs
// The below events had docs, but are no longer produced by the latest app version.
Expand Down
95 changes: 80 additions & 15 deletions src/earn/EarnCollectScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { render, waitFor } from '@testing-library/react-native'
import { fireEvent, render, waitFor } from '@testing-library/react-native'
import BigNumber from 'bignumber.js'
import React from 'react'
import { Provider } from 'react-redux'
import { EarnEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import EarnCollectScreen from 'src/earn/EarnCollectScreen'
import { fetchAavePoolInfo, fetchAaveRewards } from 'src/earn/poolInfo'
import { prepareWithdrawAndClaimTransactions } from 'src/earn/prepareTransactions'
import { withdrawStart } from 'src/earn/slice'
import { NetworkId } from 'src/transactions/types'
import { PreparedTransactionsPossible } from 'src/viem/prepareTransactions'
import { getSerializablePreparedTransactions } from 'src/viem/preparedTransactionSerialization'
import networkConfig from 'src/web3/networkConfig'
import MockedNavigator from 'test/MockedNavigator'
import { createMockStore, mockStoreBalancesToTokenBalances } from 'test/utils'
Expand All @@ -20,22 +24,22 @@ import {
mockTokenBalances,
} from 'test/values'

const store = createMockStore({
tokens: {
tokenBalances: {
...mockTokenBalances,
[networkConfig.aaveArbUsdcTokenId]: {
networkId: NetworkId['arbitrum-sepolia'],
address: mockAaveArbUsdcAddress,
tokenId: networkConfig.aaveArbUsdcTokenId,
symbol: 'aArbSepUSDC',
priceUsd: '1',
balance: '10.75',
priceFetchedAt: Date.now(),
},
const mockStoreTokens = {
tokenBalances: {
...mockTokenBalances,
[networkConfig.aaveArbUsdcTokenId]: {
networkId: NetworkId['arbitrum-sepolia'],
address: mockAaveArbUsdcAddress,
tokenId: networkConfig.aaveArbUsdcTokenId,
symbol: 'aArbSepUSDC',
priceUsd: '1',
balance: '10.75',
priceFetchedAt: Date.now(),
},
},
})
}

const store = createMockStore({ tokens: mockStoreTokens })

jest.mock('src/earn/poolInfo')
jest.mock('src/earn/prepareTransactions')
Expand Down Expand Up @@ -78,6 +82,7 @@ describe('EarnCollectScreen', () => {
jest.mocked(fetchAavePoolInfo).mockResolvedValue({ apy: 0.03 })
jest.mocked(fetchAaveRewards).mockResolvedValue(mockRewards)
jest.mocked(prepareWithdrawAndClaimTransactions).mockResolvedValue(mockPreparedTransaction)
store.clearActions()
})

it('renders total balance, rewards, apy and gas after fetching rewards and preparing tx', async () => {
Expand Down Expand Up @@ -315,4 +320,64 @@ describe('EarnCollectScreen', () => {
expect(getByTestId('EarnCollectScreen/CTA')).toBeDisabled()
expect(getByTestId('EarnCollect/GasError')).toBeTruthy()
})

it('pressing cta dispatches withdraw action and fires analytics event', async () => {
const { getByTestId } = render(
<Provider store={store}>
<MockedNavigator
component={EarnCollectScreen}
params={{
depositTokenId: mockArbUsdcTokenId,
poolTokenId: networkConfig.aaveArbUsdcTokenId,
}}
/>
</Provider>
)

await waitFor(() => {
expect(getByTestId('EarnCollectScreen/CTA')).toBeEnabled()
})

fireEvent.press(getByTestId('EarnCollectScreen/CTA'))

expect(store.getActions()).toEqual([
{
type: withdrawStart.type,
payload: {
amount: '10.75',
tokenId: mockArbUsdcTokenId,
preparedTransactions: getSerializablePreparedTransactions(
mockPreparedTransaction.transactions
),
rewards: [{ amount: '0.01', tokenId: mockArbArbTokenId }],
},
},
])

expect(ValoraAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_collect_earnings_press, {
tokenId: mockArbUsdcTokenId,
amount: '10.75',
networkId: NetworkId['arbitrum-sepolia'],
providerId: 'aave-v3',
rewards: [{ amount: '0.01', tokenId: mockArbArbTokenId }],
})
})

it('disables cta and shows loading spinner when withdraw is submitted', async () => {
const store = createMockStore({ tokens: mockStoreTokens, earn: { withdrawStatus: 'loading' } })
const { getByTestId } = render(
<Provider store={store}>
<MockedNavigator
component={EarnCollectScreen}
params={{
depositTokenId: mockArbUsdcTokenId,
poolTokenId: networkConfig.aaveArbUsdcTokenId,
}}
/>
</Provider>
)

expect(getByTestId('EarnCollectScreen/CTA')).toBeDisabled()
expect(getByTestId('EarnCollectScreen/CTA')).toContainElement(getByTestId('Button/Loading'))
})
})
39 changes: 37 additions & 2 deletions src/earn/EarnCollectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,36 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native'
import SkeletonPlaceholder from 'react-native-skeleton-placeholder'
import { EarnEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import Button, { BtnSizes } from 'src/components/Button'
import InLineNotification, { NotificationVariant } from 'src/components/InLineNotification'
import TokenDisplay from 'src/components/TokenDisplay'
import TokenIcon, { IconSize } from 'src/components/TokenIcon'
import { useAavePoolInfo, useAaveRewardsInfoAndPrepareTransactions } from 'src/earn/hooks'
import { withdrawStatusSelector } from 'src/earn/selectors'
import { withdrawStart } from 'src/earn/slice'
import { Screens } from 'src/navigator/Screens'
import { StackParamList } from 'src/navigator/types'
import { useSelector } from 'src/redux/hooks'
import { useDispatch, useSelector } from 'src/redux/hooks'
import Colors from 'src/styles/colors'
import { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import { useTokenInfo } from 'src/tokens/hooks'
import { feeCurrenciesSelector } from 'src/tokens/selectors'
import { TokenBalance } from 'src/tokens/slice'
import { getFeeCurrencyAndAmounts } from 'src/viem/prepareTransactions'
import { getSerializablePreparedTransactions } from 'src/viem/preparedTransactionSerialization'

type Props = NativeStackScreenProps<StackParamList, Screens.EarnCollectScreen>

export default function EarnCollectScreen({ route }: Props) {
const { t } = useTranslation()
const dispatch = useDispatch()
const { depositTokenId, poolTokenId } = route.params
const depositToken = useTokenInfo(depositTokenId)
const poolToken = useTokenInfo(poolTokenId)
const withdrawStatus = useSelector(withdrawStatusSelector)

if (!depositToken || !poolToken) {
// should never happen
Expand All @@ -40,7 +47,34 @@ export default function EarnCollectScreen({ route }: Props) {
feeCurrencies,
})
const onPress = () => {
// TODO(ACT-1180): submit the tx
if (!asyncRewardsInfo.result || asyncPreparedTransactions.result?.type !== 'possible') {
// should never happen because button is disabled if withdraw is not possible
throw new Error('Cannot be called without possible prepared transactions')
}

const serializedRewards = asyncRewardsInfo.result.map((info) => ({
amount: info.amount.toString(),
tokenId: info.tokenInfo.tokenId,
}))

dispatch(
withdrawStart({
amount: poolToken.balance.toString(),
tokenId: depositTokenId,
preparedTransactions: getSerializablePreparedTransactions(
asyncPreparedTransactions.result.transactions
),
rewards: serializedRewards,
})
)

ValoraAnalytics.track(EarnEvents.earn_collect_earnings_press, {
tokenId: depositTokenId,
amount: poolToken.balance.toString(),
networkId: depositToken.networkId,
providerId: 'aave-v3',
rewards: serializedRewards,
})
}

// skipping apy fetch error because that isn't blocking collecting rewards
Expand Down Expand Up @@ -107,6 +141,7 @@ export default function EarnCollectScreen({ route }: Props) {
onPress={onPress}
testID="EarnCollectScreen/CTA"
disabled={!!ctaDisabled}
showLoading={withdrawStatus === 'loading'}
/>
</SafeAreaView>
)
Expand Down
2 changes: 2 additions & 0 deletions src/earn/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { RootState } from 'src/redux/store'

export const depositStatusSelector = (state: RootState) => state.earn.depositStatus

export const withdrawStatusSelector = (state: RootState) => state.earn.withdrawStatus
10 changes: 6 additions & 4 deletions src/earn/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ export interface RewardsInfo {
tokenInfo: TokenBalance
}

export interface SerializableRewardsInfo {
amount: string
tokenId: string
}

export interface WithdrawInfo {
amount: string
tokenId: string
preparedTransactions: SerializableTransactionRequest[]
rewards: {
amount: string
tokenId: string
}[]
rewards: SerializableRewardsInfo[]
}

0 comments on commit da2b95c

Please sign in to comment.