Skip to content

Commit

Permalink
feat(earn): add cta to discover tab (#5381)
Browse files Browse the repository at this point in the history
### Description

Add CTA component and add to discover tab behind feature gate. 

[Figma](https://www.figma.com/file/E1rC3MG74qEg5V4tvbeUnU/Stablecoin-Enablement?type=design&node-id=2472-13347&mode=design&t=oKYLUKxeE593WxAR-0)

### Test plan

<img
src="https://github.com/valora-inc/wallet/assets/5062591/8ac09018-d775-46dc-81d2-f3098e7260ea"
width="250" />



### Related issues

- Part of ACT-1174

### Backwards compatibility

Yes

### 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
satish-ravi authored May 3, 2024
1 parent 1d404c5 commit fbf9d4a
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 2 deletions.
5 changes: 5 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2325,5 +2325,10 @@
"title": "More coming soon!"
}
}
},
"earnStablecoin": {
"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!"
}
}
4 changes: 4 additions & 0 deletions src/analytics/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -674,3 +674,7 @@ export enum PointsEvents {
points_screen_activity_fetch_more = 'points_screen_activity_fetch_more',
points_screen_activity_learn_more_press = 'points_screen_activity_learn_more_press',
}

export enum EarnEvents {
earn_cta_press = 'earn_cta_press',
}
8 changes: 7 additions & 1 deletion src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DappExplorerEvents,
DappKitEvents,
DappShortcutsEvents,
EarnEvents,
EscrowEvents,
FeeEvents,
FiatExchangeEvents,
Expand Down Expand Up @@ -1572,6 +1573,10 @@ interface PointsEventsProperties {
[PointsEvents.points_screen_activity_learn_more_press]: undefined
}

interface EarnEventsProperties {
[EarnEvents.earn_cta_press]: undefined
}

export type AnalyticsPropertiesList = AppEventsProperties &
HomeEventsProperties &
SettingsEventsProperties &
Expand Down Expand Up @@ -1607,6 +1612,7 @@ export type AnalyticsPropertiesList = AppEventsProperties &
BuilderHooksProperties &
DappShortcutsProperties &
TransactionDetailsProperties &
PointsEventsProperties
PointsEventsProperties &
EarnEventsProperties

