diff --git a/app/components/UI/BlockaidBanner/AttributionLink.tsx b/app/components/UI/BlockaidBanner/AttributionLink.tsx
new file mode 100644
index 00000000000..4b2de700804
--- /dev/null
+++ b/app/components/UI/BlockaidBanner/AttributionLink.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Linking, StyleSheet } from 'react-native';
+
+import { strings } from '../../../../locales/i18n';
+import { DEFAULT_BANNERBASE_DESCRIPTION_TEXTVARIANT } from '../../../component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants';
+import Text from '../../../component-library/components/Texts/Text/Text';
+import { useTheme } from '../../../util/theme';
+
+const createStyles = (colors: any) =>
+ StyleSheet.create({
+ attributionLink: { color: colors.primary.default },
+ });
+
+const AttributionLink = () => {
+ const { colors } = useTheme();
+ const styles = createStyles(colors);
+
+ return (
+ {
+ Linking.openURL(strings('blockaid_banner.attribution_link'));
+ }}
+ >
+ {strings('blockaid_banner.attribution_link_name')}
+
+ );
+};
+
+export default AttributionLink;
diff --git a/app/components/UI/BlockaidBanner/BlockaidBanner.constants.ts b/app/components/UI/BlockaidBanner/BlockaidBanner.constants.ts
new file mode 100644
index 00000000000..60823e49483
--- /dev/null
+++ b/app/components/UI/BlockaidBanner/BlockaidBanner.constants.ts
@@ -0,0 +1,28 @@
+export const ATTRIBUTION_LINE_TEST_ID = 'blockaid-banner-attribution-line';
+
+import { Reason } from './BlockaidBanner.types';
+
+export const REASON_DESCRIPTION_I18N_KEY_MAP = Object.freeze({
+ [Reason.approvalFarming]: 'blockaid_banner.approval_farming_description',
+ [Reason.blurFarming]: 'blockaid_banner.blur_farming_description',
+ [Reason.maliciousDomain]: 'blockaid_banner.malicious_domain_description',
+ [Reason.failed]: 'blockaid_banner.failed_description',
+ [Reason.other]: 'blockaid_banner.other_description',
+ [Reason.permitFarming]: 'blockaid_banner.approval_farming_description',
+ [Reason.rawNativeTokenTransfer]:
+ 'blockaid_banner.transfer_farming_description',
+ [Reason.rawSignatureFarming]:
+ 'blockaid_banner.raw_signature_farming_description',
+ [Reason.seaportFarming]: 'blockaid_banner.seaport_farming_description',
+ [Reason.setApprovalForAllFarming]:
+ 'blockaid_banner.approval_farming_description',
+ [Reason.tradeOrderFarming]: 'blockaid_banner.trade_order_farming_description',
+ [Reason.transferFarming]: 'blockaid_banner.transfer_farming_description',
+ [Reason.transferFromFarming]: 'blockaid_banner.transfer_farming_description',
+ [Reason.unfairTrade]: 'blockaid_banner.unfair_trade_description',
+});
+
+export const REASON_TITLE_I18N_KEY_MAP: Record = Object.freeze({
+ [Reason.rawSignatureFarming]: 'blockaid_banner.suspicious_request_title',
+ [Reason.failed]: 'blockaid_banner.failed_title',
+});
diff --git a/app/components/UI/BlockaidBanner/BlockaidBanner.stories.tsx b/app/components/UI/BlockaidBanner/BlockaidBanner.stories.tsx
new file mode 100644
index 00000000000..68ecdafe6ed
--- /dev/null
+++ b/app/components/UI/BlockaidBanner/BlockaidBanner.stories.tsx
@@ -0,0 +1,58 @@
+/* eslint-disable no-console */
+import React from 'react';
+
+import { select, text } from '@storybook/addon-knobs';
+import { storiesOf } from '@storybook/react-native';
+
+import {
+ SAMPLE_BANNERALERT_DESCRIPTION,
+ SAMPLE_BANNERALERT_TITLE,
+} from '../../../component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.constants';
+import { storybookPropsGroupID } from '../../../component-library/constants/storybook.constants';
+import BlockaidBanner from './BlockaidBanner';
+import { BlockaidBannerProps, FlagType, Reason } from './BlockaidBanner.types';
+
+export const getBlockaidBannerStoryProps = (): BlockaidBannerProps => {
+ const flagTypeSelector = select(
+ 'flagType',
+ FlagType,
+ FlagType.Warning,
+ storybookPropsGroupID,
+ );
+
+ const reasonSelector = select(
+ 'reason',
+ Reason,
+ Reason.approvalFarming,
+ storybookPropsGroupID,
+ );
+
+ const title = text('title', SAMPLE_BANNERALERT_TITLE, storybookPropsGroupID);
+ const description = text(
+ 'description',
+ SAMPLE_BANNERALERT_DESCRIPTION,
+ storybookPropsGroupID,
+ );
+
+ return {
+ title,
+ description,
+ reason: reasonSelector,
+ flagType: flagTypeSelector,
+ features: [
+ 'Operator is an EOA',
+ 'Operator is untrusted according to previous activity',
+ ],
+ };
+};
+
+const BlockaidBannerStory = () => (
+
+);
+
+storiesOf('Components / UI / BlockaidBanner', module).add(
+ 'BlockaidBanner',
+ BlockaidBannerStory,
+);
+
+export default BlockaidBannerStory;
diff --git a/app/components/UI/BlockaidBanner/BlockaidBanner.styles.ts b/app/components/UI/BlockaidBanner/BlockaidBanner.styles.ts
new file mode 100644
index 00000000000..8e654424591
--- /dev/null
+++ b/app/components/UI/BlockaidBanner/BlockaidBanner.styles.ts
@@ -0,0 +1,34 @@
+// Third party dependencies.
+import { Theme } from '../../../util/theme/models';
+import { StyleSheet, ViewStyle } from 'react-native';
+import { BlockaidBannerStyleSheetVars } from './BlockaidBanner.types';
+/**
+ * Style sheet function for BannerAlert component.
+ *
+ * @param params Style sheet params.
+ * @param params.theme App theme from ThemeContext.
+ * @param params.vars Inputs that the style sheet depends on.
+ * @returns StyleSheet object.
+ */
+const styleSheet = (_params: {
+ theme: Theme;
+ vars: BlockaidBannerStyleSheetVars;
+}) =>
+ StyleSheet.create({
+ attributionBase: Object.assign({
+ height: 24,
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ alignItems: 'flex-start',
+ } as ViewStyle),
+ attributionItem: {
+ marginRight: 4,
+ },
+ detailsItem: {
+ marginBottom: 4,
+ },
+ details: { marginLeft: 10, marginBottom: 10 },
+ securityTickIcon: { marginTop: 4 },
+ });
+
+export default styleSheet;
diff --git a/app/components/UI/BlockaidBanner/BlockaidBanner.test.tsx b/app/components/UI/BlockaidBanner/BlockaidBanner.test.tsx
new file mode 100644
index 00000000000..4382df4a0a6
--- /dev/null
+++ b/app/components/UI/BlockaidBanner/BlockaidBanner.test.tsx
@@ -0,0 +1,142 @@
+import React from 'react';
+
+import { fireEvent, render } from '@testing-library/react-native';
+
+import { TESTID_ACCORDION_CONTENT } from '../../../component-library/components/Accordions/Accordion/Accordion.constants';
+import { TESTID_ACCORDIONHEADER } from '../../../component-library/components/Accordions/Accordion/foundation/AccordionHeader/AccordionHeader.constants';
+import { BANNERALERT_TEST_ID } from '../../../component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.constants';
+import BlockaidBanner from './BlockaidBanner';
+import { ATTRIBUTION_LINE_TEST_ID } from './BlockaidBanner.constants';
+import { FlagType, Reason } from './BlockaidBanner.types';
+
+jest.mock('../../../util/blockaid', () => ({
+ showBlockaidUI: jest.fn().mockReturnValue(true),
+}));
+
+describe('BlockaidBanner', () => {
+ const mockFeatures = [
+ 'We found attack vectors in this request',
+ 'This request shows a fake token name and icon.',
+ 'If you approve this request, a third party known for scams might take all your assets.',
+ 'Operator is an EOA',
+ 'Operator is untrusted according to previous activity',
+ ];
+
+ it('should render correctly', () => {
+ const wrapper = render(
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render correctly with reason "raw_signature_farming"', async () => {
+ const wrapper = render(
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ expect(await wrapper.queryByTestId(TESTID_ACCORDIONHEADER)).toBeDefined();
+ expect(
+ await wrapper.getByText('This is a suspicious request'),
+ ).toBeDefined();
+ expect(
+ await wrapper.getByText(
+ 'If you approve this request, you might lose your assets.',
+ ),
+ ).toBeDefined();
+ });
+
+ it('should render correctly with attribution link', async () => {
+ const wrapper = render(
+ ,
+ );
+
+ expect(await wrapper.queryByTestId(ATTRIBUTION_LINE_TEST_ID)).toBeDefined();
+ });
+
+ it('should render correctly with list attack details', async () => {
+ const wrapper = render(
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ expect(await wrapper.queryByTestId(TESTID_ACCORDIONHEADER)).toBeDefined();
+ expect(await wrapper.queryByTestId(TESTID_ACCORDION_CONTENT)).toBeNull();
+
+ fireEvent.press(await wrapper.getByText('See details'));
+
+ expect(await wrapper.queryByTestId(TESTID_ACCORDION_CONTENT)).toBeDefined();
+ expect(
+ await wrapper.queryByText('We found attack vectors in this request'),
+ ).toBeDefined();
+ expect(
+ await wrapper.queryByText(
+ 'This request shows a fake token name and icon.',
+ ),
+ ).toBeDefined();
+ expect(
+ await wrapper.queryByText(
+ 'If you approve this request, a third party known for scams might take all your assets.',
+ ),
+ ).toBeDefined();
+ expect(await wrapper.queryByText('Operator is an EOA')).toBeDefined();
+ expect(
+ await wrapper.queryByText(
+ 'Operator is untrusted according to previous activity',
+ ),
+ ).toBeDefined();
+ });
+
+ it('should not render if flagType is benign', async () => {
+ const wrapper = render(
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ expect(await wrapper.queryByTestId(TESTID_ACCORDIONHEADER)).toBeNull();
+ expect(await wrapper.queryByTestId(TESTID_ACCORDION_CONTENT)).toBeNull();
+ });
+
+ it('should render normal banner alert if flagType is failed', async () => {
+ const wrapper = render(
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+
+ expect(await wrapper.queryByTestId(TESTID_ACCORDIONHEADER)).toBeNull();
+ expect(await wrapper.queryByTestId(TESTID_ACCORDION_CONTENT)).toBeNull();
+ expect(await wrapper.queryByTestId(BANNERALERT_TEST_ID)).toBeDefined();
+ expect(await wrapper.queryByText('Request may not be safe')).toBeDefined();
+ expect(
+ await wrapper.queryByText(
+ 'Because of an error, this request was not verified by the security provider. Proceed with caution.',
+ ),
+ ).toBeDefined();
+ });
+});
diff --git a/app/components/UI/BlockaidBanner/BlockaidBanner.tsx b/app/components/UI/BlockaidBanner/BlockaidBanner.tsx
new file mode 100644
index 00000000000..90b7d8a3cde
--- /dev/null
+++ b/app/components/UI/BlockaidBanner/BlockaidBanner.tsx
@@ -0,0 +1,129 @@
+import React from 'react';
+import { View } from 'react-native-animatable';
+
+import { captureException } from '@sentry/react-native';
+
+import { strings } from '../../../../locales/i18n';
+import { AccordionHeaderHorizontalAlignment } from '../../../component-library/components/Accordions/Accordion';
+import Accordion from '../../../component-library/components/Accordions/Accordion/Accordion';
+import { BannerAlertSeverity } from '../../../component-library/components/Banners/Banner';
+import { DEFAULT_BANNERBASE_DESCRIPTION_TEXTVARIANT } from '../../../component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants';
+import BannerAlert from '../../../component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert';
+import {
+ IconColor,
+ IconName,
+ IconSize,
+} from '../../../component-library/components/Icons/Icon';
+import Icon from '../../../component-library/components/Icons/Icon/Icon';
+import Text from '../../../component-library/components/Texts/Text/Text';
+import { useStyles } from '../../../component-library/hooks/useStyles';
+import { showBlockaidUI } from '../../../util/blockaid';
+import AttributionLink from './AttributionLink';
+import {
+ ATTRIBUTION_LINE_TEST_ID,
+ REASON_DESCRIPTION_I18N_KEY_MAP,
+ REASON_TITLE_I18N_KEY_MAP,
+} from './BlockaidBanner.constants';
+import styleSheet from './BlockaidBanner.styles';
+import { BlockaidBannerProps, FlagType, Reason } from './BlockaidBanner.types';
+
+const getTitle = (reason: Reason): string =>
+ strings(
+ REASON_TITLE_I18N_KEY_MAP[reason] ||
+ 'blockaid_banner.deceptive_request_title',
+ );
+
+const getDescription = (reason: Reason) =>
+ strings(
+ REASON_DESCRIPTION_I18N_KEY_MAP[reason] ||
+ REASON_DESCRIPTION_I18N_KEY_MAP[Reason.other],
+ );
+
+const BlockaidBanner = (bannerProps: BlockaidBannerProps) => {
+ const { style, flagType, reason, features, onToggleShowDetails } =
+ bannerProps;
+
+ const { styles } = useStyles(styleSheet, { style });
+
+ if (!showBlockaidUI()) {
+ return null;
+ }
+
+ if (flagType === FlagType.Benign) {
+ return null;
+ }
+
+ const title = getTitle(reason);
+ const description = getDescription(reason);
+
+ if (flagType === FlagType.Failed) {
+ return (
+
+ );
+ }
+
+ if (!REASON_DESCRIPTION_I18N_KEY_MAP[reason]) {
+ captureException(`BlockaidBannerAlert: Unidentified reason '${reason}'`);
+ }
+
+ const renderDetails = () =>
+ features.length <= 0 ? null : (
+
+
+ {features.map((feature, i) => (
+
+ • {feature}
+
+ ))}
+
+
+ );
+
+ return (
+
+ {renderDetails()}
+
+
+
+
+
+
+
+ {strings('blockaid_banner.attribution')}
+
+
+
+
+
+
+
+ );
+};
+
+export default BlockaidBanner;
diff --git a/app/components/UI/BlockaidBanner/BlockaidBanner.types.ts b/app/components/UI/BlockaidBanner/BlockaidBanner.types.ts
new file mode 100644
index 00000000000..32a88f4eb3d
--- /dev/null
+++ b/app/components/UI/BlockaidBanner/BlockaidBanner.types.ts
@@ -0,0 +1,39 @@
+import { BannerAlertProps } from '../../../component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.types';
+
+export enum Reason {
+ approvalFarming = 'approval_farming',
+ blurFarming = 'blur_farming',
+ failed = 'failed',
+ maliciousDomain = 'malicious_domain',
+ other = 'other',
+ permitFarming = 'permit_farming',
+ rawNativeTokenTransfer = 'raw_native_token_transfer',
+ rawSignatureFarming = 'raw_signature_farming',
+ seaportFarming = 'seaport_farming',
+ setApprovalForAllFarming = 'set_approval_for_all_farming',
+ tradeOrderFarming = 'trade_order_farming',
+ transferFarming = 'transfer_farming',
+ transferFromFarming = 'transfer_from_farming',
+ unfairTrade = 'unfair_trade',
+}
+
+export enum FlagType {
+ Benign = 'Benign',
+ Malicious = 'Malicious',
+ Warning = 'Warning',
+ Failed = 'Failed',
+}
+
+type BlockaidBannerAllProps = BannerAlertProps & {
+ reason: Reason;
+ features: string[];
+ flagType: FlagType;
+ onToggleShowDetails?: () => void;
+};
+
+export type BlockaidBannerProps = Omit;
+
+/**
+ * Style sheet input parameters.
+ */
+export type BlockaidBannerStyleSheetVars = Pick;
diff --git a/app/components/UI/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap b/app/components/UI/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap
new file mode 100644
index 00000000000..62c70ffdd6c
--- /dev/null
+++ b/app/components/UI/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap
@@ -0,0 +1,773 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BlockaidBanner should not render if flagType is benign 1`] = `null`;
+
+exports[`BlockaidBanner should render correctly 1`] = `
+
+
+
+
+
+
+ This is a deceptive request
+
+
+ If you approve this request, a third party known for scams might take all your assets.
+
+
+
+ See details
+
+
+
+
+
+
+
+
+
+
+
+ Security advice by
+
+
+
+
+ Blockaid
+
+
+
+
+
+`;
+
+exports[`BlockaidBanner should render correctly with list attack details 1`] = `
+
+
+
+
+
+
+ This is a deceptive request
+
+
+ If you approve this request, a third party known for scams might take all your assets.
+
+
+
+ See details
+
+
+
+
+
+
+
+
+
+
+
+ Security advice by
+
+
+
+
+ Blockaid
+
+
+
+
+
+`;
+
+exports[`BlockaidBanner should render correctly with reason "raw_signature_farming" 1`] = `
+
+
+
+
+
+
+ This is a suspicious request
+
+
+ If you approve this request, you might lose your assets.
+
+
+
+ See details
+
+
+
+
+
+
+
+
+
+
+
+ Security advice by
+
+
+
+
+ Blockaid
+
+
+
+
+
+`;
+
+exports[`BlockaidBanner should render normal banner alert if flagType is failed 1`] = `
+
+
+
+
+
+
+ This is a suspicious request
+
+
+ If you approve this request, you might lose your assets.
+
+
+
+`;
diff --git a/locales/languages/en.json b/locales/languages/en.json
index d468c9d521c..c77c5b6382a 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -1,4 +1,23 @@
{
+ "blockaid_banner": {
+ "approval_farming_description": "If you approve this request, a third party known for scams might take all your assets.",
+ "attribution": "Security advice by",
+ "attribution_link": "https://blockaid.me",
+ "attribution_link_name": "Blockaid",
+ "blur_farming_description": "If you approve this request, someone can steal your assets listed on Blur.",
+ "deceptive_request_title": "This is a deceptive request",
+ "failed_title": "Request may not be safe",
+ "failed_description": "Because of an error, this request was not verified by the security provider. Proceed with caution.",
+ "malicious_domain_description": "You're interacting with a malicious domain. If you approve this request, you might lose your assets.",
+ "other_description": "If you approve this request, you might lose your assets.",
+ "raw_signature_farming_description": "If you approve this request, you might lose your assets.",
+ "seaport_farming_description": "If you approve this request, someone can steal your assets listed on OpenSea.",
+ "see_details": "See details",
+ "suspicious_request_title": "This is a suspicious request",
+ "trade_order_farming_description": "If you approve this request, you might lose your assets.",
+ "transfer_farming_description": "If you approve this request, a third party known for scams will take all your assets.",
+ "unfair_trade_description": "If you approve this request, you might lose your assets."
+ },
"date": {
"months": {
"0": "Jan",