Skip to content

Commit

Permalink
feat: Adding blockaid banner to re-designed confirmation pages (#12863)
Browse files Browse the repository at this point in the history
## **Description**

Add blockaid banner to re-designed signature request pages.

## **Related issues**

Fixes: MetaMask/MetaMask-planning#3741

## **Manual testing steps**

1. Enable re-designs confirmations locally on mobile
2. Submit malicious signature request
3. Ensure that blockaid banner is present on the page

## **Screenshots/Recordings**
<img width="378" alt="Screenshot 2025-01-08 at 4 36 09 PM"
src="https://github.com/user-attachments/assets/3b0433de-73b8-4d51-997c-134d4cd63857"
/>

## **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.
  • Loading branch information
jpuri authored Jan 10, 2025
1 parent e726417 commit 596c252
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 7 deletions.
48 changes: 41 additions & 7 deletions app/components/Views/confirmations/Confirm/Confirm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import React from 'react';
import renderWithProvider from '../../../../util/test/renderWithProvider';
import {
personalSignatureConfirmationState,
securityAlertResponse,
typedSignV1ConfirmationState,
} from '../../../../util/test/confirm-data-helpers';
import Confirm from './index';

jest.mock('react-native-gzip', () => ({
deflate: (str: string) => str,
}));

describe('Confirm', () => {
it('should match snapshot for personal sign', async () => {
it('should render correct information for personal sign', async () => {
const { getAllByRole, getByText } = renderWithProvider(<Confirm />, {
state: personalSignatureConfirmationState,
});
Expand All @@ -26,13 +31,11 @@ describe('Confirm', () => {
expect(getAllByRole('button')).toHaveLength(2);
});

it('should match snapshot for typed sign v1', async () => {
const { getAllByRole, getAllByText, getByText } = renderWithProvider(
<Confirm />,
{
it('should render correct information for typed sign v1', async () => {
const { getAllByRole, getAllByText, getByText, queryByText } =
renderWithProvider(<Confirm />, {
state: typedSignV1ConfirmationState,
},
);
});
expect(getByText('Signature request')).toBeDefined();
expect(getByText('Estimated changes')).toBeDefined();
expect(
Expand All @@ -45,5 +48,36 @@ describe('Confirm', () => {
expect(getAllByText('Message')).toHaveLength(2);
expect(getByText('Hi, Alice!')).toBeDefined();
expect(getAllByRole('button')).toHaveLength(2);
expect(queryByText('This is a deceptive request')).toBeNull();
});

it('should render blockaid banner is confirmation has blockaid error response', async () => {
const typedSignApproval =
typedSignV1ConfirmationState.engine.backgroundState.ApprovalController
.pendingApprovals['7e62bcb1-a4e9-11ef-9b51-ddf21c91a998'];
const { getByText } = renderWithProvider(<Confirm />, {
state: {
...typedSignV1ConfirmationState,
engine: {
...typedSignV1ConfirmationState.engine,
backgroundState: {
...typedSignV1ConfirmationState.engine.backgroundState,
ApprovalController: {
pendingApprovals: {
'fb2029e1-b0ab-11ef-9227-05a11087c334': {
...typedSignApproval,
requestData: {
...typedSignApproval.requestData,
securityAlertResponse,
},
},
},
},
},
},
},
});
expect(getByText('Signature request')).toBeDefined();
expect(getByText('This is a deceptive request')).toBeDefined();
});
});
3 changes: 3 additions & 0 deletions app/components/Views/confirmations/Confirm/Confirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import BottomModal from '../components/UI/BottomModal';
import AccountNetworkInfo from '../components/Confirm/AccountNetworkInfo';
import Footer from '../components/Confirm/Footer';
import Info from '../components/Confirm/Info';
import SignatureBlockaidBanner from '../components/Confirm/SignatureBlockaidBanner';
import Title from '../components/Confirm/Title';
import useConfirmationRedesignEnabled from '../hooks/useConfirmationRedesignEnabled';
import styleSheet from './Confirm.styles';
Expand All @@ -23,6 +24,8 @@ const Confirm = () => {
<View style={styles.container}>
<View>
<Title />
{/* TODO: component SignatureBlockaidBanner to be removed once we implement alert system in mobile */}
<SignatureBlockaidBanner />
<AccountNetworkInfo />
<Info />
</View>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StyleSheet } from 'react-native';

const styleSheet = () =>
StyleSheet.create({
blockaidBanner: {
marginBottom: 8,
},
});

export default styleSheet;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';

import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import {
securityAlertResponse,
typedSignV1ConfirmationState,
} from '../../../../../../util/test/confirm-data-helpers';
import SignatureBlockaidBanner from './index';

jest.mock('react-native-gzip', () => ({
deflate: (str: string) => str,
}));

const mockTrackEvent = jest.fn();
jest.mock('../../../../../hooks/useMetrics', () => ({
useMetrics: () => ({
trackEvent: mockTrackEvent,
createEventBuilder: () => ({
addProperties: () => ({ build: () => ({}) }),
}),
}),
}));

jest.mock('../../../../../../util/confirmation/signatureUtils', () => ({
getAnalyticsParams: () => ({}),
}));

const typedSignApproval =
typedSignV1ConfirmationState.engine.backgroundState.ApprovalController
.pendingApprovals['7e62bcb1-a4e9-11ef-9b51-ddf21c91a998'];
const typedSignV1ConfirmationStateWithBlockaidResponse = {
engine: {
...typedSignV1ConfirmationState.engine,
backgroundState: {
...typedSignV1ConfirmationState.engine.backgroundState,
ApprovalController: {
pendingApprovals: {
'fb2029e1-b0ab-11ef-9227-05a11087c334': {
...typedSignApproval,
requestData: {
...typedSignApproval.requestData,
securityAlertResponse,
},
},
},
},
},
},
};

describe('Confirm', () => {
it('should return null if request does not have securityAlertResponse', async () => {
const { queryByText } = renderWithProvider(<SignatureBlockaidBanner />, {
state: typedSignV1ConfirmationState,
});
expect(queryByText('This is a deceptive request')).toBeNull();
});

it('should render blockaid banner alert if blockaid returns error', async () => {
const { getByText } = renderWithProvider(<SignatureBlockaidBanner />, {
state: typedSignV1ConfirmationStateWithBlockaidResponse,
});
expect(getByText('This is a deceptive request')).toBeDefined();
});

it('should call trackMetrics method when report issue link is clicked', async () => {
const { getByText, getByTestId } = renderWithProvider(
<SignatureBlockaidBanner />,
{
state: typedSignV1ConfirmationStateWithBlockaidResponse,
},
);
fireEvent.press(getByTestId('accordionheader'));
fireEvent.press(getByText('Report an issue'));
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useCallback } from 'react';

import { MetaMetricsEvents } from '../../../../../../core/Analytics';
import { getAnalyticsParams } from '../../../../../../util/confirmation/signatureUtils';
import { useStyles } from '../../../../../../component-library/hooks';
import { useMetrics } from '../../../../../hooks/useMetrics';
import BlockaidBanner from '../../../components/BlockaidBanner/BlockaidBanner';
import useApprovalRequest from '../../../hooks/useApprovalRequest';
import styleSheet from './SignatureBlockaidBanner.styles';

const SignatureBlockaidBanner = () => {
const { approvalRequest } = useApprovalRequest();
const { trackEvent, createEventBuilder } = useMetrics();
const { styles } = useStyles(styleSheet, {});

const {
type,
requestData: { from: fromAddress },
} = approvalRequest ?? {
requestData: {},
};

const onContactUsClicked = useCallback(() => {
const analyticsParams = {
...getAnalyticsParams(
{
from: fromAddress,
},
type,
),
external_link_clicked: 'security_alert_support_link',
};
trackEvent(
createEventBuilder(MetaMetricsEvents.SIGNATURE_REQUESTED)
.addProperties(analyticsParams)
.build(),
);
}, [trackEvent, createEventBuilder, type, fromAddress]);

if (!approvalRequest?.requestData?.securityAlertResponse) {
return null;
}

return (
<BlockaidBanner
onContactUsClicked={onContactUsClicked}
securityAlertResponse={
approvalRequest?.requestData?.securityAlertResponse
}
style={styles.blockaidBanner}
/>
);
};

export default SignatureBlockaidBanner;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './SignatureBlockaidBanner';
32 changes: 32 additions & 0 deletions app/util/test/confirm-data-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,35 @@ export const typedSignV3ConfirmationState = {
},
},
};

export const securityAlertResponse = {
block: 21572398,
result_type: 'Malicious',
reason: 'permit_farming',
description:
'permit_farming to spender 0x1661f1b207629e4f385da89cff535c8e5eb23ee3, classification: A known malicious address is involved in the transaction',
features: ['A known malicious address is involved in the transaction'],
source: 'api',
securityAlertId: '43d40543-463a-4400-993c-85a04017ea2b',
req: {
channelId: undefined,
data: '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","domain":{"name":"USD Coin","verifyingContract":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","chainId":1,"version":"2"},"message":{"owner":"0x8eeee1781fd885ff5ddef7789486676961873d12","spender":"0x1661F1B207629e4F385DA89cFF535C8E5Eb23Ee3","value":"1033366316628","nonce":1,"deadline":1678709555}}',
from: '0x8eeee1781fd885ff5ddef7789486676961873d12',
meta: {
analytics: {
request_platform: undefined,
request_source: 'In-App-Browser',
},
channelId: undefined,
icon: { uri: 'https://metamask.github.io/test-dapp/metamask-fox.svg' },
title: 'E2E Test Dapp',
url: 'https://metamask.github.io/test-dapp/',
},
metamaskId: '967066d0-ccf4-11ef-8589-cb239497eefc',
origin: 'metamask.github.io',
requestId: 2048976252,
securityAlertResponse: undefined,
version: 'V4',
},
chainId: '0x1',
};

0 comments on commit 596c252

Please sign in to comment.