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",