Skip to content

Commit

Permalink
feat: add claim shortcuts screen (#3951)
Browse files Browse the repository at this point in the history
### Description

As the title 
- add plumbing to navigate to a new claimable shortcuts screen
- UI for displaying claimable shortcuts
- the screen will only be navigatable when there are claimable
shortcuts, so the screen does not cater for an empty or error state

Not included in this PR
- claiming reward
- analytics

### Test plan

![Simulator Screenshot - iPhone 14 Pro - 2023-07-13 at 10 19
04](https://github.com/valora-inc/wallet/assets/20150449/57e81784-0930-48ac-b747-b6405e2d76c6)


### Related issues

- Fixes RET-733

### Backwards compatibility

Y
  • Loading branch information
kathaypacific authored Jul 13, 2023
1 parent 721d464 commit 18bbecc
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 14 deletions.
6 changes: 6 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,12 @@
"rewards": {
"title": "Your rewards",
"description": "Claim available rewards from open dapp positions"
},
"claimRewardsScreen": {
"title": "Your rewards",
"description": "Now you can claim rewards from open dapp positions directly in {{appName}}!",
"claimButton": "Claim",
"rewardLabel": "Available reward"
}
},
"dappsScreenHelpDialog": {
Expand Down
74 changes: 74 additions & 0 deletions src/dapps/DappShortcutsRewards.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { render, within } from '@testing-library/react-native'
import React from 'react'
import { Provider } from 'react-redux'
import DappShortcutsRewards from 'src/dapps/DappShortcutsRewards'
import { createMockStore } from 'test/utils'
import { mockCusdAddress, mockPositions, mockShortcuts } from 'test/values'

jest.mock('src/statsig', () => ({
getFeatureGate: jest.fn(() => true),
}))

const mockCeloAddress = '0x471ece3750da237f93b8e339c536989b8978a438'
const mockUbeAddress = '0x00be915b9dcf56a3cbe739d9b9c202ca692409ec'

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

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,
},
},
},
})}
>
<DappShortcutsRewards />
</Provider>
)

expect(getByText('dappShortcuts.claimRewardsScreen.title')).toBeTruthy()
expect(getByText('dappShortcuts.claimRewardsScreen.description')).toBeTruthy()
expect(getAllByTestId('DappShortcutsRewards/Card').length).toBe(1)

const rewardCard = getAllByTestId('DappShortcutsRewards/Card')[0]
expect(within(rewardCard).getByTestId('DappShortcutsRewards/RewardAmount')).toHaveTextContent(
'0.098 UBE, 0.95 CELO'
)
expect(
within(rewardCard).getByTestId('DappShortcutsRewards/RewardAmountFiat')
).toHaveTextContent('₱0.88') // USD value $0.66, mocked exchange rate 1.33
expect(within(rewardCard).getByTestId('DappShortcutsRewards/ClaimButton')).toBeTruthy()
})
})
171 changes: 171 additions & 0 deletions src/dapps/DappShortcutsRewards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { BigNumber } from 'bignumber.js'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Image, StyleSheet, Text, View } from 'react-native'
import Animated from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useSelector } from 'react-redux'
import Button, { BtnSizes } from 'src/components/Button'
import TokenDisplay from 'src/components/TokenDisplay'
import { positionsWithClaimableRewardsSelector } from 'src/positions/selectors'
import { ClaimablePosition } from 'src/positions/types'
import Colors from 'src/styles/colors'
import fontStyles from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import { Currency } from 'src/utils/currencies'

