Skip to content

Commit bf0f9c2

Browse files
authored
chore(money-home): show APY even if it is zero (MetaMask#30630)
## **Description** The APY for the `dev-monad` vault for the money account is zero, but we don't show it, making UAT and design reviews harder ## **Changelog** CHANGELOG entry: show even zero APY values on money home ## **Related issues** Fixes: N/A ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** <img width="735" height="1600" alt="image" src="https://github.com/user-attachments/assets/17df48b0-435e-4d8f-b24e-b41be37db0f2" /> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I've included tests if applicable - [X] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small Money UI visibility change with tests; no auth, payments, or data-layer changes. > > **Overview** > Money home now **shows APY when the rate is 0%**, not only when it is strictly positive. **`MoneyBalanceSummary`**, **`MoneyHowItWorks`**, and **`MoneyWhatYouGet`** switch from `isPositiveNumber` to a new **`isPositiveNumberOrZero`** guard so the label, info button, and inline copy render for zero APY while **undefined** and **negative** values stay hidden. > > Unit tests are updated to expect visible **0% APY** UI and **`number.test.ts`** covers the new helper. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 009b809. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 0cd15ef commit bf0f9c2

8 files changed

Lines changed: 83 additions & 20 deletions

File tree

app/components/UI/Money/components/MoneyBalanceSummary/MoneyBalanceSummary.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,16 @@ describe('MoneyBalanceSummary', () => {
9292
).not.toBeOnTheScreen();
9393
});
9494

