Skip to content

Commit dafe349

Browse files
feat: add alerts to perps deposit confirmation (MetaMask#17892)
## **Description** Add amount alerts to Perps specific deposit transaction confirmation. Specifically: - Add `PayTokenBalance` component to display available fiat balance of selected transaction pay token. - Add `TotalRow` component to display total fiat cost of transaction. - Add `TokenAmountNative` component to display native value of token amount. - Display alert message in `EditAmount` if alert exists for matching field. - Display loaders in `PayWithRow` and `TotalRow` while quotes are loading. - Add `usePerpsDepositMinimumAlert` and `useInsufficientPayTokenBalance` alert hooks. - Return `balanceHuman` from `useTransactionPayToken` hook. - Return `totalHuman` and `totalRaw` from `useTransactionPayTokenAmounts` hook. - Add `useTransactionTotalFiat` hook to aggregate all costs. - Return `amountNative` from `useTokenAmount` hook. - Add `isTransactionBridgeQuotesLoadingById` property to `confirmationMetrics` slice. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [MetaMask#5499](MetaMask/MetaMask-planning#5499) ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** <img width="250" alt="Insufficient" src="https://github.com/user-attachments/assets/b3a07707-3acc-4bca-a4bc-6b92f9d3e3fe" /> <img width="250" alt="Minimum" src="https://github.com/user-attachments/assets/2343515b-e6a8-42af-a2cc-166dcbd1ce21" /> ## **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. ## **Pre-merge reviewer checklist** - [ ] 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.
1 parent e2f3f4a commit dafe349

43 files changed

Lines changed: 1001 additions & 59 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/components/Views/confirmations/__mocks__/controllers/transaction-controller-mock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '../helpers/approve';
1111
import { ZERO_ADDRESS } from '../../constants/address';
1212

13-
const transactionIdMock = '699ca2f0-e459-11ef-b6f6-d182277cf5e1';
13+
export const transactionIdMock = '699ca2f0-e459-11ef-b6f6-d182277cf5e1';
1414
const permit2TokenMock = '0x1234567890123456789012345678901234567890';
1515

1616
export const approvalSpenderMock = '0x9876543210987654321098765432109876543210';

app/components/Views/confirmations/components/UI/info-row/alert-row/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export enum RowAlertKey {
2+
Amount = 'amount',
23
AccountTypeUpgrade = 'accountTypeUpgrade',
34
Blockaid = 'blockaid',
45
EstimatedFee = 'estimatedFee',

app/components/Views/confirmations/components/confirm/confirm-component.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import renderWithProvider from '../../../../../util/test/renderWithProvider';
1616
// eslint-disable-next-line import/no-namespace
1717
import * as ConfirmationRedesignEnabled from '../../hooks/useConfirmationRedesignEnabled';
1818
import { Confirm } from './confirm-component';
19+
import { useTokensWithBalance } from '../../../../UI/Bridge/hooks/useTokensWithBalance';
1920

2021
jest.mock('../../../../../components/hooks/useEditNonce', () => ({
2122
useEditNonce: jest.fn().mockReturnValue({}),
@@ -138,6 +139,10 @@ jest.mock('react-native-gzip', () => ({
138139
deflate: (str: string) => str,
139140
}));
140141

142+
jest.mock('../../../../UI/Bridge/hooks/useTokensWithBalance', () => ({
143+
useTokensWithBalance: () => [] as ReturnType<typeof useTokensWithBalance>,
144+
}));
145+
141146
describe('Confirm', () => {
142147
afterEach(() => {
143148
jest.restoreAllMocks();

app/components/Views/confirmations/components/edit-amount/edit-amount.styles.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import { StyleSheet } from 'react-native';
22
import { Theme } from '../../../../../util/theme/models';
33

4-
const styleSheet = (_params: { theme: Theme }) =>
4+
const styleSheet = (params: { theme: Theme; vars: { hasAlert: boolean } }) =>
55
StyleSheet.create({
66
container: {
7-
paddingTop: 40,
8-
paddingBottom: 40,
7+
display: 'flex',
8+
gap: 8,
9+
marginTop: 16,
10+
marginBottom: 16,
911
},
1012
input: {
1113
textAlign: 'center',
1214
fontSize: 64,
1315
fontWeight: '500',
16+
color: params.vars.hasAlert
17+
? params.theme.colors.error.default
18+
: params.theme.colors.text.default,
19+
},
20+
alert: {
21+
color: params.theme.colors.error.default,
22+
textAlign: 'center',
1423
},
1524
});
1625

app/components/Views/confirmations/components/edit-amount/edit-amount.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,19 @@ import { useTokenAmount } from '../../hooks/useTokenAmount';
88
import { useTokenAsset } from '../../hooks/useTokenAsset';
99
import { TokenI } from '../../../../UI/Tokens/types';
1010
import { act, fireEvent } from '@testing-library/react-native';
11+
import {
12+
AlertsContextParams,
13+
useAlerts,
14+
} from '../../context/alert-system-context';
15+
import { RowAlertKey } from '../UI/info-row/alert-row/constants';
1116

1217
jest.mock('../../hooks/useTokenAmount');
1318
jest.mock('../../hooks/useTokenAsset');
19+
jest.mock('../../context/alert-system-context');
1420

1521
const VALUE_MOCK = '1.23';
1622
const VALUE_2_MOCK = '2.34';
23+
const ALERT_MESSAGE_MOCK = 'Test Message';
1724

1825
const state = merge(
1926
simpleSendTransactionControllerMock,
@@ -27,6 +34,7 @@ function render(props: EditAmountProps = {}) {
2734
describe('EditAmount', () => {
2835
const useTokenAmountMock = jest.mocked(useTokenAmount);
2936
const useTokenAssetMock = jest.mocked(useTokenAsset);
37+
const useAlertsMock = jest.mocked(useAlerts);
3038
const updateTokenAmountMock = jest.fn();
3139

3240
beforeEach(() => {
@@ -43,6 +51,10 @@ describe('EditAmount', () => {
4351
} as TokenI,
4452
displayName: 'Test Token',
4553
});
54+
55+
useAlertsMock.mockReturnValue({
56+
fieldAlerts: [],
57+
} as unknown as AlertsContextParams);
4658
});
4759

4860
it('renders amount from current transaction data', () => {
@@ -79,4 +91,34 @@ describe('EditAmount', () => {
7991

8092
expect(getByTestId('edit-amount-input')).toHaveProp('value', VALUE_2_MOCK);
8193
});
94+
95+
it('renders alert if field is amount', () => {
96+
useAlertsMock.mockReturnValue({
97+
fieldAlerts: [
98+
{
99+
field: RowAlertKey.Amount,
100+
message: ALERT_MESSAGE_MOCK,
101+
},
102+
],
103+
} as unknown as AlertsContextParams);
104+
105+
const { getByText } = render();
106+
107+
expect(getByText(ALERT_MESSAGE_MOCK)).toBeDefined();
108+
});
109+
110+
it('does not render alert if field is not amount', () => {
111+
useAlertsMock.mockReturnValue({
112+
fieldAlerts: [
113+
{
114+
field: RowAlertKey.AccountTypeUpgrade,
115+
message: ALERT_MESSAGE_MOCK,
116+
},
117+
],
118+
} as unknown as AlertsContextParams);
119+
120+
const { queryByText } = render();
121+
122+
expect(queryByText(ALERT_MESSAGE_MOCK)).toBeNull();
123+
});
82124
});

app/components/Views/confirmations/components/edit-amount/edit-amount.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@ import { TextInput, View } from 'react-native';
33
import { useTokenAmount } from '../../hooks/useTokenAmount';
44
import { useStyles } from '../../../../../component-library/hooks';
55
import styleSheet from './edit-amount.styles';
6+
import { useAlerts } from '../../context/alert-system-context';
7+
import Text from '../../../../../component-library/components/Texts/Text';
8+
import { RowAlertKey } from '../UI/info-row/alert-row/constants';
69

710
export interface EditAmountProps {
11+
children?: React.ReactNode;
812
prefix?: string;
913
}
1014

11-
export function EditAmount({ prefix = '' }: EditAmountProps) {
12-
const { styles } = useStyles(styleSheet, {});
15+
export function EditAmount({ children, prefix = '' }: EditAmountProps) {
16+
const { fieldAlerts } = useAlerts();
17+
const alerts = fieldAlerts.filter((a) => a.field === RowAlertKey.Amount);
18+
const hasAlert = alerts.length > 0;
19+
const alertMessage = alerts[0]?.message;
20+
21+
const { styles } = useStyles(styleSheet, {
22+
hasAlert,
23+
});
1324

1425
const { amountPrecise: transactionAmountHuman, updateTokenAmount } =
1526
useTokenAmount();
@@ -38,6 +49,8 @@ export function EditAmount({ prefix = '' }: EditAmountProps) {
3849
keyboardType="numeric"
3950
style={styles.input}
4051
/>
52+
{children}
53+
{hasAlert ? <Text style={styles.alert}>{alertMessage}</Text> : null}
4154
</View>
4255
);
4356
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './pay-token-balance';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { StyleSheet } from 'react-native';
2+
import { Theme } from '../../../../../util/theme/models';
3+
4+
const styleSheet = (_params: { theme: Theme }) =>
5+
StyleSheet.create({
6+
container: {},
7+
text: {
8+
textAlign: 'center',
9+
},
10+
});
11+
12+
export default styleSheet;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react-native';
3+
import { useTokensWithBalance } from '../../../../UI/Bridge/hooks/useTokensWithBalance';
4+
import { BridgeToken } from '../../../../UI/Bridge/types';
5+
import { useTransactionPayToken } from '../../hooks/pay/useTransactionPayToken';
6+
import { PayTokenBalance } from './pay-token-balance';
7+
8+
jest.mock('../../hooks/pay/useTransactionPayToken');
9+
jest.mock('../../../../UI/Bridge/hooks/useTokensWithBalance');
10+
11+
const TOKEN_ADDRESS_MOCK = '0xabcd1234abcd1234abcd1234abcd1234abcd1234';
12+
const CHAIN_ID_MOCK = '0x123';
13+
const BALANCE_FIAT_MOCK = '$100.12';
14+
15+
describe('PayTokenBalance', () => {
16+
const useTransactionPayTokenMock = jest.mocked(useTransactionPayToken);
17+
const useTokensWithBalanceMock = jest.mocked(useTokensWithBalance);
18+
19+
beforeEach(() => {
20+
jest.resetAllMocks();
21+
22+
useTransactionPayTokenMock.mockReturnValue({
23+
balanceHuman: '1.23',
24+
decimals: 4,
25+
payToken: {
26+
address: TOKEN_ADDRESS_MOCK,
27+
chainId: CHAIN_ID_MOCK,
28+
},
29+
setPayToken: jest.fn(),
30+
});
31+
32+
useTokensWithBalanceMock.mockReturnValue([
33+
{
34+
address: TOKEN_ADDRESS_MOCK,
35+
chainId: CHAIN_ID_MOCK,
36+
balanceFiat: BALANCE_FIAT_MOCK,
37+
},
38+
] as unknown as BridgeToken[]);
39+
});
40+
41+
it('renders pay token balance', () => {
42+
const { getByText } = render(<PayTokenBalance />);
43+
expect(getByText(`Available: ${BALANCE_FIAT_MOCK}`)).toBeTruthy();
44+
});
45+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
import { View } from 'react-native';
3+
import Text, {
4+
TextColor,
5+
} from '../../../../../component-library/components/Texts/Text';
6+
import { useStyles } from '../../../../../component-library/hooks';
7+
import styleSheet from './pay-token-balance.styles';
8+
import { useTokensWithBalance } from '../../../../UI/Bridge/hooks/useTokensWithBalance';
9+
import { useTransactionPayToken } from '../../hooks/pay/useTransactionPayToken';
10+
import { strings } from '../../../../../../locales/i18n';
11+
12+
export function PayTokenBalance() {
13+
const { styles } = useStyles(styleSheet, {});
14+
const { payToken } = useTransactionPayToken();
15+
const { address: payTokenAddress, chainId } = payToken;
16+
const tokens = useTokensWithBalance({ chainIds: [chainId] });
17+
18+
const token = tokens.find(
19+
(t) =>
20+
t.address.toLowerCase() === payTokenAddress.toLowerCase() &&
21+
t.chainId === chainId,
22+
);
23+
24+
if (!token) {
25+
return null;
26+
}
27+
28+
return (
29+
<View style={styles.container}>
30+
<Text style={styles.text} color={TextColor.Alternative}>
31+
{strings('confirm.available_balance')}
32+
{token.balanceFiat}
33+
</Text>
34+
</View>
35+
);
36+
}

0 commit comments

Comments
 (0)