From 0064020643705a881ddb4904ff2503956e8d9302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Thu, 7 Nov 2024 10:23:46 +0000 Subject: [PATCH 01/18] chore: adds Solana snap to preinstall list (#28141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adds the Solana snap to the preinstall list. ## **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/develop/.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/develop/.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/snaps/preinstalled-snaps.ts | 6 ++++ package.json | 1 + shared/lib/accounts/solana-wallet-snap.ts | 9 ++++++ yarn.lock | 38 ++++++++++++++++++++++- 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 shared/lib/accounts/solana-wallet-snap.ts diff --git a/app/scripts/snaps/preinstalled-snaps.ts b/app/scripts/snaps/preinstalled-snaps.ts index c725a2cbd837..b596468853b2 100644 --- a/app/scripts/snaps/preinstalled-snaps.ts +++ b/app/scripts/snaps/preinstalled-snaps.ts @@ -6,6 +6,9 @@ import AccountWatcherSnap from '@metamask/account-watcher/dist/preinstalled-snap import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; import PreinstalledExampleSnap from '@metamask/preinstalled-example-snap/dist/preinstalled-snap.json'; ///: END:ONLY_INCLUDE_IF +///: BEGIN:ONLY_INCLUDE_IF(solana) +import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json'; +///: END:ONLY_INCLUDE_IF // The casts here are less than ideal but we expect the SnapController to validate the inputs. const PREINSTALLED_SNAPS = Object.freeze([ @@ -16,6 +19,9 @@ const PREINSTALLED_SNAPS = Object.freeze([ BitcoinWalletSnap as unknown as PreinstalledSnap, PreinstalledExampleSnap as unknown as PreinstalledSnap, ///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(solana) + SolanaWalletSnap as unknown as PreinstalledSnap, + ///: END:ONLY_INCLUDE_IF ]); export default PREINSTALLED_SNAPS; diff --git a/package.json b/package.json index 14b38d504c87..b570cc6f9245 100644 --- a/package.json +++ b/package.json @@ -346,6 +346,7 @@ "@metamask/snaps-rpc-methods": "^11.5.1", "@metamask/snaps-sdk": "^6.10.0", "@metamask/snaps-utils": "^8.5.1", + "@metamask/solana-wallet-snap": "^0.1.9", "@metamask/transaction-controller": "^38.1.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^10.0.1", diff --git a/shared/lib/accounts/solana-wallet-snap.ts b/shared/lib/accounts/solana-wallet-snap.ts new file mode 100644 index 000000000000..1f6622c8bbdc --- /dev/null +++ b/shared/lib/accounts/solana-wallet-snap.ts @@ -0,0 +1,9 @@ +import { SnapId } from '@metamask/snaps-sdk'; +// This dependency is still installed as part of the `package.json`, however +// the Snap is being pre-installed only for Flask build (for the moment). +import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json'; + +export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId; + +export const SOLANA_WALLET_NAME: string = + SolanaWalletSnap.manifest.proposedName; diff --git a/yarn.lock b/yarn.lock index 9a54de8897d6..aa975513b90a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5575,6 +5575,23 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-api@npm:^9.0.0": + version: 9.0.0 + resolution: "@metamask/keyring-api@npm:9.0.0" + dependencies: + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^9.2.1" + "@types/uuid": "npm:^9.0.8" + bech32: "npm:^2.0.0" + uuid: "npm:^9.0.1" + webextension-polyfill: "npm:^0.12.0" + peerDependencies: + "@metamask/providers": ^17.2.0 + checksum: 10/ff552c04a4d06c7b1a43d52809a9c141d38772586388f0ab96123bce445f148aa7f7e8165d03fa92ac391351de252c4b299fc2c16e690193f669b5329941fe75 + languageName: node + linkType: hard + "@metamask/keyring-controller@npm:^17.1.0, @metamask/keyring-controller@npm:^17.2.2": version: 17.2.2 resolution: "@metamask/keyring-controller@npm:17.2.2" @@ -6349,6 +6366,17 @@ __metadata: languageName: node linkType: hard +"@metamask/solana-wallet-snap@npm:^0.1.9": + version: 0.1.9 + resolution: "@metamask/solana-wallet-snap@npm:0.1.9" + dependencies: + "@metamask/keyring-api": "npm:^9.0.0" + "@metamask/snaps-sdk": "npm:^6.9.0" + buffer: "npm:^6.0.3" + checksum: 10/ec540948e1b5c693b0a31a32521d84c5d3796a5d62d1dfa0986cae47483040a0381c30419af4a86b2402efa5e95283b45a5d17bb705c11a181d0c6fa70b5be60 + languageName: node + linkType: hard + "@metamask/superstruct@npm:^3.0.0, @metamask/superstruct@npm:^3.1.0": version: 3.1.0 resolution: "@metamask/superstruct@npm:3.1.0" @@ -26550,6 +26578,7 @@ __metadata: "@metamask/snaps-rpc-methods": "npm:^11.5.1" "@metamask/snaps-sdk": "npm:^6.10.0" "@metamask/snaps-utils": "npm:^8.5.1" + "@metamask/solana-wallet-snap": "npm:^0.1.9" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.13.0" "@metamask/transaction-controller": "npm:^38.1.0" @@ -37259,7 +37288,14 @@ __metadata: languageName: node linkType: hard -"webextension-polyfill@npm:>=0.10.0 <1.0, webextension-polyfill@npm:^0.10.0": +"webextension-polyfill@npm:>=0.10.0 <1.0, webextension-polyfill@npm:^0.12.0": + version: 0.12.0 + resolution: "webextension-polyfill@npm:0.12.0" + checksum: 10/77e648b958b573ef075e75a0c180e2bbd74dee17b3145e86d21fcbb168c4999e4a311654fe634b8178997bee9b35ea5808d8d3d3e5ff2ad138f197f4f0ea75d9 + languageName: node + linkType: hard + +"webextension-polyfill@npm:^0.10.0": version: 0.10.0 resolution: "webextension-polyfill@npm:0.10.0" checksum: 10/51ff30ebed4b1aa802b7f0347f05021b2fe492078bb1a597223d43995fcee96e2da8f914a2f6e36f988c1877ed5ab36ca7077f2f3ab828955151a59e4c01bf7e From bde47a637ecd3172bb9ac6877c815cdc97014596 Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:01:00 +0700 Subject: [PATCH 02/18] refactor: rename SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST (#28337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Rename `SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS` → `SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST` to be more explicit now that we are fetching the chain ids list from the Security Provider API introduced in https://github.com/MetaMask/metamask-extension/pull/25716 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28337?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2846 Related: https://github.com/MetaMask/metamask-extension/pull/25716 ## **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/develop/.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/develop/.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.ts | 4 ++-- shared/constants/security-provider.ts | 2 +- test/e2e/mock-e2e.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts index 7662c364b651..e8c54ee5eb53 100644 --- a/app/scripts/lib/ppom/ppom-util.ts +++ b/app/scripts/lib/ppom/ppom-util.ts @@ -11,7 +11,7 @@ import { SignatureController } from '@metamask/signature-controller'; import { BlockaidReason, BlockaidResultType, - SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS, + SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST, SecurityAlertSource, } from '../../../../shared/constants/security-provider'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; @@ -123,7 +123,7 @@ export function handlePPOMError( } export async function isChainSupported(chainId: Hex): Promise { - let supportedChainIds = SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS; + let supportedChainIds = SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST; try { if (isSecurityAlertsAPIEnabled()) { diff --git a/shared/constants/security-provider.ts b/shared/constants/security-provider.ts index e6fff53ee28a..082f68aa88de 100644 --- a/shared/constants/security-provider.ts +++ b/shared/constants/security-provider.ts @@ -89,7 +89,7 @@ export const FALSE_POSITIVE_REPORT_BASE_URL = export const SECURITY_PROVIDER_UTM_SOURCE = 'metamask-ppom'; -export const SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS: Hex[] = [ +export const SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST: Hex[] = [ CHAIN_IDS.ARBITRUM, CHAIN_IDS.AVALANCHE, CHAIN_IDS.BASE, diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index 85636fcb9089..5d372f3389a3 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -1,7 +1,7 @@ const fs = require('fs'); const { - SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS, + SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST, } = require('../../shared/constants/security-provider'); const { BRIDGE_DEV_API_BASE_URL, @@ -160,7 +160,7 @@ async function setupMocking( .thenCallback(() => { return { statusCode: 200, - json: SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS, + json: SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS_FALLBACK_LIST, }; }); From 3401ef01c48343f9d2c6e6a16c36757f20f5e577 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 7 Nov 2024 12:21:19 +0000 Subject: [PATCH 03/18] fix: Updates to the simulations component (#28107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updates the copy, layout and text color for 3 scenarios: No changes, Generic Error and Fiat value not available as per design review. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28107?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27072 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** Screenshot 2024-10-25 at 14 52 54 Screenshot 2024-10-30 at 16 31 01 Screenshot 2024-10-25 at 14 14 19 ## **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/develop/.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/develop/.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/de/messages.json | 9 -- app/_locales/el/messages.json | 9 -- app/_locales/en/messages.json | 13 +-- app/_locales/en_GB/messages.json | 9 -- app/_locales/es/messages.json | 9 -- app/_locales/fr/messages.json | 9 -- app/_locales/hi/messages.json | 9 -- app/_locales/id/messages.json | 9 -- app/_locales/ja/messages.json | 9 -- app/_locales/ko/messages.json | 9 -- app/_locales/pt/messages.json | 9 -- app/_locales/ru/messages.json | 9 -- app/_locales/tl/messages.json | 9 -- app/_locales/tr/messages.json | 9 -- app/_locales/vi/messages.json | 9 -- app/_locales/zh_CN/messages.json | 9 -- .../simulation-details.spec.ts | 4 +- .../info/personal-sign/personal-sign.tsx | 92 ++++++++++++++++--- .../simulation-details/fiat-display.test.tsx | 3 - .../simulation-details/fiat-display.tsx | 2 +- .../simulation-details.test.tsx | 8 +- .../simulation-details/simulation-details.tsx | 30 ++++-- .../confirm-transaction-base.test.js.snap | 10 +- 23 files changed, 116 insertions(+), 181 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 771deef4c28c..19c7731c778f 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "Anmelden mit" }, - "simulationDetailsFailed": { - "message": "Es ist ein Fehler beim Laden Ihrer Schätzung aufgetreten." - }, "simulationDetailsFiatNotAvailable": { "message": "Nicht verfügbar" }, "simulationDetailsIncomingHeading": { "message": "Sie erhalten" }, - "simulationDetailsNoBalanceChanges": { - "message": "Keine Änderungen für Ihre Wallet prognostiziert." - }, "simulationDetailsOutgoingHeading": { "message": "Sie senden" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "Ressourcen" }, - "siweSignatureSimulationDetailInfo": { - "message": "Sie melden sich bei einer Website an und es sind keine Änderungen an Ihrem Konto vorgesehen." - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 7ae594c8b9b8..ac4542893959 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "Σύνδεση με" }, - "simulationDetailsFailed": { - "message": "Υπήρξε σφάλμα στη φόρτωση της εκτίμησής σας." - }, "simulationDetailsFiatNotAvailable": { "message": "Μη διαθέσιμο" }, "simulationDetailsIncomingHeading": { "message": "Λαμβάνετε" }, - "simulationDetailsNoBalanceChanges": { - "message": "Δεν προβλέπονται αλλαγές για το πορτοφόλι σας" - }, "simulationDetailsOutgoingHeading": { "message": "Στέλνετε" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "Πόροι" }, - "siweSignatureSimulationDetailInfo": { - "message": "Συνδέεστε σε έναν ιστότοπο και δεν προβλέπονται αλλαγές στον λογαριασμό σας." - }, "siweURI": { "message": "Διεύθυνση URL" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index ab6b06411731..62dda6c29ef0 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -4961,17 +4961,14 @@ "simulationDetailsERC20ApproveDesc": { "message": "You're giving someone else permission to spend this amount from your account." }, - "simulationDetailsFailed": { - "message": "There was an error loading your estimation." - }, "simulationDetailsFiatNotAvailable": { "message": "Not Available" }, "simulationDetailsIncomingHeading": { "message": "You receive" }, - "simulationDetailsNoBalanceChanges": { - "message": "No changes predicted for your wallet" + "simulationDetailsNoChanges": { + "message": "No changes" }, "simulationDetailsOutgoingHeading": { "message": "You send" @@ -4995,6 +4992,9 @@ "simulationDetailsTransactionReverted": { "message": "This transaction is likely to fail" }, + "simulationDetailsUnavailable": { + "message": "Unavailable" + }, "simulationErrorMessageV2": { "message": "We were not able to estimate gas. There might be an error in the contract and this transaction may fail." }, @@ -5016,9 +5016,6 @@ "siweResources": { "message": "Resources" }, - "siweSignatureSimulationDetailInfo": { - "message": "You’re signing into a site and there are no predicted changes to your account." - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index e8eb1e58ea71..8dd2e32dac39 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -4711,18 +4711,12 @@ "signingInWith": { "message": "Signing in with" }, - "simulationDetailsFailed": { - "message": "There was an error loading your estimation." - }, "simulationDetailsFiatNotAvailable": { "message": "Not Available" }, "simulationDetailsIncomingHeading": { "message": "You receive" }, - "simulationDetailsNoBalanceChanges": { - "message": "No changes predicted for your wallet" - }, "simulationDetailsOutgoingHeading": { "message": "You send" }, @@ -4760,9 +4754,6 @@ "siweResources": { "message": "Resources" }, - "siweSignatureSimulationDetailInfo": { - "message": "You’re signing into a site and there are no predicted changes to your account." - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 0f774d8f6ba2..1d23b736fbfd 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -4537,18 +4537,12 @@ "signingInWith": { "message": "Iniciar sesión con" }, - "simulationDetailsFailed": { - "message": "Se produjo un error al cargar su estimación." - }, "simulationDetailsFiatNotAvailable": { "message": "No disponible" }, "simulationDetailsIncomingHeading": { "message": "Usted recibe" }, - "simulationDetailsNoBalanceChanges": { - "message": "No se prevén cambios para su monedero" - }, "simulationDetailsOutgoingHeading": { "message": "Envía" }, @@ -4586,9 +4580,6 @@ "siweResources": { "message": "Recursos" }, - "siweSignatureSimulationDetailInfo": { - "message": "Está iniciando sesión en un sitio y no se prevén cambios en su cuenta." - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 985dfd44c9dd..e9b8fd588b89 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "Se connecter avec" }, - "simulationDetailsFailed": { - "message": "Une erreur s’est produite lors du chargement de l’estimation." - }, "simulationDetailsFiatNotAvailable": { "message": "Non disponible" }, "simulationDetailsIncomingHeading": { "message": "Vous recevez" }, - "simulationDetailsNoBalanceChanges": { - "message": "Aucun changement prévu pour votre portefeuille" - }, "simulationDetailsOutgoingHeading": { "message": "Vous envoyez" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "Ressources" }, - "siweSignatureSimulationDetailInfo": { - "message": "Vous êtes en train de vous connecter à un site, aucun changement ne devrait être apporté à votre compte." - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 540023a75fac..9b2c06f96c11 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "के साथ साइन इन करना" }, - "simulationDetailsFailed": { - "message": "आपका एस्टीमेशन लोड करने में गड़बड़ी हुई।" - }, "simulationDetailsFiatNotAvailable": { "message": "उपलब्ध नहीं है" }, "simulationDetailsIncomingHeading": { "message": "आप पाते हैं" }, - "simulationDetailsNoBalanceChanges": { - "message": "आपके वॉलेट के लिए किसी बदलाव का प्रेडिक्शन नहीं किया गया है" - }, "simulationDetailsOutgoingHeading": { "message": "आप भेजते हैं" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "संसाधन" }, - "siweSignatureSimulationDetailInfo": { - "message": "आप किसी साइट पर साइन इन कर रहे हैं और आपके अकाउंट में कोई अनुमानित परिवर्तन नहीं हैं।" - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 81f7d2a9c633..1331d7b9c482 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "Masuk dengan" }, - "simulationDetailsFailed": { - "message": "Terjadi kesalahan saat memuat estimasi Anda." - }, "simulationDetailsFiatNotAvailable": { "message": "Tidak Tersedia" }, "simulationDetailsIncomingHeading": { "message": "Anda menerima" }, - "simulationDetailsNoBalanceChanges": { - "message": "Tidak ada perubahan yang terprediksi untuk dompet Anda" - }, "simulationDetailsOutgoingHeading": { "message": "Anda mengirim" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "Sumber daya" }, - "siweSignatureSimulationDetailInfo": { - "message": "Anda masuk ke sebuah situs dan tidak ada perkiraan perubahan pada akun Anda." - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 5787bb88c397..3107eb3cb152 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "サインイン方法:" }, - "simulationDetailsFailed": { - "message": "予測結果の読み込み中にエラーが発生しました。" - }, "simulationDetailsFiatNotAvailable": { "message": "利用できません" }, "simulationDetailsIncomingHeading": { "message": "受取額" }, - "simulationDetailsNoBalanceChanges": { - "message": "ウォレット残高の増減は予測されていません" - }, "simulationDetailsOutgoingHeading": { "message": "送金額" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "リソース" }, - "siweSignatureSimulationDetailInfo": { - "message": "サイトにサインインしようとしていて、予想されるアカウントの変更はありません。" - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 32d7bd4399b5..6dc468cdd5ef 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "다음으로 로그인:" }, - "simulationDetailsFailed": { - "message": "추정치를 불러오는 동안 오류가 발생했습니다." - }, "simulationDetailsFiatNotAvailable": { "message": "이용할 수 없음" }, "simulationDetailsIncomingHeading": { "message": "받음:" }, - "simulationDetailsNoBalanceChanges": { - "message": "지갑에 예상 변동 사항 없음" - }, "simulationDetailsOutgoingHeading": { "message": "보냄:" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "리소스" }, - "siweSignatureSimulationDetailInfo": { - "message": "사이트에 로그인 중이며 계정에 예상되는 변경 사항이 없습니다." - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 4eecb941d36f..03f41c1c62b8 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "Assinando com" }, - "simulationDetailsFailed": { - "message": "Houve um erro ao carregar sua estimativa." - }, "simulationDetailsFiatNotAvailable": { "message": "Não disponível" }, "simulationDetailsIncomingHeading": { "message": "Você recebe" }, - "simulationDetailsNoBalanceChanges": { - "message": "Nenhuma alteração prevista para sua carteira" - }, "simulationDetailsOutgoingHeading": { "message": "Você envia" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "Recursos" }, - "siweSignatureSimulationDetailInfo": { - "message": "Você está fazendo login em um site e não há alterações previstas em sua conta." - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index ce53cc239de5..e56afc418785 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "Вход с помощью" }, - "simulationDetailsFailed": { - "message": "Не удалось загрузить прогноз." - }, "simulationDetailsFiatNotAvailable": { "message": "Недоступно" }, "simulationDetailsIncomingHeading": { "message": "Вы получаете" }, - "simulationDetailsNoBalanceChanges": { - "message": "Никаких изменений в вашем кошельке не прогнозируется" - }, "simulationDetailsOutgoingHeading": { "message": "Вы отправляете" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "Ресурсы" }, - "siweSignatureSimulationDetailInfo": { - "message": "Вы входите на сайт, и в вашем счете не происходит никаких прогнозируемых изменений." - }, "siweURI": { "message": "URL-адрес" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 8909ac662e34..af56d958c313 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "Nagsa-sign in gamit ang" }, - "simulationDetailsFailed": { - "message": "Mayroong error sa pag-load ng iyong pagtataya." - }, "simulationDetailsFiatNotAvailable": { "message": "Hindi Available" }, "simulationDetailsIncomingHeading": { "message": "Natanggap mo" }, - "simulationDetailsNoBalanceChanges": { - "message": "Walang pagbabagong nahulaan para sa iyong wallet" - }, "simulationDetailsOutgoingHeading": { "message": "Nagpadala ka" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "Mga Mapagkukunan" }, - "siweSignatureSimulationDetailInfo": { - "message": "Ikaw ay nagsa-sign in sa isang site at walang mga inaasahang pagbabago sa iyong account." - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 8c4e44d3192e..6732513395d8 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "Şununla giriş yap:" }, - "simulationDetailsFailed": { - "message": "Tahmininiz yüklenirken bir hata oldu." - }, "simulationDetailsFiatNotAvailable": { "message": "Mevcut Değil" }, "simulationDetailsIncomingHeading": { "message": "Aldığınız" }, - "simulationDetailsNoBalanceChanges": { - "message": "Cüzdanınız için değişiklik öngörülmüyor" - }, "simulationDetailsOutgoingHeading": { "message": "Gönderdiğiniz" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "Kaynaklar" }, - "siweSignatureSimulationDetailInfo": { - "message": "Bir siteye giriş yapıyorsunuz ve hesabınızda öngörülen herhangi bir değişiklik yok." - }, "siweURI": { "message": "URL adresi" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index b741fe6eb536..9f6ff5119366 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "Đăng nhập bằng" }, - "simulationDetailsFailed": { - "message": "Đã xảy ra lỗi khi tải kết quả ước tính." - }, "simulationDetailsFiatNotAvailable": { "message": "Không có sẵn" }, "simulationDetailsIncomingHeading": { "message": "Bạn nhận được" }, - "simulationDetailsNoBalanceChanges": { - "message": "Ví của bạn dự kiến không có thay đổi nào" - }, "simulationDetailsOutgoingHeading": { "message": "Bạn gửi" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "Tài nguyên" }, - "siweSignatureSimulationDetailInfo": { - "message": "Bạn đang đăng nhập vào một trang web và dự kiến không có thay đổi nào đối với tài khoản của bạn." - }, "siweURI": { "message": "URL" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index e5695cdfaecf..eef758dacdad 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -4540,18 +4540,12 @@ "signingInWith": { "message": "使用以下登录方式" }, - "simulationDetailsFailed": { - "message": "加载估算时出错。" - }, "simulationDetailsFiatNotAvailable": { "message": "不可用" }, "simulationDetailsIncomingHeading": { "message": "您收到" }, - "simulationDetailsNoBalanceChanges": { - "message": "预计您的钱包不会发生变化" - }, "simulationDetailsOutgoingHeading": { "message": "您发送" }, @@ -4589,9 +4583,6 @@ "siweResources": { "message": "资源" }, - "siweSignatureSimulationDetailInfo": { - "message": "您正在登录某个网站,并且您的账户没有预期变化。" - }, "siweURI": { "message": "URL" }, diff --git a/test/e2e/tests/simulation-details/simulation-details.spec.ts b/test/e2e/tests/simulation-details/simulation-details.spec.ts index 46f4cdb5860a..bbac41b5a1a1 100644 --- a/test/e2e/tests/simulation-details/simulation-details.spec.ts +++ b/test/e2e/tests/simulation-details/simulation-details.spec.ts @@ -215,7 +215,7 @@ describe('Simulation Details', () => { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ css: '[data-testid="simulation-details-layout"]', - text: 'No changes predicted for your wallet', + text: 'No changes', }); }, ); @@ -276,7 +276,7 @@ describe('Simulation Details', () => { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ css: '[data-testid="simulation-details-layout"]', - text: 'There was an error loading your estimation', + text: 'Unavailable', }); }, ); diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx index 5eb798439ca8..5c0af68742c5 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx @@ -2,24 +2,42 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { - ConfirmInfoRow, ConfirmInfoRowAddress, ConfirmInfoRowText, ConfirmInfoRowUrl, } from '../../../../../../components/app/confirm/info/row'; +import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/info/row/alert-row/alert-row'; import { RowAlertKey } from '../../../../../../components/app/confirm/info/row/constants'; -import { useI18nContext } from '../../../../../../hooks/useI18nContext'; -import { useConfirmContext } from '../../../../context/confirm'; +import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { + Box, + Icon, + IconName, + IconSize, + Text, +} from '../../../../../../components/component-library'; +import Tooltip from '../../../../../../components/ui/tooltip'; +import { + AlignItems, + BorderColor, + BorderRadius, + Display, + FlexDirection, + IconColor, + JustifyContent, + TextColor, + TextVariant, +} from '../../../../../../helpers/constants/design-system'; +import { isSnapId } from '../../../../../../helpers/utils/snaps'; import { hexToText, sanitizeString, } from '../../../../../../helpers/utils/util'; -import { SignatureRequestType } from '../../../../types/confirm'; +import { useI18nContext } from '../../../../../../hooks/useI18nContext'; +import { useConfirmContext } from '../../../../context/confirm'; import { selectUseTransactionSimulations } from '../../../../selectors/preferences'; +import { SignatureRequestType } from '../../../../types/confirm'; import { isSIWESignatureRequest } from '../../../../utils'; -import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/info/row/alert-row/alert-row'; -import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; -import { isSnapId } from '../../../../../../helpers/utils/snaps'; import { SIWESignInfo } from './siwe-sign'; const PersonalSignInfo: React.FC = () => { @@ -49,16 +67,66 @@ const PersonalSignInfo: React.FC = () => { } } + const SimulationDetailsKey = ( + + + {t('simulationDetailsTitle')} + + + + + + ); + + const SimulationDetailsValue = ( + + {t('simulationDetailsNoChanges')} + + ); + return ( <> {isSIWE && useTransactionSimulations && ( - - - + + {SimulationDetailsKey} + {SimulationDetailsValue} + + )} diff --git a/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx b/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx index 922a2e414916..7a91ba129d6b 100644 --- a/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx +++ b/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx @@ -58,7 +58,6 @@ describe('FiatDisplay', () => { describe('IndividualFiatDisplay', () => { // @ts-expect-error This is missing from the Mocha type definitions it.each([ - [FIAT_UNAVAILABLE, 'Not Available'], [100, '$100'], [-100, '$100'], ])( @@ -84,8 +83,6 @@ describe('FiatDisplay', () => { describe('TotalFiatDisplay', () => { // @ts-expect-error This is missing from the Mocha type definitions it.each([ - [[FIAT_UNAVAILABLE, FIAT_UNAVAILABLE], 'Not Available'], - [[], 'Not Available'], [[100, 200, FIAT_UNAVAILABLE, 300], 'Total = $600'], [[-100, -200, FIAT_UNAVAILABLE, -300], 'Total = $600'], ])( diff --git a/ui/pages/confirmations/components/simulation-details/fiat-display.tsx b/ui/pages/confirmations/components/simulation-details/fiat-display.tsx index ac3fcebcc159..d7ec958f2a5b 100644 --- a/ui/pages/confirmations/components/simulation-details/fiat-display.tsx +++ b/ui/pages/confirmations/components/simulation-details/fiat-display.tsx @@ -50,7 +50,7 @@ export const IndividualFiatDisplay: React.FC<{ } if (fiatAmount === FIAT_UNAVAILABLE) { - return ; + return null; } const absFiat = Math.abs(fiatAmount); const fiatDisplayValue = fiatFormatter(absFiat, { shorten }); diff --git a/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx b/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx index 339ffbdd6a1a..9294f7d1b5b5 100644 --- a/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx +++ b/ui/pages/confirmations/components/simulation-details/simulation-details.test.tsx @@ -101,17 +101,13 @@ describe('SimulationDetails', () => { renderSimulationDetails({ error: { message: 'Unknown error' }, }); - expect( - screen.getByText(/error loading your estimation/u), - ).toBeInTheDocument(); + expect(screen.getByText(/Unavailable/u)).toBeInTheDocument(); }); it('renders empty content when there are no balance changes', () => { renderSimulationDetails({}); - expect( - screen.getByText(/No changes predicted for your wallet/u), - ).toBeInTheDocument(); + expect(screen.getByText(/No changes/u)).toBeInTheDocument(); }); it('passes the correct properties to BalanceChangeList components', () => { diff --git a/ui/pages/confirmations/components/simulation-details/simulation-details.tsx b/ui/pages/confirmations/components/simulation-details/simulation-details.tsx index 382fcb180f82..57ee1f2ff3fb 100644 --- a/ui/pages/confirmations/components/simulation-details/simulation-details.tsx +++ b/ui/pages/confirmations/components/simulation-details/simulation-details.tsx @@ -62,17 +62,23 @@ const ErrorContent: React.FC<{ error: SimulationError }> = ({ error }) => { function getMessage() { return error.code === SimulationErrorCode.Reverted ? t('simulationDetailsTransactionReverted') - : t('simulationDetailsFailed'); + : t('simulationDetailsUnavailable'); } return ( - + {error.code === SimulationErrorCode.Reverted && ( + + )} {getMessage()} ); @@ -84,8 +90,8 @@ const ErrorContent: React.FC<{ error: SimulationError }> = ({ error }) => { const EmptyContent: React.FC = () => { const t = useI18nContext(); return ( - - {t('simulationDetailsNoBalanceChanges')} + + {t('simulationDetailsNoChanges')} ); }; @@ -260,12 +266,19 @@ export const SimulationDetails: React.FC = ({ } if (error) { + const inHeaderProp = error.code !== SimulationErrorCode.Reverted && { + inHeader: , + }; + return ( - + {error.code === SimulationErrorCode.Reverted && ( + + )} ); } @@ -277,9 +290,8 @@ export const SimulationDetails: React.FC = ({ - - + inHeader={} + /> ); } diff --git a/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap b/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap index 3bd2313850a4..5cdbdd5de6a5 100644 --- a/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap +++ b/ui/pages/confirmations/confirm-transaction-base/__snapshots__/confirm-transaction-base.test.js.snap @@ -337,12 +337,12 @@ exports[`Confirm Transaction Base should match snapshot 1`] = ` +

+ No changes +

-

- No changes predicted for your wallet -

Date: Thu, 7 Nov 2024 14:40:43 +0000 Subject: [PATCH 04/18] feat: Convert mmi controller to a non-controller (#27983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** We want to bring MMIController up to date with our latest controller patterns. After review, it turns out that MMIController does not have any state and therefore should not inherit from BaseController (or anything, for that matter). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27983?quickstart=1) ## **Related issues** Fixes: #25926 ## **Manual testing steps** N/A ## **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** - [ ] 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. --- .../controllers/mmi-controller.test.ts | 91 ++++++++++------ app/scripts/controllers/mmi-controller.ts | 100 ++++++++++-------- app/scripts/metamask-controller.js | 6 +- shared/constants/mmi-controller.ts | 56 ++++++++-- 4 files changed, 165 insertions(+), 88 deletions(-) diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 64bc46132724..479c4cb0f14d 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -17,13 +17,14 @@ import { NETWORK_TYPES, TEST_NETWORK_TICKER_MAP, } from '../../../shared/constants/network'; -import MMIController from './mmi-controller'; +import { MMIController, AllowedActions } from './mmi-controller'; import { AppStateController } from './app-state-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { mmiKeyringBuilderFactory } from '../mmi-keyring-builder-factory'; import MetaMetricsController from './metametrics'; import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; import { mockNetworkState } from '../../../test/stub/networks'; +import { InfuraNetworkType } from '@metamask/controller-utils'; import { API_REQUEST_LOG_EVENT } from '@metamask-institutional/sdk'; jest.mock('@metamask-institutional/portfolio-dashboard', () => ({ @@ -39,6 +40,21 @@ jest.mock('./permissions', () => ({ }), })); +export const createMockNetworkConfiguration = ( + override?: Partial, +): NetworkConfiguration => { + return { + chainId: CHAIN_IDS.SEPOLIA, + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + name: 'Mock Network', + nativeCurrency: 'MOCK TOKEN', + rpcEndpoints: [], + defaultBlockExplorerUrlIndex: 0, + ...override, + }; +}; + const mockAccount = { address: '0x758b8178a9A4B7206d1f648c4a77C515Cbac7001', id: 'mock-id', @@ -73,10 +89,10 @@ describe('MMIController', function () { mmiConfigurationController, controllerMessenger, accountsController, - networkController, keyringController, metaMetricsController, - custodyController; + custodyController, + mmiControllerMessenger; beforeEach(async function () { const mockMessenger = { @@ -89,22 +105,10 @@ describe('MMIController', function () { subscribe: jest.fn(), }; - networkController = new NetworkController({ - messenger: new ControllerMessenger().getRestricted({ - name: 'NetworkController', - allowedEvents: [ - 'NetworkController:stateChange', - 'NetworkController:networkWillChange', - 'NetworkController:networkDidChange', - 'NetworkController:infuraIsBlocked', - 'NetworkController:infuraIsUnblocked', - ], - }), - state: mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), - infuraProjectId: 'mock-infura-project-id', - }); - - controllerMessenger = new ControllerMessenger(); + const controllerMessenger = new ControllerMessenger< + AllowedActions, + never + >(); accountsController = new AccountsController({ messenger: controllerMessenger.getRestricted({ @@ -212,7 +216,31 @@ describe('MMIController', function () { onNetworkDidChange: jest.fn(), }); - const mmiControllerMessenger = controllerMessenger.getRestricted({ + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + jest.fn().mockReturnValue(mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA })), + ); + + controllerMessenger.registerActionHandler( + 'NetworkController:setActiveNetwork', + InfuraNetworkType['sepolia'], + ); + + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + jest.fn().mockReturnValue({ + configuration: { + chainId: CHAIN_IDS.SEPOLIA, + } + }), + ); + + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkConfigurationByChainId', + jest.fn().mockReturnValue(createMockNetworkConfiguration()), + ); + + mmiControllerMessenger = controllerMessenger.getRestricted({ name: 'MMIController', allowedActions: [ 'AccountsController:getAccountByAddress', @@ -220,6 +248,10 @@ describe('MMIController', function () { 'AccountsController:listAccounts', 'AccountsController:getSelectedAccount', 'AccountsController:setSelectedAccount', + 'NetworkController:getState', + 'NetworkController:setActiveNetwork', + 'NetworkController:getNetworkClientById', + 'NetworkController:getNetworkConfigurationByChainId' ], }); @@ -253,7 +285,6 @@ describe('MMIController', function () { }) } }), - networkController, permissionController, custodyController, metaMetricsController, @@ -507,9 +538,7 @@ describe('MMIController', function () { CUSTODIAN_TYPES['CUSTODIAN-TYPE'] = { keyringClass: { type: 'mock-keyring-class' }, }; - mmiController.messenger.call = jest - .fn() - .mockReturnValue({ address: '0x1' }); + jest.spyOn(mmiControllerMessenger, 'call').mockReturnValue({ address: '0x1' }); mmiController.custodyController.getCustodyTypeByAddress = jest .fn() .mockReturnValue('custodian-type'); @@ -628,9 +657,7 @@ describe('MMIController', function () { mmiController.custodyController.getAccountDetails = jest .fn() .mockReturnValue({}); - mmiController.messenger.call = jest - .fn() - .mockReturnValue([mockAccount, mockAccount2]); + jest.spyOn(mmiControllerMessenger, 'call').mockReturnValue([mockAccount, mockAccount2]); mmiController.mmiConfigurationController.store.getState = jest .fn() .mockReturnValue({ @@ -701,7 +728,7 @@ describe('MMIController', function () { describe('handleMmiCheckIfTokenIsPresent', () => { it('should check if a token is present', async () => { - mmiController.messenger.call = jest + mmiController.messagingSystem.call = jest .fn() .mockReturnValue({ address: '0x1' }); mmiController.custodyController.getCustodyTypeByAddress = jest @@ -733,7 +760,7 @@ describe('MMIController', function () { describe('handleMmiDashboardData', () => { it('should return internalAccounts as identities', async () => { - const controllerMessengerSpy = jest.spyOn(controllerMessenger, 'call'); + const controllerMessengerSpy = jest.spyOn(mmiControllerMessenger, 'call'); await mmiController.handleMmiDashboardData(); expect(controllerMessengerSpy).toHaveBeenCalledWith( @@ -811,7 +838,7 @@ describe('MMIController', function () { describe('setAccountAndNetwork', () => { it('should set a new selected account if the selectedAddress and the address from the arguments is different', async () => { - const selectedAccountSpy = jest.spyOn(controllerMessenger, 'call'); + const selectedAccountSpy = jest.spyOn(mmiControllerMessenger, 'call'); await mmiController.setAccountAndNetwork( 'mock-origin', mockAccount2.address, @@ -829,14 +856,14 @@ describe('MMIController', function () { }); it('should not set a new selected account the accounts are the same', async () => { - const selectedAccountSpy = jest.spyOn(controllerMessenger, 'call'); + const selectedAccountSpy = jest.spyOn(mmiControllerMessenger, 'call'); await mmiController.setAccountAndNetwork( 'mock-origin', mockAccount.address, '0x1', ); - expect(selectedAccountSpy).toHaveBeenCalledTimes(1); + expect(selectedAccountSpy).toHaveBeenCalledTimes(4); const selectedAccount = accountsController.getSelectedAccount(); expect(selectedAccount.id).toBe(mockAccount.id); }); diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 65cdac69ba0b..ac8de8b76656 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -1,4 +1,3 @@ -import EventEmitter from 'events'; import log from 'loglevel'; import { captureException } from '@sentry/browser'; import { @@ -21,13 +20,13 @@ import { IApiCallLogEntry } from '@metamask-institutional/types'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; import { TransactionMeta } from '@metamask/transaction-controller'; import { KeyringTypes } from '@metamask/keyring-controller'; +import { NetworkState } from '@metamask/network-controller'; import { MessageParamsPersonal, MessageParamsTyped, SignatureController, } from '@metamask/signature-controller'; import { OriginalRequest } from '@metamask/message-manager'; -import { NetworkController } from '@metamask/network-controller'; import { InternalAccount } from '@metamask/keyring-api'; import { toHex } from '@metamask/controller-utils'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; @@ -41,15 +40,12 @@ import { Label, Signature, ConnectionRequest, + MMIControllerMessenger, } from '../../../shared/constants/mmi-controller'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; import AccountTrackerController from './account-tracker-controller'; import { AppStateController } from './app-state-controller'; -import { PreferencesController } from './preferences-controller'; type UpdateCustodianTransactionsParameters = { keyring: CustodyKeyring; @@ -64,7 +60,7 @@ type UpdateCustodianTransactionsParameters = { setTxHash: (txId: string, txHash: string) => void; }; -export default class MMIController extends EventEmitter { +export class MMIController { public opts: MMIControllerOptions; public mmiConfigurationController: MmiConfigurationController; @@ -73,8 +69,6 @@ export default class MMIController extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-explicit-any public keyringController: any; - public preferencesController: PreferencesController; - public appStateController: AppStateController; public transactionUpdateController: TransactionUpdateController; @@ -93,7 +87,7 @@ export default class MMIController extends EventEmitter { private metaMetricsController: MetaMetricsController; - private networkController: NetworkController; + #networkControllerState: NetworkState; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -101,9 +95,7 @@ export default class MMIController extends EventEmitter { private signatureController: SignatureController; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private messenger: any; + private messagingSystem: MMIControllerMessenger; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -139,13 +131,10 @@ export default class MMIController extends EventEmitter { }; constructor(opts: MMIControllerOptions) { - super(); - this.opts = opts; - this.messenger = opts.messenger; + this.messagingSystem = opts.messenger; this.mmiConfigurationController = opts.mmiConfigurationController; this.keyringController = opts.keyringController; - this.preferencesController = opts.preferencesController; this.appStateController = opts.appStateController; this.transactionUpdateController = opts.transactionUpdateController; this.custodyController = opts.custodyController; @@ -153,7 +142,6 @@ export default class MMIController extends EventEmitter { this.getPendingNonce = opts.getPendingNonce; this.accountTrackerController = opts.accountTrackerController; this.metaMetricsController = opts.metaMetricsController; - this.networkController = opts.networkController; this.permissionController = opts.permissionController; this.signatureController = opts.signatureController; this.platform = opts.platform; @@ -214,6 +202,10 @@ export default class MMIController extends EventEmitter { this.setConnectionRequest(payload); }, ); + + this.#networkControllerState = this.messagingSystem.call( + 'NetworkController:getState', + ); } // End of constructor async persistKeyringsAfterRefreshTokenChange() { @@ -402,7 +394,10 @@ export default class MMIController extends EventEmitter { // Check if any address is already added if ( newAccounts.some((address) => - this.messenger.call('AccountsController:getAccountByAddress', address), + this.messagingSystem.call( + 'AccountsController:getAccountByAddress', + address, + ), ) ) { throw new Error('Cannot import duplicate accounts'); @@ -502,15 +497,17 @@ export default class MMIController extends EventEmitter { // If the label is defined if (label) { // Set the label for the address - const account = this.messenger.call( + const account = this.messagingSystem.call( 'AccountsController:getAccountByAddress', address, ); - this.messenger.call( - 'AccountsController:setAccountName', - account.id, - label, - ); + if (account) { + this.messagingSystem.call( + 'AccountsController:setAccountName', + account.id, + label, + ); + } } } }); @@ -552,7 +549,7 @@ export default class MMIController extends EventEmitter { ) { let currentCustodyType: string = ''; if (!custodianType) { - const { address } = this.messenger.call( + const { address } = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); currentCustodyType = this.custodyController.getCustodyTypeByAddress( @@ -637,7 +634,7 @@ export default class MMIController extends EventEmitter { // Based on a custodian name, get all the tokens associated with that custodian async getCustodianJWTList(custodianEnvName: string) { - const internalAccounts = this.messenger.call( + const internalAccounts = this.messagingSystem.call( 'AccountsController:listAccounts', ); @@ -736,7 +733,8 @@ export default class MMIController extends EventEmitter { const currentAddress = address || - this.messenger.call('AccountsController:getSelectedAccount').address; + this.messagingSystem.call('AccountsController:getSelectedAccount') + .address; const currentCustodyType = this.custodyController.getCustodyTypeByAddress( toChecksumHexAddress(currentAddress), ); @@ -761,7 +759,7 @@ export default class MMIController extends EventEmitter { // TEMP: Convert internal accounts to match identities format // TODO: Convert handleMmiPortfolio to use internal accounts - const internalAccounts = this.messenger + const internalAccounts = this.messagingSystem .call('AccountsController:listAccounts') .map((internalAccount: InternalAccount) => { return { @@ -774,9 +772,10 @@ export default class MMIController extends EventEmitter { this.custodyController.getAccountDetails(address); const extensionId = this.extension.runtime.id; - const networks = Object.values( - this.networkController.state.networkConfigurationsByChainId, + const { networkConfigurationsByChainId } = this.messagingSystem.call( + 'NetworkController:getState', ); + const networks = Object.values(networkConfigurationsByChainId); return handleMmiPortfolio({ keyringAccounts, @@ -848,30 +847,38 @@ export default class MMIController extends EventEmitter { async setAccountAndNetwork(origin: string, address: string, chainId: number) { await this.appStateController.getUnlockPromise(true); const addressToLowerCase = address.toLowerCase(); - const { address: selectedAddress } = this.messenger.call( + const { address: selectedAddress } = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); if (selectedAddress.toLowerCase() !== addressToLowerCase) { - const internalAccount = this.messenger.call( + const internalAccount = this.messagingSystem.call( 'AccountsController:getAccountByAddress', addressToLowerCase, ); - this.messenger.call( - 'AccountsController:setSelectedAccount', - internalAccount.id, - ); + if (internalAccount) { + this.messagingSystem.call( + 'AccountsController:setSelectedAccount', + internalAccount.id, + ); + } } - const selectedChainId = getCurrentChainId({ - metamask: this.networkController.state, - }); + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const { + configuration: { chainId: selectedChainId }, + } = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); if (selectedChainId !== toHex(chainId)) { - const networkConfiguration = - this.networkController.state.networkConfigurationsByChainId[ - toHex(chainId) - ]; + const networkConfiguration = this.messagingSystem.call( + 'NetworkController:getNetworkConfigurationByChainId', + toHex(chainId), + ); const { networkClientId } = networkConfiguration?.rpcEndpoints?.[ @@ -879,7 +886,10 @@ export default class MMIController extends EventEmitter { ] ?? {}; if (networkClientId) { - await this.networkController.setActiveNetwork(networkClientId); + await this.messagingSystem.call( + 'NetworkController:setActiveNetwork', + networkClientId, + ); } } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 89d91f134ae2..b11ffac6d2a8 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -282,7 +282,7 @@ import { checkForMultipleVersionsRunning, } from './detect-multiple-instances'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) -import MMIController from './controllers/mmi-controller'; +import { MMIController } from './controllers/mmi-controller'; import { mmiKeyringBuilderFactory } from './mmi-keyring-builder-factory'; ///: END:ONLY_INCLUDE_IF import ComposableObservableStore from './lib/ComposableObservableStore'; @@ -2026,6 +2026,8 @@ export default class MetamaskController extends EventEmitter { 'AccountsController:listAccounts', 'AccountsController:getSelectedAccount', 'AccountsController:setSelectedAccount', + 'NetworkController:getState', + 'NetworkController:setActiveNetwork', ], }); @@ -2033,7 +2035,6 @@ export default class MetamaskController extends EventEmitter { messenger: mmiControllerMessenger, mmiConfigurationController: this.mmiConfigurationController, keyringController: this.keyringController, - preferencesController: this.preferencesController, appStateController: this.appStateController, transactionUpdateController: this.transactionUpdateController, custodyController: this.custodyController, @@ -2041,7 +2042,6 @@ export default class MetamaskController extends EventEmitter { getPendingNonce: this.getPendingNonce.bind(this), accountTrackerController: this.accountTrackerController, metaMetricsController: this.metaMetricsController, - networkController: this.networkController, permissionController: this.permissionController, signatureController: this.signatureController, platform: this.platform, diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index 67be9f72cee6..83998fe6e7b9 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -3,10 +3,20 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; import { CustodyController } from '@metamask-institutional/custody-controller'; import { SignatureController } from '@metamask/signature-controller'; -import { NetworkController } from '@metamask/network-controller'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { PreferencesController } from '../../app/scripts/controllers/preferences-controller'; +import { + NetworkController, + NetworkControllerGetNetworkClientByIdAction, + NetworkControllerGetStateAction, + NetworkControllerSetActiveNetworkAction, +} from '@metamask/network-controller'; +import { + AccountsControllerGetAccountByAddressAction, + AccountsControllerSetAccountNameAction, + AccountsControllerListAccountsAction, + AccountsControllerGetSelectedAccountAction, + AccountsControllerSetSelectedAccountAction, +} from '@metamask/accounts-controller'; +import { RestrictedControllerMessenger } from '@metamask/base-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { AppStateController } from '../../app/scripts/controllers/app-state-controller'; @@ -17,18 +27,48 @@ import AccountTrackerController from '../../app/scripts/controllers/account-trac // eslint-disable-next-line import/no-restricted-paths import MetaMetricsController from '../../app/scripts/controllers/metametrics'; +// Unique name for the controller +const controllerName = 'MMIController'; + +type NetworkControllerGetNetworkConfigurationByChainId = { + type: `NetworkController:getNetworkConfigurationByChainId`; + handler: NetworkController['getNetworkConfigurationByChainId']; +}; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | AccountsControllerGetAccountByAddressAction + | AccountsControllerSetAccountNameAction + | AccountsControllerListAccountsAction + | AccountsControllerGetSelectedAccountAction + | AccountsControllerSetSelectedAccountAction + | NetworkControllerGetStateAction + | NetworkControllerSetActiveNetworkAction + | NetworkControllerGetNetworkClientByIdAction + | NetworkControllerGetNetworkConfigurationByChainId; + +/** + * Messenger type for the {@link MMIController}. + */ +export type MMIControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + AllowedActions, + never, + AllowedActions['type'], + never +>; + export type MMIControllerOptions = { mmiConfigurationController: MmiConfigurationController; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any keyringController: any; - preferencesController: PreferencesController; appStateController: AppStateController; transactionUpdateController: TransactionUpdateController; custodyController: CustodyController; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - messenger: any; + messenger: MMIControllerMessenger; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any getState: () => any; From afa736569ce1bfbd5292dc28b9694ae13c7d8941 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 7 Nov 2024 17:06:24 +0100 Subject: [PATCH 05/18] fix: disable account syncing (#28359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR disables account syncing. The QA team found a critical bug that prevents us from being confident enough to release this feature now. We'll continue investigating and we'll re-enable when we're 100% confident about this. ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28359?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. No testing steps. Account syncing is disabled ## **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. --- app/scripts/metamask-controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b11ffac6d2a8..caac30573406 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -156,6 +156,7 @@ import { NotificationServicesPushController, NotificationServicesController, } from '@metamask/notification-services-controller'; +import { isProduction } from '../../shared/modules/environment'; import { methodsRequiringNetworkSwitch, methodsThatCanSwitchNetworkWithoutApproval, @@ -1572,7 +1573,7 @@ export default class MetamaskController extends EventEmitter { }, }, env: { - isAccountSyncingEnabled: isManifestV3, + isAccountSyncingEnabled: !isProduction() && isManifestV3, }, messenger: this.controllerMessenger.getRestricted({ name: 'UserStorageController', From 1614632ab7bd8a73ed7e6e9c1e44ef26e00583b1 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Thu, 7 Nov 2024 11:01:21 -0600 Subject: [PATCH 06/18] fix: Revert "fix: Negate privacy mode in Send screen" (#28360) Reverts MetaMask/metamask-extension#28248 Need to revert this before https://github.com/MetaMask/metamask-extension/pull/28021 --- ui/components/app/currency-input/currency-input.js | 1 - .../user-preferenced-currency-display.component.d.ts | 1 - .../user-preferenced-currency-display.component.js | 3 --- .../asset-picker-amount/asset-balance/asset-balance-text.tsx | 3 --- .../ui/currency-display/currency-display.component.d.ts | 1 - .../ui/currency-display/currency-display.component.js | 4 +--- ui/pages/asset/components/asset-page.tsx | 3 --- 7 files changed, 1 insertion(+), 15 deletions(-) diff --git a/ui/components/app/currency-input/currency-input.js b/ui/components/app/currency-input/currency-input.js index da69f3cbbe70..43da00ad3ab0 100644 --- a/ui/components/app/currency-input/currency-input.js +++ b/ui/components/app/currency-input/currency-input.js @@ -222,7 +222,6 @@ export default function CurrencyInput({ suffix={suffix} className="currency-input__conversion-component" displayValue={displayValue} - privacyModeExempt /> ); }; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts index ba465f1c5f08..4db61d568f4a 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts @@ -16,7 +16,6 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion< showCurrencySuffix?: boolean; shouldCheckShowNativeToken?: boolean; isAggregatedFiatOverviewBalance?: boolean; - privacyModeExempt?: boolean; } >; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index 77157e4b5c95..613b731d0a16 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -28,7 +28,6 @@ export default function UserPreferencedCurrencyDisplay({ showNative, showCurrencySuffix, shouldCheckShowNativeToken, - privacyModeExempt, ...restProps }) { // NOTE: When displaying currencies, we need the actual account to detect whether we're in a @@ -84,7 +83,6 @@ export default function UserPreferencedCurrencyDisplay({ numberOfDecimals={numberOfDecimals} prefixComponent={prefixComponent} suffix={showCurrencySuffix && !showEthLogo && currency} - privacyModeExempt={privacyModeExempt} /> ); } @@ -128,7 +126,6 @@ const UserPreferencedCurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, shouldCheckShowNativeToken: PropTypes.bool, - privacyModeExempt: PropTypes.bool, }; UserPreferencedCurrencyDisplay.propTypes = 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 fbf608deedd8..67def7ee82b1 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 @@ -105,7 +105,6 @@ export function AssetBalanceText({ currency={secondaryCurrency} numberOfDecimals={2} displayValue={`${formattedFiat}${errorText}`} - privacyModeExempt /> ); } @@ -117,7 +116,6 @@ export function AssetBalanceText({ {...commonProps} value={asset.balance} type={PRIMARY} - privacyModeExempt /> {errorText ? ( ); } diff --git a/ui/components/ui/currency-display/currency-display.component.d.ts b/ui/components/ui/currency-display/currency-display.component.d.ts index fd642cd4e6cd..d8e1d6b60e1e 100644 --- a/ui/components/ui/currency-display/currency-display.component.d.ts +++ b/ui/components/ui/currency-display/currency-display.component.d.ts @@ -25,7 +25,6 @@ export type CurrencyDisplayProps = OverridingUnion< // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any suffixProps?: Record; - privacyModeExempt?: boolean; } >; diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js index a1b413dd6a7f..a0bb114409f6 100644 --- a/ui/components/ui/currency-display/currency-display.component.js +++ b/ui/components/ui/currency-display/currency-display.component.js @@ -35,7 +35,6 @@ export default function CurrencyDisplay({ textProps = {}, suffixProps = {}, isAggregatedFiatOverviewBalance = false, - privacyModeExempt, ...props }) { const { privacyMode } = useSelector(getPreferences); @@ -77,7 +76,7 @@ export default function CurrencyDisplay({ className="currency-display-component__text" ellipsis variant={TextVariant.inherit} - isHidden={!privacyModeExempt && privacyMode} + isHidden={privacyMode} data-testid="account-value-and-suffix" {...textProps} > @@ -126,7 +125,6 @@ const CurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, isAggregatedFiatOverviewBalance: PropTypes.bool, - privacyModeExempt: PropTypes.bool, }; CurrencyDisplay.propTypes = CurrencyDisplayPropTypes; diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx index 4fb294b17294..c70b60169edb 100644 --- a/ui/pages/asset/components/asset-page.tsx +++ b/ui/pages/asset/components/asset-page.tsx @@ -8,7 +8,6 @@ import { getCurrentCurrency, getIsBridgeChain, getIsSwapsChain, - getPreferences, getSelectedInternalAccount, getSwapsDefaultToken, getTokensMarketData, @@ -103,7 +102,6 @@ const AssetPage = ({ const conversionRate = useSelector(getConversionRate); const allMarketData = useSelector(getTokensMarketData); const isBridgeChain = useSelector(getIsBridgeChain); - const { privacyMode } = useSelector(getPreferences); const isBuyableChain = useSelector(getIsNativeTokenBuyable); const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); const account = useSelector(getSelectedInternalAccount, isEqual); @@ -198,7 +196,6 @@ const AssetPage = ({ tokenImage={image} isOriginalTokenSymbol={asset.isOriginalNativeSymbol} isNativeCurrency={true} - privacyMode={privacyMode} /> ) : ( Date: Thu, 7 Nov 2024 11:29:03 -0600 Subject: [PATCH 07/18] fix: bump `@metamask/queued-request-controller` with patch fix (#28355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Bumps version of QueuedRequestController, with a patch that fixes an issue where `QueuedRequestController.state.queuedRequestCount` is not updated after flushing requests for an origin ## References - https://github.com/MetaMask/core/pull/4899 - https://github.com/MetaMask/core/pull/4846 - https://github.com/MetaMask/metamask-extension/pull/28090 ## Fixes Fixes #28358 [Slack discussion in v12.7.0 RC Thread](https://consensys.slack.com/archives/C029JG63136/p1730918073046389?thread_ts=1729246801.516029&cid=C029JG63136) ## Before https://drive.google.com/file/d/1ujdQgVLlT8KlwRwO-Cc3XvRHPrkpxIg_/view?usp=drive_link ## After https://github.com/user-attachments/assets/e77928e5-165b-441a-b4da-0e10471c0529 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28355?quickstart=1) ## **Manual testing steps** On a dapp permissioned for chain A and B, on chain A, queue up one send transaction, then use wallet_switchEthereumChain to switch to chain B, then queue up several more send transactions. Reject/approve the first transaction. Afterwards, you should see chain B as the active chain for the dapp, and all subsequent approvals cleared/rejected automatically. - [ ] 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). - [ ] 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/develop/.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. --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index b570cc6f9245..2fd832a3ce71 100644 --- a/package.json +++ b/package.json @@ -333,7 +333,7 @@ "@metamask/preinstalled-example-snap": "^0.2.0", "@metamask/profile-sync-controller": "^0.9.7", "@metamask/providers": "^14.0.2", - "@metamask/queued-request-controller": "^7.0.0", + "@metamask/queued-request-controller": "^7.0.1", "@metamask/rate-limit-controller": "^6.0.0", "@metamask/rpc-errors": "^7.0.0", "@metamask/safe-event-emitter": "^3.1.1", diff --git a/yarn.lock b/yarn.lock index aa975513b90a..f22f36057099 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6069,12 +6069,12 @@ __metadata: languageName: node linkType: hard -"@metamask/queued-request-controller@npm:^7.0.0": - version: 7.0.0 - resolution: "@metamask/queued-request-controller@npm:7.0.0" +"@metamask/queued-request-controller@npm:^7.0.1": + version: 7.0.1 + resolution: "@metamask/queued-request-controller@npm:7.0.1" dependencies: "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.1" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/swappable-obj-proxy": "npm:^2.2.0" @@ -6082,7 +6082,7 @@ __metadata: peerDependencies: "@metamask/network-controller": ^22.0.0 "@metamask/selected-network-controller": ^19.0.0 - checksum: 10/69118c11e3faecdbec7c9f02f4ecec4734ce0950115bfac0cdd4338309898690ae3187bcef1cc4f75f54c5c02eff07d80286d3ef29088a665039c13cb50bef88 + checksum: 10/e5b16b3dc2fa0dcf74a81b5046abb65bc05da3802ee891b5a59a80b980301c790cf949d72adba00ead6f5b3d2eaac40694308297f7dc08eb5e5f05b5a68bbf57 languageName: node linkType: hard @@ -26565,7 +26565,7 @@ __metadata: "@metamask/preinstalled-example-snap": "npm:^0.2.0" "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" - "@metamask/queued-request-controller": "npm:^7.0.0" + "@metamask/queued-request-controller": "npm:^7.0.1" "@metamask/rate-limit-controller": "npm:^6.0.0" "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/safe-event-emitter": "npm:^3.1.1" From 2484e864219220a5088e831b41c0144c8a717f10 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:57:17 -0500 Subject: [PATCH 08/18] perf: ensure `setupLocale` doesn't fetch `_locales/en/messages.json` twice (#26553) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We always load the english version of `messages.json`, but we also always load the user's locale's `messages.json`. These can be the same thing but our locale loader didn't take that into consideration. This PR updates the function to only load the user's local if it differs from our default locale, otherwise it just uses the same `messages.json` between the two. Our locale function has a side effect: it loads locale-related Internationalization features as well. So this PR also updates those side-effects in a similar manner to avoid doing the work twice. --- shared/lib/error-utils.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/shared/lib/error-utils.js b/shared/lib/error-utils.js index 89caef28026d..66efd6fb61fb 100644 --- a/shared/lib/error-utils.js +++ b/shared/lib/error-utils.js @@ -5,17 +5,27 @@ import getFirstPreferredLangCode from '../../app/scripts/lib/get-first-preferred import { fetchLocale, loadRelativeTimeFormatLocaleData } from '../modules/i18n'; import switchDirection from './switch-direction'; +const defaultLocale = 'en'; const _setupLocale = async (currentLocale) => { - const currentLocaleMessages = currentLocale - ? await fetchLocale(currentLocale) - : {}; - const enLocaleMessages = await fetchLocale('en'); + const enRelativeTime = loadRelativeTimeFormatLocaleData(defaultLocale); + const enLocale = fetchLocale(defaultLocale); - await loadRelativeTimeFormatLocaleData('en'); - if (currentLocale) { - await loadRelativeTimeFormatLocaleData(currentLocale); + const promises = [enRelativeTime, enLocale]; + if (currentLocale === defaultLocale) { + // enLocaleMessages and currentLocaleMessages are the same; reuse enLocale + promises.push(enLocale); // currentLocaleMessages + } else if (currentLocale) { + // currentLocale does not match enLocaleMessages + promises.push(fetchLocale(currentLocale)); // currentLocaleMessages + promises.push(loadRelativeTimeFormatLocaleData(currentLocale)); + } else { + // currentLocale is not set + promises.push(Promise.resolve({})); // currentLocaleMessages } + const [, enLocaleMessages, currentLocaleMessages] = await Promise.all( + promises, + ); return { currentLocaleMessages, enLocaleMessages }; }; From 54c563ec980699cb28af5df143de6fa4cacd2b7e Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:19:14 +0400 Subject: [PATCH 09/18] fix: mv2 firefox csp header (#27770) 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/27770?quickstart=1) This PR implements a workaround for a long-standing Firefox MV2 bug where the content-security-policy header is not bypassed, triggering an error. The solution is simple: we check if the extension is MV2 running in Firefox. If yes, we override the header to prevent the error from raising. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/3133, https://github.com/MetaMask/MetaMask-planning/issues/3342 ## **Manual testing steps** 1. Opening github.com should not trigger the CSP error ## **Screenshots/Recordings** ### **Before** csp-toggle-off reprod ### **After** csp-toggle-on fixed ## **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** - [ ] 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: David Murdoch <187813+davidmurdoch@users.noreply.github.com> --- app/_locales/en/messages.json | 6 ++ app/scripts/background.js | 41 ++++++++ app/scripts/constants/sentry-state.ts | 1 + .../preferences-controller.test.ts | 17 ++++ .../controllers/preferences-controller.ts | 20 ++++ app/scripts/lib/backup.test.js | 1 + app/scripts/metamask-controller.js | 4 + development/build/utils.js | 2 +- development/create-static-server.js | 13 ++- development/static-server.js | 2 +- .../test/plugins.SelfInjectPlugin.test.ts | 4 +- .../utils/plugins/SelfInjectPlugin/index.ts | 21 +++- .../utils/plugins/SelfInjectPlugin/types.ts | 15 ++- package.json | 1 + shared/modules/add-nonce-to-csp.test.ts | 98 +++++++++++++++++++ shared/modules/add-nonce-to-csp.ts | 38 +++++++ shared/modules/provider-injection.js | 94 +++++++++++------- shared/modules/provider-injection.test.ts | 6 +- test/e2e/default-fixture.js | 1 + test/e2e/fixture-builder.js | 1 + test/e2e/helpers.js | 5 +- test/e2e/phishing-warning-page-server.js | 2 +- .../index.html | 9 ++ .../content-security-policy.spec.ts | 46 +++++++++ ...rs-after-init-opt-in-background-state.json | 1 + .../errors-after-init-opt-in-ui-state.json | 1 + ...s-before-init-opt-in-background-state.json | 1 + .../errors-before-init-opt-in-ui-state.json | 1 + .../synchronous-injection.spec.js | 2 +- ui/helpers/constants/settings.js | 15 +++ ui/helpers/utils/settings-search.js | 6 +- ui/helpers/utils/settings-search.test.js | 9 +- .../advanced-tab/advanced-tab.component.js | 45 +++++++++ .../advanced-tab/advanced-tab.container.js | 6 ++ .../advanced-tab/advanced-tab.stories.js | 18 ++++ ui/store/actions.ts | 12 +++ yarn.lock | 10 ++ 37 files changed, 515 insertions(+), 60 deletions(-) create mode 100644 shared/modules/add-nonce-to-csp.test.ts create mode 100644 shared/modules/add-nonce-to-csp.ts create mode 100644 test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html create mode 100644 test/e2e/tests/content-security-policy/content-security-policy.spec.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 62dda6c29ef0..202dc04c2fa8 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3789,6 +3789,12 @@ "outdatedBrowserNotification": { "message": "Your browser is out of date. If you don't update your browser, you won't be able to get security patches and new features from MetaMask." }, + "overrideContentSecurityPolicyHeader": { + "message": "Override Content-Security-Policy header" + }, + "overrideContentSecurityPolicyHeaderDescription": { + "message": "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility." + }, "padlock": { "message": "Padlock" }, diff --git a/app/scripts/background.js b/app/scripts/background.js index bacb6adddf9f..90a52b6c0d19 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -56,6 +56,8 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../ui/selectors'; +import { addNonceToCsp } from '../../shared/modules/add-nonce-to-csp'; +import { checkURLForProviderInjection } from '../../shared/modules/provider-injection'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -333,6 +335,40 @@ function maybeDetectPhishing(theController) { ); } +/** + * Overrides the Content-Security-Policy (CSP) header by adding a nonce to the `script-src` directive. + * This is a workaround for [Bug #1446231](https://bugzilla.mozilla.org/show_bug.cgi?id=1446231), + * which involves overriding the page CSP for inline script nodes injected by extension content scripts. + */ +function overrideContentSecurityPolicyHeader() { + // The extension url is unique per install on Firefox, so we can safely add it as a nonce to the CSP header + const nonce = btoa(browser.runtime.getURL('/')); + browser.webRequest.onHeadersReceived.addListener( + ({ responseHeaders, url }) => { + // Check whether inpage.js is going to be injected into the page or not. + // There is no reason to modify the headers if we are not injecting inpage.js. + const isInjected = checkURLForProviderInjection(new URL(url)); + + // Check if the user has enabled the overrideContentSecurityPolicyHeader preference + const isEnabled = + controller.preferencesController.state + .overrideContentSecurityPolicyHeader; + + if (isInjected && isEnabled) { + for (const header of responseHeaders) { + if (header.name.toLowerCase() === 'content-security-policy') { + header.value = addNonceToCsp(header.value, nonce); + } + } + } + + return { responseHeaders }; + }, + { types: ['main_frame', 'sub_frame'], urls: ['http://*/*', 'https://*/*'] }, + ['blocking', 'responseHeaders'], + ); +} + // These are set after initialization let connectRemote; let connectExternalExtension; @@ -479,6 +515,11 @@ async function initialize() { if (!isManifestV3) { await loadPhishingWarningPage(); + // Workaround for Bug #1446231 to override page CSP for inline script nodes injected by extension content scripts + // https://bugzilla.mozilla.org/show_bug.cgi?id=1446231 + if (getPlatform() === PLATFORM_FIREFOX) { + overrideContentSecurityPolicyHeader(); + } } await sendReadyMessageToTabs(); log.info('MetaMask initialization complete.'); diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 3125016ea0b5..655851590441 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -223,6 +223,7 @@ export const SENTRY_BACKGROUND_STATE = { advancedGasFee: true, currentLocale: true, dismissSeedBackUpReminder: true, + overrideContentSecurityPolicyHeader: true, featureFlags: true, forgottenPassword: true, identities: false, diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index 25010cdd3a0f..39a2d49648b2 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -837,6 +837,23 @@ describe('preferences controller', () => { }); }); + describe('overrideContentSecurityPolicyHeader', () => { + it('defaults overrideContentSecurityPolicyHeader to true', () => { + const { controller } = setupController({}); + expect( + controller.state.overrideContentSecurityPolicyHeader, + ).toStrictEqual(true); + }); + + it('set overrideContentSecurityPolicyHeader to false', () => { + const { controller } = setupController({}); + controller.setOverrideContentSecurityPolicyHeader(false); + expect( + controller.state.overrideContentSecurityPolicyHeader, + ).toStrictEqual(false); + }); + }); + describe('snapsAddSnapAccountModalDismissed', () => { it('defaults snapsAddSnapAccountModalDismissed to false', () => { const { controller } = setupController({}); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index b4ce3ca71e64..dce2ef3d0512 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -133,6 +133,7 @@ export type PreferencesControllerState = Omit< useNonceField: boolean; usePhishDetect: boolean; dismissSeedBackUpReminder: boolean; + overrideContentSecurityPolicyHeader: boolean; useMultiAccountBalanceChecker: boolean; useSafeChainsListValidation: boolean; use4ByteResolution: boolean; @@ -175,6 +176,7 @@ export const getDefaultPreferencesControllerState = useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, useMultiAccountBalanceChecker: true, useSafeChainsListValidation: true, // set to true means the dynamic list from the API is being used @@ -306,6 +308,10 @@ const controllerMetadata = { persist: true, anonymous: true, }, + overrideContentSecurityPolicyHeader: { + persist: true, + anonymous: true, + }, useMultiAccountBalanceChecker: { persist: true, anonymous: true, @@ -1009,6 +1015,20 @@ export class PreferencesController extends BaseController< }); } + /** + * A setter for the user preference to override the Content-Security-Policy header + * + * @param overrideContentSecurityPolicyHeader - User preference for overriding the Content-Security-Policy header. + */ + setOverrideContentSecurityPolicyHeader( + overrideContentSecurityPolicyHeader: boolean, + ): void { + this.update((state) => { + state.overrideContentSecurityPolicyHeader = + overrideContentSecurityPolicyHeader; + }); + } + /** * A setter for the incomingTransactions in preference to be updated * diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index b3a7f176c2e6..826aa04018d9 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -150,6 +150,7 @@ const jsonData = JSON.stringify({ useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, useTokenDetection: false, useCollectibleDetection: false, openSeaEnabled: false, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index caac30573406..d43c12ff24a1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3487,6 +3487,10 @@ export default class MetamaskController extends EventEmitter { preferencesController.setDismissSeedBackUpReminder.bind( preferencesController, ), + setOverrideContentSecurityPolicyHeader: + preferencesController.setOverrideContentSecurityPolicyHeader.bind( + preferencesController, + ), setAdvancedGasFee: preferencesController.setAdvancedGasFee.bind( preferencesController, ), diff --git a/development/build/utils.js b/development/build/utils.js index 525815d2520a..626aacd588c7 100644 --- a/development/build/utils.js +++ b/development/build/utils.js @@ -293,7 +293,7 @@ function getBuildName({ function makeSelfInjecting(filePath) { const fileContents = readFileSync(filePath, 'utf8'); const textContent = JSON.stringify(fileContents); - const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};d.documentElement.appendChild(s).remove();}`; + const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce=btoa((globalThis.browser||chrome).runtime.getURL('/'));d.documentElement.appendChild(s).remove();}`; writeFileSync(filePath, js, 'utf8'); } diff --git a/development/create-static-server.js b/development/create-static-server.js index a8d5e28b0088..8e55fa54ca13 100755 --- a/development/create-static-server.js +++ b/development/create-static-server.js @@ -4,10 +4,17 @@ const path = require('path'); const serveHandler = require('serve-handler'); -const createStaticServer = (rootDirectory) => { +/** + * Creates an HTTP server that serves static files from a directory using serve-handler. + * If a request URL starts with `/node_modules/`, it rewrites the URL and serves files from the `node_modules` directory. + * + * @param { NonNullable[2]> } options - Configuration options for serve-handler. Documentation can be found here: https://github.com/vercel/serve-handler + * @returns {http.Server} An instance of an HTTP server configured with the specified options. + */ +const createStaticServer = (options) => { return http.createServer((request, response) => { if (request.url.startsWith('/node_modules/')) { - request.url = request.url.substr(14); + request.url = request.url.slice(14); return serveHandler(request, response, { directoryListing: false, public: path.resolve('./node_modules'), @@ -15,7 +22,7 @@ const createStaticServer = (rootDirectory) => { } return serveHandler(request, response, { directoryListing: false, - public: rootDirectory, + ...options, }); }); }; diff --git a/development/static-server.js b/development/static-server.js index bb15133d6fdd..ec3a51a512f0 100755 --- a/development/static-server.js +++ b/development/static-server.js @@ -31,7 +31,7 @@ const onRequest = (request, response) => { }; const startServer = ({ port, rootDirectory }) => { - const server = createStaticServer(rootDirectory); + const server = createStaticServer({ public: rootDirectory }); server.on('request', onRequest); diff --git a/development/webpack/test/plugins.SelfInjectPlugin.test.ts b/development/webpack/test/plugins.SelfInjectPlugin.test.ts index 3a3ef729eacf..b6390654a4bb 100644 --- a/development/webpack/test/plugins.SelfInjectPlugin.test.ts +++ b/development/webpack/test/plugins.SelfInjectPlugin.test.ts @@ -55,7 +55,7 @@ describe('SelfInjectPlugin', () => { // reference the `sourceMappingURL` assert.strictEqual( newSource, - `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`, + `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa((globalThis.browser||chrome).runtime.getURL("/"));d.documentElement.appendChild(s).remove()}`, ); } else { // the new source should NOT reference the new sourcemap, since it's @@ -66,7 +66,7 @@ describe('SelfInjectPlugin', () => { // console. assert.strictEqual( newSource, - `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`, + `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa((globalThis.browser||chrome).runtime.getURL("/"));d.documentElement.appendChild(s).remove()}`, ); } diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts index b80f6102ab75..18d3624310ae 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts @@ -6,6 +6,19 @@ import type { SelfInjectPluginOptions, Source, Compiler } from './types'; export { type SelfInjectPluginOptions } from './types'; +/** + * Generates a runtime URL expression for a given path. + * + * This function constructs a URL string using the `runtime.getURL` method + * from either the `globalThis.browser` or `chrome` object, depending on + * which one is available in the global scope. + * + * @param path - The path of the runtime URL. + * @returns The constructed runtime URL string. + */ +const getRuntimeURLExpression = (path: string) => + `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(path)})`; + /** * Default options for the SelfInjectPlugin. */ @@ -13,8 +26,11 @@ const defaultOptions = { // The default `sourceUrlExpression` is configured for browser extensions. // It generates the absolute url of the given file as an extension url. // e.g., `chrome-extension:///scripts/inpage.js` - sourceUrlExpression: (filename: string) => - `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(filename)})`, + sourceUrlExpression: getRuntimeURLExpression, + // The default `nonceExpression` is configured for browser extensions. + // It generates the absolute url of a path as an extension url in base64. + // e.g., `Y2hyb21lLWV4dGVuc2lvbjovLzxleHRlbnNpb24taWQ+Lw==` + nonceExpression: (path: string) => `btoa(${getRuntimeURLExpression(path)})`, } satisfies SelfInjectPluginOptions; /** @@ -142,6 +158,7 @@ export class SelfInjectPlugin { `\`\\n//# sourceURL=\${${this.options.sourceUrlExpression(file)}};\``, ); newSource.add(`;`); + newSource.add(`s.nonce=${this.options.nonceExpression('/')};`); // add and immediately remove the script to avoid modifying the DOM. newSource.add(`d.documentElement.appendChild(s).remove()`); newSource.add(`}`); diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts index 2240227bd76a..e70467cd270b 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts @@ -23,7 +23,7 @@ export type SelfInjectPluginOptions = { * will be injected into matched file to provide a sourceURL for the self * injected script. * - * Defaults to `(filename: string) => (globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")` + * Defaults to `(filename: string) => (globalThis.browser||chrome).runtime.getURL("${filename}")` * * @example Custom * ```js @@ -39,11 +39,22 @@ export type SelfInjectPluginOptions = { * * ```js * { - * sourceUrlExpression: (filename) => `(globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")` + * sourceUrlExpression: (filename) => `(globalThis.browser||chrome).runtime.getURL("${filename}")` * } * ``` * @param filename - the chunk's relative filename as it will exist in the output directory * @returns */ sourceUrlExpression?: (filename: string) => string; + /** + * A function that returns a JavaScript expression escaped as a string which + * will be injected into matched file to set a nonce for the self + * injected script. + * + * Defaults to `(path: string) => btoa((globalThis.browser||chrome).runtime.getURL("${path}"))` + * + * @param path - the path to be encoded as a nonce + * @returns + */ + nonceExpression?: (path: string) => string; }; diff --git a/package.json b/package.json index 2fd832a3ce71..c612dd76fb20 100644 --- a/package.json +++ b/package.json @@ -528,6 +528,7 @@ "@types/redux-mock-store": "1.0.6", "@types/remote-redux-devtools": "^0.5.5", "@types/selenium-webdriver": "^4.1.19", + "@types/serve-handler": "^6.1.4", "@types/sinon": "^10.0.13", "@types/sprintf-js": "^1", "@types/w3c-web-hid": "^1.0.3", diff --git a/shared/modules/add-nonce-to-csp.test.ts b/shared/modules/add-nonce-to-csp.test.ts new file mode 100644 index 000000000000..dc80bfd9a92a --- /dev/null +++ b/shared/modules/add-nonce-to-csp.test.ts @@ -0,0 +1,98 @@ +import { addNonceToCsp } from './add-nonce-to-csp'; + +describe('addNonceToCsp', () => { + it('empty string', () => { + const input = ''; + const expected = ''; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one empty directive', () => { + const input = 'script-src'; + const expected = `script-src 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one directive, one value', () => { + const input = 'script-src default.example'; + const expected = `script-src default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one directive, two values', () => { + const input = "script-src 'self' default.example"; + const expected = `script-src 'self' default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('multiple directives', () => { + const input = + "default-src 'self'; script-src 'unsafe-eval' scripts.example; object-src; style-src styles.example"; + const expected = `default-src 'self'; script-src 'unsafe-eval' scripts.example 'nonce-test'; object-src; style-src styles.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('no applicable directive', () => { + const input = 'img-src https://example.com'; + const expected = `img-src https://example.com`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('non-ASCII directives', () => { + const input = 'script-src default.example;\u0080;style-src style.example'; + const expected = `script-src default.example 'nonce-test';\u0080;style-src style.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('uppercase directive names', () => { + const input = 'SCRIPT-SRC DEFAULT.EXAMPLE'; + const expected = `SCRIPT-SRC DEFAULT.EXAMPLE 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('duplicate directive names', () => { + const input = + 'default-src default.example; script-src script.example; script-src script.example'; + const expected = `default-src default.example; script-src script.example 'nonce-test'; script-src script.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('nonce value contains script-src', () => { + const input = + "default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com"; + const expected = `default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('url value contains script-src', () => { + const input = + "default-src 'self' https://script-src.com; script-src 'self' https://example.com"; + const expected = `default-src 'self' https://script-src.com; script-src 'self' https://example.com 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('fallback to default-src', () => { + const input = `default-src 'none'`; + const expected = `default-src 'none' 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('keep ascii whitespace characters', () => { + const input = ' script-src default.example '; + const expected = ` script-src default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); +}); diff --git a/shared/modules/add-nonce-to-csp.ts b/shared/modules/add-nonce-to-csp.ts new file mode 100644 index 000000000000..a8b7fe333089 --- /dev/null +++ b/shared/modules/add-nonce-to-csp.ts @@ -0,0 +1,38 @@ +// ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE. +// See . +const ASCII_WHITESPACE_CHARS = ['\t', '\n', '\f', '\r', ' '].join(''); + +const matchDirective = (directive: string) => + /* eslint-disable require-unicode-regexp */ + new RegExp( + `^([${ASCII_WHITESPACE_CHARS}]*${directive}[${ASCII_WHITESPACE_CHARS}]*)`, // Match the directive and surrounding ASCII whitespace + 'is', // Case-insensitive, including newlines + ); +const matchScript = matchDirective('script-src'); +const matchDefault = matchDirective('default-src'); + +/** + * Adds a nonce to a Content Security Policy (CSP) string. + * + * @param text - The Content Security Policy (CSP) string to add the nonce to. + * @param nonce - The nonce to add to the Content Security Policy (CSP) string. + * @returns The updated Content Security Policy (CSP) string. + */ +export const addNonceToCsp = (text: string, nonce: string) => { + const formattedNonce = ` 'nonce-${nonce}'`; + const directives = text.split(';'); + const scriptIndex = directives.findIndex((directive) => + matchScript.test(directive), + ); + if (scriptIndex >= 0) { + directives[scriptIndex] += formattedNonce; + } else { + const defaultIndex = directives.findIndex((directive) => + matchDefault.test(directive), + ); + if (defaultIndex >= 0) { + directives[defaultIndex] += formattedNonce; + } + } + return directives.join(';'); +}; diff --git a/shared/modules/provider-injection.js b/shared/modules/provider-injection.js index 25a316e93440..b96df88c29e2 100644 --- a/shared/modules/provider-injection.js +++ b/shared/modules/provider-injection.js @@ -5,40 +5,36 @@ */ export default function shouldInjectProvider() { return ( - doctypeCheck() && - suffixCheck() && - documentElementCheck() && - !blockedDomainCheck() + checkURLForProviderInjection(new URL(window.location)) && + checkDocumentForProviderInjection() ); } /** - * Checks the doctype of the current document if it exists + * Checks if a given URL is eligible for provider injection. * - * @returns {boolean} {@code true} if the doctype is html or if none exists + * This function determines if a URL passes the suffix check and is not part of the blocked domains. + * + * @param {URL} url - The URL to be checked for injection. + * @returns {boolean} Returns `true` if the URL passes the suffix check and is not blocked, otherwise `false`. */ -function doctypeCheck() { - const { doctype } = window.document; - if (doctype) { - return doctype.name === 'html'; - } - return true; +export function checkURLForProviderInjection(url) { + return suffixCheck(url) && !blockedDomainCheck(url); } /** - * Returns whether or not the extension (suffix) of the current document is prohibited + * Returns whether or not the extension (suffix) of the given URL's pathname is prohibited * - * This checks {@code window.location.pathname} against a set of file extensions - * that we should not inject the provider into. This check is indifferent of - * query parameters in the location. + * This checks the provided URL's pathname against a set of file extensions + * that we should not inject the provider into. * - * @returns {boolean} whether or not the extension of the current document is prohibited + * @param {URL} url - The URL to check + * @returns {boolean} whether or not the extension of the given URL's pathname is prohibited */ -function suffixCheck() { +function suffixCheck({ pathname }) { const prohibitedTypes = [/\.xml$/u, /\.pdf$/u]; - const currentUrl = window.location.pathname; for (let i = 0; i < prohibitedTypes.length; i++) { - if (prohibitedTypes[i].test(currentUrl)) { + if (prohibitedTypes[i].test(pathname)) { return false; } } @@ -46,24 +42,12 @@ function suffixCheck() { } /** - * Checks the documentElement of the current document + * Checks if the given domain is blocked * - * @returns {boolean} {@code true} if the documentElement is an html node or if none exists + * @param {URL} url - The URL to check + * @returns {boolean} {@code true} if the given domain is blocked */ -function documentElementCheck() { - const documentElement = document.documentElement.nodeName; - if (documentElement) { - return documentElement.toLowerCase() === 'html'; - } - return true; -} - -/** - * Checks if the current domain is blocked - * - * @returns {boolean} {@code true} if the current domain is blocked - */ -function blockedDomainCheck() { +function blockedDomainCheck(url) { // If making any changes, please also update the same list found in the MetaMask-Mobile & SDK repositories const blockedDomains = [ 'execution.consensys.io', @@ -85,8 +69,7 @@ function blockedDomainCheck() { 'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html', ]; - const { hostname: currentHostname, pathname: currentPathname } = - window.location; + const { hostname: currentHostname, pathname: currentPathname } = url; const trimTrailingSlash = (str) => str.endsWith('/') ? str.slice(0, -1) : str; @@ -104,3 +87,38 @@ function blockedDomainCheck() { ) ); } + +/** + * Checks if the document is suitable for provider injection by verifying the doctype and document element. + * + * @returns {boolean} `true` if the document passes both the doctype and document element checks, otherwise `false`. + */ +export function checkDocumentForProviderInjection() { + return doctypeCheck() && documentElementCheck(); +} + +/** + * Checks the doctype of the current document if it exists + * + * @returns {boolean} {@code true} if the doctype is html or if none exists + */ +function doctypeCheck() { + const { doctype } = window.document; + if (doctype) { + return doctype.name === 'html'; + } + return true; +} + +/** + * Checks the documentElement of the current document + * + * @returns {boolean} {@code true} if the documentElement is an html node or if none exists + */ +function documentElementCheck() { + const documentElement = document.documentElement.nodeName; + if (documentElement) { + return documentElement.toLowerCase() === 'html'; + } + return true; +} diff --git a/shared/modules/provider-injection.test.ts b/shared/modules/provider-injection.test.ts index 1742e31b4db6..c7da24b46642 100644 --- a/shared/modules/provider-injection.test.ts +++ b/shared/modules/provider-injection.test.ts @@ -8,11 +8,7 @@ describe('shouldInjectProvider', () => { const urlObj = new URL(urlString); mockedWindow.mockImplementation(() => ({ - location: { - hostname: urlObj.hostname, - origin: urlObj.origin, - pathname: urlObj.pathname, - }, + location: urlObj, document: { doctype: { name: 'html', diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 2d3d2999ed43..fd2d5be42891 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -193,6 +193,7 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { currentLocale: 'en', useExternalServices: true, dismissSeedBackUpReminder: true, + overrideContentSecurityPolicyHeader: true, featureFlags: {}, forgottenPassword: false, identities: { diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 334e2f74ceca..bea9e9bad77f 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -63,6 +63,7 @@ function onboardingFixture() { advancedGasFee: {}, currentLocale: 'en', dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, featureFlags: {}, forgottenPassword: false, identities: {}, diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 1cb9e47f1f80..c3705c1ebf6c 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -65,6 +65,7 @@ async function withFixtures(options, testSuite) { smartContract, driverOptions, dappOptions, + staticServerOptions, title, ignoredConsoleErrors = [], dappPath = undefined, @@ -159,7 +160,9 @@ async function withFixtures(options, testSuite) { 'dist', ); } - dappServer.push(createStaticServer(dappDirectory)); + dappServer.push( + createStaticServer({ public: dappDirectory, ...staticServerOptions }), + ); dappServer[i].listen(`${dappBasePort + i}`); await new Promise((resolve, reject) => { dappServer[i].on('listening', resolve); diff --git a/test/e2e/phishing-warning-page-server.js b/test/e2e/phishing-warning-page-server.js index 2f6099e1a3d0..a66c9211ed83 100644 --- a/test/e2e/phishing-warning-page-server.js +++ b/test/e2e/phishing-warning-page-server.js @@ -13,7 +13,7 @@ const phishingWarningDirectory = path.resolve( class PhishingWarningPageServer { constructor() { - this._server = createStaticServer(phishingWarningDirectory); + this._server = createStaticServer({ public: phishingWarningDirectory }); } async start({ port = 9999 } = {}) { diff --git a/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html b/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html new file mode 100644 index 000000000000..dae42d094890 --- /dev/null +++ b/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html @@ -0,0 +1,9 @@ + + + + Mock CSP header testing + + +
Mock Page for Content-Security-Policy header testing
+ + diff --git a/test/e2e/tests/content-security-policy/content-security-policy.spec.ts b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts new file mode 100644 index 000000000000..996c64343c5b --- /dev/null +++ b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts @@ -0,0 +1,46 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import { + defaultGanacheOptions, + openDapp, + unlockWallet, + withFixtures, +} from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; + +describe('Content-Security-Policy', function (this: Suite) { + it('opening a restricted website should still load the extension', async function () { + await withFixtures( + { + dapp: true, + dappPaths: [ + './tests/content-security-policy/content-security-policy-mock-page', + ], + staticServerOptions: { + headers: [ + { + source: 'index.html', + headers: [ + { + key: 'Content-Security-Policy', + value: `default-src 'none'`, + }, + ], + }, + ], + }, + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + await openDapp(driver); + const isExtensionLoaded: boolean = await driver.executeScript( + 'return typeof window.ethereum !== "undefined"', + ); + assert.equal(isExtensionLoaded, true); + }, + ); + }); +}); diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 1a871780591f..cebacab2b1d9 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -198,6 +198,7 @@ "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "useMultiAccountBalanceChecker": true, "useSafeChainsListValidation": "boolean", "useTokenDetection": true, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index e8f8f81a6293..97399d34c508 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -115,6 +115,7 @@ "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "useMultiAccountBalanceChecker": true, "useSafeChainsListValidation": true, "useTokenDetection": true, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 1a51023a2ca1..7622ad15937c 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -115,6 +115,7 @@ "currentLocale": "en", "useExternalServices": "boolean", "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "featureFlags": {}, "forgottenPassword": false, "identities": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index bf7f87a16134..b3fa8d117beb 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -115,6 +115,7 @@ "currentLocale": "en", "useExternalServices": "boolean", "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "featureFlags": {}, "forgottenPassword": false, "identities": "object", diff --git a/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js b/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js index 917e289f320f..8e4254fb8b34 100644 --- a/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js +++ b/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js @@ -7,7 +7,7 @@ const dappPort = 8080; describe('The provider', function () { it('can be injected synchronously and successfully used by a dapp', async function () { - const dappServer = createStaticServer(__dirname); + const dappServer = createStaticServer({ public: __dirname }); dappServer.listen(dappPort); await new Promise((resolve, reject) => { dappServer.on('listening', resolve); diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index c22b0cbcf183..232bcdae5aff 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -1,4 +1,8 @@ /* eslint-disable @metamask/design-tokens/color-no-hex*/ +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getPlatform } from '../../../app/scripts/lib/util'; +import { PLATFORM_FIREFOX } from '../../../shared/constants/app'; import { IconName } from '../../components/component-library'; import { ADVANCED_ROUTE, @@ -19,6 +23,7 @@ import { * # @param {string} route tab route with appended arbitrary, unique anchor tag / hash route * # @param {string} iconName * # @param {string} featureFlag ENV variable name. If the ENV value exists, the route will be searchable; else, route will not be searchable. + * # @param {boolean} hidden If true, the route will not be searchable. */ /** @type {SettingRouteConfig[]} */ @@ -154,6 +159,16 @@ const SETTINGS_CONSTANTS = [ route: `${ADVANCED_ROUTE}#export-data`, icon: 'fas fa-download', }, + // advanced settingsRefs[11] + { + tabMessage: (t) => t('advanced'), + sectionMessage: (t) => t('overrideContentSecurityPolicyHeader'), + descriptionMessage: (t) => + t('overrideContentSecurityPolicyHeaderDescription'), + route: `${ADVANCED_ROUTE}#override-content-security-policy-header`, + icon: 'fas fa-sliders-h', + hidden: getPlatform() !== PLATFORM_FIREFOX, + }, { tabMessage: (t) => t('contacts'), sectionMessage: (t) => t('contacts'), diff --git a/ui/helpers/utils/settings-search.js b/ui/helpers/utils/settings-search.js index 07b4501c0208..8c11ad8fad52 100644 --- a/ui/helpers/utils/settings-search.js +++ b/ui/helpers/utils/settings-search.js @@ -8,8 +8,10 @@ export function getSettingsRoutes() { if (settingsRoutes) { return settingsRoutes; } - settingsRoutes = SETTINGS_CONSTANTS.filter((routeObject) => - routeObject.featureFlag ? process.env[routeObject.featureFlag] : true, + settingsRoutes = SETTINGS_CONSTANTS.filter( + (routeObject) => + (routeObject.featureFlag ? process.env[routeObject.featureFlag] : true) && + !routeObject.hidden, ); return settingsRoutes; } diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index cc7b875d8c5e..30af3ee6b9da 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -68,6 +68,10 @@ const t = (key) => { return 'Dismiss Secret Recovery Phrase backup reminder'; case 'dismissReminderDescriptionField': return 'Turn this on to dismiss the Secret Recovery Phrase backup reminder message. We highly recommend that you back up your Secret Recovery Phrase to avoid loss of funds'; + case 'overrideContentSecurityPolicyHeader': + return 'Override Content-Security-Policy header'; + case 'overrideContentSecurityPolicyHeaderDescription': + return "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility."; case 'Contacts': return 'Contacts'; case 'securityAndPrivacy': @@ -147,9 +151,12 @@ describe('Settings Search Utils', () => { describe('getSettingsRoutes', () => { it('should be an array of settings routes objects', () => { const NUM_OF_ENV_FEATURE_FLAG_SETTINGS = 4; + const NUM_OF_HIDDEN_SETTINGS = 1; expect(getSettingsRoutes()).toHaveLength( - SETTINGS_CONSTANTS.length - NUM_OF_ENV_FEATURE_FLAG_SETTINGS, + SETTINGS_CONSTANTS.length - + NUM_OF_ENV_FEATURE_FLAG_SETTINGS - + NUM_OF_HIDDEN_SETTINGS, ); }); }); diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js index 132b97f7caa9..6be7bcd52004 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.js @@ -29,6 +29,10 @@ import { getNumberOfSettingRoutesInTab, handleSettingsRefs, } from '../../../helpers/utils/settings-search'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getPlatform } from '../../../../app/scripts/lib/util'; +import { PLATFORM_FIREFOX } from '../../../../shared/constants/app'; export default class AdvancedTab extends PureComponent { static contextTypes = { @@ -58,6 +62,8 @@ export default class AdvancedTab extends PureComponent { backupUserData: PropTypes.func.isRequired, showExtensionInFullSizeView: PropTypes.bool, setShowExtensionInFullSizeView: PropTypes.func.isRequired, + overrideContentSecurityPolicyHeader: PropTypes.bool, + setOverrideContentSecurityPolicyHeader: PropTypes.func.isRequired, }; state = { @@ -583,6 +589,42 @@ export default class AdvancedTab extends PureComponent { ); } + renderOverrideContentSecurityPolicyHeader() { + const { t } = this.context; + const { + overrideContentSecurityPolicyHeader, + setOverrideContentSecurityPolicyHeader, + } = this.props; + + return ( + +
+ {t('overrideContentSecurityPolicyHeader')} +
+ {t('overrideContentSecurityPolicyHeaderDescription')} +
+
+ +
+ setOverrideContentSecurityPolicyHeader(!value)} + offLabel={t('off')} + onLabel={t('on')} + /> +
+
+ ); + } + render() { const { errorInSettings } = this.props; // When adding/removing/editing the order of renders, double-check the order of the settingsRefs. This affects settings-search.js @@ -602,6 +644,9 @@ export default class AdvancedTab extends PureComponent { {this.renderAutoLockTimeLimit()} {this.renderUserDataBackup()} {this.renderDismissSeedBackupReminderControl()} + {getPlatform() === PLATFORM_FIREFOX + ? this.renderOverrideContentSecurityPolicyHeader() + : null}
); } diff --git a/ui/pages/settings/advanced-tab/advanced-tab.container.js b/ui/pages/settings/advanced-tab/advanced-tab.container.js index aaa094e0655c..0be61499c1af 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.container.js @@ -7,6 +7,7 @@ import { backupUserData, setAutoLockTimeLimit, setDismissSeedBackUpReminder, + setOverrideContentSecurityPolicyHeader, setFeatureFlag, setShowExtensionInFullSizeView, setShowFiatConversionOnTestnetsPreference, @@ -31,6 +32,7 @@ export const mapStateToProps = (state) => { featureFlags: { sendHexData } = {}, useNonceField, dismissSeedBackUpReminder, + overrideContentSecurityPolicyHeader, } = metamask; const { showFiatInTestnets, @@ -49,6 +51,7 @@ export const mapStateToProps = (state) => { autoLockTimeLimit, useNonceField, dismissSeedBackUpReminder, + overrideContentSecurityPolicyHeader, }; }; @@ -81,6 +84,9 @@ export const mapDispatchToProps = (dispatch) => { setDismissSeedBackUpReminder: (value) => { return dispatch(setDismissSeedBackUpReminder(value)); }, + setOverrideContentSecurityPolicyHeader: (value) => { + return dispatch(setOverrideContentSecurityPolicyHeader(value)); + }, }; }; diff --git a/ui/pages/settings/advanced-tab/advanced-tab.stories.js b/ui/pages/settings/advanced-tab/advanced-tab.stories.js index 36c84cbba8c0..855bdd8aa86a 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.stories.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.stories.js @@ -12,6 +12,7 @@ export default { showFiatInTestnets: { control: 'boolean' }, useLedgerLive: { control: 'boolean' }, dismissSeedBackUpReminder: { control: 'boolean' }, + overrideContentSecurityPolicyHeader: { control: 'boolean' }, setAutoLockTimeLimit: { action: 'setAutoLockTimeLimit' }, setShowFiatConversionOnTestnetsPreference: { action: 'setShowFiatConversionOnTestnetsPreference', @@ -20,6 +21,9 @@ export default { setIpfsGateway: { action: 'setIpfsGateway' }, setIsIpfsGatewayEnabled: { action: 'setIsIpfsGatewayEnabled' }, setDismissSeedBackUpReminder: { action: 'setDismissSeedBackUpReminder' }, + setOverrideContentSecurityPolicyHeader: { + action: 'setOverrideContentSecurityPolicyHeader', + }, setUseNonceField: { action: 'setUseNonceField' }, setHexDataFeatureFlag: { action: 'setHexDataFeatureFlag' }, displayErrorInSettings: { action: 'displayErrorInSettings' }, @@ -38,6 +42,7 @@ export const DefaultStory = (args) => { sendHexData, showFiatInTestnets, dismissSeedBackUpReminder, + overrideContentSecurityPolicyHeader, }, updateArgs, ] = useArgs(); @@ -65,6 +70,12 @@ export const DefaultStory = (args) => { dismissSeedBackUpReminder: !dismissSeedBackUpReminder, }); }; + + const handleOverrideContentSecurityPolicyHeader = () => { + updateArgs({ + overrideContentSecurityPolicyHeader: !overrideContentSecurityPolicyHeader, + }); + }; return (
{ setShowFiatConversionOnTestnetsPreference={handleShowFiatInTestnets} dismissSeedBackUpReminder={dismissSeedBackUpReminder} setDismissSeedBackUpReminder={handleDismissSeedBackUpReminder} + overrideContentSecurityPolicyHeader={ + overrideContentSecurityPolicyHeader + } + setOverrideContentSecurityPolicyHeader={ + handleOverrideContentSecurityPolicyHeader + } ipfsGateway="ipfs-gateway" />
@@ -91,4 +108,5 @@ DefaultStory.args = { showFiatInTestnets: false, useLedgerLive: false, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, }; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 4f66067a5759..67de497817c8 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4218,6 +4218,18 @@ export function setDismissSeedBackUpReminder( }; } +export function setOverrideContentSecurityPolicyHeader( + value: boolean, +): ThunkAction { + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch(showLoadingIndication()); + await submitRequestToBackground('setOverrideContentSecurityPolicyHeader', [ + value, + ]); + dispatch(hideLoadingIndication()); + }; +} + export function getRpcMethodPreferences(): ThunkAction< void, MetaMaskReduxState, diff --git a/yarn.lock b/yarn.lock index f22f36057099..4352ef21767a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11134,6 +11134,15 @@ __metadata: languageName: node linkType: hard +"@types/serve-handler@npm:^6.1.4": + version: 6.1.4 + resolution: "@types/serve-handler@npm:6.1.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/c92ae204605659b37202af97cfcc7690be43b9290692c1d6c3c93805b399044fd67573af4eb2e7b1fd975451db6d0d5c6cd2f09b20997209fa3341f345f661e4 + languageName: node + linkType: hard + "@types/serve-index@npm:^1.9.4": version: 1.9.4 resolution: "@types/serve-index@npm:1.9.4" @@ -26654,6 +26663,7 @@ __metadata: "@types/redux-mock-store": "npm:1.0.6" "@types/remote-redux-devtools": "npm:^0.5.5" "@types/selenium-webdriver": "npm:^4.1.19" + "@types/serve-handler": "npm:^6.1.4" "@types/sinon": "npm:^10.0.13" "@types/sprintf-js": "npm:^1" "@types/w3c-web-hid": "npm:^1.0.3" From 82fdd64f5787ac4da20d2da6ddcd55787d136163 Mon Sep 17 00:00:00 2001 From: Nick Gambino <35090461+gambinish@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:16:31 -0800 Subject: [PATCH 10/18] fix: Bug 28347 - Privacy mode tweaks (#28367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Privacy Mode should only effect PortfolioView and main account picker popover. It should not impact other areas of the App like Send/Swap/Gas because the toggle only exists on PortfolioView. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28367?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28347 ## **Manual testing steps** You can toggle privacyMode with eyeball on main PortfolioView Should respect privacyMode: 1. Go to PortfolioView, toggling eyeball should show/hide balances for tokens as well as main balance 2. Go to AccountPicker from main Portfolio View, balances should hide/show 3. Go to AccountPicker from asset detail view, balances should hide/show Should _not_ respect privacyMode: 1. Go to AssetDetails, token balance should show on main page 2. Should not be respected on send/swap/gas screens 3. Balances should not be impacted elsewhere in the app. Please try to verify this while reviewing. ## **Screenshots/Recordings** https://github.com/user-attachments/assets/695fa68a-c9bb-4871-b03c-8c41c88b1344 ## **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** - [ ] 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/components/app/assets/token-cell/token-cell.tsx | 5 +++-- ui/components/app/assets/token-list/token-list.tsx | 4 +++- .../user-preferenced-currency-display.component.d.ts | 1 + .../user-preferenced-currency-display.component.js | 3 +++ ui/components/app/wallet-overview/coin-overview.tsx | 6 +++--- .../multichain/account-list-item/account-list-item.js | 7 +++++++ .../multichain/account-list-menu/account-list-menu.tsx | 3 +++ .../ui/currency-display/currency-display.component.js | 5 ++--- ui/pages/routes/routes.component.js | 7 ++++++- ui/pages/routes/routes.container.js | 3 ++- 10 files changed, 33 insertions(+), 11 deletions(-) diff --git a/ui/components/app/assets/token-cell/token-cell.tsx b/ui/components/app/assets/token-cell/token-cell.tsx index 3a042de1ebb8..31bb388aa65b 100644 --- a/ui/components/app/assets/token-cell/token-cell.tsx +++ b/ui/components/app/assets/token-cell/token-cell.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { getTokenList, getPreferences } from '../../../../selectors'; +import { getTokenList } from '../../../../selectors'; import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; import { TokenListItem } from '../../../multichain'; import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils'; @@ -12,6 +12,7 @@ type TokenCellProps = { symbol: string; string?: string; image: string; + privacyMode?: boolean; onClick?: (arg: string) => void; }; @@ -20,10 +21,10 @@ export default function TokenCell({ image, symbol, string, + privacyMode = false, onClick, }: TokenCellProps) { const tokenList = useSelector(getTokenList); - const { privacyMode } = useSelector(getPreferences); const tokenData = Object.values(tokenList).find( (token) => isEqualCaseInsensitive(token.symbol, symbol) && diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 11190c68f267..f0b17d686026 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -30,7 +30,8 @@ export default function TokenList({ nativeToken, }: TokenListProps) { const t = useI18nContext(); - const { tokenSortConfig, tokenNetworkFilter } = useSelector(getPreferences); + const { tokenSortConfig, tokenNetworkFilter, privacyMode } = + useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const conversionRate = useSelector(getConversionRate); const nativeTokenWithBalance = useNativeTokenBalance(); @@ -88,6 +89,7 @@ export default function TokenList({ ); diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts index 4db61d568f4a..779309858a18 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts @@ -16,6 +16,7 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion< showCurrencySuffix?: boolean; shouldCheckShowNativeToken?: boolean; isAggregatedFiatOverviewBalance?: boolean; + privacyMode?: boolean; } >; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index 613b731d0a16..a466f7813672 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -28,6 +28,7 @@ export default function UserPreferencedCurrencyDisplay({ showNative, showCurrencySuffix, shouldCheckShowNativeToken, + privacyMode = false, ...restProps }) { // NOTE: When displaying currencies, we need the actual account to detect whether we're in a @@ -83,6 +84,7 @@ export default function UserPreferencedCurrencyDisplay({ numberOfDecimals={numberOfDecimals} prefixComponent={prefixComponent} suffix={showCurrencySuffix && !showEthLogo && currency} + privacyMode={privacyMode} /> ); } @@ -126,6 +128,7 @@ const UserPreferencedCurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, shouldCheckShowNativeToken: PropTypes.bool, + privacyMode: PropTypes.bool, }; UserPreferencedCurrencyDisplay.propTypes = diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index 9f267c96a53d..93d9e1061428 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -132,7 +132,8 @@ export const CoinOverview = ({ const shouldShowPopover = useSelector(getShouldShowAggregatedBalancePopover); const isTestnet = useSelector(getIsTestnet); - const { showFiatInTestnets, privacyMode } = useSelector(getPreferences); + const { showFiatInTestnets, privacyMode, showNativeTokenAsMainBalance } = + useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const shouldHideZeroBalanceTokens = useSelector( @@ -143,8 +144,6 @@ export const CoinOverview = ({ shouldHideZeroBalanceTokens, ); - const { showNativeTokenAsMainBalance } = useSelector(getPreferences); - const isEvm = useSelector(getMultichainIsEvm); const isNotAggregatedFiatBalance = showNativeTokenAsMainBalance || isTestnet || !isEvm; @@ -281,6 +280,7 @@ export const CoinOverview = ({ isAggregatedFiatOverviewBalance={ !showNativeTokenAsMainBalance && !isTestnet } + privacyMode={privacyMode} /> { const t = useI18nContext(); const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false); @@ -313,6 +314,7 @@ const AccountListItem = ({ type={PRIMARY} showFiat={showFiat} data-testid="first-currency-display" + privacyMode={privacyMode} />
@@ -360,6 +362,7 @@ const AccountListItem = ({ type={SECONDARY} showNative data-testid="second-currency-display" + privacyMode={privacyMode} /> @@ -507,6 +510,10 @@ AccountListItem.propTypes = { * Determines if list item should be scrolled to when selected */ shouldScrollToWhenSelected: PropTypes.bool, + /** + * Determines if list balance should be obfuscated + */ + privacyMode: PropTypes.bool, }; AccountListItem.displayName = 'AccountListItem'; diff --git a/ui/components/multichain/account-list-menu/account-list-menu.tsx b/ui/components/multichain/account-list-menu/account-list-menu.tsx index eff0d3cb8868..cfb49d246ca6 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx @@ -188,6 +188,7 @@ export const mergeAccounts = ( type AccountListMenuProps = { onClose: () => void; + privacyMode?: boolean; showAccountCreation?: boolean; accountListItemProps?: object; allowedAccountTypes?: KeyringAccountType[]; @@ -195,6 +196,7 @@ type AccountListMenuProps = { export const AccountListMenu = ({ onClose, + privacyMode = false, showAccountCreation = true, accountListItemProps, allowedAccountTypes = [ @@ -644,6 +646,7 @@ export const AccountListMenu = ({ isHidden={Boolean(account.hidden)} currentTabOrigin={currentTabOrigin} isActive={Boolean(account.active)} + privacyMode={privacyMode} {...accountListItemProps} /> diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js index a0bb114409f6..7e2569ffaee3 100644 --- a/ui/components/ui/currency-display/currency-display.component.js +++ b/ui/components/ui/currency-display/currency-display.component.js @@ -1,10 +1,8 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; import { EtherDenomination } from '../../../../shared/constants/common'; -import { getPreferences } from '../../../selectors'; import { SensitiveText, Box } from '../../component-library'; import { AlignItems, @@ -35,9 +33,9 @@ export default function CurrencyDisplay({ textProps = {}, suffixProps = {}, isAggregatedFiatOverviewBalance = false, + privacyMode = false, ...props }) { - const { privacyMode } = useSelector(getPreferences); const [title, parts] = useCurrencyDisplay(value, { account, displayValue, @@ -125,6 +123,7 @@ const CurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, isAggregatedFiatOverviewBalance: PropTypes.bool, + privacyMode: PropTypes.bool, }; CurrencyDisplay.propTypes = CurrencyDisplayPropTypes; diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index e26e17be9e23..83e707c30f85 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -138,6 +138,7 @@ export default class Routes extends Component { history: PropTypes.object, location: PropTypes.object, autoLockTimeLimit: PropTypes.number, + privacyMode: PropTypes.bool, pageChanged: PropTypes.func.isRequired, browserEnvironmentOs: PropTypes.string, browserEnvironmentBrowser: PropTypes.string, @@ -417,6 +418,7 @@ export default class Routes extends Component { switchedNetworkDetails, clearSwitchedNetworkDetails, clearEditedNetwork, + privacyMode, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) isShowKeyringSnapRemovalResultModal, hideShowKeyringSnapRemovalResultModal, @@ -494,7 +496,10 @@ export default class Routes extends Component { ///: END:ONLY_INCLUDE_IF } {isAccountMenuOpen ? ( - toggleAccountMenu()} /> + toggleAccountMenu()} + privacyMode={privacyMode} + /> ) : null} {isNetworkMenuOpen ? ( Date: Thu, 7 Nov 2024 17:34:47 -0800 Subject: [PATCH 11/18] build: update yarn to v4.5.1 (#28365) ## **Description** Updates yarn to the latest v4.5.1, and adjusts LavaMoat policies to accommodate. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28365?quickstart=1) --- .../generate-attributions/package.json | 2 +- lavamoat/build-system/policy.json | 20 ++----------------- package.json | 2 +- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/development/generate-attributions/package.json b/development/generate-attributions/package.json index 92bf1a5153c2..5778cbd0c634 100644 --- a/development/generate-attributions/package.json +++ b/development/generate-attributions/package.json @@ -9,7 +9,7 @@ }, "engines": { "node": ">= 20", - "yarn": "^4.4.1" + "yarn": "^4.5.1" }, "lavamoat": { "allowScripts": { diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 5a607452526c..2d6b7ce3ad14 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -3004,7 +3004,7 @@ "eslint-plugin-prettier": true, "eslint-plugin-react": true, "eslint-plugin-react-hooks": true, - "eslint>@eslint/eslintrc>ajv": true, + "eslint>ajv": true, "eslint>globals": true, "eslint>ignore": true, "eslint>minimatch": true, @@ -3012,17 +3012,6 @@ "nock>debug": true } }, - "eslint>@eslint/eslintrc>ajv": { - "globals": { - "console": true - }, - "packages": { - "@metamask/snaps-utils>fast-json-stable-stringify": true, - "eslint>@eslint/eslintrc>ajv>json-schema-traverse": true, - "eslint>fast-deep-equal": true, - "uri-js": true - } - }, "eslint>@eslint/eslintrc>import-fresh": { "builtin": { "path.dirname": true @@ -8491,17 +8480,12 @@ "process.stdout.write": true }, "packages": { + "eslint>ajv": true, "lodash": true, - "stylelint>table>ajv": true, "stylelint>table>slice-ansi": true, "stylelint>table>string-width": true } }, - "stylelint>table>ajv": { - "packages": { - "eslint>fast-deep-equal": true - } - }, "stylelint>table>slice-ansi": { "packages": { "stylelint>table>slice-ansi>ansi-styles": true, diff --git a/package.json b/package.json index c612dd76fb20..338e3eaa2068 100644 --- a/package.json +++ b/package.json @@ -751,5 +751,5 @@ "jest-preview": false } }, - "packageManager": "yarn@4.4.1" + "packageManager": "yarn@4.5.1" } From 04dd7d318ec039640960f1fcd4ba26acedb7940e Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 8 Nov 2024 10:17:15 +0000 Subject: [PATCH 12/18] fix: gas limit estimation (#28327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Upgrade transaction controller to fix gas limit estimation on specific networks. ## **Related issues** Fixes: #28307 #28175 ## **Manual testing steps** See issue. ## **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** - [ ] 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. --- package.json | 2 +- test/e2e/flask/user-operations.spec.ts | 6 +++-- .../tests/transaction/edit-gas-fee.spec.js | 16 +++++------ .../transaction/multiple-transactions.spec.js | 27 ++++++++----------- yarn.lock | 12 ++++----- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 338e3eaa2068..13bddaa69287 100644 --- a/package.json +++ b/package.json @@ -347,7 +347,7 @@ "@metamask/snaps-sdk": "^6.10.0", "@metamask/snaps-utils": "^8.5.1", "@metamask/solana-wallet-snap": "^0.1.9", - "@metamask/transaction-controller": "^38.1.0", + "@metamask/transaction-controller": "^38.3.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^10.0.1", "@ngraveio/bc-ur": "^1.1.12", diff --git a/test/e2e/flask/user-operations.spec.ts b/test/e2e/flask/user-operations.spec.ts index be7141444c97..7512e0b563c9 100644 --- a/test/e2e/flask/user-operations.spec.ts +++ b/test/e2e/flask/user-operations.spec.ts @@ -256,7 +256,8 @@ describe('User Operations', function () { from: ERC_4337_ACCOUNT, to: GANACHE_ACCOUNT, value: convertETHToHexGwei(1), - data: '0x', + maxFeePerGas: '0x0', + maxPriorityFeePerGas: '0x0', }); await confirmTransaction(driver); @@ -294,7 +295,8 @@ describe('User Operations', function () { from: ERC_4337_ACCOUNT, to: GANACHE_ACCOUNT, value: convertETHToHexGwei(1), - data: '0x', + maxFeePerGas: '0x0', + maxPriorityFeePerGas: '0x0', }); await confirmTransaction(driver); diff --git a/test/e2e/tests/transaction/edit-gas-fee.spec.js b/test/e2e/tests/transaction/edit-gas-fee.spec.js index 85ae4da3a31f..918831f8f3ad 100644 --- a/test/e2e/tests/transaction/edit-gas-fee.spec.js +++ b/test/e2e/tests/transaction/edit-gas-fee.spec.js @@ -1,11 +1,11 @@ const { strict: assert } = require('assert'); const { createInternalTransaction, + createDappTransaction, } = require('../../page-objects/flows/transaction'); const { withFixtures, - openDapp, unlockWallet, generateGanacheOptions, WINDOW_TITLES, @@ -172,11 +172,9 @@ describe('Editing Confirm Transaction', function () { // login to extension await unlockWallet(driver); - // open dapp and connect - await openDapp(driver); - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', + await createDappTransaction(driver, { + maxFeePerGas: '0x2000000000', + maxPriorityFeePerGas: '0x1000000000', }); // check transaction in extension popup @@ -198,12 +196,12 @@ describe('Editing Confirm Transaction', function () { '.currency-display-component__text', ); const transactionAmount = transactionAmounts[0]; - assert.equal(await transactionAmount.getText(), '0'); + assert.equal(await transactionAmount.getText(), '0.001'); // has correct updated value on the confirm screen the transaction await driver.waitForSelector({ css: '.currency-display-component__text', - text: '0.00021', + text: '0.00185144', }); // confirms the transaction @@ -227,7 +225,7 @@ describe('Editing Confirm Transaction', function () { '[data-testid="transaction-list-item-primary-currency"]', ); assert.equal(txValues.length, 1); - assert.ok(/-0\s*ETH/u.test(await txValues[0].getText())); + assert.ok(/-0.001\s*ETH/u.test(await txValues[0].getText())); }, ); }); diff --git a/test/e2e/tests/transaction/multiple-transactions.spec.js b/test/e2e/tests/transaction/multiple-transactions.spec.js index 8f1318c31e3b..4d913cb07edb 100644 --- a/test/e2e/tests/transaction/multiple-transactions.spec.js +++ b/test/e2e/tests/transaction/multiple-transactions.spec.js @@ -26,19 +26,13 @@ describe('Multiple transactions', function () { // initiates a transaction from the dapp await openDapp(driver); // creates first transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // creates second transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // confirms second transaction @@ -94,19 +88,13 @@ describe('Multiple transactions', function () { // initiates a transaction from the dapp await openDapp(driver); // creates first transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // creates second transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // rejects second transaction @@ -141,3 +129,10 @@ describe('Multiple transactions', function () { ); }); }); + +async function createDappTransaction(driver) { + await driver.clickElement({ + text: 'Send EIP 1559 Without Gas', + tag: 'button', + }); +} diff --git a/yarn.lock b/yarn.lock index 4352ef21767a..bb91a8129ac2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6458,9 +6458,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^38.1.0": - version: 38.1.0 - resolution: "@metamask/transaction-controller@npm:38.1.0" +"@metamask/transaction-controller@npm:^38.3.0": + version: 38.3.0 + resolution: "@metamask/transaction-controller@npm:38.3.0" dependencies: "@ethereumjs/common": "npm:^3.2.0" "@ethereumjs/tx": "npm:^4.2.0" @@ -6469,7 +6469,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.1" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/eth-query": "npm:^4.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/nonce-tracker": "npm:^6.0.0" @@ -6487,7 +6487,7 @@ __metadata: "@metamask/approval-controller": ^7.0.0 "@metamask/gas-fee-controller": ^22.0.0 "@metamask/network-controller": ^22.0.0 - checksum: 10/c1bdca52bbbce42a76ec9c640197534ec6c223b0f5d5815acfa53490dc1175850ea9aeeb6ae3c5ec34218f0bdbbbeb3e8731e2552aa9411e3ed7798a5dea8ab5 + checksum: 10/f4e8e3a1a31e3e62b0d1a59bbe15ebfa4dc3e4cf077fb95c1815c00661c60ef4676046c49f57eab9749cd31d3e55ac3fed7bc247e3f5a3d459f2dcb03998633d languageName: node linkType: hard @@ -26590,7 +26590,7 @@ __metadata: "@metamask/solana-wallet-snap": "npm:^0.1.9" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.13.0" - "@metamask/transaction-controller": "npm:^38.1.0" + "@metamask/transaction-controller": "npm:^38.3.0" "@metamask/user-operation-controller": "npm:^13.0.0" "@metamask/utils": "npm:^10.0.1" "@ngraveio/bc-ur": "npm:^1.1.12" From c5643837734ce1cbdd0b2e55b5c35aa5accef816 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 8 Nov 2024 10:25:30 +0000 Subject: [PATCH 13/18] refactor: remove global network usage from transaction confirmations (#28236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Remove usages of global network selectors from transaction confirmation React components and hooks. Specifically: - Remove usages of the following selectors: - `getConversionRate` - `getCurrentChainId` - `getNativeCurrency` - `getNetworkIdentifier` - `getNftContracts` - `getNfts` - `getProviderConfig` - `getRpcPrefsForCurrentProvider` - Add new selectors: - `selectConversionRateByChainId` - `selectNftsByChainId` - `selectNftContractsByChainId` - `selectNetworkIdentifierByChainId` - Add ESLint rule to prevent further usage of global network selectors in confirmations directory. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28236?quickstart=1) ## **Related issues** Fixes: [#3469](https://github.com/MetaMask/MetaMask-planning/issues/3469) [#3373](https://github.com/MetaMask/MetaMask-planning/issues/3373) [#3486](https://github.com/MetaMask/MetaMask-planning/issues/3486) [#3487](https://github.com/MetaMask/MetaMask-planning/issues/3487) ## **Manual testing steps** Full regression of all transaction confirmations and related functionality. ## **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** - [ ] 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: Jyoti Puri --- .eslintrc.js | 25 +++++++++++ .storybook/preview.js | 5 ++- .../confirmations/contract-interaction.ts | 3 +- .../advanced-gas-fee-defaults.js | 14 +++--- .../advanced-gas-fee-defaults.test.js | 4 +- .../confirm-hexdata/confirm-hexdata.test.js | 18 +++++++- .../confirm-page-container.component.js | 12 +++-- .../confirm-page-container.container.js | 3 -- .../confirm-subtitle/confirm-subtitle.test.js | 5 ++- .../approve-static-simulation.tsx | 1 + .../confirm/info/approve/approve.tsx | 1 + .../edit-spending-cap-modal.tsx | 1 + .../approve/spending-cap/spending-cap.tsx | 1 + .../confirm/info/hooks/use-token-values.ts | 1 + .../confirm/info/hooks/useFeeCalculations.ts | 12 +++-- .../advanced-details.stories.tsx | 2 +- .../native-send-heading.tsx | 13 ++++-- .../nft-send-heading/nft-send-heading.tsx | 2 + .../transaction-data.stories.tsx | 1 - .../transaction-details.stories.tsx | 3 -- .../title/hooks/useCurrentSpendingCap.ts | 8 +++- .../contract-details-modal.js | 2 +- .../contract-token-values.js | 8 ++-- .../useBalanceChanges.test.ts | 17 ++++--- .../simulation-details/useBalanceChanges.ts | 11 +++-- .../transaction-alerts/transaction-alerts.js | 11 ++++- .../transaction-alerts.stories.js | 42 ++++++----------- .../transaction-alerts.test.js | 40 ++++++++++++++++- .../confirm-approve-content.component.js | 12 ++--- .../confirm-approve/confirm-approve.js | 26 ++++++----- .../confirm-send-token/confirm-send-token.js | 20 ++++++--- .../confirm-token-transaction-base.js | 37 +++++++++------ .../confirm-transaction-base.component.js | 11 +---- .../confirm-transaction-base.container.js | 31 +++++++++---- .../confirm-transaction-base.test.js | 41 ----------------- .../confirm-token-transaction-switch.js | 4 +- .../contract-interaction.stories.tsx | 19 ++++---- .../confirmations/confirm/stories/utils.tsx | 21 +-------- ui/pages/confirmations/hooks/test-utils.js | 12 ++--- .../confirmations/hooks/useAssetDetails.js | 13 ++++-- .../hooks/useTransactionFunctionType.js | 16 +++++-- .../hooks/useTransactionFunctionType.test.js | 27 ++++++++--- .../confirmations/hooks/useTransactionInfo.js | 3 +- .../hooks/useTransactionInfo.test.js | 7 ++- .../send/gas-display/gas-display.js | 18 ++++---- .../token-allowance.test.js.snap | 14 +++--- .../token-allowance/token-allowance.js | 28 +++++++++--- .../token-allowance/token-allowance.test.js | 2 +- ui/selectors/selectors.js | 45 +++++++++++++++++++ 49 files changed, 416 insertions(+), 257 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 846158a741ef..4aa9ef688592 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -472,5 +472,30 @@ module.exports = { '@metamask/design-tokens/color-no-hex': 'off', }, }, + { + files: ['ui/pages/confirmations/**/*.{js,ts,tsx}'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: `ImportSpecifier[imported.name=/${[ + 'getConversionRate', + 'getCurrentChainId', + 'getNativeCurrency', + 'getNetworkIdentifier', + 'getNftContracts', + 'getNfts', + 'getProviderConfig', + 'getRpcPrefsForCurrentProvider', + 'getUSDConversionRate', + 'isCurrentProviderCustom', + ] + .map((method) => `(${method})`) + .join('|')}/]`, + message: 'Avoid using global network selectors in confirmations', + }, + ], + }, + }, ], }; diff --git a/.storybook/preview.js b/.storybook/preview.js index b46c91339273..525c364f2072 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -15,6 +15,7 @@ import MetaMetricsProviderStorybook from './metametrics'; import testData from './test-data.js'; import { Router } from 'react-router-dom'; import { createBrowserHistory } from 'history'; +import { MemoryRouter } from 'react-router-dom'; import { setBackgroundConnection } from '../ui/store/background-connection'; import { metamaskStorybookTheme } from './metamask-storybook-theme'; import { DocsContainer } from '@storybook/addon-docs'; @@ -147,7 +148,7 @@ const metamaskDecorator = (story, context) => { return ( - + { - + ); }; diff --git a/test/data/confirmations/contract-interaction.ts b/test/data/confirmations/contract-interaction.ts index 49a6e1aad1ab..cbe5dd2bd2ba 100644 --- a/test/data/confirmations/contract-interaction.ts +++ b/test/data/confirmations/contract-interaction.ts @@ -1,4 +1,5 @@ import { + CHAIN_IDS, SimulationData, TransactionMeta, TransactionStatus, @@ -18,7 +19,7 @@ export const CONTRACT_INTERACTION_SENDER_ADDRESS = export const DEPOSIT_METHOD_DATA = '0xd0e30db0'; -export const CHAIN_ID = '0xaa36a7'; +export const CHAIN_ID = CHAIN_IDS.GOERLI; export const genUnapprovedContractInteractionConfirmation = ({ address = CONTRACT_INTERACTION_SENDER_ADDRESS, diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js index a52b4c16f893..106984363699 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js @@ -11,8 +11,7 @@ import { } from '../../../../../helpers/constants/design-system'; import { getAdvancedGasFeeValues, - getCurrentChainId, - getNetworkIdentifier, + selectNetworkIdentifierByChainId, } from '../../../../../selectors'; import { setAdvancedGasFee } from '../../../../../store/actions'; import { useGasFeeContext } from '../../../../../contexts/gasFee'; @@ -35,11 +34,14 @@ const AdvancedGasFeeDefaults = () => { 10, ).toString(); const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues); - // This will need to use a different chainId in multinetwork - const chainId = useSelector(getCurrentChainId); - const networkIdentifier = useSelector(getNetworkIdentifier); const { updateTransactionEventFragment } = useTransactionEventFragment(); - const { editGasMode } = useGasFeeContext(); + const { editGasMode, transaction } = useGasFeeContext(); + const { chainId } = transaction; + + const networkIdentifier = useSelector((state) => + selectNetworkIdentifierByChainId(state, chainId), + ); + const [isDefaultSettingsSelected, setDefaultSettingsSelected] = useState( Boolean(advancedGasFeeValues) && advancedGasFeeValues.maxBaseFee === maxBaseFee && diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js index 5e330a8b90c7..07c7fc8faaab 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js @@ -21,6 +21,7 @@ import { mockNetworkState } from '../../../../../../test/stub/networks'; import AdvancedGasFeeDefaults from './advanced-gas-fee-defaults'; const TEXT_SELECTOR = 'Save these values as my default for the Goerli network.'; +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; jest.mock('../../../../../store/actions', () => ({ gasFeeStartPollingByNetworkClientId: jest @@ -43,7 +44,7 @@ const render = async (defaultGasParams, contextParams) => { metamask: { ...mockState.metamask, ...defaultGasParams, - ...mockNetworkState({ chainId: CHAIN_IDS.GOERLI }), + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), accounts: { [mockSelectedInternalAccount.address]: { address: mockSelectedInternalAccount.address, @@ -71,6 +72,7 @@ const render = async (defaultGasParams, contextParams) => { (result = renderWithProvider( { - const store = configureStore(mockState); + const store = configureStore(STATE_MOCK); it('should render function type', async () => { const { findByText } = renderWithProvider( { const { container } = renderWithProvider( { const { container } = renderWithProvider( { const { getByText } = renderWithProvider( { useState(false); const isBuyableChain = useSelector(getIsNativeTokenBuyable); const contact = useSelector((state) => getAddressBookEntry(state, toAddress)); - const networkIdentifier = useSelector(getNetworkIdentifier); const defaultToken = useSelector(getSwapsDefaultToken); const accountBalance = defaultToken.string; const internalAccounts = useSelector(getInternalAccounts); @@ -139,8 +138,13 @@ const ConfirmPageContainer = (props) => { const shouldDisplayWarning = contentComponent && disabled && (errorKey || errorMessage); - const networkName = - NETWORK_TO_NAME_MAP[currentTransaction.chainId] || networkIdentifier; + const { chainId } = currentTransaction; + + const networkIdentifier = useSelector((state) => + selectNetworkIdentifierByChainId(state, chainId), + ); + + const networkName = NETWORK_TO_NAME_MAP[chainId] || networkIdentifier; const fetchCollectionBalance = useCallback(async () => { const tokenBalance = await fetchTokenBalance( diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js index db07ad4117e7..23eaa43d44ab 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { getAddressBookEntry, - getNetworkIdentifier, getSwapsDefaultToken, getMetadataContractName, getAccountName, @@ -12,7 +11,6 @@ import ConfirmPageContainer from './confirm-page-container.component'; function mapStateToProps(state, ownProps) { const to = ownProps.toAddress; const contact = getAddressBookEntry(state, to); - const networkIdentifier = getNetworkIdentifier(state); const defaultToken = getSwapsDefaultToken(state); const accountBalance = defaultToken.string; const internalAccounts = getInternalAccounts(state); @@ -26,7 +24,6 @@ function mapStateToProps(state, ownProps) { toMetadataName, recipientIsOwnedAccount: Boolean(ownedAccountName), to, - networkIdentifier, accountBalance, }; } diff --git a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js index baa2d112b671..fa00f0275350 100644 --- a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js +++ b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js @@ -1,11 +1,11 @@ import React from 'react'; import { ERC1155, ERC721 } from '@metamask/controller-utils'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import mockState from '../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import configureStore from '../../../../store/store'; import { getSelectedInternalAccountFromMockState } from '../../../../../test/jest/mocks'; -import { getProviderConfig } from '../../../../ducks/metamask/metamask'; import ConfirmSubTitle from './confirm-subtitle'; const mockSelectedInternalAccount = @@ -51,7 +51,7 @@ describe('ConfirmSubTitle', () => { mockState.metamask.preferences.showFiatInTestnets = false; mockState.metamask.allNftContracts = { [mockSelectedInternalAccount.address]: { - [getProviderConfig(mockState).chainId]: [{ address: '0x9' }], + [CHAIN_IDS.GOERLI]: [{ address: '0x9' }], }, }; store = configureStore(mockState); @@ -59,6 +59,7 @@ describe('ConfirmSubTitle', () => { const { findByText } = renderWithProvider( { transactionMeta?.txParams?.to, transactionMeta?.txParams?.from, transactionMeta?.txParams?.data, + transactionMeta?.chainId, ); const decimals = initialDecimals || '0'; diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx index fa1766a8aa72..d1d9fbc08364 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx @@ -30,6 +30,7 @@ const ApproveInfo = () => { transactionMeta.txParams.to, transactionMeta.txParams.from, transactionMeta.txParams.data, + transactionMeta.chainId, ); const { spendingCap, pending } = useApproveTokenSimulation( diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx index 961e63ae8f92..1eb0ac2cde05 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx @@ -54,6 +54,7 @@ export const EditSpendingCapModal = ({ transactionMeta.txParams.to, transactionMeta.txParams.from, transactionMeta.txParams.data, + transactionMeta.chainId, ); const accountBalance = calcTokenAmount( diff --git a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx index 638ff638844c..8e7b522f050a 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx @@ -87,6 +87,7 @@ export const SpendingCap = ({ transactionMeta.txParams.to, transactionMeta.txParams.from, transactionMeta.txParams.data, + transactionMeta.chainId, ); const accountBalance = calcTokenAmount( diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts index f416282ee4ef..087a17c473c6 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts @@ -17,6 +17,7 @@ export const useTokenValues = (transactionMeta: TransactionMeta) => { transactionMeta.txParams.to, transactionMeta.txParams.from, transactionMeta.txParams.data, + transactionMeta.chainId, ); const decodedResponse = useDecodedTransactionData(); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts index 70bd2c0e3af2..5b54dcc710b8 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts @@ -12,10 +12,12 @@ import { multiplyHexes, } from '../../../../../../../shared/modules/conversion.utils'; import { Numeric } from '../../../../../../../shared/modules/Numeric'; -import { getConversionRate } from '../../../../../../ducks/metamask/metamask'; import { useFiatFormatter } from '../../../../../../hooks/useFiatFormatter'; import { useGasFeeEstimates } from '../../../../../../hooks/useGasFeeEstimates'; -import { getCurrentCurrency } from '../../../../../../selectors'; +import { + getCurrentCurrency, + selectConversionRateByChainId, +} from '../../../../../../selectors'; import { getMultichainNetwork } from '../../../../../../selectors/multichain'; import { HEX_ZERO } from '../shared/constants'; import { useEIP1559TxFees } from './useEIP1559TxFees'; @@ -30,9 +32,13 @@ const EMPTY_FEES = { export function useFeeCalculations(transactionMeta: TransactionMeta) { const currentCurrency = useSelector(getCurrentCurrency); - const conversionRate = useSelector(getConversionRate); + const { chainId } = transactionMeta; const fiatFormatter = useFiatFormatter(); + const conversionRate = useSelector((state) => + selectConversionRateByChainId(state, chainId), + ); + const multichainNetwork = useSelector(getMultichainNetwork); const ticker = multichainNetwork?.network?.ticker; diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx index 8654400a0bfa..aa9ec6cdc87c 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx @@ -22,6 +22,6 @@ const Story = { export default Story; -export const DefaultStory = () => ; +export const DefaultStory = () => ; DefaultStory.storyName = 'Default'; diff --git a/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx index bd5c7ba8b4f2..9eb70a856464 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx @@ -14,7 +14,6 @@ import { } from '../../../../../../../components/component-library'; import Tooltip from '../../../../../../../components/ui/tooltip'; import { getIntlLocale } from '../../../../../../../ducks/locale/locale'; -import { getConversionRate } from '../../../../../../../ducks/metamask/metamask'; import { AlignItems, Display, @@ -25,7 +24,10 @@ import { } from '../../../../../../../helpers/constants/design-system'; import { MIN_AMOUNT } from '../../../../../../../hooks/useCurrencyDisplay'; import { useFiatFormatter } from '../../../../../../../hooks/useFiatFormatter'; -import { getPreferences } from '../../../../../../../selectors'; +import { + getPreferences, + selectConversionRateByChainId, +} from '../../../../../../../selectors'; import { getMultichainNetwork } from '../../../../../../../selectors/multichain'; import { useConfirmContext } from '../../../../../context/confirm'; import { @@ -38,11 +40,16 @@ const NativeSendHeading = () => { const { currentConfirmation: transactionMeta } = useConfirmContext(); + const { chainId } = transactionMeta; + const nativeAssetTransferValue = new BigNumber( transactionMeta.txParams.value as string, ).dividedBy(new BigNumber(10).pow(18)); - const conversionRate = useSelector(getConversionRate); + const conversionRate = useSelector((state) => + selectConversionRateByChainId(state, chainId), + ); + const fiatValue = conversionRate && nativeAssetTransferValue && diff --git a/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx index 006613e206c9..2f36d10ce42c 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx @@ -24,11 +24,13 @@ const NFTSendHeading = () => { const tokenAddress = transactionMeta.txParams.to; const userAddress = transactionMeta.txParams.from; const { data } = transactionMeta.txParams; + const { chainId } = transactionMeta; const { assetName, tokenImage, tokenId } = useAssetDetails( tokenAddress, userAddress, data, + chainId, ); const TokenImage = ; diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx index 091185ae23f3..105838af78c2 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx @@ -24,7 +24,6 @@ function getStore(transactionData?: string, to?: string) { const confirmation = { ...confirmationTemplate, - chainId: '0x1', txParams: { ...confirmationTemplate.txParams, to: to ?? confirmationTemplate.txParams.to, diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx index dfec9c8ef4d0..f37317c230c4 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx @@ -5,7 +5,6 @@ import { PAYMASTER_AND_DATA, genUnapprovedContractInteractionConfirmation, } from '../../../../../../../../test/data/confirmations/contract-interaction'; -import mockState from '../../../../../../../../test/data/mock-state.json'; import { getMockConfirmStateForTransaction } from '../../../../../../../../test/data/confirmations/helper'; import configureStore from '../../../../../../../store/store'; import { ConfirmContextProvider } from '../../../../../context/confirm'; @@ -20,9 +19,7 @@ function getStore() { return configureStore( getMockConfirmStateForTransaction(confirmation, { metamask: { - ...mockState.metamask, preferences: { - ...mockState.metamask.preferences, petnamesEnabled: true, }, userOperations: { diff --git a/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts index 5f588a971561..b50948259734 100644 --- a/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts +++ b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts @@ -32,8 +32,14 @@ export function useCurrentSpendingCap(currentConfirmation: Confirmation) { const txParamsData = isTxWithSpendingCap ? currentConfirmation.txParams.data : null; + const chainId = isTxWithSpendingCap ? currentConfirmation.chainId : null; - const { decimals } = useAssetDetails(txParamsTo, txParamsFrom, txParamsData); + const { decimals } = useAssetDetails( + txParamsTo, + txParamsFrom, + txParamsData, + chainId, + ); const { spendingCap, pending } = useApproveTokenSimulation( currentConfirmation as TransactionMeta, diff --git a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js index 795401673691..f433bf4caee3 100644 --- a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js +++ b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js @@ -338,7 +338,7 @@ ContractDetailsModal.propTypes = { */ chainId: PropTypes.string, /** - * Block explorer URL of the current network + * Block explorer URL for the contract chain */ blockExplorerUrl: PropTypes.string, /** diff --git a/ui/pages/confirmations/components/contract-token-values/contract-token-values.js b/ui/pages/confirmations/components/contract-token-values/contract-token-values.js index 6858689f77d0..0e8890b607ec 100644 --- a/ui/pages/confirmations/components/contract-token-values/contract-token-values.js +++ b/ui/pages/confirmations/components/contract-token-values/contract-token-values.js @@ -25,7 +25,7 @@ export default function ContractTokenValues({ address, tokenName, chainId, - rpcPrefs, + blockExplorerUrl, }) { const t = useI18nContext(); const [copied, handleCopy] = useCopyToClipboard(); @@ -69,7 +69,7 @@ export default function ContractTokenValues({ address, chainId, { - blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null, + blockExplorerUrl: blockExplorerUrl ?? null, }, null, ); @@ -98,7 +98,7 @@ ContractTokenValues.propTypes = { */ chainId: PropTypes.string, /** - * RPC prefs + * URL for the block explorer */ - rpcPrefs: PropTypes.object, + blockExplorerUrl: PropTypes.string, }; diff --git a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts index f77774c6ec4a..5f2e1dcbfd16 100644 --- a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts +++ b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts @@ -6,10 +6,10 @@ import { } from '@metamask/transaction-controller'; import { BigNumber } from 'bignumber.js'; import { TokenStandard } from '../../../../../shared/constants/transaction'; -import { getConversionRate } from '../../../../ducks/metamask/metamask'; import { getTokenStandardAndDetails } from '../../../../store/actions'; import { fetchTokenExchangeRates } from '../../../../helpers/utils/util'; import { memoizedGetTokenStandardAndDetails } from '../../utils/token'; +import { selectConversionRateByChainId } from '../../../../selectors'; import { useBalanceChanges } from './useBalanceChanges'; import { FIAT_UNAVAILABLE } from './types'; @@ -17,13 +17,10 @@ jest.mock('react-redux', () => ({ useSelector: jest.fn((selector) => selector()), })); -jest.mock('../../../../ducks/metamask/metamask', () => ({ - getConversionRate: jest.fn(), -})); - jest.mock('../../../../selectors', () => ({ getCurrentChainId: jest.fn(), getCurrentCurrency: jest.fn(), + selectConversionRateByChainId: jest.fn(), })); jest.mock('../../../../helpers/utils/util', () => ({ @@ -34,7 +31,9 @@ jest.mock('../../../../store/actions', () => ({ getTokenStandardAndDetails: jest.fn(), })); -const mockGetConversionRate = getConversionRate as jest.Mock; +const mockSelectConversionRateByChainId = jest.mocked( + selectConversionRateByChainId, +); const mockGetTokenStandardAndDetails = getTokenStandardAndDetails as jest.Mock; const mockFetchTokenExchangeRates = fetchTokenExchangeRates as jest.Mock; @@ -85,7 +84,7 @@ describe('useBalanceChanges', () => { } return Promise.reject(new Error('Unable to determine token standard')); }); - mockGetConversionRate.mockReturnValue(ETH_TO_FIAT_RATE); + mockSelectConversionRateByChainId.mockReturnValue(ETH_TO_FIAT_RATE); mockFetchTokenExchangeRates.mockResolvedValue({ [ERC20_TOKEN_ADDRESS_1_MOCK]: ERC20_TO_FIAT_RATE_1_MOCK, [ERC20_TOKEN_ADDRESS_2_MOCK]: ERC20_TO_FIAT_RATE_2_MOCK, @@ -344,7 +343,7 @@ describe('useBalanceChanges', () => { }); it('handles native fiat rate with more than 15 significant digits', async () => { - mockGetConversionRate.mockReturnValue(0.1234567890123456); + mockSelectConversionRateByChainId.mockReturnValue(0.1234567890123456); const { result, waitForNextUpdate } = setupHook({ ...dummyBalanceChange, difference: DIFFERENCE_ETH_MOCK, @@ -357,7 +356,7 @@ describe('useBalanceChanges', () => { }); it('handles unavailable native fiat rate', async () => { - mockGetConversionRate.mockReturnValue(null); + mockSelectConversionRateByChainId.mockReturnValue(null); const { result, waitForNextUpdate } = setupHook({ ...dummyBalanceChange, difference: DIFFERENCE_ETH_MOCK, diff --git a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts index 1bbc9cb5eec8..666682c95c76 100644 --- a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts +++ b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts @@ -10,8 +10,10 @@ import { BigNumber } from 'bignumber.js'; import { ContractExchangeRates } from '@metamask/assets-controllers'; import { useAsyncResultOrThrow } from '../../../../hooks/useAsyncResult'; import { TokenStandard } from '../../../../../shared/constants/transaction'; -import { getConversionRate } from '../../../../ducks/metamask/metamask'; -import { getCurrentCurrency } from '../../../../selectors'; +import { + getCurrentCurrency, + selectConversionRateByChainId, +} from '../../../../selectors'; import { fetchTokenExchangeRates } from '../../../../helpers/utils/util'; import { ERC20_DEFAULT_DECIMALS, fetchErc20Decimals } from '../../utils/token'; @@ -158,7 +160,10 @@ export const useBalanceChanges = ({ simulationData?: SimulationData; }): { pending: boolean; value: BalanceChange[] } => { const fiatCurrency = useSelector(getCurrentCurrency); - const nativeFiatRate = useSelector(getConversionRate); + + const nativeFiatRate = useSelector((state) => + selectConversionRateByChainId(state, chainId), + ); const { nativeBalanceChange, tokenBalanceChanges = [] } = simulationData ?? {}; diff --git a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js index a641ecbab93a..5127003a81e8 100644 --- a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js +++ b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js @@ -14,7 +14,10 @@ import { } from '../../../../components/component-library'; import SimulationErrorMessage from '../simulation-error-message'; import { SEVERITIES } from '../../../../helpers/constants/design-system'; +// eslint-disable-next-line import/no-duplicates +import { selectNetworkConfigurationByChainId } from '../../../../selectors'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +// eslint-disable-next-line import/no-duplicates import { submittedPendingTransactionsSelector } from '../../../../selectors'; import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; ///: END:ONLY_INCLUDE_IF @@ -22,7 +25,6 @@ import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; import { isSuspiciousResponse } from '../../../../../shared/modules/security-provider.utils'; import BlockaidBannerAlert from '../security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert'; import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message'; -import { getNativeCurrency } from '../../../../ducks/metamask/metamask'; import { parseStandardTokenTransactionData } from '../../../../../shared/modules/transaction.utils'; import { getTokenValueParam } from '../../../../../shared/lib/metamask-controller-utils'; import { QueuedRequestsBannerAlert } from '../../confirmation/components/queued-requests-banner-alert'; @@ -47,7 +49,12 @@ const TransactionAlerts = ({ ///: END:ONLY_INCLUDE_IF const t = useI18nContext(); - const nativeCurrency = useSelector(getNativeCurrency); + const { chainId } = txData; + + const { nativeCurrency } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const transactionData = txData.txParams.data; const currentTokenSymbol = tokenSymbol || nativeCurrency; let currentTokenAmount; diff --git a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js index ae46a4a83903..99a60a94e25d 100644 --- a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js +++ b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js @@ -10,6 +10,8 @@ import { CHAIN_IDS } from '../../../../../shared/constants/network'; import { mockNetworkState } from '../../../../../test/stub/networks'; import TransactionAlerts from '.'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + const mockSelectedInternalAccount = getSelectedInternalAccountFromMockState(testData); @@ -24,7 +26,7 @@ const customTransaction = ({ userFeeLevel: estimateUsed ? 'low' : 'medium', blockNumber: `${10902987 + i}`, id: 4678200543090545 + i, - chainId: '0x1', + chainId: CHAIN_ID_MOCK, status: 'confirmed', time: 1600654021000, txParams: { @@ -48,23 +50,14 @@ const customTransaction = ({ }; // simulate gas fee state -const customStore = ({ - supportsEIP1559, - isNetworkBusy, - pendingCount = 0, -} = {}) => { +const customStore = ({ supportsEIP1559, pendingCount = 0 } = {}) => { const data = cloneDeep({ ...testData, metamask: { ...testData?.metamask, - // isNetworkBusy - gasFeeEstimates: { - ...testData?.metamask?.gasFeeEstimates, - networkCongestion: isNetworkBusy ? 1 : 0.1, - }, // supportsEIP1559 ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, + chainId: CHAIN_ID_MOCK, metadata: { EIPS: { 1559: Boolean(supportsEIP1559), @@ -75,15 +68,12 @@ const customStore = ({ featureFlags: { ...testData?.metamask?.featureFlags, }, - incomingTransactions: { - ...testData?.metamask?.incomingTransactions, - ...Object.fromEntries( - Array.from({ length: pendingCount }).map((_, i) => { - const transaction = customTransaction({ i, status: 'submitted' }); - return [transaction?.hash, transaction]; - }), + transactions: [ + ...testData.metamask.transactions, + ...Array.from({ length: pendingCount }).map((_, i) => + customTransaction({ i, status: 'submitted' }), ), - }, + ], }, }); return configureStore(data); @@ -99,6 +89,7 @@ export default { args: { userAcknowledgedGasMissing: false, txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -129,6 +120,7 @@ DefaultStory.storyName = 'Default'; DefaultStory.args = { ...DefaultStory.args, txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x0', }, @@ -176,15 +168,6 @@ export const LowPriority = (args) => ( ); LowPriority.storyName = 'LowPriority'; -export const BusyNetwork = (args) => ( - - - - - -); -BusyNetwork.storyName = 'BusyNetwork'; - export const SendingZeroAmount = (args) => ( @@ -195,6 +178,7 @@ export const SendingZeroAmount = (args) => ( SendingZeroAmount.storyName = 'SendingZeroAmount'; SendingZeroAmount.args = { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x0', }, diff --git a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js index 6ae1a59591dd..4695120fbae7 100644 --- a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js +++ b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import sinon from 'sinon'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { SECURITY_PROVIDER_MESSAGE_SEVERITY } from '../../../../../shared/constants/security-provider'; import { renderWithProvider } from '../../../../../test/jest'; import { submittedPendingTransactionsSelector } from '../../../../selectors/transactions'; @@ -9,6 +10,7 @@ import configureStore from '../../../../store/store'; import mockState from '../../../../../test/data/mock-state.json'; import * as txUtil from '../../../../../shared/modules/transaction.utils'; import * as metamaskControllerUtils from '../../../../../shared/lib/metamask-controller-utils'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import TransactionAlerts from './transaction-alerts'; jest.mock('../../../../selectors/transactions', () => { @@ -22,17 +24,28 @@ jest.mock('../../../../contexts/gasFee'); jest.mock('../../../../selectors/account-abstraction'); +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + +const STATE_MOCK = { + ...mockState, + metamask: { + ...mockState.metamask, + ...mockNetworkState({ + chainId: CHAIN_ID_MOCK, + }), + }, +}; + function render({ componentProps = {}, useGasFeeContextValue = {}, submittedPendingTransactionsSelectorValue = null, - mockedStore = mockState, }) { useGasFeeContext.mockReturnValue(useGasFeeContextValue); submittedPendingTransactionsSelector.mockReturnValue( submittedPendingTransactionsSelectorValue, ); - const store = configureStore(mockedStore); + const store = configureStore(STATE_MOCK); return renderWithProvider(, store); } @@ -41,6 +54,7 @@ describe('TransactionAlerts', () => { const { getByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, securityAlertResponse: { resultType: 'Malicious', reason: 'blur_farming', @@ -64,6 +78,7 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, securityProviderResponse: { flagAsDangerous: '?', reason: 'Some reason...', @@ -89,6 +104,7 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, securityProviderResponse: { flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITY.NOT_MALICIOUS, }, @@ -118,6 +134,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -141,6 +158,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -160,6 +178,7 @@ describe('TransactionAlerts', () => { componentProps: { setUserAcknowledgedGasMissing, txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -181,6 +200,7 @@ describe('TransactionAlerts', () => { componentProps: { userAcknowledgedGasMissing: true, txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -199,6 +219,7 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -220,6 +241,7 @@ describe('TransactionAlerts', () => { submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }], componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -242,6 +264,7 @@ describe('TransactionAlerts', () => { ], componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -261,6 +284,7 @@ describe('TransactionAlerts', () => { submittedPendingTransactionsSelectorValue: [], componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -282,6 +306,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -301,6 +326,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -322,6 +348,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -345,6 +372,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -367,6 +395,7 @@ describe('TransactionAlerts', () => { submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }], componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -388,6 +417,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -407,6 +437,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -445,6 +476,7 @@ describe('TransactionAlerts', () => { const { getByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x0', }, @@ -461,6 +493,7 @@ describe('TransactionAlerts', () => { const { getByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x0', }, @@ -478,6 +511,7 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x5af3107a4000', }, @@ -492,6 +526,7 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x0', }, @@ -508,6 +543,7 @@ describe('TransactionAlerts', () => { const { getByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index ebd57c35a141..007aec372c62 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -70,7 +70,7 @@ export default class ConfirmApproveContent extends Component { fromAddressIsLedger: PropTypes.bool, chainId: PropTypes.string, tokenAddress: PropTypes.string, - rpcPrefs: PropTypes.object, + blockExplorerUrl: PropTypes.string, isContract: PropTypes.bool, hexTransactionTotal: PropTypes.string, hexMinimumTransactionFee: PropTypes.string, @@ -375,10 +375,10 @@ export default class ConfirmApproveContent extends Component { } getTitleTokenDescription() { - const { tokenId, tokenAddress, rpcPrefs, chainId, userAddress } = + const { tokenId, tokenAddress, blockExplorerUrl, chainId, userAddress } = this.props; const useBlockExplorer = - rpcPrefs?.blockExplorerUrl || + blockExplorerUrl || [...TEST_CHAINS, CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET].includes( chainId, ); @@ -393,7 +393,7 @@ export default class ConfirmApproveContent extends Component { null, userAddress, { - blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null, + blockExplorerUrl: blockExplorerUrl ?? null, }, ); const blockExplorerElement = ( @@ -529,7 +529,7 @@ export default class ConfirmApproveContent extends Component { fromAddressIsLedger, toAddress, chainId, - rpcPrefs, + blockExplorerUrl, assetStandard, tokenId, tokenAddress, @@ -612,7 +612,7 @@ export default class ConfirmApproveContent extends Component { tokenAddress={tokenAddress} toAddress={toAddress} chainId={chainId} - rpcPrefs={rpcPrefs} + blockExplorerUrl={blockExplorerUrl} tokenId={tokenId} assetName={assetName} assetStandard={assetStandard} diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve.js b/ui/pages/confirmations/confirm-approve/confirm-approve.js index 1b604ec43e6b..e167c15196ba 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve.js @@ -12,10 +12,7 @@ import { getTokenApprovedParam } from '../../../helpers/utils/token-util'; import { readAddressAsContract } from '../../../../shared/modules/contract-utils'; import { GasFeeContextProvider } from '../../../contexts/gasFee'; import { TransactionModalContextProvider } from '../../../contexts/transaction-modal'; -import { - getNativeCurrency, - isAddressLedger, -} from '../../../ducks/metamask/metamask'; +import { isAddressLedger } from '../../../ducks/metamask/metamask'; import ConfirmContractInteraction from '../confirm-contract-interaction'; import { getCurrentCurrency, @@ -23,10 +20,9 @@ import { getUseNonceField, getCustomNonceValue, getNextSuggestedNonce, - getCurrentChainId, - getRpcPrefsForCurrentProvider, checkNetworkAndAccountSupports1559, getUseCurrencyRateCheck, + selectNetworkConfigurationByChainId, } from '../../../selectors'; import { useApproveTransaction } from '../hooks/useApproveTransaction'; import { useSimulationFailureWarning } from '../hooks/useSimulationFailureWarning'; @@ -66,16 +62,23 @@ export default function ConfirmApprove({ isSetApproveForAll, }) { const dispatch = useDispatch(); - const { txParams: { data: transactionData, from } = {} } = transaction; + const { chainId, txParams: { data: transactionData, from } = {} } = + transaction; const currentCurrency = useSelector(getCurrentCurrency); - const nativeCurrency = useSelector(getNativeCurrency); + + const { nativeCurrency } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const subjectMetadata = useSelector(getSubjectMetadata); const useNonceField = useSelector(getUseNonceField); const nextNonce = useSelector(getNextSuggestedNonce); const customNonceValue = useSelector(getCustomNonceValue); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const { blockExplorerUrls } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const blockExplorerUrl = blockExplorerUrls?.[0]; const networkAndAccountSupports1559 = useSelector( checkNetworkAndAccountSupports1559, ); @@ -291,7 +294,7 @@ export default function ConfirmApprove({ txData={transaction} fromAddressIsLedger={fromAddressIsLedger} chainId={chainId} - rpcPrefs={rpcPrefs} + blockExplorerUrl={blockExplorerUrl} isContract={isContract} hasLayer1GasFee={layer1GasFee !== undefined} supportsEIP1559={supportsEIP1559} @@ -334,6 +337,7 @@ ConfirmApprove.propTypes = { userAddress: PropTypes.string, toAddress: PropTypes.string, transaction: PropTypes.shape({ + chainId: PropTypes.string, layer1GasFee: PropTypes.string, origin: PropTypes.string, txParams: PropTypes.shape({ diff --git a/ui/pages/confirmations/confirm-send-token/confirm-send-token.js b/ui/pages/confirmations/confirm-send-token/confirm-send-token.js index 3e24c72541d5..4e68783e9be7 100644 --- a/ui/pages/confirmations/confirm-send-token/confirm-send-token.js +++ b/ui/pages/confirmations/confirm-send-token/confirm-send-token.js @@ -8,11 +8,9 @@ import { editExistingTransaction } from '../../../ducks/send'; import { contractExchangeRateSelector, getCurrentCurrency, + selectConversionRateByChainId, + selectNetworkConfigurationByChainId, } from '../../../selectors'; -import { - getConversionRate, - getNativeCurrency, -} from '../../../ducks/metamask/metamask'; import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck'; import { showSendTokenPage } from '../../../store/actions'; import { @@ -49,8 +47,17 @@ export default function ConfirmSendToken({ history.push(SEND_ROUTE); }); }; - const conversionRate = useSelector(getConversionRate); - const nativeCurrency = useSelector(getNativeCurrency); + + const { chainId } = transaction; + + const conversionRate = useSelector((state) => + selectConversionRateByChainId(state, chainId), + ); + + const { nativeCurrency } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const currentCurrency = useSelector(getCurrentCurrency); const contractExchangeRate = useSelector(contractExchangeRateSelector); @@ -98,6 +105,7 @@ ConfirmSendToken.propTypes = { toAddress: PropTypes.string, tokenAddress: PropTypes.string, transaction: PropTypes.shape({ + chainId: PropTypes.string, origin: PropTypes.string, txParams: PropTypes.shape({ data: PropTypes.string, diff --git a/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js b/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js index 7b60bf9ab4fe..077d5c757dcb 100644 --- a/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js +++ b/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js @@ -15,16 +15,12 @@ import { import { PRIMARY } from '../../../helpers/constants/common'; import { contractExchangeRateSelector, - getCurrentChainId, getCurrentCurrency, - getRpcPrefsForCurrentProvider, getSelectedInternalAccount, + selectConversionRateByChainId, + selectNetworkConfigurationByChainId, + selectNftContractsByChainId, } from '../../../selectors'; -import { - getConversionRate, - getNativeCurrency, - getNftContracts, -} from '../../../ducks/metamask/metamask'; import { TokenStandard } from '../../../../shared/constants/transaction'; import { getWeiHexFromDecimalValue, @@ -47,16 +43,28 @@ export default function ConfirmTokenTransactionBase({ ethTransactionTotal, fiatTransactionTotal, hexMaximumTransactionFee, + transaction, }) { const t = useContext(I18nContext); const contractExchangeRate = useSelector(contractExchangeRateSelector); - const nativeCurrency = useSelector(getNativeCurrency); + const { chainId } = transaction; + + const { blockExplorerUrls, nativeCurrency } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + + const blockExplorerUrl = blockExplorerUrls?.[0]; const currentCurrency = useSelector(getCurrentCurrency); - const conversionRate = useSelector(getConversionRate); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const chainId = useSelector(getCurrentChainId); + + const conversionRate = useSelector((state) => + selectConversionRateByChainId(state, chainId), + ); + const { address: userAddress } = useSelector(getSelectedInternalAccount); - const nftCollections = useSelector(getNftContracts); + + const nftCollections = useSelector((state) => + selectNftContractsByChainId(state, chainId), + ); const ethTransactionTotalMaxAmount = Number( hexWEIToDecETH(hexMaximumTransactionFee), @@ -64,7 +72,7 @@ export default function ConfirmTokenTransactionBase({ const getTitleTokenDescription = (renderType) => { const useBlockExplorer = - rpcPrefs?.blockExplorerUrl || + blockExplorerUrl || [...TEST_CHAINS, CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET].includes( chainId, ); @@ -87,7 +95,7 @@ export default function ConfirmTokenTransactionBase({ null, userAddress, { - blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null, + blockExplorerUrl: blockExplorerUrl ?? null, }, ); const blockExplorerElement = ( @@ -219,4 +227,5 @@ ConfirmTokenTransactionBase.propTypes = { ethTransactionTotal: PropTypes.string, fiatTransactionTotal: PropTypes.string, hexMaximumTransactionFee: PropTypes.string, + transaction: PropTypes.string, }; diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js index 25d99e8a9f16..bcc3279752e2 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js @@ -1015,24 +1015,15 @@ export default class ConfirmTransactionBase extends Component { const { toAddress, fromAddress, - txData: { origin, chainId: txChainId } = {}, + txData: { origin } = {}, getNextNonce, tryReverseResolveAddress, smartTransactionsPreferenceEnabled, currentChainSupportsSmartTransactions, setSwapsFeatureFlags, fetchSmartTransactionsLiveness, - chainId, } = this.props; - // If the user somehow finds themselves seeing a confirmation - // on a network which is not presently selected, throw - if (txChainId === undefined || txChainId !== chainId) { - throw new Error( - `Currently selected chainId (${chainId}) does not match chainId (${txChainId}) on which the transaction was proposed.`, - ); - } - const { trackEvent } = this.context; trackEvent({ category: MetaMetricsEventCategory.Transactions, diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js index 795dfae0c63a..c34025e2f0c5 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js @@ -69,11 +69,8 @@ import { isAddressLedger, updateGasFees, getIsGasEstimatesLoading, - getNativeCurrency, getSendToAccounts, - getProviderConfig, findKeyringForAddress, - getConversionRate, } from '../../../ducks/metamask/metamask'; import { addHexPrefix, @@ -97,10 +94,19 @@ import { CUSTOM_GAS_ESTIMATE } from '../../../../shared/constants/gas'; // eslint-disable-next-line import/no-duplicates import { getIsUsingPaymaster } from '../../../selectors/account-abstraction'; +import { + selectConversionRateByChainId, + selectNetworkConfigurationByChainId, + // eslint-disable-next-line import/no-duplicates +} from '../../../selectors/selectors'; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) +import { + getAccountType, + selectDefaultRpcEndpointByChainId, + // eslint-disable-next-line import/no-duplicates +} from '../../../selectors/selectors'; // eslint-disable-next-line import/no-duplicates -import { getAccountType } from '../../../selectors/selectors'; - import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../shared/constants/app'; import { getIsNoteToTraderSupported, @@ -168,15 +174,16 @@ const mapStateToProps = (state, ownProps) => { const gasLoadingAnimationIsShowing = getGasLoadingAnimationIsShowing(state); const isBuyableChain = getIsNativeTokenBuyable(state); const { confirmTransaction, metamask } = state; - const conversionRate = getConversionRate(state); const { addressBook, nextNonce } = metamask; const unapprovedTxs = getUnapprovedTransactions(state); - const { chainId } = getProviderConfig(state); const { tokenData, txData, tokenProps, nonce } = confirmTransaction; const { txParams = {}, id: transactionId, type } = txData; const txId = transactionId || paramsTransactionId; const transaction = getUnapprovedTransaction(state, txId) ?? {}; + const { chainId } = transaction; + const conversionRate = selectConversionRateByChainId(state, chainId); + const { from: fromAddress, to: txParamsToAddress, @@ -184,6 +191,7 @@ const mapStateToProps = (state, ownProps) => { gas: gasLimit, data, } = (transaction && transaction.txParams) || txParams; + const accounts = getMetaMaskAccounts(state); const smartTransactionsPreferenceEnabled = getSmartTransactionsPreferenceEnabled(state); @@ -270,7 +278,10 @@ const mapStateToProps = (state, ownProps) => { fullTxData.userFeeLevel === CUSTOM_GAS_ESTIMATE || txParamsAreDappSuggested(fullTxData); const fromAddressIsLedger = isAddressLedger(state, fromAddress); - const nativeCurrency = getNativeCurrency(state); + + const { nativeCurrency } = + selectNetworkConfigurationByChainId(state, chainId) ?? {}; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) const accountType = getAccountType(state, fromAddress); const fromChecksumHexAddress = toChecksumHexAddress(fromAddress); @@ -281,7 +292,9 @@ const mapStateToProps = (state, ownProps) => { const custodianPublishesTransaction = getIsCustodianPublishesTransactionSupported(state, fromChecksumHexAddress); const builtinRpcUrl = CHAIN_ID_TO_RPC_URL_MAP[chainId]; - const { rpcUrl: customRpcUrl } = getProviderConfig(state); + + const { url: customRpcUrl } = + selectDefaultRpcEndpointByChainId(state, chainId) ?? {}; const rpcUrl = customRpcUrl || builtinRpcUrl; diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js index bea6aef1d84d..5a846874fed3 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js @@ -969,45 +969,4 @@ describe('Confirm Transaction Base', () => { expect(confirmButton).toBeDisabled(); }); }); - - describe('Preventing transaction submission', () => { - it('should throw error when on wrong chain', async () => { - const txParams = { - ...mockTxParams, - to: undefined, - data: '0xa22cb46500000000000000', - chainId: '0x5', - }; - const state = { - ...baseStore, - metamask: { - ...baseStore.metamask, - transactions: [ - { - id: baseStore.confirmTransaction.txData.id, - chainId: '0x5', - status: 'unapproved', - txParams, - }, - ], - ...mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), - }, - confirmTransaction: { - ...baseStore.confirmTransaction, - txData: { - ...baseStore.confirmTransaction.txData, - value: '0x0', - isUserOperation: true, - txParams, - chainId: '0x5', - }, - }, - }; - - // Error will be triggered by componentDidMount - await expect(render({ state })).rejects.toThrow( - 'Currently selected chainId (0xaa36a7) does not match chainId (0x5) on which the transaction was proposed.', - ); - }); - }); }); diff --git a/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js b/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js index 1bf5c8771b4f..72f570c5b98b 100644 --- a/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js +++ b/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js @@ -27,6 +27,7 @@ import { useAssetDetails } from '../hooks/useAssetDetails'; export default function ConfirmTokenTransactionSwitch({ transaction }) { const { + chainId, txParams: { data, to: tokenAddress, from: userAddress } = {}, layer1GasFee, } = transaction; @@ -44,7 +45,7 @@ export default function ConfirmTokenTransactionSwitch({ transaction }) { tokenAmount, tokenId, toAddress, - } = useAssetDetails(tokenAddress, userAddress, data); + } = useAssetDetails(tokenAddress, userAddress, data, chainId); const { ethTransactionTotal, @@ -221,6 +222,7 @@ export default function ConfirmTokenTransactionSwitch({ transaction }) { ConfirmTokenTransactionSwitch.propTypes = { transaction: PropTypes.shape({ + chainId: PropTypes.string, origin: PropTypes.string, txParams: PropTypes.shape({ data: PropTypes.string, diff --git a/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx b/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx index 33a8c2e67f0b..5a531a97787d 100644 --- a/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx +++ b/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx @@ -39,15 +39,16 @@ export const UserOperationStory = () => { }; const confirmState = getMockConfirmStateForTransaction(confirmation, { - metamask: {}, - preferences: { - ...mockState.metamask.preferences, - petnamesEnabled: true, - }, - userOperations: { - [confirmation.id]: { - userOperation: { - paymasterAndData: PAYMASTER_AND_DATA, + metamask: { + preferences: { + ...mockState.metamask.preferences, + petnamesEnabled: true, + }, + userOperations: { + [confirmation.id]: { + userOperation: { + paymasterAndData: PAYMASTER_AND_DATA, + }, }, }, }, diff --git a/ui/pages/confirmations/confirm/stories/utils.tsx b/ui/pages/confirmations/confirm/stories/utils.tsx index 9c68a392cbd7..18f9ae734e31 100644 --- a/ui/pages/confirmations/confirm/stories/utils.tsx +++ b/ui/pages/confirmations/confirm/stories/utils.tsx @@ -1,17 +1,11 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { MemoryRouter, Route } from 'react-router-dom'; import configureStore from '../../../../store/store'; -import { ConfirmContextProvider } from '../../context/confirm'; import ConfirmPage from '../confirm'; export const CONFIRM_PAGE_DECORATOR = [ (story: () => React.ReactFragment) => { - return ( - -
{story()}
-
- ); + return
{story()}
; }, ]; @@ -36,18 +30,7 @@ export function ConfirmStoryTemplate( return ( - {/* Adding the MemoryRouter and Route is a workaround to bypass a 404 error in storybook that - is caused when the 'ui/pages/confirmations/hooks/syncConfirmPath.ts' hook calls - history.replace. To avoid history.replace, we can provide a param id. */} - - } /> - + ); } diff --git a/ui/pages/confirmations/hooks/test-utils.js b/ui/pages/confirmations/hooks/test-utils.js index 908f600564f8..5e327b0467c5 100644 --- a/ui/pages/confirmations/hooks/test-utils.js +++ b/ui/pages/confirmations/hooks/test-utils.js @@ -3,10 +3,6 @@ import { useSelector } from 'react-redux'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; import { GasEstimateTypes } from '../../../../shared/constants/gas'; -import { - getConversionRate, - getNativeCurrency, -} from '../../../ducks/metamask/metamask'; import { getCurrentCurrency, getShouldShowFiat, @@ -14,6 +10,7 @@ import { getCurrentKeyring, getTokenExchangeRates, getPreferences, + selectConversionRateByChainId, } from '../../../selectors'; import { @@ -107,13 +104,10 @@ export const generateUseSelectorRouter = if (selector === getMultichainIsEvm) { return true; } - if (selector === getConversionRate) { + if (selector === selectConversionRateByChainId) { return MOCK_ETH_USD_CONVERSION_RATE; } - if ( - selector === getMultichainNativeCurrency || - selector === getNativeCurrency - ) { + if (selector === getMultichainNativeCurrency) { return EtherDenomination.ETH; } if (selector === getPreferences) { diff --git a/ui/pages/confirmations/hooks/useAssetDetails.js b/ui/pages/confirmations/hooks/useAssetDetails.js index 4a9afaf05468..bea2f6da89a8 100644 --- a/ui/pages/confirmations/hooks/useAssetDetails.js +++ b/ui/pages/confirmations/hooks/useAssetDetails.js @@ -1,7 +1,7 @@ import { isEqual } from 'lodash'; import { useState, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { getNfts, getTokens } from '../../../ducks/metamask/metamask'; +import { getTokens } from '../../../ducks/metamask/metamask'; import { getAssetDetails } from '../../../helpers/utils/token-util'; import { hideLoadingIndication, @@ -10,11 +10,18 @@ import { import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; import { usePrevious } from '../../../hooks/usePrevious'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; +import { selectNftsByChainId } from '../../../selectors'; -export function useAssetDetails(tokenAddress, userAddress, transactionData) { +export function useAssetDetails( + tokenAddress, + userAddress, + transactionData, + chainId, +) { const dispatch = useDispatch(); + // state selectors - const nfts = useSelector(getNfts); + const nfts = useSelector((state) => selectNftsByChainId(state, chainId)); const tokens = useSelector(getTokens, isEqual); const currentToken = tokens.find((token) => isEqualCaseInsensitive(token.address, tokenAddress), diff --git a/ui/pages/confirmations/hooks/useTransactionFunctionType.js b/ui/pages/confirmations/hooks/useTransactionFunctionType.js index 559367fff066..10255149eda7 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunctionType.js +++ b/ui/pages/confirmations/hooks/useTransactionFunctionType.js @@ -2,8 +2,10 @@ import { useSelector } from 'react-redux'; import { TransactionType } from '@metamask/transaction-controller'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; -import { getKnownMethodData } from '../../../selectors'; -import { getNativeCurrency } from '../../../ducks/metamask/metamask'; +import { + getKnownMethodData, + selectNetworkConfigurationByChainId, +} from '../../../selectors'; import { getTransactionTypeTitle } from '../../../helpers/utils/transactions.util'; import { getMethodName } from '../../../helpers/utils/metrics'; @@ -11,8 +13,12 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; export const useTransactionFunctionType = (txData = {}) => { const t = useI18nContext(); - const nativeCurrency = useSelector(getNativeCurrency); - const { txParams } = txData; + const { chainId, txParams } = txData; + + const networkConfiguration = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const methodData = useSelector( (state) => getKnownMethodData(state, txParams?.data) || {}, ); @@ -21,6 +27,8 @@ export const useTransactionFunctionType = (txData = {}) => { return {}; } + const { nativeCurrency } = networkConfiguration ?? {}; + const isTokenApproval = txData.type === TransactionType.tokenMethodSetApprovalForAll || txData.type === TransactionType.tokenMethodApprove || diff --git a/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js b/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js index 2bdc008e05dd..da625ad68267 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js +++ b/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js @@ -1,47 +1,64 @@ -import { TransactionType } from '@metamask/transaction-controller'; +import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller'; import { renderHookWithProvider } from '../../../../test/lib/render-helpers'; import mockState from '../../../../test/data/mock-state.json'; +import { mockNetworkState } from '../../../../test/stub/networks'; import { useTransactionFunctionType } from './useTransactionFunctionType'; +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; + +const STATE_MOCK = { + ...mockState, + metamask: { + ...mockState.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + }, +}; + describe('useTransactionFunctionType', () => { it('should return functionType depending on transaction data if present', () => { const { result } = renderHookWithProvider( () => useTransactionFunctionType({ + chainId: CHAIN_ID_MOCK, txParams: { data: '0x095ea7b30000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c9700000000000000000000000000000000000000000000000000000000000011170', }, type: TransactionType.tokenMethodApprove, }), - mockState, + STATE_MOCK, ); expect(result.current.functionType).toStrictEqual('Approve spend limit'); }); + it('should return functionType depending on transaction type if method not present in transaction data', () => { const { result } = renderHookWithProvider( () => useTransactionFunctionType({ + chainId: CHAIN_ID_MOCK, txParams: {}, type: TransactionType.tokenMethodTransfer, }), - mockState, + STATE_MOCK, ); expect(result.current.functionType).toStrictEqual('Transfer'); }); + it('should return functionType Contract interaction by default', () => { const { result } = renderHookWithProvider( () => useTransactionFunctionType({ + chainId: CHAIN_ID_MOCK, txParams: {}, }), - mockState, + STATE_MOCK, ); expect(result.current.functionType).toStrictEqual('Contract interaction'); }); + it('should return undefined is txData is not present', () => { const { result } = renderHookWithProvider( () => useTransactionFunctionType(), - mockState, + STATE_MOCK, ); expect(result.current.functionType).toBeUndefined(); }); diff --git a/ui/pages/confirmations/hooks/useTransactionInfo.js b/ui/pages/confirmations/hooks/useTransactionInfo.js index 452e44c83dfb..ef709de50486 100644 --- a/ui/pages/confirmations/hooks/useTransactionInfo.js +++ b/ui/pages/confirmations/hooks/useTransactionInfo.js @@ -1,5 +1,4 @@ import { useSelector } from 'react-redux'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; import { getSelectedInternalAccount } from '../../../selectors'; @@ -7,7 +6,7 @@ import { getSelectedInternalAccount } from '../../../selectors'; export const useTransactionInfo = (txData = {}) => { const { allNftContracts } = useSelector((state) => state.metamask); const selectedInternalAccount = useSelector(getSelectedInternalAccount); - const { chainId } = useSelector(getProviderConfig); + const { chainId } = txData; const isNftTransfer = Boolean( allNftContracts?.[selectedInternalAccount.address]?.[chainId]?.find( diff --git a/ui/pages/confirmations/hooks/useTransactionInfo.test.js b/ui/pages/confirmations/hooks/useTransactionInfo.test.js index 61ae036c4000..272c27abbfa4 100644 --- a/ui/pages/confirmations/hooks/useTransactionInfo.test.js +++ b/ui/pages/confirmations/hooks/useTransactionInfo.test.js @@ -1,7 +1,7 @@ +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { renderHookWithProvider } from '../../../../test/lib/render-helpers'; import mockState from '../../../../test/data/mock-state.json'; import { getSelectedInternalAccountFromMockState } from '../../../../test/jest/mocks'; -import { getCurrentChainId } from '../../../selectors'; import { useTransactionInfo } from './useTransactionInfo'; const mockSelectedInternalAccount = @@ -13,22 +13,25 @@ describe('useTransactionInfo', () => { const { result } = renderHookWithProvider( () => useTransactionInfo({ + chainId: CHAIN_IDS.GOERLI, txParams: {}, }), mockState, ); expect(result.current.isNftTransfer).toStrictEqual(false); }); + it('should return true if transaction is NFT transfer', () => { mockState.metamask.allNftContracts = { [mockSelectedInternalAccount.address]: { - [getCurrentChainId(mockState)]: [{ address: '0x9' }], + [CHAIN_IDS.GOERLI]: [{ address: '0x9' }], }, }; const { result } = renderHookWithProvider( () => useTransactionInfo({ + chainId: CHAIN_IDS.GOERLI, txParams: { to: '0x9', }, diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index 33a011c2966a..312854d168a7 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -27,14 +27,11 @@ import { getIsTestnet, getUseCurrencyRateCheck, getUnapprovedTransactions, + selectNetworkConfigurationByChainId, } from '../../../../selectors'; import { INSUFFICIENT_TOKENS_ERROR } from '../send.constants'; import { getCurrentDraftTransaction } from '../../../../ducks/send'; -import { - getNativeCurrency, - getProviderConfig, -} from '../../../../ducks/metamask/metamask'; import { showModal } from '../../../../store/actions'; import { addHexes, @@ -52,25 +49,26 @@ import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; export default function GasDisplay({ gasError }) { const t = useContext(I18nContext); const dispatch = useDispatch(); - const { estimateUsed } = useGasFeeContext(); + const { estimateUsed, transaction } = useGasFeeContext(); + const { chainId } = transaction; const trackEvent = useContext(MetaMetricsContext); - const { openBuyCryptoInPdapp } = useRamps(); - const providerConfig = useSelector(getProviderConfig); + const { name: networkNickname, nativeCurrency } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const isTestnet = useSelector(getIsTestnet); const isBuyableChain = useSelector(getIsNativeTokenBuyable); const draftTransaction = useSelector(getCurrentDraftTransaction); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const { showFiatInTestnets } = useSelector(getPreferences); const unapprovedTxs = useSelector(getUnapprovedTransactions); - const nativeCurrency = useSelector(getNativeCurrency); - const { chainId } = providerConfig; const networkName = NETWORK_TO_NAME_MAP[chainId]; const isInsufficientTokenError = draftTransaction?.amount?.error === INSUFFICIENT_TOKENS_ERROR; const editingTransaction = unapprovedTxs[draftTransaction.id]; - const currentNetworkName = networkName || providerConfig.nickname; + const currentNetworkName = networkName || networkNickname; const transactionData = { txParams: { diff --git a/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap b/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap index 91ac3c735f84..bc44e728df86 100644 --- a/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap +++ b/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap @@ -148,14 +148,14 @@ exports[`TokenAllowancePage when mounted should match snapshot 1`] = `
- mainnet + Ethereum Mainnet
getTargetAccountWithSendEtherInfo(state, userAddress), ); - const networkIdentifier = useSelector(getNetworkIdentifier); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + + const { chainId } = txData; + + const networkIdentifier = useSelector((state) => + selectNetworkIdentifierByChainId(state, chainId), + ); + + const { blockExplorerUrls } = + useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ) ?? {}; + + const blockExplorerUrl = blockExplorerUrls?.[0]; const unapprovedTxCount = useSelector(getUnapprovedTxCount); const unapprovedTxs = useSelector(getUnapprovedTransactions); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); @@ -387,7 +403,7 @@ export default function TokenAllowance({ tokenName={tokenSymbol} address={tokenAddress} chainId={fullTxData.chainId} - rpcPrefs={rpcPrefs} + blockExplorerUrl={blockExplorerUrl} /> ); @@ -710,7 +726,7 @@ export default function TokenAllowance({ tokenAddress={tokenAddress} toAddress={toAddress} chainId={fullTxData.chainId} - rpcPrefs={rpcPrefs} + blockExplorerUrl={blockExplorerUrl} /> )} diff --git a/ui/pages/confirmations/token-allowance/token-allowance.test.js b/ui/pages/confirmations/token-allowance/token-allowance.test.js index ddfc48a2cfaa..68df89777c7e 100644 --- a/ui/pages/confirmations/token-allowance/token-allowance.test.js +++ b/ui/pages/confirmations/token-allowance/token-allowance.test.js @@ -203,7 +203,7 @@ describe('TokenAllowancePage', () => { status: 'unapproved', originalGasEstimate: '0xea60', userEditedGasLimit: false, - chainId: '0x3', + chainId: CHAIN_IDS.MAINNET, loadingDefaults: false, dappSuggestedGasFees: { gasPrice: '0x4a817c800', diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 94d8f39252d8..c4f2928d8ef2 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -734,6 +734,51 @@ export const selectDefaultRpcEndpointByChainId = createSelector( }, ); +/** + * @type (state: any, chainId: string) => number | undefined + */ +export const selectConversionRateByChainId = createSelector( + selectNetworkConfigurationByChainId, + (state) => state, + (networkConfiguration, state) => { + if (!networkConfiguration) { + return undefined; + } + + const { nativeCurrency } = networkConfiguration; + return state.metamask.currencyRates[nativeCurrency]?.conversionRate; + }, +); + +export const selectNftsByChainId = createSelector( + getSelectedInternalAccount, + (state) => state.metamask.allNfts, + (_state, chainId) => chainId, + (selectedAccount, nfts, chainId) => { + return nfts?.[selectedAccount.address]?.[chainId] ?? []; + }, +); + +export const selectNftContractsByChainId = createSelector( + getSelectedInternalAccount, + (state) => state.metamask.allNftContracts, + (_state, chainId) => chainId, + (selectedAccount, nftContracts, chainId) => { + return nftContracts?.[selectedAccount.address]?.[chainId] ?? []; + }, +); + +export const selectNetworkIdentifierByChainId = createSelector( + selectNetworkConfigurationByChainId, + selectDefaultRpcEndpointByChainId, + (networkConfiguration, defaultRpcEndpoint) => { + const { name: nickname } = networkConfiguration ?? {}; + const { url: rpcUrl, networkClientId } = defaultRpcEndpoint ?? {}; + + return nickname || rpcUrl || networkClientId; + }, +); + export function getRequestingNetworkInfo(state, chainIds) { // If chainIds is undefined, set it to an empty array let processedChainIds = chainIds === undefined ? [] : chainIds; From e882da087d6028a5cd884306a2d4b6c8a59db366 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Fri, 8 Nov 2024 11:19:07 +0000 Subject: [PATCH 14/18] fix: Address design review for ERC20 token send (#28212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR implements usage of ENS resolved names existing in state for the PetNames component. It also tweaks various UI elements as per design review. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28212?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3585 ## **Manual testing steps** 1. Initiate an erc20 token send on the wallet UI 2. Check the confirmation screen ## **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/develop/.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/develop/.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. --- .../info/row/__snapshots__/row.test.tsx.snap | 2 +- ui/components/app/confirm/info/row/row.tsx | 2 +- .../__snapshots__/name-details.test.tsx.snap | 50 +++- .../name/name-details/name-details.test.tsx | 283 +++++++++++++++++- ui/hooks/useDisplayName.test.ts | 70 ++++- ui/hooks/useDisplayName.ts | 39 ++- .../__snapshots__/approve.test.tsx.snap | 2 +- .../native-transfer.test.tsx.snap | 193 ++++++------ .../native-transfer/native-transfer.test.tsx | 8 +- .../nft-token-transfer.test.tsx.snap | 197 ++++++------ .../nft-token-transfer.test.tsx | 8 +- .../transaction-data.test.tsx.snap | 8 +- .../token-details-section.test.tsx.snap | 17 +- .../token-transfer.test.tsx.snap | 242 ++++++++------- .../token-transfer/token-details-section.tsx | 7 +- .../token-transfer/token-transfer.test.tsx | 8 +- 16 files changed, 770 insertions(+), 366 deletions(-) diff --git a/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap index 545d548fa3b7..7dad4ee50357 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap @@ -37,7 +37,7 @@ exports[`ConfirmInfoRow should match snapshot when copy is enabled 1`] = `
-
-
-
- -

- 0x2e0D7...5d09B -

-
-
- -
+ + + + + + +
- -
-
-
+ -

- You send -

-
-
-
-
-
-

- - 4 -

-
-
-
-
-
-
- E -
-

- ETH -

-
-
-
-
-
-
+ + + + + +
@@ -195,7 +173,7 @@ exports[`NativeTransferInfo renders correctly 1`] = ` class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" >
G
@@ -221,6 +199,21 @@ exports[`NativeTransferInfo renders correctly 1`] = ` > Interacting with

+
+
+ +
+
({ })); describe('NativeTransferInfo', () => { - it('renders correctly', async () => { + it('renders correctly', () => { const state = getMockTokenTransferConfirmState({}); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider( @@ -32,10 +30,6 @@ describe('NativeTransferInfo', () => { mockStore, ); - await waitFor(() => { - expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument(); - }); - expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap index 8bd4a73fe440..77794c9e70a4 100644 --- a/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap @@ -15,39 +15,50 @@ exports[`NFTTokenTransferInfo renders correctly 1`] = ` />

- 1 -

+ />
-
-
-
- -

- 0x2e0D7...5d09B -

-
-
- -
+ + + + + + +
-
-
-
-
+ -

- You send -

-
-
-
-
-
-

- - 4 -

-
-
-
-
-
-
- E -
-

- ETH -

-
-
-
-
-
-
+ + + + + +
@@ -198,7 +174,7 @@ exports[`NFTTokenTransferInfo renders correctly 1`] = ` class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" >
G
@@ -224,6 +200,21 @@ exports[`NFTTokenTransferInfo renders correctly 1`] = ` > Interacting with

+
+
+ +
+
({ })); describe('NFTTokenTransferInfo', () => { - it('renders correctly', async () => { + it('renders correctly', () => { const state = getMockTokenTransferConfirmState({}); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider( @@ -32,10 +30,6 @@ describe('NFTTokenTransferInfo', () => { mockStore, ); - await waitFor(() => { - expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument(); - }); - expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap index d03e04d2c673..1b7fd2aeb460 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap @@ -13,7 +13,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
-
- ? -
-

- 0 Unknown -

+ + + + + + + +
-
-
-
- -

- 0x2e0D7...5d09B -

-
-
- -
+ + + + + + +
-
-
-
-
+ -

- You send -

-
-
-
-
-
-

- - 4 -

-
-
-
-
-
-
- E -
-

- ETH -

-
-
-
-
-
-
+ + + + + +
@@ -195,7 +202,7 @@ exports[`TokenTransferInfo renders correctly 1`] = ` class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" >
G
@@ -221,6 +228,21 @@ exports[`TokenTransferInfo renders correctly 1`] = ` > Interacting with

+
+
+ +
+
{ > { ); const tokenRow = transactionMeta.type !== TransactionType.simpleSend && ( - + ({ })); describe('TokenTransferInfo', () => { - it('renders correctly', async () => { + it('renders correctly', () => { const state = getMockTokenTransferConfirmState({}); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider( @@ -32,10 +30,6 @@ describe('TokenTransferInfo', () => { mockStore, ); - await waitFor(() => { - expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument(); - }); - expect(container).toMatchSnapshot(); }); }); From 1d4fcc0c31b497b9c4d3a867d31b37c6dd38e32d Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 8 Nov 2024 19:22:56 +0800 Subject: [PATCH 15/18] fix: disable buy for btc testnet accounts (#28341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR disables the Buy / sell button for btc testnet accounts ## **Related issues** Fixes: https://github.com/MetaMask/accounts-planning/issues/655 ## **Manual testing steps** 1. Create a testnet btc account 2. Go to the overview page 3. See that the buy/sell button is disabled. ## **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** - [ ] 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/wallet-overview/btc-overview.test.tsx | 30 +++++++++++++++++++ .../app/wallet-overview/btc-overview.tsx | 12 +++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx index abff2cb2b239..ffaed244e958 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -233,4 +233,34 @@ describe('BtcOverview', () => { const receiveButton = queryByTestId(BTC_OVERVIEW_RECEIVE); expect(receiveButton).toBeInTheDocument(); }); + + it('"Buy & Sell" button is disabled for testnet accounts', () => { + const storeWithBtcBuyable = getStore({ + metamask: { + ...mockMetamaskStore, + internalAccounts: { + ...mockMetamaskStore.internalAccounts, + accounts: { + [mockNonEvmAccount.id]: { + ...mockNonEvmAccount, + address: 'tb1q9lakrt5sw0w0twnc6ww4vxs7hm0q23e03286k8', + }, + }, + }, + }, + ramps: { + buyableChains: mockBuyableChainsWithBtc, + }, + }); + + const { queryByTestId } = renderWithProvider( + , + storeWithBtcBuyable, + ); + + const buyButton = queryByTestId(BTC_OVERVIEW_BUY); + + expect(buyButton).toBeInTheDocument(); + expect(buyButton).toBeDisabled(); + }); }); diff --git a/ui/components/app/wallet-overview/btc-overview.tsx b/ui/components/app/wallet-overview/btc-overview.tsx index dc47df7567b5..2ddaefd92f58 100644 --- a/ui/components/app/wallet-overview/btc-overview.tsx +++ b/ui/components/app/wallet-overview/btc-overview.tsx @@ -1,11 +1,16 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + getMultichainIsMainnet, + ///: END:ONLY_INCLUDE_IF getMultichainProviderConfig, getMultichainSelectedAccountCachedBalance, } from '../../../selectors/multichain'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsBitcoinBuyable } from '../../../ducks/ramps'; +import { getSelectedInternalAccount } from '../../../selectors'; +import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; ///: END:ONLY_INCLUDE_IF import { CoinOverview } from './coin-overview'; @@ -17,6 +22,11 @@ const BtcOverview = ({ className }: BtcOverviewProps) => { const { chainId } = useSelector(getMultichainProviderConfig); const balance = useSelector(getMultichainSelectedAccountCachedBalance); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + const selectedAccount = useSelector(getSelectedInternalAccount); + const isBtcMainnetAccount = useMultichainSelector( + getMultichainIsMainnet, + selectedAccount, + ); const isBtcBuyable = useSelector(getIsBitcoinBuyable); ///: END:ONLY_INCLUDE_IF @@ -31,7 +41,7 @@ const BtcOverview = ({ className }: BtcOverviewProps) => { isSwapsChain={false} ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain={false} - isBuyableChain={isBtcBuyable} + isBuyableChain={isBtcBuyable && isBtcMainnetAccount} ///: END:ONLY_INCLUDE_IF /> ); From 152a50ef8fec9d8a038313c50df107f97ec80c20 Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Fri, 8 Nov 2024 14:13:07 +0000 Subject: [PATCH 16/18] feat(1852): Implement sentry user report on error screen (#27857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Sentry provides a feature to collect user feedback to users when an issue occurs. This could be a great help to fix Sentry issues which sometimes come without a lot of context and are difficult to reproduce. Hence we use out-of-box solution from sentry to implement [User Feedback Widget](https://docs.sentry.io/platforms/javascript/user-feedback/configuration/#user-feedback-widget) via `Sentry.feedbackIntegration`. You can find more technical details in [this comment](https://github.com/MetaMask/MetaMask-planning/issues/1852#issuecomment-2392480544). Design Figma link [here](https://www.figma.com/design/DM5G1pyp74sMJwyKbw1KiR/Error-message-and-bug-report?node-id=1-4243&node-type=frame&t=iZI13qsxataukM4a-0). ### What's expected in this PR: - Refactor original `ui/pages/error/error.component.js` to typescript, clean up / update language content, and improve the layout based on new design (see above Figma link) that would be consistent as mobile implementation - Add a new option in `develop options` to cause a page crash by remove one language file (for me was easiest way to trigger), which will bring us to error page - In new error page, we have 3 options: 1. Describe what happened - open a form to sent a message to sentry 2. Contact support - existing link to redirect to `process.env.SUPPORT_REQUEST_LINK` 3. Try again - close the extension and allow user to open again - Convert `ui/ducks/locale/locale.js` to typescript and add related tests - Add e2e tests with POM pattern **This is the scenario for extension:** - GIVEN a user has MM installed - AND Sentry is enabled (user enabled MetaMetrics) - WHEN an unhandled issue occurs in MM - THEN MM crashes - AND an event is sent to Sentry - AND user is given the possibility to describe what happened to him by filling a form - AND his feedback gets paired to the Sentry event once user presses the "submit" button at the bottom of the form - AND user is given more comprehensive error screen when it crashes [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27857?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/1852 ## **Manual testing steps** 1. Set up sentry (https://github.com/MetaMask/metamask-extension/blob/develop/development/README.md) 2. Add DSN to `SENTRY_DSN_DEV` in local env set up, and mark `ENABLE_SETTINGS_PAGE_DEV_OPTIONS`=true 3. Run `yarn webpack --watch --sentry` 4. Ensure `Participate in MetaMetrics` is opt in 5. Click "develop options" in settings 6. Click "Generate A Page Crash" button 7. User is redirected to new error page 8. Click `Describe what happened` can open a sentry feedback form, then in your sentry project you can find the submitted form within `User Feedback` section 9. Click `Contact support` will redirect user to metamask support page 10. Click `Try again` will close the extension and ready for reload ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/56145a44-82d3-4d07-be03-87d81ee6a9d7 ## **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/develop/.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/develop/.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/de/messages.json | 12 - app/_locales/el/messages.json | 12 - app/_locales/en/messages.json | 40 ++- app/_locales/en_GB/messages.json | 12 - app/_locales/es/messages.json | 12 - app/_locales/es_419/messages.json | 12 - app/_locales/fr/messages.json | 12 - app/_locales/hi/messages.json | 12 - app/_locales/id/messages.json | 12 - app/_locales/it/messages.json | 4 - app/_locales/ja/messages.json | 12 - app/_locales/ko/messages.json | 12 - app/_locales/ph/messages.json | 12 - app/_locales/pt/messages.json | 12 - app/_locales/pt_BR/messages.json | 12 - app/_locales/ru/messages.json | 12 - app/_locales/tl/messages.json | 12 - app/_locales/tr/messages.json | 12 - app/_locales/vi/messages.json | 12 - app/_locales/zh_CN/messages.json | 12 - app/_locales/zh_TW/messages.json | 12 - privacy-snapshot.json | 2 + shared/constants/metametrics.ts | 4 + shared/modules/i18n.test.ts | 16 +- shared/modules/i18n.ts | 3 +- test/data/mock-state.json | 8 +- test/e2e/helpers.js | 5 +- .../pages/developer-options-page.ts | 38 +++ test/e2e/page-objects/pages/error-page.ts | 95 ++++++ test/e2e/page-objects/pages/settings-page.ts | 10 + .../metrics/developer-options-sentry.spec.ts | 88 +++++ test/e2e/tests/metrics/errors.spec.js | 3 +- ui/ducks/locale/locale.js | 42 --- ui/ducks/locale/locale.test.ts | 80 ++++- ui/ducks/locale/locale.ts | 108 ++++++ ui/pages/error-page/error-component.test.tsx | 192 +++++++++++ ui/pages/error-page/error-page.component.tsx | 312 ++++++++++++++++++ ui/pages/error-page/index.scss | 41 +++ ui/pages/error-page/index.ts | 1 + ui/pages/error/error.component.js | 106 ------ ui/pages/error/index.js | 1 - ui/pages/error/index.scss | 47 --- ui/pages/index.js | 7 +- ui/pages/pages.scss | 2 +- .../developer-options-tab.test.tsx.snap | 46 ++- .../developer-options-tab.tsx | 2 +- .../developer-options-tab/sentry-test.tsx | 66 +++- ui/pages/settings/settings.component.js | 5 +- 48 files changed, 1124 insertions(+), 478 deletions(-) create mode 100644 test/e2e/page-objects/pages/developer-options-page.ts create mode 100644 test/e2e/page-objects/pages/error-page.ts create mode 100644 test/e2e/tests/metrics/developer-options-sentry.spec.ts delete mode 100644 ui/ducks/locale/locale.js create mode 100644 ui/ducks/locale/locale.ts create mode 100644 ui/pages/error-page/error-component.test.tsx create mode 100644 ui/pages/error-page/error-page.component.tsx create mode 100644 ui/pages/error-page/index.scss create mode 100644 ui/pages/error-page/index.ts delete mode 100644 ui/pages/error/error.component.js delete mode 100644 ui/pages/error/index.js delete mode 100644 ui/pages/error/index.scss diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 19c7731c778f..f62b15c08c93 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1696,10 +1696,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Fehlerdetails", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Fehler beim Abrufen der Liste sicherer Ketten, bitte mit Vorsicht fortfahren." }, @@ -1711,14 +1707,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Versuchen Sie es erneut, indem Sie die Seite neu laden oder kontaktieren Sie den Support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Versuchen Sie es erneut, indem das Popup schließen und neu laden oder kontaktieren Sie den Support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask hat einen Fehler festgestellt.", "description": "Title of generic error page" diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index ac4542893959..3bede7c85edb 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1696,10 +1696,6 @@ "message": "Κωδικός: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Λεπτομέρειες σφάλματος", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Σφάλμα κατά τη λήψη της λίστας ασφαλών αλυσίδων, συνεχίστε με προσοχή." }, @@ -1711,14 +1707,6 @@ "message": "Κωδικός: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Προσπαθήστε ξανά με επαναφόρτωση της σελίδας ή επικοινωνήστε με την υποστήριξη $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Προσπαθήστε ξανά κλείνοντας και ανοίγοντας ξανά το αναδυόμενο παράθυρο ή επικοινωνήστε με την υποστήριξη $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Το MetaMask αντιμετώπισε ένα σφάλμα", "description": "Title of generic error page" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 202dc04c2fa8..56e3614e3f39 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1940,10 +1940,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Error details", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error while getting safe chain list, please continue with caution." }, @@ -1955,18 +1951,42 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Try again by reloading the page, or contact support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + "errorPageContactSupport": { + "message": "Contact support", + "description": "Button for contact MM support" + }, + "errorPageDescribeUsWhatHappened": { + "message": "Describe what happened", + "description": "Button for submitting report to sentry" + }, + "errorPageInfo": { + "message": "Your information can’t be shown. Don’t worry, your wallet and funds are safe.", + "description": "Information banner shown in the error page" + }, + "errorPageMessageTitle": { + "message": "Error message", + "description": "Title for description, which is displayed for debugging purposes" }, - "errorPagePopupMessage": { - "message": "Try again by closing and reopening the popup, or contact support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + "errorPageSentryFormTitle": { + "message": "Describe what happened", + "description": "In sentry feedback form, The title at the top of the feedback form." + }, + "errorPageSentryMessagePlaceholder": { + "message": "Sharing details like how we can reproduce the bug will help us fix the problem.", + "description": "In sentry feedback form, The placeholder for the feedback description input field." + }, + "errorPageSentrySuccessMessageText": { + "message": "Thanks! We will take a look soon.", + "description": "In sentry feedback form, The message displayed after a successful feedback submission." }, "errorPageTitle": { "message": "MetaMask encountered an error", "description": "Title of generic error page" }, + "errorPageTryAgain": { + "message": "Try again", + "description": "Button for try again" + }, "errorStack": { "message": "Stack:", "description": "Title for error stack, which is displayed for debugging purposes" diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 8dd2e32dac39..9ee7771947ba 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1792,10 +1792,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Error details", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error while getting safe chain list, please continue with caution." }, @@ -1807,14 +1803,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Try again by reloading the page, or contact support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Try again by closing and reopening the popup, or contact support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encountered an error", "description": "Title of generic error page" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 1d23b736fbfd..f13313976e74 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1693,10 +1693,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalles del error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error al obtener la lista de cadenas seguras, por favor continúe con precaución." }, @@ -1708,14 +1704,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Vuelva a cargar la página para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Cierre la ventana emergente y vuelva a abrirla para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encontró un error", "description": "Title of generic error page" diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index cebfc3cef106..511ee6cbef71 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -686,10 +686,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalles del error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensaje: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -698,14 +694,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Vuelva a cargar la página para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Cierre la ventana emergente y vuelva a abrirla para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encontró un error", "description": "Title of generic error page" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index e9b8fd588b89..c785d2e0a00a 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1696,10 +1696,6 @@ "message": "Code : $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Détails de l’erreur", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Erreur lors de l’obtention de la liste des chaînes sécurisées, veuillez continuer avec précaution." }, @@ -1711,14 +1707,6 @@ "message": "Code : $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Essayez à nouveau en rechargeant la page, ou contactez le service d’assistance $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Réessayez en fermant puis en rouvrant le pop-up, ou contactez le service d’assistance $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask a rencontré une erreur", "description": "Title of generic error page" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 9b2c06f96c11..554bb2e91b84 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1696,10 +1696,6 @@ "message": "कोड: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "गड़बड़ी की जानकारी", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "सेफ चेन लिस्ट पाते समय गड़बड़ी हुई, कृपया सावधानी के साथ जारी रखें।" }, @@ -1711,14 +1707,6 @@ "message": "कोड: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "पेज को दोबारा लोड करके फिर से कोशिश करें या सपोर्ट $1 से कॉन्टेक्ट करें।", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "पॉपअप को बंद करके और फिर से खोलने की कोशिश करें या $1 पर सपोर्ट से कॉन्टेक्ट करें।", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask में कोई गड़बड़ी हुई", "description": "Title of generic error page" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 1331d7b9c482..8e60e6fe5a50 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1696,10 +1696,6 @@ "message": "Kode: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detail kesalahan", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Terjadi kesalahan saat mendapatkan daftar rantai aman, lanjutkan dengan hati-hati." }, @@ -1711,14 +1707,6 @@ "message": "Kode: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Coba lagi dengan memuat kembali halaman, atau hubungi dukungan $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Coba lagi dengan menutup dan membuka kembali sembulan, atau hubungi dukungan $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask mengalami kesalahan", "description": "Title of generic error page" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 0c79dd8fc6fc..05cccdac0359 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -736,10 +736,6 @@ "message": "Codice: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Dettagli Errore", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Messaggio: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 3107eb3cb152..69824ae33b52 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1696,10 +1696,6 @@ "message": "コード: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "エラーの詳細", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "安全なチェーンリストの取得中にエラーが発生しました。慎重に続けてください。" }, @@ -1711,14 +1707,6 @@ "message": "コード: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "ページを再ロードしてもう一度実行するか、$1からサポートまでお問い合わせください。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "ポップアップを閉じてから再び開いてもう一度実行するか、$1からサポートまでお問い合わせください。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMaskにエラーが発生しました", "description": "Title of generic error page" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 6dc468cdd5ef..f160b3dccbc2 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1696,10 +1696,6 @@ "message": "코드: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "오류 세부 정보", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "안전 체인 목록을 가져오는 동안 오류가 발생했습니다. 주의하여 계속 진행하세요." }, @@ -1711,14 +1707,6 @@ "message": "코드: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "페이지를 새로고침하여 다시 시도하거나 $1로 지원을 요청하여 도움을 받으세요.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "팝업을 닫은 후 다시 열어 다시 시도하거나 $1에서 지원을 요청하여 도움을 받으세요.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 오류 발생", "description": "Title of generic error page" diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index e6b4bc3e7811..3c08d76cb186 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -423,10 +423,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Mga Detalye ng Error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensahe: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -435,14 +431,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Subukan ulit sa pamamagitan ng pag-reload ng page, o makipag-ugnayan sa suporta sa $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Subukan ulit sa pamamagitan ng pagsara at pagbukas ulit ng popup, o makipag-ugnayan sa suporta sa $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Nagkaroon ng error sa MetaMask", "description": "Title of generic error page" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 03f41c1c62b8..589a58e94907 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1696,10 +1696,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalhes do erro", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Erro ao obter uma lista segura da cadeia. Por favor, prossiga com cautela." }, @@ -1711,14 +1707,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Recarregue a página para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Feche e reabra o pop-up para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "A MetaMask encontrou um erro", "description": "Title of generic error page" diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 58d7f6ea8718..ebf2af88c186 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -686,10 +686,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalhes do erro", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensagem: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -698,14 +694,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Recarregue a página para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Feche e reabra o pop-up para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "A MetaMask encontrou um erro", "description": "Title of generic error page" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index e56afc418785..9462d9c6eb4a 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1696,10 +1696,6 @@ "message": "Код: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Сведения об ошибке", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Ошибка при получении списка безопасных блокчейнов. Продолжайте с осторожностью." }, @@ -1711,14 +1707,6 @@ "message": "Код: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Повторите попытку, перезагрузив страницу, или обратитесь в поддержку $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Повторите попытку, закрыв и вновь открыв всплывающее окно, или обратитесь в поддержку $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask обнаружил ошибку", "description": "Title of generic error page" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index af56d958c313..41612a6ce177 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1696,10 +1696,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Mga Detalye ng Error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "May error habang kinukuha ang ligtas na chain list, mangyaring magpatuloy nang may pag-iingat." }, @@ -1711,14 +1707,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Subukang muling i-reload ang page, o kontakin ang support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Subukan muli sa pamamagitan ng pagsasara o muling pagbubukas ng pop-up, kontakin ang support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Nagkaroon ng error sa MetaMask", "description": "Title of generic error page" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 6732513395d8..f11cc0d17523 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1696,10 +1696,6 @@ "message": "Kod: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Hata ayrıntıları", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Güvenli zincir listesi alınırken hata oluştu, lütfen dikkatli bir şekilde devam edin." }, @@ -1711,14 +1707,6 @@ "message": "Kod: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Sayfayı yeniden yükleyerek tekrar deneyin veya $1 destek bölümümüze ulaşın.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Açılır pencereyi kapatarak ve yeniden açarak tekrar deneyin $1 destek bölümümüze ulaşın.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask bir hata ile karşılaştı", "description": "Title of generic error page" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 9f6ff5119366..dd518df1bf22 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1696,10 +1696,6 @@ "message": "Mã: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Chi tiết về lỗi", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Lỗi khi lấy danh sách chuỗi an toàn, vui lòng tiếp tục một cách thận trọng." }, @@ -1711,14 +1707,6 @@ "message": "Mã: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Hãy thử lại bằng cách tải lại trang hoặc liên hệ với bộ phận hỗ trợ $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Hãy thử lại bằng cách đóng và mở lại cửa sổ bật lên hoặc liên hệ với bộ phận hỗ trợ $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask đã gặp lỗi", "description": "Title of generic error page" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index eef758dacdad..818f1ffdb82b 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1696,10 +1696,6 @@ "message": "代码:$1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "错误详情", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "获取安全链列表时出错,请谨慎继续。" }, @@ -1711,14 +1707,6 @@ "message": "代码:$1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "通过重新加载页面再试一次,或联系支持团队 $1。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "通过关闭并重新打开弹出窗口再试一次,或联系支持团队 $1。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 遇到了一个错误", "description": "Title of generic error page" diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 7a7fdb68cf1f..29953f67c941 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -425,10 +425,6 @@ "message": "代碼:$1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "錯誤詳細資訊", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "訊息:$1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -437,14 +433,6 @@ "message": "代碼:$1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "重新整理頁面然後再試一次,或從$1聯絡我們尋求支援。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "重新開啟彈跳視窗然後再試一次,或從$1聯絡我們尋求支援。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 遭遇錯誤", "description": "Title of generic error page" diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 6e041ea3d71b..817d1e102bff 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -56,6 +56,8 @@ "sourcify.dev", "start.metamask.io", "static.cx.metamask.io", + "support.metamask.io", + "support.metamask-institutional.io", "swap.api.cx.metamask.io", "test.metamask-phishing.io", "token.api.cx.metamask.io", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 4096d04b11d6..103a24c2463d 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -158,6 +158,10 @@ export type MetaMetricsEventOptions = { * as not conforming to our schema. */ matomoEvent?: boolean; + /** + * Values that can used in the "properties" tracking object as keys, + */ + contextPropsIntoEventProperties?: string | string[]; }; export type MetaMetricsEventFragment = { diff --git a/shared/modules/i18n.test.ts b/shared/modules/i18n.test.ts index 8452ef48238c..7a2f49bd9f68 100644 --- a/shared/modules/i18n.test.ts +++ b/shared/modules/i18n.test.ts @@ -109,7 +109,21 @@ describe('I18N Module', () => { ); }); - it('throws if test env set', () => { + it('throws if IN_TEST is set true', () => { + expect(() => + getMessage( + FALLBACK_LOCALE, + {} as unknown as I18NMessageDict, + keyMock, + ), + ).toThrow( + `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`, + ); + }); + + it('throws if ENABLE_SETTINGS_PAGE_DEV_OPTIONS is set true', () => { + process.env.IN_TEST = String(false); + process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS = String(true); expect(() => getMessage( FALLBACK_LOCALE, diff --git a/shared/modules/i18n.ts b/shared/modules/i18n.ts index d19cfa4b3b11..b5c22c869b54 100644 --- a/shared/modules/i18n.ts +++ b/shared/modules/i18n.ts @@ -177,7 +177,7 @@ function missingKeyError( onError?.(error); log.error(error); - if (process.env.IN_TEST) { + if (process.env.IN_TEST || process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS) { throw error; } } @@ -188,7 +188,6 @@ function missingKeyError( warned[localeCode] = warned[localeCode] ?? {}; warned[localeCode][key] = true; - log.warn( `Translator - Unable to find value of key "${key}" for locale "${localeCode}"`, ); diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 184787b07836..80e5499447d7 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -46,7 +46,13 @@ "mostRecentOverviewPage": "/mostRecentOverviewPage" }, "localeMessages": { - "currentLocale": "en" + "currentLocale": "en", + "current": { + "user": "user" + }, + "en": { + "user": "user" + } }, "metamask": { "use4ByteResolution": true, diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index c3705c1ebf6c..1c3f53b30224 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -55,7 +55,7 @@ const convertETHToHexGwei = (eth) => convertToHexValue(eth * 10 ** 18); /** * * @param {object} options - * @param {(fixtures: Fixtures) => Promise} testSuite + * @param {({driver: Driver, mockedEndpoint: MockedEndpoint}: TestSuiteArguments) => Promise} testSuite */ async function withFixtures(options, testSuite) { const { @@ -1308,6 +1308,8 @@ async function openMenuSafe(driver) { } } +const sentryRegEx = /^https:\/\/sentry\.io\/api\/\d+\/envelope/gu; + module.exports = { DAPP_HOST_ADDRESS, DAPP_URL, @@ -1379,4 +1381,5 @@ module.exports = { getSelectedAccountAddress, tempToggleSettingRedesignedConfirmations, openMenuSafe, + sentryRegEx, }; diff --git a/test/e2e/page-objects/pages/developer-options-page.ts b/test/e2e/page-objects/pages/developer-options-page.ts new file mode 100644 index 000000000000..c15f6c767a82 --- /dev/null +++ b/test/e2e/page-objects/pages/developer-options-page.ts @@ -0,0 +1,38 @@ +import { Driver } from '../../webdriver/driver'; + +class DevelopOptions { + private readonly driver: Driver; + + // Locators + private readonly generatePageCrashButton: string = + '[data-testid="developer-options-generate-page-crash-button"]'; + + private readonly developOptionsPageTitle: object = { + text: 'Developer Options', + css: 'h4', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.developOptionsPageTitle); + } catch (e) { + console.log( + 'Timeout while waiting for Developer options page to be loaded', + e, + ); + throw e; + } + console.log('Developer option page is loaded'); + } + + async clickGenerateCrashButton(): Promise { + console.log('Generate a page crash in Developer option page'); + await this.driver.clickElement(this.generatePageCrashButton); + } +} + +export default DevelopOptions; diff --git a/test/e2e/page-objects/pages/error-page.ts b/test/e2e/page-objects/pages/error-page.ts new file mode 100644 index 000000000000..76acacdcf9dd --- /dev/null +++ b/test/e2e/page-objects/pages/error-page.ts @@ -0,0 +1,95 @@ +import { Driver } from '../../webdriver/driver'; +import HeaderNavbar from './header-navbar'; +import SettingsPage from './settings-page'; +import DevelopOptionsPage from './developer-options-page'; + +const FEEDBACK_MESSAGE = + 'Message: Unable to find value of key "developerOptions" for locale "en"'; + +class ErrorPage { + private readonly driver: Driver; + + // Locators + private readonly errorPageTitle: object = { + text: 'MetaMask encountered an error', + css: 'h3', + }; + + private readonly errorMessage = '[data-testid="error-page-error-message"]'; + + private readonly sendReportToSentryButton = + '[data-testid="error-page-describe-what-happened-button"]'; + + private readonly sentryReportForm = + '[data-testid="error-page-sentry-feedback-modal"]'; + + private readonly contactSupportButton = + '[data-testid="error-page-contact-support-button"]'; + + private readonly sentryFeedbackTextarea = + '[data-testid="error-page-sentry-feedback-textarea"]'; + + private readonly sentryFeedbackSubmitButton = + '[data-testid="error-page-sentry-feedback-submit-button"]'; + + private readonly sentryFeedbackSuccessModal = + '[data-testid="error-page-sentry-feedback-success-modal"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.errorPageTitle); + } catch (e) { + console.log('Timeout while waiting for Error page to be loaded', e); + throw e; + } + console.log('Error page is loaded'); + } + + async triggerPageCrash(): Promise { + const headerNavbar = new HeaderNavbar(this.driver); + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(this.driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToDevelopOptionSettings(); + + const developOptionsPage = new DevelopOptionsPage(this.driver); + await developOptionsPage.check_pageIsLoaded(); + await developOptionsPage.clickGenerateCrashButton(); + } + + async validate_errorMessage(): Promise { + await this.driver.waitForSelector({ + text: `Message: Unable to find value of key "developerOptions" for locale "en"`, + css: this.errorMessage, + }); + } + + async submitToSentryUserFeedbackForm(): Promise { + console.log(`Open sentry user feedback form in error page`); + await this.driver.clickElement(this.sendReportToSentryButton); + await this.driver.waitForSelector(this.sentryReportForm); + await this.driver.fill(this.sentryFeedbackTextarea, FEEDBACK_MESSAGE); + await this.driver.clickElementAndWaitToDisappear( + this.sentryFeedbackSubmitButton, + ); + } + + async contactAndValidateMetaMaskSupport(): Promise { + console.log(`Contact metamask support form in a separate page`); + await this.driver.waitUntilXWindowHandles(1); + await this.driver.clickElement(this.contactSupportButton); + // metamask, help page + await this.driver.waitUntilXWindowHandles(2); + } + + async waitForSentrySuccessModal(): Promise { + await this.driver.waitForSelector(this.sentryFeedbackSuccessModal); + await this.driver.assertElementNotPresent(this.sentryFeedbackSuccessModal); + } +} + +export default ErrorPage; diff --git a/test/e2e/page-objects/pages/settings-page.ts b/test/e2e/page-objects/pages/settings-page.ts index 547f9e43a34e..c029e34efc7e 100644 --- a/test/e2e/page-objects/pages/settings-page.ts +++ b/test/e2e/page-objects/pages/settings-page.ts @@ -9,6 +9,11 @@ class SettingsPage { css: '.tab-bar__tab__content__title', }; + private readonly developerOptionsButton: object = { + text: 'Developer Options', + css: '.tab-bar__tab__content__title', + }; + private readonly settingsPageTitle: object = { text: 'Settings', css: 'h3', @@ -32,6 +37,11 @@ class SettingsPage { console.log('Navigating to Experimental Settings page'); await this.driver.clickElement(this.experimentalSettingsButton); } + + async goToDevelopOptionSettings(): Promise { + console.log('Navigating to Develop options page'); + await this.driver.clickElement(this.developerOptionsButton); + } } export default SettingsPage; diff --git a/test/e2e/tests/metrics/developer-options-sentry.spec.ts b/test/e2e/tests/metrics/developer-options-sentry.spec.ts new file mode 100644 index 000000000000..3c20f931f302 --- /dev/null +++ b/test/e2e/tests/metrics/developer-options-sentry.spec.ts @@ -0,0 +1,88 @@ +import { Suite } from 'mocha'; +import { MockttpServer } from 'mockttp'; +import { withFixtures, sentryRegEx } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { Driver } from '../../webdriver/driver'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import SettingsPage from '../../page-objects/pages/settings-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import DevelopOptions from '../../page-objects/pages/developer-options-page'; +import ErrorPage from '../../page-objects/pages/error-page'; + +const triggerCrash = async (driver: Driver): Promise => { + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToDevelopOptionSettings(); + + const developOptionsPage = new DevelopOptions(driver); + await developOptionsPage.check_pageIsLoaded(); + await developOptionsPage.clickGenerateCrashButton(); +}; + +async function mockSentryError(mockServer: MockttpServer) { + return [ + await mockServer + .forPost(sentryRegEx) + .withBodyIncluding('feedback') + .thenCallback(() => { + return { + statusCode: 200, + json: {}, + }; + }), + ]; +} + +describe('Developer Options - Sentry', function (this: Suite) { + it('gives option to cause a page crash and provides sentry form to report', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + title: this.test?.fullTitle(), + testSpecificMock: mockSentryError, + ignoredConsoleErrors: [ + 'Error#1: Unable to find value of key "developerOptions" for locale "en"', + 'React will try to recreate this component tree from scratch using the error boundary you provided, Index.', + ], + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await triggerCrash(driver); + const errorPage = new ErrorPage(driver); + await errorPage.check_pageIsLoaded(); + await errorPage.validate_errorMessage(); + await errorPage.submitToSentryUserFeedbackForm(); + await errorPage.waitForSentrySuccessModal(); + }, + ); + }); + + it('gives option to cause a page crash and offer contact support option', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + ignoredConsoleErrors: [ + 'Error#1: Unable to find value of key "developerOptions" for locale "en"', + 'React will try to recreate this component tree from scratch using the error boundary you provided, Index.', + ], + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await triggerCrash(driver); + + const errorPage = new ErrorPage(driver); + await errorPage.check_pageIsLoaded(); + + await errorPage.contactAndValidateMetaMaskSupport(); + }, + ); + }); +}); diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index 3b003b044b5a..d4c27a87aba6 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -13,6 +13,7 @@ const { convertToHexValue, logInWithBalanceValidation, withFixtures, + sentryRegEx, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -181,8 +182,6 @@ function getMissingProperties(complete, object) { } describe('Sentry errors', function () { - const sentryRegEx = /^https:\/\/sentry\.io\/api\/\d+\/envelope/gu; - const migrationError = process.env.SELENIUM_BROWSER === Browser.CHROME ? `"type":"TypeError","value":"Cannot read properties of undefined (reading 'version')` diff --git a/ui/ducks/locale/locale.js b/ui/ducks/locale/locale.js deleted file mode 100644 index 5118a749ab58..000000000000 --- a/ui/ducks/locale/locale.js +++ /dev/null @@ -1,42 +0,0 @@ -import { createSelector } from 'reselect'; -import * as actionConstants from '../../store/actionConstants'; - -export default function reduceLocaleMessages(state = {}, { type, payload }) { - switch (type) { - case actionConstants.SET_CURRENT_LOCALE: - return { - ...state, - current: payload.messages, - currentLocale: payload.locale, - }; - default: - return state; - } -} - -/** - * This selector returns a code from file://./../../../app/_locales/index.json. - * - * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that. - * - * @param state - * @returns {string} the user's selected locale. - * These codes are not safe to use with the Intl API. - */ -export const getCurrentLocale = (state) => state.localeMessages.currentLocale; - -/** - * This selector returns a - * [BCP 47 Language Tag](https://en.wikipedia.org/wiki/IETF_language_tag) - * for use with the Intl API. - * - * @returns {Intl.UnicodeBCP47LocaleIdentifier} the user's selected locale. - */ -export const getIntlLocale = createSelector( - getCurrentLocale, - (locale) => Intl.getCanonicalLocales(locale?.replace(/_/gu, '-'))[0], -); - -export const getCurrentLocaleMessages = (state) => state.localeMessages.current; - -export const getEnLocaleMessages = (state) => state.localeMessages.en; diff --git a/ui/ducks/locale/locale.test.ts b/ui/ducks/locale/locale.test.ts index 37fc1f99e29a..67627bb73423 100644 --- a/ui/ducks/locale/locale.test.ts +++ b/ui/ducks/locale/locale.test.ts @@ -1,32 +1,80 @@ // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import locales from '../../../app/_locales/index.json'; -import { getIntlLocale } from './locale'; +import testData from '../../../test/data/mock-state.json'; +import { + getCurrentLocale, + getIntlLocale, + getCurrentLocaleMessages, + getEnLocaleMessages, +} from './locale'; + +// Mock state creation functions const createMockStateWithLocale = (locale: string) => ({ localeMessages: { currentLocale: locale }, }); -describe('getIntlLocale', () => { - it('returns the canonical BCP 47 language tag for the currently selected locale', () => { - const mockState = createMockStateWithLocale('ab-cd'); +describe('Locale Selectors', () => { + describe('getCurrentLocale', () => { + it('returns the current locale from the state', () => { + expect(getCurrentLocale(testData)).toBe('en'); + }); - expect(getIntlLocale(mockState)).toBe('ab-CD'); + it('returns undefined if no current locale is set', () => { + const newAppState = { + ...testData, + localeMessages: { + currentLocale: undefined, + }, + }; + expect(getCurrentLocale(newAppState)).toBeUndefined(); + }); }); - it('throws an error if locale cannot be made into BCP 47 language tag', () => { - const mockState = createMockStateWithLocale('xxxinvalid-locale'); + describe('getIntlLocale', () => { + it('returns the canonical BCP 47 language tag for the currently selected locale', () => { + const mockState = createMockStateWithLocale('en_US'); + expect(getIntlLocale(mockState)).toBe('en-US'); + }); - expect(() => getIntlLocale(mockState)).toThrow(); + locales.forEach((locale: { code: string; name: string }) => { + it(`handles supported locale - "${locale.code}"`, () => { + const mockState = createMockStateWithLocale(locale.code); + expect(() => getIntlLocale(mockState)).not.toThrow(); + }); + }); }); - // @ts-expect-error This is missing from the Mocha type definitions - it.each(locales)( - 'handles all supported locales – "%s"', - (locale: { code: string; name: string }) => { - const mockState = createMockStateWithLocale(locale.code); + describe('getCurrentLocaleMessages', () => { + it('returns the current locale messages from the state', () => { + expect(getCurrentLocaleMessages(testData)).toEqual({ user: 'user' }); + }); + + it('returns undefined if there are no current locale messages', () => { + const newAppState = { + ...testData, + localeMessages: { + current: undefined, + }, + }; + expect(getCurrentLocaleMessages(newAppState)).toEqual(undefined); + }); + }); - expect(() => getIntlLocale(mockState)).not.toThrow(); - }, - ); + describe('getEnLocaleMessages', () => { + it('returns the English locale messages from the state', () => { + expect(getEnLocaleMessages(testData)).toEqual({ user: 'user' }); + }); + + it('returns undefined if there are no English locale messages', () => { + const newAppState = { + ...testData, + localeMessages: { + en: undefined, + }, + }; + expect(getEnLocaleMessages(newAppState)).toBeUndefined(); + }); + }); }); diff --git a/ui/ducks/locale/locale.ts b/ui/ducks/locale/locale.ts new file mode 100644 index 000000000000..2ca0cbaac4dc --- /dev/null +++ b/ui/ducks/locale/locale.ts @@ -0,0 +1,108 @@ +import { createSelector } from 'reselect'; +import { Action } from 'redux'; // Import types for actions +import * as actionConstants from '../../store/actionConstants'; +import { FALLBACK_LOCALE } from '../../../shared/modules/i18n'; + +/** + * Type for the locale messages part of the state + */ +type LocaleMessagesState = { + current?: { [key: string]: string }; // Messages for the current locale + currentLocale?: string; // User's selected locale (unsafe for Intl API) + en?: { [key: string]: string }; // English locale messages +}; + +/** + * Payload for the SET_CURRENT_LOCALE action + */ +type SetCurrentLocaleAction = Action & { + type: typeof actionConstants.SET_CURRENT_LOCALE; + payload: { + messages: { [key: string]: string }; + locale: string; + }; +}; + +/** + * Type for actions that can be handled by reduceLocaleMessages + */ +type LocaleMessagesActions = SetCurrentLocaleAction; + +/** + * Initial state for localeMessages reducer + */ +const initialState: LocaleMessagesState = {}; + +/** + * Reducer for localeMessages + * + * @param state - The current state + * @param action - The action being dispatched + * @returns The updated locale messages state + */ +export default function reduceLocaleMessages( + // eslint-disable-next-line @typescript-eslint/default-param-last + state: LocaleMessagesState = initialState, + action: LocaleMessagesActions, +): LocaleMessagesState { + switch (action.type) { + case actionConstants.SET_CURRENT_LOCALE: + return { + ...state, + current: action.payload.messages, + currentLocale: action.payload.locale, + }; + default: + return state; + } +} + +/** + * Type for the overall Redux state + */ +type AppState = { + localeMessages: LocaleMessagesState; +}; + +/** + * This selector returns a code from file://./../../../app/_locales/index.json. + * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that. + * + * @param state - The overall state + * @returns The user's selected locale + */ +export const getCurrentLocale = (state: AppState): string | undefined => + state.localeMessages.currentLocale; + +/** + * This selector returns a BCP 47 Language Tag for use with the Intl API. + * + * @returns The user's selected locale in BCP 47 format + */ +export const getIntlLocale = createSelector( + getCurrentLocale, + (locale): string => + Intl.getCanonicalLocales( + locale ? locale.replace(/_/gu, '-') : FALLBACK_LOCALE, + )[0], +); + +/** + * This selector returns the current locale messages. + * + * @param state - The overall state + * @returns The current locale's messages + */ +export const getCurrentLocaleMessages = ( + state: AppState, +): Record | undefined => state.localeMessages.current; + +/** + * This selector returns the English locale messages. + * + * @param state - The overall state + * @returns The English locale's messages + */ +export const getEnLocaleMessages = ( + state: AppState, +): Record | undefined => state.localeMessages.en; diff --git a/ui/pages/error-page/error-component.test.tsx b/ui/pages/error-page/error-component.test.tsx new file mode 100644 index 000000000000..16188c30881a --- /dev/null +++ b/ui/pages/error-page/error-component.test.tsx @@ -0,0 +1,192 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import '@testing-library/jest-dom/extend-expect'; +import browser from 'webextension-polyfill'; +import { fireEvent } from '@testing-library/react'; +import { renderWithProvider } from '../../../test/lib/render-helpers'; +import { useI18nContext } from '../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../contexts/metametrics'; +import { getParticipateInMetaMetrics } from '../../selectors'; +import { getMessage } from '../../helpers/utils/i18n-helper'; +// eslint-disable-next-line import/no-restricted-paths +import messages from '../../../app/_locales/en/messages.json'; +import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common'; +import { + MetaMetricsContextProp, + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../shared/constants/metametrics'; +import ErrorPage from './error-page.component'; + +jest.mock('../../hooks/useI18nContext', () => ({ + useI18nContext: jest.fn(), +})); + +jest.mock('webextension-polyfill', () => ({ + runtime: { + reload: jest.fn(), + }, +})); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); + +describe('ErrorPage', () => { + const useSelectorMock = useSelector as jest.Mock; + const mockTrackEvent = jest.fn(); + const MockError = new Error( + "Cannot read properties of undefined (reading 'message')", + ) as Error & { code?: string }; + MockError.code = '500'; + + const mockI18nContext = jest + .fn() + .mockReturnValue((key: string, variables: string[]) => + getMessage('en', messages, key, variables), + ); + + beforeEach(() => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getParticipateInMetaMetrics) { + return true; + } + return undefined; + }); + (useI18nContext as jest.Mock).mockImplementation(mockI18nContext); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should render the error message, code, and name if provided', () => { + const { getByTestId } = renderWithProvider( + + + , + ); + + expect( + getByTestId('error-page-error-message').textContent, + ).toMatchInlineSnapshot( + `"Message: Cannot read properties of undefined (reading 'message')"`, + ); + expect( + getByTestId('error-page-error-code').textContent, + ).toMatchInlineSnapshot(`"Code: 500"`); + expect( + getByTestId('error-page-error-name').textContent, + ).toMatchInlineSnapshot(`"Code: Error"`); + }); + + it('should not render error details if no error information is provided', () => { + const error = {}; + + const { queryByTestId } = renderWithProvider( + + + , + ); + + expect(queryByTestId('error-page-error-message')).toBeNull(); + expect(queryByTestId('error-page-error-code')).toBeNull(); + expect(queryByTestId('error-page-error-name')).toBeNull(); + expect(queryByTestId('error-page-error-stack')).toBeNull(); + }); + + it('should render sentry user feedback form and submit sentry report successfully when metrics is opted in', () => { + const { getByTestId, queryByTestId } = renderWithProvider( + + + , + ); + const describeButton = getByTestId( + 'error-page-describe-what-happened-button', + ); + fireEvent.click(describeButton); + expect( + queryByTestId('error-page-sentry-feedback-modal'), + ).toBeInTheDocument(); + const textarea = getByTestId('error-page-sentry-feedback-textarea'); + fireEvent.change(textarea, { + target: { value: 'Something went wrong on develop option page' }, + }); + const submitButton = getByTestId( + 'error-page-sentry-feedback-submit-button', + ); + fireEvent.click(submitButton); + expect( + queryByTestId('error-page-sentry-feedback-modal'), + ).not.toBeInTheDocument(); + expect( + queryByTestId('error-page-sentry-feedback-success-modal'), + ).toBeInTheDocument(); + jest.advanceTimersByTime(5000); + expect( + queryByTestId('error-page-sentry-feedback-modal'), + ).not.toBeInTheDocument(); + }); + + it('should render not sentry user feedback option when metrics is not opted in', () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getParticipateInMetaMetrics) { + return false; + } + return undefined; + }); + const { queryByTestId } = renderWithProvider( + + + , + ); + const describeButton = queryByTestId( + 'error-page-describe-what-happened-button', + ); + + expect(describeButton).toBeNull(); + }); + + it('should reload the extension when the "Try Again" button is clicked', () => { + const { getByTestId } = renderWithProvider( + + + , + ); + const tryAgainButton = getByTestId('error-page-try-again-button'); + fireEvent.click(tryAgainButton); + expect(browser.runtime.reload).toHaveBeenCalled(); + }); + + it('should open the support link and track the MetaMetrics event when the "Contact Support" button is clicked', () => { + window.open = jest.fn(); + + const { getByTestId } = renderWithProvider( + + + , + ); + + const contactSupportButton = getByTestId( + 'error-page-contact-support-button', + ); + fireEvent.click(contactSupportButton); + + expect(window.open).toHaveBeenCalledWith(SUPPORT_REQUEST_LINK, '_blank'); + + expect(mockTrackEvent).toHaveBeenCalledWith( + { + category: MetaMetricsEventCategory.Error, + event: MetaMetricsEventName.SupportLinkClicked, + properties: { + url: SUPPORT_REQUEST_LINK, + }, + }, + { + contextPropsIntoEventProperties: [MetaMetricsContextProp.PageTitle], + }, + ); + }); +}); diff --git a/ui/pages/error-page/error-page.component.tsx b/ui/pages/error-page/error-page.component.tsx new file mode 100644 index 000000000000..9a094057dcc0 --- /dev/null +++ b/ui/pages/error-page/error-page.component.tsx @@ -0,0 +1,312 @@ +import React, { useEffect, useContext, useState } from 'react'; +import { useSelector } from 'react-redux'; +import * as Sentry from '@sentry/browser'; +import browser from 'webextension-polyfill'; +import { + MetaMetricsContextProp, + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../shared/constants/metametrics'; + +import { getParticipateInMetaMetrics } from '../../selectors'; +import { MetaMetricsContext } from '../../contexts/metametrics'; +import { useI18nContext } from '../../hooks/useI18nContext'; +import { + BannerAlert, + Box, + Icon, + IconName, + IconSize, + Text, + Button, + ButtonVariant, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, +} from '../../components/component-library'; +import { + AlignItems, + BackgroundColor, + BlockSize, + BorderRadius, + Display, + FlexDirection, + IconColor, + JustifyContent, + TextColor, + TextVariant, +} from '../../helpers/constants/design-system'; + +import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common'; + +import { Textarea } from '../../components/component-library/textarea/textarea'; +import { TextareaResize } from '../../components/component-library/textarea/textarea.types'; +import { ButtonSize } from '../../components/component-library/button/button.types'; + +type ErrorPageProps = { + error: { + message?: string; + code?: string; + name?: string; + stack?: string; + }; +}; + +const ErrorPage: React.FC = ({ error }) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); + + const [feedbackMessage, setFeedbackMessage] = useState(''); + const [isFeedbackModalOpen, setIsFeedbackModalOpen] = useState(false); + const [isSuccessModalShown, setIsSuccessModalShown] = useState(false); + + const handleClickDescribeButton = (): void => { + setIsFeedbackModalOpen(true); + }; + + const handleCloseDescribeModal = (): void => { + setIsFeedbackModalOpen(false); + }; + + const handleSubmitFeedback = (e: React.MouseEvent) => { + e.preventDefault(); + const eventId = Sentry.lastEventId(); + + Sentry.captureFeedback({ + message: feedbackMessage, + associatedEventId: eventId, + }); + handleCloseDescribeModal(); + setIsSuccessModalShown(true); + }; + + useEffect(() => { + if (isSuccessModalShown) { + const timeoutId = setTimeout(() => { + setIsSuccessModalShown(false); // Close the modal after 5 seconds + }, 5000); + + // Cleanup function to clear timeout if the component unmounts or state changes + return () => clearTimeout(timeoutId); + } + return undefined; + }, [isSuccessModalShown]); + + return ( +
+
+ + + + {t('errorPageTitle')} + + + +
+ {t('errorPageInfo')} +
+ + {t('errorPageMessageTitle')} + + + {error.message ? ( + + {t('errorMessage', [error.message])} + + ) : null} + {error.code ? ( + + {t('errorCode', [error.code])} + + ) : null} + {error.name ? ( + + {t('errorName', [error.name])} + + ) : null} + {error.stack ? ( + <> + + {t('errorStack')} + +
+                {error.stack}
+              
+ + ) : null} +
+ + {isFeedbackModalOpen && ( + + + + + {t('errorPageSentryFormTitle')} + + +