95-
it('hides the APY text and tooltip button when apy is zero', () => {
95+
it('shows the APY text and tooltip button when apy is zero', () => {
9696
const mockInfoPress = jest.fn();
97-
const { queryByTestId } = render(
97+
const { getByTestId } = render(
9898
<MoneyBalanceSummary apy={0} onApyInfoPress={mockInfoPress} />,
9999
);
100100

101-
expect(queryByTestId(MoneyBalanceSummaryTestIds.APY)).not.toBeOnTheScreen();
101+
expect(getByTestId(MoneyBalanceSummaryTestIds.APY)).toBeOnTheScreen();
102102
expect(
103-
queryByTestId(MoneyBalanceSummaryTestIds.APY_INFO_BUTTON),
104-
).not.toBeOnTheScreen();
103+
getByTestId(MoneyBalanceSummaryTestIds.APY_INFO_BUTTON),
104+
).toBeOnTheScreen();
105105
});
106106

107107
it('hides the APY tooltip button when isLoading is true', () => {

app/components/UI/Money/components/MoneyBalanceSummary/MoneyBalanceSummary.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from '@metamask/design-system-react-native';
1616
import { strings } from '../../../../../../locales/i18n';
1717
import { MoneyBalanceSummaryTestIds } from './MoneyBalanceSummary.testIds';
18-
import { isPositiveNumber } from '../../utils/number';
18+
import { isPositiveNumberOrZero } from '../../utils/number';
1919

2020
const DEFAULT_BALANCE = '$0.00';
2121

@@ -76,7 +76,7 @@ const MoneyBalanceSummary = ({
7676
testID={MoneyBalanceSummaryTestIds.APY_SKELETON}
7777
/>
7878
) : (
79-
isPositiveNumber(apy) && (
79+
isPositiveNumberOrZero(apy) && (
8080
<Text
8181
variant={TextVariant.BodyMd}
8282
fontWeight={FontWeight.Medium}
@@ -94,7 +94,7 @@ const MoneyBalanceSummary = ({
9494
</Text>
9595
)
9696
)}
97-
{onApyInfoPress && isPositiveNumber(apy) && !isLoading && (
97+
{onApyInfoPress && isPositiveNumberOrZero(apy) && !isLoading && (
9898
<ButtonIcon
9999
iconName={IconName.Info}
100100
iconProps={{ color: IconColor.IconAlternative }}

app/components/UI/Money/components/MoneyHowItWorks/MoneyHowItWorks.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ describe('MoneyHowItWorks', () => {
6262
expect(queryByTestId(MoneyHowItWorksTestIds.APY)).not.toBeOnTheScreen();
6363
});
6464

65-
it('hides the highlighted APY text when apy is zero', () => {
66-
const { queryByTestId } = render(<MoneyHowItWorks apy={0} />);
65+
it('shows the highlighted APY text when apy is zero', () => {
66+
const { getByTestId } = render(<MoneyHowItWorks apy={0} />);
6767

68-
expect(queryByTestId(MoneyHowItWorksTestIds.APY)).not.toBeOnTheScreen();
68+
expect(getByTestId(MoneyHowItWorksTestIds.APY)).toBeOnTheScreen();
6969
});
7070

7171
it('hides the highlighted APY text when apy is negative', () => {

app/components/UI/Money/components/MoneyHowItWorks/MoneyHowItWorks.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { strings } from '../../../../../../locales/i18n';
1010
import MoneySectionHeader from '../MoneySectionHeader';
1111
import { MoneyHowItWorksTestIds } from './MoneyHowItWorks.testIds';
12-
import { isPositiveNumber } from '../../utils/number';
12+
import { isPositiveNumberOrZero } from '../../utils/number';
1313

1414
interface MoneyHowItWorksProps {
1515
/** APY expressed as a percentage (e.g. 3 for 3%). */
@@ -35,7 +35,7 @@ const MoneyHowItWorks = ({
3535
testID={MoneyHowItWorksTestIds.DESCRIPTION}
3636
>
3737
{strings('money.how_it_works.description_prefix')}
38-
{!isLoading && isPositiveNumber(apy) && (
38+
{!isLoading && isPositiveNumberOrZero(apy) && (
3939
<Text
4040
variant={TextVariant.BodyMd}
4141
fontWeight={FontWeight.Medium}

app/components/UI/Money/components/MoneyWhatYouGet/MoneyWhatYouGet.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ describe('MoneyWhatYouGet', () => {
7575
).not.toBeOnTheScreen();
7676
});
7777

78-
it('hides the inline APY text in the auto-earn benefit when apy is zero', () => {
79-
const { queryByText } = render(<MoneyWhatYouGet apy={0} />);
78+
it('renders the inline APY text in the auto-earn benefit when apy is zero', () => {
79+
const { getByText } = render(<MoneyWhatYouGet apy={0} />);
8080

8181
expect(
82-
queryByText(strings('money.apy_label', { percentage: 0 })),
83-
).not.toBeOnTheScreen();
82+
getByText(strings('money.apy_label', { percentage: 0 })),
83+
).toBeOnTheScreen();
8484
});
8585
});

app/components/UI/Money/components/MoneyWhatYouGet/MoneyWhatYouGet.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import { strings } from '../../../../../../locales/i18n';
1818
import MoneySectionHeader from '../MoneySectionHeader';
1919
import { MoneyWhatYouGetTestIds } from './MoneyWhatYouGet.testIds';
20-
import { isPositiveNumber } from '../../utils/number';
20+
import { isPositiveNumberOrZero } from '../../utils/number';
2121

2222
interface MoneyWhatYouGetProps {
2323
/** APY expressed as a percentage (e.g. 3 for 3%). */
@@ -53,7 +53,7 @@ const MoneyWhatYouGet = ({ apy, onLearnMorePress }: MoneyWhatYouGetProps) => (
5353
<BenefitRow>
5454
<Text variant={TextVariant.BodyMd}>
5555
{`${strings('money.what_you_get.benefit_auto_earn')} `}
56-
{isPositiveNumber(apy) && (
56+
{isPositiveNumberOrZero(apy) && (
5757
<Text variant={TextVariant.BodyMd} color={TextColor.SuccessDefault}>
5858
{strings('money.apy_label', { percentage: apy })}
5959
</Text>

app/components/UI/Money/utils/number.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isPositiveNumber } from './number';
1+
import { isPositiveNumber, isPositiveNumberOrZero } from './number';
22

33
describe('isPositiveNumber', () => {
44
describe('returns true for positive finite numbers', () => {
@@ -52,3 +52,56 @@ describe('isPositiveNumber', () => {
5252
});
5353
});
5454
});
55+
56+
describe('isPositiveNumberOrZero', () => {
57+
describe('returns true for positive finite numbers and zero', () => {
58+
it.each([
59+
['a positive integer', 1],
60+
['a positive decimal', 0.01],
61+
['zero', 0],
62+
['Number.MAX_SAFE_INTEGER', Number.MAX_SAFE_INTEGER],
63+
])('returns true for %s', (_label, value) => {
64+
const result = isPositiveNumberOrZero(value);
65+
66+
expect(result).toBe(true);
67+
});
68+
});
69+
70+
describe('returns false for non-number types', () => {
71+
it.each([
72+
['a numeric string', '1'],
73+
['null', null],
74+
['undefined', undefined],
75+
['a boolean', true],
76+
['an array', [1]],
77+
['an object', { value: 1 }],
78+
])('returns false for %s', (_label, value) => {
79+
const result = isPositiveNumberOrZero(value);
80+
81+
expect(result).toBe(false);
82+
});
83+
});
84+
85+
describe('returns false for non-finite numbers', () => {
86+
it.each([
87+
['Infinity', Infinity],
88+
['-Infinity', -Infinity],
89+
['NaN', NaN],
90+
])('returns false for %s', (_label, value) => {
91+
const result = isPositiveNumberOrZero(value);
92+
93+
expect(result).toBe(false);
94+
});
95+
});
96+
97+
describe('returns false for negative numbers', () => {
98+
it.each([
99+
['a negative integer', -1],
100+
['a negative decimal', -0.01],
101+
])('returns false for %s', (_label, value) => {
102+
const result = isPositiveNumberOrZero(value);
103+
104+
expect(result).toBe(false);
105+
});
106+
});
107+
});

app/components/UI/Money/utils/number.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@
77
*/
88
export const isPositiveNumber = (value: unknown): value is number =>
99
typeof value === 'number' && isFinite(value) && value > 0;
10+
11+
/**
12+
* Determines if a value is a positive number or zero.
13+
* Used to avoid littering Money components with these repeated checks.
14+
*
15+
* @param value - The value to check.
16+
* @returns True if the value is a positive number or zero, false otherwise.
17+
*/
18+
export const isPositiveNumberOrZero = (value: unknown): value is number =>
19+
typeof value === 'number' && isFinite(value) && value >= 0;

0 commit comments

Comments
 (0)