From c4dce82e6e016f79e2f2c67cdbaa350e75989f1a Mon Sep 17 00:00:00 2001 From: Priya Date: Fri, 20 Dec 2024 07:10:35 +0100 Subject: [PATCH 01/23] chore: remove duplicated tests for metrics for redesigned signatures (#29359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removes the duplicated tests for redesigned signature metrics, the same metrics are checked in the tests in the folder : test/e2e/tests/confirmations/signatures [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29359?quickstart=1) ## **Related issues** Fixes: [29228](https://github.com/MetaMask/metamask-extension/issues/29228) ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../tests/metrics/signature-approved.spec.js | 174 ------------------ 1 file changed, 174 deletions(-) diff --git a/test/e2e/tests/metrics/signature-approved.spec.js b/test/e2e/tests/metrics/signature-approved.spec.js index d0d1bb9c32c3..56e376f878ff 100644 --- a/test/e2e/tests/metrics/signature-approved.spec.js +++ b/test/e2e/tests/metrics/signature-approved.spec.js @@ -10,8 +10,6 @@ const { clickSignOnSignatureConfirmation, tempToggleSettingRedesignedConfirmations, validateContractDetails, - clickSignOnRedesignedSignatureConfirmation, - WINDOW_TITLES, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -60,10 +58,6 @@ const expectedEventPropertiesBase = { security_alert_response: 'loading', }; -const additionalRedesignEventProperties = { - ui_customizations: ['redesigned_confirmation'], -}; - describe('Signature Approved Event @no-mmi', function () { describe('Old confirmation screens', function () { it('Successfully tracked for signTypedData_v4', async function () { @@ -230,172 +224,4 @@ describe('Signature Approved Event @no-mmi', function () { ); }); }); - - describe('Redesigned confirmation screens', function () { - it('Successfully tracked for signTypedData_v4', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withMetaMetricsController({ - metaMetricsId: 'fake-metrics-id', - participateInMetaMetrics: true, - }) - .build(), - defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await unlockWallet(driver); - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement('#signTypedDataV4'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await clickSignOnRedesignedSignatureConfirmation({ driver }); - const events = await getEventPayloads(driver, mockedEndpoints); - - assert.deepStrictEqual(events[0].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData_v4', - eip712_primary_type: 'Mail', - }); - - assert.deepStrictEqual(events[1].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData_v4', - eip712_primary_type: 'Mail', - security_alert_response: 'Benign', - }); - }, - ); - }); - - it('Successfully tracked for signTypedData_v3', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withMetaMetricsController({ - metaMetricsId: 'fake-metrics-id', - participateInMetaMetrics: true, - }) - .build(), - defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await unlockWallet(driver); - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement('#signTypedDataV3'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await clickSignOnRedesignedSignatureConfirmation({ driver }); - const events = await getEventPayloads(driver, mockedEndpoints); - - assert.deepStrictEqual(events[0].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData_v3', - }); - - assert.deepStrictEqual(events[1].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData_v3', - security_alert_response: 'Benign', - }); - }, - ); - }); - - it('Successfully tracked for signTypedData', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withMetaMetricsController({ - metaMetricsId: 'fake-metrics-id', - participateInMetaMetrics: true, - }) - .build(), - defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await unlockWallet(driver); - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement('#signTypedData'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await clickSignOnRedesignedSignatureConfirmation({ driver }); - const events = await getEventPayloads(driver, mockedEndpoints); - - assert.deepStrictEqual(events[0].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData', - }); - - assert.deepStrictEqual(events[1].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData', - security_alert_response: 'Benign', - }); - }, - ); - }); - - it('Successfully tracked for personalSign', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withMetaMetricsController({ - metaMetricsId: 'fake-metrics-id', - participateInMetaMetrics: true, - }) - .build(), - defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await unlockWallet(driver); - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement('#personalSign'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await clickSignOnRedesignedSignatureConfirmation({ driver }); - const events = await getEventPayloads(driver, mockedEndpoints); - - assert.deepStrictEqual(events[0].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'personal_sign', - }); - - assert.deepStrictEqual(events[1].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'personal_sign', - security_alert_response: 'Benign', - }); - }, - ); - }); - }); }); From fd3c51cf42b124277effd0a0f68e693f76872c45 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 20 Dec 2024 08:29:46 +0100 Subject: [PATCH 02/23] fix: Sanitize `signTypedDatav3v4` params before calling security API (#29343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR aims to filter request params before calling security API call if method is `signTypedDatav3v4` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29343?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3830 ## **Manual testing steps** 1. Copy the following payload ``` // Request the current account addresses from the Ethereum provider const addresses = await window.ethereum.request({ "method": "eth_accounts" }); // Construct the JSON string for eth_signTypedData_v4, including the dynamic owner address const jsonData = { domain: { name: "USD Coin", version: "2", chainId: "1", verifyingContract: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" }, types: { EIP712Domain: [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" } ], Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" } ] }, primaryType: "Permit", message: { owner: addresses[0], spender: "0xa2d86c5ff6fbf5f455b1ba2737938776c24d7a58", value: "115792089237316195423570985008687907853269984665640564039457584007913129639935", nonce: "0", deadline: "115792089237316195423570985008687907853269984665640564039457584007913129639935" } }; // Use the first account address for signing the typed data window.ethereum.sendAsync({ method: "eth_signTypedData_v4", params: [ addresses[0], JSON.stringify(jsonData), {}, {}, {} ] }); ``` 2. Navigate to MM E2E Test Dapp > Connect Wallet > Open up the console > Paste the payload above > Hit enter 3. Notice that the transaction is considered as malicious (which was not flagged before) ## **Screenshots/Recordings** https://github.com/user-attachments/assets/ffcdd83f-bb79-4490-b729-f96559ce5769 ### **Before** ### **After** ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/lib/ppom/ppom-util.test.ts | 50 +++++++++++++++++++++++++- app/scripts/lib/ppom/ppom-util.ts | 20 ++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/ppom/ppom-util.test.ts b/app/scripts/lib/ppom/ppom-util.test.ts index d3ff3015ca41..c143f3dfe11b 100644 --- a/app/scripts/lib/ppom/ppom-util.test.ts +++ b/app/scripts/lib/ppom/ppom-util.test.ts @@ -10,7 +10,7 @@ import { SignatureController, SignatureRequest, } from '@metamask/signature-controller'; -import { Hex } from '@metamask/utils'; +import { Hex, JsonRpcRequest } from '@metamask/utils'; import { BlockaidReason, BlockaidResultType, @@ -22,6 +22,8 @@ import { AppStateController } from '../../controllers/app-state-controller'; import { generateSecurityAlertId, isChainSupported, + METHOD_SIGN_TYPED_DATA_V3, + METHOD_SIGN_TYPED_DATA_V4, updateSecurityAlertResponse, validateRequestWithPPOM, } from './ppom-util'; @@ -57,6 +59,10 @@ const TRANSACTION_PARAMS_MOCK_1: TransactionParams = { value: '0x123', }; +const SIGN_TYPED_DATA_PARAMS_MOCK_1 = '0x123'; +const SIGN_TYPED_DATA_PARAMS_MOCK_2 = + '{"primaryType":"Permit","domain":{},"types":{}}'; + const TRANSACTION_PARAMS_MOCK_2: TransactionParams = { ...TRANSACTION_PARAMS_MOCK_1, to: '0x456', @@ -261,6 +267,48 @@ describe('PPOM Utils', () => { ); }); + // @ts-expect-error This is missing from the Mocha type definitions + it.each([METHOD_SIGN_TYPED_DATA_V3, METHOD_SIGN_TYPED_DATA_V4])( + 'sanitizes request params if method is %s', + async (method: string) => { + const ppom = createPPOMMock(); + const ppomController = createPPOMControllerMock(); + + ppomController.usePPOM.mockImplementation( + (callback) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback(ppom as any) as any, + ); + + const firstTwoParams = [ + SIGN_TYPED_DATA_PARAMS_MOCK_1, + SIGN_TYPED_DATA_PARAMS_MOCK_2, + ]; + + const unwantedParams = [{}, undefined, 1, null]; + + const params = [...firstTwoParams, ...unwantedParams]; + + const request = { + ...REQUEST_MOCK, + method, + params, + } as unknown as JsonRpcRequest; + + await validateRequestWithPPOM({ + ...validateRequestWithPPOMOptionsBase, + ppomController, + request, + }); + + expect(ppom.validateJsonRpc).toHaveBeenCalledTimes(1); + expect(ppom.validateJsonRpc).toHaveBeenCalledWith({ + ...request, + params: firstTwoParams, + }); + }, + ); + it('updates response indicating chain is not supported', async () => { const ppomController = {} as PPOMController; const CHAIN_ID_UNSUPPORTED_MOCK = '0x2'; diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts index 7dbf8c92ec5f..0407c0604a69 100644 --- a/app/scripts/lib/ppom/ppom-util.ts +++ b/app/scripts/lib/ppom/ppom-util.ts @@ -29,6 +29,8 @@ import { const { sentry } = global; const METHOD_SEND_TRANSACTION = 'eth_sendTransaction'; +export const METHOD_SIGN_TYPED_DATA_V3 = 'eth_signTypedData_v3'; +export const METHOD_SIGN_TYPED_DATA_V4 = 'eth_signTypedData_v4'; const SECURITY_ALERT_RESPONSE_ERROR = { result_type: BlockaidResultType.Errored, @@ -171,7 +173,7 @@ function normalizePPOMRequest( request, ) ) { - return request; + return sanitizeRequest(request); } const transactionParams = request.params[0]; @@ -183,6 +185,22 @@ function normalizePPOMRequest( }; } +function sanitizeRequest(request: JsonRpcRequest): JsonRpcRequest { + // This is a temporary fix to prevent a PPOM bypass + if ( + request.method === METHOD_SIGN_TYPED_DATA_V4 || + request.method === METHOD_SIGN_TYPED_DATA_V3 + ) { + if (Array.isArray(request.params)) { + return { + ...request, + params: request.params.slice(0, 2), + }; + } + } + return request; +} + function getErrorMessage(error: unknown) { if (error instanceof Error) { return `${error.name}: ${error.message}`; From d4c5a7368df7ce83ea8d63caf6fe2f592f3aaa28 Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:46:25 +0700 Subject: [PATCH 03/23] fix: Network URL toPunycodeUrl preserve no path slash (#29325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29325?quickstart=1) ## **Related issues** Related to: https://github.com/MetaMask/metamask-extension/pull/29322 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../confirmation/templates/add-ethereum-chain.test.js | 1 + ui/pages/confirmations/utils/confirm.test.ts | 9 ++++++--- ui/pages/confirmations/utils/confirm.ts | 7 ++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.test.js b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.test.js index 19f51b6fa798..50fb4a04e4c7 100644 --- a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.test.js +++ b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.test.js @@ -159,6 +159,7 @@ describe('add-ethereum-chain confirmation', () => { "Attackers sometimes mimic sites by making small changes to the site address. Make sure you're interacting with the intended site before you continue. Punycode version: https://xn--ifura-dig.io/gnosis", ), ).toBeInTheDocument(); + expect(getByText('https://iոfura.io/gnosis')).toBeInTheDocument(); }); }); }); diff --git a/ui/pages/confirmations/utils/confirm.test.ts b/ui/pages/confirmations/utils/confirm.test.ts index 9a8b3d1a0f8a..a81b5959a916 100644 --- a/ui/pages/confirmations/utils/confirm.test.ts +++ b/ui/pages/confirmations/utils/confirm.test.ts @@ -93,8 +93,11 @@ describe('confirm util', () => { expect(toPunycodeURL('https://iոfura.io/gnosis')).toStrictEqual( 'https://xn--ifura-dig.io/gnosis', ); - expect(toPunycodeURL('https://www.google.com')).toStrictEqual( - 'https://www.google.com/', + expect(toPunycodeURL('https://iոfura.io')).toStrictEqual( + 'https://xn--ifura-dig.io', + ); + expect(toPunycodeURL('https://iոfura.io/')).toStrictEqual( + 'https://xn--ifura-dig.io/', ); expect( toPunycodeURL('https://iոfura.io/gnosis:5050?test=iոfura&foo=bar'), @@ -102,7 +105,7 @@ describe('confirm util', () => { 'https://xn--ifura-dig.io/gnosis:5050?test=i%D5%B8fura&foo=bar', ); expect(toPunycodeURL('https://www.google.com')).toStrictEqual( - 'https://www.google.com/', + 'https://www.google.com', ); }); }); diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index f464f51e8159..379d98728be2 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -80,7 +80,12 @@ export const isValidASCIIURL = (urlString?: string) => { export const toPunycodeURL = (urlString: string) => { try { - return new URL(urlString).href; + const url = new URL(urlString); + const { protocol, hostname, port, search, hash } = url; + const pathname = + url.pathname === '/' && !urlString.endsWith('/') ? '' : url.pathname; + + return `${protocol}//${hostname}${port}${pathname}${search}${hash}`; } catch (err: unknown) { console.error(`Failed to convert URL to Punycode: ${err}`); return undefined; From 367769b9e299428e836e6e9d5bb49a1ebbf04cf0 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:19:26 +0100 Subject: [PATCH 04/23] test: [POM] Dapp subscribe network switch spec migration (#29346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR migrates the `test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js` spec to use page object model and typescript. It also updates the assertions to test what it's intended. 1. We go to the test dapp and connect 2. We subscribe to the newHeads event ``` await window.ethereum.request({ "method": "eth_subscribe", "params": [ "newHeads" ], }); ``` 3. We add an event listener for subscribe messages, and we'll store this into a window variable, so we can access it later ``` window.ethereum.on('message', (message) => { if (message.type === 'eth_subscription' && message.data.subscription === '0x4bc2639eb3ac769db7a90f60a47b33c4') { console.log('New block header:', message.data.result); } }) ``` 4. We switch networks from MM wide screen 5. We go back to the dapp 6. We mine a block deterministically --> In ganache we have setup auto-mining by default, however this happens every some seconds, by performing a mine ourselves, we know for sure that this happened at least once at the point we want 7. We wait a couple of seconds to see if more event logs appear 8. We assert that we got more events, after switching networks [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29346?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/29348 ## **Manual testing steps** 1. Check ci continues to pass 3. Run spec manually `yarn test:e2e:single test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts --browser=chrome --leave-running=true ## **Screenshots/Recordings** Messages when we console log them in the spec ![Screenshot from 2024-12-19 12-04-23](https://github.com/user-attachments/assets/7d55edc0-107f-419f-9f2a-d7bdbe3fc80a) This is the flow that happens in the spec, done manually. https://github.com/user-attachments/assets/cd68d1ac-c0ac-4bd2-b089-fec605ffed6d ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/seeder/ganache.ts | 7 ++ .../dapp1-subscribe-network-switch.spec.js | 95 ------------------ .../dapp1-subscribe-network-switch.spec.ts | 98 +++++++++++++++++++ 3 files changed, 105 insertions(+), 95 deletions(-) delete mode 100644 test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js create mode 100644 test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts diff --git a/test/e2e/seeder/ganache.ts b/test/e2e/seeder/ganache.ts index 7fa3d1ab0238..d262924ab61e 100644 --- a/test/e2e/seeder/ganache.ts +++ b/test/e2e/seeder/ganache.ts @@ -76,6 +76,13 @@ export class Ganache { }); } + async mineBlock() { + return await this.getProvider()?.request({ + method: 'evm_mine', + params: [], + }); + } + async quit() { if (!this.#server) { throw new Error('Server not running yet'); diff --git a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js deleted file mode 100644 index 53c763d8891f..000000000000 --- a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js +++ /dev/null @@ -1,95 +0,0 @@ -const { strict: assert } = require('assert'); -const FixtureBuilder = require('../../fixture-builder'); -const { - withFixtures, - openDapp, - unlockWallet, - DAPP_URL, - regularDelayMs, - WINDOW_TITLES, - switchToNotificationWindow, - defaultGanacheOptions, -} = require('../../helpers'); - -describe('Request Queueing', function () { - it('should keep subscription on dapp network when switching different mm network', async function () { - const port = 8546; - const chainId = 1338; - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() - .build(), - ganacheOptions: { - ...defaultGanacheOptions, - concurrent: [ - { - port, - chainId, - ganacheOptions2: defaultGanacheOptions, - }, - ], - }, - title: this.test.fullTitle(), - }, - - async ({ driver }) => { - await unlockWallet(driver); - - await openDapp(driver, undefined, DAPP_URL); - - // Connect to dapp - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); - - await driver.delay(regularDelayMs); - - await switchToNotificationWindow(driver); - - await driver.clickElement({ - text: 'Connect', - tag: 'button', - }); - - // Navigate to test dapp - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - const subscribeRequest = JSON.stringify({ - jsonrpc: '2.0', - method: 'eth_subscribe', - params: ['newHeads'], - }); - - const subscribe = await driver.executeScript( - `return window.ethereum.request(${subscribeRequest})`, - ); - - const subscriptionMessage = await driver.executeAsyncScript( - `const callback = arguments[arguments.length - 1];` + - `window.ethereum.on('message', (message) => callback(message))`, - ); - - assert.strictEqual(subscribe, subscriptionMessage.data.subscription); - assert.strictEqual(subscriptionMessage.type, 'eth_subscription'); - - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // Network Selector - await driver.clickElement('[data-testid="network-display"]'); - - // Switch to second network - await driver.clickElement({ - text: 'Localhost 8546', - css: 'p', - }); - - assert.strictEqual(subscribe, subscriptionMessage.data.subscription); - assert.strictEqual(subscriptionMessage.type, 'eth_subscription'); - }, - ); - }); -}); diff --git a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts new file mode 100644 index 000000000000..ec607fc34936 --- /dev/null +++ b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts @@ -0,0 +1,98 @@ +import { strict as assert } from 'assert'; +import FixtureBuilder from '../../fixture-builder'; +import { + defaultGanacheOptions, + WINDOW_TITLES, + withFixtures, +} from '../../helpers'; +import TestDapp from '../../page-objects/pages/test-dapp'; +import { loginWithoutBalanceValidation } from '../../page-objects/flows/login.flow'; +import { switchToNetworkFlow } from '../../page-objects/flows/network.flow'; + +describe('Request Queueing', function () { + it('should keep subscription on dapp network when switching different mm network', async function () { + const port = 8546; + const chainId = 1338; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerDoubleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .build(), + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port, + chainId, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + title: this.test?.fullTitle(), + }, + + async ({ driver, ganacheServer }) => { + await loginWithoutBalanceValidation(driver); + + // Connect to dapp + const testDapp = new TestDapp(driver); + await testDapp.openTestDappPage(); + await testDapp.check_pageIsLoaded(); + await testDapp.connectAccount({}); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // Subscribe to newHeads event + const subscribeRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_subscribe', + params: ['newHeads'], + }); + + await driver.executeScript( + `return window.ethereum.request(${subscribeRequest})`, + ); + + // Save event logs into the messages variable in the window context, to access it later + await driver.executeScript(` + window.messages = []; + window.ethereum.on('message', (message) => { + if (message.type === 'eth_subscription') { + console.log('New block header:', message.data.result); + window.messages.push(message.data.result); + } + }); + `); + + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // Switch networks + await switchToNetworkFlow(driver, 'Localhost 8546'); + + // Navigate back to the test dapp + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // Access to the window messages variable + const messagesBeforeMining = await driver.executeScript( + 'return window.messages;', + ); + + // Mine a block deterministically + await ganacheServer.mineBlock(); + + // Wait a couple of seconds for the logs to populate into the messages window variable + await driver.delay(5000); + + // Access the window messages variable and check there are more events than before mining + const messagesAfterMining = await driver.executeScript( + 'return window.messages;', + ); + + assert.ok(messagesBeforeMining.length < messagesAfterMining.length); + }, + ); + }); +}); From fd6e75559bc374698e84a1a4c42808e542c2838a Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 20 Dec 2024 10:34:24 +0100 Subject: [PATCH 05/23] fix: Use `toUnicode` function to normalize ens domains in the UI (#29231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** In `ENSController`(https://github.com/MetaMask/core/blob/main/packages/ens-controller/src/EnsController.ts#L375) right before saving the ens domain in the state we use `toASCII`. This function is typically used to convert a domain name from its Unicode representation to ASCII, specifically using the `Punycode` package encoding. This is necessary because the Domain Name System (DNS) operates with ASCII characters, and internationalized domain names (IDNs) need to be converted to a format that DNS can understand. On the other side, in the client, we are not converting/normalizing this domain value. That is causing that unwanted ASCII coded domain in the UI when using smileys in the ENS domain. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29231?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28610 ## **Manual testing steps** See https://github.com/MetaMask/metamask-extension/issues/28610 for repro steps ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-12-16 at 13 37 04](https://github.com/user-attachments/assets/62163ce0-007a-404b-8f2e-7a49eaa7b927) ### **After** ![Screenshot 2024-12-16 at 13 36 32](https://github.com/user-attachments/assets/cd287cb0-aa81-49da-aafb-1e753d8544e7) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/selectors/selectors.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 61c15000baba..403db6cadb91 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1,3 +1,4 @@ +import { toUnicode } from 'punycode/punycode.js'; import { SubjectType } from '@metamask/permission-controller'; import { ApprovalType } from '@metamask/controller-utils'; import { @@ -729,7 +730,10 @@ export function getAddressBook(state) { export function getEnsResolutionByAddress(state, address) { if (state.metamask.ensResolutionsByAddress[address]) { - return state.metamask.ensResolutionsByAddress[address]; + const ensResolution = state.metamask.ensResolutionsByAddress[address]; + // ensResolution is a punycode encoded string hence toUnicode is used to decode it from same package + const normalizedEnsResolution = toUnicode(ensResolution); + return normalizedEnsResolution; } const entry = From a33f52631f87275f86459b74d0e7ae12511b954c Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 20 Dec 2024 15:26:18 +0530 Subject: [PATCH 06/23] fix: UI is not displaying gas limit set by dapp (#29352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** UI is not displaying correct gas limit set by dapp. It always displays `21000`. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/18417 ## **Manual testing steps** 1. Submit confirmation request with gas limit not `21000` 2. Check gas limit in gas editing popup ## **Screenshots/Recordings** Screenshot 2024-12-19 at 7 20 45 PM ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/pages/confirmations/hooks/useGasFeeInputs.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ui/pages/confirmations/hooks/useGasFeeInputs.js b/ui/pages/confirmations/hooks/useGasFeeInputs.js index 8675b726038a..129778bfcf20 100644 --- a/ui/pages/confirmations/hooks/useGasFeeInputs.js +++ b/ui/pages/confirmations/hooks/useGasFeeInputs.js @@ -164,7 +164,11 @@ export function useGasFeeInputs( }); const [gasLimit, setGasLimit] = useState(() => - Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0')), + Number( + hexToDecimal( + transaction?.txParams?.gasLimit ?? transaction?.txParams?.gas ?? '0x0', + ), + ), ); const properGasLimit = Number(hexToDecimal(transaction?.originalGasEstimate)); @@ -195,7 +199,15 @@ export function useGasFeeInputs( setEstimateUsed(transaction?.userFeeLevel); } - setGasLimit(Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0'))); + setGasLimit( + Number( + hexToDecimal( + transaction?.txParams?.gasLimit ?? + transaction?.txParams?.gas ?? + '0x0', + ), + ), + ); } }, [ setEstimateUsed, From 356ad476f75183e17f9493b75625575daedfd018 Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:38:04 +0100 Subject: [PATCH 07/23] test: [POM] Migrate watch account tests (#29314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Created a new page class `AccountDetailsModal`. Previously, it was a part of `AccountList`. I think it's better to separate it and make it an independent class. - I also took the chance to improve the function `addAccount` and remove the origin `addNewAccount`. So now for creating ethereum, bitcoin, solana accounts, we use the same `addAccount` function with the account type as a parameter. - Migrate watch account e2e tests to Page Object Model - Created `watchEoaAddress` flow that can be reusable. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1) ## **Related issues** ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/constants.ts | 7 + test/e2e/flask/btc/common-btc.ts | 4 +- test/e2e/flask/btc/create-btc-account.spec.ts | 37 +- test/e2e/flask/create-watch-account.spec.ts | 407 +++++++----------- test/e2e/flask/solana/common-solana.ts | 7 +- .../solana/create-solana-account.spec.ts | 7 +- test/e2e/page-objects/common.ts | 6 - .../page-objects/flows/watch-account.flow.ts | 22 + .../page-objects/pages/account-list-page.ts | 194 ++++----- .../pages/dialog/account-details-modal.ts | 106 +++++ test/e2e/page-objects/pages/header-navbar.ts | 26 ++ .../pages/home/bitcoin-homepage.ts | 10 +- test/e2e/page-objects/pages/home/homepage.ts | 50 ++- .../pages/settings/experimental-settings.ts | 20 + .../tests/account/account-custom-name.spec.ts | 14 +- test/e2e/tests/account/add-account.spec.ts | 13 +- test/e2e/tests/account/import-flow.spec.ts | 5 +- .../account-syncing/new-user-sync.spec.ts | 6 +- .../onboarding-with-opt-out.spec.ts | 7 +- .../sync-after-adding-account.spec.ts | 10 +- .../sync-after-modifying-account-name.spec.ts | 11 +- .../sync-with-account-balances.spec.ts | 14 +- test/e2e/tests/tokens/nft/import-nft.spec.ts | 5 +- 23 files changed, 559 insertions(+), 429 deletions(-) create mode 100644 test/e2e/page-objects/flows/watch-account.flow.ts create mode 100644 test/e2e/page-objects/pages/dialog/account-details-modal.ts diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 9d4d5bbf3c8d..6af1056d7232 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -76,3 +76,10 @@ export const DEFAULT_SOLANA_BALANCE = 1; // SOL /* Title of the mocked E2E test empty HTML page */ export const EMPTY_E2E_TEST_PAGE_TITLE = 'E2E Test Page'; + +/* Account types */ +export enum ACCOUNT_TYPE { + Ethereum, + Bitcoin, + Solana, +} diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts index db2db85eb554..ea9cb6b1f2e6 100644 --- a/test/e2e/flask/btc/common-btc.ts +++ b/test/e2e/flask/btc/common-btc.ts @@ -2,6 +2,7 @@ import { Mockttp } from 'mockttp'; import FixtureBuilder from '../../fixture-builder'; import { withFixtures } from '../../helpers'; import { + ACCOUNT_TYPE, DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE, DEFAULT_BTC_FEES_RATE, @@ -14,7 +15,6 @@ import { Driver } from '../../webdriver/driver'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; import AccountListPage from '../../page-objects/pages/account-list-page'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; -import { ACCOUNT_TYPE } from '../../page-objects/common'; const QUICKNODE_URL_REGEX = /^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u; @@ -218,7 +218,7 @@ export async function withBtcAccountSnap( await new HeaderNavbar(driver).openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, ''); + await accountListPage.addAccount({ accountType: ACCOUNT_TYPE.Bitcoin }); await test(driver, mockServer); }, ); diff --git a/test/e2e/flask/btc/create-btc-account.spec.ts b/test/e2e/flask/btc/create-btc-account.spec.ts index f563454ad1e6..68cdcd82caf5 100644 --- a/test/e2e/flask/btc/create-btc-account.spec.ts +++ b/test/e2e/flask/btc/create-btc-account.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; import { WALLET_PASSWORD } from '../../helpers'; +import AccountDetailsModal from '../../page-objects/pages/dialog/account-details-modal'; import AccountListPage from '../../page-objects/pages/account-list-page'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; import LoginPage from '../../page-objects/pages/login-page'; import PrivacySettings from '../../page-objects/pages/settings/privacy-settings'; import ResetPasswordPage from '../../page-objects/pages/reset-password-page'; import SettingsPage from '../../page-objects/pages/settings/settings-page'; -import { ACCOUNT_TYPE } from '../../page-objects/common'; +import { ACCOUNT_TYPE } from '../../constants'; import { withBtcAccountSnap } from './common-btc'; describe('Create BTC Account', function (this: Suite) { @@ -82,9 +83,11 @@ describe('Create BTC Account', function (this: Suite) { await headerNavbar.openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - const accountAddress = await accountListPage.getAccountAddress( - 'Bitcoin Account', - ); + await accountListPage.openAccountDetailsModal('Bitcoin Account'); + + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + const accountAddress = await accountDetailsModal.getAccountAddress(); await headerNavbar.openAccountMenu(); await accountListPage.removeAccount('Bitcoin Account'); @@ -97,14 +100,15 @@ describe('Create BTC Account', function (this: Suite) { ); await accountListPage.closeAccountModal(); await headerNavbar.openAccountMenu(); - await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, ''); + await accountListPage.addAccount({ accountType: ACCOUNT_TYPE.Bitcoin }); await headerNavbar.check_accountLabel('Bitcoin Account'); await headerNavbar.openAccountMenu(); await accountListPage.check_pageIsLoaded(); - const recreatedAccountAddress = await accountListPage.getAccountAddress( - 'Bitcoin Account', - ); + await accountListPage.openAccountDetailsModal('Bitcoin Account'); + await accountDetailsModal.check_pageIsLoaded(); + const recreatedAccountAddress = + await accountDetailsModal.getAccountAddress(); assert(accountAddress === recreatedAccountAddress); }, @@ -123,9 +127,10 @@ describe('Create BTC Account', function (this: Suite) { await headerNavbar.openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - const accountAddress = await accountListPage.getAccountAddress( - 'Bitcoin Account', - ); + await accountListPage.openAccountDetailsModal('Bitcoin Account'); + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + const accountAddress = await accountDetailsModal.getAccountAddress(); // go to privacy settings page and get the SRP await headerNavbar.openSettingsPage(); @@ -151,14 +156,16 @@ describe('Create BTC Account', function (this: Suite) { await headerNavbar.check_pageIsLoaded(); await headerNavbar.openAccountMenu(); await accountListPage.check_pageIsLoaded(); - await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, ''); + await accountListPage.addAccount({ accountType: ACCOUNT_TYPE.Bitcoin }); await headerNavbar.check_accountLabel('Bitcoin Account'); await headerNavbar.openAccountMenu(); await accountListPage.check_pageIsLoaded(); - const recreatedAccountAddress = await accountListPage.getAccountAddress( - 'Bitcoin Account', - ); + await accountListPage.openAccountDetailsModal('Bitcoin Account'); + await accountDetailsModal.check_pageIsLoaded(); + const recreatedAccountAddress = + await accountDetailsModal.getAccountAddress(); + assert(accountAddress === recreatedAccountAddress); }, ); diff --git a/test/e2e/flask/create-watch-account.spec.ts b/test/e2e/flask/create-watch-account.spec.ts index f25f38f0b2ce..5cfca1dda5ef 100644 --- a/test/e2e/flask/create-watch-account.spec.ts +++ b/test/e2e/flask/create-watch-account.spec.ts @@ -1,70 +1,22 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; -import messages from '../../../app/_locales/en/messages.json'; import FixtureBuilder from '../fixture-builder'; -import { defaultGanacheOptions, unlockWallet, withFixtures } from '../helpers'; +import { withFixtures } from '../helpers'; import { Driver } from '../webdriver/driver'; +import AccountDetailsModal from '../page-objects/pages/dialog/account-details-modal'; +import AccountListPage from '../page-objects/pages/account-list-page'; +import ExperimentalSettings from '../page-objects/pages/settings/experimental-settings'; +import HeaderNavbar from '../page-objects/pages/header-navbar'; +import HomePage from '../page-objects/pages/home/homepage'; +import SettingsPage from '../page-objects/pages/settings/settings-page'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import { watchEoaAddress } from '../page-objects/flows/watch-account.flow'; const ACCOUNT_1 = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; const EOA_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; const SHORTENED_EOA_ADDRESS = '0xd8dA6...96045'; const DEFAULT_WATCHED_ACCOUNT_NAME = 'Watched Account 1'; -/** - * Start the flow to create a watch account by clicking the account menu and selecting the option to add a watch account. - * - * @param driver - The WebDriver instance used to control the browser. - * @param unlockWalletFirst - Whether to unlock the wallet before starting the flow. - */ -async function startCreateWatchAccountFlow( - driver: Driver, - unlockWalletFirst: boolean = true, -): Promise { - if (unlockWalletFirst) { - await unlockWallet(driver); - } - - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-add-watch-only-account"]', - ); -} - -/** - * Watches an EOA address. - * - * @param driver - The WebDriver instance used to control the browser. - * @param unlockWalletFirst - Whether to unlock the wallet before watching the address. - * @param address - The EOA address to watch. - */ -async function watchEoaAddress( - driver: Driver, - unlockWalletFirst: boolean = true, - address: string = EOA_ADDRESS, -): Promise { - await startCreateWatchAccountFlow(driver, unlockWalletFirst); - await driver.fill('input#address-input[type="text"]', address); - await driver.clickElement({ text: 'Watch account', tag: 'button' }); - await driver.clickElement('[data-testid="submit-add-account-with-name"]'); -} - -/** - * Removes the selected account. - * - * @param driver - The WebDriver instance used to control the browser. - */ -async function removeSelectedAccount(driver: Driver): Promise { - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]', - ); - await driver.clickElement('[data-testid="account-list-menu-remove"]'); - await driver.clickElement({ text: 'Remove', tag: 'button' }); -} - describe('Account-watcher snap', function (this: Suite) { describe('Adding watched accounts', function () { it('adds watch account with valid EOA address', async function () { @@ -76,22 +28,17 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { // watch an EOA address - await watchEoaAddress(driver); + await loginWithBalanceValidation(driver); + await watchEoaAddress(driver, EOA_ADDRESS); // new account should be displayed in the account list - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: DEFAULT_WATCHED_ACCOUNT_NAME, - }); - await driver.findElement({ - css: '.mm-text--ellipsis', - text: SHORTENED_EOA_ADDRESS, - }); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel(DEFAULT_WATCHED_ACCOUNT_NAME); + await headerNavbar.check_accountAddress(SHORTENED_EOA_ADDRESS); }, ); }); @@ -105,40 +52,29 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { // watch an EOA address - await watchEoaAddress(driver); + await loginWithBalanceValidation(driver); + await watchEoaAddress(driver, EOA_ADDRESS); + const homePage = new HomePage(driver); + await homePage.headerNavbar.check_accountLabel( + DEFAULT_WATCHED_ACCOUNT_NAME, + ); // 'Send' button should be disabled - await driver.findElement( - '[data-testid="eth-overview-send"][disabled]', - ); - await driver.findElement( - '[data-testid="eth-overview-send"].icon-button--disabled', - ); + assert.equal(await homePage.check_ifSendButtonIsClickable(), false); // 'Swap' button should be disabled - await driver.findElement( - '[data-testid="token-overview-button-swap"][disabled]', - ); - await driver.findElement( - '[data-testid="token-overview-button-swap"].icon-button--disabled', - ); + assert.equal(await homePage.check_ifSwapButtonIsClickable(), false); // 'Bridge' button should be disabled - await driver.findElement( - '[data-testid="eth-overview-bridge"][disabled]', - ); - await driver.findElement( - '[data-testid="eth-overview-bridge"].icon-button--disabled', - ); + assert.equal(await homePage.check_ifBridgeButtonIsClickable(), false); // check tooltips for disabled buttons - await driver.findElement( - '.icon-button--disabled [data-tooltipped][data-original-title="Not supported with this account."]', + await homePage.check_disabledButtonTooltip( + 'Not supported with this account.', ); }, ); @@ -182,20 +118,19 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { - await startCreateWatchAccountFlow(driver); - - await driver.fill('input#address-input[type="text"]', input); - await driver.clickElement({ text: 'Watch account', tag: 'button' }); - - // error message should be displayed by the snap - await driver.findElement({ - css: '.snap-ui-renderer__text', - text: message, - }); + await loginWithBalanceValidation(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // error message should be displayed by snap when try to watch an EOA with invalid input + await homePage.headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.addEoaAccount(input, message); }, ); }); @@ -216,30 +151,23 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { // watch an EOA address for ACCOUNT_2 - await watchEoaAddress(driver, true, ACCOUNT_2); - - // try to import private key of watched ACCOUNT_2 address - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement({ text: 'Import account', tag: 'button' }); - await driver.findClickableElement('#private-key-box'); - await driver.fill('#private-key-box', PRIVATE_KEY_TWO); - await driver.clickElement( - '[data-testid="import-account-confirm-button"]', + await loginWithBalanceValidation(driver); + await watchEoaAddress(driver, ACCOUNT_2); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel(DEFAULT_WATCHED_ACCOUNT_NAME); + + // try to import private key of watched ACCOUNT_2 address and check error message + await headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.addNewImportedAccount( + PRIVATE_KEY_TWO, + 'KeyringController - The account you are trying to import is a duplicate', ); - - // error message should be displayed - await driver.findElement({ - css: '.mm-box--color-error-default', - text: 'KeyringController - The account you are trying to import is a duplicate', - }); }, ); }); @@ -253,62 +181,27 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // watch an EOA address - await watchEoaAddress(driver); - - // click to view account details - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - await driver.clickElement( - '[data-testid="account-list-menu-details"]', - ); - // 'Show private key' button should not be displayed - await driver.assertElementNotPresent({ - css: 'button', - text: 'Show private key', - }); - }, - ); - }); - - it('removes a watched account', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder() - .withPreferencesControllerAndFeatureFlag({ - watchEthereumAccountEnabled: true, - }) - .withNetworkControllerOnMainnet() - .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { // watch an EOA address - await watchEoaAddress(driver); - - // remove the selected watched account - await removeSelectedAccount(driver); - - // account should be removed from the account list - await driver.assertElementNotPresent({ - css: '[data-testid="account-menu-icon"]', - text: DEFAULT_WATCHED_ACCOUNT_NAME, - }); - await driver.assertElementNotPresent({ - css: '.mm-text--ellipsis', - text: SHORTENED_EOA_ADDRESS, - }); + await loginWithBalanceValidation(driver); + await watchEoaAddress(driver, EOA_ADDRESS); + + // open account details modal in header navbar + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel(DEFAULT_WATCHED_ACCOUNT_NAME); + await headerNavbar.openAccountDetailsModal(); + + // check 'Show private key' button should not be displayed + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + await accountDetailsModal.check_showPrivateKeyButtonIsNotDisplayed(); }, ); }); - it('can remove and recreate a watched account', async function () { + it('removes a watched account and recreate a watched account', async function () { await withFixtures( { fixtures: new FixtureBuilder() @@ -317,113 +210,83 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { // watch an EOA address - await watchEoaAddress(driver); + await loginWithBalanceValidation(driver); + await watchEoaAddress(driver, EOA_ADDRESS); + const homePage = new HomePage(driver); + await homePage.headerNavbar.check_accountLabel( + DEFAULT_WATCHED_ACCOUNT_NAME, + ); // remove the selected watched account - await removeSelectedAccount(driver); + await homePage.headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.removeAccount(DEFAULT_WATCHED_ACCOUNT_NAME); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); // account should be removed from the account list - await driver.assertElementNotPresent({ - css: '[data-testid="account-menu-icon"]', - text: DEFAULT_WATCHED_ACCOUNT_NAME, - }); - await driver.assertElementNotPresent({ - css: '.mm-text--ellipsis', - text: SHORTENED_EOA_ADDRESS, - }); - - // watch the same EOA address again - await watchEoaAddress(driver, false); - - // same account should be displayed in the account list - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: DEFAULT_WATCHED_ACCOUNT_NAME, - }); - await driver.findElement({ - css: '.mm-text--ellipsis', - text: SHORTENED_EOA_ADDRESS, - }); + await homePage.headerNavbar.openAccountMenu(); + await accountListPage.check_accountIsNotDisplayedInAccountList( + DEFAULT_WATCHED_ACCOUNT_NAME, + ); + await accountListPage.closeAccountModal(); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // watch the same EOA address again and check the account is recreated + await watchEoaAddress(driver, EOA_ADDRESS); + await homePage.headerNavbar.check_accountLabel( + DEFAULT_WATCHED_ACCOUNT_NAME, + ); + await homePage.headerNavbar.check_accountAddress( + SHORTENED_EOA_ADDRESS, + ); }, ); }); }); describe('Experimental toggle', function () { - const navigateToExperimentalSettings = async (driver: Driver) => { - await driver.clickElement('[data-testid="account-options-menu-button"]'); - await driver.clickElement({ text: 'Settings', tag: 'div' }); - await driver.clickElement({ text: 'Experimental', tag: 'div' }); - await driver.waitForSelector({ - text: messages.watchEthereumAccountsToggle.message, - tag: 'span', - }); - }; - - const getToggleState = async (driver: Driver): Promise => { - const toggleInput = await driver.findElement( - '[data-testid="watch-account-toggle"]', - ); - return toggleInput.isSelected(); - }; - - const toggleWatchAccountOptionAndCloseSettings = async (driver: Driver) => { - await driver.clickElement('[data-testid="watch-account-toggle-div"]'); - await driver.clickElement( - '.settings-page__header__title-container__close-button', - ); - }; - - const verifyWatchAccountOptionAndCloseMenu = async ( - driver: Driver, - shouldBePresent: boolean, - ) => { - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - if (shouldBePresent) { - await driver.waitForSelector({ - text: messages.addEthereumWatchOnlyAccount.message, - tag: 'button', - }); - } else { - await driver.assertElementNotPresent({ - text: messages.addEthereumWatchOnlyAccount.message, - tag: 'button', - }); - } - await driver.clickElement('header button[aria-label="Close"]'); - }; - it("will show the 'Watch an Ethereum account (Beta)' option when setting is enabled", async function () { await withFixtures( { fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }) => { - await unlockWallet(driver); - await navigateToExperimentalSettings(driver); - - // verify toggle is off by default + await loginWithBalanceValidation(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // navigate to experimental settings + await homePage.headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + const experimentalSettings = new ExperimentalSettings(driver); + await experimentalSettings.check_pageIsLoaded(); + + // verify watch account toggle is off by default and enable the toggle assert.equal( - await getToggleState(driver), + await experimentalSettings.getWatchAccountToggleState(), false, 'Toggle should be off by default', ); - - // enable the toggle - await toggleWatchAccountOptionAndCloseSettings(driver); + await experimentalSettings.toggleWatchAccount(); + await settingsPage.closeSettingsPage(); // verify the 'Watch and Ethereum account (Beta)' option is available - await verifyWatchAccountOptionAndCloseMenu(driver, true); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + await homePage.headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_addWatchAccountAvailable(true); }, ); }); @@ -432,27 +295,49 @@ describe('Account-watcher snap', function (this: Suite) { await withFixtures( { fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }) => { - await unlockWallet(driver); - await navigateToExperimentalSettings(driver); - - // enable the toggle - await toggleWatchAccountOptionAndCloseSettings(driver); + await loginWithBalanceValidation(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // navigate to experimental settings and enable the toggle + await homePage.headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + const experimentalSettings = new ExperimentalSettings(driver); + await experimentalSettings.check_pageIsLoaded(); + await experimentalSettings.toggleWatchAccount(); + await settingsPage.closeSettingsPage(); // verify the 'Watch and Ethereum account (Beta)' option is available - await verifyWatchAccountOptionAndCloseMenu(driver, true); - - // navigate back to experimental settings - await navigateToExperimentalSettings(driver); - - // disable the toggle - await toggleWatchAccountOptionAndCloseSettings(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + await homePage.headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_addWatchAccountAvailable(true); + await accountListPage.closeAccountModal(); + + // navigate back to experimental settings and disable the toggle + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + await homePage.headerNavbar.openSettingsPage(); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + await experimentalSettings.check_pageIsLoaded(); + await experimentalSettings.toggleWatchAccount(); + await settingsPage.closeSettingsPage(); // verify the 'Watch and Ethereum account (Beta)' option is not available - await verifyWatchAccountOptionAndCloseMenu(driver, false); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + await homePage.headerNavbar.openAccountMenu(); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_addWatchAccountAvailable(false); }, ); }); diff --git a/test/e2e/flask/solana/common-solana.ts b/test/e2e/flask/solana/common-solana.ts index 2ac107bb442c..ac36d5ebed86 100644 --- a/test/e2e/flask/solana/common-solana.ts +++ b/test/e2e/flask/solana/common-solana.ts @@ -4,7 +4,7 @@ import { Driver } from '../../webdriver/driver'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; import AccountListPage from '../../page-objects/pages/account-list-page'; import FixtureBuilder from '../../fixture-builder'; -import { ACCOUNT_TYPE } from '../../page-objects/common'; +import { ACCOUNT_TYPE } from '../../constants'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; const SOLANA_URL_REGEX = /^https:\/\/.*\.solana.*/u; @@ -64,7 +64,10 @@ export async function withSolanaAccountSnap( const headerComponen = new HeaderNavbar(driver); await headerComponen.openAccountMenu(); const accountListPage = new AccountListPage(driver); - await accountListPage.addAccount(ACCOUNT_TYPE.Solana, 'Solana 1'); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Solana, + accountName: 'Solana 1', + }); await test(driver, mockServer); }, ); diff --git a/test/e2e/flask/solana/create-solana-account.spec.ts b/test/e2e/flask/solana/create-solana-account.spec.ts index cca4f5222993..0cac9fab7375 100644 --- a/test/e2e/flask/solana/create-solana-account.spec.ts +++ b/test/e2e/flask/solana/create-solana-account.spec.ts @@ -1,7 +1,7 @@ import { Suite } from 'mocha'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; import AccountListPage from '../../page-objects/pages/account-list-page'; -import { ACCOUNT_TYPE } from '../../page-objects/common'; +import { ACCOUNT_TYPE } from '../../constants'; import { withSolanaAccountSnap } from './common-solana'; // Scenarios skipped due to https://consensyssoftware.atlassian.net/browse/SOL-87 @@ -17,7 +17,10 @@ describe('Create Solana Account', function (this: Suite) { await headerNavbar.openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_accountDisplayedInAccountList('Account 1'); - await accountListPage.addAccount(ACCOUNT_TYPE.Solana, 'Solana 2'); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Solana, + accountName: 'Solana 2', + }); await headerNavbar.check_accountLabel('Solana 2'); await headerNavbar.openAccountMenu(); await accountListPage.check_numberOfAvailableAccounts(3); diff --git a/test/e2e/page-objects/common.ts b/test/e2e/page-objects/common.ts index 40eb625d94ac..5bf1a91e1859 100644 --- a/test/e2e/page-objects/common.ts +++ b/test/e2e/page-objects/common.ts @@ -2,9 +2,3 @@ export type RawLocator = | string | { css?: string; text?: string } | { tag: string; text: string }; - -export enum ACCOUNT_TYPE { - Ethereum, - Bitcoin, - Solana, -} diff --git a/test/e2e/page-objects/flows/watch-account.flow.ts b/test/e2e/page-objects/flows/watch-account.flow.ts new file mode 100644 index 000000000000..8481c71599a6 --- /dev/null +++ b/test/e2e/page-objects/flows/watch-account.flow.ts @@ -0,0 +1,22 @@ +import { Driver } from '../../webdriver/driver'; +import HomePage from '../pages/home/homepage'; +import AccountListPage from '../pages/account-list-page'; + +/** + * Initiates the flow of watching an EOA address. + * + * @param driver - The WebDriver instance. + * @param address - The EOA address that is to be watched. + */ +export async function watchEoaAddress( + driver: Driver, + address: string, +): Promise { + // watch a new EOA + const homePage = new HomePage(driver); + await homePage.headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.addEoaAccount(address); + await homePage.check_pageIsLoaded(); +} diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts index 628d758e7e71..92eede81652b 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -1,13 +1,11 @@ import { Driver } from '../../webdriver/driver'; import { largeDelayMs, regularDelayMs } from '../../helpers'; import messages from '../../../../app/_locales/en/messages.json'; -import { ACCOUNT_TYPE } from '../common'; +import { ACCOUNT_TYPE } from '../../constants'; class AccountListPage { private readonly driver: Driver; - private readonly accountAddressText = '.qr-code__address-segments'; - private readonly accountListAddressItem = '[data-testid="account-list-address"]'; @@ -28,10 +26,6 @@ class AccountListPage { private readonly accountOptionsMenuButton = '[data-testid="account-list-item-menu-button"]'; - private readonly accountQrCodeImage = '.qr-code__wrapper'; - - private readonly accountQrCodeAddress = '.qr-code__address-segments'; - private readonly addAccountConfirmButton = '[data-testid="submit-add-account-with-name"]'; @@ -48,6 +42,9 @@ class AccountListPage { private readonly addEthereumAccountButton = '[data-testid="multichain-account-menu-popover-add-account"]'; + private readonly addEoaAccountButton = + '[data-testid="multichain-account-menu-popover-add-watch-only-account"]'; + private readonly addHardwareWalletButton = { text: 'Add hardware wallet', tag: 'button', @@ -70,11 +67,6 @@ class AccountListPage { private readonly currentSelectedAccount = '.multichain-account-list-item--selected'; - private readonly editableLabelButton = - '[data-testid="editable-label-button"]'; - - private readonly editableLabelInput = '[data-testid="editable-input"] input'; - private readonly hiddenAccountOptionsMenuButton = '.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]'; @@ -124,8 +116,18 @@ class AccountListPage { tag: 'button', }; - private readonly saveAccountLabelButton = - '[data-testid="save-account-label-input"]'; + private readonly watchAccountAddressInput = + 'input#address-input[type="text"]'; + + private readonly watchAccountConfirmButton = { + text: 'Watch account', + tag: 'button', + }; + + private readonly watchAccountModalTitle = { + text: 'Watch any Ethereum account', + tag: 'h4', + }; constructor(driver: Driver) { this.driver = driver; @@ -145,34 +147,36 @@ class AccountListPage { } /** - * Adds a new account with an optional custom label. + * Watch an EOA (external owned account). * - * @param customLabel - The custom label for the new account. If not provided, a default name will be used. + * @param address - The address to watch. + * @param expectedErrorMessage - Optional error message to display if the address is invalid. */ - async addNewAccount(customLabel?: string): Promise { - if (customLabel) { - console.log(`Adding new account with custom label: ${customLabel}`); - } else { - console.log(`Adding new account with default name`); - } + async addEoaAccount( + address: string, + expectedErrorMessage: string = '', + ): Promise { + console.log(`Watch EOA account with address ${address}`); await this.driver.clickElement(this.createAccountButton); - await this.driver.clickElement(this.addEthereumAccountButton); - if (customLabel) { - await this.driver.fill(this.accountNameInput, customLabel); - } - // needed to mitigate a race condition with the state update - // there is no condition we can wait for in the UI - await this.driver.delay(largeDelayMs); + await this.driver.clickElement(this.addEoaAccountButton); + await this.driver.waitForSelector(this.watchAccountModalTitle); + await this.driver.fill(this.watchAccountAddressInput, address); await this.driver.clickElementAndWaitToDisappear( - this.addAccountConfirmButton, + this.watchAccountConfirmButton, ); - } - - async isBtcAccountCreationButtonEnabled() { - const createButton = await this.driver.findElement( - this.addBtcAccountButton, - ); - return await createButton.isEnabled(); + if (expectedErrorMessage) { + console.log( + `Check if error message is displayed: ${expectedErrorMessage}`, + ); + await this.driver.waitForSelector({ + css: '.snap-ui-renderer__text', + text: expectedErrorMessage, + }); + } else { + await this.driver.clickElementAndWaitToDisappear( + this.addAccountConfirmButton, + ); + } } /** @@ -205,20 +209,27 @@ class AccountListPage { /** * Adds a new account of the specified type with an optional custom name. * - * @param accountType - The type of account to add (Ethereum, Bitcoin, or Solana) - * @param accountName - Optional custom name for the new account + * @param options - Options for adding a new account + * @param options.accountType - The type of account to add (Ethereum, Bitcoin, or Solana) + * @param [options.accountName] - Optional custom name for the new account * @throws {Error} If the specified account type is not supported * @example * // Add a new Ethereum account with default name - * await accountListPage.addAccount(ACCOUNT_TYPE.Ethereum); + * await accountListPage.addAccount({ accountType: ACCOUNT_TYPE.Ethereum }); * * // Add a new Bitcoin account with custom name - * await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, 'My BTC Wallet'); + * await accountListPage.addAccount({ accountType: ACCOUNT_TYPE.Bitcoin, accountName: 'My BTC Wallet' }); */ - async addAccount(accountType: ACCOUNT_TYPE, accountName?: string) { + async addAccount({ + accountType, + accountName, + }: { + accountType: ACCOUNT_TYPE; + accountName?: string; + }) { + console.log(`Adding new account of type: ${ACCOUNT_TYPE[accountType]}`); await this.driver.clickElement(this.createAccountButton); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let addAccountButton: any; + let addAccountButton; switch (accountType) { case ACCOUNT_TYPE.Ethereum: addAccountButton = this.addEthereumAccountButton; @@ -235,56 +246,20 @@ class AccountListPage { await this.driver.clickElement(addAccountButton); if (accountName) { + console.log( + `Customize the new account with account name: ${accountName}`, + ); await this.driver.fill(this.accountNameInput, accountName); } - + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await this.driver.delay(largeDelayMs); await this.driver.clickElementAndWaitToDisappear( this.addAccountConfirmButton, 5000, ); } - /** - * Changes the label of the current account. - * - * @param newLabel - The new label to set for the account. - */ - async changeAccountLabel(newLabel: string): Promise { - console.log(`Changing account label to: ${newLabel}`); - await this.driver.clickElement(this.accountMenuButton); - await this.changeLabelFromAccountDetailsModal(newLabel); - } - - /** - * Changes the account label from within an already opened account details modal. - * Note: This method assumes the account details modal is already open. - * - * Recommended usage: - * ```typescript - * await accountListPage.openAccountDetailsModal('Current Account Name'); - * await accountListPage.changeLabelFromAccountDetailsModal('New Account Name'); - * ``` - * - * @param newLabel - The new label to set for the account - * @throws Will throw an error if the modal is not open when method is called - * @example - * // To rename a specific account, first open its details modal: - * await accountListPage.openAccountDetailsModal('Current Account Name'); - * await accountListPage.changeLabelFromAccountDetailsModal('New Account Name'); - * - * // Note: Using changeAccountLabel() alone will only work for the first account - */ - async changeLabelFromAccountDetailsModal(newLabel: string): Promise { - await this.driver.waitForSelector(this.editableLabelButton); - console.log( - `Account details modal opened, changing account label to: ${newLabel}`, - ); - await this.driver.clickElement(this.editableLabelButton); - await this.driver.fill(this.editableLabelInput, newLabel); - await this.driver.clickElement(this.saveAccountLabelButton); - await this.driver.clickElement(this.closeAccountModalButton); - } - async closeAccountModal(): Promise { console.log(`Close account modal in account list`); await this.driver.clickElementAndWaitToDisappear( @@ -292,23 +267,6 @@ class AccountListPage { ); } - /** - * Get the address of the specified account. - * - * @param accountLabel - The label of the account to get the address. - */ - async getAccountAddress(accountLabel: string): Promise { - console.log(`Get account address in account list`); - await this.openAccountOptionsInAccountList(accountLabel); - await this.driver.clickElement(this.accountMenuButton); - await this.driver.waitForSelector(this.accountAddressText); - const accountAddress = await ( - await this.driver.findElement(this.accountAddressText) - ).getText(); - await this.driver.clickElement(this.closeAccountModalButton); - return accountAddress; - } - async hideAccount(): Promise { console.log(`Hide account in account list`); await this.driver.clickElement(this.hideUnhideAccountButton); @@ -340,6 +298,13 @@ class AccountListPage { ); } + async isBtcAccountCreationButtonEnabled(): Promise { + const createButton = await this.driver.findElement( + this.addBtcAccountButton, + ); + return await createButton.isEnabled(); + } + /** * Open the account details modal for the specified account in account list. * @@ -556,21 +521,24 @@ class AccountListPage { } /** - * Check that the correct address is displayed in the account details modal. + * Checks that the add watch account button is displayed in the create account modal. * - * @param expectedAddress - The expected address to check. + * @param expectedAvailability - Whether the add watch account button is expected to be displayed. */ - async check_addressInAccountDetailsModal( - expectedAddress: string, + async check_addWatchAccountAvailable( + expectedAvailability: boolean, ): Promise { console.log( - `Check that address ${expectedAddress} is displayed in account details modal`, + `Check add watch account button is ${ + expectedAvailability ? 'displayed ' : 'not displayed' + }`, ); - await this.driver.waitForSelector(this.accountQrCodeImage); - await this.driver.waitForSelector({ - css: this.accountQrCodeAddress, - text: expectedAddress, - }); + await this.openAddAccountModal(); + if (expectedAvailability) { + await this.driver.waitForSelector(this.addEoaAccountButton); + } else { + await this.driver.assertElementNotPresent(this.addEoaAccountButton); + } } /** diff --git a/test/e2e/page-objects/pages/dialog/account-details-modal.ts b/test/e2e/page-objects/pages/dialog/account-details-modal.ts new file mode 100644 index 000000000000..0dbb2e0f87d7 --- /dev/null +++ b/test/e2e/page-objects/pages/dialog/account-details-modal.ts @@ -0,0 +1,106 @@ +import { Driver } from '../../../webdriver/driver'; + +class AccountDetailsModal { + private driver: Driver; + + private readonly accountAddressText = '.qr-code__address-segments'; + + private readonly accountQrCodeAddress = '.qr-code__address-segments'; + + private readonly accountQrCodeImage = '.qr-code__wrapper'; + + private readonly closeAccountModalButton = + 'header button[aria-label="Close"]'; + + private readonly copyAddressButton = + '[data-testid="address-copy-button-text"]'; + + private readonly editableLabelButton = + '[data-testid="editable-label-button"]'; + + private readonly editableLabelInput = '[data-testid="editable-input"] input'; + + private readonly saveAccountLabelButton = + '[data-testid="save-account-label-input"]'; + + private readonly showPrivateKeyButton = { + css: 'button', + text: 'Show private key', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.editableLabelButton, + this.copyAddressButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for account details modal to be loaded', + e, + ); + throw e; + } + console.log('Account details modal is loaded'); + } + + async closeAccountDetailsModal(): Promise { + await this.driver.clickElementAndWaitToDisappear( + this.closeAccountModalButton, + ); + } + + /** + * Change the label of the account in the account details modal. + * + * @param newLabel - The new label to set for the account. + */ + async changeAccountLabel(newLabel: string): Promise { + console.log( + `Account details modal opened, changing account label to: ${newLabel}`, + ); + await this.driver.clickElement(this.editableLabelButton); + await this.driver.fill(this.editableLabelInput, newLabel); + await this.driver.clickElement(this.saveAccountLabelButton); + await this.closeAccountDetailsModal(); + } + + async getAccountAddress(): Promise { + console.log(`Get account address in account details modal`); + await this.driver.waitForSelector(this.accountAddressText); + const accountAddress = await ( + await this.driver.findElement(this.accountAddressText) + ).getText(); + await this.closeAccountDetailsModal(); + return accountAddress; + } + + /** + * Check that the correct address is displayed in the account details modal. + * + * @param expectedAddress - The expected address to check. + */ + async check_addressInAccountDetailsModal( + expectedAddress: string, + ): Promise { + console.log( + `Check that address ${expectedAddress} is displayed in account details modal`, + ); + await this.driver.waitForSelector(this.accountQrCodeImage); + await this.driver.waitForSelector({ + css: this.accountQrCodeAddress, + text: expectedAddress, + }); + } + + async check_showPrivateKeyButtonIsNotDisplayed(): Promise { + console.log('Check that show private key button is not displayed'); + await this.driver.assertElementNotPresent(this.showPrivateKeyButton); + } +} + +export default AccountDetailsModal; diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts index 57e02e864624..e64fdc5a4cd7 100644 --- a/test/e2e/page-objects/pages/header-navbar.ts +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -9,6 +9,8 @@ class HeaderNavbar { private readonly allPermissionsButton = '[data-testid="global-menu-connected-sites"]'; + private readonly copyAddressButton = '[data-testid="app-header-copy-button"]'; + private readonly threeDotMenuButton = '[data-testid="account-options-menu-button"]'; @@ -19,6 +21,9 @@ class HeaderNavbar { private readonly mmiPortfolioButton = '[data-testid="global-menu-mmi-portfolio"]'; + private readonly openAccountDetailsButton = + '[data-testid="account-list-menu-details"]'; + private readonly settingsButton = '[data-testid="global-menu-settings"]'; private readonly switchNetworkDropDown = '[data-testid="network-display"]'; @@ -51,6 +56,12 @@ class HeaderNavbar { await this.driver.clickElement(this.accountMenuButton); } + async openAccountDetailsModal(): Promise { + console.log('Open account details modal'); + await this.openThreeDotMenu(); + await this.driver.clickElement(this.openAccountDetailsButton); + } + async openThreeDotMenu(): Promise { console.log('Open account options menu'); await this.driver.clickElement(this.threeDotMenuButton); @@ -98,6 +109,21 @@ class HeaderNavbar { ); } + /** + * Verifies that the displayed account address in header matches the expected address. + * + * @param expectedAddress - The expected address of the account. + */ + async check_accountAddress(expectedAddress: string): Promise { + console.log( + `Verify the displayed account address in header is: ${expectedAddress}`, + ); + await this.driver.waitForSelector({ + css: this.copyAddressButton, + text: expectedAddress, + }); + } + /** * Verifies that the displayed account label in header matches the expected label. * diff --git a/test/e2e/page-objects/pages/home/bitcoin-homepage.ts b/test/e2e/page-objects/pages/home/bitcoin-homepage.ts index 19b235636405..691d85763b14 100644 --- a/test/e2e/page-objects/pages/home/bitcoin-homepage.ts +++ b/test/e2e/page-objects/pages/home/bitcoin-homepage.ts @@ -4,10 +4,7 @@ class BitcoinHomepage extends HomePage { protected readonly balance = '[data-testid="coin-overview__primary-currency"]'; - private readonly bridgeButton = { - text: 'Bridge', - tag: 'button', - }; + protected readonly bridgeButton = '[data-testid="coin-overview-bridge"]'; private readonly buySellButton = '[data-testid="coin-overview-buy"]'; @@ -15,10 +12,7 @@ class BitcoinHomepage extends HomePage { protected readonly sendButton = '[data-testid="coin-overview-send"]'; - private readonly swapButton = { - text: 'Swap', - tag: 'button', - }; + protected readonly swapButton = '[data-testid="coin-overview-swap"]'; async check_pageIsLoaded(): Promise { try { diff --git a/test/e2e/page-objects/pages/home/homepage.ts b/test/e2e/page-objects/pages/home/homepage.ts index b4b79846fb06..708ae5ac0277 100644 --- a/test/e2e/page-objects/pages/home/homepage.ts +++ b/test/e2e/page-objects/pages/home/homepage.ts @@ -20,6 +20,9 @@ class HomePage { css: '.mm-banner-alert', }; + protected readonly bridgeButton: string = + '[data-testid="eth-overview-bridge"]'; + private readonly closeUseNetworkNotificationModalButton = { text: 'Got it', tag: 'h6', @@ -45,12 +48,15 @@ class HomePage { testId: 'sensitive-toggle', }; + protected readonly sendButton: string = '[data-testid="eth-overview-send"]'; + + protected readonly swapButton: string = + '[data-testid="token-overview-button-swap"]'; + private readonly refreshErc20Tokens = { testId: 'refreshList', }; - protected readonly sendButton: string = '[data-testid="eth-overview-send"]'; - private readonly tokensTab = { testId: 'account-overview__asset-tab', }; @@ -142,6 +148,13 @@ class HomePage { await this.driver.waitForSelector(this.basicFunctionalityOffWarningMessage); } + async check_disabledButtonTooltip(tooltipText: string): Promise { + console.log(`Check if disabled button tooltip is displayed on homepage`); + await this.driver.waitForSelector( + `.icon-button--disabled [data-tooltipped][data-original-title="${tooltipText}"]`, + ); + } + /** * Checks if the toaster message for editing a network is displayed on the homepage. * @@ -197,6 +210,39 @@ class HomePage { }, 10000); } + async check_ifBridgeButtonIsClickable(): Promise { + try { + await this.driver.findClickableElement(this.bridgeButton, 1000); + } catch (e) { + console.log('Bridge button not clickable', e); + return false; + } + console.log('Bridge button is clickable'); + return true; + } + + async check_ifSendButtonIsClickable(): Promise { + try { + await this.driver.findClickableElement(this.sendButton, 1000); + } catch (e) { + console.log('Send button not clickable', e); + return false; + } + console.log('Send button is clickable'); + return true; + } + + async check_ifSwapButtonIsClickable(): Promise { + try { + await this.driver.findClickableElement(this.swapButton, 1000); + } catch (e) { + console.log('Swap button not clickable', e); + return false; + } + console.log('Swap button is clickable'); + return true; + } + async check_localBlockchainBalanceIsDisplayed( localBlockchainServer?: Ganache, address = null, diff --git a/test/e2e/page-objects/pages/settings/experimental-settings.ts b/test/e2e/page-objects/pages/settings/experimental-settings.ts index f551db6d47c5..69df0525093d 100644 --- a/test/e2e/page-objects/pages/settings/experimental-settings.ts +++ b/test/e2e/page-objects/pages/settings/experimental-settings.ts @@ -22,6 +22,12 @@ class ExperimentalSettings { private readonly requestQueueToggle = '[data-testid="experimental-setting-toggle-request-queue"] label'; + private readonly watchAccountToggleState = + '[data-testid="watch-account-toggle"]'; + + private readonly watchAccountToggle = + '[data-testid="watch-account-toggle-div"]'; + constructor(driver: Driver) { this.driver = driver; } @@ -39,6 +45,15 @@ class ExperimentalSettings { console.log('Experimental Settings page is loaded'); } + // Get the state of the Watch Account Toggle, returns true if the toggle is selected + async getWatchAccountToggleState(): Promise { + console.log('Get Watch Account Toggle State'); + const toggleInput = await this.driver.findElement( + this.watchAccountToggleState, + ); + return toggleInput.isSelected(); + } + async toggleBitcoinAccount(): Promise { console.log('Toggle Add new Bitcoin account on experimental setting page'); await this.driver.waitForSelector({ @@ -62,6 +77,11 @@ class ExperimentalSettings { console.log('Toggle Request Queue on experimental setting page'); await this.driver.clickElement(this.requestQueueToggle); } + + async toggleWatchAccount(): Promise { + console.log('Toggle Watch Account on experimental setting page'); + await this.driver.clickElement(this.watchAccountToggle); + } } export default ExperimentalSettings; diff --git a/test/e2e/tests/account/account-custom-name.spec.ts b/test/e2e/tests/account/account-custom-name.spec.ts index 4c0ecbe196f9..92302e032224 100644 --- a/test/e2e/tests/account/account-custom-name.spec.ts +++ b/test/e2e/tests/account/account-custom-name.spec.ts @@ -1,8 +1,10 @@ import { Suite } from 'mocha'; import { Driver } from '../../webdriver/driver'; import { withFixtures } from '../../helpers'; +import { ACCOUNT_TYPE } from '../../constants'; import FixtureBuilder from '../../fixture-builder'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import AccountDetailsModal from '../../page-objects/pages/dialog/account-details-modal'; import AccountListPage from '../../page-objects/pages/account-list-page'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; @@ -25,14 +27,20 @@ describe('Account Custom Name Persistence', function (this: Suite) { // Change account label for existing account and verify edited account label const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.openAccountOptionsMenu(); - await accountListPage.changeAccountLabel(newAccountLabel); + await accountListPage.openAccountDetailsModal('Account 1'); + + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + await accountDetailsModal.changeAccountLabel(newAccountLabel); await headerNavbar.check_accountLabel(newAccountLabel); // Add new account with custom label and verify new added account label await headerNavbar.openAccountMenu(); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewAccount(anotherAccountLabel); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + accountName: anotherAccountLabel, + }); await headerNavbar.check_accountLabel(anotherAccountLabel); // Switch back to the first account and verify first custom account persists diff --git a/test/e2e/tests/account/add-account.spec.ts b/test/e2e/tests/account/add-account.spec.ts index db62927b91d7..2df140899212 100644 --- a/test/e2e/tests/account/add-account.spec.ts +++ b/test/e2e/tests/account/add-account.spec.ts @@ -1,5 +1,6 @@ import { E2E_SRP } from '../../default-fixture'; import FixtureBuilder from '../../fixture-builder'; +import { ACCOUNT_TYPE } from '../../constants'; import { WALLET_PASSWORD, defaultGanacheOptions, @@ -43,7 +44,9 @@ describe('Add account', function () { const newAccountName = 'Account 2'; const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); await headerNavbar.check_accountLabel(newAccountName); await homePage.check_expectedBalanceIsDisplayed(); @@ -112,7 +115,9 @@ describe('Add account', function () { const newAccountName = 'Account 2'; const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); await headerNavbar.check_accountLabel(newAccountName); await homePage.check_expectedBalanceIsDisplayed(); @@ -177,7 +182,9 @@ describe('Add account', function () { // Create new account with default name Account 2 const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); await headerNavbar.check_accountLabel('Account 2'); await homePage.check_expectedBalanceIsDisplayed(); diff --git a/test/e2e/tests/account/import-flow.spec.ts b/test/e2e/tests/account/import-flow.spec.ts index dbb7b2f85d49..c8ffcc334e49 100644 --- a/test/e2e/tests/account/import-flow.spec.ts +++ b/test/e2e/tests/account/import-flow.spec.ts @@ -3,6 +3,7 @@ import { DEFAULT_FIXTURE_ACCOUNT } from '../../constants'; import { withFixtures } from '../../helpers'; import FixtureBuilder from '../../fixture-builder'; import AccountListPage from '../../page-objects/pages/account-list-page'; +import AccountDetailsModal from '../../page-objects/pages/dialog/account-details-modal'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; import HomePage from '../../page-objects/pages/home/homepage'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; @@ -32,7 +33,9 @@ describe('Import flow @no-mmi', function () { const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); await accountListPage.openAccountDetailsModal('Account 1'); - await accountListPage.check_addressInAccountDetailsModal( + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + await accountDetailsModal.check_addressInAccountDetailsModal( DEFAULT_FIXTURE_ACCOUNT.toLowerCase(), ); }, diff --git a/test/e2e/tests/identity/account-syncing/new-user-sync.spec.ts b/test/e2e/tests/identity/account-syncing/new-user-sync.spec.ts index e9a82b128352..64a3ddbf5fd7 100644 --- a/test/e2e/tests/identity/account-syncing/new-user-sync.spec.ts +++ b/test/e2e/tests/identity/account-syncing/new-user-sync.spec.ts @@ -2,6 +2,7 @@ import { Mockttp } from 'mockttp'; import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; +import { ACCOUNT_TYPE } from '../../../constants'; import { mockIdentityServices } from '../mocks'; import { IDENTITY_TEAM_PASSWORD } from '../constants'; import { UserStorageMockttpController } from '../../../helpers/identity/user-storage/userStorageMockttpController'; @@ -64,7 +65,10 @@ describe('Account syncing - New User @no-mmi', function () { // Add a second account await accountListPage.openAccountOptionsMenu(); - await accountListPage.addNewAccount('My Second Account'); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + accountName: 'My Second Account', + }); // Set SRP to use for retreival const headerNavbar = new HeaderNavbar(driver); diff --git a/test/e2e/tests/identity/account-syncing/onboarding-with-opt-out.spec.ts b/test/e2e/tests/identity/account-syncing/onboarding-with-opt-out.spec.ts index 19908211181b..bd063c182baf 100644 --- a/test/e2e/tests/identity/account-syncing/onboarding-with-opt-out.spec.ts +++ b/test/e2e/tests/identity/account-syncing/onboarding-with-opt-out.spec.ts @@ -7,6 +7,7 @@ import { IDENTITY_TEAM_PASSWORD, IDENTITY_TEAM_SEED_PHRASE, } from '../constants'; +import { ACCOUNT_TYPE } from '../../../constants'; import { UserStorageMockttpController } from '../../../helpers/identity/user-storage/userStorageMockttpController'; import AccountListPage from '../../../page-objects/pages/account-list-page'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; @@ -135,7 +136,11 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { await accountListPage.check_accountDisplayedInAccountList( 'Account 1', ); - await accountListPage.addNewAccount('New Account'); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + accountName: 'New Account', + }); + // Set SRP to use for retreival const headerNavbar = new HeaderNavbar(driver); await headerNavbar.check_pageIsLoaded(); diff --git a/test/e2e/tests/identity/account-syncing/sync-after-adding-account.spec.ts b/test/e2e/tests/identity/account-syncing/sync-after-adding-account.spec.ts index 9c2fdb69c7e9..e02833e7c172 100644 --- a/test/e2e/tests/identity/account-syncing/sync-after-adding-account.spec.ts +++ b/test/e2e/tests/identity/account-syncing/sync-after-adding-account.spec.ts @@ -3,6 +3,7 @@ import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sd import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockIdentityServices } from '../mocks'; +import { ACCOUNT_TYPE } from '../../../constants'; import { IDENTITY_TEAM_PASSWORD, IDENTITY_TEAM_SEED_PHRASE, @@ -65,7 +66,10 @@ describe('Account syncing - Add Account @no-mmi', function () { await accountListPage.check_accountDisplayedInAccountList( 'My Second Synced Account', ); - await accountListPage.addNewAccount('My third account'); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + accountName: 'My third account', + }); }, ); @@ -164,7 +168,9 @@ describe('Account syncing - Add Account @no-mmi', function () { await accountListPage.check_accountDisplayedInAccountList( 'My Second Synced Account', ); - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); }, ); diff --git a/test/e2e/tests/identity/account-syncing/sync-after-modifying-account-name.spec.ts b/test/e2e/tests/identity/account-syncing/sync-after-modifying-account-name.spec.ts index 07f6e4848aba..ce10b3129d58 100644 --- a/test/e2e/tests/identity/account-syncing/sync-after-modifying-account-name.spec.ts +++ b/test/e2e/tests/identity/account-syncing/sync-after-modifying-account-name.spec.ts @@ -9,6 +9,7 @@ import { } from '../constants'; import { UserStorageMockttpController } from '../../../helpers/identity/user-storage/userStorageMockttpController'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import AccountDetailsModal from '../../../page-objects/pages/dialog/account-details-modal'; import AccountListPage from '../../../page-objects/pages/account-list-page'; import HomePage from '../../../page-objects/pages/home/homepage'; import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; @@ -65,8 +66,14 @@ describe('Account syncing - Rename Accounts @no-mmi', function () { await accountListPage.check_accountDisplayedInAccountList( 'My Second Synced Account', ); - await accountListPage.openAccountOptionsMenu(); - await accountListPage.changeAccountLabel('My Renamed First Account'); + await accountListPage.openAccountDetailsModal( + 'My First Synced Account', + ); + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + await accountDetailsModal.changeAccountLabel( + 'My Renamed First Account', + ); }, ); diff --git a/test/e2e/tests/identity/account-syncing/sync-with-account-balances.spec.ts b/test/e2e/tests/identity/account-syncing/sync-with-account-balances.spec.ts index 6fcc9e9cc0d0..ec52bb3124bd 100644 --- a/test/e2e/tests/identity/account-syncing/sync-with-account-balances.spec.ts +++ b/test/e2e/tests/identity/account-syncing/sync-with-account-balances.spec.ts @@ -6,8 +6,10 @@ import { IDENTITY_TEAM_PASSWORD, IDENTITY_TEAM_SEED_PHRASE, } from '../constants'; +import { ACCOUNT_TYPE } from '../../../constants'; import { UserStorageMockttpController } from '../../../helpers/identity/user-storage/userStorageMockttpController'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import AccountDetailsModal from '../../../page-objects/pages/dialog/account-details-modal'; import AccountListPage from '../../../page-objects/pages/account-list-page'; import HomePage from '../../../page-objects/pages/home/homepage'; import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; @@ -108,7 +110,9 @@ describe('Account syncing - User already has balances on multple accounts @no-mm } // Create new account and prepare for additional accounts - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); accountsToMock = [...INITIAL_ACCOUNTS, ...ADDITIONAL_ACCOUNTS]; }, ); @@ -158,11 +162,13 @@ describe('Account syncing - User already has balances on multple accounts @no-mm // Rename Account 6 to verify update to user storage await accountListPage.switchToAccount('Account 6'); + await header.check_accountLabel('Account 6'); await header.openAccountMenu(); + await accountListPage.check_pageIsLoaded(); await accountListPage.openAccountDetailsModal('Account 6'); - await accountListPage.changeLabelFromAccountDetailsModal( - 'My Renamed Account 6', - ); + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + await accountDetailsModal.changeAccountLabel('My Renamed Account 6'); }, ); diff --git a/test/e2e/tests/tokens/nft/import-nft.spec.ts b/test/e2e/tests/tokens/nft/import-nft.spec.ts index 5983d1002035..e151e83a355f 100644 --- a/test/e2e/tests/tokens/nft/import-nft.spec.ts +++ b/test/e2e/tests/tokens/nft/import-nft.spec.ts @@ -1,4 +1,5 @@ import { defaultGanacheOptions, withFixtures } from '../../../helpers'; +import { ACCOUNT_TYPE } from '../../../constants'; import { SMART_CONTRACTS } from '../../../seeder/smart-contracts'; import FixtureBuilder from '../../../fixture-builder'; import AccountListPage from '../../../page-objects/pages/account-list-page'; @@ -67,7 +68,9 @@ describe('Import NFT', function () { await headerNavbar.openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); await headerNavbar.check_accountLabel('Account 2'); await homepage.check_expectedBalanceIsDisplayed(); From 7be1b0d3b193764eef0489ba3c9df49eba01f2fa Mon Sep 17 00:00:00 2001 From: Priya Date: Fri, 20 Dec 2024 11:55:45 +0100 Subject: [PATCH 08/23] test: remove duplicate signature tests (#29377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removing duplicated tests that are already covered in test/e2e/tests/confirmations/signatures [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29377?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../e2e/tests/signature/personal-sign.spec.js | 37 ------------ .../tests/signature/signature-request.spec.js | 58 ------------------- 2 files changed, 95 deletions(-) diff --git a/test/e2e/tests/signature/personal-sign.spec.js b/test/e2e/tests/signature/personal-sign.spec.js index 092a9518ba01..908a422bfea0 100644 --- a/test/e2e/tests/signature/personal-sign.spec.js +++ b/test/e2e/tests/signature/personal-sign.spec.js @@ -116,43 +116,6 @@ describe('Personal sign', function () { }); describe('Redesigned confirmation screens', function () { - it('can initiate and confirm a personal sign', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver, ganacheServer }) => { - const addresses = await ganacheServer.getAccounts(); - const publicAddress = addresses[0]; - await unlockWallet(driver); - - await openDapp(driver); - await driver.clickElement('#personalSign'); - - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - - await driver.findElement({ - css: 'p', - text: 'Example `personal_sign` message', - }); - - await driver.clickElement('[data-testid="confirm-footer-button"]'); - - await verifyAndAssertPersonalMessage(driver, publicAddress); - }, - ); - }); - it('can queue multiple personal signs and confirm', async function () { await withFixtures( { diff --git a/test/e2e/tests/signature/signature-request.spec.js b/test/e2e/tests/signature/signature-request.spec.js index 716ee1f98d95..7d37da3de0db 100644 --- a/test/e2e/tests/signature/signature-request.spec.js +++ b/test/e2e/tests/signature/signature-request.spec.js @@ -337,64 +337,6 @@ describe('Sign Typed Data Signature Request', function () { }); describe('Redesigned confirmation screens', function () { - testData.forEach((data) => { - it(`can initiate and confirm a Signature Request of ${data.type}`, async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver, ganacheServer }) => { - const addresses = await ganacheServer.getAccounts(); - const publicAddress = addresses[0]; - await unlockWallet(driver); - - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement(data.buttonId); - - await driver.waitUntilXWindowHandles(3); - let windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - - await verifyAndAssertRedesignedSignTypedData( - driver, - data.expectedMessage, - ); - - // Approve signing typed data - await finalizeSignatureRequest( - driver, - '.confirm-scroll-to-bottom__button', - 'Confirm', - ); - await driver.waitUntilXWindowHandles(2); - windowHandles = await driver.getAllWindowHandles(); - - // switch to the Dapp and verify the signed address - await driver.switchToWindowWithTitle( - 'E2E Test Dapp', - windowHandles, - ); - await driver.clickElement(data.verifyId); - const recoveredAddress = await driver.findElement( - data.verifyResultId, - ); - - assert.equal(await recoveredAddress.getText(), publicAddress); - }, - ); - }); - }); - testData.forEach((data) => { it(`can queue multiple Signature Requests of ${data.type} and confirm`, async function () { await withFixtures( From 8a6f4f9198e19356ce545fb4d2561d86daabb6f5 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari Date: Fri, 20 Dec 2024 10:59:13 +0000 Subject: [PATCH 09/23] fix: fixed truncation issue for long help text (#29269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR aims to fix the truncation issue in send flow. The decimal value in error message used to show all the decimal numbers after decimal. This PR updates the max value to be shown after decimals to be 4 which is also the standard decimal value we try to show in other places of code ## **Related issues** Fixes: #26766 ## **Manual testing steps** 1. Go to Send Flow 2. Try sending a token with large input than the available balance 3. Check the value in helptext doesn't overlap and only shows 4 numbers ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-12-17 at 11 17 19 AM](https://github.com/user-attachments/assets/8f9e39ba-60a6-4cd9-88f3-c7e56aa14e1c) ### **After** ![Screenshot 2024-12-17 at 11 18 05 AM](https://github.com/user-attachments/assets/08a6913e-13a1-40ad-bd48-ae8528412287) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../asset-balance/asset-balance-text.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx b/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx index 3aec458341bd..29cbf7e40619 100644 --- a/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx @@ -48,6 +48,10 @@ export function AssetBalanceText({ const balanceString = hexToDecimal(asset.balance) || tokensWithBalances[0]?.string; + const showFixedBalanceString = balanceString?.includes('.') + ? balanceString.slice(0, balanceString.indexOf('.') + 5) // Include 4 digits after the decimal + : balanceString; + const balanceValue = useSelector(getSelectedAccountCachedBalance); const nativeTokenFiatBalance = useCurrencyDisplay(balanceValue, { @@ -135,7 +139,7 @@ export function AssetBalanceText({ return ( ); } From 3fd27a9e9a170da753963ddc751adc9cf585596d Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:12:48 +0000 Subject: [PATCH 10/23] fix: `gasFeeEstimates` property undefined (#29312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes an issue created by Sentry where the property `gasFeeEstimates` is `undefined`. Added an early return when the property is `undefined` and covered the scenario with unit tests. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29312?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27501 https://github.com/MetaMask/metamask-extension/issues/27241 ## **Manual testing steps** 1. Go to this test dapp 2. Block requests against gas-api 3. Start a send or contract interaction 4. Verify the console ## **Screenshots/Recordings** [block-gas-api.webm](https://github.com/user-attachments/assets/1c3cf638-905e-4df9-b875-84f0056979b1) ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../hooks/useTransactionFunction.test.js | 12 ++++++++++++ .../confirmations/hooks/useTransactionFunctions.js | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/ui/pages/confirmations/hooks/useTransactionFunction.test.js b/ui/pages/confirmations/hooks/useTransactionFunction.test.js index 22984ba71a37..ec823d2acf70 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunction.test.js +++ b/ui/pages/confirmations/hooks/useTransactionFunction.test.js @@ -164,4 +164,16 @@ describe('useMaxPriorityFeePerGasInput', () => { userFeeLevel: 'dappSuggested', }); }); + + it('returns early when gasFeeEstimates is undefined', () => { + const mockUpdateTransaction = jest + .spyOn(Actions, 'updateTransactionGasFees') + .mockImplementation(() => ({ type: '' })); + + const { result } = renderUseTransactionFunctions({ + gasFeeEstimates: undefined, + }); + result.current.updateTransactionUsingEstimate(GasRecommendations.low); + expect(mockUpdateTransaction).not.toHaveBeenCalled(); + }); }); diff --git a/ui/pages/confirmations/hooks/useTransactionFunctions.js b/ui/pages/confirmations/hooks/useTransactionFunctions.js index 17b5165da94e..96b41cd5e08e 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunctions.js +++ b/ui/pages/confirmations/hooks/useTransactionFunctions.js @@ -201,9 +201,10 @@ export const useTransactionFunctions = ({ const updateTransactionUsingEstimate = useCallback( (gasFeeEstimateToUse) => { - if (!gasFeeEstimates[gasFeeEstimateToUse]) { + if (!gasFeeEstimates?.[gasFeeEstimateToUse]) { return; } + const { suggestedMaxFeePerGas, suggestedMaxPriorityFeePerGas } = gasFeeEstimates[gasFeeEstimateToUse]; updateTransaction({ From 02230c725bcc28b327df9b81c2198e74c9e875ea Mon Sep 17 00:00:00 2001 From: Salim TOUBAL Date: Fri, 20 Dec 2024 12:22:10 +0100 Subject: [PATCH 11/23] fix: remove Text in the Activity Empty State (#29318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR updates the Activity empty state by removing the placeholder text. The change simplifies the UI, ensuring a cleaner and more visually appealing experience when no activity is present. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29318?quickstart=1) ## **Related issues** Fixes: #26669 ## **Manual testing steps** 1. Go to activity tab for any network where you don't have transactions 2. check the message ## **Screenshots/Recordings** ### **Before** Screenshot 2024-12-18 at 13 35 07 ### **After** Screenshot 2024-12-18 at 13 35 12 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/am/messages.json | 3 - app/_locales/ar/messages.json | 3 - app/_locales/bg/messages.json | 3 - app/_locales/bn/messages.json | 3 - app/_locales/ca/messages.json | 3 - app/_locales/cs/messages.json | 3 - app/_locales/da/messages.json | 3 - app/_locales/de/messages.json | 3 - app/_locales/el/messages.json | 3 - app/_locales/en/messages.json | 10 -- app/_locales/en_GB/messages.json | 3 - app/_locales/es/messages.json | 3 - app/_locales/es_419/messages.json | 3 - app/_locales/et/messages.json | 3 - app/_locales/fa/messages.json | 3 - app/_locales/fi/messages.json | 3 - app/_locales/fil/messages.json | 3 - app/_locales/fr/messages.json | 3 - app/_locales/he/messages.json | 3 - app/_locales/hi/messages.json | 3 - app/_locales/hn/messages.json | 3 - app/_locales/hr/messages.json | 3 - app/_locales/ht/messages.json | 3 - app/_locales/hu/messages.json | 3 - app/_locales/id/messages.json | 3 - app/_locales/it/messages.json | 3 - app/_locales/ja/messages.json | 3 - app/_locales/kn/messages.json | 3 - app/_locales/ko/messages.json | 3 - app/_locales/lt/messages.json | 3 - app/_locales/lv/messages.json | 3 - app/_locales/ms/messages.json | 3 - app/_locales/nl/messages.json | 3 - app/_locales/no/messages.json | 3 - app/_locales/ph/messages.json | 3 - app/_locales/pl/messages.json | 3 - app/_locales/pt/messages.json | 3 - app/_locales/pt_BR/messages.json | 3 - app/_locales/ro/messages.json | 3 - app/_locales/ru/messages.json | 3 - app/_locales/sk/messages.json | 3 - app/_locales/sl/messages.json | 3 - app/_locales/sr/messages.json | 3 - app/_locales/sv/messages.json | 3 - app/_locales/sw/messages.json | 3 - app/_locales/ta/messages.json | 3 - app/_locales/th/messages.json | 3 - app/_locales/tl/messages.json | 3 - app/_locales/tr/messages.json | 3 - app/_locales/uk/messages.json | 3 - app/_locales/vi/messages.json | 3 - app/_locales/zh_CN/messages.json | 3 - app/_locales/zh_TW/messages.json | 3 - .../transaction/multiple-transactions.spec.js | 12 --- .../transaction-list.component.js | 97 ++++++++----------- .../transaction-list/transaction-list.test.js | 22 +++-- .../__snapshots__/asset-page.test.tsx.snap | 36 +------ ui/pages/asset/components/asset-page.tsx | 4 +- 58 files changed, 55 insertions(+), 282 deletions(-) diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index ccb81d489af7..fac85861828c 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -428,9 +428,6 @@ "noConversionRateAvailable": { "message": "ምንም የልወጣ ተመን አይገኝም" }, - "noTransactions": { - "message": "ግብይቶች የሉዎትም" - }, "noWebcamFound": { "message": "የኮምፒዩተርዎ ካሜራ አልተገኘም። እባክዎ እንደገና ይሞክሩ።" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 00685f39df87..33585243d016 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -444,9 +444,6 @@ "noConversionRateAvailable": { "message": "لا يوجد معدل تحويل متاح" }, - "noTransactions": { - "message": "لا توجد لديك معاملات" - }, "noWebcamFound": { "message": "لم يتم العثور على كاميرا ويب للكمبيوتر الخاص بك. حاول مرة اخرى." }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 2e51cf4b24b4..a6dbac690242 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "Няма наличен процент на преобръщане" }, - "noTransactions": { - "message": "Нямате транзакции" - }, "noWebcamFound": { "message": "Уеб камерата на компютърa Ви не беше намерена. Моля, опитайте отново." }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 6f3bb290215d..1df74dc0e941 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -437,9 +437,6 @@ "noConversionRateAvailable": { "message": "কোনো বিনিময় হার উপলভ্য নয়" }, - "noTransactions": { - "message": "আপনার কোনো লেনদেন নেই" - }, "noWebcamFound": { "message": "আপনার কম্পিউটারের ওয়েবক্যাম খুঁজে পাওয়া যায়নি। অনুগ্রহ করে আবার চেষ্টা করুন।" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index c54e236d8a21..b988ae488143 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -431,9 +431,6 @@ "noConversionRateAvailable": { "message": "No hi ha cap tarifa de conversió disponible" }, - "noTransactions": { - "message": "No tens transaccions" - }, "noWebcamFound": { "message": "No s'ha trovat la webcam del teu ordinador. Si us plau prova de nou." }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index b6161b00a979..adf67dbda77d 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -210,9 +210,6 @@ "next": { "message": "Další" }, - "noTransactions": { - "message": "Žádné transakce" - }, "passwordNotLongEnough": { "message": "Heslo není dost dlouhé" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index e9c884f28dbb..080dd75f22d6 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -431,9 +431,6 @@ "noConversionRateAvailable": { "message": "Ingen tilgængelig omregningskurs" }, - "noTransactions": { - "message": "Du har ingen transaktioner" - }, "noWebcamFound": { "message": "Din computers webkamera blev ikke fundet. Prøv venligst igen." }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 04c45bd81348..9ad22ea297e0 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Nein, danke!" }, - "noTransactions": { - "message": "Keine Transaktionen" - }, "noWebcamFound": { "message": "Die Webcam Ihres Computers wurde nicht gefunden. Bitte versuchen Sie es erneut." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 24da4df88460..aa5a819d532b 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Όχι, ευχαριστώ" }, - "noTransactions": { - "message": "Δεν έχετε καμιά συναλλαγή" - }, "noWebcamFound": { "message": "Η κάμερα του υπολογιστή σας δεν βρέθηκε. Προσπαθήστε ξανά." }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 2d0f8baa4158..e8d8fe2779f9 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3498,16 +3498,6 @@ "noThanks": { "message": "No thanks" }, - "noTransactions": { - "message": "You have no transactions" - }, - "noTransactionsChainIdMismatch": { - "message": "Please switch network to view transactions" - }, - "noTransactionsNetworkName": { - "message": "Please switch to $1 network to view transactions", - "description": "$1 represents the network name" - }, "noWebcamFound": { "message": "Your computer's webcam was not found. Please try again." }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index d242dc7e88f8..f66111064a90 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -3141,9 +3141,6 @@ "noThanks": { "message": "No thanks" }, - "noTransactions": { - "message": "You have no transactions" - }, "noWebcamFound": { "message": "Your computer's webcam was not found. Please try again." }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 0f24f5bc5b1a..2c6fa0a29885 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "No, gracias" }, - "noTransactions": { - "message": "No tiene transacciones" - }, "noWebcamFound": { "message": "No se encontró la cámara web del equipo. Vuelva a intentarlo." }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 5d940a269091..4e82c875c760 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1273,9 +1273,6 @@ "noConversionRateAvailable": { "message": "No hay tasa de conversión disponible" }, - "noTransactions": { - "message": "No tiene transacciones" - }, "noWebcamFound": { "message": "No se encontró la cámara web del equipo. Vuelva a intentarlo." }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index c5f55c6d3327..a2a85b36d987 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -437,9 +437,6 @@ "noConversionRateAvailable": { "message": "Ühtegi vahetuskurssi pole saadaval" }, - "noTransactions": { - "message": "Teil ei ole tehinguid" - }, "noWebcamFound": { "message": "Teie arvuti veebikaamerat ei leitud. Proovige uuesti." }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index 8399ecb91aec..3ec5211c2dfb 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "هیچ نرخ تغییر موجود نمیباشد" }, - "noTransactions": { - "message": "شما هیچ معامله ندارید" - }, "noWebcamFound": { "message": "وب کم کمپیوتر تان پیدا نشد. لطفًا دوباره کوشش کنید." }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 5002c4eed2b3..c49ea583598d 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "Vaihtokurssi ei saatavilla" }, - "noTransactions": { - "message": "Sinulla ei ole tapahtumia" - }, "noWebcamFound": { "message": "Tietokoneesi verkkokameraa ei löytynyt. Ole hyvä ja yritä uudestaan." }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index abee8f9c0a5e..14f91bc5c16a 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -381,9 +381,6 @@ "noConversionRateAvailable": { "message": "Walang Presyo ng Palitan na Available" }, - "noTransactions": { - "message": "Wala kang mga transaksyon" - }, "noWebcamFound": { "message": "Hindi nakita ang webcam ng iyong computer. Pakisubukang muli." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index ce615bddd591..1c8436635797 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Non merci" }, - "noTransactions": { - "message": "Aucune transaction" - }, "noWebcamFound": { "message": "La caméra de votre ordinateur n’a pas été trouvée. Veuillez réessayer." }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 3bd6b6b67408..c9360ff612de 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -440,9 +440,6 @@ "noConversionRateAvailable": { "message": "אין שער המרה זמין" }, - "noTransactions": { - "message": "אין לך עסקאות" - }, "noWebcamFound": { "message": "מצלמת הרשת של מחשבך לא נמצאה. נא לנסות שוב." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 05488711d94f..8d115d214652 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "जी नहीं, धन्यवाद" }, - "noTransactions": { - "message": "आपके पास कोई ट्रांसेक्शन नहीं है" - }, "noWebcamFound": { "message": "आपके कंप्यूटर का वेबकैम नहीं मिला। कृपया फिर से कोशिश करें।" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index f6bb938c4886..cecc6f26385b 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -193,9 +193,6 @@ "next": { "message": "अगला" }, - "noTransactions": { - "message": "कोई लेन-देन नहीं" - }, "pastePrivateKey": { "message": "यहां अपनी निजी कुंजी स्ट्रिंग चिपकाएं:", "description": "For importing an account from a private key" diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index d1ca9bac8057..77b864172ae2 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -440,9 +440,6 @@ "noConversionRateAvailable": { "message": "Nijedan konverzijski tečaj nije dostupan" }, - "noTransactions": { - "message": "Nemate transkacija" - }, "noWebcamFound": { "message": "Mrežna kamera vašeg računala nije pronađena. Pokušajte ponovno." }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 8d0e7e703559..29df50803fa9 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -310,9 +310,6 @@ "noConversionRateAvailable": { "message": "Pa gen okenn Konvèsyon Disponib" }, - "noTransactions": { - "message": "Pa gen tranzaksyon" - }, "noWebcamFound": { "message": "Nou pakay jwenn webcam òdinatè ou. Tanpri eseye ankò." }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index ee8699a64545..9255c752b4a7 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -440,9 +440,6 @@ "noConversionRateAvailable": { "message": "Nincs elérhető átváltási díj" }, - "noTransactions": { - "message": "Nincsenek tranzakciói" - }, "noWebcamFound": { "message": "Nem található számítógéped webkamerája. Kérünk, próbáld újra." }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index c918a6cc0fb0..fb488c423c61 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Tidak, terima kasih" }, - "noTransactions": { - "message": "Anda tidak memiliki transaksi" - }, "noWebcamFound": { "message": "Webcam komputer Anda tidak ditemukan. Harap coba lagi." }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index da1da37952f6..a88d710a6f81 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1046,9 +1046,6 @@ "noConversionRateAvailable": { "message": "Tasso di conversione non disponibile" }, - "noTransactions": { - "message": "Nessuna Transazione" - }, "noWebcamFound": { "message": "La fotocamera del tuo computer non è stata trovata. Riprova." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index ead0da87b7b8..35c812fa53f0 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "結構です" }, - "noTransactions": { - "message": "トランザクションがありません" - }, "noWebcamFound": { "message": "お使いのコンピューターのWebカメラが見つかりませんでした。もう一度お試しください。" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index 805de3c78fc0..148a820f5bfa 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "ಯಾವುದೇ ಪರಿವರ್ತನೆ ದರ ಲಭ್ಯವಿಲ್ಲ" }, - "noTransactions": { - "message": "ನೀವು ಯಾವುದೇ ವಹಿವಾಟುಗಳನ್ನು ಹೊಂದಿಲ್ಲ" - }, "noWebcamFound": { "message": "ನಿಮ್ಮ ಕಂಪ್ಯೂಟರ್‌ನ ವೆಬ್‌ಕ್ಯಾಮ್ ಕಂಡುಬಂದಿಲ್ಲ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ." }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index f22491040b4f..158a8a333c15 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "괜찮습니다" }, - "noTransactions": { - "message": "트랜잭션이 없습니다." - }, "noWebcamFound": { "message": "컴퓨터의 웹캠을 찾을 수 없습니다. 다시 시도하세요." }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index cd034a303c4a..3e5586f39f50 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "Nėra keitimo kurso" }, - "noTransactions": { - "message": "Neturite jokių operacijų" - }, "noWebcamFound": { "message": "Jūsų kompiuterio vaizdo kamera nerasta. Bandykite dar kartą." }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index be5d43f4afd9..a1e166937385 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "Konversijas kurss nav pieejams" }, - "noTransactions": { - "message": "Jums nav neviena darījuma." - }, "noWebcamFound": { "message": "Jūsu datora tīmekļa kamera netika atrasta. Lūdzu, mēģiniet vēlreiz." }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 20161aaf34da..3605febbc7d7 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -430,9 +430,6 @@ "noConversionRateAvailable": { "message": "Tiada Kadar Penukaran yang Tersedia" }, - "noTransactions": { - "message": "Anda tiada transaksi" - }, "noWebcamFound": { "message": "Webcam komputer anda tidak dijumpai. Sila cuba semula." }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index 7ac22889df2d..acf66091845f 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -183,9 +183,6 @@ "next": { "message": "volgende" }, - "noTransactions": { - "message": "Geen transacties" - }, "pastePrivateKey": { "message": "Plak hier uw privésleutelstring:", "description": "For importing an account from a private key" diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 67948544fdbf..8459d3ef3a5f 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -431,9 +431,6 @@ "noConversionRateAvailable": { "message": "Ingen konverteringsrate tilgjengelig " }, - "noTransactions": { - "message": "Du har ingen transaksjoner" - }, "noWebcamFound": { "message": "Datamaskinens webkamera ble ikke funnet. Vennligst prøv igjen." }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index af995dbf5f5f..3dd9ae28d896 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -819,9 +819,6 @@ "noConversionRateAvailable": { "message": "Hindi Available ang Rate ng Conversion" }, - "noTransactions": { - "message": "Wala kang transaksyon" - }, "noWebcamFound": { "message": "Hindi nakita ang webcam ng iyong computer. Pakisubukan ulit." }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 5baeee9b8a00..f7f5111c9973 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -440,9 +440,6 @@ "noConversionRateAvailable": { "message": "Brak kursu waluty" }, - "noTransactions": { - "message": "Nie ma transakcji" - }, "noWebcamFound": { "message": "Twoja kamera nie została znaleziona. Spróbuj ponownie." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 50bf0a7d9996..e4fe18f9bab9 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Não, obrigado" }, - "noTransactions": { - "message": "Você não tem transações" - }, "noWebcamFound": { "message": "A webcam do seu computador não foi encontrada. Tente novamente." }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 3c61987263b7..3c9c30147276 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1273,9 +1273,6 @@ "noConversionRateAvailable": { "message": "Não há uma taxa de conversão disponível" }, - "noTransactions": { - "message": "Você não tem transações" - }, "noWebcamFound": { "message": "A webcam do seu computador não foi encontrada. Tente novamente." }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index f149a976cf1a..db440fcfd264 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -434,9 +434,6 @@ "noConversionRateAvailable": { "message": "Nici o rată de conversie disponibilă" }, - "noTransactions": { - "message": "Nu aveți tranzacții" - }, "noWebcamFound": { "message": "Webcam-ul computerului dvs. nu a fost găsit. Vă rugăm să încercați din nou." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index b1445befdd17..984223a22503 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Нет, спасибо" }, - "noTransactions": { - "message": "У вас нет транзакций" - }, "noWebcamFound": { "message": "Веб-камера вашего компьютера не найдена. Попробуйте еще раз." }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 1616af135d1a..a65bc68593f6 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -418,9 +418,6 @@ "noConversionRateAvailable": { "message": "Nie je k dispozícii žiadna sadzba konverzie" }, - "noTransactions": { - "message": "Žádné transakce" - }, "noWebcamFound": { "message": "Webová kamera vášho počítača sa nenašla. Skúste znova." }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index dd73bc8e373f..ebc303e90c5c 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -431,9 +431,6 @@ "noConversionRateAvailable": { "message": "Menjalni tečaj ni na voljo" }, - "noTransactions": { - "message": "Nimate transakcij" - }, "noWebcamFound": { "message": "Spletna kamera ni najdena. Poskusite znova kasneje." }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index c3986a0c0b98..1bbcd0754b92 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -434,9 +434,6 @@ "noConversionRateAvailable": { "message": "Nije dostupan kurs za konverziju" }, - "noTransactions": { - "message": "Nemate transakcije" - }, "noWebcamFound": { "message": "Nije pronađena veb kamera na vašem kompjuteru. Molimo vas pokušajte ponovo." }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index 278515e908eb..818f1eb498d6 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -431,9 +431,6 @@ "noConversionRateAvailable": { "message": "Ingen omräkningskurs tillgänglig" }, - "noTransactions": { - "message": "Du har inga överföringar" - }, "noWebcamFound": { "message": "Din dators webbkamera hittades inte. Vänligen försök igen." }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 7c4a52f733fa..c33abcb51a18 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -425,9 +425,6 @@ "noConversionRateAvailable": { "message": "Hakuna Kiwango cha Ubadilishaji" }, - "noTransactions": { - "message": "Huna miamala." - }, "noWebcamFound": { "message": "Kamera yako ya kumpyuta haikupatikana. Tafadhali jaribu tena." }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index c5c36501cbb9..c5905e4e0bee 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -253,9 +253,6 @@ "next": { "message": "அடுத்தது" }, - "noTransactions": { - "message": "பரிவர்த்தனைகள் இல்லை" - }, "off": { "message": "ஆஃப்" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index 08c9cb0df6dc..96166c6fb6ba 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -232,9 +232,6 @@ "next": { "message": "ถัดไป" }, - "noTransactions": { - "message": "ยังไม่มีรายการธุรกรรม" - }, "on": { "message": "เปิด" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 327be7838680..04650491ac24 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Salamat na lang" }, - "noTransactions": { - "message": "Wala kang transaksyon" - }, "noWebcamFound": { "message": "Hindi nakita ang webcam ng iyong computer. Pakisubukan ulit." }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 35ec9700e7d0..06cba108c1e0 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Hayır, istemiyorum" }, - "noTransactions": { - "message": "İşleminiz yok" - }, "noWebcamFound": { "message": "Bilgisayarınızın web kamerası bulunamadı. Lütfen tekrar deneyin." }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 36a181b03ab2..58787e9a2238 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "Немає доступного обмінного курсу" }, - "noTransactions": { - "message": "У вас немає транзакцій" - }, "noWebcamFound": { "message": "Веб-камеру комп’ютера не знайдено. Повторіть спробу." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 21b187998966..6f9b8264e82a 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Không, cảm ơn" }, - "noTransactions": { - "message": "Bạn không có giao dịch nào" - }, "noWebcamFound": { "message": "Không tìm thấy webcam trên máy tính của bạn. Vui lòng thử lại." }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 635753750343..2f011db8e804 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "不,谢谢" }, - "noTransactions": { - "message": "您没有任何交易" - }, "noWebcamFound": { "message": "未找到您电脑的网络摄像头。请重试。" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 87d8ebfb5520..7b4a425a03ad 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -812,9 +812,6 @@ "noConversionRateAvailable": { "message": "尚未有匯率比較值" }, - "noTransactions": { - "message": "尚未有交易" - }, "noWebcamFound": { "message": "無法搜尋到攝影鏡頭裝置。請再試一次" }, diff --git a/test/e2e/tests/transaction/multiple-transactions.spec.js b/test/e2e/tests/transaction/multiple-transactions.spec.js index b85937f5b6bd..72dbbe638e3b 100644 --- a/test/e2e/tests/transaction/multiple-transactions.spec.js +++ b/test/e2e/tests/transaction/multiple-transactions.spec.js @@ -122,12 +122,6 @@ describe('Multiple transactions', function () { '[data-testid="account-overview__activity-tab"]', ); - const isTransactionListEmpty = - await driver.isElementPresentAndVisible( - '.transaction-list__empty-text', - ); - assert.equal(isTransactionListEmpty, true); - // The previous isTransactionListEmpty wait already serves as the guard here for the assertElementNotPresent await driver.assertElementNotPresent( '.transaction-list__completed-transactions .activity-list-item', @@ -244,12 +238,6 @@ describe('Multiple transactions', function () { '[data-testid="account-overview__activity-tab"]', ); - const isTransactionListEmpty = - await driver.isElementPresentAndVisible( - '.transaction-list__empty-text', - ); - assert.equal(isTransactionListEmpty, true); - // The previous isTransactionListEmpty wait already serves as the guard here for the assertElementNotPresent await driver.assertElementNotPresent( '.transaction-list__completed-transactions .activity-list-item', diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index 27b0e1b5c60c..8ea78dee1948 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -13,10 +13,7 @@ import { nonceSortedCompletedTransactionsSelector, nonceSortedPendingTransactionsSelector, } from '../../../selectors/transactions'; -import { - getCurrentChainId, - getNetworkConfigurationsByChainId, -} from '../../../../shared/modules/selectors/networks'; +import { getCurrentChainId } from '../../../../shared/modules/selectors/networks'; import { getSelectedAccount, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -143,7 +140,6 @@ export default function TransactionList({ hideTokenTransactions, tokenAddress, boxProps, - tokenChainId, }) { const [limit, setLimit] = useState(PAGE_INCREMENT); const t = useI18nContext(); @@ -156,16 +152,7 @@ export default function TransactionList({ ); const chainId = useSelector(getCurrentChainId); - const networkConfigurationsByChainId = useSelector( - getNetworkConfigurationsByChainId, - ); - const networkName = networkConfigurationsByChainId[tokenChainId]?.name; const selectedAccount = useSelector(getSelectedAccount); - const isChainIdMismatch = tokenChainId && tokenChainId !== chainId; - - const noTransactionsMessage = networkName - ? t('noTransactionsNetworkName', [networkName]) - : t('noTransactionsChainIdMismatch'); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const shouldHideZeroBalanceTokens = useSelector( @@ -358,51 +345,43 @@ export default function TransactionList({ )} - {completedTransactions.length > 0 ? ( - completedTransactions - .map(removeIncomingTxsButToAnotherAddress) - .map(removeTxGroupsWithNoTx) - .filter(dateGroupsWithTransactionGroups) - .slice(0, limit) - .map((dateGroup) => { - return dateGroup.transactionGroups.map( - (transactionGroup, index) => { - return ( - - {renderDateStamp(index, dateGroup)} - {transactionGroup.initialTransaction - ?.isSmartTransaction ? ( - - ) : ( - - )} - - ); - }, - ); - }) - ) : ( - - - {isChainIdMismatch - ? noTransactionsMessage - : t('noTransactions')} - - - )} + {completedTransactions.length > 0 + ? completedTransactions + .map(removeIncomingTxsButToAnotherAddress) + .map(removeTxGroupsWithNoTx) + .filter(dateGroupsWithTransactionGroups) + .slice(0, limit) + .map((dateGroup) => { + return dateGroup.transactionGroups.map( + (transactionGroup, index) => { + return ( + + {renderDateStamp(index, dateGroup)} + {transactionGroup.initialTransaction + ?.isSmartTransaction ? ( + + ) : ( + + )} + + ); + }, + ); + }) + : null} {completedTransactions.length > limit && ( ); }; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/banner.ts b/ui/components/app/snaps/snap-ui-renderer/components/banner.ts new file mode 100644 index 000000000000..fceb03d4be5d --- /dev/null +++ b/ui/components/app/snaps/snap-ui-renderer/components/banner.ts @@ -0,0 +1,20 @@ +import { BannerElement, JSXElement } from '@metamask/snaps-sdk/jsx'; +import { getJsxChildren } from '@metamask/snaps-utils'; +import { mapToTemplate } from '../utils'; +import { UIComponentFactory } from './types'; + +export const banner: UIComponentFactory = ({ + element, + ...params +}) => { + return { + element: 'SnapUIBanner', + children: getJsxChildren(element).map((children) => + mapToTemplate({ element: children as JSXElement, ...params }), + ), + props: { + title: element.props.title, + severity: element.props.severity, + }, + }; +}; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/button.ts b/ui/components/app/snaps/snap-ui-renderer/components/button.ts index f624ffb23195..4b0cdb808e79 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/button.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/button.ts @@ -2,6 +2,7 @@ import { ButtonElement, JSXElement } from '@metamask/snaps-sdk/jsx'; import { getJsxChildren } from '@metamask/snaps-utils'; import { NonEmptyArray } from '@metamask/utils'; import { mapTextToTemplate } from '../utils'; +import { TextVariant } from '../../../../../helpers/constants/design-system'; import { UIComponentFactory } from './types'; export const button: UIComponentFactory = ({ @@ -15,6 +16,9 @@ export const button: UIComponentFactory = ({ variant: element.props.variant, name: element.props.name, disabled: element.props.disabled, + loading: element.props.loading, + textVariant: + element.props.size === 'sm' ? TextVariant.bodySm : TextVariant.bodyMd, }, children: mapTextToTemplate( getJsxChildren(element) as NonEmptyArray, diff --git a/ui/components/app/snaps/snap-ui-renderer/components/index.ts b/ui/components/app/snaps/snap-ui-renderer/components/index.ts index 17a9b6aa37c1..f6173b7199b0 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/index.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/index.ts @@ -27,6 +27,7 @@ import { selector } from './selector'; import { icon } from './icon'; import { section } from './section'; import { avatar } from './avatar'; +import { banner } from './banner'; export const COMPONENT_MAPPING = { Box: box, @@ -58,4 +59,5 @@ export const COMPONENT_MAPPING = { Container: container, Selector: selector, Section: section, + Banner: banner, }; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/text.ts b/ui/components/app/snaps/snap-ui-renderer/components/text.ts index fe9194817ca3..96bb6c520e48 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/text.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/text.ts @@ -6,32 +6,45 @@ import { TextVariant, OverflowWrap, TextColor, + FontWeight, } from '../../../../../helpers/constants/design-system'; import { UIComponentFactory } from './types'; +function getTextColor(color: TextElement['props']['color']) { + switch (color) { + case 'default': + return TextColor.textDefault; + case 'alternative': + return TextColor.textAlternative; + case 'muted': + return TextColor.textMuted; + case 'error': + return TextColor.errorDefault; + case 'success': + return TextColor.successDefault; + case 'warning': + return TextColor.warningDefault; + default: + return TextColor.inherit; + } +} + +function getFontWeight(color: TextElement['props']['fontWeight']) { + switch (color) { + case 'bold': + return FontWeight.Bold; + case 'medium': + return FontWeight.Medium; + case 'regular': + default: + return FontWeight.Normal; + } +} + export const text: UIComponentFactory = ({ element, ...params }) => { - const getTextColor = () => { - switch (element.props.color) { - case 'default': - return TextColor.textDefault; - case 'alternative': - return TextColor.textAlternative; - case 'muted': - return TextColor.textMuted; - case 'error': - return TextColor.errorDefault; - case 'success': - return TextColor.successDefault; - case 'warning': - return TextColor.warningDefault; - default: - return TextColor.inherit; - } - }; - return { element: 'Text', children: mapTextToTemplate( @@ -41,8 +54,9 @@ export const text: UIComponentFactory = ({ props: { variant: element.props.size === 'sm' ? TextVariant.bodySm : TextVariant.bodyMd, + fontWeight: getFontWeight(element.props.fontWeight), overflowWrap: OverflowWrap.Anywhere, - color: getTextColor(), + color: getTextColor(element.props.color), className: 'snap-ui-renderer__text', textAlign: element.props.alignment, }, diff --git a/ui/components/app/tab-bar/index.scss b/ui/components/app/tab-bar/index.scss index ce149f922879..6efdbab6c51e 100644 --- a/ui/components/app/tab-bar/index.scss +++ b/ui/components/app/tab-bar/index.scss @@ -38,11 +38,16 @@ display: flex; align-items: center; position: relative; + overflow: hidden; width: 100%; &__title { @include design-system.H4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + @include design-system.screen-sm-min { @include design-system.H6; } diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index da12a52be812..ac21c32a2b1b 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -62,6 +62,9 @@ PATH_NAME_MAP[CONTACT_ADD_ROUTE] = 'Add Contact Settings Page'; export const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact'; PATH_NAME_MAP[`${CONTACT_VIEW_ROUTE}/:address`] = 'View Contact Settings Page'; +export const SNAP_SETTINGS_ROUTE = '/settings/snap'; +PATH_NAME_MAP[`${SNAP_SETTINGS_ROUTE}/:snapId`] = 'Snap Settings Page'; + export const REVEAL_SEED_ROUTE = '/seed'; PATH_NAME_MAP[REVEAL_SEED_ROUTE] = 'Reveal Secret Recovery Phrase Page'; diff --git a/ui/helpers/utils/snaps.js b/ui/helpers/utils/snaps.js deleted file mode 100644 index 8a57ce516cf2..000000000000 --- a/ui/helpers/utils/snaps.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Check if the given value is a valid snap ID. - * - * NOTE: This function is a duplicate oF a yet to be released version in @metamask/snaps-utils. - * - * @param value - The value to check. - * @returns `true` if the value is a valid snap ID, and `false` otherwise. - */ -export function isSnapId(value) { - return ( - (typeof value === 'string' || value instanceof String) && - (value.startsWith('local:') || value.startsWith('npm:')) - ); -} diff --git a/ui/helpers/utils/snaps.ts b/ui/helpers/utils/snaps.ts new file mode 100644 index 000000000000..c788393e83ab --- /dev/null +++ b/ui/helpers/utils/snaps.ts @@ -0,0 +1,40 @@ +import { SnapId } from '@metamask/snaps-sdk'; +import { isProduction } from '../../../shared/modules/environment'; + +/** + * Check if the given value is a valid snap ID. + * + * NOTE: This function is a duplicate oF a yet to be released version in @metamask/snaps-utils. + * + * @param value - The value to check. + * @returns `true` if the value is a valid snap ID, and `false` otherwise. + */ +export function isSnapId(value: unknown): value is SnapId { + return ( + (typeof value === 'string' || value instanceof String) && + (value.startsWith('local:') || value.startsWith('npm:')) + ); +} + +/** + * Decode a snap ID fron a pathname. + * + * @param pathname - The pathname to decode the snap ID from. + * @returns The decoded snap ID, or `undefined` if the snap ID could not be decoded. + */ +export const decodeSnapIdFromPathname = (pathname: string) => { + const snapIdURI = pathname?.match(/[^/]+$/u)?.[0]; + return snapIdURI && decodeURIComponent(snapIdURI); +}; + +const IGNORED_EXAMPLE_SNAPS = ['npm:@metamask/preinstalled-example-snap']; + +/** + * Check if the given snap ID is ignored in production. + * + * @param snapId - The snap ID to check. + * @returns `true` if the snap ID is ignored in production, and `false` otherwise. + */ +export const isSnapIgnoredInProd = (snapId: string) => { + return isProduction() ? IGNORED_EXAMPLE_SNAPS.includes(snapId) : false; +}; diff --git a/ui/hooks/snaps/useSnapSettings.ts b/ui/hooks/snaps/useSnapSettings.ts new file mode 100644 index 000000000000..ad2debc8cac2 --- /dev/null +++ b/ui/hooks/snaps/useSnapSettings.ts @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { + forceUpdateMetamaskState, + handleSnapRequest, +} from '../../store/actions'; + +export function useSnapSettings({ snapId }: { snapId?: string }) { + const dispatch = useDispatch(); + const [loading, setLoading] = useState(true); + const [data, setData] = useState<{ id: string } | undefined>(undefined); + const [error, setError] = useState(undefined); + + useEffect(() => { + let cancelled = false; + async function fetchPage(id: string) { + try { + setError(undefined); + setLoading(true); + + const newData = (await handleSnapRequest({ + snapId: id, + origin: '', + handler: 'onSettingsPage', + request: { + jsonrpc: '2.0', + method: ' ', + }, + })) as { id: string }; + if (!cancelled) { + setData(newData); + forceUpdateMetamaskState(dispatch); + } + } catch (err) { + if (!cancelled) { + setError(err as Error); + } + } finally { + if (!cancelled) { + setLoading(false); + } + } + } + + if (snapId) { + fetchPage(snapId); + } + + return () => { + cancelled = true; + }; + }, [snapId]); + + return { data, error, loading }; +} diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx index 3b5d1dfc3e62..336ef3f9f92f 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx @@ -149,7 +149,7 @@ describe('PersonalSignInfo', () => { getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(state); const { queryByText, getByText } = renderWithConfirmContextProvider( @@ -171,7 +171,7 @@ describe('PersonalSignInfo', () => { const state = getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(state); const { getByText, queryByText } = renderWithConfirmContextProvider( diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx index 2b1e6969ddd5..83696b69ac64 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx @@ -65,7 +65,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , @@ -88,7 +88,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(false); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(false); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx index 56421561ccd2..640b663e5cc1 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx @@ -153,7 +153,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , @@ -177,7 +177,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(false); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(false); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap index 12e984bc207a..656b1fc49740 100644 --- a/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap @@ -46,7 +46,7 @@ exports[`SnapsSection renders section for typed sign request 1`] = ` style="overflow-y: auto;" >

Hello world again!

@@ -104,7 +104,7 @@ exports[`SnapsSection renders section personal sign request 1`] = ` style="overflow-y: auto;" >

Hello world!

diff --git a/ui/pages/settings/index.scss b/ui/pages/settings/index.scss index f57e1c310998..f9b08529ee65 100644 --- a/ui/pages/settings/index.scss +++ b/ui/pages/settings/index.scss @@ -56,7 +56,7 @@ &__title { @include design-system.screen-sm-min { - width: 197px; + margin-right: 16px; } @include design-system.screen-sm-max { @@ -230,6 +230,7 @@ display: flex; flex-direction: column; flex: 1 1 auto; + max-width: 100vw; @include design-system.screen-sm-min { flex: 0 0 40%; diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js index 724c661c9aeb..37257e2c8fcb 100644 --- a/ui/pages/settings/settings.component.js +++ b/ui/pages/settings/settings.component.js @@ -21,6 +21,7 @@ import { ADD_POPULAR_CUSTOM_NETWORK, DEFAULT_ROUTE, NOTIFICATIONS_SETTINGS_ROUTE, + SNAP_SETTINGS_ROUTE, } from '../../helpers/constants/routes'; import { getSettingsRoutes } from '../../helpers/utils/settings-search'; @@ -31,6 +32,7 @@ import { IconName, Box, Text, + IconSize, } from '../../components/component-library'; import { AlignItems, @@ -44,6 +46,8 @@ import MetafoxLogo from '../../components/ui/metafox-logo'; // eslint-disable-next-line import/no-restricted-paths import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../shared/constants/app'; +import { SnapIcon } from '../../components/app/snaps/snap-icon'; +import { SnapSettingsRenderer } from '../../components/app/snaps/snap-settings-page'; import SettingsTab from './settings-tab'; import AdvancedTab from './advanced-tab'; import InfoTab from './info-tab'; @@ -70,6 +74,8 @@ class SettingsPage extends PureComponent { mostRecentOverviewPage: PropTypes.string.isRequired, pathnameI18nKey: PropTypes.string, remoteFeatureFlags: PropTypes.object.isRequired, + settingsPageSnaps: PropTypes.array, + snapSettingsTitle: PropTypes.string, toggleNetworkMenu: PropTypes.func.isRequired, useExternalServices: PropTypes.bool, }; @@ -210,19 +216,24 @@ class SettingsPage extends PureComponent { renderTitle() { const { t } = this.context; - const { isPopup, pathnameI18nKey, addressName } = this.props; + const { isPopup, pathnameI18nKey, addressName, snapSettingsTitle } = + this.props; let titleText; if (isPopup && addressName) { titleText = t('details'); } else if (pathnameI18nKey && isPopup) { titleText = t(pathnameI18nKey); + } else if (snapSettingsTitle) { + titleText = snapSettingsTitle; } else { titleText = t('settings'); } return (
- {titleText} + + {titleText} +
); } @@ -293,15 +304,31 @@ class SettingsPage extends PureComponent { } renderTabs() { - const { history, currentPath, useExternalServices } = this.props; + const { history, currentPath, useExternalServices, settingsPageSnaps } = + this.props; const { t } = this.context; + const snapsSettings = settingsPageSnaps.map(({ id, name }) => { + return { + content: name, + icon: ( + + ), + key: `${SNAP_SETTINGS_ROUTE}/${encodeURIComponent(id)}`, + }; + }); + const tabs = [ { content: t('general'), icon: , key: GENERAL_ROUTE, }, + ...snapsSettings, { content: t('advanced'), icon: , @@ -390,6 +417,10 @@ class SettingsPage extends PureComponent { )} /> + { metamask: { currencyRates }, } = state; const remoteFeatureFlags = getRemoteFeatureFlags(state); + + const settingsPageSnapsIds = getSettingsPageSnapsIds(state); + const snapsMetadata = getSnapsMetadata(state); const conversionDate = currencyRates[ticker]?.conversionDate; const pathNameTail = pathname.match(/[^/]+$/u)[0]; @@ -75,6 +83,7 @@ const mapStateToProps = (state, ownProps) => { const isAddPopularCustomNetwork = Boolean( pathname.match(ADD_POPULAR_CUSTOM_NETWORK), ); + const isSnapSettingsRoute = Boolean(pathname.match(SNAP_SETTINGS_ROUTE)); const isPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP; const pathnameI18nKey = ROUTES_TO_I18N_KEYS[pathname]; @@ -102,6 +111,16 @@ const mapStateToProps = (state, ownProps) => { ); const useExternalServices = getUseExternalServices(state); + const snapNameGetter = getSnapName(snapsMetadata); + + const settingsPageSnaps = settingsPageSnapsIds.map((snapId) => ({ + id: snapId, + name: snapNameGetter(snapId), + })); + + const snapSettingsTitle = + isSnapSettingsRoute && snapNameGetter(decodeSnapIdFromPathname(pathname)); + return { addNewNetwork, addressName, @@ -115,6 +134,8 @@ const mapStateToProps = (state, ownProps) => { mostRecentOverviewPage: getMostRecentOverviewPage(state), pathnameI18nKey, remoteFeatureFlags, + settingsPageSnaps, + snapSettingsTitle, useExternalServices, }; }; diff --git a/ui/pages/settings/settings.stories.js b/ui/pages/settings/settings.stories.js index 53437f4175db..b6b695a89cb1 100644 --- a/ui/pages/settings/settings.stories.js +++ b/ui/pages/settings/settings.stories.js @@ -62,6 +62,7 @@ const Settings = ({ history }) => { pathnameI18nKey={pathnameI18nKey} backRoute={SETTINGS_ROUTE} remoteFeatureFlags={{}} + settingsPageSnaps={[]} /> ); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 403db6cadb91..ce117af76262 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -13,6 +13,7 @@ import { NameType } from '@metamask/name-controller'; import { TransactionStatus } from '@metamask/transaction-controller'; import { isEvmAccountType } from '@metamask/keyring-api'; import { RpcEndpointType } from '@metamask/network-controller'; +import { SnapEndowments } from '@metamask/snaps-rpc-methods'; import { getCurrentChainId, getProviderConfig, @@ -111,6 +112,7 @@ import { BridgeFeatureFlagsKey } from '../../shared/types/bridge'; import { hasTransactionData } from '../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; +import { isSnapIgnoredInProd } from '../helpers/utils/snaps'; import { getAllUnapprovedTransactions, getCurrentNetworkTransactions, @@ -1918,6 +1920,19 @@ export const getInsightSnaps = createDeepEqualSelector( }, ); +export const getSettingsPageSnaps = createDeepEqualSelector( + getEnabledSnaps, + getPermissionSubjects, + (snaps, subjects) => { + return Object.values(snaps).filter( + ({ id, preinstalled }) => + subjects[id]?.permissions[SnapEndowments.SettingsPage] && + preinstalled && + !isSnapIgnoredInProd(id), + ); + }, +); + export const getSignatureInsightSnaps = createDeepEqualSelector( getEnabledSnaps, getPermissionSubjects, @@ -1948,6 +1963,11 @@ export const getNameLookupSnapsIds = createDeepEqualSelector( }, ); +export const getSettingsPageSnapsIds = createDeepEqualSelector( + getSettingsPageSnaps, + (snaps) => snaps.map((snap) => snap.id), +); + export const getNotifySnaps = createDeepEqualSelector( getEnabledSnaps, getPermissionSubjects, diff --git a/yarn.lock b/yarn.lock index 530e073f615d..e00947e58386 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6063,12 +6063,12 @@ __metadata: languageName: node linkType: hard -"@metamask/preinstalled-example-snap@npm:^0.2.0": - version: 0.2.0 - resolution: "@metamask/preinstalled-example-snap@npm:0.2.0" +"@metamask/preinstalled-example-snap@npm:^0.3.0": + version: 0.3.0 + resolution: "@metamask/preinstalled-example-snap@npm:0.3.0" dependencies: - "@metamask/snaps-sdk": "npm:^6.9.0" - checksum: 10/f8ad6f42c9bd7ce3b7fc9b45eecda6191320ff762b48c482ba4944a6d7a228682b833c15e56058f26ac7bb10417dfe9de340af1c8eb9bbe5dc03c665426ccb13 + "@metamask/snaps-sdk": "npm:^6.14.0" + checksum: 10/add8f89c1b7327bc90486d868a9d4b7eff426ef98a5a96235fc6fdce4710c6d17842636ccd02db6638d061ce2b16939c6fe1f06e69cdde8bde2f6026c7b82df5 languageName: node linkType: hard @@ -6265,9 +6265,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.15.0": - version: 9.15.0 - resolution: "@metamask/snaps-controllers@npm:9.15.0" +"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.16.0": + version: 9.16.0 + resolution: "@metamask/snaps-controllers@npm:9.16.0" dependencies: "@metamask/approval-controller": "npm:^7.1.1" "@metamask/base-controller": "npm:^7.0.2" @@ -6280,9 +6280,9 @@ __metadata: "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/snaps-registry": "npm:^3.2.2" - "@metamask/snaps-rpc-methods": "npm:^11.7.0" - "@metamask/snaps-sdk": "npm:^6.13.0" - "@metamask/snaps-utils": "npm:^8.6.1" + "@metamask/snaps-rpc-methods": "npm:^11.8.0" + "@metamask/snaps-sdk": "npm:^6.14.0" + "@metamask/snaps-utils": "npm:^8.7.0" "@metamask/utils": "npm:^10.0.0" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" @@ -6296,30 +6296,30 @@ __metadata: semver: "npm:^7.5.4" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.10.0 + "@metamask/snaps-execution-environments": ^6.11.0 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/dd849398c4deefbca55b3b4a5e3fe885c45012cd8132bb83367d024c0c2dc99b13be036aa84049d5a1ba1431f9fd66897623f3961a34ebbbf70fe7bce4db322e + checksum: 10/f0a9efaad8fac2aa833edd5df6a4929a84de31f3e11457d407f39793c9ecd3b94eff543135729691b125c32f4290183375ae6416bc04e4aad31466517727af4f languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^6.10.0": - version: 6.10.0 - resolution: "@metamask/snaps-execution-environments@npm:6.10.0" +"@metamask/snaps-execution-environments@npm:^6.11.0": + version: 6.11.0 + resolution: "@metamask/snaps-execution-environments@npm:6.11.0" dependencies: "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/providers": "npm:^18.1.1" "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/snaps-sdk": "npm:^6.11.0" - "@metamask/snaps-utils": "npm:^8.6.0" + "@metamask/snaps-sdk": "npm:^6.14.0" + "@metamask/snaps-utils": "npm:^8.7.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" - checksum: 10/a881696ec942f268d7485869fcb8c6bc0c278319bbfaf7e5c6099e86278c7f59049595f00ecfc27511d0106b5ad2f7621f734c7b17f088b835e38e638d80db01 + checksum: 10/3fc46e1b1d7e11996ce8c3694738d1cdab9b5d6c129a45e691b98f0d753e044869c3d0471729cba9e120bb2ff7ebd8e9aa644e608792903e74eee61213509b08 languageName: node linkType: hard @@ -6335,38 +6335,38 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.7.0": - version: 11.7.0 - resolution: "@metamask/snaps-rpc-methods@npm:11.7.0" +"@metamask/snaps-rpc-methods@npm:^11.8.0": + version: 11.8.0 + resolution: "@metamask/snaps-rpc-methods@npm:11.8.0" dependencies: "@metamask/key-tree": "npm:^10.0.1" "@metamask/permission-controller": "npm:^11.0.3" "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/snaps-sdk": "npm:^6.13.0" - "@metamask/snaps-utils": "npm:^8.6.1" + "@metamask/snaps-sdk": "npm:^6.14.0" + "@metamask/snaps-utils": "npm:^8.7.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" "@noble/hashes": "npm:^1.3.1" - checksum: 10/92e4131d15b8dd68a29bd845e6c795aab8c3299048eaff2c3970db78a5eb476d8841f6a612b42e878812bb0757f2126287581f4e12259846851f02d6e6d836f5 + checksum: 10/a84c3648195efaeaeb021bd86c8a90e3f555236a8804cb2191778dddcf2acf7ea23ebc30f4670f6669dc881736c28f387b6ca2fc61050393411ee947a86cd47b languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.13.0": - version: 6.13.0 - resolution: "@metamask/snaps-sdk@npm:6.13.0" +"@metamask/snaps-sdk@npm:^6.14.0": + version: 6.14.0 + resolution: "@metamask/snaps-sdk@npm:6.14.0" dependencies: "@metamask/key-tree": "npm:^10.0.1" "@metamask/providers": "npm:^18.1.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" - checksum: 10/115c738cb140810856ded055ac92a538c011adbd6a5f32a4e1fde42dcbd162c7eac182aab904de6b65af99b9520995d768627bd7f460da11d0aa359700e05b04 + checksum: 10/dafe8618418c607c5d962bbcf675324651254631791a257898f4a939c58a8b2f56a743dcff534aa7889662d5fb1a4dd1048558f4b025404e0dcd507ff5a5e89a languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.6.0, @metamask/snaps-utils@npm:^8.6.1": - version: 8.6.1 - resolution: "@metamask/snaps-utils@npm:8.6.1" +"@metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.7.0": + version: 8.7.0 + resolution: "@metamask/snaps-utils@npm:8.7.0" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -6376,7 +6376,7 @@ __metadata: "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.2" - "@metamask/snaps-sdk": "npm:^6.13.0" + "@metamask/snaps-sdk": "npm:^6.14.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" "@noble/hashes": "npm:^1.3.1" @@ -6391,7 +6391,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/d58e276b2849662e764a4ce5f45a03df361a5c65e9c09814e41b723407dffbd3a215626f5d9a8c4705a0cad73b702e10d76201174b44f92fbc68fdf59fb24d5d + checksum: 10/0681878e29c010853b610ed99569044feaa37b4cc92bafdba28b1eec68694d7779833fb4262a05a4d18182c6931d258531ed628e422d7c96567b61d1b710a95d languageName: node linkType: hard @@ -26663,7 +26663,7 @@ __metadata: "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.36.0" "@metamask/preferences-controller": "npm:^15.0.1" - "@metamask/preinstalled-example-snap": "npm:^0.2.0" + "@metamask/preinstalled-example-snap": "npm:^0.3.0" "@metamask/profile-sync-controller": "npm:^3.1.1" "@metamask/providers": "npm:^18.2.0" "@metamask/queued-request-controller": "npm:^7.0.1" @@ -26675,11 +26675,11 @@ __metadata: "@metamask/selected-network-controller": "npm:^19.0.0" "@metamask/signature-controller": "npm:^23.1.0" "@metamask/smart-transactions-controller": "npm:^16.0.0" - "@metamask/snaps-controllers": "npm:^9.15.0" - "@metamask/snaps-execution-environments": "npm:^6.10.0" - "@metamask/snaps-rpc-methods": "npm:^11.7.0" - "@metamask/snaps-sdk": "npm:^6.13.0" - "@metamask/snaps-utils": "npm:^8.6.1" + "@metamask/snaps-controllers": "npm:^9.16.0" + "@metamask/snaps-execution-environments": "npm:^6.11.0" + "@metamask/snaps-rpc-methods": "npm:^11.8.0" + "@metamask/snaps-sdk": "npm:^6.14.0" + "@metamask/snaps-utils": "npm:^8.7.0" "@metamask/solana-wallet-snap": "npm:^1.0.4" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.13.0" From 36f084c904b97c5f34432e44b1434acc5dff5430 Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Fri, 20 Dec 2024 15:34:09 +0000 Subject: [PATCH 18/23] fix(28081): design tweak for network badge (#29324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR address feedback from design quality check to tweak board color and width for network badge. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29324?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28081 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** See PR for related changes ### **After** See PR for related changes ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: georgewrmarshall --- .../__snapshots__/token-cell.test.tsx.snap | 2 +- .../detected-token-details/detected-token-details.js | 1 + .../__snapshots__/account-list-item.test.js.snap | 4 ++-- .../account-list-item/account-list-item.js | 2 +- .../multichain/badge-status/badge-status.stories.tsx | 2 +- .../multichain/badge-status/badge-status.tsx | 2 +- .../connect-accounts-modal.test.tsx.snap | 2 +- .../__snapshots__/connected-site-menu.test.js.snap | 4 ++-- .../connected-site-menu/connected-site-menu.js | 4 ++-- .../multichain/connected-status/connected-status.tsx | 7 +++---- .../__snapshots__/connections.test.tsx.snap | 2 +- .../pages/send/__snapshots__/send.test.js.snap | 2 +- .../__snapshots__/your-accounts.test.tsx.snap | 12 ++++++------ .../__snapshots__/token-list-item.test.tsx.snap | 12 ++++++------ .../multichain/token-list-item/token-list-item.tsx | 5 ++--- .../__snapshots__/asset-page.test.tsx.snap | 6 +++--- .../__snapshots__/remove-snap-account.test.js.snap | 2 +- 17 files changed, 35 insertions(+), 36 deletions(-) diff --git a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap index 7fb51f212ebd..3ec0face8295 100644 --- a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap +++ b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.tsx.snap @@ -28,7 +28,7 @@ exports[`Token Cell should match snapshot 1`] = ` class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
network logo } marginRight={2} diff --git a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap index fb3bed6fd34b..cd0bb60f90e1 100644 --- a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap +++ b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap @@ -116,7 +116,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam style="bottom: -1px; right: 2px;" >
@@ -424,7 +424,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam style="bottom: -1px; right: 2px;" >
diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index 3ab048597f40..2c080c940d8a 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -62,7 +62,7 @@ import { getMultichainShouldShowFiat, } from '../../../selectors/multichain'; import { useMultichainAccountTotalFiatBalance } from '../../../hooks/useMultichainAccountTotalFiatBalance'; -import { ConnectedStatus } from '../connected-status/connected-status'; +import { ConnectedStatus } from '../connected-status'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { getCustodianIconForAddress } from '../../../selectors/institutional/selectors'; import { useTheme } from '../../../hooks/useTheme'; diff --git a/ui/components/multichain/badge-status/badge-status.stories.tsx b/ui/components/multichain/badge-status/badge-status.stories.tsx index b6cc485ef0e6..7122e10468a9 100644 --- a/ui/components/multichain/badge-status/badge-status.stories.tsx +++ b/ui/components/multichain/badge-status/badge-status.stories.tsx @@ -42,7 +42,7 @@ export const DefaultStory = Template.bind({}); export const NotConnectedStory = Template.bind({}); NotConnectedStory.args = { - badgeBackgroundColor: Color.borderMuted, + badgeBackgroundColor: BackgroundColor.iconAlternative, badgeBorderColor: BackgroundColor.backgroundDefault, }; diff --git a/ui/components/multichain/badge-status/badge-status.tsx b/ui/components/multichain/badge-status/badge-status.tsx index d4e3668bccd8..30e156779308 100644 --- a/ui/components/multichain/badge-status/badge-status.tsx +++ b/ui/components/multichain/badge-status/badge-status.tsx @@ -74,7 +74,7 @@ export const BadgeStatus: React.FC = ({ backgroundColor={badgeBackgroundColor} borderRadius={BorderRadius.full} borderColor={badgeBorderColor} - borderWidth={isConnectedAndNotActive ? 2 : 4} + borderWidth={2} /> } > diff --git a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap index b4a4836db2d6..8bcb788bb745 100644 --- a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap +++ b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap @@ -232,7 +232,7 @@ exports[`Connect More Accounts Modal should render correctly 1`] = ` style="bottom: -1px; right: 2px;" >
diff --git a/ui/components/multichain/connected-site-menu/__snapshots__/connected-site-menu.test.js.snap b/ui/components/multichain/connected-site-menu/__snapshots__/connected-site-menu.test.js.snap index fa6f6a0b202a..18b794d1e971 100644 --- a/ui/components/multichain/connected-site-menu/__snapshots__/connected-site-menu.test.js.snap +++ b/ui/components/multichain/connected-site-menu/__snapshots__/connected-site-menu.test.js.snap @@ -32,7 +32,7 @@ exports[`Connected Site Menu should render the site menu in connected state 1`] style="bottom: -1px; right: -4px; z-index: 1;" >
@@ -74,7 +74,7 @@ exports[`Connected Site Menu should render the site menu in not connected state style="bottom: -1px; right: -4px; z-index: 1;" >
diff --git a/ui/components/multichain/connected-site-menu/connected-site-menu.js b/ui/components/multichain/connected-site-menu/connected-site-menu.js index 4f532710288e..cbceafa7d99f 100644 --- a/ui/components/multichain/connected-site-menu/connected-site-menu.js +++ b/ui/components/multichain/connected-site-menu/connected-site-menu.js @@ -88,9 +88,9 @@ export const ConnectedSiteMenu = ({ borderColor={ isConnectedtoOtherAccountOrSnap ? BorderColor.successDefault - : BackgroundColor.backgroundDefault + : BorderColor.backgroundDefault } - borderWidth={isConnectedtoOtherAccountOrSnap ? 2 : 3} + borderWidth={2} /> } > diff --git a/ui/components/multichain/connected-status/connected-status.tsx b/ui/components/multichain/connected-status/connected-status.tsx index 83dc0c77ec45..8de3d7c4b38b 100644 --- a/ui/components/multichain/connected-status/connected-status.tsx +++ b/ui/components/multichain/connected-status/connected-status.tsx @@ -3,7 +3,6 @@ import { useSelector } from 'react-redux'; import { BackgroundColor, BorderColor, - Color, } from '../../../helpers/constants/design-system'; import { isAccountConnectedToCurrentTab } from '../../../selectors'; import { @@ -43,11 +42,11 @@ export const ConnectedStatus: React.FC = ({ status = STATUS_CONNECTED_TO_ANOTHER_ACCOUNT; } - let badgeBorderColor = BackgroundColor.backgroundDefault; // TODO: Replace it once border-color has this value. - let badgeBackgroundColor = Color.borderMuted; // //TODO: Replace it once Background color has this value. + let badgeBorderColor = BorderColor.backgroundDefault; // TODO: Replace it once border-color has this value. + let badgeBackgroundColor = BackgroundColor.iconAlternative; let tooltipText = t('statusNotConnected'); if (status === STATUS_CONNECTED) { - badgeBorderColor = BackgroundColor.backgroundDefault; + badgeBorderColor = BorderColor.backgroundDefault; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: type 'string' can't be used to index type '{}' badgeBackgroundColor = BackgroundColor.successDefault; diff --git a/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap b/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap index ad2dc490d7c0..9250404743fc 100644 --- a/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap +++ b/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap @@ -171,7 +171,7 @@ exports[`Connections Content should render correctly 1`] = ` style="bottom: -1px; right: 2px;" >
diff --git a/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap b/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap index 7b0605b7ea60..a23161205c8e 100644 --- a/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap +++ b/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap @@ -348,7 +348,7 @@ exports[`SendPage render and initialization should render correctly even when a style="bottom: -1px; right: 2px;" >
diff --git a/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap b/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap index 71431a330f94..8ffdb6565c93 100644 --- a/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap +++ b/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap @@ -122,7 +122,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
@@ -421,7 +421,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
@@ -720,7 +720,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
@@ -1028,7 +1028,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
@@ -1327,7 +1327,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
@@ -1639,7 +1639,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
diff --git a/ui/components/multichain/token-list-item/__snapshots__/token-list-item.test.tsx.snap b/ui/components/multichain/token-list-item/__snapshots__/token-list-item.test.tsx.snap index 2359a5d9a5a0..e0efd4cd3fe6 100644 --- a/ui/components/multichain/token-list-item/__snapshots__/token-list-item.test.tsx.snap +++ b/ui/components/multichain/token-list-item/__snapshots__/token-list-item.test.tsx.snap @@ -24,7 +24,7 @@ exports[`TokenListItem handles clicking staking opens tab 1`] = ` class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
network logo
?
@@ -191,7 +191,7 @@ exports[`TokenListItem should display warning scam modal fallback when safechain class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
?
@@ -270,7 +270,7 @@ exports[`TokenListItem should render correctly 1`] = ` class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
network logo
?
@@ -403,7 +403,7 @@ exports[`TokenListItem should render crypto balance with warning scam 1`] = ` class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
?
diff --git a/ui/components/multichain/token-list-item/token-list-item.tsx b/ui/components/multichain/token-list-item/token-list-item.tsx index 40b91a001f17..d5ce348e0297 100644 --- a/ui/components/multichain/token-list-item/token-list-item.tsx +++ b/ui/components/multichain/token-list-item/token-list-item.tsx @@ -40,7 +40,6 @@ import { } from '../../component-library'; import { getMetaMetricsId, - getTestNetworkBackgroundColor, getParticipateInMetaMetrics, getDataCollectionForMarketing, getMarketData, @@ -228,7 +227,6 @@ export const TokenListItem = ({ ); // Used for badge icon const allNetworks = useSelector(getNetworkConfigurationsByChainId); - const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor); return ( } diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap index 162f017afd0a..2caa7f6eb5bb 100644 --- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap +++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap @@ -198,7 +198,7 @@ exports[`AssetPage should render a native asset 1`] = ` class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
static-logo
static-logo
static-logo
From d510d5cab2e7e5265e9cf6580498a4a03ede660c Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Fri, 20 Dec 2024 15:45:29 +0000 Subject: [PATCH 19/23] feat(14507): improve error message for failed txn in activity details view (#29338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** When we encounter a failed transaction: - in activity list from home view, it renders as `failed`, hovering the status will render an error message - when clicking this activity, the popup view for txn details will render `failed` as well, with no hover effect - In the meanwhile, there's a new and more user friendly error banner showing for user when txn failed. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29338?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/14507 Figma: https://www.figma.com/design/ZzVQ6iu13C67K807Z1bg5I/Smart-Transactions-1.0?node-id=4296-25303&t=ff749RbiH6F4IUqk-0 ## **Manual testing steps** 1. Render extension and test dapp 2. Trigger a failed txn from test dapp 3. Check activity for that txn 4. Check activity details by clicking item from step 3 and validate the banner, as well as hover ## **Screenshots/Recordings** ### **Before** Screenshot 2024-12-18 at 18 25 45 ### **After** Screenshot 2024-12-19 at 01 54 39 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 3 + .../failing-contract.spec.js | 9 + .../transaction-list-item-details/index.scss | 7 +- ...transaction-list-item-details.component.js | 53 ++- ...action-list-item-details.component.test.js | 66 ++-- .../smart-transaction-list-item.component.js | 1 + .../transaction-list-item.component.js | 2 + .../transaction-status-label.test.js.snap | 10 +- .../transaction-status-label.js | 20 +- .../transaction-status-label.test.js | 370 +++++++----------- 10 files changed, 258 insertions(+), 283 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 30475784e64a..0ef2ec82406f 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -6407,6 +6407,9 @@ "transactionFailed": { "message": "Transaction Failed" }, + "transactionFailedBannerMessage": { + "message": "This transaction would have cost you extra fees, so we stopped it. Your money is still in your wallet." + }, "transactionFee": { "message": "Transaction fee" }, diff --git a/test/e2e/tests/dapp-interactions/failing-contract.spec.js b/test/e2e/tests/dapp-interactions/failing-contract.spec.js index c05938d668e0..c02a279adb3f 100644 --- a/test/e2e/tests/dapp-interactions/failing-contract.spec.js +++ b/test/e2e/tests/dapp-interactions/failing-contract.spec.js @@ -72,6 +72,15 @@ describe('Failing contract interaction ', function () { css: '.activity-list-item .transaction-status-label', text: 'Failed', }); + // inspect transaction details + await driver.clickElement({ + css: '.activity-list-item .transaction-status-label', + text: 'Failed', + }); + await driver.waitForSelector('.transaction-list-item-details'); + await driver.waitForSelector( + '[data-testid="transaction-list-item-details-banner-error-message"]', + ); }, ); }); diff --git a/ui/components/app/transaction-list-item-details/index.scss b/ui/components/app/transaction-list-item-details/index.scss index 13adb780fa4d..34d4af1ea82e 100644 --- a/ui/components/app/transaction-list-item-details/index.scss +++ b/ui/components/app/transaction-list-item-details/index.scss @@ -49,6 +49,8 @@ display: flex; flex-direction: column; align-items: flex-end; + height: 42px; + justify-content: space-between; .btn-link { font-size: 12px; @@ -62,9 +64,10 @@ } &__operations { - margin: 0 0 16px 16px; + margin: 0 16px 16px 16px; display: flex; - justify-content: flex-end; + justify-content: space-between; + align-items: center; } &__header { diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 226a2a9113c0..f614a4dbc250 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -13,9 +13,19 @@ import Tooltip from '../../ui/tooltip'; import CancelButton from '../cancel-button'; import Popover from '../../ui/popover'; import { Box } from '../../component-library/box'; +import { Text } from '../../component-library/text'; +import { + BannerAlert, + BannerAlertSeverity, +} from '../../component-library/banner-alert'; +import { + TextVariant, + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + IconColor, + ///: END:ONLY_INCLUDE_IF +} from '../../../helpers/constants/design-system'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) -import { Icon, IconName, Text } from '../../component-library'; -import { IconColor } from '../../../helpers/constants/design-system'; +import { Icon, IconName } from '../../component-library'; ///: END:ONLY_INCLUDE_IF import { SECOND } from '../../../../shared/constants/time'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; @@ -55,6 +65,7 @@ export default class TransactionListItemDetails extends PureComponent { recipientNickname: PropTypes.string, transactionStatus: PropTypes.func, isCustomNetwork: PropTypes.bool, + showErrorBanner: PropTypes.bool, history: PropTypes.object, blockExplorerLinkText: PropTypes.object, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -204,6 +215,7 @@ export default class TransactionListItemDetails extends PureComponent { onClose, recipientNickname, showCancel, + showErrorBanner, transactionStatus: TransactionStatus, blockExplorerLinkText, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -220,6 +232,17 @@ export default class TransactionListItemDetails extends PureComponent {
+ {showErrorBanner && ( + + + {t('transactionFailedBannerMessage')} + + + )}
{showSpeedUp && ( )} @@ -258,22 +281,18 @@ export default class TransactionListItemDetails extends PureComponent { data-testid="transaction-list-item-details-tx-status" >
{t('status')}
-
- -
+
-
- -
+
{ +const render = (overrideProps) => { const rpcPrefs = { blockExplorerUrl: 'https://customblockexplorer.com/', }; @@ -71,7 +71,7 @@ const render = async (overrideProps) => { senderAddress: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', tryReverseResolveAddress: jest.fn(), transactionGroup, - transactionStatus: () =>
, + transactionStatus: () =>
, blockExplorerLinkText, rpcPrefs, ...overrideProps, @@ -79,59 +79,57 @@ const render = async (overrideProps) => { const mockStore = configureMockStore([thunk])(mockState); - let result; - - await act( - async () => - (result = renderWithProvider( - , - mockStore, - )), + const result = renderWithProvider( + , + mockStore, ); return result; }; describe('TransactionListItemDetails Component', () => { - it('should render title with title prop', async () => { - const { queryByText } = await render(); + describe('matches snapshot', () => { + it('for non-error details', async () => { + const { queryByText, queryByTestId } = render(); + expect(queryByText('Test Transaction Details')).toBeInTheDocument(); + expect( + queryByTestId('transaction-list-item-details-banner-error-message'), + ).not.toBeInTheDocument(); + }); - await waitFor(() => { + it('for error details', async () => { + const { queryByText, queryByTestId } = render({ showErrorBanner: true }); expect(queryByText('Test Transaction Details')).toBeInTheDocument(); + expect( + queryByTestId('transaction-list-item-details-banner-error-message'), + ).toBeInTheDocument(); }); }); - describe('Retry button', () => { - it('should render retry button with showRetry prop', async () => { - const { queryByTestId } = await render({ showRetry: true }); - + describe('Action buttons', () => { + it('renders retry button with showRetry prop', async () => { + const { queryByTestId } = render({ showRetry: true }); expect(queryByTestId('rety-button')).toBeInTheDocument(); }); - }); - - describe('Cancel button', () => { - it('should render cancel button with showCancel prop', async () => { - const { queryByTestId } = await render({ showCancel: true }); + it('renders cancel button with showCancel prop', async () => { + const { queryByTestId } = render({ showCancel: true }); expect(queryByTestId('cancel-button')).toBeInTheDocument(); }); - }); - - describe('Speedup button', () => { - it('should render speedup button with showSpeedUp prop', async () => { - const { queryByTestId } = await render({ showSpeedUp: true }); + it('renders speedup button with showSpeedUp prop', async () => { + const { queryByTestId } = render({ showSpeedUp: true }); expect(queryByTestId('speedup-button')).toBeInTheDocument(); }); }); describe('Institutional', () => { - it('should render correctly if custodyTransactionDeepLink has a url', async () => { + it('renders correctly if custodyTransactionDeepLink has a url', async () => { mockGetCustodianTransactionDeepLink = jest .fn() .mockReturnValue({ url: 'https://url.com' }); - await render({ showCancel: true }); + render({ showCancel: true }); await waitFor(() => { const custodianViewButton = document.querySelector( @@ -143,7 +141,7 @@ describe('TransactionListItemDetails Component', () => { }); }); - it('should render correctly if transactionNote is provided', async () => { + it('renders correctly if transactionNote is provided', async () => { const newTransaction = { ...transaction, metadata: { @@ -159,13 +157,11 @@ describe('TransactionListItemDetails Component', () => { initialTransaction: newTransaction, }; - const { queryByText } = await render({ + const { queryByText } = render({ transactionGroup: newTransactionGroup, }); - await waitFor(() => { - expect(queryByText('some note')).toBeInTheDocument(); - }); + expect(queryByText('some note')).toBeInTheDocument(); }); }); }); diff --git a/ui/components/app/transaction-list-item/smart-transaction-list-item.component.js b/ui/components/app/transaction-list-item/smart-transaction-list-item.component.js index 5c1658918dbe..547f2e460497 100644 --- a/ui/components/app/transaction-list-item/smart-transaction-list-item.component.js +++ b/ui/components/app/transaction-list-item/smart-transaction-list-item.component.js @@ -125,6 +125,7 @@ export default function SmartTransactionListItem({ date={date} status={displayedStatusKey} statusOnly + shouldShowTooltip={false} /> )} /> diff --git a/ui/components/app/transaction-list-item/transaction-list-item.component.js b/ui/components/app/transaction-list-item/transaction-list-item.component.js index 09e8983b9196..5dc8cf48ef35 100644 --- a/ui/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/components/app/transaction-list-item/transaction-list-item.component.js @@ -459,6 +459,7 @@ function TransactionListItemInner({ !hasCancelled && !isBridgeTx } + showErrorBanner={Boolean(error)} transactionStatus={() => (
`; -exports[`TransactionStatusLabel Component should render PENDING properly 1`] = ` +exports[`TransactionStatusLabel Component renders PENDING properly and tooltip 1`] = `
`; -exports[`TransactionStatusLabel Component should render QUEUED properly 1`] = ` +exports[`TransactionStatusLabel Component renders QUEUED properly and tooltip 1`] = `
`; -exports[`TransactionStatusLabel Component should render SIGNING if status is approved 1`] = ` +exports[`TransactionStatusLabel Component renders SIGNING properly and tooltip 1`] = `
`; -exports[`TransactionStatusLabel Component should render UNAPPROVED properly 1`] = ` +exports[`TransactionStatusLabel Component renders UNAPPROVED properly and tooltip 1`] = `
{statusText} + ) : ( +
+ {statusText} +
); } @@ -120,8 +131,13 @@ TransactionStatusLabel.propTypes = { error: PropTypes.object, isEarliestNonce: PropTypes.bool, statusOnly: PropTypes.bool, + shouldShowTooltip: PropTypes.bool, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) custodyStatus: PropTypes.string, custodyStatusDisplayText: PropTypes.string, ///: END:ONLY_INCLUDE_IF }; + +TransactionStatusLabel.defaultProps = { + shouldShowTooltip: true, +}; diff --git a/ui/components/app/transaction-status-label/transaction-status-label.test.js b/ui/components/app/transaction-status-label/transaction-status-label.test.js index 4fa8e832201f..663cb45e802e 100644 --- a/ui/components/app/transaction-status-label/transaction-status-label.test.js +++ b/ui/components/app/transaction-status-label/transaction-status-label.test.js @@ -7,103 +7,160 @@ import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import TransactionStatusLabel from '.'; -describe('TransactionStatusLabel Component', () => { - const createMockStore = configureMockStore([thunk]); - const mockState = { - metamask: { - custodyStatusMaps: {}, - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Test Account', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, +const TEST_ACCOUNT_ID = 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3'; +const TEST_ACCOUNT_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; + +const createBasicAccount = (type = 'HD Key Tree') => ({ + address: TEST_ACCOUNT_ADDRESS, + id: TEST_ACCOUNT_ID, + metadata: { + name: 'Test Account', + keyring: { + type, }, - }; + }, + options: {}, + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, +}); - let store = createMockStore(mockState); - it('should render CONFIRMED properly', () => { - const confirmedProps = { - status: 'confirmed', - date: 'June 1', - }; +const createCustodyAccount = () => ({ + ...createBasicAccount('Custody - JSONRPC'), + metadata: { + name: 'Account 1', + keyring: { + type: 'Custody - JSONRPC', + }, + }, +}); - const { container } = renderWithProvider( - , - store, - ); +const createMockStateWithAccount = (account) => ({ + metamask: { + custodyStatusMaps: {}, + internalAccounts: { + accounts: { + [TEST_ACCOUNT_ID]: account, + }, + selectedAccount: TEST_ACCOUNT_ID, + }, + }, +}); - expect(container).toMatchSnapshot(); - }); +const createCustodyMockState = (account) => ({ + metamask: { + custodyStatusMaps: { + saturn: { + approved: { + shortText: 'Short Text Test', + longText: 'Long Text Test', + }, + }, + }, + internalAccounts: { + accounts: { + [TEST_ACCOUNT_ID]: account, + }, + selectedAccount: TEST_ACCOUNT_ID, + }, + keyrings: [ + { + type: 'Custody - JSONRPC', + accounts: [TEST_ACCOUNT_ADDRESS], + }, + ], + }, +}); - it('should render PENDING properly', () => { - const props = { +const statusTestCases = [ + { + name: 'CONFIRMED', + props: { status: 'confirmed', date: 'June 1' }, + }, + { + name: 'PENDING', + props: { date: 'June 1', status: TransactionStatus.submitted, isEarliestNonce: true, - }; - - const { container } = renderWithProvider( - , - store, - ); - - expect(container).toMatchSnapshot(); - }); - - it('should render QUEUED properly', () => { - const props = { + }, + }, + { + name: 'QUEUED', + props: { status: TransactionStatus.submitted, isEarliestNonce: false, - }; - - const { container } = renderWithProvider( - , - store, - ); - - expect(container).toMatchSnapshot(); - }); - - it('should render UNAPPROVED properly', () => { - const props = { + }, + }, + { + name: 'UNAPPROVED', + props: { status: TransactionStatus.unapproved, - }; + }, + }, + { + name: 'SIGNING', + props: { + status: TransactionStatus.approved, + }, + }, +]; - const { container } = renderWithProvider( - , - store, - ); +const errorTestCases = [ + { + name: 'error message', + props: { + status: 'approved', + custodyStatus: 'approved', + error: { message: 'An error occurred' }, + }, + expectedText: 'Error', + }, + { + name: 'error with aborted custody status', + props: { + status: 'approved', + custodyStatus: 'aborted', + error: { message: 'An error occurred' }, + custodyStatusDisplayText: 'Test', + shouldShowTooltip: true, + }, + expectedText: 'Test', + }, +]; + +describe('TransactionStatusLabel Component', () => { + const createMockStore = configureMockStore([thunk]); + let store; - expect(container).toMatchSnapshot(); + beforeEach(() => { + const basicAccount = createBasicAccount(); + const mockState = createMockStateWithAccount(basicAccount); + store = createMockStore(mockState); }); - it('should render SIGNING if status is approved', () => { - const props = { - status: TransactionStatus.approved, - }; + statusTestCases.forEach(({ name, props }) => { + it(`renders ${name} properly and tooltip`, () => { + const { container, queryByTestId } = renderWithProvider( + , + store, + ); + expect(container).toMatchSnapshot(); + expect(queryByTestId('transaction-status-label')).not.toBeInTheDocument(); + }); + }); - const { container } = renderWithProvider( - , + it('renders pure text for status when shouldShowTooltip is specified as false', () => { + const { queryByTestId } = renderWithProvider( + , store, ); - - expect(container).toMatchSnapshot(); + expect(queryByTestId('transaction-status-label')).toBeInTheDocument(); }); - it('should render statusText properly when is custodyStatusDisplayText is defined', () => { + it('renders statusText properly when is custodyStatusDisplayText is defined', () => { const props = { custodyStatusDisplayText: 'test', }; @@ -116,51 +173,15 @@ describe('TransactionStatusLabel Component', () => { expect(getByText(props.custodyStatusDisplayText)).toBeVisible(); }); - it('should display the correct status text and tooltip', () => { - const mockShortText = 'Short Text Test'; - const mockLongText = 'Long Text Test'; + it('displays correct text and tooltip', () => { const props = { status: 'approved', custodyStatus: 'approved', custodyStatusDisplayText: 'Test', }; - const customMockStore = { - metamask: { - custodyStatusMaps: { - saturn: { - approved: { - shortText: mockShortText, - longText: mockLongText, - }, - }, - }, - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 1', - keyring: { - type: 'Custody - JSONRPC', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - keyrings: [ - { - type: 'Custody - JSONRPC', - accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], - }, - ], - }, - }; + const custodyAccount = createCustodyAccount(); + const customMockStore = createCustodyMockState(custodyAccount); store = createMockStore(customMockStore); const { getByText } = renderWithProvider( @@ -170,114 +191,19 @@ describe('TransactionStatusLabel Component', () => { expect(getByText(props.custodyStatusDisplayText)).toBeVisible(); }); - it('should display the error message when there is an error', () => { - const mockShortText = 'Short Text Test'; - const mockLongText = 'Long Text Test'; - const props = { - status: 'approved', - custodyStatus: 'approved', - error: { message: 'An error occurred' }, - }; - const customMockStore = { - metamask: { - custodyStatusMaps: { - saturn: { - approved: { - shortText: mockShortText, - longText: mockLongText, - }, - }, - }, - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 1', - keyring: { - type: 'Custody - JSONRPC', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - keyrings: [ - { - type: 'Custody - JSONRPC', - accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], - }, - ], - }, - }; - store = createMockStore(customMockStore); - - const { getByText } = renderWithProvider( - , - store, - ); - - expect(getByText('Error')).toBeVisible(); - }); - - it('should display correctly the error message when there is an error and custodyStatus is aborted', () => { - const mockShortText = 'Short Text Test'; - const mockLongText = 'Long Text Test'; - const props = { - status: 'approved', - custodyStatus: 'aborted', - error: { message: 'An error occurred' }, - custodyStatusDisplayText: 'Test', - }; - const customMockStore = { - metamask: { - custodyStatusMaps: { - saturn: { - approved: { - shortText: mockShortText, - longText: mockLongText, - }, - }, - }, - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 1', - keyring: { - type: 'Custody - JSONRPC', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - keyrings: [ - { - type: 'Custody - JSONRPC', - accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], - }, - ], - }, - }; + errorTestCases.forEach(({ name, props, expectedText }) => { + it(`displays correctly the ${name}`, () => { + const custodyAccount = createCustodyAccount(); + const customMockStore = createCustodyMockState(custodyAccount); + store = createMockStore(customMockStore); - store = createMockStore(customMockStore); + const { getByText } = renderWithProvider( + , + store, + ); - const { getByText } = renderWithProvider( - , - store, - ); - - expect(getByText(props.custodyStatusDisplayText)).toBeVisible(); + expect(getByText(expectedText)).toBeVisible(); + }); }); }); From 695d0db025fff9d9b29d6ca2c03c42bfd58cf57e Mon Sep 17 00:00:00 2001 From: AugmentedMode <31675118+AugmentedMode@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:15:55 -0500 Subject: [PATCH 20/23] fix: Add main frame URL property to req object whenever req is triggered from an iframe (#29337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** See the attached issue in metamask planning for more details. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29337?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to `https://develop.d3bkcslj57l47p.amplifyapp.com/` 2. Click on Proceed anyways (This phishing warning page here is expected) 3. Open the network tab to monitor network requests 4. Connect your wallet and click on a signature or transaction 5. Verify that mainFrameOrigin is included in the payload of the network request to the security alerts API Screenshot 2024-12-20 at 10 46 05 AM ## **Screenshots/Recordings** Below are screenshots demonstrating the behavior of a test HTML page I created: 1. In the first screenshot, before the iframe is loaded, the console shows only the origin of the main frame. 2. In the second screenshot, after clicking the button to load an iframe pointing to example.com, the solution correctly identifies both the mainFrameOrigin (main frame) and the origin (iframe). Screenshot 2024-12-18 at 10 24 48 PM Screenshot 2024-12-18 at 10 24 54 PM ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../lib/createMainFrameOriginMiddleware.ts | 24 +++++++++++++++++++ app/scripts/metamask-controller.js | 22 ++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 app/scripts/lib/createMainFrameOriginMiddleware.ts diff --git a/app/scripts/lib/createMainFrameOriginMiddleware.ts b/app/scripts/lib/createMainFrameOriginMiddleware.ts new file mode 100644 index 000000000000..bcbc2cb7d6fd --- /dev/null +++ b/app/scripts/lib/createMainFrameOriginMiddleware.ts @@ -0,0 +1,24 @@ +// Request and responses are currently untyped. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Returns a middleware that appends the mainFrameOrigin to request + * + * @param {{ mainFrameOrigin: string }} opts - The middleware options + * @returns {Function} + */ + +export default function createMainFrameOriginMiddleware({ + mainFrameOrigin, +}: { + mainFrameOrigin: string; +}) { + return function mainFrameOriginMiddleware( + req: any, + _res: any, + next: () => void, + ) { + req.mainFrameOrigin = mainFrameOrigin; + next(); + }; +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 933522c449f6..62fe3c942589 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -305,6 +305,7 @@ import { createUnsupportedMethodMiddleware, } from './lib/rpc-method-middleware'; import createOriginMiddleware from './lib/createOriginMiddleware'; +import createMainFrameOriginMiddleware from './lib/createMainFrameOriginMiddleware'; import createTabIdMiddleware from './lib/createTabIdMiddleware'; import { NetworkOrderController } from './controllers/network-order'; import { AccountOrderController } from './controllers/account-order'; @@ -5804,11 +5805,18 @@ export default class MetamaskController extends EventEmitter { tabId = sender.tab.id; } + let mainFrameOrigin = origin; + if (sender.tab && sender.tab.url) { + // If sender origin is an iframe, then get the top-level frame's origin + mainFrameOrigin = new URL(sender.tab.url).origin; + } + const engine = this.setupProviderEngineEip1193({ origin, sender, subjectType, tabId, + mainFrameOrigin, }); const dupeReqFilterStream = createDupeReqFilterStream(); @@ -5929,13 +5937,25 @@ export default class MetamaskController extends EventEmitter { * @param {MessageSender | SnapSender} options.sender - The sender object. * @param {string} options.subjectType - The type of the sender subject. * @param {tabId} [options.tabId] - The tab ID of the sender - if the sender is within a tab + * @param {mainFrameOrigin} [options.mainFrameOrigin] - The origin of the main frame if the sender is an iframe */ - setupProviderEngineEip1193({ origin, subjectType, sender, tabId }) { + setupProviderEngineEip1193({ + origin, + subjectType, + sender, + tabId, + mainFrameOrigin, + }) { const engine = new JsonRpcEngine(); // Append origin to each request engine.push(createOriginMiddleware({ origin })); + // Append mainFrameOrigin to each request if present + if (mainFrameOrigin) { + engine.push(createMainFrameOriginMiddleware({ mainFrameOrigin })); + } + // Append selectedNetworkClientId to each request engine.push(createSelectedNetworkMiddleware(this.controllerMessenger)); From 547b264a3993aa4d40caad5d2993df2a1c7ca32e Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Fri, 20 Dec 2024 17:46:37 +0000 Subject: [PATCH 21/23] chore: Update to the latest transaction controller (#29395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updates from v42 to v42.1 in order to get the validation of the gas limit hexadecimal string properties. See https://github.com/MetaMask/core/pull/5093 for more details. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29395?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3826 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot --- lavamoat/browserify/beta/policy.json | 17 ++++++++++++++++- lavamoat/browserify/flask/policy.json | 17 ++++++++++++++++- lavamoat/browserify/main/policy.json | 17 ++++++++++++++++- lavamoat/browserify/mmi/policy.json | 17 ++++++++++++++++- package.json | 2 +- yarn.lock | 24 ++++++++++++------------ 6 files changed, 77 insertions(+), 17 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index ee84b5c3e0e8..e55d57f5ec0f 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1835,7 +1835,7 @@ "@metamask/network-controller": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, "browserify>buffer": true, @@ -2256,6 +2256,21 @@ "semver": true } }, + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@keystonehq/alias-sampling": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index ee84b5c3e0e8..e55d57f5ec0f 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1835,7 +1835,7 @@ "@metamask/network-controller": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, "browserify>buffer": true, @@ -2256,6 +2256,21 @@ "semver": true } }, + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@keystonehq/alias-sampling": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index ee84b5c3e0e8..e55d57f5ec0f 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1835,7 +1835,7 @@ "@metamask/network-controller": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, "browserify>buffer": true, @@ -2256,6 +2256,21 @@ "semver": true } }, + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@keystonehq/alias-sampling": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 831feb96e1c5..5658498ad3a7 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -1927,7 +1927,7 @@ "@metamask/network-controller": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, "browserify>buffer": true, @@ -2348,6 +2348,21 @@ "semver": true } }, + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@keystonehq/alias-sampling": true, diff --git a/package.json b/package.json index 4a7632d2d2d3..da93c6c75761 100644 --- a/package.json +++ b/package.json @@ -349,7 +349,7 @@ "@metamask/snaps-sdk": "^6.14.0", "@metamask/snaps-utils": "^8.7.0", "@metamask/solana-wallet-snap": "^1.0.4", - "@metamask/transaction-controller": "^42.0.0", + "@metamask/transaction-controller": "^42.1.0", "@metamask/user-operation-controller": "^21.0.0", "@metamask/utils": "^10.0.1", "@ngraveio/bc-ur": "^1.1.12", diff --git a/yarn.lock b/yarn.lock index e00947e58386..f45eb5cf233e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5095,13 +5095,13 @@ __metadata: languageName: node linkType: hard -"@metamask/base-controller@npm:^7.0.0, @metamask/base-controller@npm:^7.0.1, @metamask/base-controller@npm:^7.0.2": - version: 7.0.2 - resolution: "@metamask/base-controller@npm:7.0.2" +"@metamask/base-controller@npm:^7.0.0, @metamask/base-controller@npm:^7.0.1, @metamask/base-controller@npm:^7.0.2, @metamask/base-controller@npm:^7.1.0": + version: 7.1.0 + resolution: "@metamask/base-controller@npm:7.1.0" dependencies: "@metamask/utils": "npm:^10.0.0" immer: "npm:^9.0.6" - checksum: 10/6f78ec5af840c9947aa8eac6e402df6469600260d613a92196daefd5b072097a176fe5da1c386f2d36853513254b74140d667d817a12880c46f088e18ff3606a + checksum: 10/5a0b50c1e096cbf6483e308eddb3ca2e5e1865b803b5dba778bf635ec59657290895e21ada71c7508d8e34ff9695a192a414fd75e287d290346359ef8e23960a languageName: node linkType: hard @@ -6446,9 +6446,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^42.0.0": - version: 42.0.0 - resolution: "@metamask/transaction-controller@npm:42.0.0" +"@metamask/transaction-controller@npm:^42.1.0": + version: 42.1.0 + resolution: "@metamask/transaction-controller@npm:42.1.0" dependencies: "@ethereumjs/common": "npm:^3.2.0" "@ethereumjs/tx": "npm:^4.2.0" @@ -6456,13 +6456,13 @@ __metadata: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" - "@metamask/base-controller": "npm:^7.0.2" + "@metamask/base-controller": "npm:^7.1.0" "@metamask/controller-utils": "npm:^11.4.4" "@metamask/eth-query": "npm:^4.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/nonce-tracker": "npm:^6.0.0" - "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/utils": "npm:^10.0.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/utils": "npm:^11.0.1" async-mutex: "npm:^0.5.0" bn.js: "npm:^5.2.1" eth-method-registry: "npm:^4.0.0" @@ -6476,7 +6476,7 @@ __metadata: "@metamask/eth-block-tracker": ">=9" "@metamask/gas-fee-controller": ^22.0.0 "@metamask/network-controller": ^22.0.0 - checksum: 10/73c510803a720b4c1da0b82f1279a404a9b11c4ab76f8e5e4378c65d5d08bbb32c52062abfe319476cc3f5e2623a8987775c4524e55aa94002af73d73721b869 + checksum: 10/9f842e2b68e84cbffdda301a0e15faab08226fd8e22eb954690ed41df60fe92c24acffdd9186b4c9f1da911a368cbe22cdb9ee046fc02d079c53f76100c66755 languageName: node linkType: hard @@ -26683,7 +26683,7 @@ __metadata: "@metamask/solana-wallet-snap": "npm:^1.0.4" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.13.0" - "@metamask/transaction-controller": "npm:^42.0.0" + "@metamask/transaction-controller": "npm:^42.1.0" "@metamask/user-operation-controller": "npm:^21.0.0" "@metamask/utils": "npm:^10.0.1" "@ngraveio/bc-ur": "npm:^1.1.12" From a02799d97397fdfda492341566851203ae04027d Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 20 Dec 2024 14:49:06 -0330 Subject: [PATCH 22/23] ci: Migrate dependency linting (#29370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Migrate dependency/lockfile linting steps from CircleCI to GitHub actions. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29370?quickstart=1) ## **Related issues** Relates to #28572 These changes were extracted from #29256 ## **Manual testing steps** Review logs to ensure the same commands are run. Introduce errors on a branch from here to ensure the problems are caught. https://github.com/MetaMask/metamask-extension/pull/29391 ## **Screenshots/Recordings** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 43 ------------------------ .github/workflows/main.yml | 14 ++++++++ .github/workflows/test-deps-audit.yml | 18 ++++++++++ .github/workflows/test-deps-depcheck.yml | 18 ++++++++++ .github/workflows/test-yarn-dedupe.yml | 18 ++++++++++ 5 files changed, 68 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/test-deps-audit.yml create mode 100644 .github/workflows/test-deps-depcheck.yml create mode 100644 .github/workflows/test-yarn-dedupe.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 552aa3305509..5598e6450cfe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,15 +127,6 @@ workflows: - master requires: - prep-deps - - test-deps-audit: - requires: - - prep-deps - - test-deps-depcheck: - requires: - - prep-deps - - test-yarn-dedupe: - requires: - - prep-deps - validate-lavamoat-allow-scripts: requires: - prep-deps @@ -291,7 +282,6 @@ workflows: - prep-build-flask-mv2 - all-tests-pass: requires: - - test-deps-depcheck - validate-lavamoat-allow-scripts - validate-lavamoat-policy-build - validate-lavamoat-policy-webapp @@ -964,17 +954,6 @@ jobs: name: Rerun workflows from failed command: yarn ci-rerun-from-failed - test-yarn-dedupe: - executor: node-browsers-small - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: Detect yarn lock deduplications - command: yarn dedupe --check - test-lint: executor: node-browsers-medium steps: @@ -1053,28 +1032,6 @@ jobs: name: Validate release candidate changelog command: .circleci/scripts/validate-changelog-in-rc.sh - test-deps-audit: - executor: node-browsers-small - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: yarn audit - command: yarn audit - - test-deps-depcheck: - executor: node-browsers-small - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: depcheck - command: yarn depcheck - test-e2e-chrome-webpack: executor: node-browsers-medium-plus parallelism: 20 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c7907455701d..d8850502e7da 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,18 @@ jobs: run: ${{ steps.download-actionlint.outputs.executable }} -color shell: bash + test-deps-audit: + name: Test deps audit + uses: ./.github/workflows/test-deps-audit.yml + + test-yarn-dedupe: + name: Test yarn dedupe + uses: ./.github/workflows/test-yarn-dedupe.yml + + test-deps-depcheck: + name: Test deps depcheck + uses: ./.github/workflows/test-deps-depcheck.yml + run-tests: name: Run tests uses: ./.github/workflows/run-tests.yml @@ -41,6 +53,8 @@ jobs: runs-on: ubuntu-latest needs: - check-workflows + - test-yarn-dedupe + - test-deps-depcheck - run-tests - wait-for-circleci-workflow-status outputs: diff --git a/.github/workflows/test-deps-audit.yml b/.github/workflows/test-deps-audit.yml new file mode 100644 index 000000000000..271746da2429 --- /dev/null +++ b/.github/workflows/test-deps-audit.yml @@ -0,0 +1,18 @@ +name: Test deps audit + +on: + workflow_call: + +jobs: + test-deps-audit: + name: Test deps audit + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Run audit + run: yarn audit diff --git a/.github/workflows/test-deps-depcheck.yml b/.github/workflows/test-deps-depcheck.yml new file mode 100644 index 000000000000..3860c485f25b --- /dev/null +++ b/.github/workflows/test-deps-depcheck.yml @@ -0,0 +1,18 @@ +name: Test deps depcheck + +on: + workflow_call: + +jobs: + test-deps-depcheck: + name: Test deps depcheck + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Run depcheck + run: yarn depcheck diff --git a/.github/workflows/test-yarn-dedupe.yml b/.github/workflows/test-yarn-dedupe.yml new file mode 100644 index 000000000000..40bda1dfb3d2 --- /dev/null +++ b/.github/workflows/test-yarn-dedupe.yml @@ -0,0 +1,18 @@ +name: Test yarn dedupe + +on: + workflow_call: + +jobs: + test-yarn-dedupe: + name: Test yarn dedupe + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Detect yarn lock deduplications + run: yarn dedupe --check From 6f11eda56785b6ca1bd253a6fc6a3498eef5bc5f Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 20 Dec 2024 16:13:27 -0330 Subject: [PATCH 23/23] ci: Migrate lint CI steps (#29371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Migrate lint steps from CircleCI to GitHub Actions. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29371?quickstart=1) ## **Related issues** Relates to #28572 These changes were extracted from #29256 ## **Manual testing steps** Branch from here, create a new draft PR, Introduce lint errors, then ensure the jobs fail. https://github.com/MetaMask/metamask-extension/pull/29390 ## **Screenshots/Recordings** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 82 ---------------------- .github/workflows/main.yml | 20 ++++++ .github/workflows/test-lint-changelog.yml | 23 ++++++ .github/workflows/test-lint-lockfile.yml | 21 ++++++ .github/workflows/test-lint-shellcheck.yml | 15 ++++ .github/workflows/test-lint.yml | 21 ++++++ 6 files changed, 100 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/test-lint-changelog.yml create mode 100644 .github/workflows/test-lint-lockfile.yml create mode 100644 .github/workflows/test-lint-shellcheck.yml create mode 100644 .github/workflows/test-lint.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 5598e6450cfe..60bb80eaf449 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,10 +25,6 @@ executors: resource_class: medium+ environment: NODE_OPTIONS: --max_old_space_size=4096 - shellcheck: - docker: - - image: koalaman/shellcheck-alpine@sha256:dfaf08fab58c158549d3be64fb101c626abc5f16f341b569092577ae207db199 - resource_class: small playwright: docker: - image: mcr.microsoft.com/playwright:v1.44.1-focal @@ -184,16 +180,6 @@ workflows: - prep-build-ts-migration-dashboard: requires: - prep-deps - - test-lint: - requires: - - prep-deps - - test-lint-shellcheck - - test-lint-lockfile: - requires: - - prep-deps - - test-lint-changelog: - requires: - - prep-deps - test-e2e-chrome-webpack: <<: *main_master_rc_only requires: @@ -285,10 +271,6 @@ workflows: - validate-lavamoat-allow-scripts - validate-lavamoat-policy-build - validate-lavamoat-policy-webapp - - test-lint - - test-lint-shellcheck - - test-lint-lockfile - - test-lint-changelog - validate-source-maps - validate-source-maps-beta - validate-source-maps-flask @@ -954,20 +936,6 @@ jobs: name: Rerun workflows from failed command: yarn ci-rerun-from-failed - test-lint: - executor: node-browsers-medium - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: Lint - command: yarn lint - - run: - name: Verify locales - command: yarn verify-locales --quiet - test-storybook: executor: node-browsers-medium-plus steps: @@ -982,56 +950,6 @@ jobs: name: Test Storybook command: yarn test-storybook:ci - test-lint-shellcheck: - executor: shellcheck - steps: - - checkout - - run: apk add --no-cache bash jq yarn - - run: - name: ShellCheck Lint - command: ./development/shellcheck.sh - - test-lint-lockfile: - executor: node-browsers-medium-plus - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: lockfile-lint - command: yarn lint:lockfile - - run: - name: check yarn resolutions - command: yarn --check-resolutions - - test-lint-changelog: - executor: node-browsers-small - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - when: - condition: - not: - matches: - pattern: /^Version-v(\d+)[.](\d+)[.](\d+)$/ - value: << pipeline.git.branch >> - steps: - - run: - name: Validate changelog - command: yarn lint:changelog - - when: - condition: - matches: - pattern: /^Version-v(\d+)[.](\d+)[.](\d+)$/ - value: << pipeline.git.branch >> - steps: - - run: - name: Validate release candidate changelog - command: .circleci/scripts/validate-changelog-in-rc.sh - test-e2e-chrome-webpack: executor: node-browsers-medium-plus parallelism: 20 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d8850502e7da..ee29c54e94ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,22 @@ jobs: run: ${{ steps.download-actionlint.outputs.executable }} -color shell: bash + test-lint-shellcheck: + name: Test lint shellcheck + uses: ./.github/workflows/test-lint-shellcheck.yml + + test-lint: + name: Test lint + uses: ./.github/workflows/test-lint.yml + + test-lint-changelog: + name: Test lint changelog + uses: ./.github/workflows/test-lint-changelog.yml + + test-lint-lockfile: + name: Test lint lockfile + uses: ./.github/workflows/test-lint-lockfile.yml + test-deps-audit: name: Test deps audit uses: ./.github/workflows/test-deps-audit.yml @@ -53,6 +69,10 @@ jobs: runs-on: ubuntu-latest needs: - check-workflows + - test-lint-shellcheck + - test-lint + - test-lint-changelog + - test-lint-lockfile - test-yarn-dedupe - test-deps-depcheck - run-tests diff --git a/.github/workflows/test-lint-changelog.yml b/.github/workflows/test-lint-changelog.yml new file mode 100644 index 000000000000..66c0219551f4 --- /dev/null +++ b/.github/workflows/test-lint-changelog.yml @@ -0,0 +1,23 @@ +name: Test lint changelog + +on: + workflow_call: + +jobs: + test-lint-changelog: + name: Test lint changelog + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Validate changelog + if: ${{ !startsWith(github.head_ref || github.ref_name, 'Version-v') }} + run: yarn lint:changelog + + - name: Validate release candidate changelog + if: ${{ startsWith(github.head_ref || github.ref_name, 'Version-v') }} + run: .circleci/scripts/validate-changelog-in-rc.sh diff --git a/.github/workflows/test-lint-lockfile.yml b/.github/workflows/test-lint-lockfile.yml new file mode 100644 index 000000000000..cc84318624ce --- /dev/null +++ b/.github/workflows/test-lint-lockfile.yml @@ -0,0 +1,21 @@ +name: Test lint lockfile + +on: + workflow_call: + +jobs: + test-lint-lockfile: + name: Test lint lockfile + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Lint lockfile + run: yarn lint:lockfile + + - name: Check yarn resolutions + run: yarn --check-resolutions diff --git a/.github/workflows/test-lint-shellcheck.yml b/.github/workflows/test-lint-shellcheck.yml new file mode 100644 index 000000000000..c4127902a2f4 --- /dev/null +++ b/.github/workflows/test-lint-shellcheck.yml @@ -0,0 +1,15 @@ +name: Test lint shellcheck + +on: + workflow_call: + +jobs: + test-lint-shellcheck: + name: Test lint shellcheck + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: ShellCheck Lint + run: ./development/shellcheck.sh diff --git a/.github/workflows/test-lint.yml b/.github/workflows/test-lint.yml new file mode 100644 index 000000000000..df40a3a7ef27 --- /dev/null +++ b/.github/workflows/test-lint.yml @@ -0,0 +1,21 @@ +name: Test lint + +on: + workflow_call: + +jobs: + test-lint: + name: Test lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Lint + run: yarn lint + + - name: Verify locales + run: yarn verify-locales --quiet