Skip to content

Commit

Permalink
feat: enable trigger shortcuts (#3957)
Browse files Browse the repository at this point in the history
### Description

This PR does 2 things (that made sense to test together)
1. add a saga for triggering a shortcut
2. add some redux state management to display available / claiming /
claimed statuses on the rewards screen

The logic for 2 is a little more hefty than I originally planned, i may
have overthought the scenario. What i wanted to cater for is:
1. a user claims a reward successfully
2. their balance and positions are refreshed in the background while the
user remains on the rewards screen
3. they should see that their reward was claimed, but the positions may
refresh and the claimable parts removed. so the screen essentially
tracks all the data it was loaded with, and if any rewards go "missing"
from the redux store then we mark it as "claimed".
4. to make this a little robust for rewards that can be continuously
claimed (i.e. on successful claim, the reward remains but the reward
amount changes) i made up a reward id that relies on a combination of
reward amount and reward address.

### Test plan



https://github.com/valora-inc/wallet/assets/20150449/595bb6ae-26fc-426f-aede-12356929022f


### Related issues

- Fixes RET-733

### Backwards compatibility

Y
  • Loading branch information
kathaypacific authored Jul 17, 2023
1 parent e62f8e8 commit 5ffffdf
Show file tree
Hide file tree
Showing 16 changed files with 559 additions and 63 deletions.
7 changes: 5 additions & 2 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,8 +1346,11 @@
"title": "Your rewards",
"description": "Now you can claim rewards from open dapp positions directly in {{appName}}!",
"claimButton": "Claim",
"rewardLabel": "Available reward"
}
"claimedLabel": "Claimed",
"rewardLabel": "Available reward",
"claimSuccess": "Success! You claimed a reward"
},
"claimRewardFailure": "There was an error claiming your reward, please try again later"
},
"dappsScreenHelpDialog": {
"title": "What are dapps?",
Expand Down
1 change: 1 addition & 0 deletions src/app/ErrorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,5 @@ export enum ErrorMessages {
KYC_TRY_AGAIN_FAILED = 'fiatConnectKycStatusScreen.tryAgainFailed',
INSUFFICIENT_BALANCE_STABLE = 'insufficientBalanceStable',
HOOKS_INVALID_PREVIEW_API_URL = 'hooksPreview.invalidApiUrl',
SHORTCUT_CLAIM_REWARD_FAILED = 'dappShortcuts.claimRewardFailure',
}
250 changes: 214 additions & 36 deletions src/dapps/DappShortcutsRewards.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { render, within } from '@testing-library/react-native'
import { fireEvent, render, within } from '@testing-library/react-native'
import React from 'react'
import { Provider } from 'react-redux'
import DappShortcutsRewards from 'src/dapps/DappShortcutsRewards'
import { Position } from 'src/positions/types'
import { createMockStore } from 'test/utils'
import { mockCusdAddress, mockPositions, mockShortcuts } from 'test/values'

Expand All @@ -12,48 +13,117 @@ jest.mock('src/statsig', () => ({
const mockCeloAddress = '0x471ece3750da237f93b8e339c536989b8978a438'
const mockUbeAddress = '0x00be915b9dcf56a3cbe739d9b9c202ca692409ec'

const getPositionWithClaimableBalance = (balance?: string): Position => ({
type: 'contract-position',
network: 'celo',
address: '0xda7f463c27ec862cfbf2369f3f74c364d050d93f',
appId: 'ubeswap',
appName: 'Ubeswap',
displayProps: {
title: 'CELO / cUSD',
description: 'Farm',
imageUrl: '',
},
tokens: [
{
type: 'app-token',
network: 'celo',
address: '0x1e593f1fe7b61c53874b54ec0c59fd0d5eb8621e',
appId: 'ubeswap',
symbol: 'ULP',
decimals: 18,
appName: 'Ubeswap',
displayProps: {
title: 'CELO / cUSD',
description: 'Pool',
imageUrl: '',
},
tokens: [
{
type: 'base-token',
network: 'celo',
address: '0x471ece3750da237f93b8e339c536989b8978a438',
symbol: 'CELO',
decimals: 18,
priceUsd: '0.6959536890241361',
balance: balance ?? '0.950545800159603456',
category: 'claimable',
},
{
type: 'base-token',
network: 'celo',
address: '0x765de816845861e75a25fca122bb6898b8b1282a',
symbol: 'cUSD',
decimals: 18,
priceUsd: '1',
balance: '0.659223169268731392',
},
],
pricePerShare: ['2.827719585853931', '1.961082008754231'],
priceUsd: '3.9290438860550765',
balance: '0.336152780111169400',
supply: '42744.727037884449180591',
availableShortcutIds: [],
},
{
priceUsd: '0.00904673476946796903',
type: 'base-token',
category: 'claimable',
decimals: 18,
network: 'celo',
balance: balance ?? '0.098322815093446616',
symbol: 'UBE',
address: '0x00be915b9dcf56a3cbe739d9b9c202ca692409ec',
},
],
balanceUsd: '1.3207590254762067',
availableShortcutIds: ['claim-reward'],
})

const defaultState = {
positions: {
positions: [...mockPositions.slice(0, 2), getPositionWithClaimableBalance()],
shortcuts: mockShortcuts,
},
tokens: {
tokenBalances: {
[mockCeloAddress]: {
address: mockCeloAddress,
symbol: 'CELO',
usdPrice: '0.6959536890241361', // matches data in mockPositions
balance: '10',
priceFetchedAt: Date.now(),
isCoreToken: true,
},
[mockUbeAddress]: {
address: mockUbeAddress,
symbol: 'UBE',
usdPrice: '0.00904673476946796903', // matches data in mockPositions
balance: '10',
priceFetchedAt: Date.now(),
},
[mockCusdAddress]: {
address: mockCusdAddress,
symbol: 'cUSD',
usdPrice: '1',
balance: '10',
priceFetchedAt: Date.now(),
isCoreToken: true,
},
},
},
}
const mockStore = createMockStore(defaultState)

describe('DappShortcutsRewards', () => {
beforeEach(() => {
jest.clearAllMocks()
mockStore.clearActions()
})

it('should render claimable rewards correctly', () => {
const { getByText, getAllByTestId } = render(
<Provider
store={createMockStore({
positions: {
positions: mockPositions,
shortcuts: mockShortcuts,
},
tokens: {
tokenBalances: {
[mockCeloAddress]: {
address: mockCeloAddress,
symbol: 'CELO',
usdPrice: '0.6959536890241361', // matches data in mockPositions
balance: '10',
priceFetchedAt: Date.now(),
isCoreToken: true,
},
[mockUbeAddress]: {
address: mockUbeAddress,
symbol: 'UBE',
usdPrice: '0.00904673476946796903', // matches data in mockPositions
balance: '10',
priceFetchedAt: Date.now(),
},
[mockCusdAddress]: {
address: mockCusdAddress,
symbol: 'cUSD',
usdPrice: '1',
balance: '10',
priceFetchedAt: Date.now(),
isCoreToken: true,
},
},
},
})}
>
<Provider store={mockStore}>
<DappShortcutsRewards />
</Provider>
)
Expand All @@ -71,4 +141,112 @@ describe('DappShortcutsRewards', () => {
).toHaveTextContent('₱0.88') // USD value $0.66, mocked exchange rate 1.33
expect(within(rewardCard).getByTestId('DappShortcutsRewards/ClaimButton')).toBeTruthy()
})

it('should dispatch the correct action on claim', () => {
const { getAllByTestId } = render(
<Provider store={mockStore}>
<DappShortcutsRewards />
</Provider>
)

fireEvent.press(getAllByTestId('DappShortcutsRewards/ClaimButton')[0])

expect(mockStore.getActions()).toMatchInlineSnapshot(`
Array [
Object {
"payload": Object {
"address": "0x0000000000000000000000000000000000007e57",
"appId": "ubeswap",
"id": "claim-reward-0xda7f463c27ec862cfbf2369f3f74c364d050d93f-1.048868615253050072",
"network": "celo",
"positionAddress": "0xda7f463c27ec862cfbf2369f3f74c364d050d93f",
"shortcutId": "claim-reward",
},
"type": "positions/triggerShortcut",
},
]
`)
})

it('should show a reward being claimed', () => {
const { getByTestId } = render(
<Provider
store={createMockStore({
...defaultState,
positions: {
...defaultState.positions,
triggeredShortcutsStatus: {
'claim-reward-0xda7f463c27ec862cfbf2369f3f74c364d050d93f-1.048868615253050072':
'loading',
},
},
})}
>
<DappShortcutsRewards />
</Provider>
)

expect(getByTestId('DappShortcutsRewards/ClaimButton')).toBeDisabled()
expect(getByTestId('Button/Loading')).toBeTruthy()
})

it('should show a claimed reward when it is no longer claimable in redux', () => {
const { getByTestId, getByText, queryByText, rerender } = render(
<Provider store={mockStore}>
<DappShortcutsRewards />
</Provider>
)

expect(getByTestId('DappShortcutsRewards/ClaimButton')).toBeEnabled()
expect(queryByText('dappShortcuts.claimRewardsScreen.claimedLabel')).toBeFalsy()
expect(getByText('dappShortcuts.claimRewardsScreen.claimButton')).toBeTruthy()

// simulate data refresh after a successful claim
const updatedStore = createMockStore({
...defaultState,
positions: {
...defaultState.positions,
positions: [...mockPositions.slice(0, 2), getPositionWithClaimableBalance('0')],
},
})
rerender(
<Provider store={updatedStore}>
<DappShortcutsRewards />
</Provider>
)

expect(getByTestId('DappShortcutsRewards/ClaimButton')).toBeDisabled()
expect(queryByText('dappShortcuts.claimRewardsScreen.claimButton')).toBeFalsy()
expect(getByText('dappShortcuts.claimRewardsScreen.claimedLabel')).toBeTruthy()
})

it('should show a claimed, updated reward correctly', () => {
const { getByTestId, getByText, queryByText, rerender } = render(
<Provider store={mockStore}>
<DappShortcutsRewards />
</Provider>
)

expect(getByTestId('DappShortcutsRewards/ClaimButton')).toBeEnabled()
expect(queryByText('dappShortcuts.claimRewardsScreen.claimedLabel')).toBeFalsy()
expect(getByText('dappShortcuts.claimRewardsScreen.claimButton')).toBeTruthy()

// simulate data refresh after a successful claim, for a continuously claimable reward
const updatedStore = createMockStore({
...defaultState,
positions: {
...defaultState.positions,
positions: [...mockPositions.slice(0, 2), getPositionWithClaimableBalance('0.01')],
},
})
rerender(
<Provider store={updatedStore}>
<DappShortcutsRewards />
</Provider>
)

expect(getByTestId('DappShortcutsRewards/ClaimButton')).toBeEnabled()
expect(queryByText('dappShortcuts.claimRewardsScreen.claimedLabel')).toBeFalsy()
expect(getByText('dappShortcuts.claimRewardsScreen.claimButton')).toBeTruthy()
})
})
Loading

0 comments on commit 5ffffdf

Please sign in to comment.