Skip to content

Commit

Permalink
feat(earn): Add Crypto bottom sheet (#5376)
Browse files Browse the repository at this point in the history
### Description

Creates new component for the Earn "Add Crypto" bottom sheet. This
component should be used in the same way as the TokenDetailsMoreActions
component [is
used](https://github.com/valora-inc/wallet/blob/6df3859e7271db5e643aba11eda8cde19151135d/src/tokens/TokenDetails.tsx#L119).

Decided to make a new component as to avoid adding extra props to decide
copy, choose actions, and add an amount prop which would not be used in
the TokenDetailsMoreActions case.

Talked with product and Nitya and confirmed that:
- On the QR code page, have title be "Deposit Crypto" like in the
cash-in exchange case
- Use "Arbitrum One" as the network name in copy

Note: Price is not passed to the swap component, there is not support
for that and based on discussion in quick sync it seems hard.

### Test plan

Unit tests added.

**Manual testing:**
Bottom sheet:

![bottom-sheet](https://github.com/valora-inc/wallet/assets/140328381/0a029234-6bba-46dd-8512-6a73a12b660a)

Tapping Buy:


https://github.com/valora-inc/wallet/assets/140328381/2a8985ca-9ae2-41e1-b0f7-abed4071b723

Tapping Transfer:


https://github.com/valora-inc/wallet/assets/140328381/61e38183-d641-43c8-9d2d-39af4236215d



Tapping Swap:


https://github.com/valora-inc/wallet/assets/140328381/393d00a6-41d7-49fc-a0f7-162e1ed15e71


Tapping 

### Related issues

- Fixes #ACT-1177

### Backwards compatibility

Yes, new component so not changing past stuff

### 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
finnian0826 authored May 9, 2024
1 parent d5e7740 commit 7d421cf
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 21 deletions.
14 changes: 14 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2332,6 +2332,20 @@
}
},
"earnFlow": {
"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",
"transfer": "Transfer",
"swap": "Swap"
},
"actionDescriptions": {
"add": "Buy {{tokenSymbol}} on {{tokenNetwork}} using one of our trusted providers",
"transfer": "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
1 change: 1 addition & 0 deletions src/analytics/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -677,4 +677,5 @@ export enum PointsEvents {

export enum EarnEvents {
earn_cta_press = 'earn_cta_press',
earn_add_crypto_action_press = 'earn_add_crypto_action_press',
}
9 changes: 6 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 @@ -1575,6 +1575,9 @@ interface PointsEventsProperties {

interface EarnEventsProperties {
[EarnEvents.earn_cta_press]: undefined
[EarnEvents.earn_add_crypto_action_press]: {
action: TokenActionName
} & TokenProperties
}

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 @@ -592,6 +592,7 @@ export const eventDocs: Record<AnalyticsEventType, string> = {

// Events related to earn program
[EarnEvents.earn_cta_press]: `When a user taps on the earn your stablecoins CTA on the discover tab`,
[EarnEvents.earn_add_crypto_action_press]: `When a user in the Earn flow enters an amount higher than their balance and chooses an option to add crypto`,

// Legacy event docs
// The below events had docs, but are no longer produced by the latest app version.
Expand Down
190 changes: 190 additions & 0 deletions src/earn/EarnAddCryptoBottomSheet.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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 { getDynamicConfigParams } from 'src/statsig'
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(),
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', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.mocked(getDynamicConfigParams).mockReturnValue({
showCico: ['arbitrum-sepolia'],
showSwap: ['arbitrum-sepolia'],
})
})
it('Renders all actions', () => {
const { getByText } = render(
<Provider store={store}>
<EarnAddCryptoBottomSheet
forwardedRef={{ current: null }}
token={mockArbitrumUsdcBalance}
tokenAmount={new BigNumber(100)}
/>
</Provider>
)

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

it('Does not render swap action when no tokens available to swap', () => {
const mockStore = createMockStore({
tokens: {
tokenBalances: {
['arbitrum-sepolia:0x123']: {
...mockStoredArbitrumUsdcTokenBalance,
balance: `${mockStoredArbitrumUsdcTokenBalance.balance!}`,
},
},
},
app: {
showSwapMenuInDrawerMenu: true,
},
})
const { getByText, queryByText } = render(
<Provider store={mockStore}>
<EarnAddCryptoBottomSheet
forwardedRef={{ current: null }}
token={mockArbitrumUsdcBalance}
tokenAmount={new BigNumber(100)}
/>
</Provider>
)

expect(getByText('earnFlow.addCryptoBottomSheet.actions.transfer')).toBeTruthy()
expect(queryByText('earnFlow.addCryptoBottomSheet.actions.swap')).toBeFalsy()
expect(getByText('earnFlow.addCryptoBottomSheet.actions.add')).toBeTruthy()
})

it('Does not render swap or add when network is not in dynamic config', () => {
jest.mocked(getDynamicConfigParams).mockReturnValue({
showCico: ['ethereum-sepolia'],
showSwap: ['ethereum-sepolia'],
})
const { getByText, queryByText } = render(
<Provider store={store}>
<EarnAddCryptoBottomSheet
forwardedRef={{ current: null }}
token={mockArbitrumUsdcBalance}
tokenAmount={new BigNumber(100)}
/>
</Provider>
)

expect(getByText('earnFlow.addCryptoBottomSheet.actions.transfer')).toBeTruthy()
expect(queryByText('earnFlow.addCryptoBottomSheet.actions.swap')).toBeFalsy()
expect(queryByText('earnFlow.addCryptoBottomSheet.actions.add')).toBeFalsy()
})

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.transfer',
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_add_crypto_action_press, {
action: actionName,
address: '0x123',
balanceUsd: 5.8,
networkId: mockArbitrumUsdcBalance.networkId,
symbol: mockArbitrumUsdcBalance.symbol,
tokenId: 'arbitrum-sepolia:0x123',
})

expect(navigate).toHaveBeenCalledWith(navigateScreen, navigateProps)
}
)
})
Loading

0 comments on commit 7d421cf

Please sign in to comment.