function DappShortcutsRewards() {
const { t } = useTranslation()
const insets = useSafeAreaInsets()

const positionsWithClaimableRewards = useSelector(positionsWithClaimableRewardsSelector)

const handleClaimReward = (position: ClaimablePosition) => () => {
// do something
}

const renderItem = ({ item }: { item: ClaimablePosition }) => {
let claimableValueUsd = new BigNumber(0)
item.claimableShortcut.claimableTokens.forEach((token) => {
claimableValueUsd = claimableValueUsd.plus(
BigNumber(token.priceUsd).times(BigNumber(token.balance))
)
})

return (
<View style={styles.card} testID="DappShortcutsRewards/Card">
<View style={styles.rewardInfoContainer}>
<View style={styles.rewardAmountContainer}>
<Text style={styles.rewardLabel}>
{t('dappShortcuts.claimRewardsScreen.rewardLabel')}
</Text>

<Text style={styles.rewardAmount} testID="DappShortcutsRewards/RewardAmount">
{item.claimableShortcut.claimableTokens.map((token, index) => (
<React.Fragment key={token.address}>
{index > 0 && ', '}
<TokenDisplay
amount={token.balance}
tokenAddress={token.address}
showLocalAmount={false}
/>
</React.Fragment>
))}
</Text>
{claimableValueUsd && (
<TokenDisplay
style={styles.rewardFiatAmount}
amount={claimableValueUsd}
currency={Currency.Dollar}
showLocalAmount={true}
testID="DappShortcutsRewards/RewardAmountFiat"
/>
)}
</View>
<Button
onPress={handleClaimReward(item)}
text={t('dappShortcuts.claimRewardsScreen.claimButton')}
size={BtnSizes.SMALL}
touchableStyle={styles.claimButton}
testID="DappShortcutsRewards/ClaimButton"
/>
</View>
<View style={styles.dappInfoContainer}>
<Image source={{ uri: item.displayProps.imageUrl }} style={styles.dappLogo} />
<Text style={styles.dappName}>{item.appName}</Text>
</View>
</View>
)
}

const renderHeader = () => {
return (
<View style={styles.headerContainer}>
<Text style={styles.heading}>{t('dappShortcuts.claimRewardsScreen.title')}</Text>
<Text style={styles.subHeading}>{t('dappShortcuts.claimRewardsScreen.description')}</Text>
</View>
)
}

return (
<>
<Animated.FlatList
contentContainerStyle={{
paddingHorizontal: Spacing.Thick24,
paddingBottom: insets.bottom,
}}
scrollEventThrottle={16}
renderItem={renderItem}
data={positionsWithClaimableRewards}
ListHeaderComponent={renderHeader}
/>
</>
)
}

const styles = StyleSheet.create({
card: {
borderWidth: 1,
borderColor: Colors.gray2,
borderRadius: 12,
},
rewardInfoContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: Spacing.Regular16,
paddingVertical: Spacing.Small12,
},
rewardAmountContainer: {
flex: 1,
marginRight: Spacing.Small12,
},
rewardLabel: {
...fontStyles.xsmall,
color: Colors.gray3,
},
rewardAmount: {
...fontStyles.large600,
lineHeight: 28,
flexWrap: 'wrap',
},
rewardFiatAmount: {
...fontStyles.small,
},
dappInfoContainer: {
flexDirection: 'row',
paddingHorizontal: Spacing.Regular16,
paddingVertical: Spacing.Small12,
backgroundColor: Colors.gray1,
borderBottomLeftRadius: 12,
borderBottomRightRadius: 12,
},
dappLogo: {
width: 18,
height: 18,
marginRight: Spacing.Smallest8,
backgroundColor: Colors.light,
borderRadius: 100,
},
dappName: {
...fontStyles.small600,
},
headerContainer: {
paddingTop: Spacing.Smallest8,
paddingBottom: Spacing.Thick24,
},
heading: {
...fontStyles.large600,
fontSize: 24,
lineHeight: 32,
marginBottom: Spacing.Tiny4,
},
subHeading: {
...fontStyles.small,
color: Colors.gray3,
},
claimButton: {
minWidth: 72,
},
})

