Skip to content

Commit

Permalink
feat(earn): add safety score bottom sheet (#6167)
Browse files Browse the repository at this point in the history
### 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)
  • Loading branch information
satish-ravi authored Oct 18, 2024
1 parent d03543c commit 96e2283
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 36 deletions.
5 changes: 4 additions & 1 deletion locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}</0>.",
"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"
Expand Down
2 changes: 1 addition & 1 deletion src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 15 additions & 3 deletions src/earn/EarnPoolInfoScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down
22 changes: 21 additions & 1 deletion src/earn/EarnPoolInfoScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) {
const ageInfoBottomSheetRef = useRef<BottomSheetModalRefType>(null)
const yieldRateInfoBottomSheetRef = useRef<BottomSheetModalRefType>(null)
const dailyYieldRateInfoBottomSheetRef = useRef<BottomSheetModalRefType>(null)
const safetyScoreInfoBottomSheetRef = useRef<BottomSheetModalRefType>(null)

// Scroll Aware Header
const scrollPosition = useSharedValue(0)
Expand Down Expand Up @@ -606,7 +607,17 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) {
/>
)}
{!!dataProps.safety && (
<SafetyCard safety={dataProps.safety} commonAnalyticsProps={commonAnalyticsProps} />
<SafetyCard
safety={dataProps.safety}
commonAnalyticsProps={commonAnalyticsProps}
onInfoIconPress={() => {
AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, {
type: 'safetyScore',
...commonAnalyticsProps,
})
safetyScoreInfoBottomSheetRef.current?.snapToIndex(0)
}}
/>
)}
<TvlCard
earnPosition={pool}
Expand Down Expand Up @@ -683,6 +694,15 @@ export default function EarnPoolInfoScreen({ route, navigation }: Props) {
linkUrl={dataProps.manageUrl}
testId="DailyYieldRateInfoBottomSheet"
/>
<InfoBottomSheet
infoBottomSheetRef={safetyScoreInfoBottomSheetRef}
titleKey="earnFlow.poolInfoScreen.infoBottomSheet.safetyScoreTitle"
descriptionKey="earnFlow.poolInfoScreen.infoBottomSheet.safetyScoreDescription"
providerName={appName}
linkKey="earnFlow.poolInfoScreen.infoBottomSheet.safetyScoreRateLink"
linkUrl={dataProps.manageUrl}
testId="SafetyScoreInfoBottomSheet"
/>
<BeforeDepositBottomSheet
forwardedRef={beforeDepositBottomSheetRef}
token={depositToken}
Expand Down
54 changes: 27 additions & 27 deletions src/earn/SafetyCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@ 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', () => {
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(
<SafetyCard safety={{ level: 'low', risks: [] }} commonAnalyticsProps={mockAnalyticsProps} />
)
const { getByTestId, getAllByTestId } = render(<SafetyCard {...mockProps} />)

expect(getByTestId('SafetyCard')).toBeTruthy()
expect(getByTestId('SafetyCardInfoIcon')).toBeTruthy()
Expand All @@ -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(
<SafetyCard safety={{ level, risks: [] }} commonAnalyticsProps={mockAnalyticsProps} />
)
const { getAllByTestId } = render(<SafetyCard {...mockProps} safety={{ level, risks: [] }} />)

const bars = getAllByTestId('SafetyCard/Bar')
expect(bars).toHaveLength(3)
Expand All @@ -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(
<SafetyCard
safety={{
level: 'low',
risks: [
{ title: 'Risk 1', category: 'Category 1', isPositive: true },
{ title: 'Risk 2', category: 'Category 2', isPositive: false },
],
}}
commonAnalyticsProps={mockAnalyticsProps}
/>
)
const { getByTestId, getAllByTestId, queryByTestId } = render(<SafetyCard {...mockProps} />)

expect(queryByTestId('SafetyCard/Risk')).toBeFalsy()
expect(getByTestId('SafetyCard/ViewDetails')).toHaveTextContent(
Expand All @@ -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)

Expand All @@ -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(<SafetyCard {...mockProps} />)
fireEvent.press(getByTestId('SafetyCardInfoIcon'))
expect(mockProps.onInfoIconPress).toHaveBeenCalledTimes(1)
})
})
6 changes: 3 additions & 3 deletions src/earn/SafetyCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -53,9 +55,7 @@ export function SafetyCard({
<View style={styles.cardLineContainer}>
<View style={styles.cardLineLabel}>
<LabelWithInfo
onPress={() => {
// todo(act-1405): open bottom sheet
}}
onPress={onInfoIconPress}
label={t('earnFlow.poolInfoScreen.safetyScore')}
labelStyle={styles.cardTitleText}
testID="SafetyCardInfoIcon"
Expand Down

0 comments on commit 96e2283

Please sign in to comment.