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 all 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
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": {
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

"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
Loading