From 96c9e13f125d455b5c13a86f23fa3934ccfb5904 Mon Sep 17 00:00:00 2001 From: Tom McGuire Date: Fri, 24 May 2024 12:35:45 -0700 Subject: [PATCH] feat(earn): add earn info screen (#5456) ### Description Adds the earn info screen in a stable coin agnostic way and passes the tokenId to `EarnEnterAmount.tsx`. #### Screenshots | iOS | Android | | ----- | ----- | | ![](https://github.com/valora-inc/wallet/assets/26950305/82fbbaf6-10ee-4341-86a0-7f3b3046cb71) | ![](https://github.com/valora-inc/wallet/assets/26950305/bc1fea45-2754-4106-9cda-7247b42da117) | ### Test plan For easy testing I hooked it up the screen in `src/home/NotificationBell.tsx` ```TypeScript const onPress = () => { ValoraAnalytics.track(HomeEvents.notification_bell_pressed, { hasNotifications }) - navigate(Screens.NotificationCenter) + navigate(Screens.EarnInfoScreen, { tokenId: networkConfig.arbUsdcTokenId }) } ``` - [x] Tested locally on iOS - [x] Tested locally on Android - [x] Unit tests added ### Related issues - Fixed ACT-1192 ### 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) --- branding/celo/src/brandingConfig.ts | 1 + locales/base/translation.json | 23 ++++ src/analytics/Events.tsx | 2 + src/analytics/Properties.tsx | 4 + src/analytics/docs.ts | 2 + src/earn/EarnInfoScreen.test.tsx | 97 +++++++++++++++ src/earn/EarnInfoScreen.tsx | 185 ++++++++++++++++++++++++++++ src/icons/ArrowDown.tsx | 20 +++ src/icons/Blob.tsx | 21 ++++ src/icons/Palm.tsx | 21 ++++ src/navigator/Navigator.tsx | 6 + src/navigator/Screens.tsx | 1 + src/navigator/types.tsx | 3 + test/RootStateSchema.json | 1 + 14 files changed, 387 insertions(+) create mode 100644 src/earn/EarnInfoScreen.test.tsx create mode 100644 src/earn/EarnInfoScreen.tsx create mode 100644 src/icons/ArrowDown.tsx create mode 100644 src/icons/Blob.tsx create mode 100644 src/icons/Palm.tsx diff --git a/branding/celo/src/brandingConfig.ts b/branding/celo/src/brandingConfig.ts index 5e008c5b1a2..75f8ff8412a 100644 --- a/branding/celo/src/brandingConfig.ts +++ b/branding/celo/src/brandingConfig.ts @@ -20,3 +20,4 @@ export const INVITE_REWARDS_NFTS_LEARN_MORE = 'https://valoraapp.com/support/invite-rewards-nfts-learn-more' export const INVITE_REWARDS_STABLETOKEN_LEARN_MORE = 'https://valoraapp.com/support/invite-rewards-stabletoken-learn-more' +export const EARN_STABLECOINS_LEARN_MORE = 'https://valoraapp.com/support/earn-on-your-stablecoins' diff --git a/locales/base/translation.json b/locales/base/translation.json index ca6686801fd..3a639d7747c 100644 --- a/locales/base/translation.json +++ b/locales/base/translation.json @@ -2377,6 +2377,29 @@ "subtitle": "Deposit today and earn returns", "description": "See how much you can earn when supplying a lending pool" }, + "earnInfo": { + "title": "Earn on your\nstablecoins", + "details": { + "earn": { + "titleGasSubsidy": "Maximize Earnings without Gas Fees*", + "title": "Maximize Earnings", + "subtitle": "Earn on your stablecoins and get rewards when you supply a pool.", + "footnoteSubsidy": "*Gas fees are temporarily covered by Valora" + }, + "manage": { + "title": "Manage Directly from Valora", + "subtitle": "We’ll take care of the setup so you can manage your position directly from Valora." + }, + "access": { + "title": "Easily Access your Funds", + "subtitle": "Collect your funds effortlessly with a single tap. Enjoy the same instant access as you would directly through your favorite protocols." + } + }, + "action": { + "learn": "Learn More", + "earn": "Start Earning" + } + }, "ctaV1_86": { "title": "Maximize earnings on stablecoins", "subtitle": "Buy {{symbol}}", diff --git a/src/analytics/Events.tsx b/src/analytics/Events.tsx index 157d42ccf96..82e710a80c1 100644 --- a/src/analytics/Events.tsx +++ b/src/analytics/Events.tsx @@ -703,4 +703,6 @@ export enum EarnEvents { earn_withdraw_submit_error = 'earn_withdraw_submit_error', earn_withdraw_submit_cancel = 'earn_withdraw_submit_cancel', earn_withdraw_add_gas_press = 'earn_withdraw_add_gas_press', + earn_info_learn_press = 'earn_info_learn_press', + earn_info_earn_press = 'earn_info_earn_press', } diff --git a/src/analytics/Properties.tsx b/src/analytics/Properties.tsx index 3849a9c5108..7b22f7d7932 100644 --- a/src/analytics/Properties.tsx +++ b/src/analytics/Properties.tsx @@ -1651,6 +1651,10 @@ interface EarnEventsProperties { } [EarnEvents.earn_withdraw_submit_cancel]: EarnWithdrawProperties [EarnEvents.earn_withdraw_add_gas_press]: { gasTokenId: string } + [EarnEvents.earn_info_learn_press]: undefined + [EarnEvents.earn_info_earn_press]: { + tokenId: string + } } export type AnalyticsPropertiesList = AppEventsProperties & diff --git a/src/analytics/docs.ts b/src/analytics/docs.ts index 870d12546ef..2e3039957c1 100644 --- a/src/analytics/docs.ts +++ b/src/analytics/docs.ts @@ -618,6 +618,8 @@ export const eventDocs: Record = { [EarnEvents.earn_withdraw_submit_error]: `When the withdraw and claim transactions fail`, [EarnEvents.earn_withdraw_submit_cancel]: `When the user cancels the withdraw and claim transactions after submitting by cancelling PIN input`, [EarnEvents.earn_withdraw_add_gas_press]: `When the user doesn't have enough for gas and clicks on the button to add gas token`, + [EarnEvents.earn_info_learn_press]: `When the user taps 'Learn More' on the earn info page`, + [EarnEvents.earn_info_earn_press]: `When the user taps 'Start Earning' on the earn info page `, // Legacy event docs // The below events had docs, but are no longer produced by the latest app version. diff --git a/src/earn/EarnInfoScreen.test.tsx b/src/earn/EarnInfoScreen.test.tsx new file mode 100644 index 00000000000..f93644e3f0a --- /dev/null +++ b/src/earn/EarnInfoScreen.test.tsx @@ -0,0 +1,97 @@ +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 { EARN_STABLECOINS_LEARN_MORE } from 'src/config' +import EarnInfoScreen from 'src/earn/EarnInfoScreen' +import { navigate } from 'src/navigator/NavigationService' +import { Screens } from 'src/navigator/Screens' +import { getFeatureGate } from 'src/statsig' +import { StatsigFeatureGates } from 'src/statsig/types' +import networkConfig from 'src/web3/networkConfig' +import MockedNavigator from 'test/MockedNavigator' +import { createMockStore } from 'test/utils' + +jest.mock('src/statsig', () => ({ + getFeatureGate: jest.fn(), +})) + +const store = createMockStore({}) +const tokenId = networkConfig.arbUsdcTokenId + +describe('EarnInfoScreen', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should render correctly when no gas subsidy', async () => { + const { getByText, queryByText } = render( + + + + ) + + // First details item - includes subsidy code + expect(getByText('earnFlow.earnInfo.title')).toBeTruthy() + expect(getByText('earnFlow.earnInfo.details.earn.title')).toBeTruthy() + expect(queryByText('earnFlow.earnInfo.details.earn.titleGasSubsidy')).toBeFalsy() + expect(queryByText('earnFlow.earnInfo.details.earn.footnoteSubsidy')).toBeFalsy() + + // Second details item + expect(getByText('earnFlow.earnInfo.details.manage.title')).toBeTruthy() + expect(getByText('earnFlow.earnInfo.details.manage.subtitle')).toBeTruthy() + + // Third details item + expect(getByText('earnFlow.earnInfo.details.access.title')).toBeTruthy() + expect(getByText('earnFlow.earnInfo.details.access.subtitle')).toBeTruthy() + + // Buttons + expect(getByText('earnFlow.earnInfo.action.learn')).toBeTruthy() + expect(getByText('earnFlow.earnInfo.action.earn')).toBeTruthy() + }) + + it('should render correctly when gas subsidy enabled', () => { + jest + .mocked(getFeatureGate) + .mockImplementation((gate) => gate === StatsigFeatureGates.SUBSIDIZE_STABLECOIN_EARN_GAS_FEES) + + const { getByText, queryByText } = render( + + + + ) + + expect(getByText('earnFlow.earnInfo.details.earn.titleGasSubsidy')).toBeTruthy() + expect(getByText('earnFlow.earnInfo.details.earn.footnoteSubsidy')).toBeTruthy() + expect(queryByText('earnFlow.earnInfo.details.earn.title')).toBeFalsy() + }) + + it('should navigate and fire analytics correctly on Learn More button press', () => { + const { getByText } = render( + + + + ) + + fireEvent.press(getByText('earnFlow.earnInfo.action.learn')) + expect(navigate).toHaveBeenCalledWith(Screens.WebViewScreen, { + uri: EARN_STABLECOINS_LEARN_MORE, + }) + expect(ValoraAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_info_learn_press) + }) + + it('should navigate and fire analytics correctly on Start Earning button press', () => { + const { getByText } = render( + + + + ) + + fireEvent.press(getByText('earnFlow.earnInfo.action.earn')) + expect(navigate).toHaveBeenCalledWith(Screens.EarnEnterAmount, { + tokenId, + }) + expect(ValoraAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_info_earn_press, { tokenId }) + }) +}) diff --git a/src/earn/EarnInfoScreen.tsx b/src/earn/EarnInfoScreen.tsx new file mode 100644 index 00000000000..71cb3d78065 --- /dev/null +++ b/src/earn/EarnInfoScreen.tsx @@ -0,0 +1,185 @@ +import { useHeaderHeight } from '@react-navigation/elements' +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { ReactElement } from 'react' +import { useTranslation } from 'react-i18next' +import { Platform, ScrollView, StyleSheet, Text, View } from 'react-native' +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' +import { EarnEvents } from 'src/analytics/Events' +import ValoraAnalytics from 'src/analytics/ValoraAnalytics' +import Button, { BtnSizes, BtnTypes } from 'src/components/Button' +import { EARN_STABLECOINS_LEARN_MORE } from 'src/config' +import ArrowDown from 'src/icons/ArrowDown' +import Blob from 'src/icons/Blob' +import CircledIcon from 'src/icons/CircledIcon' +import Logo from 'src/icons/Logo' +import Palm from 'src/icons/Palm' +import UpwardGraph from 'src/icons/UpwardGraph' +import { headerWithCloseButton } from 'src/navigator/Headers' +import { navigate } from 'src/navigator/NavigationService' +import { Screens } from 'src/navigator/Screens' +import { StackParamList } from 'src/navigator/types' +import { getFeatureGate } from 'src/statsig' +import { StatsigFeatureGates } from 'src/statsig/types' +import Colors from 'src/styles/colors' +import { typeScale } from 'src/styles/fonts' +import { Spacing } from 'src/styles/styles' + +const ICON_SIZE = 24 +const ICON_BACKGROUND_CIRCLE_SIZE = 36 + +function DetailsItem({ + icon, + title, + subtitle, + footnote, +}: { + icon: ReactElement + title: string + subtitle: string + footnote?: string +}) { + return ( + + + {icon} + + + {title} + {subtitle} + {!!footnote && {footnote}} + + + ) +} + +type Props = NativeStackScreenProps + +export default function EarnInfoScreen({ route }: Props) { + const { t } = useTranslation() + const { tokenId } = route.params + const isGasSubsidized = getFeatureGate(StatsigFeatureGates.SUBSIDIZE_STABLECOIN_EARN_GAS_FEES) + + const headerHeight = useHeaderHeight() + const { bottom } = useSafeAreaInsets() + const insetsStyle = Platform.OS === 'android' && { + paddingBottom: Math.max(bottom, Spacing.Regular16), + } + + return ( + + + {t('earnFlow.earnInfo.title')} + + } + title={ + isGasSubsidized + ? t('earnFlow.earnInfo.details.earn.titleGasSubsidy') + : t('earnFlow.earnInfo.details.earn.title') + } + subtitle={t('earnFlow.earnInfo.details.earn.subtitle')} + footnote={ + isGasSubsidized ? t('earnFlow.earnInfo.details.earn.footnoteSubsidy') : undefined + } + /> + } + title={t('earnFlow.earnInfo.details.manage.title')} + subtitle={t('earnFlow.earnInfo.details.manage.subtitle')} + /> + } + title={t('earnFlow.earnInfo.details.access.title')} + subtitle={t('earnFlow.earnInfo.details.access.subtitle')} + /> + + + +