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(earn): Add Crypto bottom sheet #5376

Merged
merged 20 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from 16 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
17 changes: 17 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2327,6 +2327,23 @@
}
},
"earnFlow": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this (and a few other files) conflicts with main. I used earnStablecoin as the key, but I like earnFlow and updated it here https://github.com/valora-inc/wallet/pull/5394/files#diff-d3d67f4b3f8dbec345426f534268e40d9b087dfa5d83750165d63caf136cb24cR2329

"title": "Earn on your stablecoins",
"subtitle": "Deposit today and earn returns",
"description": "If you deposit <0></0>, you can get up to <1></1> at the end of the year!",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these can be removed, these have moved to earnFlow.cta

"addCryptoBottomSheet": {
"title": "Add {{tokenSymbol}} on {{tokenNetwork}}",
"description": "Once you add tokens you'll have to come back to finish depositing into a pool.",
"actions": {
"add": "Buy",
"receive": "Transfer",
"swap": "Swap"
},
"actionDescriptions": {
"add": "Buy {{tokenSymbol}} on {{tokenNetwork}} using one of our trusted providers",
"receive": "Use any {{tokenNetwork}} compatible wallet or exchange to deposit {{tokenSymbol}}",
"swap": "Swap into {{tokenSymbol}} from another {{tokenNetwork}} token"
}
},
"cta": {
"title": "Earn on your stablecoins",
"subtitle": "Deposit today and earn returns",
Expand Down
4 changes: 4 additions & 0 deletions src/analytics/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -678,3 +678,7 @@ export enum PointsEvents {
export enum EarnEvents {
earn_cta_press = 'earn_cta_press',
}

export enum EarnEvents {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add to existing enum

earn_tap_add_crypto_action = 'earn_tap_add_crypto_action',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
earn_tap_add_crypto_action = 'earn_tap_add_crypto_action',
earn_add_crypto_action_press = 'earn_add_crypto_action_press',

to be consistent with existing earn event

}
12 changes: 9 additions & 3 deletions src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import { PointsActivityId } from 'src/points/types'
import { RecipientType } from 'src/recipients/recipient'
import { AmountEnteredIn, QrCode } from 'src/send/types'
import { Field } from 'src/swap/types'
import { TokenDetailsActionName } from 'src/tokens/types'
import { TokenActionName } from 'src/tokens/types'
import { NetworkId, TokenTransactionTypeV2, TransactionStatus } from 'src/transactions/types'

type Web3LibraryProps = { web3Library: 'contract-kit' | 'viem' }
Expand Down Expand Up @@ -1393,11 +1393,11 @@ interface AssetsEventsProperties {
} & TokenProperties)
[AssetsEvents.tap_claim_rewards]: undefined
[AssetsEvents.tap_token_details_action]: {
action: TokenDetailsActionName
action: TokenActionName
} & TokenProperties
[AssetsEvents.tap_token_details_learn_more]: TokenProperties
[AssetsEvents.tap_token_details_bottom_sheet_action]: {
action: TokenDetailsActionName
action: TokenActionName
} & TokenProperties
[AssetsEvents.import_token_screen_open]: undefined
[AssetsEvents.import_token_submit]: {
Expand Down Expand Up @@ -1577,6 +1577,12 @@ interface EarnEventsProperties {
[EarnEvents.earn_cta_press]: undefined
}

interface EarnEventsProperties {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add to existing enum

[EarnEvents.earn_tap_add_crypto_action]: {
action: TokenActionName
} & TokenProperties
}

export type AnalyticsPropertiesList = AppEventsProperties &
HomeEventsProperties &
SettingsEventsProperties &
Expand Down
1 change: 1 addition & 0 deletions src/analytics/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
[TransactionDetailsEvents.transaction_details_tap_check_status]: `When a user press 'Check status' on transaction details page`,
[TransactionDetailsEvents.transaction_details_tap_retry]: `When a user press 'Retry' on transaction details page`,
[TransactionDetailsEvents.transaction_details_tap_block_explorer]: `When a user press 'View on block explorer' on transaction details page`,
[EarnEvents.earn_tap_add_crypto_action]: `When a user in the Earn flow enters an amount higher than their balance and chooses an option to add crypto`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

group with other earn events


// Events related to earn program
[EarnEvents.earn_cta_press]: `When a user taps on the earn your stablecoins CTA on the discover tab`,
Expand Down
138 changes: 138 additions & 0 deletions src/earn/EarnAddCryptoBottomSheet.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { fireEvent, render } 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 EarnAddCryptoBottomSheet from 'src/earn/EarnAddCryptoBottomSheet'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { StoredTokenBalance, TokenBalance } from 'src/tokens/slice'
import { TokenActionName } from 'src/tokens/types'
import { NetworkId } from 'src/transactions/types'
import { createMockStore } from 'test/utils'

jest.mock('src/statsig', () => ({
getDynamicConfigParams: jest.fn(() => {
return {
showCico: ['arbitrum-sepolia'],
showSwap: ['arbitrum-sepolia'],
}
}),
getFeatureGate: jest.fn().mockReturnValue(false),
}))

const mockStoredArbitrumUsdcTokenBalance: StoredTokenBalance = {
tokenId: 'arbitrum-sepolia:0x123',
priceUsd: '1.16',
address: '0x123',
isNative: false,
symbol: 'USDC',
imageUrl:
'https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_CELO.png',
name: 'USDC',
decimals: 6,
balance: '5',
isFeeCurrency: true,
canTransferWithComment: false,
priceFetchedAt: Date.now(),
networkId: NetworkId['arbitrum-sepolia'],
isSwappable: true,
isCashInEligible: true,
isCashOutEligible: true,
}

const mockArbitrumUsdcBalance: TokenBalance = {
...mockStoredArbitrumUsdcTokenBalance,
balance: new BigNumber(mockStoredArbitrumUsdcTokenBalance.balance!),
lastKnownPriceUsd: new BigNumber(mockStoredArbitrumUsdcTokenBalance.priceUsd!),
priceUsd: new BigNumber(mockStoredArbitrumUsdcTokenBalance.priceUsd!),
}

const store = createMockStore({
tokens: {
tokenBalances: {
['arbitrum-sepolia:0x123']: {
...mockStoredArbitrumUsdcTokenBalance,
balance: `${mockStoredArbitrumUsdcTokenBalance.balance!}`,
},
['arbitrum-sepolia:0x456']: {
...mockStoredArbitrumUsdcTokenBalance,
address: '0x456',
tokenId: 'arbitrum-sepolia:0x456',
balance: `${mockStoredArbitrumUsdcTokenBalance.balance!}`,
},
},
},
app: {
showSwapMenuInDrawerMenu: true,
},
})

describe('EarnAddCryptoBottomSheet', () => {
it('Renders correct actions', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also add a test that asserts the not visible case for actions?

const { getByText } = render(
<Provider store={store}>
<EarnAddCryptoBottomSheet
forwardedRef={{ current: null }}
token={mockArbitrumUsdcBalance}
tokenAmount={new BigNumber(100)}
/>
</Provider>
)

expect(getByText('earnFlow.addCryptoBottomSheet.actions.receive')).toBeTruthy()
expect(getByText('earnFlow.addCryptoBottomSheet.actions.swap')).toBeTruthy()
expect(getByText('earnFlow.addCryptoBottomSheet.actions.add')).toBeTruthy()
})

it.each([
{
actionName: TokenActionName.Add,
actionTitle: 'earnFlow.addCryptoBottomSheet.actions.add',
navigateScreen: Screens.SelectProvider,
navigateProps: {
amount: { crypto: 100, fiat: 154 },
flow: 'CashIn',
tokenId: 'arbitrum-sepolia:0x123',
},
},
{
actionName: TokenActionName.Transfer,
actionTitle: 'earnFlow.addCryptoBottomSheet.actions.receive',
navigateScreen: Screens.ExchangeQR,
navigateProps: { exchanges: [], flow: 'CashIn' },
},
{
actionName: TokenActionName.Swap,
actionTitle: 'earnFlow.addCryptoBottomSheet.actions.swap',
navigateScreen: Screens.SwapScreenWithBack,
navigateProps: { toTokenId: 'arbitrum-sepolia:0x123' },
},
])(
'triggers the correct analytics and navigation for $actionName',
async ({ actionName, actionTitle, navigateScreen, navigateProps }) => {
const { getByText } = render(
<Provider store={store}>
<EarnAddCryptoBottomSheet
forwardedRef={{ current: null }}
token={mockArbitrumUsdcBalance}
tokenAmount={new BigNumber(100)}
/>
</Provider>
)

fireEvent.press(getByText(actionTitle))
expect(ValoraAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_tap_add_crypto_action, {
action: actionName,
address: '0x123',
balanceUsd: 5.8,
networkId: mockArbitrumUsdcBalance.networkId,
symbol: mockArbitrumUsdcBalance.symbol,
tokenId: 'arbitrum-sepolia:0x123',
})

expect(navigate).toHaveBeenCalledWith(navigateScreen, navigateProps)
}
)
})
164 changes: 164 additions & 0 deletions src/earn/EarnAddCryptoBottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import BigNumber from 'bignumber.js'
import React, { RefObject } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { useSelector } from 'react-redux'
import { EarnEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import BottomSheet, { BottomSheetRefType } from 'src/components/BottomSheet'
import Touchable from 'src/components/Touchable'
import { CICOFlow } from 'src/fiatExchanges/utils'
import QuickActionsAdd from 'src/icons/quick-actions/Add'
import QuickActionsSend from 'src/icons/quick-actions/Send'
import QuickActionsSwap from 'src/icons/quick-actions/Swap'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { isAppSwapsEnabledSelector } from 'src/navigator/selectors'
import { NETWORK_NAMES } from 'src/shared/conts'
import { Colors } from 'src/styles/colors'
import { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import { useCashInTokens, useSwappableTokens, useTokenToLocalAmount } from 'src/tokens/hooks'
import { TokenBalance } from 'src/tokens/slice'
import { TokenActionName } from 'src/tokens/types'
import { getTokenAnalyticsProps } from 'src/tokens/utils'

export default function EarnAddCryptoBottomSheet({
forwardedRef,
token,
tokenAmount,
}: {
forwardedRef: RefObject<BottomSheetRefType>
token: TokenBalance
tokenAmount: BigNumber
}) {
const { t } = useTranslation()
const { swappableFromTokens } = useSwappableTokens()
const cashInTokens = useCashInTokens()
const isSwapEnabled = useSelector(isAppSwapsEnabledSelector)

const showAdd = !!cashInTokens.find((tokenInfo) => tokenInfo.tokenId === token.tokenId)
const showSwap =
isSwapEnabled &&
!!swappableFromTokens.find(
(tokenInfo) => tokenInfo.networkId === token.networkId && tokenInfo.tokenId !== token.tokenId
)
const addAmount = {
crypto: tokenAmount.toNumber(),
fiat: Math.round(
(useTokenToLocalAmount(tokenAmount, token.tokenId) || new BigNumber(0)).toNumber()
),
}

const actions = [
{
name: TokenActionName.Add,
title: t('earnFlow.addCryptoBottomSheet.actions.add'),
details: t('earnFlow.addCryptoBottomSheet.actionDescriptions.add', {
tokenSymbol: token.symbol,
tokenNetwork: NETWORK_NAMES[token.networkId],
}),
iconComponent: QuickActionsAdd,
onPress: () => {
navigate(Screens.SelectProvider, {
tokenId: token.tokenId,
flow: CICOFlow.CashIn,
amount: addAmount,
})
},
visible: showAdd,
},
{
name: TokenActionName.Transfer,
title: t('earnFlow.addCryptoBottomSheet.actions.receive'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

receive -> transfer

details: t('earnFlow.addCryptoBottomSheet.actionDescriptions.receive', {
tokenSymbol: token.symbol,
tokenNetwork: NETWORK_NAMES[token.networkId],
}),
iconComponent: QuickActionsSend,
onPress: () => {
navigate(Screens.ExchangeQR, { flow: CICOFlow.CashIn, exchanges: [] })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are exchanges supposed to be empty for the MVP?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding in a follow-up PR

},
visible: true,
},
{
name: TokenActionName.Swap,
title: t('earnFlow.addCryptoBottomSheet.actions.swap'),
details: t('earnFlow.addCryptoBottomSheet.actionDescriptions.swap', {
tokenSymbol: token.symbol,
tokenNetwork: NETWORK_NAMES[token.networkId],
}),
iconComponent: QuickActionsSwap,
onPress: () => {
navigate(Screens.SwapScreenWithBack, { toTokenId: token.tokenId })
},
visible: showSwap,
},
].filter((action) => action.visible)

return (
<BottomSheet
forwardedRef={forwardedRef}
title={t('earnFlow.addCryptoBottomSheet.title', {
tokenSymbol: token.symbol,
tokenNetwork: NETWORK_NAMES[token.networkId],
})}
description={t('earnFlow.addCryptoBottomSheet.description')}
testId={'Earn/AddCrypto'}
titleStyle={styles.title}
>
<View style={styles.actionsContainer}>
{actions.map((action) => (
<Touchable
style={styles.touchable}
key={action.name}
borderRadius={20}
onPress={() => {
ValoraAnalytics.track(EarnEvents.earn_tap_add_crypto_action, {
action: action.name,
...getTokenAnalyticsProps(token),
})
action.onPress()
}}
testID={`Earn/AddCrypto/${action.name}`}
>
<>
<action.iconComponent color={Colors.black} />
<View style={{ flex: 1 }}>
<Text style={styles.actionTitle}>{action.title}</Text>
<Text style={styles.actionDetails}>{action.details}</Text>
</View>
</>
</Touchable>
))}
</View>
</BottomSheet>
)
}

const styles = StyleSheet.create({
actionsContainer: {
flex: 1,
gap: Spacing.Regular16,
marginVertical: Spacing.Thick24,
},
actionTitle: {
...typeScale.labelMedium,
color: Colors.black,
},
actionDetails: {
...typeScale.bodySmall,
color: Colors.black,
},
title: {
...typeScale.titleSmall,
color: Colors.black,
},
touchable: {
backgroundColor: Colors.gray1,
padding: Spacing.Regular16,
flexDirection: 'row',
gap: Spacing.Regular16,
alignItems: 'center',
},
})
Loading
Loading