export type AnalyticsEventType = keyof AnalyticsPropertiesList
4 changes: 4 additions & 0 deletions src/analytics/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
DappExplorerEvents,
DappKitEvents,
DappShortcutsEvents,
EarnEvents,
EscrowEvents,
FeeEvents,
FiatExchangeEvents,
Expand Down Expand Up @@ -589,6 +590,9 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
[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`,

// Events related to earn program
[EarnEvents.earn_cta_press]: `When a user taps on the earn your stablecoins CTA on the discover tab`,

// Legacy event docs
// The below events had docs, but are no longer produced by the latest app version.
// [HomeEvents.home_send]: `when "send" button is pressed from home screen send or request bar (NOT from home screen actions)`,
Expand Down
29 changes: 28 additions & 1 deletion src/dappsExplorer/TabDiscover.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { dappSelected, favoriteDapp, fetchDappsList, unfavoriteDapp } from 'src/dapps/slice'
import { DappCategory, DappSection } from 'src/dapps/types'
import TabDiscover from 'src/dappsExplorer/TabDiscover'
import { getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import MockedNavigator from 'test/MockedNavigator'
import { createMockStore } from 'test/utils'
import { mockDappListWithCategoryNames } from 'test/values'
Expand All @@ -16,7 +18,7 @@ jest.mock('src/statsig', () => ({
dappsFilterEnabled: true,
dappsSearchEnabled: true,
})),
getFeatureGate: jest.fn(() => true),
getFeatureGate: jest.fn(),
}))

const dappsList = mockDappListWithCategoryNames
Expand Down Expand Up @@ -44,6 +46,7 @@ describe('TabDiscover', () => {
beforeEach(() => {
defaultStore.clearActions()
jest.clearAllMocks()
jest.mocked(getFeatureGate).mockReturnValue(false)
})

it('renders correctly and fires the correct actions on press dapp', () => {
Expand Down Expand Up @@ -724,4 +727,28 @@ describe('TabDiscover', () => {
})
})
})

describe('earn', () => {
it('does not display earn cta if feature gate is false', () => {
const { queryByTestId } = render(
<Provider store={defaultStore}>
<MockedNavigator component={TabDiscover} />
</Provider>
)

expect(queryByTestId('EarnCta')).toBeFalsy()
})
it('displays earn cta if feature gate is true', () => {
jest
.mocked(getFeatureGate)
.mockImplementation((gate) => gate === StatsigFeatureGates.SHOW_STABLECOIN_EARN)
const { getByTestId } = render(
<Provider store={defaultStore}>
<MockedNavigator component={TabDiscover} />
</Provider>
)

expect(getByTestId('EarnCta')).toBeTruthy()
})
})
})
4 changes: 4 additions & 0 deletions src/dappsExplorer/TabDiscover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ import NoResults from 'src/dappsExplorer/NoResults'
import { searchDappList } from 'src/dappsExplorer/searchDappList'
import useDappFavoritedToast from 'src/dappsExplorer/useDappFavoritedToast'
import useOpenDapp from 'src/dappsExplorer/useOpenDapp'
import EarnCta from 'src/earn/EarnCta'
import { currentLanguageSelector } from 'src/i18n/selectors'
import { Screens } from 'src/navigator/Screens'
import useScrollAwareHeader from 'src/navigator/ScrollAwareHeader'
import { StackParamList } from 'src/navigator/types'
import { useDispatch, useSelector } from 'src/redux/hooks'
import { getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import { Colors } from 'src/styles/colors'
import fontStyles, { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
Expand Down Expand Up @@ -234,6 +237,7 @@ function TabDiscover({ navigation }: Props) {
</Text>
}
<DappFeaturedActions onPressShowDappRankings={handleShowDappRankings} />
{getFeatureGate(StatsigFeatureGates.SHOW_STABLECOIN_EARN) && <EarnCta />}
<SearchInput
onChangeText={(text) => {
setSearchTerm(text)
Expand Down
46 changes: 46 additions & 0 deletions src/earn/EarnCta.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { fireEvent, render } from '@testing-library/react-native'
import React from 'react'
import { Provider } from 'react-redux'
import { EarnEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import EarnCta from 'src/earn/EarnCta'
import { NetworkId } from 'src/transactions/types'
import networkConfig from 'src/web3/networkConfig'
import { createMockStore } from 'test/utils'

const store = createMockStore({
tokens: {
tokenBalances: {
[networkConfig.arbUsdcTokenId]: {
tokenId: networkConfig.arbUsdcTokenId,
symbol: 'USDC',
priceUsd: '1',
priceFetchedAt: Date.now(),
networkId: NetworkId['arbitrum-sepolia'],
},
},
},
})

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

it('should render correctly', () => {
const { getByText, getByTestId } = render(
<Provider store={store}>
<EarnCta />
</Provider>
)

expect(getByText('earnStablecoin.title')).toBeTruthy()
expect(getByText('earnStablecoin.subtitle')).toBeTruthy()
expect(getByTestId('EarnCta/Description')).toHaveTextContent(
'earnStablecoin.description10.00 USDC₱1.33'
)

fireEvent.press(getByTestId('EarnCta'))
expect(ValoraAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_cta_press)
})
})
77 changes: 77 additions & 0 deletions src/earn/EarnCta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { EarnEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import TokenDisplay from 'src/components/TokenDisplay'
import Touchable from 'src/components/Touchable'
import EarnAave from 'src/icons/EarnAave'
import Colors from 'src/styles/colors'
import { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import networkConfig from 'src/web3/networkConfig'

export default function EarnCta() {
const { t } = useTranslation()
return (
<Touchable
borderRadius={8}
style={styles.touchable}
onPress={() => {
ValoraAnalytics.track(EarnEvents.earn_cta_press)
}}
testID="EarnCta"
>
<>
<Text style={styles.title}>{t('earnStablecoin.title')}</Text>
<View style={styles.row}>
<EarnAave />
<View style={styles.subtitleContainer}>
<Text style={styles.subtitle}>{t('earnStablecoin.subtitle')}</Text>
<Text style={styles.description} testID="EarnCta/Description">
<Trans i18nKey="earnStablecoin.description">
{/* TODO(ACT-1174): use the right amounts here */}
<TokenDisplay
amount={10}
tokenId={networkConfig.arbUsdcTokenId}
showLocalAmount={false}
/>
<TokenDisplay amount={1} tokenId={networkConfig.arbUsdcTokenId} />
</Trans>
</Text>
</View>
</View>
</>
</Touchable>
)
}

const styles = StyleSheet.create({
touchable: {
padding: Spacing.Regular16,
borderColor: Colors.gray2,
borderWidth: 1,
borderRadius: 8,
marginBottom: Spacing.Thick24,
},
title: {
...typeScale.labelSemiBoldMedium,
color: Colors.black,
},
row: {
flexDirection: 'row',
marginTop: 20,
gap: Spacing.Smallest8,
},
subtitleContainer: {
flex: 1,
},
subtitle: {
...typeScale.labelSemiBoldXSmall,
color: Colors.black,
},
description: {
...typeScale.bodyXSmall,
color: Colors.gray4,
},
})
44 changes: 44 additions & 0 deletions src/icons/EarnAave.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as React from 'react'
import Svg, { ClipPath, Defs, G, LinearGradient, Path, Rect, Stop } from 'react-native-svg'
import Colors from 'src/styles/colors'

const EarnAave = () => (
<Svg width={34} height={34} viewBox="0 0 34 34" fill="none">
<Rect width={32} height={32} rx={16} fill={Colors.successLight} />
<G clipPath="url(#clip0_2472_13704)">
<G>
<Path
d="M26 33.76C30.2857 33.76 33.76 30.2857 33.76 26C33.76 21.7143 30.2857 18.24 26 18.24C21.7143 18.24 18.24 21.7143 18.24 26C18.24 30.2857 21.7143 33.76 26 33.76Z"
fill="url(#paint0_linear_2472_13704)"
/>
<Path
d="M29.3961 29.092L26.7721 22.748C26.6241 22.42 26.4041 22.26 26.1141 22.26H25.8821C25.5921 22.26 25.3721 22.42 25.2241 22.748L24.0821 25.512H23.2181C22.9601 25.514 22.7501 25.722 22.7481 25.982V25.988C22.7501 26.246 22.9601 26.456 23.2181 26.458H23.6821L22.5921 29.092C22.5721 29.15 22.5601 29.21 22.5601 29.272C22.5601 29.42 22.6061 29.536 22.6881 29.626C22.7701 29.716 22.8881 29.76 23.0361 29.76C23.1341 29.758 23.2281 29.728 23.3061 29.67C23.3901 29.612 23.4481 29.528 23.4941 29.432L24.6941 26.456H25.5261C25.7841 26.454 25.9941 26.246 25.9961 25.986V25.974C25.9941 25.716 25.7841 25.506 25.5261 25.504H25.0821L25.9981 23.222L28.4941 29.43C28.5401 29.526 28.5981 29.61 28.6821 29.668C28.7601 29.726 28.8561 29.756 28.9521 29.758C29.1001 29.758 29.2161 29.714 29.3001 29.624C29.3841 29.534 29.4281 29.418 29.4281 29.27C29.4301 29.21 29.4201 29.148 29.3961 29.092Z"
fill="white"
/>
</G>
</G>
<Path
d="M13.3154 16.0464L8.269 21.0929L7.70687 20.5308L13.6692 14.5603L16.9154 17.8066L17.269 18.1601L17.6226 17.8066L22.0146 13.4146L22.3681 13.061L22.0146 12.7074L20.8071 11.5H24.5V15.1929L23.2926 13.9854L22.9393 13.6322L22.5857 13.9852L17.2693 19.2932L14.0226 16.0464L13.669 15.6929L13.3154 16.0464Z"
fill={Colors.successDark}
stroke={Colors.successDark}
/>
<Defs>
<LinearGradient
id="paint0_linear_2472_13704"
x1={31.7857}
y1={21.1317}
x2={20.2389}
y2={30.8473}
gradientUnits="userSpaceOnUse"
>
<Stop stopColor="#B6509E" />
<Stop offset={1} stopColor="#2EBAC6" />
</LinearGradient>
<ClipPath id="clip0_2472_13704">
<Rect width={16} height={16} fill="white" transform="translate(18 18)" />
</ClipPath>
</Defs>
</Svg>
)

export default EarnAave
6 changes: 6 additions & 0 deletions src/web3/networkConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ interface NetworkConfig {
ceurTokenId: string
crealTokenId: string
celoTokenId: string
arbUsdcTokenId: string
spendTokenIds: string[]
saveContactsUrl: string
getPointsConfigUrl: string
Expand Down Expand Up @@ -140,6 +141,9 @@ const CREAL_TOKEN_ID_MAINNET = `${NetworkId['celo-mainnet']}:0xe8537a3d056da4466
const ETH_TOKEN_ID_STAGING = `${NetworkId['ethereum-sepolia']}:native`
const ETH_TOKEN_ID_MAINNET = `${NetworkId['ethereum-mainnet']}:native`

const ARB_USDC_TOKEN_ID_STAGING = `${NetworkId['arbitrum-sepolia']}:0xf3c3351d6bd0098eeb33ca8f830faf2a141ea2e1`
const ARB_USDC_TOKEN_ID_MAINNET = `${NetworkId['arbitrum-one']}:0xaf88d065e77c8cc2239327c5edb3a432268e5831`

const CLOUD_FUNCTIONS_STAGING = 'https://api.alfajores.valora.xyz'
const CLOUD_FUNCTIONS_MAINNET = 'https://api.mainnet.valora.xyz'

Expand Down Expand Up @@ -346,6 +350,7 @@ const networkConfigs: { [testnet: string]: NetworkConfig } = {
ceurTokenId: CEUR_TOKEN_ID_STAGING,
crealTokenId: CREAL_TOKEN_ID_STAGING,
celoTokenId: CELO_TOKEN_ID_STAGING,
arbUsdcTokenId: ARB_USDC_TOKEN_ID_STAGING,
spendTokenIds: [CUSD_TOKEN_ID_STAGING, CEUR_TOKEN_ID_STAGING],
saveContactsUrl: SAVE_CONTACTS_ALFAJORES,
getPointsConfigUrl: GET_POINTS_CONFIG_ALFAJORES,
Expand Down Expand Up @@ -437,6 +442,7 @@ const networkConfigs: { [testnet: string]: NetworkConfig } = {
ceurTokenId: CEUR_TOKEN_ID_MAINNET,
crealTokenId: CREAL_TOKEN_ID_MAINNET,
celoTokenId: CELO_TOKEN_ID_MAINNET,
arbUsdcTokenId: ARB_USDC_TOKEN_ID_MAINNET,
spendTokenIds: [CUSD_TOKEN_ID_MAINNET, CEUR_TOKEN_ID_MAINNET],
saveContactsUrl: SAVE_CONTACTS_MAINNET,
getPointsConfigUrl: GET_POINTS_CONFIG_MAINNET,
Expand Down

0 comments on commit fbf9d4a

Please sign in to comment.