export default DappShortcutsRewards
4 changes: 3 additions & 1 deletion src/dappsExplorer/DappFeaturedActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import Touchable from 'src/components/Touchable'
import { mostPopularDappsSelector } from 'src/dapps/selectors'
import Trophy from 'src/icons/Trophy'
import Wallet from 'src/icons/Wallet'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { positionsWithClaimableRewardsSelector } from 'src/positions/selectors'
import { getExperimentParams, getFeatureGate } from 'src/statsig'
import { ExperimentConfigs } from 'src/statsig/constants'
Expand Down Expand Up @@ -58,7 +60,7 @@ export function DappFeaturedActions({
// TODO impression analytics on scroll

const handleShowRewardsShortcuts = () => {
// TODO
navigate(Screens.DappShortcutsRewards)
}

const scrollEnabled = showDappRankings && showClaimRewards // more than one item in the view
Expand Down
28 changes: 17 additions & 11 deletions src/navigator/Navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
useBottomSheetDynamicSnapPoints,
} from '@gorhom/bottom-sheet'
import {
NativeStackNavigationOptions,
createNativeStackNavigator,
NativeStackNavigationOptions,
} from '@react-navigation/native-stack'
import { createBottomSheetNavigator } from '@th3rdwave/react-navigation-bottom-sheet'
import * as React from 'react'
Expand All @@ -31,11 +31,21 @@ import BackupQuiz, { navOptionsForQuiz } from 'src/backup/BackupQuiz'
import ConsumerIncentivesHomeScreen from 'src/consumerIncentives/ConsumerIncentivesHomeScreen'
import DappKitAccountScreen from 'src/dappkit/DappKitAccountScreen'
import DappKitSignTxScreen from 'src/dappkit/DappKitSignTxScreen'
import DappShortcutsRewards from 'src/dapps/DappShortcutsRewards'
import EscrowedPaymentListScreen from 'src/escrow/EscrowedPaymentListScreen'
import ReclaimPaymentConfirmationScreen from 'src/escrow/ReclaimPaymentConfirmationScreen'
import WithdrawCeloQrScannerScreen from 'src/exchange/WithdrawCeloQrScannerScreen'
import WithdrawCeloReviewScreen from 'src/exchange/WithdrawCeloReviewScreen'
import WithdrawCeloScreen from 'src/exchange/WithdrawCeloScreen'
import FiatDetailsScreen from 'src/fiatconnect/FiatDetailsScreen'
import KycDenied from 'src/fiatconnect/kyc/KycDenied'
import KycExpired from 'src/fiatconnect/kyc/KycExpired'
import KycPending from 'src/fiatconnect/kyc/KycPending'
import KycLanding from 'src/fiatconnect/KycLanding'
import FiatConnectLinkAccountScreen from 'src/fiatconnect/LinkAccountScreen'
import FiatConnectRefetchQuoteScreen from 'src/fiatconnect/RefetchQuoteScreen'
import FiatConnectReviewScreen from 'src/fiatconnect/ReviewScreen'
import FiatConnectTransferStatusScreen from 'src/fiatconnect/TransferStatusScreen'
import BidaliScreen from 'src/fiatExchanges/BidaliScreen'
import CashInSuccess from 'src/fiatExchanges/CashInSuccess'
import CoinbasePayScreen from 'src/fiatExchanges/CoinbasePayScreen'
Expand All @@ -51,15 +61,6 @@ import SelectProviderScreen from 'src/fiatExchanges/SelectProvider'
import SimplexScreen from 'src/fiatExchanges/SimplexScreen'
import Spend, { spendScreenOptions } from 'src/fiatExchanges/Spend'
import WithdrawSpend from 'src/fiatExchanges/WithdrawSpend'
import FiatDetailsScreen from 'src/fiatconnect/FiatDetailsScreen'
import KycLanding from 'src/fiatconnect/KycLanding'
import FiatConnectLinkAccountScreen from 'src/fiatconnect/LinkAccountScreen'
import FiatConnectRefetchQuoteScreen from 'src/fiatconnect/RefetchQuoteScreen'
import FiatConnectReviewScreen from 'src/fiatconnect/ReviewScreen'
import FiatConnectTransferStatusScreen from 'src/fiatconnect/TransferStatusScreen'
import KycDenied from 'src/fiatconnect/kyc/KycDenied'
import KycExpired from 'src/fiatconnect/kyc/KycExpired'
import KycPending from 'src/fiatconnect/kyc/KycPending'
import { currentLanguageSelector } from 'src/i18n/selectors'
import PhoneNumberLookupQuotaScreen from 'src/identity/PhoneNumberLookupQuotaScreen'
import ImportWallet from 'src/import/ImportWallet'
Expand All @@ -78,9 +79,9 @@ import {
noHeaderGestureDisabled,
nuxNavigationOptions,
} from 'src/navigator/Headers'
import { getInitialRoute } from 'src/navigator/initialRoute'
import QRNavigator from 'src/navigator/QRNavigator'
import { Screens } from 'src/navigator/Screens'
import { getInitialRoute } from 'src/navigator/initialRoute'
import { StackParamList } from 'src/navigator/types'
import NftsInfoCarousel from 'src/nfts/NftsInfoCarousel'
import ChooseYourAdventure from 'src/onboarding/ChooseYourAdventure'
Expand Down Expand Up @@ -317,6 +318,11 @@ const consumerIncentivesScreens = (Navigator: typeof Stack) => (
component={ConsumerIncentivesHomeScreen}
options={ConsumerIncentivesHomeScreen.navOptions}
/>
<Navigator.Screen
name={Screens.DappShortcutsRewards}
component={DappShortcutsRewards}
options={headerWithBackButton}
/>
</>
)

Expand Down
1 change: 1 addition & 0 deletions src/navigator/Screens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum Screens {
DappKitAccountScreen = 'DappKitAccountScreen',
DappKitSignTxScreen = 'DappKitSignTxScreen',
DAppsExplorerScreen = 'DAppsExplorerScreen',
DappShortcutsRewards = 'DappShortcutsRewards',
Debug = 'Debug',
DrawerNavigator = 'DrawerNavigator',
EnableBiometry = 'EnableBiometry',
Expand Down
1 change: 1 addition & 0 deletions src/navigator/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export type StackParamList = {
dappKitRequest: SignTxRequest
}
[Screens.DAppsExplorerScreen]: undefined
[Screens.DappShortcutsRewards]: undefined
[Screens.Debug]: undefined
[Screens.DrawerNavigator]: {
initialScreen?: Screens
Expand Down
1 change: 1 addition & 0 deletions src/styles/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StyleSheet } from 'react-native'
const BASE_UNIT = 8

export enum Spacing {
Tiny4 = BASE_UNIT / 2,
Smallest8 = BASE_UNIT,
Small12 = BASE_UNIT * 1.5,
Regular16 = BASE_UNIT * 2,
Expand Down
1 change: 1 addition & 0 deletions test/RootStateSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2337,6 +2337,7 @@
"DAppsExplorerScreen",
"DappKitAccountScreen",
"DappKitSignTxScreen",
"DappShortcutsRewards",
"Debug",
"DrawerNavigator",
"EnableBiometry",
Expand Down
4 changes: 2 additions & 2 deletions test/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,7 @@ export const mockPositions: Position[] = [
symbol: 'CELO',
decimals: 18,
priceUsd: '0.6959536890241361',
balance: '0.950545800159603456',
balance: '0.950545800159603456', // total USD value = priceUsd * balance = $0.66
category: 'claimable',
},
{
Expand All @@ -1208,7 +1208,7 @@ export const mockPositions: Position[] = [
category: 'claimable',
decimals: 18,
network: 'celo',
balance: '0.098322815093446616',
balance: '0.098322815093446616', // total USD value = priceUsd * balance = $0.00009
symbol: 'UBE',
address: '0x00be915b9dcf56a3cbe739d9b9c202ca692409ec',
},
Expand Down

0 comments on commit 18bbecc

Please sign in to comment.