diff --git a/locales/base/translation.json b/locales/base/translation.json index 1be62160d9..9f83e127b9 100644 --- a/locales/base/translation.json +++ b/locales/base/translation.json @@ -2723,7 +2723,8 @@ "dailyYieldRateDescription": "The daily rate displayed reflects the daily rate provided by {{providerName}}.", "dailyYieldRateLink": "View More Daily Rate Details On {{providerName}}" }, - "viewMoreDetails": "View More Details" + "viewMoreDetails": "View More Details", + "viewLessDetails": "View Less Details" }, "beforeDepositBottomSheet": { "youNeedTitle": "You Need {{tokenSymbol}} on {{tokenNetwork}} to Deposit", diff --git a/src/analytics/Events.tsx b/src/analytics/Events.tsx index 5a72800dce..ba71da2522 100644 --- a/src/analytics/Events.tsx +++ b/src/analytics/Events.tsx @@ -682,4 +682,5 @@ export enum EarnEvents { earn_pool_info_tap_info_icon = 'earn_pool_info_tap_info_icon', earn_pool_info_tap_withdraw = 'earn_pool_info_tap_withdraw', earn_pool_info_tap_deposit = 'earn_pool_info_tap_deposit', + earn_pool_info_tap_safety_details = 'earn_pool_info_tap_safety_details', } diff --git a/src/analytics/Properties.tsx b/src/analytics/Properties.tsx index f9b2254571..81861d68b4 100644 --- a/src/analytics/Properties.tsx +++ b/src/analytics/Properties.tsx @@ -1631,6 +1631,9 @@ interface EarnEventsProperties { hasTokensOnSameNetwork: boolean hasTokensOnOtherNetworks: boolean } + [EarnEvents.earn_pool_info_tap_safety_details]: EarnCommonProperties & { + action: 'expand' | 'collapse' + } } export type AnalyticsPropertiesList = AppEventsProperties & diff --git a/src/analytics/docs.ts b/src/analytics/docs.ts index fe0d04f9c8..4ace34ae8f 100644 --- a/src/analytics/docs.ts +++ b/src/analytics/docs.ts @@ -609,6 +609,7 @@ export const eventDocs: Record = { [EarnEvents.earn_pool_info_tap_info_icon]: `When the user taps an info icon on the earn pool info screen`, [EarnEvents.earn_pool_info_tap_withdraw]: `When the user taps the withdraw button on the pool info screen`, [EarnEvents.earn_pool_info_tap_deposit]: `When the user taps the deposit button on the pool info screen`, + [EarnEvents.earn_pool_info_tap_safety_details]: `When the user taps the view more/less details on the safety card on the pool info screen`, // Legacy event docs // The below events had docs, but are no longer produced by the latest app version. diff --git a/src/earn/EarnPoolInfoScreen.tsx b/src/earn/EarnPoolInfoScreen.tsx index c6422c89cb..51f406464b 100644 --- a/src/earn/EarnPoolInfoScreen.tsx +++ b/src/earn/EarnPoolInfoScreen.tsx @@ -8,6 +8,7 @@ import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import AppAnalytics from 'src/analytics/AppAnalytics' import { EarnEvents } from 'src/analytics/Events' +import { EarnCommonProperties } from 'src/analytics/Properties' import { openUrl } from 'src/app/actions' import BottomSheet, { BottomSheetModalRefType } from 'src/components/BottomSheet' import Button, { BtnSizes, BtnTypes } from 'src/components/Button' @@ -37,7 +38,6 @@ import variables from 'src/styles/variables' import { useTokenInfo, useTokensInfo } from 'src/tokens/hooks' import { tokensByIdSelector } from 'src/tokens/selectors' import { TokenBalance } from 'src/tokens/slice' -import { NetworkId } from 'src/transactions/types' import { navigateToURI } from 'src/utils/linking' import { formattedDuration } from 'src/utils/time' @@ -411,17 +411,11 @@ function AgeCard({ ageOfPool, onInfoIconPress }: { ageOfPool: Date; onInfoIconPr function LearnMoreTouchable({ url, providerName, - appId, - positionId, - networkId, - depositTokenId, + commonAnalyticsProps, }: { url: string providerName: string - appId: string - positionId: string - networkId: NetworkId - depositTokenId: string + commonAnalyticsProps: EarnCommonProperties }) { const { t } = useTranslation() return ( @@ -429,12 +423,7 @@ function LearnMoreTouchable({ { - AppAnalytics.track(EarnEvents.earn_pool_info_view_pool, { - providerId: appId, - poolId: positionId, - networkId, - depositTokenId, - }) + AppAnalytics.track(EarnEvents.earn_pool_info_view_pool, commonAnalyticsProps) navigateToURI(url) }} > @@ -516,6 +505,13 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) { throw new Error(`Token ${dataProps.depositTokenId} not found`) } + const commonAnalyticsProps: EarnCommonProperties = { + providerId: appId, + poolId: positionId, + networkId, + depositTokenId: dataProps.depositTokenId, + } + const { hasDepositToken, hasTokensOnSameNetwork, @@ -526,10 +522,7 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) { const onPressDeposit = () => { AppAnalytics.track(EarnEvents.earn_pool_info_tap_deposit, { - poolId: positionId, - providerId: appId, - networkId: networkId, - depositTokenId: dataProps.depositTokenId, + ...commonAnalyticsProps, hasDepositToken, hasTokensOnSameNetwork, hasTokensOnOtherNetworks, @@ -582,11 +575,8 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) { earnPosition={pool} onInfoIconPress={() => { AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, { - providerId: appId, - poolId: positionId, type: 'deposit', - networkId, - depositTokenId: dataProps.depositTokenId, + ...commonAnalyticsProps, }) depositInfoBottomSheetRef.current?.snapToIndex(0) }} @@ -595,11 +585,8 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) { { AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, { - providerId: appId, - poolId: positionId, type: 'yieldRate', - networkId, - depositTokenId: dataProps.depositTokenId, + ...commonAnalyticsProps, }) yieldRateInfoBottomSheetRef.current?.snapToIndex(0) }} @@ -611,26 +598,22 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) { dailyYieldRate={dataProps.dailyYieldRatePercentage} onInfoIconPress={() => { AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, { - providerId: appId, - poolId: positionId, type: 'dailyYieldRate', - networkId, - depositTokenId: dataProps.depositTokenId, + ...commonAnalyticsProps, }) dailyYieldRateInfoBottomSheetRef.current?.snapToIndex(0) }} /> )} - {!!dataProps.safety && } + {!!dataProps.safety && ( + + )} { AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, { - providerId: appId, - poolId: positionId, type: 'tvl', - networkId, - depositTokenId: dataProps.depositTokenId, + ...commonAnalyticsProps, }) tvlInfoBottomSheetRef.current?.snapToIndex(0) }} @@ -640,11 +623,8 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) { ageOfPool={new Date(dataProps.contractCreatedAt)} onInfoIconPress={() => { AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, { - providerId: appId, - poolId: positionId, type: 'age', - networkId, - depositTokenId: dataProps.depositTokenId, + ...commonAnalyticsProps, }) ageInfoBottomSheetRef.current?.snapToIndex(0) }} @@ -654,10 +634,7 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) { ) : null} diff --git a/src/earn/SafetyCard.test.tsx b/src/earn/SafetyCard.test.tsx index 7d30d5a2df..f6f70246ca 100644 --- a/src/earn/SafetyCard.test.tsx +++ b/src/earn/SafetyCard.test.tsx @@ -1,18 +1,35 @@ -import { render } from '@testing-library/react-native' +import { fireEvent, render } from '@testing-library/react-native' import React from 'react' +import AppAnalytics from 'src/analytics/AppAnalytics' +import { EarnEvents } from 'src/analytics/Events' import { SafetyCard } from 'src/earn/SafetyCard' import Colors from 'src/styles/colors' +import { NetworkId } from 'src/transactions/types' + +const mockAnalyticsProps = { + poolId: 'poolId', + providerId: 'providerId', + networkId: NetworkId['arbitrum-sepolia'], + depositTokenId: 'depositTokenId', +} describe('SafetyCard', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + it('renders correctly', () => { const { getByTestId, getAllByTestId } = render( - + ) - expect(getByTestId('SafetyCard')).toBeDefined() - expect(getByTestId('SafetyCardInfoIcon')).toBeDefined() + expect(getByTestId('SafetyCard')).toBeTruthy() + expect(getByTestId('SafetyCardInfoIcon')).toBeTruthy() expect(getAllByTestId('SafetyCard/Bar')).toHaveLength(3) - expect(getByTestId('SafetyCard/ViewDetails')).toBeDefined() + expect(getByTestId('SafetyCard/ViewDetails')).toBeTruthy() + expect(getByTestId('SafetyCard/ViewDetails')).toHaveTextContent( + 'earnFlow.poolInfoScreen.viewMoreDetails' + ) }) it.each([ @@ -20,12 +37,68 @@ describe('SafetyCard', () => { { level: 'medium', colors: [Colors.primary, Colors.primary, Colors.gray2] }, { level: 'high', colors: [Colors.primary, Colors.primary, Colors.primary] }, ] as const)('should render correct triple bars for safety level $level', ({ level, colors }) => { - const { getAllByTestId } = render() + const { getAllByTestId } = render( + + ) const bars = getAllByTestId('SafetyCard/Bar') - expect(bars.length).toBe(3) + expect(bars).toHaveLength(3) expect(bars[0]).toHaveStyle({ backgroundColor: colors[0] }) expect(bars[1]).toHaveStyle({ backgroundColor: colors[1] }) expect(bars[2]).toHaveStyle({ backgroundColor: colors[2] }) }) + + it('expands and collapses card and displays risks when View More/Less Details is pressed', () => { + const { getByTestId, getAllByTestId, queryByTestId } = render( + + ) + + expect(queryByTestId('SafetyCard/Risk')).toBeFalsy() + expect(getByTestId('SafetyCard/ViewDetails')).toHaveTextContent( + 'earnFlow.poolInfoScreen.viewMoreDetails' + ) + + // expand + fireEvent.press(getByTestId('SafetyCard/ViewDetails')) + expect(getAllByTestId('SafetyCard/Risk')).toHaveLength(2) + expect(getByTestId('SafetyCard/ViewDetails')).toHaveTextContent( + 'earnFlow.poolInfoScreen.viewLessDetails' + ) + expect(getAllByTestId('SafetyCard/Risk')[0].children[0]).toContainElement( + getByTestId('SafetyCard/RiskPositive') + ) + expect(getAllByTestId('SafetyCard/Risk')[0]).toHaveTextContent('Risk 1') + expect(getAllByTestId('SafetyCard/Risk')[0]).toHaveTextContent('Category 1') + expect(getAllByTestId('SafetyCard/Risk')[1].children[0]).toContainElement( + getByTestId('SafetyCard/RiskNegative') + ) + expect(getAllByTestId('SafetyCard/Risk')[1]).toHaveTextContent('Risk 2') + expect(getAllByTestId('SafetyCard/Risk')[1]).toHaveTextContent('Category 2') + expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_pool_info_tap_safety_details, { + action: 'expand', + ...mockAnalyticsProps, + }) + expect(AppAnalytics.track).toHaveBeenCalledTimes(1) + + // collapse + fireEvent.press(getByTestId('SafetyCard/ViewDetails')) + expect(queryByTestId('SafetyCard/Risk')).toBeFalsy() + expect(getByTestId('SafetyCard/ViewDetails')).toHaveTextContent( + 'earnFlow.poolInfoScreen.viewMoreDetails' + ) + expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_pool_info_tap_safety_details, { + action: 'collapse', + ...mockAnalyticsProps, + }) + expect(AppAnalytics.track).toHaveBeenCalledTimes(2) + }) }) diff --git a/src/earn/SafetyCard.tsx b/src/earn/SafetyCard.tsx index fa900211e1..f23f1dd531 100644 --- a/src/earn/SafetyCard.tsx +++ b/src/earn/SafetyCard.tsx @@ -1,9 +1,14 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { StyleSheet, Text, View } from 'react-native' +import AppAnalytics from 'src/analytics/AppAnalytics' +import { EarnEvents } from 'src/analytics/Events' +import { EarnCommonProperties } from 'src/analytics/Properties' import { LabelWithInfo } from 'src/components/LabelWithInfo' import Touchable from 'src/components/Touchable' -import { Safety } from 'src/positions/types' +import DataDown from 'src/icons/DataDown' +import DataUp from 'src/icons/DataUp' +import { Safety, SafetyRisk } from 'src/positions/types' import Colors from 'src/styles/colors' import { typeScale } from 'src/styles/fonts' import { Spacing } from 'src/styles/styles' @@ -16,8 +21,33 @@ const LEVEL_TO_MAX_HIGHLIGHTED_BAR: Record = { high: 3, } -export function SafetyCard({ safety }: { safety: Safety }) { +function Risk({ risk }: { risk: SafetyRisk }) { + return ( + + + {risk.isPositive ? ( + + ) : ( + + )} + + + {risk.title} + {risk.category} + + + ) +} + +export function SafetyCard({ + safety, + commonAnalyticsProps, +}: { + safety: Safety + commonAnalyticsProps: EarnCommonProperties +}) { const { t } = useTranslation() + const [expanded, setExpanded] = React.useState(false) return ( @@ -45,14 +75,29 @@ export function SafetyCard({ safety }: { safety: Safety }) { ))} + {expanded && ( + + {safety.risks.map((risk, index) => ( + + ))} + + )} { - // todo(act-1405): expand and display risks + setExpanded((prev) => !prev) + AppAnalytics.track(EarnEvents.earn_pool_info_tap_safety_details, { + action: expanded ? 'collapse' : 'expand', + ...commonAnalyticsProps, + }) }} > - {t('earnFlow.poolInfoScreen.viewMoreDetails')} + + {expanded + ? t('earnFlow.poolInfoScreen.viewLessDetails') + : t('earnFlow.poolInfoScreen.viewMoreDetails')} + ) @@ -99,4 +144,28 @@ const styles = StyleSheet.create({ textAlign: 'center', flex: 1, }, + risksContainer: { + gap: Spacing.Thick24, + }, + riskContainer: { + flexDirection: 'row', + gap: Spacing.Smallest8, + }, + riskTextContainer: { + flex: 1, + }, + icon: { + width: 24, + height: 24, + alignItems: 'center', + justifyContent: 'center', + }, + riskTitle: { + ...typeScale.labelMedium, + color: Colors.black, + }, + riskCategory: { + ...typeScale.bodySmall, + color: Colors.gray3, + }, }) diff --git a/src/positions/types.ts b/src/positions/types.ts index a6c3a000f7..e76c7b497f 100644 --- a/src/positions/types.ts +++ b/src/positions/types.ts @@ -25,7 +25,7 @@ export interface EarningItem { includedInPoolBalance?: boolean } -interface SafetyRisk { +export interface SafetyRisk { isPositive: boolean title: string category: string