-
Notifications
You must be signed in to change notification settings - Fork 97
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
Changes from 16 commits
3d51fe5
5af8fa3
3d4e39c
8d5c045
fa72cdb
7803b50
4765c22
c8691b5
0968b7f
f3720a2
ded18ff
07035ab
7b24cf3
bd6dbd3
de4a527
17316ef
13d40d4
d92c733
5866b76
418d8c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2327,6 +2327,23 @@ | |
} | ||
}, | ||
"earnFlow": { | ||
"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!", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these can be removed, these have moved to |
||
"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", | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -678,3 +678,7 @@ export enum PointsEvents { | |||||
export enum EarnEvents { | ||||||
earn_cta_press = 'earn_cta_press', | ||||||
} | ||||||
|
||||||
export enum EarnEvents { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
to be consistent with existing earn event |
||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' } | ||
|
@@ -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]: { | ||
|
@@ -1577,6 +1577,12 @@ interface EarnEventsProperties { | |
[EarnEvents.earn_cta_press]: undefined | ||
} | ||
|
||
interface EarnEventsProperties { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 & | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`, | ||
|
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', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
) | ||
}) |
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'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: [] }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are exchanges supposed to be empty for the MVP? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
}, | ||
}) |
There was a problem hiding this comment.
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 likeearnFlow
and updated it here https://github.com/valora-inc/wallet/pull/5394/files#diff-d3d67f4b3f8dbec345426f534268e40d9b087dfa5d83750165d63caf136cb24cR2329