diff --git a/app/scripts/lib/transaction/metrics.test.ts b/app/scripts/lib/transaction/metrics.test.ts index 1ff509da3c09..f3dfa87799fc 100644 --- a/app/scripts/lib/transaction/metrics.test.ts +++ b/app/scripts/lib/transaction/metrics.test.ts @@ -147,6 +147,7 @@ describe('Transaction metrics', () => { eip_1559_version: '0', gas_edit_attempted: 'none', gas_estimation_failed: false, + is_smart_transaction: undefined, gas_edit_type: 'none', network: mockNetworkId, referrer: ORIGIN_METAMASK, @@ -155,8 +156,8 @@ describe('Transaction metrics', () => { token_standard: TokenStandard.none, transaction_speed_up: false, transaction_type: TransactionType.simpleSend, - ui_customizations: null, - transaction_advanced_view: null, + ui_customizations: ['redesigned_confirmation'], + transaction_advanced_view: undefined, transaction_contract_method: undefined, }; @@ -233,7 +234,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['gas_estimation_failed'], + ui_customizations: [ + 'gas_estimation_failed', + 'redesigned_confirmation', + ], gas_estimation_failed: true, }, sensitiveProperties: expectedSensitiveProperties, @@ -263,7 +267,10 @@ describe('Transaction metrics', () => { ...expectedProperties, security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], ppom_eth_call_count: 5, ppom_eth_getCode_count: 3, }, @@ -353,7 +360,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -370,7 +380,10 @@ describe('Transaction metrics', () => { { properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -490,7 +503,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -510,7 +526,10 @@ describe('Transaction metrics', () => { { properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -687,7 +706,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -709,7 +731,10 @@ describe('Transaction metrics', () => { { properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -820,7 +845,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -841,7 +869,10 @@ describe('Transaction metrics', () => { { properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -947,7 +978,10 @@ describe('Transaction metrics', () => { persist: true, properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, @@ -964,7 +998,10 @@ describe('Transaction metrics', () => { { properties: { ...expectedProperties, - ui_customizations: ['flagged_as_malicious'], + ui_customizations: [ + 'flagged_as_malicious', + 'redesigned_confirmation', + ], security_alert_reason: BlockaidReason.maliciousDomain, security_alert_response: 'Malicious', ppom_eth_call_count: 5, diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index 9da41bcb22d5..afff2f37e57e 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -34,6 +34,8 @@ class TestDapp { tag: 'button', }; + private readonly simpleSendButton = '#sendButton'; + private readonly erc721MintButton = '#mintButton'; private readonly erc721TransferFromButton = '#transferFromButton'; @@ -186,6 +188,10 @@ class TestDapp { }); } + async clickSimpleSendButton() { + await this.driver.clickElement(this.simpleSendButton); + } + async clickERC721MintButton() { await this.driver.clickElement(this.erc721MintButton); } diff --git a/test/e2e/tests/confirmations/helpers.ts b/test/e2e/tests/confirmations/helpers.ts index ff467f42c320..355f664ec61c 100644 --- a/test/e2e/tests/confirmations/helpers.ts +++ b/test/e2e/tests/confirmations/helpers.ts @@ -46,8 +46,8 @@ export function withRedesignConfirmationFixtures( transactionEnvelopeType === TransactionEnvelopeType.legacy ? defaultGanacheOptions : defaultGanacheOptionsForType2Transactions, - smartContract, - testSpecificMock: mocks, + ...(smartContract && { smartContract }), + ...(mocks && { testSpecificMock: mocks }), title, }, testFunction, diff --git a/test/e2e/tests/confirmations/navigation.spec.ts b/test/e2e/tests/confirmations/navigation.spec.ts index 747ba15872b3..38d29ad3ad77 100644 --- a/test/e2e/tests/confirmations/navigation.spec.ts +++ b/test/e2e/tests/confirmations/navigation.spec.ts @@ -66,10 +66,15 @@ describe('Navigation Signature - Different signature types', function (this: Sui '[data-testid="confirm-nav__next-confirmation"]', ); - // Verify Transaction Sending ETH is displayed - await verifyTransaction(driver, 'Sending ETH'); + // Verify simple send transaction is displayed + await driver.waitForSelector({ + tag: 'h3', + text: 'Transfer request', + }); - await driver.clickElement('[data-testid="next-page"]'); + await driver.clickElement( + '[data-testid="confirm-nav__next-confirmation"]', + ); // Verify Sign Typed Data v3 confirmation is displayed await verifySignedTypeV3Confirmation(driver); @@ -78,10 +83,15 @@ describe('Navigation Signature - Different signature types', function (this: Sui '[data-testid="confirm-nav__previous-confirmation"]', ); - // Verify Sign Typed Data v3 confirmation is displayed - await verifyTransaction(driver, 'Sending ETH'); + // Verify simple send transaction is displayed + await driver.waitForSelector({ + tag: 'h3', + text: 'Transfer request', + }); - await driver.clickElement('[data-testid="previous-page"]'); + await driver.clickElement( + '[data-testid="confirm-nav__previous-confirmation"]', + ); // Verify Sign Typed Data v3 confirmation is displayed await verifySignTypedData(driver); @@ -179,13 +189,3 @@ async function queueSignaturesAndTransactions(driver: Driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 3']")); } - -async function verifyTransaction( - driver: Driver, - expectedTransactionType: string, -) { - await driver.waitForSelector({ - tag: 'span', - text: expectedTransactionType, - }); -} diff --git a/test/e2e/tests/confirmations/transactions/native-send-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/native-send-redesign.spec.ts new file mode 100644 index 000000000000..e8226977d019 --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/native-send-redesign.spec.ts @@ -0,0 +1,113 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { TransactionEnvelopeType } from '@metamask/transaction-controller'; +import { DAPP_URL } from '../../../constants'; +import { + unlockWallet, + veryLargeDelayMs, + WINDOW_TITLES, +} from '../../../helpers'; +import TokenTransferTransactionConfirmation from '../../../page-objects/pages/confirmations/redesign/token-transfer-confirmation'; +import HomePage from '../../../page-objects/pages/homepage'; +import SendTokenPage from '../../../page-objects/pages/send/send-token-page'; +import TestDapp from '../../../page-objects/pages/test-dapp'; +import { Driver } from '../../../webdriver/driver'; +import { withRedesignConfirmationFixtures } from '../helpers'; +import { TestSuiteArguments } from './shared'; + +const TOKEN_RECIPIENT_ADDRESS = '0x2f318C334780961FB129D2a6c30D0763d9a5C970'; + +describe('Confirmation Redesign Native Send @no-mmi', function () { + describe('Wallet initiated', async function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ driver }: TestSuiteArguments) => { + await createWalletInitiatedTransactionAndAssertDetails(driver); + }, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver }: TestSuiteArguments) => { + await createWalletInitiatedTransactionAndAssertDetails(driver); + }, + ); + }); + }); + + describe('dApp initiated', async function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ driver }: TestSuiteArguments) => { + await createDAppInitiatedTransactionAndAssertDetails(driver); + }, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.feeMarket, + async ({ driver }: TestSuiteArguments) => { + await createDAppInitiatedTransactionAndAssertDetails(driver); + }, + ); + }); + }); +}); + +async function createWalletInitiatedTransactionAndAssertDetails( + driver: Driver, +) { + await unlockWallet(driver); + + const testDapp = new TestDapp(driver); + + await testDapp.openTestDappPage({ contractAddress: null, url: DAPP_URL }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + const homePage = new HomePage(driver); + await homePage.startSendFlow(); + const sendToPage = new SendTokenPage(driver); + await sendToPage.check_pageIsLoaded(); + await sendToPage.fillRecipient(TOKEN_RECIPIENT_ADDRESS); + await sendToPage.fillAmount('1'); + await sendToPage.goToNextScreen(); + + const tokenTransferTransactionConfirmation = + new TokenTransferTransactionConfirmation(driver); + await tokenTransferTransactionConfirmation.check_walletInitiatedHeadingTitle(); + await tokenTransferTransactionConfirmation.check_networkParagraph(); + await tokenTransferTransactionConfirmation.check_networkFeeParagraph(); + + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} + +async function createDAppInitiatedTransactionAndAssertDetails(driver: Driver) { + await unlockWallet(driver); + + const testDapp = new TestDapp(driver); + + await testDapp.openTestDappPage({ contractAddress: null, url: DAPP_URL }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + await testDapp.clickSimpleSendButton(); + + await driver.delay(veryLargeDelayMs); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const tokenTransferTransactionConfirmation = + new TokenTransferTransactionConfirmation(driver); + await tokenTransferTransactionConfirmation.check_dappInitiatedHeadingTitle(); + await tokenTransferTransactionConfirmation.check_networkParagraph(); + await tokenTransferTransactionConfirmation.check_networkFeeParagraph(); + + await tokenTransferTransactionConfirmation.clickScrollToBottomButton(); + await tokenTransferTransactionConfirmation.clickFooterConfirmButton(); +} diff --git a/ui/pages/confirmations/components/confirm/header/header.tsx b/ui/pages/confirmations/components/confirm/header/header.tsx index 6c7c0e1cdd7f..278b21f85cbb 100644 --- a/ui/pages/confirmations/components/confirm/header/header.tsx +++ b/ui/pages/confirmations/components/confirm/header/header.tsx @@ -3,6 +3,7 @@ import { TransactionType, } from '@metamask/transaction-controller'; import React from 'react'; +import { ORIGIN_METAMASK } from '../../../../../../shared/constants/app'; import { AvatarNetwork, AvatarNetworkSize, @@ -30,6 +31,7 @@ const CONFIRMATIONS_WITH_NEW_HEADER = [ TransactionType.tokenMethodTransfer, TransactionType.tokenMethodTransferFrom, TransactionType.tokenMethodSafeTransferFrom, + TransactionType.simpleSend, ]; const Header = () => { @@ -88,7 +90,7 @@ const Header = () => { currentConfirmation?.type && CONFIRMATIONS_WITH_NEW_HEADER.includes(currentConfirmation.type); const isWalletInitiated = - (currentConfirmation as TransactionMeta)?.origin === 'metamask'; + (currentConfirmation as TransactionMeta)?.origin === ORIGIN_METAMASK; if (isConfirmationWithNewHeader && isWalletInitiated) { return ; } else if (isConfirmationWithNewHeader && !isWalletInitiated) { diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts index efdf2b66ac56..9011569bac3e 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.test.ts @@ -1,11 +1,39 @@ import { TransactionMeta } from '@metamask/transaction-controller'; +import { useSelector } from 'react-redux'; import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer'; import mockState from '../../../../../../../test/data/mock-state.json'; import { renderHookWithProvider } from '../../../../../../../test/lib/render-helpers'; +import { getTokenList } from '../../../../../../selectors'; import { useTokenDetails } from './useTokenDetails'; +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); + +const ICON_SYMBOL = 'FROG'; +const ICON_URL = + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a2c375553e6965b42c135bb8b15a8914b08de0c.png'; +const MOCK_TOKEN_LIST = (transactionMeta: TransactionMeta) => ({ + [transactionMeta.txParams.to as string]: { + address: transactionMeta.txParams.to, + aggregators: ['CoinGecko', 'Socket', 'Coinmarketcap'], + decimals: 9, + iconUrl: ICON_URL, + name: 'Frog on ETH', + occurrences: 3, + symbol: ICON_SYMBOL, + }, +}); + describe('useTokenDetails', () => { - it('returns iconUrl from selected token if it exists', () => { + const useSelectorMock = useSelector as jest.Mock; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('returns token details from selected token if it exists', () => { const transactionMeta = genUnapprovedTokenTransferConfirmation( {}, ) as TransactionMeta; @@ -18,8 +46,17 @@ describe('useTokenDetails', () => { image: 'image', }; + useSelectorMock.mockImplementation((selector) => { + if (selector === getTokenList) { + return MOCK_TOKEN_LIST(transactionMeta); + } else if (selector?.toString().includes('getWatchedToken')) { + return TEST_SELECTED_TOKEN; + } + return undefined; + }); + const { result } = renderHookWithProvider( - () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), + () => useTokenDetails(transactionMeta), mockState, ); @@ -29,6 +66,37 @@ describe('useTokenDetails', () => { }); }); + it('returns token details from the token list if it exists', () => { + const transactionMeta = genUnapprovedTokenTransferConfirmation( + {}, + ) as TransactionMeta; + + const TEST_SELECTED_TOKEN = { + address: 'address', + decimals: 18, + }; + + useSelectorMock.mockImplementation((selector) => { + if (selector === getTokenList) { + return MOCK_TOKEN_LIST(transactionMeta); + } else if (selector?.toString().includes('getWatchedToken')) { + return TEST_SELECTED_TOKEN; + } + + return undefined; + }); + + const { result } = renderHookWithProvider( + () => useTokenDetails(transactionMeta), + mockState, + ); + + expect(result.current).toEqual({ + tokenImage: ICON_URL, + tokenSymbol: ICON_SYMBOL, + }); + }); + it('returns selected token image if no iconUrl is included', () => { const transactionMeta = genUnapprovedTokenTransferConfirmation( {}, @@ -41,8 +109,17 @@ describe('useTokenDetails', () => { image: 'image', }; + useSelectorMock.mockImplementation((selector) => { + if (selector === getTokenList) { + return MOCK_TOKEN_LIST(transactionMeta); + } else if (selector?.toString().includes('getWatchedToken')) { + return TEST_SELECTED_TOKEN; + } + return undefined; + }); + const { result } = renderHookWithProvider( - () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), + () => useTokenDetails(transactionMeta), mockState, ); @@ -63,23 +140,22 @@ describe('useTokenDetails', () => { symbol: 'symbol', }; + useSelectorMock.mockImplementation((selector) => { + if (selector === getTokenList) { + return MOCK_TOKEN_LIST(transactionMeta); + } else if (selector?.toString().includes('getWatchedToken')) { + return TEST_SELECTED_TOKEN; + } + return undefined; + }); + const { result } = renderHookWithProvider( - () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), - { - ...mockState, - metamask: { - ...mockState.metamask, - tokenList: { - '0x076146c765189d51be3160a2140cf80bfc73ad68': { - iconUrl: 'tokenListIconUrl', - }, - }, - }, - }, + () => useTokenDetails(transactionMeta), + mockState, ); expect(result.current).toEqual({ - tokenImage: 'tokenListIconUrl', + tokenImage: ICON_URL, tokenSymbol: 'symbol', }); }); @@ -95,8 +171,17 @@ describe('useTokenDetails', () => { symbol: 'symbol', }; + useSelectorMock.mockImplementation((selector) => { + if (selector === getTokenList) { + return {}; + } else if (selector?.toString().includes('getWatchedToken')) { + return TEST_SELECTED_TOKEN; + } + return undefined; + }); + const { result } = renderHookWithProvider( - () => useTokenDetails(transactionMeta, TEST_SELECTED_TOKEN), + () => useTokenDetails(transactionMeta), mockState, ); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts index be9578496205..da2faacaeb5f 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTokenDetails.ts @@ -2,15 +2,14 @@ import { TokenListMap } from '@metamask/assets-controllers'; import { TransactionMeta } from '@metamask/transaction-controller'; import { useSelector } from 'react-redux'; import { useI18nContext } from '../../../../../../hooks/useI18nContext'; -import { getTokenList } from '../../../../../../selectors'; -import { SelectedToken } from '../shared/selected-token'; +import { getTokenList, getWatchedToken } from '../../../../../../selectors'; +import { MultichainState } from '../../../../../../selectors/multichain'; -export const useTokenDetails = ( - transactionMeta: TransactionMeta, - selectedToken: SelectedToken, -) => { +export const useTokenDetails = (transactionMeta: TransactionMeta) => { const t = useI18nContext(); - + const selectedToken = useSelector((state: MultichainState) => + getWatchedToken(transactionMeta)(state), + ); const tokenList = useSelector(getTokenList) as TokenListMap; const tokenImage = diff --git a/ui/pages/confirmations/components/confirm/info/info.tsx b/ui/pages/confirmations/components/confirm/info/info.tsx index 67bac40d7e61..f283cbfc2e61 100644 --- a/ui/pages/confirmations/components/confirm/info/info.tsx +++ b/ui/pages/confirmations/components/confirm/info/info.tsx @@ -4,19 +4,23 @@ import { useConfirmContext } from '../../../context/confirm'; import { SignatureRequestType } from '../../../types/confirm'; import ApproveInfo from './approve/approve'; import BaseTransactionInfo from './base-transaction-info/base-transaction-info'; +import NativeTransferInfo from './native-transfer/native-transfer'; +import NFTTokenTransferInfo from './nft-token-transfer/nft-token-transfer'; import PersonalSignInfo from './personal-sign/personal-sign'; import SetApprovalForAllInfo from './set-approval-for-all-info/set-approval-for-all-info'; import TokenTransferInfo from './token-transfer/token-transfer'; import TypedSignV1Info from './typed-sign-v1/typed-sign-v1'; import TypedSignInfo from './typed-sign/typed-sign'; -import NFTTokenTransferInfo from './nft-token-transfer/nft-token-transfer'; const Info = () => { const { currentConfirmation } = useConfirmContext(); const ConfirmationInfoComponentMap = useMemo( () => ({ + [TransactionType.contractInteraction]: () => BaseTransactionInfo, + [TransactionType.deployContract]: () => BaseTransactionInfo, [TransactionType.personalSign]: () => PersonalSignInfo, + [TransactionType.simpleSend]: () => NativeTransferInfo, [TransactionType.signTypedData]: () => { const { version } = (currentConfirmation as SignatureRequestType)?.msgParams ?? {}; @@ -25,15 +29,13 @@ const Info = () => { } return TypedSignInfo; }, - [TransactionType.contractInteraction]: () => BaseTransactionInfo, - [TransactionType.deployContract]: () => BaseTransactionInfo, [TransactionType.tokenMethodApprove]: () => ApproveInfo, [TransactionType.tokenMethodIncreaseAllowance]: () => ApproveInfo, + [TransactionType.tokenMethodSafeTransferFrom]: () => NFTTokenTransferInfo, [TransactionType.tokenMethodSetApprovalForAll]: () => SetApprovalForAllInfo, [TransactionType.tokenMethodTransfer]: () => TokenTransferInfo, [TransactionType.tokenMethodTransferFrom]: () => NFTTokenTransferInfo, - [TransactionType.tokenMethodSafeTransferFrom]: () => NFTTokenTransferInfo, }), [currentConfirmation], ); diff --git a/ui/pages/confirmations/components/confirm/info/native-transfer/__snapshots__/native-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/native-transfer/__snapshots__/native-transfer.test.tsx.snap new file mode 100644 index 000000000000..234c0b704d5c --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/native-transfer/__snapshots__/native-transfer.test.tsx.snap @@ -0,0 +1,402 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NativeTransferInfo renders correctly 1`] = ` +
+
+
+ G +
+

+ 0 ETH +

+

+ 0 +

+
+
+
+
+
+ +

+ 0x2e0D7...5d09B +

+
+
+ +
+
+
+
+
+
+
+
+

+ Estimated changes +

+
+
+ +
+
+
+
+
+
+
+
+
+
+

+ You send +

+
+
+
+
+
+

+ - 4 +

+
+
+
+
+
+
+ E +
+

+ ETH +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network +

+
+
+
+
+ G +
+

+ Goerli +

+
+
+
+
+
+

+ Interacting with +

+
+
+
+
+ +

+ 0x07614...3ad68 +

+
+
+
+
+
+
+
+
+

+ Network fee +

+
+
+ +
+
+
+
+
+

+ 0.0001 ETH +

+

+ $0.04 +

+ +
+
+
+
+
+

+ Speed +

+
+
+
+
+

+ 🦊 Market +

+

+ + ~ + 0 sec + +

+
+
+
+
+
+`; diff --git a/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.stories.tsx b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.stories.tsx new file mode 100644 index 000000000000..1de48b2bb2a0 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.stories.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import { Box } from '../../../../../../components/component-library'; +import { + AlignItems, + Display, + FlexDirection, + JustifyContent, +} from '../../../../../../helpers/constants/design-system'; +import configureStore from '../../../../../../store/store'; +import { ConfirmContextProvider } from '../../../../context/confirm'; +import NativeTransferInfo from './native-transfer'; + +const store = configureStore(getMockTokenTransferConfirmState({})); + +const Story = { + title: 'Components/App/Confirm/info/NativeTransferInfo', + component: NativeTransferInfo, + decorators: [ + (story: () => any) => ( + + + + {story()} + + + + ), + ], +}; + +export default Story; + +export const DefaultStory = () => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.test.tsx b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.test.tsx new file mode 100644 index 000000000000..f4b2b4afab50 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.test.tsx @@ -0,0 +1,41 @@ +import { screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { getMockTokenTransferConfirmState } from '../../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; +import { tEn } from '../../../../../../../test/lib/i18n-helpers'; +import NativeTransferInfo from './native-transfer'; + +jest.mock( + '../../../../../../components/app/alert-system/contexts/alertMetricsContext', + () => ({ + useAlertMetrics: jest.fn(() => ({ + trackAlertMetrics: jest.fn(), + })), + }), +); + +jest.mock('../../../../../../store/actions', () => ({ + ...jest.requireActual('../../../../../../store/actions'), + getGasFeeTimeEstimate: jest.fn().mockResolvedValue({ + lowerTimeBound: 0, + upperTimeBound: 60000, + }), +})); + +describe('NativeTransferInfo', () => { + it('renders correctly', async () => { + const state = getMockTokenTransferConfirmState({}); + const mockStore = configureMockStore([])(state); + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + + await waitFor(() => { + expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument(); + }); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.tsx b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.tsx new file mode 100644 index 000000000000..a2dd3ceaaa05 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.tsx @@ -0,0 +1,37 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import React from 'react'; +import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { useConfirmContext } from '../../../../context/confirm'; +import { SimulationDetails } from '../../../simulation-details'; +import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; +import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; +import NativeSendHeading from '../shared/native-send-heading/native-send-heading'; +import { TokenDetailsSection } from '../token-transfer/token-details-section'; +import { TransactionFlowSection } from '../token-transfer/transaction-flow-section'; + +const NativeTransferInfo = () => { + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + + const isWalletInitiated = transactionMeta.origin === 'metamask'; + + return ( + <> + + + {!isWalletInitiated && ( + + + + )} + + + + + ); +}; + +export default NativeTransferInfo; diff --git a/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx new file mode 100644 index 000000000000..a3c2b91c9f8e --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx @@ -0,0 +1,118 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { BigNumber } from 'bignumber.js'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../../../shared/constants/network'; +import { + AvatarToken, + AvatarTokenSize, + Box, + Text, +} from '../../../../../../../components/component-library'; +import Tooltip from '../../../../../../../components/ui/tooltip'; +import { getIntlLocale } from '../../../../../../../ducks/locale/locale'; +import { getConversionRate } from '../../../../../../../ducks/metamask/metamask'; +import { + AlignItems, + Display, + FlexDirection, + JustifyContent, + TextColor, + TextVariant, +} from '../../../../../../../helpers/constants/design-system'; +import { MIN_AMOUNT } from '../../../../../../../hooks/useCurrencyDisplay'; +import { useFiatFormatter } from '../../../../../../../hooks/useFiatFormatter'; +import { getMultichainNetwork } from '../../../../../../../selectors/multichain'; +import { useConfirmContext } from '../../../../../context/confirm'; +import { + formatAmount, + formatAmountMaxPrecision, +} from '../../../../simulation-details/formatAmount'; +import { toNonScientificString } from '../../hooks/use-token-values'; + +const NativeSendHeading = () => { + const { currentConfirmation: transactionMeta } = + useConfirmContext(); + + const nativeAssetTransferValue = new BigNumber( + transactionMeta.txParams.value as string, + ).dividedBy(new BigNumber(10).pow(18)); + + const conversionRate = useSelector(getConversionRate); + const fiatValue = + conversionRate && + nativeAssetTransferValue && + new BigNumber(conversionRate) + .times(nativeAssetTransferValue, 10) + .toNumber(); + const fiatFormatter = useFiatFormatter(); + const fiatDisplayValue = + fiatValue && fiatFormatter(fiatValue, { shorten: true }); + + const multichainNetwork = useSelector(getMultichainNetwork); + const ticker = multichainNetwork?.network?.ticker; + + const locale = useSelector(getIntlLocale); + const roundedTransferValue = formatAmount(locale, nativeAssetTransferValue); + + const transferValue = toNonScientificString( + nativeAssetTransferValue.toNumber(), + ); + + const NetworkImage = ( + + ); + + const NativeAssetAmount = + roundedTransferValue === + `<${formatAmountMaxPrecision(locale, MIN_AMOUNT)}` ? ( + + + {`${roundedTransferValue} ${ticker}`} + + + ) : ( + + {`${roundedTransferValue} ${ticker}`} + + ); + + const NativeAssetFiatConversion = ( + + {fiatDisplayValue} + + ); + + return ( + + {NetworkImage} + {NativeAssetAmount} + {NativeAssetFiatConversion} + + ); +}; + +export default NativeSendHeading; diff --git a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx index b6aff206a26a..3f6bd429d20b 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/send-heading/send-heading.tsx @@ -19,8 +19,7 @@ import { TextVariant, } from '../../../../../../../helpers/constants/design-system'; import { MIN_AMOUNT } from '../../../../../../../hooks/useCurrencyDisplay'; -import { getWatchedToken } from '../../../../../../../selectors'; -import { MultichainState } from '../../../../../../../selectors/multichain'; +import { useI18nContext } from '../../../../../../../hooks/useI18nContext'; import { useConfirmContext } from '../../../../../context/confirm'; import { formatAmountMaxPrecision } from '../../../../simulation-details/formatAmount'; import { useTokenValues } from '../../hooks/use-token-values'; @@ -28,16 +27,11 @@ import { useTokenDetails } from '../../hooks/useTokenDetails'; import { ConfirmLoader } from '../confirm-loader/confirm-loader'; const SendHeading = () => { + const t = useI18nContext(); const { currentConfirmation: transactionMeta } = useConfirmContext(); const locale = useSelector(getIntlLocale); - const selectedToken = useSelector((state: MultichainState) => - getWatchedToken(transactionMeta)(state), - ); - const { tokenImage, tokenSymbol } = useTokenDetails( - transactionMeta, - selectedToken, - ); + const { tokenImage, tokenSymbol } = useTokenDetails(transactionMeta); const { decodedTransferValue, displayTransferValue, @@ -48,15 +42,17 @@ const SendHeading = () => { const TokenImage = ( ); diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx index 6d686873ea1c..629d7d64df3a 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx @@ -1,4 +1,7 @@ -import { TransactionMeta } from '@metamask/transaction-controller'; +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; import React from 'react'; import { useSelector } from 'react-redux'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../../../shared/constants/network'; @@ -61,7 +64,7 @@ export const TokenDetailsSection = () => { ); - const tokenRow = ( + const tokenRow = transactionMeta.type !== TransactionType.simpleSend && ( { const addresses = value?.data[0].params.filter( (param) => param.type === 'address', ); - // sometimes there's more than one address, in which case we want the last one - const recipientAddress = addresses?.[addresses.length - 1].value; + const recipientAddress = + transactionMeta.type === TransactionType.simpleSend + ? transactionMeta.txParams.to + : // sometimes there's more than one address, in which case we want the last one + addresses?.[addresses.length - 1].value; if (pending) { return ; diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index e33ff79d6f01..a007ca0aa0b2 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -25,6 +25,7 @@ export const REDESIGN_USER_TRANSACTION_TYPES = [ TransactionType.tokenMethodTransfer, TransactionType.tokenMethodTransferFrom, TransactionType.tokenMethodSafeTransferFrom, + TransactionType.simpleSend, ]; export const REDESIGN_DEV_TRANSACTION_TYPES = [