Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adding blockaid banner to re-designed confirmation pages #12863

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
};
Loading