From 96e2283dfc7433ebe9b4f38e106ab1577595a553 Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Thu, 17 Oct 2024 19:13:28 -0700 Subject: [PATCH] feat(earn): add safety score bottom sheet (#6167) ### Description As the title ### Test plan https://github.com/user-attachments/assets/12648e84-c6e0-4212-b602-c88d46d05887 ### Related issues - Fixes ACT-1405 ### 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) --- locales/base/translation.json | 5 ++- src/analytics/Properties.tsx | 2 +- src/earn/EarnPoolInfoScreen.test.tsx | 18 ++++++++-- src/earn/EarnPoolInfoScreen.tsx | 22 +++++++++++- src/earn/SafetyCard.test.tsx | 54 ++++++++++++++-------------- src/earn/SafetyCard.tsx | 6 ++-- 6 files changed, 71 insertions(+), 36 deletions(-) diff --git a/locales/base/translation.json b/locales/base/translation.json index 9f83e127b9e..13d77a8ee90 100644 --- a/locales/base/translation.json +++ b/locales/base/translation.json @@ -2721,7 +2721,10 @@ "yieldRateDescription": "While most pools offer earnings in the form of a liquidity pool token, some give additional token(s) as a reward or added incentive.\n\nSince {{appName}} aggregates pools across multiple protocols, we have combined all the earning and reward rates into a single, overall yield rate to help easily evaluate your earning potential. This number is an estimate since the earning and reward values fluctuate constantly.\n\nFor further information about earning breakdowns you can visit <0>{{providerName}}.", "dailyYieldRateTitle": "Daily Rate", "dailyYieldRateDescription": "The daily rate displayed reflects the daily rate provided by {{providerName}}.", - "dailyYieldRateLink": "View More Daily Rate Details On {{providerName}}" + "dailyYieldRateLink": "View More Daily Rate Details On {{providerName}}", + "safetyScoreTitle": "Safety Score", + "safetyScoreDescription": "The Safety Score breaks down the safety of the underlying vault asset into several key factors. It aims to draw your attention to the risks to consider before investing, and to inform you about technical details that you may not be able to evaluate for yourself.\nThough the full position is always more complicated, the Safety Score is provided to simplify the key risk and safety considerations and kick-start your own due diligence. Our team carefully considers each factor before a vault is deployed, and keeps a watchful eye on each asset, chain and protocol which {{providerName}} has integrated.", + "safetyScoreRateLink": "View More Safety Score Details On {{providerName}}" }, "viewMoreDetails": "View More Details", "viewLessDetails": "View Less Details" diff --git a/src/analytics/Properties.tsx b/src/analytics/Properties.tsx index 81861d68b4e..fdb87bd4a69 100644 --- a/src/analytics/Properties.tsx +++ b/src/analytics/Properties.tsx @@ -1621,7 +1621,7 @@ interface EarnEventsProperties { [EarnEvents.earn_home_error_try_again]: undefined [EarnEvents.earn_pool_info_view_pool]: EarnCommonProperties [EarnEvents.earn_pool_info_tap_info_icon]: { - type: 'tvl' | 'age' | 'yieldRate' | 'deposit' | 'dailyYieldRate' + type: 'tvl' | 'age' | 'yieldRate' | 'deposit' | 'dailyYieldRate' | 'safetyScore' } & EarnCommonProperties [EarnEvents.earn_pool_info_tap_withdraw]: { poolAmount: string diff --git a/src/earn/EarnPoolInfoScreen.test.tsx b/src/earn/EarnPoolInfoScreen.test.tsx index 5ea8045aba6..610ea2c1009 100644 --- a/src/earn/EarnPoolInfoScreen.test.tsx +++ b/src/earn/EarnPoolInfoScreen.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, within } from '@testing-library/react-native' +import { fireEvent, render, waitFor, within } from '@testing-library/react-native' import React from 'react' import { Provider } from 'react-redux' import AppAnalytics from 'src/analytics/AppAnalytics' @@ -345,7 +345,12 @@ describe('EarnPoolInfoScreen', () => { infoIconTestId: 'YieldRateInfoIcon', type: 'yieldRate', }, - ])('opens $testId and track analytics event', ({ testId, infoIconTestId, type }) => { + { + testId: 'SafetyScoreInfoBottomSheet', + infoIconTestId: 'SafetyCardInfoIcon', + type: 'safetyScore', + }, + ])('opens $testId and track analytics event', async ({ testId, infoIconTestId, type }) => { const mockPool = { ...mockEarnPositions[0], balance: '100', @@ -360,12 +365,19 @@ describe('EarnPoolInfoScreen', () => { includedInPoolBalance: false, }, ], + safety: { + level: 'high' as const, + risks: [ + { isPositive: false, title: 'Risk 1', category: 'Category 1' }, + { isPositive: true, title: 'Risk 2', category: 'Category 2' }, + ], + }, }, } const { getByTestId } = renderEarnPoolInfoScreen(mockPool) fireEvent.press(getByTestId(infoIconTestId)) - expect(getByTestId(testId)).toBeVisible() + await waitFor(() => expect(getByTestId(testId)).toBeVisible()) expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_pool_info_tap_info_icon, { providerId: 'aave', poolId: 'arbitrum-sepolia:0x460b97bd498e1157530aeb3086301d5225b91216', diff --git a/src/earn/EarnPoolInfoScreen.tsx b/src/earn/EarnPoolInfoScreen.tsx index 51f406464bf..68f23bc6f13 100644 --- a/src/earn/EarnPoolInfoScreen.tsx +++ b/src/earn/EarnPoolInfoScreen.tsx @@ -540,6 +540,7 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) { const ageInfoBottomSheetRef = useRef(null) const yieldRateInfoBottomSheetRef = useRef(null) const dailyYieldRateInfoBottomSheetRef = useRef(null) + const safetyScoreInfoBottomSheetRef = useRef(null) // Scroll Aware Header const scrollPosition = useSharedValue(0) @@ -606,7 +607,17 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) { /> )} {!!dataProps.safety && ( - + { + AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, { + type: 'safetyScore', + ...commonAnalyticsProps, + }) + safetyScoreInfoBottomSheetRef.current?.snapToIndex(0) + }} + /> )} + { + const mockProps = { + commonAnalyticsProps: { + poolId: 'poolId', + providerId: 'providerId', + networkId: NetworkId['arbitrum-sepolia'], + depositTokenId: 'depositTokenId', + }, + safety: { + level: 'low' as const, + risks: [ + { title: 'Risk 1', category: 'Category 1', isPositive: true }, + { title: 'Risk 2', category: 'Category 2', isPositive: false }, + ], + }, + onInfoIconPress: jest.fn(), + } beforeEach(() => { jest.clearAllMocks() }) it('renders correctly', () => { - const { getByTestId, getAllByTestId } = render( - - ) + const { getByTestId, getAllByTestId } = render() expect(getByTestId('SafetyCard')).toBeTruthy() expect(getByTestId('SafetyCardInfoIcon')).toBeTruthy() @@ -37,9 +44,7 @@ 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).toHaveLength(3) @@ -49,18 +54,7 @@ describe('SafetyCard', () => { }) it('expands and collapses card and displays risks when View More/Less Details is pressed', () => { - const { getByTestId, getAllByTestId, queryByTestId } = render( - - ) + const { getByTestId, getAllByTestId, queryByTestId } = render() expect(queryByTestId('SafetyCard/Risk')).toBeFalsy() expect(getByTestId('SafetyCard/ViewDetails')).toHaveTextContent( @@ -85,7 +79,7 @@ describe('SafetyCard', () => { expect(getAllByTestId('SafetyCard/Risk')[1]).toHaveTextContent('Category 2') expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_pool_info_tap_safety_details, { action: 'expand', - ...mockAnalyticsProps, + ...mockProps.commonAnalyticsProps, }) expect(AppAnalytics.track).toHaveBeenCalledTimes(1) @@ -97,8 +91,14 @@ describe('SafetyCard', () => { ) expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_pool_info_tap_safety_details, { action: 'collapse', - ...mockAnalyticsProps, + ...mockProps.commonAnalyticsProps, }) expect(AppAnalytics.track).toHaveBeenCalledTimes(2) }) + + it('triggers callback when info icon is pressed', () => { + const { getByTestId } = render() + fireEvent.press(getByTestId('SafetyCardInfoIcon')) + expect(mockProps.onInfoIconPress).toHaveBeenCalledTimes(1) + }) }) diff --git a/src/earn/SafetyCard.tsx b/src/earn/SafetyCard.tsx index f23f1dd5314..c54656e3072 100644 --- a/src/earn/SafetyCard.tsx +++ b/src/earn/SafetyCard.tsx @@ -42,9 +42,11 @@ function Risk({ risk }: { risk: SafetyRisk }) { export function SafetyCard({ safety, commonAnalyticsProps, + onInfoIconPress, }: { safety: Safety commonAnalyticsProps: EarnCommonProperties + onInfoIconPress: () => void }) { const { t } = useTranslation() const [expanded, setExpanded] = React.useState(false) @@ -53,9 +55,7 @@ export function SafetyCard({ { - // todo(act-1405): open bottom sheet - }} + onPress={onInfoIconPress} label={t('earnFlow.poolInfoScreen.safetyScore')} labelStyle={styles.cardTitleText} testID="SafetyCardInfoIcon"