-
Notifications
You must be signed in to change notification settings - Fork 97
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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)
- Loading branch information
Showing
14 changed files
with
387 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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( | ||
<Provider store={store}> | ||
<MockedNavigator component={EarnInfoScreen} params={{ tokenId }} /> | ||
</Provider> | ||
) | ||
|
||
// 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( | ||
<Provider store={store}> | ||
<MockedNavigator component={EarnInfoScreen} params={{ tokenId }} /> | ||
</Provider> | ||
) | ||
|
||
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( | ||
<Provider store={store}> | ||
<MockedNavigator component={EarnInfoScreen} params={{ tokenId }} /> | ||
</Provider> | ||
) | ||
|
||
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( | ||
<Provider store={store}> | ||
<MockedNavigator component={EarnInfoScreen} params={{ tokenId }} /> | ||
</Provider> | ||
) | ||
|
||
fireEvent.press(getByText('earnFlow.earnInfo.action.earn')) | ||
expect(navigate).toHaveBeenCalledWith(Screens.EarnEnterAmount, { | ||
tokenId, | ||
}) | ||
expect(ValoraAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_info_earn_press, { tokenId }) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<View style={styles.detailsItemContainer}> | ||
<CircledIcon backgroundColor={Colors.gray2} radius={ICON_BACKGROUND_CIRCLE_SIZE}> | ||
{icon} | ||
</CircledIcon> | ||
<View style={styles.flex}> | ||
<Text style={styles.detailsItemTitle}>{title}</Text> | ||
<Text style={styles.detailsItemSubtitle}>{subtitle}</Text> | ||
{!!footnote && <Text style={styles.detailsItemFootnote}>{footnote}</Text>} | ||
</View> | ||
</View> | ||
) | ||
} | ||
|
||
type Props = NativeStackScreenProps<StackParamList, Screens.EarnInfoScreen> | ||
|
||
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 ( | ||
<SafeAreaView | ||
style={[styles.safeAreaContainer, { paddingTop: headerHeight }]} | ||
edges={['bottom']} | ||
> | ||
<ScrollView> | ||
<Text style={styles.title}>{t('earnFlow.earnInfo.title')}</Text> | ||
<View style={styles.detailsContainer}> | ||
<DetailsItem | ||
icon={<UpwardGraph size={ICON_SIZE} color={Colors.black} />} | ||
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 | ||
} | ||
/> | ||
<DetailsItem | ||
icon={<Logo size={ICON_SIZE} color={Colors.black} />} | ||
title={t('earnFlow.earnInfo.details.manage.title')} | ||
subtitle={t('earnFlow.earnInfo.details.manage.subtitle')} | ||
/> | ||
<DetailsItem | ||
icon={<ArrowDown size={ICON_SIZE} color={Colors.black} />} | ||
title={t('earnFlow.earnInfo.details.access.title')} | ||
subtitle={t('earnFlow.earnInfo.details.access.subtitle')} | ||
/> | ||
</View> | ||
</ScrollView> | ||
<View style={[styles.buttonContainer, insetsStyle]}> | ||
<Button | ||
onPress={() => { | ||
ValoraAnalytics.track(EarnEvents.earn_info_learn_press) | ||
navigate(Screens.WebViewScreen, { uri: EARN_STABLECOINS_LEARN_MORE }) | ||
}} | ||
text={t('earnFlow.earnInfo.action.learn')} | ||
type={BtnTypes.SECONDARY} | ||
size={BtnSizes.FULL} | ||
/> | ||
<Button | ||
onPress={() => { | ||
ValoraAnalytics.track(EarnEvents.earn_info_earn_press, { tokenId }) | ||
navigate(Screens.EarnEnterAmount, { tokenId }) | ||
}} | ||
text={t('earnFlow.earnInfo.action.earn')} | ||
type={BtnTypes.PRIMARY} | ||
size={BtnSizes.FULL} | ||
/> | ||
</View> | ||
<Blob | ||
style={{ | ||
position: 'absolute', | ||
left: 53, | ||
zIndex: -1, | ||
}} | ||
/> | ||
<Palm | ||
style={{ | ||
position: 'absolute', | ||
bottom: 0, | ||
zIndex: -1, | ||
}} | ||
/> | ||
</SafeAreaView> | ||
) | ||
} | ||
|
||
EarnInfoScreen.navigationOptions = () => ({ | ||
...headerWithCloseButton, | ||
headerTransparent: true, | ||
headerShown: true, | ||
headerStyle: { | ||
backgroundColor: 'transparent', | ||
}, | ||
}) | ||
|
||
const styles = StyleSheet.create({ | ||
safeAreaContainer: { | ||
flex: 1, | ||
paddingHorizontal: Spacing.Regular16, | ||
}, | ||
flex: { | ||
flex: 1, | ||
}, | ||
title: { | ||
color: Colors.black, | ||
textAlign: 'center', | ||
marginBottom: Spacing.Thick24, | ||
...typeScale.titleLarge, | ||
}, | ||
detailsContainer: { | ||
gap: Spacing.Large32, | ||
}, | ||
detailsItemContainer: { | ||
flexDirection: 'row', | ||
gap: Spacing.Regular16, | ||
}, | ||
detailsItemTitle: { | ||
color: Colors.black, | ||
...typeScale.labelSemiBoldSmall, | ||
}, | ||
detailsItemSubtitle: { | ||
color: Colors.black, | ||
...typeScale.bodyXSmall, | ||
}, | ||
detailsItemFootnote: { | ||
color: Colors.black, | ||
marginTop: Spacing.Smallest8, | ||
...typeScale.bodyXXSmall, | ||
}, | ||
buttonContainer: { | ||
gap: Spacing.Smallest8, | ||
marginHorizontal: Spacing.Smallest8, | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import * as React from 'react' | ||
import Svg, { Path } from 'react-native-svg' | ||
import { Colors } from 'src/styles/colors' | ||
|
||
interface Props { | ||
color?: Colors | ||
size?: number | ||
} | ||
|
||
const ArrowDown = ({ color = Colors.black, size = 24 }: Props) => ( | ||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none"> | ||
<Path | ||
id="Vector" | ||
d="M11.053 4.5L12.947 4.5L12.947 15.8636L18.1553 10.6553L19.5 12L12 19.5L4.5 12L5.8447 10.6553L11.053 15.8636L11.053 4.5Z" | ||
fill={color} | ||
/> | ||
</Svg> | ||
) | ||
|
||
export default React.memo(ArrowDown) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import React from 'react' | ||
import { ViewStyle } from 'react-native' | ||
import Svg, { Path } from 'react-native-svg' | ||
|
||
interface Props { | ||
width?: number | ||
height?: number | ||
color?: string | ||
style?: ViewStyle | ||
} | ||
|
||
export default function Blob({ width = 210, height = 212.5, color = '#FFD62C', style }: Props) { | ||
return ( | ||
<Svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} fill="none" style={style}> | ||
<Path | ||
d="M102.846 -29.5006C109.785 -29.5006 116.724 -29.8502 123.638 -29.3508C126.971 -29.1261 130.652 -28.0025 133.388 -26.13C144.53 -18.4399 155.398 -10.3254 166.292 -2.26089C171.167 1.35942 175.693 5.4291 180.593 8.99948C193.277 18.2625 201.832 30.5965 207.776 44.928C210.661 51.894 210.189 59.2594 209.393 66.4501C206.632 91.3429 200.439 115.287 187.979 137.233C179.598 151.989 167.486 163.35 153.608 172.737C149.504 175.509 144.232 177.007 139.282 178.006C129.483 180.003 119.51 182.525 109.636 182.5C96.181 182.475 82.4771 181.276 69.8676 175.709C44.6237 164.548 24.0059 147.945 10.2772 123.451C0.428372 105.924 -2.38205 87.5228 2.02009 67.8233C9.48134 34.5414 27.1396 7.90092 54.3483 -12.0482C67.6044 -21.7607 82.3776 -29.0013 99.3893 -29.7503C100.558 -29.8002 101.727 -29.9251 102.871 -30C102.871 -29.8252 102.871 -29.6504 102.871 -29.4756L102.846 -29.5006Z" | ||
fill={color} | ||
/> | ||
</Svg> | ||
) | ||
} |
Oops, something went wrong.