From e424debfc0ed834a06dc825fe6470953ccab1b6a Mon Sep 17 00:00:00 2001 From: vthomas13 <10986371+vthomas13@users.noreply.github.com> Date: Thu, 23 Mar 2023 10:55:43 -0400 Subject: [PATCH 01/12] adding opensea security provider image (#18292) --- app/images/open-sea-security-provider.svg | 91 +++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/images/open-sea-security-provider.svg diff --git a/app/images/open-sea-security-provider.svg b/app/images/open-sea-security-provider.svg new file mode 100644 index 000000000000..ac79c0026adb --- /dev/null +++ b/app/images/open-sea-security-provider.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From ed5b78d61ba42ac85a0382072811554795fb7b7b Mon Sep 17 00:00:00 2001 From: Garrett Bear Date: Thu, 23 Mar 2023 08:24:23 -0700 Subject: [PATCH 02/12] Feat/add header base component (#18043) * add header base component * fix resizing issue * add center * add demo * header base using flexbox * fix button issue * header base clean up * update tests * add readme description * add docs * update snapshot * add more to readme * convert to TS * fix file name * fix types and colors * fix classname error * fix boxprops import * fix boxprops * prop fix * fix errors * Update ui/components/component-library/header-base/header-base.stories.tsx Co-authored-by: George Marshall * Update ui/components/component-library/header-base/header-base.types.ts Co-authored-by: George Marshall * Update ui/components/component-library/header-base/header-base.types.ts Co-authored-by: George Marshall * headerbase fixes * fix export * remove Math.max * change order for index on storybook to prep build * revert back to order * remove type from export * add type to export * change export of headerbase function * export update * revert back to normal * add type to export * Removing interface export from index --------- Co-authored-by: George Marshall --- .../component-library/header-base/README.mdx | 114 +++++++ .../__snapshots__/header-base.test.tsx.snap | 17 + .../header-base/header-base.stories.tsx | 293 ++++++++++++++++++ .../header-base/header-base.test.tsx | 61 ++++ .../header-base/header-base.tsx | 117 +++++++ .../header-base/header-base.types.ts | 33 ++ .../component-library/header-base/index.ts | 2 + ui/components/component-library/index.js | 1 + 8 files changed, 638 insertions(+) create mode 100644 ui/components/component-library/header-base/README.mdx create mode 100644 ui/components/component-library/header-base/__snapshots__/header-base.test.tsx.snap create mode 100644 ui/components/component-library/header-base/header-base.stories.tsx create mode 100644 ui/components/component-library/header-base/header-base.test.tsx create mode 100644 ui/components/component-library/header-base/header-base.tsx create mode 100644 ui/components/component-library/header-base/header-base.types.ts create mode 100644 ui/components/component-library/header-base/index.ts diff --git a/ui/components/component-library/header-base/README.mdx b/ui/components/component-library/header-base/README.mdx new file mode 100644 index 000000000000..ab8c671a5467 --- /dev/null +++ b/ui/components/component-library/header-base/README.mdx @@ -0,0 +1,114 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; +import { HeaderBase } from './header-base'; + +### This is a base component. It should not be used in your feature code directly but as a "base" for other UI components + +# HeaderBase + +The `HeaderBase` component is a reusable UI component for displaying a header with optional startAccessory, children (title) and endAccessory content areas. It is designed to be flexible and customizable for various use cases to keep a visually balanced appearance. + + + + + +## Props + +The `HeaderBase` accepts all props below as well as all [Box](/docs/components-ui-box--default-story#props) component props + + + +### Children + +Wrapping content in the `HeaderBase` component will be rendered in the center of the header. + +Use the `childrenWrapperProps` prop to customize the wrapper element around the `children` content. + + + + + +```jsx +import { HeaderBase, Text } from '../../component-library'; +import { + TEXT_ALIGN, + TextVariant, +} from '../../../helpers/constants/design-system'; + + + + Title is sentence case no period + +; +``` + +### startAccessory + +Using the `startAccessory` prop will render the content in the start (left) side of the header. + +Use the `startAccessoryWrapperProps` prop to customize the wrapper element around the `startAccessory` content. + + + + + +```jsx +import { HeaderBase, Text } from '../../component-library'; +import { + TEXT_ALIGN, + TextVariant, +} from '../../../helpers/constants/design-system'; + + + } +> + + Title is sentence case no period + +; +``` + +### endAccessory + +Using the `endAccessory` prop will render the content in the end (right) side of the header. + +Use the `endAccessoryWrapperProps` prop to customize the wrapper element around the `endAccessory` content. + + + + + +```jsx +import { HeaderBase, Text } from '../../component-library'; +import { + TEXT_ALIGN, + TextVariant, +} from '../../../helpers/constants/design-system'; + + + } +> + + Title is sentence case no period + +; +``` + +### Use Case Demos + +Some examples of how the `HeaderBase` component can be used in various use cases with background colors set for visual aid. + + + + diff --git a/ui/components/component-library/header-base/__snapshots__/header-base.test.tsx.snap b/ui/components/component-library/header-base/__snapshots__/header-base.test.tsx.snap new file mode 100644 index 000000000000..6253be824a9f --- /dev/null +++ b/ui/components/component-library/header-base/__snapshots__/header-base.test.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HeaderBase should render HeaderBase element correctly 1`] = ` +
+
+
+ should render HeaderBase element correctly +
+
+
+`; diff --git a/ui/components/component-library/header-base/header-base.stories.tsx b/ui/components/component-library/header-base/header-base.stories.tsx new file mode 100644 index 000000000000..be26fe8741ca --- /dev/null +++ b/ui/components/component-library/header-base/header-base.stories.tsx @@ -0,0 +1,293 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import Box from '../../ui/box'; +import { + ICON_NAMES, + Button, + ButtonIcon, + BUTTON_ICON_SIZES, + BUTTON_SIZES, + Text, +} from '..'; +import { + AlignItems, + BackgroundColor, + TextVariant, + TEXT_ALIGN, +} from '../../../helpers/constants/design-system'; +import { HeaderBase } from './header-base'; +import README from './README.mdx'; + +export default { + title: 'Components/ComponentLibrary/HeaderBase', + component: HeaderBase, + parameters: { + docs: { + page: README, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const DefaultStory = Template.bind({}); + +DefaultStory.args = { + children: ( + + Title is sentence case no period + + ), + startAccessory: ( + + ), + endAccessory: ( + + ), +}; + +DefaultStory.storyName = 'Default'; + +export const Children = (args) => { + return ( + + + Title is sentence case no period + + + ); +}; + +export const StartAccessory = (args) => { + return ( + + } + {...args} + > + + Title is sentence case no period + + + ); +}; + +export const EndAccessory = (args) => { + return ( + + } + {...args} + > + + Title is sentence case no period + + + ); +}; + +export const UseCaseDemos = (args) => ( + <> + children only assigned + + + + Title is sentence case no period + + + + children and endAccessory assigned + + + } + {...args} + > + + Title is sentence case no period + + + + children and startAccessory assigned + + + } + {...args} + > + + Title is sentence case no period + + + + children, startAccessory, and endAccessory assigned + + + } + endAccessory={ + + } + {...args} + > + + Title is sentence case no period + + + + children, startAccessory, and endAccessory assigned + + + Unlock Now + + } + endAccessory={ + + } + {...args} + > + + Title is sentence case no period + + + + + children, startAccessory, and endAccessory assigned with prop alignItems= + {AlignItems.center} passed at HeaderBase + + + + } + endAccessory={ + + } + {...args} + > + + Title is sentence case no period + + + + startAccessory and endAccessory assigned + + + Unlock + + } + endAccessory={ + + } + {...args} + > + + +); diff --git a/ui/components/component-library/header-base/header-base.test.tsx b/ui/components/component-library/header-base/header-base.test.tsx new file mode 100644 index 000000000000..9d4ccec69f26 --- /dev/null +++ b/ui/components/component-library/header-base/header-base.test.tsx @@ -0,0 +1,61 @@ +/* eslint-disable jest/require-top-level-describe */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { Icon, ICON_NAMES } from '..'; +import { HeaderBase } from './header-base'; + +describe('HeaderBase', () => { + it('should render HeaderBase element correctly', () => { + const { getByTestId, container } = render( + + should render HeaderBase element correctly + , + ); + expect(getByTestId('header-base')).toHaveClass('mm-header-base'); + expect(container).toMatchSnapshot(); + }); + + it('should render with added classname', () => { + const { getByTestId } = render( + + should render HeaderBase element correctly + , + ); + expect(getByTestId('header-base')).toHaveClass('mm-header-base--test'); + }); + + it('should render HeaderBase children', () => { + const { getByText } = render( + HeaderBase children test, + ); + expect(getByText('HeaderBase children test')).toBeDefined(); + }); + + it('should render HeaderBase startAccessory', () => { + const { getByTestId } = render( + + } + />, + ); + + expect(getByTestId('start-accessory')).toBeDefined(); + }); + + it('should render HeaderBase endAccessory', () => { + const { getByTestId } = render( + + } + />, + ); + + expect(getByTestId('end-accessory')).toBeDefined(); + }); +}); diff --git a/ui/components/component-library/header-base/header-base.tsx b/ui/components/component-library/header-base/header-base.tsx new file mode 100644 index 000000000000..220a596d43dd --- /dev/null +++ b/ui/components/component-library/header-base/header-base.tsx @@ -0,0 +1,117 @@ +import React, { useRef, useLayoutEffect, useMemo, useState } from 'react'; +import classnames from 'classnames'; +import { + BLOCK_SIZES, + DISPLAY, + JustifyContent, +} from '../../../helpers/constants/design-system'; +import Box from '../../ui/box'; + +import { HeaderBaseProps } from './header-base.types'; + +export const HeaderBase: React.FC = ({ + startAccessory, + endAccessory, + className = '', + children, + childrenWrapperProps, + startAccessoryWrapperProps, + endAccessoryWrapperProps, + ...props +}) => { + const startAccessoryRef = useRef(null); + const endAccessoryRef = useRef(null); + const [accessoryMinWidth, setAccessoryMinWidth] = useState(); + + useLayoutEffect(() => { + function handleResize() { + if (startAccessoryRef.current && endAccessoryRef.current) { + const accMinWidth = Math.max( + startAccessoryRef.current.scrollWidth, + endAccessoryRef.current.scrollWidth, + ); + setAccessoryMinWidth(accMinWidth); + } else if (startAccessoryRef.current && !endAccessoryRef.current) { + setAccessoryMinWidth(startAccessoryRef.current.scrollWidth); + } else if (!startAccessoryRef.current && endAccessoryRef.current) { + setAccessoryMinWidth(endAccessoryRef.current.scrollWidth); + } else { + setAccessoryMinWidth(0); + } + } + + handleResize(); + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [startAccessoryRef, endAccessoryRef, children]); + + const getTitleStyles = useMemo(() => { + if (startAccessory && !endAccessory) { + return { + marginRight: `${accessoryMinWidth}px`, + }; + } else if (!startAccessory && endAccessory) { + return { + marginLeft: `${accessoryMinWidth}px`, + }; + } + return {}; + }, [accessoryMinWidth, startAccessory, endAccessory]); + + return ( + + {startAccessory && ( + + {startAccessory} + + )} + {children && ( + + {children} + + )} + {endAccessory && ( + + {endAccessory} + + )} + + ); +}; diff --git a/ui/components/component-library/header-base/header-base.types.ts b/ui/components/component-library/header-base/header-base.types.ts new file mode 100644 index 000000000000..a871ce744a04 --- /dev/null +++ b/ui/components/component-library/header-base/header-base.types.ts @@ -0,0 +1,33 @@ +import React from 'react'; +import type { BoxProps } from '../../ui/box/box.d'; + +export interface HeaderBaseProps extends BoxProps { + /** + * The children is the title area of the HeaderBase + */ + children?: React.ReactNode; + /** + * Use the `childrenWrapperProps` prop to define the props to the children wrapper + */ + childrenWrapperProps?: BoxProps; + /** + * The start(default left) content area of HeaderBase + */ + startAccessory?: React.ReactNode; + /** + * Use the `startAccessoryWrapperProps` prop to define the props to the start accessory wrapper + */ + startAccessoryWrapperProps?: BoxProps; + /** + * The end (default right) content area of HeaderBase + */ + endAccessory?: React.ReactNode; + /** + * Use the `endAccessoryWrapperProps` prop to define the props to the end accessory wrapper + */ + endAccessoryWrapperProps?: BoxProps; + /** + * An additional className to apply to the HeaderBase + */ + className?: string; +} diff --git a/ui/components/component-library/header-base/index.ts b/ui/components/component-library/header-base/index.ts new file mode 100644 index 000000000000..1b47557f8897 --- /dev/null +++ b/ui/components/component-library/header-base/index.ts @@ -0,0 +1,2 @@ +export { HeaderBase } from './header-base'; +export type { HeaderBaseProps } from './header-base.types'; diff --git a/ui/components/component-library/index.js b/ui/components/component-library/index.js index 33e8ab60480e..f24721433eeb 100644 --- a/ui/components/component-library/index.js +++ b/ui/components/component-library/index.js @@ -21,6 +21,7 @@ export { ButtonLink, BUTTON_LINK_SIZES } from './button-link'; export { ButtonPrimary, BUTTON_PRIMARY_SIZES } from './button-primary'; export { ButtonSecondary, BUTTON_SECONDARY_SIZES } from './button-secondary'; export { FormTextField } from './form-text-field'; +export { HeaderBase } from './header-base'; export { HelpText } from './help-text'; export { Icon, ICON_NAMES, ICON_SIZES } from './icon'; export { Label } from './label'; From 048c3e32583c4db3c05a9d6da9de0803e4a148e3 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 23 Mar 2023 16:52:01 +0100 Subject: [PATCH 03/12] [FLASK] Update iframe-execution-environment (#18299) --- app/scripts/metamask-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 6a2c1107d07c..1e9698e7827b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -748,7 +748,7 @@ export default class MetamaskController extends EventEmitter { ///: BEGIN:ONLY_INCLUDE_IN(flask) const snapExecutionServiceArgs = { iframeUrl: new URL( - 'https://metamask.github.io/iframe-execution-environment/0.13.0', + 'https://metamask.github.io/iframe-execution-environment/0.14.0', ), messenger: this.controllerMessenger.getRestricted({ name: 'ExecutionService', From 270ff26561677c173c8137be914e618261827aca Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 23 Mar 2023 09:07:28 -0700 Subject: [PATCH 04/12] Fixes to the Linea Goerli implementation (#18290) * Ensure that NonInfuraDefaultNetworks are only selected in the dropdown if they are the currently selected network * Ensure Linea Goerli network appears in network settings tab if added manually --- .../app/dropdowns/network-dropdown.js | 21 ++++++++----------- .../settings/networks-tab/networks-tab.js | 20 ++++++++++-------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/ui/components/app/dropdowns/network-dropdown.js b/ui/components/app/dropdowns/network-dropdown.js index 09e94c4981d7..c40ccc79d887 100644 --- a/ui/components/app/dropdowns/network-dropdown.js +++ b/ui/components/app/dropdowns/network-dropdown.js @@ -286,22 +286,20 @@ class NetworkDropdown extends Component { } renderNonInfuraDefaultNetwork(networkConfigurations, network) { - const { - provider: { type: providerType }, - setActiveNetwork, - upsertNetworkConfiguration, - } = this.props; + const { provider, setActiveNetwork, upsertNetworkConfiguration } = + this.props; - const isCurrentRpcTarget = providerType === NETWORK_TYPES.RPC; + const { chainId, ticker, blockExplorerUrl } = BUILT_IN_NETWORKS[network]; + const networkName = NETWORK_TO_NAME_MAP[network]; + const rpcUrl = CHAIN_ID_TO_RPC_URL_MAP[chainId]; + + const isCurrentRpcTarget = + provider.type === NETWORK_TYPES.RPC && rpcUrl === provider.rpcUrl; return ( { - const { chainId, ticker, blockExplorerUrl } = - BUILT_IN_NETWORKS[network]; - const networkName = NETWORK_TO_NAME_MAP[network]; - const networkConfiguration = pickBy( networkConfigurations, (config) => config.rpcUrl === CHAIN_ID_TO_RPC_URL_MAP[chainId], @@ -310,7 +308,6 @@ class NetworkDropdown extends Component { let configurationId = null; // eslint-disable-next-line no-extra-boolean-cast, no-implicit-coercion if (!!networkConfiguration) { - const rpcUrl = CHAIN_ID_TO_RPC_URL_MAP[chainId]; configurationId = await upsertNetworkConfiguration( { rpcUrl, @@ -346,7 +343,7 @@ class NetworkDropdown extends Component { data-testid={`${network}-network-item`} style={{ color: - providerType === network + provider.type === network ? 'var(--color-text-default)' : 'var(--color-text-alternative)', }} diff --git a/ui/pages/settings/networks-tab/networks-tab.js b/ui/pages/settings/networks-tab/networks-tab.js index bbc0443cd105..7ba0e29af917 100644 --- a/ui/pages/settings/networks-tab/networks-tab.js +++ b/ui/pages/settings/networks-tab/networks-tab.js @@ -30,11 +30,13 @@ import NetworksTabContent from './networks-tab-content'; import NetworksForm from './networks-form'; import NetworksFormSubheader from './networks-tab-subheader'; -const defaultNetworks = defaultNetworksData.map((network) => ({ - ...network, - viewOnly: true, - isATestNetwork: TEST_CHAINS.includes(network.chainId), -})); +const defaultNetworks = defaultNetworksData + .map((network) => ({ + ...network, + viewOnly: true, + isATestNetwork: TEST_CHAINS.includes(network.chainId), + })) + .filter((network) => network.chainId !== CHAIN_IDS.LINEA_TESTNET); const NetworksTab = ({ addNewNetwork }) => { const t = useI18nContext(); @@ -55,8 +57,8 @@ const NetworksTab = ({ addNewNetwork }) => { getNetworksTabSelectedNetworkConfigurationId, ); - const networkConfigurationsList = Object.entries(networkConfigurations) - .map(([networkConfigurationId, networkConfiguration]) => { + const networkConfigurationsList = Object.entries(networkConfigurations).map( + ([networkConfigurationId, networkConfiguration]) => { return { label: networkConfiguration.nickname, iconColor: 'var(--color-icon-alternative)', @@ -68,8 +70,8 @@ const NetworksTab = ({ addNewNetwork }) => { isATestNetwork: TEST_CHAINS.includes(networkConfiguration.chainId), networkConfigurationId, }; - }) - .filter((network) => network.chainId !== CHAIN_IDS.LINEA_TESTNET); + }, + ); let networksToRender = [...defaultNetworks, ...networkConfigurationsList]; if (!SHOULD_SHOW_LINEA_TESTNET_NETWORK) { From c89b93dc1dfe12176c276fd397b8c7f280b4f26e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Oliv=C3=A9?= Date: Thu, 23 Mar 2023 17:09:09 +0100 Subject: [PATCH 05/12] adding code fence in extension file (#17874) --- app/scripts/platforms/extension.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index faf9af160bf7..3a298e699c41 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -176,9 +176,14 @@ export default class ExtensionPlatform { _showFailedTransaction(txMeta, errorMessage) { const nonce = parseInt(txMeta.txParams.nonce, 16); const title = 'Failed transaction'; - const message = `Transaction ${nonce} failed! ${ + let message = `Transaction ${nonce} failed! ${ errorMessage || txMeta.err.message }`; + ///: BEGIN:ONLY_INCLUDE_IN(mmi) + if (isNaN(nonce)) { + message = `Transaction failed! ${errorMessage || txMeta.err.message}`; + } + ///: END:ONLY_INCLUDE_IN this._showNotification(title, message); } From 196b8408d0cdf5e35e241be9f0ca98337b4acf0d Mon Sep 17 00:00:00 2001 From: Ariella Vu <20778143+digiwand@users.noreply.github.com> Date: Thu, 23 Mar 2023 09:32:54 -0700 Subject: [PATCH 06/12] PermissionsConnectHeader: unlock SiteOrigin title (#18270) * PermissionsConnectHeader: unlock SiteOrigin title * SignatureRequestOriginal: unlock SiteOrigin title * signature-req: update snapshots --- .../permissions-connect-header.component.js | 1 + .../__snapshots__/signature-request-original.test.js.snap | 1 + .../signature-request-original.component.js | 1 + .../__snapshots__/signature-request-siwe.test.js.snap | 1 + 4 files changed, 4 insertions(+) diff --git a/ui/components/app/permissions-connect-header/permissions-connect-header.component.js b/ui/components/app/permissions-connect-header/permissions-connect-header.component.js index cc8f955135df..43b6d5f7e323 100644 --- a/ui/components/app/permissions-connect-header/permissions-connect-header.component.js +++ b/ui/components/app/permissions-connect-header/permissions-connect-header.component.js @@ -64,6 +64,7 @@ export default class PermissionsConnectHeader extends Component {
Date: Thu, 23 Mar 2023 18:01:51 +0100 Subject: [PATCH 07/12] OpenSea security provider metrics (#17688) * Added metrics for the OpenSea security provider * Fixed tests * Fixed a test * Fixed metrics * Code refactor * Lint fixed * Removed unnecessary code * Fix build * Fix e2e * Cleanup * Fix e2e * Code refactor * Removed unnecessary code * rpc middleware: catch securityProviderCheck errors to not block dapp rpc requests * Fixed an issue * Added aditional test * Applied some changes * Fixed a test * Fixed a test * Code refactor * Covered more code with tests * Updated a test * Fixed an issue --------- Co-authored-by: Jyoti Puri Co-authored-by: digiwand <20778143+digiwand@users.noreply.github.com> Co-authored-by: Brad Decker --- app/scripts/controllers/metametrics.js | 3 + app/scripts/controllers/metametrics.test.js | 2 + app/scripts/controllers/transactions/index.js | 12 ++ .../controllers/transactions/index.test.js | 170 ++++++++++++++++++ .../lib/createRPCMethodTrackingMiddleware.js | 128 ++++++++++--- .../createRPCMethodTrackingMiddleware.test.js | 122 ++++++++++++- app/scripts/lib/security-provider-helpers.js | 12 +- app/scripts/metamask-controller.js | 9 +- shared/constants/metametrics.js | 4 + test/e2e/tests/eth-sign.spec.js | 2 +- 10 files changed, 424 insertions(+), 40 deletions(-) diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index f9266a540e4c..a949f3cc7152 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -721,6 +721,9 @@ export default class MetaMetricsController { ///: BEGIN:ONLY_INCLUDE_IN(flask) [TRAITS.DESKTOP_ENABLED]: metamaskState.desktopEnabled || false, ///: END:ONLY_INCLUDE_IN + [TRAITS.SECURITY_PROVIDERS]: metamaskState.transactionSecurityCheckEnabled + ? ['opensea'] + : [], }; if (!previousUserTraits) { diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 494d4f763f95..f3969a7b3843 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -952,6 +952,7 @@ describe('MetaMetricsController', function () { theme: 'default', useTokenDetection: true, desktopEnabled: false, + security_providers: [], }); assert.deepEqual(traits, { @@ -970,6 +971,7 @@ describe('MetaMetricsController', function () { [TRAITS.THEME]: 'default', [TRAITS.TOKEN_DETECTION_ENABLED]: true, [TRAITS.DESKTOP_ENABLED]: false, + [TRAITS.SECURITY_PROVIDERS]: [], }); }); diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 32f405617d76..112071ffd604 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -2147,6 +2147,7 @@ export default class TransactionController extends EventEmitter { originalApprovalAmount, finalApprovalAmount, contractMethodName, + securityProviderResponse, } = txMeta; const source = referrer === ORIGIN_METAMASK ? 'user' : 'dapp'; @@ -2298,6 +2299,16 @@ export default class TransactionController extends EventEmitter { } } + let uiCustomizations; + + if (securityProviderResponse?.flagAsDangerous === 1) { + uiCustomizations = ['flagged_as_malicious']; + } else if (securityProviderResponse?.flagAsDangerous === 2) { + uiCustomizations = ['flagged_as_safety_unknown']; + } else { + uiCustomizations = null; + } + let properties = { chain_id: chainId, referrer, @@ -2312,6 +2323,7 @@ export default class TransactionController extends EventEmitter { token_standard: tokenStandard, transaction_type: transactionType, transaction_speed_up: type === TransactionType.retry, + ui_customizations: uiCustomizations, }; if (transactionContractMethod === contractMethodNames.APPROVE) { diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index 7cf1f5a013da..096783faebde 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -1740,6 +1740,9 @@ describe('Transaction Controller', function () { gas: '0x7b0d', gasPrice: '0x77359400', }, + securityProviderResponse: { + flagAsDangerous: 0, + }, }; }); @@ -1766,6 +1769,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { default_gas: '0.000031501', @@ -1852,6 +1856,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { default_gas: '0.000031501', @@ -1921,6 +1926,9 @@ describe('Transaction Controller', function () { gas: '0x7b0d', gasPrice: '0x77359400', }, + securityProviderResponse: { + flagAsDangerous: 0, + }, }; }); @@ -1947,6 +1955,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { default_gas: '0.000031501', @@ -2035,6 +2044,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { default_gas: '0.000031501', @@ -2099,6 +2109,9 @@ describe('Transaction Controller', function () { chainId: currentChainId, time: 1624408066355, metamaskNetworkId: currentNetworkId, + securityProviderResponse: { + flagAsDangerous: 0, + }, }; const expectedPayload = { @@ -2122,6 +2135,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { gas_price: '2', @@ -2167,6 +2181,9 @@ describe('Transaction Controller', function () { chainId: currentChainId, time: 1624408066355, metamaskNetworkId: currentNetworkId, + securityProviderResponse: { + flagAsDangerous: 0, + }, }; const expectedPayload = { actionId, @@ -2190,6 +2207,155 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, + }, + sensitiveProperties: { + baz: 3.0, + foo: 'bar', + gas_price: '2', + gas_limit: '0x7b0d', + transaction_contract_method: undefined, + transaction_replaced: undefined, + first_seen: 1624408066355, + transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY, + status: 'unapproved', + }, + }; + + await txController._trackTransactionMetricsEvent( + txMeta, + TransactionMetaMetricsEvent.added, + actionId, + { + baz: 3.0, + foo: 'bar', + }, + ); + assert.equal(createEventFragmentSpy.callCount, 1); + assert.equal(finalizeEventFragmentSpy.callCount, 0); + assert.deepEqual( + createEventFragmentSpy.getCall(0).args[0], + expectedPayload, + ); + }); + + it('should call _trackMetaMetricsEvent with the correct payload (extra params) when flagAsDangerous is malicious', async function () { + const txMeta = { + id: 1, + status: TransactionStatus.unapproved, + txParams: { + from: fromAccount.address, + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + gasPrice: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + type: TransactionType.simpleSend, + origin: 'other', + chainId: currentChainId, + time: 1624408066355, + metamaskNetworkId: currentNetworkId, + securityProviderResponse: { + flagAsDangerous: 1, + }, + }; + const expectedPayload = { + actionId, + initialEvent: 'Transaction Added', + successEvent: 'Transaction Approved', + failureEvent: 'Transaction Rejected', + uniqueIdentifier: 'transaction-added-1', + persist: true, + category: EVENT.CATEGORIES.TRANSACTIONS, + properties: { + network: '5', + referrer: 'other', + source: EVENT.SOURCE.TRANSACTION.DAPP, + transaction_type: TransactionType.simpleSend, + chain_id: '0x5', + eip_1559_version: '0', + gas_edit_attempted: 'none', + gas_edit_type: 'none', + account_type: 'MetaMask', + asset_type: AssetType.native, + token_standard: TokenStandard.none, + device_model: 'N/A', + transaction_speed_up: false, + ui_customizations: ['flagged_as_malicious'], + }, + sensitiveProperties: { + baz: 3.0, + foo: 'bar', + gas_price: '2', + gas_limit: '0x7b0d', + transaction_contract_method: undefined, + transaction_replaced: undefined, + first_seen: 1624408066355, + transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY, + status: 'unapproved', + }, + }; + + await txController._trackTransactionMetricsEvent( + txMeta, + TransactionMetaMetricsEvent.added, + actionId, + { + baz: 3.0, + foo: 'bar', + }, + ); + assert.equal(createEventFragmentSpy.callCount, 1); + assert.equal(finalizeEventFragmentSpy.callCount, 0); + assert.deepEqual( + createEventFragmentSpy.getCall(0).args[0], + expectedPayload, + ); + }); + + it('should call _trackMetaMetricsEvent with the correct payload (extra params) when flagAsDangerous is unknown', async function () { + const txMeta = { + id: 1, + status: TransactionStatus.unapproved, + txParams: { + from: fromAccount.address, + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + gasPrice: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + type: TransactionType.simpleSend, + origin: 'other', + chainId: currentChainId, + time: 1624408066355, + metamaskNetworkId: currentNetworkId, + securityProviderResponse: { + flagAsDangerous: 2, + }, + }; + const expectedPayload = { + actionId, + initialEvent: 'Transaction Added', + successEvent: 'Transaction Approved', + failureEvent: 'Transaction Rejected', + uniqueIdentifier: 'transaction-added-1', + persist: true, + category: EVENT.CATEGORIES.TRANSACTIONS, + properties: { + network: '5', + referrer: 'other', + source: EVENT.SOURCE.TRANSACTION.DAPP, + transaction_type: TransactionType.simpleSend, + chain_id: '0x5', + eip_1559_version: '0', + gas_edit_attempted: 'none', + gas_edit_type: 'none', + account_type: 'MetaMask', + asset_type: AssetType.native, + token_standard: TokenStandard.none, + device_model: 'N/A', + transaction_speed_up: false, + ui_customizations: ['flagged_as_safety_unknown'], }, sensitiveProperties: { baz: 3.0, @@ -2245,6 +2411,9 @@ describe('Transaction Controller', function () { maxFeePerGas: '0x77359400', maxPriorityFeePerGas: '0x77359400', }, + securityProviderResponse: { + flagAsDangerous: 0, + }, }; const expectedPayload = { actionId, @@ -2268,6 +2437,7 @@ describe('Transaction Controller', function () { token_standard: TokenStandard.none, device_model: 'N/A', transaction_speed_up: false, + ui_customizations: null, }, sensitiveProperties: { baz: 3.0, diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index e1376ae1e785..8a5186d42148 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -107,14 +107,16 @@ const rateLimitTimeouts = {}; * MetaMetricsController * @param {number} [opts.rateLimitSeconds] - number of seconds to wait before * allowing another set of events to be tracked. + * @param opts.securityProviderRequest * @returns {Function} */ export default function createRPCMethodTrackingMiddleware({ trackEvent, getMetricsState, rateLimitSeconds = 60 * 5, + securityProviderRequest, }) { - return function rpcMethodTrackingMiddleware( + return async function rpcMethodTrackingMiddleware( /** @type {any} */ req, /** @type {any} */ res, /** @type {Function} */ next, @@ -162,20 +164,63 @@ export default function createRPCMethodTrackingMiddleware({ const properties = {}; + let msgParams; + if (event === EVENT_NAMES.SIGNATURE_REQUESTED) { properties.signature_type = method; - } else { - properties.method = method; - } - if (method === MESSAGE_TYPE.PERSONAL_SIGN) { const data = req?.params?.[0]; - const { isSIWEMessage } = detectSIWE({ data }); - if (isSIWEMessage) { - properties.ui_customizations = [ - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS].SIWE, - ]; + const from = req?.params?.[1]; + const paramsExamplePassword = req?.params?.[2]; + + msgParams = { + ...paramsExamplePassword, + from, + data, + origin, + }; + + const msgData = { + msgParams, + status: 'unapproved', + type: req.method, + }; + + try { + const securityProviderResponse = await securityProviderRequest( + msgData, + req.method, + ); + + if (securityProviderResponse?.flagAsDangerous === 1) { + properties.ui_customizations = ['flagged_as_malicious']; + } else if (securityProviderResponse?.flagAsDangerous === 2) { + properties.ui_customizations = ['flagged_as_safety_unknown']; + } else { + properties.ui_customizations = null; + } + + if (method === MESSAGE_TYPE.PERSONAL_SIGN) { + const { isSIWEMessage } = detectSIWE({ data }); + if (isSIWEMessage) { + properties.ui_customizations === null + ? (properties.ui_customizations = [ + METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] + .SIWE, + ]) + : properties.ui_customizations.push( + METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] + .SIWE, + ); + } + } + } catch (e) { + console.warn( + `createRPCMethodTrackingMiddleware: Error calling securityProviderRequest - ${e}`, + ); } + } else { + properties.method = method; } trackEvent({ @@ -192,7 +237,7 @@ export default function createRPCMethodTrackingMiddleware({ }, SECOND * rateLimitSeconds); } - next((callback) => { + next(async (callback) => { if (shouldTrackEvent === false || typeof eventType === 'undefined') { return callback(); } @@ -216,20 +261,63 @@ export default function createRPCMethodTrackingMiddleware({ event = eventType.APPROVED; } + let msgParams; + if (eventType.REQUESTED === EVENT_NAMES.SIGNATURE_REQUESTED) { properties.signature_type = method; - } else { - properties.method = method; - } - if (method === MESSAGE_TYPE.PERSONAL_SIGN) { const data = req?.params?.[0]; - const { isSIWEMessage } = detectSIWE({ data }); - if (isSIWEMessage) { - properties.ui_customizations = [ - METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS].SIWE, - ]; + const from = req?.params?.[1]; + const paramsExamplePassword = req?.params?.[2]; + + msgParams = { + ...paramsExamplePassword, + from, + data, + origin, + }; + + const msgData = { + msgParams, + status: 'unapproved', + type: req.method, + }; + + try { + const securityProviderResponse = await securityProviderRequest( + msgData, + req.method, + ); + + if (securityProviderResponse?.flagAsDangerous === 1) { + properties.ui_customizations = ['flagged_as_malicious']; + } else if (securityProviderResponse?.flagAsDangerous === 2) { + properties.ui_customizations = ['flagged_as_safety_unknown']; + } else { + properties.ui_customizations = null; + } + + if (method === MESSAGE_TYPE.PERSONAL_SIGN) { + const { isSIWEMessage } = detectSIWE({ data }); + if (isSIWEMessage) { + properties.ui_customizations === null + ? (properties.ui_customizations = [ + METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] + .SIWE, + ]) + : properties.ui_customizations.push( + METAMETRIC_KEY_OPTIONS[METAMETRIC_KEY.UI_CUSTOMIZATIONS] + .SIWE, + ); + } + } + } catch (e) { + console.warn( + `createRPCMethodTrackingMiddleware: Error calling securityProviderRequest - ${e}`, + ); } + } else { + properties.method = method; } trackEvent({ diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index 9a9f72157703..af910279e859 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -8,10 +8,19 @@ const trackEvent = jest.fn(); const metricsState = { participateInMetaMetrics: null }; const getMetricsState = () => metricsState; +let flagAsDangerous = 0; + +const securityProviderRequest = () => { + return { + flagAsDangerous, + }; +}; + const handler = createRPCMethodTrackingMiddleware({ trackEvent, getMetricsState, rateLimitSeconds: 1, + securityProviderRequest, }); function getNext(timeout = 500) { @@ -92,7 +101,7 @@ describe('createRPCMethodTrackingMiddleware', () => { metricsState.participateInMetaMetrics = true; }); - it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event`, () => { + it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event`, async () => { const req = { method: MESSAGE_TYPE.ETH_SIGN, origin: 'some.dapp', @@ -102,12 +111,14 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { next } = getNext(); - handler(req, res, next); + await handler(req, res, next); expect(trackEvent).toHaveBeenCalledTimes(1); expect(trackEvent.mock.calls[0][0]).toMatchObject({ category: 'inpage_provider', event: EVENT_NAMES.SIGNATURE_REQUESTED, - properties: { signature_type: MESSAGE_TYPE.ETH_SIGN }, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + }, referrer: { url: 'some.dapp' }, }); }); @@ -122,13 +133,15 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { next, executeMiddlewareStack } = getNext(); - handler(req, res, next); + await handler(req, res, next); await executeMiddlewareStack(); expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ category: 'inpage_provider', event: EVENT_NAMES.SIGNATURE_APPROVED, - properties: { signature_type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4 }, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, + }, referrer: { url: 'some.dapp' }, }); }); @@ -143,13 +156,15 @@ describe('createRPCMethodTrackingMiddleware', () => { error: { code: 4001 }, }; const { next, executeMiddlewareStack } = getNext(); - handler(req, res, next); + await handler(req, res, next); await executeMiddlewareStack(); expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ category: 'inpage_provider', event: EVENT_NAMES.SIGNATURE_REJECTED, - properties: { signature_type: MESSAGE_TYPE.PERSONAL_SIGN }, + properties: { + signature_type: MESSAGE_TYPE.PERSONAL_SIGN, + }, referrer: { url: 'some.dapp' }, }); }); @@ -162,7 +177,7 @@ describe('createRPCMethodTrackingMiddleware', () => { const res = {}; const { next, executeMiddlewareStack } = getNext(); - handler(req, res, next); + await handler(req, res, next); await executeMiddlewareStack(); expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ @@ -227,7 +242,7 @@ describe('createRPCMethodTrackingMiddleware', () => { }; const { next, executeMiddlewareStack } = getNext(); - handler(req, res, next); + await handler(req, res, next); await executeMiddlewareStack(); expect(trackEvent).toHaveBeenCalledTimes(2); @@ -244,4 +259,93 @@ describe('createRPCMethodTrackingMiddleware', () => { }); }); }); + + describe('participateInMetaMetrics is set to true with a request flagged as safe', () => { + beforeEach(() => { + metricsState.participateInMetaMetrics = true; + }); + + it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safe`, async () => { + const req = { + method: MESSAGE_TYPE.ETH_SIGN, + origin: 'some.dapp', + }; + + const res = { + error: null, + }; + const { next } = getNext(); + await handler(req, res, next); + expect(trackEvent).toHaveBeenCalledTimes(1); + expect(trackEvent.mock.calls[0][0]).toMatchObject({ + category: 'inpage_provider', + event: EVENT_NAMES.SIGNATURE_REQUESTED, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + ui_customizations: null, + }, + referrer: { url: 'some.dapp' }, + }); + }); + }); + + describe('participateInMetaMetrics is set to true with a request flagged as malicious', () => { + beforeEach(() => { + metricsState.participateInMetaMetrics = true; + flagAsDangerous = 1; + }); + + it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as malicious`, async () => { + const req = { + method: MESSAGE_TYPE.ETH_SIGN, + origin: 'some.dapp', + }; + + const res = { + error: null, + }; + const { next } = getNext(); + await handler(req, res, next); + expect(trackEvent).toHaveBeenCalledTimes(1); + expect(trackEvent.mock.calls[0][0]).toMatchObject({ + category: 'inpage_provider', + event: EVENT_NAMES.SIGNATURE_REQUESTED, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + ui_customizations: ['flagged_as_malicious'], + }, + referrer: { url: 'some.dapp' }, + }); + }); + }); + + describe('participateInMetaMetrics is set to true with a request flagged as safety unknown', () => { + beforeEach(() => { + metricsState.participateInMetaMetrics = true; + flagAsDangerous = 2; + }); + + it(`should immediately track a ${EVENT_NAMES.SIGNATURE_REQUESTED} event which is flagged as safety unknown`, async () => { + const req = { + method: MESSAGE_TYPE.ETH_SIGN, + origin: 'some.dapp', + }; + + const res = { + error: null, + }; + const { next } = getNext(); + await handler(req, res, next); + expect(trackEvent).toHaveBeenCalledTimes(1); + expect(trackEvent.mock.calls[0][0]).toMatchObject({ + category: 'inpage_provider', + event: EVENT_NAMES.SIGNATURE_REQUESTED, + properties: { + signature_type: MESSAGE_TYPE.ETH_SIGN, + ui_customizations: ['flagged_as_safety_unknown'], + }, + referrer: { url: 'some.dapp' }, + }); + }); + }); }); diff --git a/app/scripts/lib/security-provider-helpers.js b/app/scripts/lib/security-provider-helpers.js index db66c462d213..b05c9a40bb8f 100644 --- a/app/scripts/lib/security-provider-helpers.js +++ b/app/scripts/lib/security-provider-helpers.js @@ -39,12 +39,12 @@ export async function securityProviderCheck( rpc_method_name: methodName, chain_id: chainId, data: { - from_address: requestData.txParams.from, - to_address: requestData.txParams.to, - gas: requestData.txParams.gas, - gasPrice: requestData.txParams.gasPrice, - value: requestData.txParams.value, - data: requestData.txParams.data, + from_address: requestData?.txParams?.from, + to_address: requestData?.txParams?.to, + gas: requestData?.txParams?.gas, + gasPrice: requestData?.txParams?.gasPrice, + value: requestData?.txParams?.value, + data: requestData?.txParams?.data, }, currentLocale, }; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 1e9698e7827b..b4c2ef4ba55e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3729,6 +3729,7 @@ export default class MetamaskController extends EventEmitter { getMetricsState: this.metaMetricsController.store.getState.bind( this.metaMetricsController.store, ), + securityProviderRequest: this.securityProviderRequest.bind(this), }), ); @@ -4383,11 +4384,11 @@ export default class MetamaskController extends EventEmitter { const { currentLocale, transactionSecurityCheckEnabled } = this.preferencesController.store.getState(); - const chainId = Number( - hexToDecimal(this.networkController.store.getState().provider.chainId), - ); - if (transactionSecurityCheckEnabled) { + const chainId = Number( + hexToDecimal(this.networkController.store.getState().provider.chainId), + ); + try { const securityProviderResponse = await securityProviderCheck( requestData, diff --git a/shared/constants/metametrics.js b/shared/constants/metametrics.js index 853807ce5ab9..a9691d5a29a2 100644 --- a/shared/constants/metametrics.js +++ b/shared/constants/metametrics.js @@ -187,6 +187,8 @@ * identify the token_detection_enabled trait * @property {'install_date_ext'} INSTALL_DATE_EXT - when the user installed the extension * @property {'desktop_enabled'} [DESKTOP_ENABLED] - optional / does the user have desktop enabled? + * @property {'security_providers'} SECURITY_PROVIDERS - when security provider feature is toggled we + * identify the security_providers trait */ /** @@ -210,6 +212,7 @@ export const TRAITS = { THREE_BOX_ENABLED: 'three_box_enabled', TOKEN_DETECTION_ENABLED: 'token_detection_enabled', DESKTOP_ENABLED: 'desktop_enabled', + SECURITY_PROVIDERS: 'security_providers', }; /** @@ -240,6 +243,7 @@ export const TRAITS = { * @property {string} [theme] - which theme the user has selected * @property {boolean} [token_detection_enabled] - does the user have token detection is enabled? * @property {boolean} [desktop_enabled] - optional / does the user have desktop enabled? + * @property {Array} [security_providers] - whether security provider feature toggle is on or off */ // Mixpanel converts the zero address value to a truly anonymous event, which diff --git a/test/e2e/tests/eth-sign.spec.js b/test/e2e/tests/eth-sign.spec.js index 51ef2168c217..5b0feb3b3618 100644 --- a/test/e2e/tests/eth-sign.spec.js +++ b/test/e2e/tests/eth-sign.spec.js @@ -31,10 +31,10 @@ describe('Eth sign', function () { await driver.openNewPage('http://127.0.0.1:8080/'); await driver.clickElement('#ethSign'); + await driver.delay(1000); const ethSignButton = await driver.findElement('#ethSign'); const exceptionString = 'ERROR: ETH_SIGN HAS BEEN DISABLED. YOU MUST ENABLE IT IN THE ADVANCED SETTINGS'; - assert.equal(await ethSignButton.getText(), exceptionString); }, ); From 2fc0d9378902fe7c3abf7393a443b82cff067f84 Mon Sep 17 00:00:00 2001 From: micaelae <100321200+micaelae@users.noreply.github.com> Date: Thu, 23 Mar 2023 10:24:10 -0700 Subject: [PATCH 08/12] Disable Bridge button on unsupported networks (#18268) --- shared/constants/bridge.ts | 10 ++++ .../app/wallet-overview/eth-overview.js | 43 +++++++++++------ .../app/wallet-overview/eth-overview.test.js | 46 +++++++++++++++++-- ui/components/ui/icon-button/icon-button.scss | 1 + ui/selectors/selectors.js | 7 +++ ui/selectors/selectors.test.js | 10 ++++ 6 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 shared/constants/bridge.ts diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts new file mode 100644 index 000000000000..a70c0a62a55a --- /dev/null +++ b/shared/constants/bridge.ts @@ -0,0 +1,10 @@ +import { CHAIN_IDS } from './network'; + +export const ALLOWED_BRIDGE_CHAIN_IDS = [ + CHAIN_IDS.MAINNET, + CHAIN_IDS.BSC, + CHAIN_IDS.POLYGON, + CHAIN_IDS.AVALANCHE, + CHAIN_IDS.OPTIMISM, + CHAIN_IDS.ARBITRUM, +]; diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index a02c8459d295..12e69b59be63 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -19,6 +19,7 @@ import { getCurrentKeyring, getSwapsDefaultToken, getIsSwapsChain, + getIsBridgeChain, getIsBuyableChain, getNativeCurrencyImage, getSelectedAccountCachedBalance, @@ -56,6 +57,7 @@ const EthOverview = ({ className }) => { const showFiat = useSelector(getShouldShowFiat); const balance = useSelector(getSelectedAccountCachedBalance); const isSwapsChain = useSelector(getIsSwapsChain); + const isBridgeChain = useSelector(getIsBridgeChain); const isBuyableChain = useSelector(getIsBuyableChain); const primaryTokenImage = useSelector(getNativeCurrencyImage); const defaultSwapsToken = useSelector(getSwapsDefaultToken); @@ -232,26 +234,41 @@ const EthOverview = ({ className }) => { /> } label={t('bridge')} onClick={() => { - const portfolioUrl = process.env.PORTFOLIO_URL; - const bridgeUrl = `${portfolioUrl}/bridge`; - global.platform.openTab({ - url: `${bridgeUrl}?metamaskEntry=ext`, - }); - trackEvent({ - category: EVENT.CATEGORIES.NAVIGATION, - event: EVENT_NAMES.BRIDGE_LINK_CLICKED, - properties: { - location: 'Home', - text: 'Bridge', - }, - }); + if (isBridgeChain) { + const portfolioUrl = process.env.PORTFOLIO_URL; + const bridgeUrl = `${portfolioUrl}/bridge`; + global.platform.openTab({ + url: `${bridgeUrl}?metamaskEntry=ext`, + }); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.BRIDGE_LINK_CLICKED, + properties: { + location: 'Home', + text: 'Bridge', + }, + }); + } }} + tooltipRender={ + isBridgeChain + ? null + : (contents) => ( + + {contents} + + ) + } /> } diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index c7337719940c..3d4f459c18f2 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -145,13 +145,30 @@ describe('EthOverview', () => { expect(secondaryBalance).toHaveTextContent('0'); }); - it('should always show the Bridge button', () => { - const { queryByTestId } = renderWithProvider(, store); + it('should have the Bridge button enabled if chain id is part of supported chains', () => { + const mockedAvalancheStore = { + ...mockStore, + metamask: { + ...mockStore.metamask, + provider: { ...mockStore.metamask.provider, chainId: '0xa86a' }, + }, + }; + const mockedStore = configureMockStore([thunk])(mockedAvalancheStore); + + const { queryByTestId, queryByText } = renderWithProvider( + , + mockedStore, + ); const bridgeButton = queryByTestId(ETH_OVERVIEW_BRIDGE); expect(bridgeButton).toBeInTheDocument(); + expect(bridgeButton).toBeEnabled(); + expect(queryByText('Bridge').parentElement).not.toHaveAttribute( + 'data-original-title', + 'Unavailable on this network', + ); }); - it('should open the Bridge URI when clicking on Bridge button', async () => { + it('should open the Bridge URI when clicking on Bridge button on supported network', async () => { const { queryByTestId } = renderWithProvider(, store); const bridgeButton = queryByTestId(ETH_OVERVIEW_BRIDGE); @@ -169,6 +186,29 @@ describe('EthOverview', () => { ); }); + it('should have the Bridge button disabled if chain id is not part of supported chains', () => { + const mockedFantomStore = { + ...mockStore, + metamask: { + ...mockStore.metamask, + provider: { ...mockStore.metamask.provider, chainId: '0xfa' }, + }, + }; + const mockedStore = configureMockStore([thunk])(mockedFantomStore); + + const { queryByTestId, queryByText } = renderWithProvider( + , + mockedStore, + ); + const bridgeButton = queryByTestId(ETH_OVERVIEW_BRIDGE); + expect(bridgeButton).toBeInTheDocument(); + expect(bridgeButton).toBeDisabled(); + expect(queryByText('Bridge').parentElement).toHaveAttribute( + 'data-original-title', + 'Unavailable on this network', + ); + }); + it('should always show the Portfolio button', () => { const { queryByTestId } = renderWithProvider(, store); const portfolioButton = queryByTestId(ETH_OVERVIEW_PORTFOLIO); diff --git a/ui/components/ui/icon-button/icon-button.scss b/ui/components/ui/icon-button/icon-button.scss index 4ce70027cc44..8769d532c22b 100644 --- a/ui/components/ui/icon-button/icon-button.scss +++ b/ui/components/ui/icon-button/icon-button.scss @@ -22,6 +22,7 @@ border-radius: 18px; margin-top: 6px; margin-bottom: 5px; + margin-inline: auto; } &--disabled { diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index dea043e15076..42510747566a 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -41,6 +41,8 @@ import { ALLOWED_DEV_SWAPS_CHAIN_IDS, } from '../../shared/constants/swaps'; +import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../shared/constants/bridge'; + import { shortenAddress, getAccountByAddress, @@ -750,6 +752,11 @@ export function getIsSwapsChain(state) { : ALLOWED_DEV_SWAPS_CHAIN_IDS.includes(chainId); } +export function getIsBridgeChain(state) { + const chainId = getCurrentChainId(state); + return ALLOWED_BRIDGE_CHAIN_IDS.includes(chainId); +} + export function getIsBuyableChain(state) { const chainId = getCurrentChainId(state); return Object.keys(BUYABLE_CHAINS_MAP).includes(chainId); diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index f3593f93739a..f1f7bfe5e61f 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -390,4 +390,14 @@ describe('Selectors', () => { const isDesktopEnabled = selectors.getIsDesktopEnabled(mockState); expect(isDesktopEnabled).toBeFalsy(); }); + + it('#getIsBridgeChain', () => { + mockState.metamask.provider.chainId = '0xa'; + const isOptimismSupported = selectors.getIsBridgeChain(mockState); + expect(isOptimismSupported).toBeTruthy(); + + mockState.metamask.provider.chainId = '0xfa'; + const isFantomSupported = selectors.getIsBridgeChain(mockState); + expect(isFantomSupported).toBeFalsy(); + }); }); From 5dee7904d6c2efd7763afda844b1fca5072c6216 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 23 Mar 2023 22:21:33 +0400 Subject: [PATCH 09/12] Extracting out title component from confirm-transaction-base (#17991) --- test/e2e/metamask-ui.spec.js | 18 ++--- ...onfirm-page-container-content.component.js | 9 --- ...onfirm-page-container-summary.component.js | 29 +------ .../confirm-page-container-summary/index.scss | 25 ------ .../confirm-page-container.component.js | 9 --- ui/components/app/confirm-subtitle/README.mdx | 10 +++ .../app/confirm-subtitle/confirm-subtitle.js | 56 +++++++++++++ .../confirm-subtitle.stories.js | 51 ++++++++++++ .../confirm-subtitle/confirm-subtitle.test.js | 80 +++++++++++++++++++ ui/components/app/confirm-subtitle/index.js | 1 + ui/components/app/confirm-title/README.mdx | 10 +++ .../app/confirm-title/confirm-title.js | 68 ++++++++++++++++ .../confirm-title/confirm-title.stories.js | 54 +++++++++++++ .../app/confirm-title/confirm-title.test.js | 54 +++++++++++++ ui/components/app/confirm-title/index.js | 1 + ui/hooks/useTransactionInfo.js | 19 +++++ ui/hooks/useTransactionInfo.test.js | 36 +++++++++ .../confirm-transaction-base.test.js.snap | 2 +- .../confirm-transaction-base.component.js | 41 +++------- .../confirm-transaction-base.container.js | 10 --- 20 files changed, 463 insertions(+), 120 deletions(-) create mode 100644 ui/components/app/confirm-subtitle/README.mdx create mode 100644 ui/components/app/confirm-subtitle/confirm-subtitle.js create mode 100644 ui/components/app/confirm-subtitle/confirm-subtitle.stories.js create mode 100644 ui/components/app/confirm-subtitle/confirm-subtitle.test.js create mode 100644 ui/components/app/confirm-subtitle/index.js create mode 100644 ui/components/app/confirm-title/README.mdx create mode 100644 ui/components/app/confirm-title/confirm-title.js create mode 100644 ui/components/app/confirm-title/confirm-title.stories.js create mode 100644 ui/components/app/confirm-title/confirm-title.test.js create mode 100644 ui/components/app/confirm-title/index.js create mode 100644 ui/hooks/useTransactionInfo.js create mode 100644 ui/hooks/useTransactionInfo.test.js diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index c1fa2a4a1de4..6f3754bd8879 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -313,11 +313,10 @@ describe('MetaMask', function () { text: 'Transfer', }); - const tokenAmount = await driver.findElement( - '.confirm-page-container-summary__title-text', - ); - const tokenAmountText = await tokenAmount.getText(); - assert.equal(tokenAmountText, '1 TST'); + await driver.findElement({ + tag: 'h1', + text: '1 TST', + }); await driver.waitForSelector({ tag: 'p', @@ -419,11 +418,10 @@ describe('MetaMask', function () { }); it('submits the transaction', async function () { - const tokenAmount = await driver.findElement( - '.confirm-page-container-summary__title-text', - ); - const tokenAmountText = await tokenAmount.getText(); - assert.equal(tokenAmountText, '1.5 TST'); + await driver.findElement({ + tag: 'h1', + text: '1.5 TST', + }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.delay(regularDelayMs); diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index 9ac822f5930d..c10bb8c06b55 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -29,11 +29,9 @@ export default class ConfirmPageContainerContent extends Component { ///: END:ONLY_INCLUDE_IN errorKey: PropTypes.string, errorMessage: PropTypes.string, - hideSubtitle: PropTypes.bool, tokenAddress: PropTypes.string, nonce: PropTypes.string, subtitleComponent: PropTypes.node, - title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), image: PropTypes.string, titleComponent: PropTypes.node, warning: PropTypes.string, @@ -48,7 +46,6 @@ export default class ConfirmPageContainerContent extends Component { disabled: PropTypes.bool, unapprovedTxCount: PropTypes.number, rejectNText: PropTypes.string, - hideTitle: PropTypes.bool, supportsEIP1559: PropTypes.bool, hasTopBorder: PropTypes.bool, nativeCurrency: PropTypes.string, @@ -136,11 +133,9 @@ export default class ConfirmPageContainerContent extends Component { action, errorKey, errorMessage, - title, image, titleComponent, subtitleComponent, - hideSubtitle, tokenAddress, nonce, detailsComponent, @@ -156,7 +151,6 @@ export default class ConfirmPageContainerContent extends Component { rejectNText, origin, ethGasPriceWarning, - hideTitle, supportsEIP1559, hasTopBorder, nativeCurrency, @@ -199,15 +193,12 @@ export default class ConfirmPageContainerContent extends Component { !detailsComponent || !dataComponent, })} action={action} - title={title} image={image} titleComponent={titleComponent} subtitleComponent={subtitleComponent} - hideSubtitle={hideSubtitle} tokenAddress={tokenAddress} nonce={nonce} origin={origin} - hideTitle={hideTitle} toAddress={toAddress} transactionType={transactionType} /> diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index 90f3dc162572..46a3f068f42e 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -14,8 +14,6 @@ import { getIpfsGateway } from '../../../../../selectors'; import Identicon from '../../../../ui/identicon'; import InfoTooltip from '../../../../ui/info-tooltip'; import NicknamePopovers from '../../../modals/nickname-popovers'; -import { Text } from '../../../../component-library'; -import { TextVariant } from '../../../../../helpers/constants/design-system'; import { ORIGIN_METAMASK } from '../../../../../../shared/constants/app'; import SiteOrigin from '../../../../ui/site-origin'; import { getAssetImageURL } from '../../../../../helpers/utils/util'; @@ -23,16 +21,13 @@ import { getAssetImageURL } from '../../../../../helpers/utils/util'; const ConfirmPageContainerSummary = (props) => { const { action, - title, titleComponent, subtitleComponent, - hideSubtitle, className, tokenAddress, toAddress, nonce, origin, - hideTitle, image, transactionType, } = props; @@ -130,26 +125,9 @@ const ConfirmPageContainerSummary = (props) => { <>
{renderImage()} - {!hideTitle ? ( - - {titleComponent || title} - - ) : null} + {titleComponent}
- {hideSubtitle ? null : ( -
- {subtitleComponent} -
- )} + {subtitleComponent} {showNicknamePopovers && ( { ConfirmPageContainerSummary.propTypes = { action: PropTypes.string, - title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), image: PropTypes.string, titleComponent: PropTypes.node, subtitleComponent: PropTypes.node, - hideSubtitle: PropTypes.bool, className: PropTypes.string, tokenAddress: PropTypes.string, toAddress: PropTypes.string, nonce: PropTypes.string, origin: PropTypes.string.isRequired, - hideTitle: PropTypes.bool, transactionType: PropTypes.string, }; diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss index 05e7132e416a..9514cbef278f 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss @@ -69,31 +69,6 @@ margin-right: 8px; } - &__title-text { - @include H1; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__title-text-long { - @include H3; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__subtitle { - @include H5; - - color: var(--color-text-alternative); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - &--border { border-bottom: 1px solid var(--color-border-muted); } diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index 4c31366c75cd..1943c964b145 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -66,7 +66,6 @@ const ConfirmPageContainer = (props) => { image, titleComponent, subtitleComponent, - hideSubtitle, detailsComponent, dataComponent, dataHexComponent, @@ -123,11 +122,6 @@ const ConfirmPageContainer = (props) => { const shouldDisplayWarning = contentComponent && disabled && (errorKey || errorMessage); - const hideTitle = - (currentTransaction.type === TransactionType.contractInteraction || - currentTransaction.type === TransactionType.deployContract) && - currentTransaction.txParams?.value === '0x0'; - const networkName = NETWORK_TO_NAME_MAP[currentTransaction.chainId] || networkIdentifier; @@ -203,7 +197,6 @@ const ConfirmPageContainer = (props) => { image={image} titleComponent={titleComponent} subtitleComponent={subtitleComponent} - hideSubtitle={hideSubtitle} detailsComponent={detailsComponent} dataComponent={dataComponent} dataHexComponent={dataHexComponent} @@ -225,7 +218,6 @@ const ConfirmPageContainer = (props) => { rejectNText={t('rejectTxsN', [unapprovedTxCount])} origin={origin} ethGasPriceWarning={ethGasPriceWarning} - hideTitle={hideTitle} supportsEIP1559={supportsEIP1559} currentTransaction={currentTransaction} nativeCurrency={nativeCurrency} @@ -341,7 +333,6 @@ const ConfirmPageContainer = (props) => { ConfirmPageContainer.propTypes = { // Header action: PropTypes.string, - hideSubtitle: PropTypes.bool, onEdit: PropTypes.func, showEdit: PropTypes.bool, subtitleComponent: PropTypes.node, diff --git a/ui/components/app/confirm-subtitle/README.mdx b/ui/components/app/confirm-subtitle/README.mdx new file mode 100644 index 000000000000..8c5da7bec89d --- /dev/null +++ b/ui/components/app/confirm-subtitle/README.mdx @@ -0,0 +1,10 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; +import { ConfirmSubTitle } from '.'; + +# Confirm Sub Title + +Confirm Sub Title is used on confirmation screen to display transaction amoutn in header. + + + + diff --git a/ui/components/app/confirm-subtitle/confirm-subtitle.js b/ui/components/app/confirm-subtitle/confirm-subtitle.js new file mode 100644 index 000000000000..fd4366d870ae --- /dev/null +++ b/ui/components/app/confirm-subtitle/confirm-subtitle.js @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; + +import { SECONDARY } from '../../../helpers/constants/common'; +import { Text } from '../../component-library'; +import { + Color, + FONT_WEIGHT, + TextVariant, +} from '../../../helpers/constants/design-system'; +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'; +import { getShouldShowFiat } from '../../../selectors'; +import { useTransactionInfo } from '../../../hooks/useTransactionInfo'; + +const ConfirmSubTitle = ({ + txData, + hexTransactionAmount, + subtitleComponent, +}) => { + const shouldShowFiat = useSelector(getShouldShowFiat); + const { isNftTransfer } = useTransactionInfo(txData); + + if (!shouldShowFiat && !isNftTransfer) { + return null; + } + + if (subtitleComponent) { + return subtitleComponent; + } + + return ( + + + + ); +}; + +ConfirmSubTitle.propTypes = { + hexTransactionAmount: PropTypes.string, + subtitleComponent: PropTypes.element, + txData: PropTypes.object.isRequired, +}; + +export default ConfirmSubTitle; diff --git a/ui/components/app/confirm-subtitle/confirm-subtitle.stories.js b/ui/components/app/confirm-subtitle/confirm-subtitle.stories.js new file mode 100644 index 000000000000..9a05e509cde5 --- /dev/null +++ b/ui/components/app/confirm-subtitle/confirm-subtitle.stories.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { Provider } from 'react-redux'; + +import mockState from '../../../../test/data/mock-state.json'; +import configureStore from '../../../store/store'; + +import README from './README.mdx'; +import ConfirmSubTitle from './confirm-subtitle'; + +mockState.metamask.preferences.showFiatInTestnets = true; +const store = configureStore(mockState); + +export default { + title: 'Components/App/ConfirmSubTitle', + + component: ConfirmSubTitle, + decorators: [(story) => {story()}], + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + txData: 'object', + hexTransactionAmount: 'number', + title: 'string', + }, + args: { + txData: { + txParams: {}, + type: 'transfer', + }, + hexTransactionAmount: '0x9184e72a000', + subtitleComponent: undefined, + }, +}; + +export const DefaultStory = (args) => { + return ; +}; + +DefaultStory.storyName = 'Default'; + +export const CustomSubTitleStory = (args) => { + return ; +}; + +CustomSubTitleStory.storyName = 'CustomSubTitle'; +CustomSubTitleStory.args = { + subtitleComponent: 'Any custom sub title passed', +}; diff --git a/ui/components/app/confirm-subtitle/confirm-subtitle.test.js b/ui/components/app/confirm-subtitle/confirm-subtitle.test.js new file mode 100644 index 000000000000..ac2662a396a3 --- /dev/null +++ b/ui/components/app/confirm-subtitle/confirm-subtitle.test.js @@ -0,0 +1,80 @@ +import React from 'react'; + +import mockState from '../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import configureStore from '../../../store/store'; +import ConfirmSubTitle from './confirm-subtitle'; + +describe('ConfirmSubTitle', () => { + let store; + beforeEach(() => { + mockState.metamask.preferences.showFiatInTestnets = true; + store = configureStore(mockState); + }); + + it('should render subtitle correctly', async () => { + const { findByText } = renderWithProvider( + , + store, + ); + expect(await findByText('$0.01')).toBeInTheDocument(); + }); + + it('should return null if showFiatInTestnets preference if false', () => { + mockState.metamask.preferences.showFiatInTestnets = false; + store = configureStore(mockState); + + const { container } = renderWithProvider( + , + store, + ); + expect(container.firstChild).toStrictEqual(null); + }); + + it('should not null if showFiatInTestnets preference if false but it is NFT Transfer', async () => { + mockState.metamask.preferences.showFiatInTestnets = false; + mockState.metamask.allNftContracts = { + [mockState.metamask.selectedAddress]: { + [mockState.metamask.provider.chainId]: [{ address: '0x9' }], + }, + }; + store = configureStore(mockState); + + const { findByText } = renderWithProvider( + , + store, + ); + expect(await findByText('0.00001')).toBeInTheDocument(); + }); + + it('should render subtitleComponent if passed', () => { + const { getByText } = renderWithProvider( + dummy_sub_title_passed
} + />, + store, + ); + expect(getByText('dummy_sub_title_passed')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/confirm-subtitle/index.js b/ui/components/app/confirm-subtitle/index.js new file mode 100644 index 000000000000..ecb69693168d --- /dev/null +++ b/ui/components/app/confirm-subtitle/index.js @@ -0,0 +1 @@ +export { default as ConfirmSubTitle } from './confirm-subtitle'; diff --git a/ui/components/app/confirm-title/README.mdx b/ui/components/app/confirm-title/README.mdx new file mode 100644 index 000000000000..d68f7d30cea3 --- /dev/null +++ b/ui/components/app/confirm-title/README.mdx @@ -0,0 +1,10 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; +import { ConfirmTitle } from '.'; + +# Confirm Title + +Confirm Title is used on the confirmation screen to display the transaction amount in the header. + + + + diff --git a/ui/components/app/confirm-title/confirm-title.js b/ui/components/app/confirm-title/confirm-title.js new file mode 100644 index 000000000000..125d8d8a347f --- /dev/null +++ b/ui/components/app/confirm-title/confirm-title.js @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { TransactionType } from '../../../../shared/constants/transaction'; +import { PRIMARY } from '../../../helpers/constants/common'; +import { Text } from '../../component-library'; +import { + FONT_WEIGHT, + TextVariant, +} from '../../../helpers/constants/design-system'; +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'; + +const ConfirmTitle = ({ title, hexTransactionAmount, txData }) => { + const isContractInteraction = + txData.type === TransactionType.contractInteraction; + + const hideTitle = + (isContractInteraction || txData.type === TransactionType.deployContract) && + txData.txParams?.value === '0x0'; + + if (hideTitle) { + return null; + } + + if (title) { + return ( + + {title} + + ); + } + + return ( + + + + ); +}; + +ConfirmTitle.propTypes = { + txData: PropTypes.object.isRequired, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + hexTransactionAmount: PropTypes.string, +}; + +export default ConfirmTitle; diff --git a/ui/components/app/confirm-title/confirm-title.stories.js b/ui/components/app/confirm-title/confirm-title.stories.js new file mode 100644 index 000000000000..60037daa1db3 --- /dev/null +++ b/ui/components/app/confirm-title/confirm-title.stories.js @@ -0,0 +1,54 @@ +import React from 'react'; +import README from './README.mdx'; +import ConfirmTitle from './confirm-title'; + +export default { + title: 'Components/App/ConfirmTitle', + + component: ConfirmTitle, + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + txData: 'object', + hexTransactionAmount: 'string', + title: 'string', + }, + args: { + txData: { + txParams: {}, + type: 'transfer', + }, + hexTransactionAmount: '0x9184e72a000', + title: undefined, + }, +}; + +export const DefaultStory = (args) => { + return ; +}; + +DefaultStory.storyName = 'Default'; + +export const ContractInteractionStory = (args) => { + return ; +}; + +ContractInteractionStory.storyName = 'ContractInteraction'; +ContractInteractionStory.args = { + txData: { + txParams: {}, + type: 'contractInteraction', + }, +}; + +export const CustomTitleStory = (args) => { + return ; +}; + +CustomTitleStory.storyName = 'CustomTitle'; +CustomTitleStory.args = { + title: 'Any custom title passed', +}; diff --git a/ui/components/app/confirm-title/confirm-title.test.js b/ui/components/app/confirm-title/confirm-title.test.js new file mode 100644 index 000000000000..e6098dd04791 --- /dev/null +++ b/ui/components/app/confirm-title/confirm-title.test.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { TransactionType } from '../../../../shared/constants/transaction'; + +import mockState from '../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; + +import configureStore from '../../../store/store'; +import ConfirmTitle from './confirm-title'; + +describe('ConfirmTitle', () => { + const store = configureStore(mockState); + + it('should render title correctly', async () => { + const { findByText } = renderWithProvider( + , + store, + ); + expect(await findByText('0.00001')).toBeInTheDocument(); + }); + + it('should return null if transaction is contract interation with 0 value', () => { + const { container } = renderWithProvider( + , + store, + ); + expect(container.firstChild).toStrictEqual(null); + }); + + it('should render title if passed', () => { + const { getByText } = renderWithProvider( + , + store, + ); + expect(getByText('dummy_title_passed')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/confirm-title/index.js b/ui/components/app/confirm-title/index.js new file mode 100644 index 000000000000..11db0d32c5c1 --- /dev/null +++ b/ui/components/app/confirm-title/index.js @@ -0,0 +1 @@ +export { default as ConfirmTitle } from './confirm-title'; diff --git a/ui/hooks/useTransactionInfo.js b/ui/hooks/useTransactionInfo.js new file mode 100644 index 000000000000..245a24734dc5 --- /dev/null +++ b/ui/hooks/useTransactionInfo.js @@ -0,0 +1,19 @@ +import { useSelector } from 'react-redux'; + +import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; + +export const useTransactionInfo = (txData = {}) => { + const { + allNftContracts, + selectedAddress, + provider: { chainId }, + } = useSelector((state) => state.metamask); + + const isNftTransfer = Boolean( + allNftContracts?.[selectedAddress]?.[chainId]?.find((contract) => { + return isEqualCaseInsensitive(contract.address, txData.txParams.to); + }), + ); + + return { isNftTransfer }; +}; diff --git a/ui/hooks/useTransactionInfo.test.js b/ui/hooks/useTransactionInfo.test.js new file mode 100644 index 000000000000..e7fa5f40a741 --- /dev/null +++ b/ui/hooks/useTransactionInfo.test.js @@ -0,0 +1,36 @@ +import { renderHookWithProvider } from '../../test/lib/render-helpers'; +import mockState from '../../test/data/mock-state.json'; +import { useTransactionInfo } from './useTransactionInfo'; + +describe('useTransactionInfo', () => { + describe('isNftTransfer', () => { + it('should return false if transaction is not NFT transfer', () => { + const { result } = renderHookWithProvider( + () => + useTransactionInfo({ + txParams: {}, + }), + mockState, + ); + expect(result.current.isNftTransfer).toStrictEqual(false); + }); + it('should return true if transaction is NFT transfer', () => { + mockState.metamask.allNftContracts = { + [mockState.metamask.selectedAddress]: { + [mockState.metamask.provider.chainId]: [{ address: '0x9' }], + }, + }; + + const { result } = renderHookWithProvider( + () => + useTransactionInfo({ + txParams: { + to: '0x9', + }, + }), + mockState, + ); + expect(result.current.isNftTransfer).toStrictEqual(true); + }); + }); +}); diff --git a/ui/pages/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap b/ui/pages/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap index 46a976a57cd2..c48acbbe6812 100644 --- a/ui/pages/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap +++ b/ui/pages/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap @@ -254,7 +254,7 @@ exports[`Confirm Transaction Base should match snapshot 1`] = ` class="confirm-page-container-summary__title" >

process.env.IN_TEST ? null : ; @@ -108,7 +110,6 @@ export default class ConfirmTransactionBase extends Component { contentComponent: PropTypes.node, dataComponent: PropTypes.node, dataHexComponent: PropTypes.node, - hideSubtitle: PropTypes.bool, tokenAddress: PropTypes.string, customTokenAmount: PropTypes.string, dappProposedTokenAmount: PropTypes.string, @@ -837,38 +838,24 @@ export default class ConfirmTransactionBase extends Component { renderTitleComponent() { const { title, hexTransactionAmount, txData } = this.props; - // Title string passed in by props takes priority - if (title) { - return null; - } - - const isContractInteraction = - txData.type === TransactionType.contractInteraction; - return ( - ); } renderSubtitleComponent() { - const { subtitleComponent, hexTransactionAmount } = this.props; + const { subtitleComponent, hexTransactionAmount, txData } = this.props; return ( - subtitleComponent || ( - - ) + ); } @@ -946,8 +933,6 @@ export default class ConfirmTransactionBase extends Component { toEns, toNickname, methodData, - title, - hideSubtitle, tokenAddress, contentComponent, onEdit, @@ -1024,11 +1009,9 @@ export default class ConfirmTransactionBase extends Component { toNickname={toNickname} showEdit={!isContractInteractionFromDapp && Boolean(onEdit)} action={functionType} - title={title} image={image} titleComponent={this.renderTitleComponent()} subtitleComponent={this.renderSubtitleComponent()} - hideSubtitle={hideSubtitle} detailsComponent={this.renderDetails()} dataComponent={this.renderData(functionType)} dataHexComponent={this.renderDataHex(functionType)} diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js index 12078cc87e7f..9abc1ee22fa4 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -63,7 +63,6 @@ import { TransactionStatus, TransactionType, } from '../../../shared/constants/transaction'; -import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; import { getTokenAddressParam } from '../../helpers/utils/token-util'; import { calcGasTotal } from '../../../shared/lib/transactions-controller-utils'; import ConfirmTransactionBase from './confirm-transaction-base.component'; @@ -105,8 +104,6 @@ const mapStateToProps = (state, ownProps) => { network, unapprovedTxs, nextNonce, - allNftContracts, - selectedAddress, provider: { chainId }, } = metamask; const { tokenData, txData, tokenProps, nonce } = confirmTransaction; @@ -184,12 +181,6 @@ const mapStateToProps = (state, ownProps) => { customTxParamsData, ); - const isNftTransfer = Boolean( - allNftContracts?.[selectedAddress]?.[chainId]?.find((contract) => { - return isEqualCaseInsensitive(contract.address, fullTxData.txParams.to); - }), - ); - customNonceValue = getCustomNonceValue(state); const isEthGasPrice = getIsEthGasPriceFetched(state); const noGasPrice = !supportsEIP1559 && getNoGasPriceFetched(state); @@ -235,7 +226,6 @@ const mapStateToProps = (state, ownProps) => { useNonceField: getUseNonceField(state), customNonceValue, insufficientBalance, - hideSubtitle: !getShouldShowFiat(state) && !isNftTransfer, hideFiatConversion: !getShouldShowFiat(state), type, nextNonce, From 0d2c54e808f5bfd1a8102a69e40479c4a98c5b48 Mon Sep 17 00:00:00 2001 From: Ariella Vu <20778143+digiwand@users.noreply.github.com> Date: Thu, 23 Mar 2023 12:04:24 -0700 Subject: [PATCH 10/12] Fix Signature Request scroll (#18305) * signature-req: fix scroll * signature-req: fix scroll allow items to shrink --- .../app/signature-request-original/index.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ui/components/app/signature-request-original/index.scss b/ui/components/app/signature-request-original/index.scss index 281fae02fa07..10466f21b290 100644 --- a/ui/components/app/signature-request-original/index.scss +++ b/ui/components/app/signature-request-original/index.scss @@ -67,6 +67,13 @@ flex: 1 1 0; min-height: 0; max-width: 100%; + overflow-y: auto; + overflow-x: hidden; + + > * { + flex: 0 0 auto; + min-height: 0; + } } &__origin { @@ -92,13 +99,9 @@ } &__rows { - overflow-y: auto; - overflow-x: hidden; border-top: 1px solid var(--color-border-default); display: flex; flex-flow: column; - flex: 1 1 0; - min-height: 0; } &__row { From fcdc5c9c149f36c3d0331cf45e48c580782064a5 Mon Sep 17 00:00:00 2001 From: Victorien Gauch <85494462+VGau@users.noreply.github.com> Date: Thu, 23 Mar 2023 20:56:07 +0100 Subject: [PATCH 11/12] fix: update zkevm feature toggle date (#18307) --- shared/constants/network.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 0774126239b1..e2d6cf40eebe 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -688,4 +688,4 @@ export const FEATURED_RPCS: RPCDefinition[] = [ ]; export const SHOULD_SHOW_LINEA_TESTNET_NETWORK = - new Date().getTime() > Date.UTC(2023, 2, 28); + new Date().getTime() > Date.UTC(2023, 2, 28, 8); From 79b2deb19439755d4a4199f27db68920ab37f737 Mon Sep 17 00:00:00 2001 From: Garrett Bear Date: Thu, 23 Mar 2023 13:00:37 -0700 Subject: [PATCH 12/12] update text component color to use box color (#18246) snapshot updates fix snapshots add background colors fix snapshots --- .../__snapshots__/avatar-account.test.js.snap | 2 +- .../__snapshots__/avatar-base.test.js.snap | 2 +- .../avatar-base/avatar-base.test.js | 4 +-- .../__snapshots__/avatar-favicon.test.js.snap | 2 +- .../__snapshots__/avatar-icon.test.js.snap | 2 +- .../avatar-icon/avatar-icon.test.js | 4 +-- .../__snapshots__/avatar-network.test.js.snap | 2 +- .../avatar-network/avatar-network.test.js | 4 +-- .../__snapshots__/avatar-token.test.js.snap | 2 +- .../avatar-token/avatar-token.test.js | 4 +-- .../__snapshots__/banner-alert.test.js.snap | 4 +-- .../__snapshots__/banner-base.test.js.snap | 4 +-- .../__snapshots__/banner-tip.test.js.snap | 4 +-- .../__snapshots__/button-base.test.js.snap | 4 +-- .../__snapshots__/button-link.test.js.snap | 2 +- .../__snapshots__/button-primary.test.js.snap | 2 +- .../button-secondary.test.js.snap | 2 +- .../button/__snapshots__/button.test.js.snap | 8 ++--- .../form-text-field.test.js.snap | 2 +- .../form-text-field/form-text-field.test.js | 4 +-- .../__snapshots__/help-text.test.js.snap | 2 +- .../help-text/help-text.test.js | 18 +++++----- .../input/__snapshots__/input.test.js.snap | 2 +- .../label/__snapshots__/label.test.js.snap | 2 +- .../__snapshots__/picker-network.test.js.snap | 4 +-- .../__snapshots__/tag-url.test.js.snap | 4 +-- .../tag/__snapshots__/tag.test.js.snap | 2 +- .../text-field-search.test.js.snap | 2 +- .../__snapshots__/text-field.test.js.snap | 2 +- .../text/__snapshots__/text.test.js.snap | 20 +++++------ ui/components/component-library/text/text.js | 2 +- .../component-library/text/text.scss | 6 ---- .../component-library/text/text.stories.js | 9 +++++ .../component-library/text/text.test.js | 34 +++++++------------ .../__snapshots__/jwt-dropdown.test.js.snap | 2 +- .../__snapshots__/jwt-url-form.test.js.snap | 4 +-- .../__snapshots__/note-to-trader.test.js.snap | 4 +-- .../account-list-item.test.js.snap | 10 +++--- .../detected-token-banner.test.js.snap | 4 +-- .../multichain-import-token-link.test.js.snap | 8 ++--- .../multichain-token-list-item.test.js.snap | 10 +++--- ...irm-approve-content.component.test.js.snap | 24 ++++++------- .../confirm-transaction-base.test.js.snap | 2 +- .../__snapshots__/reveal-seed.test.js.snap | 24 ++++++------- .../experimental-tab.test.js.snap | 4 +-- 45 files changed, 128 insertions(+), 141 deletions(-) diff --git a/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap b/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap index 0dfe9536f963..6a46eb54663b 100644 --- a/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap +++ b/ui/components/component-library/avatar-account/__snapshots__/avatar-account.test.js.snap @@ -3,7 +3,7 @@ exports[`AvatarAccount should render correctly 1`] = `