From d4cc2d2dbd56118c4b403e2cc4ddc6c7d206ac60 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Fri, 15 Sep 2023 14:38:54 -0230 Subject: [PATCH 001/219] Version v11.2.0 --- CHANGELOG.md | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4225375f477a..d43da1156375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.2.0] + ## [11.1.0] ## [11.0.0] @@ -3973,7 +3975,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Uncategorized - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v11.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v11.2.0...HEAD +[11.2.0]: https://github.com/MetaMask/metamask-extension/compare/v11.1.0...v11.2.0 [11.1.0]: https://github.com/MetaMask/metamask-extension/compare/v11.0.0...v11.1.0 [11.0.0]: https://github.com/MetaMask/metamask-extension/compare/v10.35.1...v11.0.0 [10.35.1]: https://github.com/MetaMask/metamask-extension/compare/v10.35.0...v10.35.1 diff --git a/package.json b/package.json index 529ec9106d7a..b4cc7756b7d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "11.1.0", + "version": "11.2.0", "private": true, "repository": { "type": "git", From b489d64ca52422b5229df741da6f762b4c378e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Tue, 19 Sep 2023 13:53:54 +0100 Subject: [PATCH 002/219] [MMI] updates the "connect custodian account" flow (#20387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * adds stories file and unit test * adds the logic to open custodian url or not * lint * fixes styles, and clean up * test update * remove unused locale * small update * lint * ran verify locales fix * fix typo * updates code with configuration api v2 * clean up * lint and prettier * test fix * prettier * packages update * updates for config v2 * updates for config v2 * clean up * Update LavaMoat policies --------- Co-authored-by: Albert Olivé Co-authored-by: MetaMask Bot Co-authored-by: Danica Shen --- app/_locales/de/messages.json | 9 -- app/_locales/el/messages.json | 9 -- app/_locales/en/messages.json | 31 +++-- 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 -- builds.yml | 2 +- lavamoat/browserify/mmi/policy.json | 3 +- package.json | 10 +- .../find-by-custodian-name.test.ts | 53 +++++++++ .../institutional/find-by-custodian-name.ts | 35 ++++++ .../confirm-add-custodian-token.js | 111 ++++++------------ .../confirm-add-custodian-token.test.js | 19 +-- .../confirm-connect-custodian-modal.js | 99 ++++++++++++++++ ...confirm-connect-custodian-modal.stories.js | 23 ++++ .../confirm-connect-custodian-modal.test.js | 42 +++++++ .../confirm-connect-custodian-modal/index.js | 1 + ui/pages/institutional/custody/custody.js | 45 ++++++- .../institutional/custody/custody.test.js | 7 +- .../institutional-entity-done-page.js | 37 +++--- yarn.lock | 84 ++++++------- 30 files changed, 417 insertions(+), 311 deletions(-) create mode 100644 ui/helpers/utils/institutional/find-by-custodian-name.test.ts create mode 100644 ui/helpers/utils/institutional/find-by-custodian-name.ts create mode 100644 ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.js create mode 100644 ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.stories.js create mode 100644 ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.js create mode 100644 ui/pages/institutional/confirm-connect-custodian-modal/index.js diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 0000e0c1ebc6..730cd338ff57 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "Betrag" }, - "apiUrl": { - "message": "API-URL" - }, "appDescription": { "message": "Ethereum Browsererweiterung", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "Verwahrer" }, - "custodianAccount": { - "message": "Verwahrungskonto" - }, "custodianAccountAddedDesc": { "message": "Sie können jetzt Verwahrungskonten in MetaMask Institutional verwenden." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "Hier anfragen" }, - "mmiAddToken": { - "message": "Die Seite bei $1 möchte das folgende Verwahrungstoken in MetaMask Institutional autorisieren." - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional ist weltweit konzipiert und aufgebaut." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 5cd056ebbbae..132e7a9ddc72 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "Ποσό" }, - "apiUrl": { - "message": "API URL" - }, "appDescription": { "message": "Ένα Πορτοφόλι Ethereum στο Πρόγραμμα Περιήγησής σας", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "Θεματοφύλακας" }, - "custodianAccount": { - "message": "Λογαριασμός θεματοφύλακα" - }, "custodianAccountAddedDesc": { "message": "Μπορείτε τώρα να χρησιμοποιήσετε τους λογαριασμούς θεματοφύλακά σας στο MetaMask Institutional." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "Υποβάλετε αίτημα εδώ" }, - "mmiAddToken": { - "message": "Η σελίδα στο $1 θα ήθελε να εξουσιοδοτήσει το ακόλουθο token θεματοφύλακα στο MetaMask Institutional" - }, "mmiBuiltAroundTheWorld": { "message": "Το MetaMask Institutional έχει σχεδιαστεί και λειτουργεί σε όλο τον κόσμο." }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 6092721aa172..d851b2529421 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -351,9 +351,15 @@ "message": "All of your NFTs from $1", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" }, + "allow": { + "message": "Allow" + }, "allowExternalExtensionTo": { "message": "Allow this external extension to:" }, + "allowMmiToConnectToCustodian": { + "message": "This will allow MMI to connect to $1 to import your accounts." + }, "allowSpendToken": { "message": "Give permission to access your $1?", "description": "$1 is the symbol of the token that are requesting to spend" @@ -371,9 +377,6 @@ "amount": { "message": "Amount" }, - "apiUrl": { - "message": "API URL" - }, "appDescription": { "message": "An Ethereum Wallet in your Browser", "description": "The description of the application" @@ -732,6 +735,15 @@ "confirm": { "message": "Confirm" }, + "confirmConnectCustodianRedirect": { + "message": "We will redirect you to $1 upon clicking continue." + }, + "confirmConnectCustodianText": { + "message": "To connect your accounts log into your $1 account and click on the 'connect to MMI' button." + }, + "confirmConnectionTitle": { + "message": "Confirm connection to $1" + }, "confirmPassword": { "message": "Confirm password" }, @@ -765,6 +777,9 @@ "connectCustodialAccountTitle": { "message": "Custodial Accounts" }, + "connectCustodianAccounts": { + "message": "Connect $1 accounts" + }, "connectManually": { "message": "Manually connect to current site" }, @@ -981,14 +996,11 @@ "custodian": { "message": "Custodian" }, - "custodianAccount": { - "message": "Custodian account" - }, "custodianAccountAddedDesc": { - "message": "You can now use your custodian accounts in MetaMask Institutional." + "message": "You can now use your accounts in MetaMask Institutional." }, "custodianAccountAddedTitle": { - "message": "Selected custodian accounts have been added." + "message": "Selected $1 accounts have been added." }, "custodianReplaceRefreshTokenChangedFailed": { "message": "Please go to $1 and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again." @@ -2346,9 +2358,6 @@ "missingSettingRequest": { "message": "Request here" }, - "mmiAddToken": { - "message": "The page at $1 would like to authorise the following custodian token in MetaMask Institutional" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional is designed and built around the world." }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 61f6fd18b123..a8c3be966f45 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "Importe" }, - "apiUrl": { - "message": "URL de la API" - }, "appDescription": { "message": "Un monedero de Ethereum en el explorador", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "Custodio" }, - "custodianAccount": { - "message": "Cuenta custodiada" - }, "custodianAccountAddedDesc": { "message": "Ahora ya puede usar sus cuentas custodiadas en MetaMask Institutional." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "Solicítelo aquí" }, - "mmiAddToken": { - "message": "A la página de $1 le gustaría autorizar el siguiente token custodiado en MetaMask Institutional" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional está diseñado y construido en todo el mundo." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index a8cb8fc43363..5d92ea6756ee 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "Montant" }, - "apiUrl": { - "message": "URL de l’API" - }, "appDescription": { "message": "Extension Ethereum pour navigateur", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "Dépôt" }, - "custodianAccount": { - "message": "Compte de dépôt" - }, "custodianAccountAddedDesc": { "message": "Vous pouvez maintenant utiliser vos comptes de dépôt dans MetaMask Institutional." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "Demandez ici" }, - "mmiAddToken": { - "message": "La page à $1 souhaite autoriser le jeton de dépôt suivant dans MetaMask institutionnel" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional est conçu et établi dans le monde entier." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 7cdc108f1112..eaebc034dfc0 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "अमाउंट" }, - "apiUrl": { - "message": "API URL" - }, "appDescription": { "message": "आपके ब्राउज़र में एक Ethereum वॉलेट", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "कस्टोडियन" }, - "custodianAccount": { - "message": "कस्टोडियन अकाउंट" - }, "custodianAccountAddedDesc": { "message": "अब आप MetaMask Institutional में अपने कस्टोडियन एकाउंट्स का इस्तेमाल कर सकते हैं।" }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "यहां रिक्वेस्ट करें" }, - "mmiAddToken": { - "message": "$1 पर मौजूद पेज MetaMask Institutional में निम्नलिखित कस्टोडियन टोकन को अधिकृत करना चाहता है" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional को दुनिया भर में डिजाइन किया और बनाया गया है।" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index ce485b6e7837..e0d4921b4dac 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "Jumlah" }, - "apiUrl": { - "message": "URL API" - }, "appDescription": { "message": "Dompet Ethereum pada Browser Anda", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "Kustodian" }, - "custodianAccount": { - "message": "Akun kustodian" - }, "custodianAccountAddedDesc": { "message": "Saat ini Anda dapat menggunakan akun kustodian di MetaMask Institutional." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "Minta di sini" }, - "mmiAddToken": { - "message": "Halaman di $1 ingin mengotorisasi token kustodian berikut di MetaMask Institutional" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional dirancang dan dibangun di seluruh dunia." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 3464133a1e2b..0ce08bf3df7c 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "金額" }, - "apiUrl": { - "message": "API URL" - }, "appDescription": { "message": "ブラウザにあるイーサリアムウォレット", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "カストディアン" }, - "custodianAccount": { - "message": "カストディアンアカウント" - }, "custodianAccountAddedDesc": { "message": "カストディアンアカウントをMetaMask Institutionalで使えるようになりました。" }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "ここからリクエスト" }, - "mmiAddToken": { - "message": "$1のページがMetaMask Institutionalで次のカストディアントークンの承認を求めています" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMaskは、世界中でデザイン・開発されています。" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index d8fc9a3b7bb8..294bcb5c82de 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "금액" }, - "apiUrl": { - "message": "API URL" - }, "appDescription": { "message": "브라우저의 이더리움 지갑", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "수탁자" }, - "custodianAccount": { - "message": "수탁 계정" - }, "custodianAccountAddedDesc": { "message": "이제 MetaMask Institutional에서 수탁 계정을 사용할 수 있습니다." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "여기에서 요청하세요" }, - "mmiAddToken": { - "message": "$1의 페이지는 다음과 같은 MetaMask 기관 수탁 토큰을 승인하려고 합니다" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional은 전 세계적으로 설계 및 구축되었습니다." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 9be840098d4e..03b770aeabd8 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "Valor" }, - "apiUrl": { - "message": "URL da API" - }, "appDescription": { "message": "Uma carteira de Ethereum no seu navegador", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "Custodiante" }, - "custodianAccount": { - "message": "Conta custodiante" - }, "custodianAccountAddedDesc": { "message": "Agora você pode usar suas contas de custódia no MetaMask Institutional." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "Solicite aqui" }, - "mmiAddToken": { - "message": "A página em $1 gostaria de autorizar o seguinte token de custodiante na MetaMask Institutional" - }, "mmiBuiltAroundTheWorld": { "message": "O MetaMask Institutional é projetado e desenvolvido ao redor do mundo." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 615acee30cfc..493b1f33bf1f 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "Сумма" }, - "apiUrl": { - "message": "URL API" - }, "appDescription": { "message": "Кошелек Ethereum в вашем браузере", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "Депозитарий" }, - "custodianAccount": { - "message": "Депозитарный счет" - }, "custodianAccountAddedDesc": { "message": "Теперь вы можете использовать свои депозитарные счета в MetaMask Institutional." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "Запросите здесь" }, - "mmiAddToken": { - "message": "Страница в $1 хочет авторизовать следующий токен депозитария в MetaMask Institutional" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional разработан и создан с учетом потребностей в разных частях мира." }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index b6cb89849a2f..167d0fd77526 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "Halaga" }, - "apiUrl": { - "message": "API URL" - }, "appDescription": { "message": "Ethereum Wallet sa iyong Browser", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "Tagapangalaga" }, - "custodianAccount": { - "message": "Account ng tagapangalaga" - }, "custodianAccountAddedDesc": { "message": "Maaari mo na ngayong gamitin ang iyong mga custodian account sa MetaMask Institutional." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "Humiling dito" }, - "mmiAddToken": { - "message": "Ang pahina sa $1 ay nais pahintulutan ang sumusunod na tagapangalaga na token sa MetaMask Institutional" - }, "mmiBuiltAroundTheWorld": { "message": "Ang MetaMask Institutional ay dinisenyo at binuo sa buong mundo." }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 57f96b4a585c..09e233a827f6 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "Tutar" }, - "apiUrl": { - "message": "API URL" - }, "appDescription": { "message": "Tarayıcında bir Ethereum Cüzdanı", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "Saklayıcı Kurum" }, - "custodianAccount": { - "message": "Saklayıcı kurum hesabı" - }, "custodianAccountAddedDesc": { "message": "Artık saklayıcı kurum hesaplarınızı MetaMask Institutional'da kullanabilirsiniz." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "Buradan talep et" }, - "mmiAddToken": { - "message": "$1 alanındaki sayfa MetaMask Institutional'daki aşağıdaki saklayıcı kurum tokenine yetki vermek istiyor" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional dünya çapında tasarlanmış ve yapılmıştır." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index b7e7d3d06e1d..f78d61cc37fe 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "Số tiền" }, - "apiUrl": { - "message": "URL API" - }, "appDescription": { "message": "Ví Ethereum trên trình duyệt của bạn", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "Lưu ký" }, - "custodianAccount": { - "message": "Tài khoản lưu ký" - }, "custodianAccountAddedDesc": { "message": "Giờ đây bạn có thể sử dụng tài khoản lưu ký của mình trong MetaMask Institutional." }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "Yêu cầu tại đây" }, - "mmiAddToken": { - "message": "Trang tại $1 muốn ủy quyền token lưu ký sau trong MetaMask Institutional" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional được thiết kế và xây dựng trên khắp thế giới." }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 25db029fac40..62fdb4d56b6a 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -371,9 +371,6 @@ "amount": { "message": "数额" }, - "apiUrl": { - "message": "API URL" - }, "appDescription": { "message": "浏览器中的以太坊钱包", "description": "The description of the application" @@ -981,9 +978,6 @@ "custodian": { "message": "托管人" }, - "custodianAccount": { - "message": "托管账户" - }, "custodianAccountAddedDesc": { "message": "您现在可以在 MetaMask Institutional 使用您的托管账户。 " }, @@ -2346,9 +2340,6 @@ "missingSettingRequest": { "message": "在这里请求" }, - "mmiAddToken": { - "message": "$1的页面想在 MetaMask Institutional 中授权以下托管代币" - }, "mmiBuiltAroundTheWorld": { "message": "MetaMask Institutional 面向全球各地设计并建立。" }, diff --git a/builds.yml b/builds.yml index 5ad53a2ff993..e0e133afbc5f 100644 --- a/builds.yml +++ b/builds.yml @@ -92,7 +92,7 @@ buildTypes: - SEGMENT_MMI_WRITE_KEY - INFURA_ENV_KEY_REF: INFURA_MMI_PROJECT_ID - SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY - - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v1/configuration/default + - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default - SUPPORT_LINK: https://mmi-support.zendesk.com/hc/en-us - SUPPORT_REQUEST_LINK: https://mmi-support.zendesk.com/hc/en-us/requests/new # For some reason, MMI uses this type of versioning diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 578f541b1d3e..1ed715b4d89a 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -761,8 +761,7 @@ }, "@metamask-institutional/extension": { "globals": { - "console.log": true, - "fetch": true + "console.log": true }, "packages": { "@ethereumjs/tx>@ethereumjs/util": true, diff --git a/package.json b/package.json index b4cc7756b7d4..5274641bc3f5 100644 --- a/package.json +++ b/package.json @@ -224,14 +224,14 @@ "@keystonehq/metamask-airgapped-keyring": "^0.13.1", "@lavamoat/snow": "^1.5.0", "@material-ui/core": "^4.11.0", - "@metamask-institutional/custody-controller": "^0.2.10", - "@metamask-institutional/custody-keyring": "^0.0.27", - "@metamask-institutional/extension": "^0.3.3", - "@metamask-institutional/institutional-features": "^1.2.2", + "@metamask-institutional/custody-controller": "^0.2.12", + "@metamask-institutional/custody-keyring": "^1.0.1", + "@metamask-institutional/extension": "^0.3.5", + "@metamask-institutional/institutional-features": "^1.2.4", "@metamask-institutional/portfolio-dashboard": "^1.4.0", "@metamask-institutional/rpc-allowlist": "^1.0.0", "@metamask-institutional/sdk": "^0.1.18", - "@metamask-institutional/transaction-update": "^0.1.25", + "@metamask-institutional/transaction-update": "^0.1.27", "@metamask/address-book-controller": "^3.0.0", "@metamask/announcement-controller": "^4.0.0", "@metamask/approval-controller": "^3.4.0", diff --git a/ui/helpers/utils/institutional/find-by-custodian-name.test.ts b/ui/helpers/utils/institutional/find-by-custodian-name.test.ts new file mode 100644 index 000000000000..86cdb977e4ba --- /dev/null +++ b/ui/helpers/utils/institutional/find-by-custodian-name.test.ts @@ -0,0 +1,53 @@ +import { findCustodianByDisplayName } from './find-by-custodian-name'; + +describe('findCustodianByDisplayName', () => { + const custodians = [ + { + type: 'JSONRPC', + iconUrl: '', + name: 'Qredo', + website: 'https://www.qredo.com/', + envName: 'qredo', + apiUrl: null, + displayName: null, + production: false, + refreshTokenUrl: null, + websocketApiUrl: 'wss://websocket.dev.metamask-institutional.io/v1/ws', + isNoteToTraderSupported: true, + version: 2, + }, + { + type: 'JSONRPC', + iconUrl: + 'https://saturn-custody-ui.dev.metamask-institutional.io/saturn.svg', + name: 'Saturn Custody', + website: 'https://saturn-custody-ui.dev.metamask-institutional.io/', + envName: 'saturn', + apiUrl: 'https://saturn-custody.dev.metamask-institutional.io/eth', + displayName: null, + production: false, + refreshTokenUrl: + 'https://saturn-custody.dev.metamask-institutional.io/oauth/token', + websocketApiUrl: 'wss://websocket.dev.metamask-institutional.io/v1/ws', + isNoteToTraderSupported: true, + version: 2, + }, + ]; + it('should return the custodian if the display name is found in custodianKey', () => { + const displayName = 'Qredo'; + const custodian = findCustodianByDisplayName(displayName, custodians); + expect(custodian?.name).toBe('Qredo'); + }); + + it('should return the custodian if the display name is found in custodianDisplayName', () => { + const displayName = 'Saturn Custody'; + const custodian = findCustodianByDisplayName(displayName, custodians); + expect(custodian?.name).toContain('Saturn'); + }); + + it('should return null if no matching custodian is found', () => { + const displayName = 'Non-existent Custodian'; + const custodian = findCustodianByDisplayName(displayName, custodians); + expect(custodian).toBeNull(); + }); +}); diff --git a/ui/helpers/utils/institutional/find-by-custodian-name.ts b/ui/helpers/utils/institutional/find-by-custodian-name.ts new file mode 100644 index 000000000000..52a05b3b7b27 --- /dev/null +++ b/ui/helpers/utils/institutional/find-by-custodian-name.ts @@ -0,0 +1,35 @@ +type Custodian = { + type: string; + iconUrl: string; + name: string; + website: string; + envName: string; + apiUrl: string | null; + displayName: string | null; + production: boolean; + refreshTokenUrl: string | null; + websocketApiUrl: string; + isNoteToTraderSupported: boolean; + version: number; +}; + +export function findCustodianByDisplayName( + displayName: string, + custodians: Custodian[], +): Custodian | null { + const formatedDisplayName = displayName.toLowerCase(); + + if (!custodians) { + return null; + } + + for (const custodian of custodians) { + const custodianName = custodian.name.toLowerCase(); + + if (formatedDisplayName.includes(custodianName)) { + return custodian; + } + } + + return null; // no matching custodian is found +} diff --git a/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.js b/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.js index f8d867f6aa56..c30dad8efee5 100644 --- a/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.js +++ b/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.js @@ -5,21 +5,22 @@ import { useHistory } from 'react-router-dom'; import PulseLoader from '../../../components/ui/pulse-loader'; import { CUSTODY_ACCOUNT_ROUTE } from '../../../helpers/constants/routes'; import { - AlignItems, Display, TextColor, TextAlign, - FlexDirection, + FontWeight, + TextVariant, + BorderColor, } from '../../../helpers/constants/design-system'; +import Chip from '../../../components/ui/chip'; import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; import { I18nContext } from '../../../contexts/i18n'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { setProviderType } from '../../../store/actions'; import { mmiActionsFactory } from '../../../store/institutional/institution-background'; +import { getMMIConfiguration } from '../../../selectors/institutional/selectors'; import { - Label, - ButtonLink, Button, BUTTON_SIZES, BUTTON_VARIANT, @@ -31,6 +32,7 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { getInstitutionalConnectRequests } from '../../../ducks/institutional/institutional'; +import { findCustodianByDisplayName } from '../../../helpers/utils/institutional/find-by-custodian-name'; const ConfirmAddCustodianToken = () => { const t = useContext(I18nContext); @@ -39,9 +41,9 @@ const ConfirmAddCustodianToken = () => { const trackEvent = useContext(MetaMetricsContext); const mmiActions = mmiActionsFactory(); + const { custodians } = useSelector(getMMIConfiguration); const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage); const connectRequests = useSelector(getInstitutionalConnectRequests, isEqual); - const [showMore, setShowMore] = useState(false); const [isLoading, setIsLoading] = useState(false); const [connectError, setConnectError] = useState(''); @@ -68,7 +70,7 @@ const ConfirmAddCustodianToken = () => { }, }); - let custodianLabel = ''; + let custodianLabel = t('custodian'); if ( connectRequest.labels && @@ -79,81 +81,38 @@ const ConfirmAddCustodianToken = () => { ).value; } + const custodian = findCustodianByDisplayName(custodianLabel, custodians); + return ( - - {t('custodianAccount')} - - {t('mmiAddToken', [connectRequest.origin])} - + + - {custodianLabel && ( - <> - - {t('custodian')} - - - - )} - - - {t('token')} + + {t('confirmConnectionTitle', [custodianLabel])} - - - - {showMore && connectRequest?.token - ? connectRequest?.token - : `...${connectRequest?.token.slice(-9)}`} - - {!showMore && ( - - { - setShowMore(true); - }} - > - {t('showMore')} - - - )} - - - {connectRequest.apiUrl && ( - - - {t('apiUrl')} - - - {connectRequest.apiUrl} - - - )} + {t('allowMmiToConnectToCustodian', [custodianLabel])} + @@ -258,7 +217,7 @@ const ConfirmAddCustodianToken = () => { } }} > - {t('confirm')} + {t('allow')} )} diff --git a/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.js b/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.js index 8fafcdd864eb..cc0c0b1cc5c4 100644 --- a/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.js +++ b/ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.test.js @@ -50,23 +50,6 @@ describe('Confirm Add Custodian Token', () => { const store = configureMockStore()(mockStore); - it('opens confirm add custodian token with correct token', () => { - renderWithProvider(, store); - - const tokenContainer = screen.getByText('...testToken'); - expect(tokenContainer).toBeInTheDocument(); - }); - - it('shows the custodian on cancel click', () => { - renderWithProvider(, store); - - const cancelButton = screen.getByTestId('cancel-btn'); - - fireEvent.click(cancelButton); - - expect(screen.getByText('Custodian')).toBeInTheDocument(); - }); - it('tries to connect to custodian with empty token', async () => { const customMockedStore = { metamask: { @@ -119,7 +102,7 @@ describe('Confirm Add Custodian Token', () => { const confirmButton = screen.getByTestId('confirm-btn'); fireEvent.click(confirmButton); - expect(screen.getByText('test')).toBeInTheDocument(); + expect(screen.getByText('Confirm connection to test')).toBeInTheDocument(); }); it('shows the error area', () => { diff --git a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.js b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.js new file mode 100644 index 000000000000..b3269b2ad607 --- /dev/null +++ b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.js @@ -0,0 +1,99 @@ +import PropTypes from 'prop-types'; +import React, { useContext } from 'react'; +import { + AlignItems, + Display, + TextColor, + FlexDirection, + TextAlign, +} from '../../../helpers/constants/design-system'; +import { I18nContext } from '../../../contexts/i18n'; +import { + Button, + BUTTON_VARIANT, + Box, + Text, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, +} from '../../../components/component-library'; + +const ConfirmConnectCustodianModal = ({ + onModalClose, + custodianName, + custodianURL, +}) => { + const t = useContext(I18nContext); + + return ( + + + + + {t('connectCustodianAccounts', [custodianName])} + + + + {t('confirmConnectCustodianText', [custodianName])} + + + + {t('confirmConnectCustodianRedirect', [custodianURL])} + + + + + + + + + + ); +}; + +export default ConfirmConnectCustodianModal; + +ConfirmConnectCustodianModal.propTypes = { + onModalClose: PropTypes.func.isRequired, + custodianName: PropTypes.string.isRequired, + custodianURL: PropTypes.string.isRequired, +}; diff --git a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.stories.js b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.stories.js new file mode 100644 index 000000000000..66585a421bfc --- /dev/null +++ b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.stories.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import ConfirmConnectCustodianModal from '.'; + +const store = configureStore(testData); + +export default { + title: 'Components/Institutional/ConfirmConnectCustodianModal', + decorators: [(story) => {story()}], + component: ConfirmConnectCustodianModal, +}; + +export const DefaultStory = () => ( + +); + +DefaultStory.storyName = 'ConfirmConnectCustodianModal'; diff --git a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.js b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.js new file mode 100644 index 000000000000..83f2d631a5dd --- /dev/null +++ b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.test.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import ConfirmAddCustodianToken from '.'; + +describe('Confirm Add Custodian Token', () => { + global.platform = { openTab: jest.fn() }; + + const mockStore = { + metamask: { + providerConfig: { + type: 'test', + }, + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + history: { + push: '/', + mostRecentOverviewPage: '/', + }, + }; + + const store = configureMockStore()(mockStore); + + it('shows the modal with its text', () => { + renderWithProvider( + console.log('Close')} + custodianName="Qredo" + custodianURL="https://qredo.com" + />, + store, + ); + + const tokenContainer = screen.getByText( + "To connect your accounts log into your Qredo account and click on the 'connect to MMI' button.", + ); + expect(tokenContainer).toBeInTheDocument(); + }); +}); diff --git a/ui/pages/institutional/confirm-connect-custodian-modal/index.js b/ui/pages/institutional/confirm-connect-custodian-modal/index.js new file mode 100644 index 000000000000..99ed12dcdad1 --- /dev/null +++ b/ui/pages/institutional/confirm-connect-custodian-modal/index.js @@ -0,0 +1 @@ +export { default } from './confirm-connect-custodian-modal'; diff --git a/ui/pages/institutional/custody/custody.js b/ui/pages/institutional/custody/custody.js index 1f017e90a9bd..5d078b03b5b5 100644 --- a/ui/pages/institutional/custody/custody.js +++ b/ui/pages/institutional/custody/custody.js @@ -39,6 +39,7 @@ import { } from '../../../helpers/constants/design-system'; import { CUSTODY_ACCOUNT_DONE_ROUTE, + CUSTODY_ACCOUNT_ROUTE, DEFAULT_ROUTE, } from '../../../helpers/constants/routes'; import { getCurrentChainId, getSelectedAddress } from '../../../selectors'; @@ -51,6 +52,8 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import PulseLoader from '../../../components/ui/pulse-loader/pulse-loader'; +import ConfirmConnectCustodianModal from '../confirm-connect-custodian-modal'; +import { findCustodianByDisplayName } from '../../../helpers/utils/institutional/find-by-custodian-name'; const CustodyPage = () => { const t = useI18nContext(); @@ -63,11 +66,16 @@ const CustodyPage = () => { const { custodians } = useSelector(getMMIConfiguration); const [loading, setLoading] = useState(true); + const [ + isConfirmConnectCustodianModalVisible, + setIsConfirmConnectCustodianModalVisible, + ] = useState(false); const [selectedAccounts, setSelectedAccounts] = useState({}); const [selectedCustodianName, setSelectedCustodianName] = useState(''); const [selectedCustodianImage, setSelectedCustodianImage] = useState(null); const [selectedCustodianDisplayName, setSelectedCustodianDisplayName] = useState(''); + const [matchedCustodian, setMatchedCustodian] = useState(null); const [selectedCustodianType, setSelectedCustodianType] = useState(''); const [connectError, setConnectError] = useState(''); const [currentJwt, setCurrentJwt] = useState(''); @@ -158,16 +166,31 @@ const CustodyPage = () => { data-testid="custody-connect-button" onClick={async () => { try { + const custodianByDisplayName = findCustodianByDisplayName( + custodian.displayName, + custodians, + ); const jwtListValue = await dispatch( mmiActions.getCustodianJWTList(custodian.name), ); setSelectedCustodianName(custodian.name); - setSelectedCustodianType(custodian.type); - setSelectedCustodianImage(custodian.iconUrl); setSelectedCustodianDisplayName(custodian.displayName); + setSelectedCustodianImage(custodian.iconUrl); setApiUrl(custodian.apiUrl); setCurrentJwt(jwtListValue[0] || ''); setJwtList(jwtListValue); + + // open confirm Connect Custodian modal except for gk8 + if ( + custodianByDisplayName.displayName.toLocaleLowerCase() === + 'gk8' + ) { + setSelectedCustodianType(custodian.type); + } else { + setMatchedCustodian(custodianByDisplayName); + setIsConfirmConnectCustodianModalVisible(true); + } + trackEvent({ category: MetaMetricsEventCategory.MMI, event: MetaMetricsEventName.CustodianSelected, @@ -326,6 +349,8 @@ const CustodyPage = () => { setCurrentJwt(''); setConnectError(''); setSelectError(''); + + history.push(CUSTODY_ACCOUNT_ROUTE); }; const setSelectAllAccounts = (e) => { @@ -598,7 +623,9 @@ const CustodyPage = () => { pathname: CUSTODY_ACCOUNT_DONE_ROUTE, state: { imgSrc: selectedCustodian.iconUrl, - title: t('custodianAccountAddedTitle'), + title: t('custodianAccountAddedTitle', [ + selectedCustodian.displayName, + ]), description: t('custodianAccountAddedDesc'), }, }); @@ -615,9 +642,7 @@ const CustodyPage = () => { setApiUrl(''); setAddNewTokenClicked(false); - if (Object.keys(connectRequest).length) { - history.push(DEFAULT_ROUTE); - } + history.push(DEFAULT_ROUTE); trackEvent({ category: MetaMetricsEventCategory.MMI, @@ -663,6 +688,14 @@ const CustodyPage = () => { )} + + {isConfirmConnectCustodianModalVisible && ( + setIsConfirmConnectCustodianModalVisible(false)} + custodianName={selectedCustodianDisplayName} + custodianURL={matchedCustodian?.website} + /> + )} ); }; diff --git a/ui/pages/institutional/custody/custody.test.js b/ui/pages/institutional/custody/custody.test.js index 22612a45637b..e0cdc16585f8 100644 --- a/ui/pages/institutional/custody/custody.test.js +++ b/ui/pages/institutional/custody/custody.test.js @@ -85,7 +85,7 @@ describe('CustodyPage', function () { }); }); - it('calls getCustodianJwtList on custody select when connect btn is click and clicks connect button and shows the jwt form', async () => { + it('calls getCustodianJwtList on custody select when connect btn is click and clicks connect button and shows the redirect to custodian modal', async () => { act(() => { renderWithProvider(, store); }); @@ -96,8 +96,9 @@ describe('CustodyPage', function () { }); await waitFor(() => { - expect(screen.getByTestId('jwt-form-connect-button')).toBeInTheDocument(); - expect(mockedGetCustodianJWTList).toHaveBeenCalled(); + expect( + screen.getByTestId('confirm-connect-custodian-modal'), + ).toBeInTheDocument(); }); }); }); diff --git a/ui/pages/institutional/institutional-entity-done-page/institutional-entity-done-page.js b/ui/pages/institutional/institutional-entity-done-page/institutional-entity-done-page.js index a7c134e91cd2..3b0e9e7f6c72 100644 --- a/ui/pages/institutional/institutional-entity-done-page/institutional-entity-done-page.js +++ b/ui/pages/institutional/institutional-entity-done-page/institutional-entity-done-page.js @@ -11,11 +11,11 @@ import { } from '../../../components/component-library'; import { TextColor, - TypographyVariant, Display, FlexDirection, AlignItems, - TextAlign, + TextVariant, + FontWeight, } from '../../../helpers/constants/design-system'; export default function InstitutionalEntityDonePage(props) { @@ -45,25 +45,20 @@ export default function InstitutionalEntityDonePage(props) { alt="Entity image" /> )} - - {state.title} - - - {state.description} - + + + {state.title} + + + + {state.description} + + diff --git a/yarn.lock b/yarn.lock index 701f81bc2f04..a68f41b9756e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3621,66 +3621,66 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/configuration-client@npm:^1.0.6": - version: 1.0.6 - resolution: "@metamask-institutional/configuration-client@npm:1.0.6" - checksum: 924ff201a99286aac332402dbc74884a72f5352f29ba066e788cd045b330df7775b2b247731db4ee453f2fee7f2662b61477703e70eb822f044895bdc858f79b +"@metamask-institutional/configuration-client@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask-institutional/configuration-client@npm:2.0.0" + checksum: 58856e9d2676110aede32d506d0c0602c4b9940bc810a1b7834ded52407f321d64bba08ce55de3e4be29dfc4e9d492ce1ee0fbab72b150c8e86feb8d99570699 languageName: node linkType: hard -"@metamask-institutional/custody-controller@npm:^0.2.10": - version: 0.2.10 - resolution: "@metamask-institutional/custody-controller@npm:0.2.10" +"@metamask-institutional/custody-controller@npm:^0.2.12": + version: 0.2.12 + resolution: "@metamask-institutional/custody-controller@npm:0.2.12" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-keyring": "npm:^0.0.27" + "@metamask-institutional/custody-keyring": "npm:^1.0.1" "@metamask-institutional/sdk": "npm:^0.1.18" "@metamask-institutional/types": "npm:^1.0.3" "@metamask/obs-store": "npm:^8.0.0" - checksum: 4ab4c26649f7e7e6e37308b00b2aa21cf40c0a861435a54c49bf0e092b49cbef71189774bd8ca038ace57d561facb8f79a0d98ad55ef0196101a857508e447d9 + checksum: 62d54cf12b5ff00d8038261de835d07f654077c39a66081fb88d4c79d37227ef6671dfef437e2af925cf72576abd523013a0fa652845960d937c6e58afff796b languageName: node linkType: hard -"@metamask-institutional/custody-keyring@npm:^0.0.27": - version: 0.0.27 - resolution: "@metamask-institutional/custody-keyring@npm:0.0.27" +"@metamask-institutional/custody-keyring@npm:^1.0.1": + version: 1.0.1 + resolution: "@metamask-institutional/custody-keyring@npm:1.0.1" dependencies: "@ethereumjs/tx": "npm:^4.1.1" "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/configuration-client": "npm:^1.0.6" + "@metamask-institutional/configuration-client": "npm:^2.0.0" "@metamask-institutional/sdk": "npm:^0.1.18" "@metamask-institutional/types": "npm:^1.0.3" "@metamask/obs-store": "npm:^8.0.0" crypto: "npm:^1.0.1" lodash.clonedeep: "npm:^4.5.0" - checksum: f006671892e9abc3f72105a5276193f9194e54c51dd732affb632d1cf5d4cbfdc2c0b323e65b8265f1dedfb81dabd9ab2ce1e2c177ea5e791bd9af238b187336 + checksum: c9c763bd8416bd4434f4ae00d687f3bd33c6f4c6b7c299f0887e772e077b82291f59ecb6eebcac18cce56775887dfa8bc0a1be121d38ba35b5d5650bbb85ddf9 languageName: node linkType: hard -"@metamask-institutional/extension@npm:^0.3.3": - version: 0.3.3 - resolution: "@metamask-institutional/extension@npm:0.3.3" +"@metamask-institutional/extension@npm:^0.3.5": + version: 0.3.5 + resolution: "@metamask-institutional/extension@npm:0.3.5" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-controller": "npm:^0.2.10" - "@metamask-institutional/custody-keyring": "npm:^0.0.27" + "@metamask-institutional/custody-controller": "npm:^0.2.12" + "@metamask-institutional/custody-keyring": "npm:^1.0.1" "@metamask-institutional/portfolio-dashboard": "npm:^1.4.0" "@metamask-institutional/sdk": "npm:^0.1.18" - "@metamask-institutional/transaction-update": "npm:^0.1.25" + "@metamask-institutional/transaction-update": "npm:^0.1.27" "@metamask-institutional/types": "npm:^1.0.3" jest-create-mock-instance: "npm:^2.0.0" jest-fetch-mock: "npm:3.0.3" - checksum: cb19b44b686dfa08bee0764c7ac8ff782f4ada8439e17d63b96700e043f07c406c727d1abe1c5d32ba97ecdb394748c8ed93b8d6ee9d5fafa9c3d66f13aa108d + checksum: a8b242d4837abc3812e01c38945428dcde0b29090e6c60d828c259f968378144c2e7e3f4bc3591f72232e0c037e6e3718627a65f45c3d446b96c45752e03eb4c languageName: node linkType: hard -"@metamask-institutional/institutional-features@npm:^1.2.2": - version: 1.2.2 - resolution: "@metamask-institutional/institutional-features@npm:1.2.2" +"@metamask-institutional/institutional-features@npm:^1.2.4": + version: 1.2.4 + resolution: "@metamask-institutional/institutional-features@npm:1.2.4" dependencies: - "@metamask-institutional/custody-keyring": "npm:^0.0.27" + "@metamask-institutional/custody-keyring": "npm:^1.0.1" "@metamask/obs-store": "npm:^8.0.0" - checksum: 2eb4cd7d36e38ba89a11e22c8522e1a091572e171abec71b6fa7b28b0f54c7fec0cbeb238d1066112dfbaf70075529f37f5aae55d65fa2639a73597a222b65a6 + checksum: 988dc946563c820951da37a1aba2905b3d4987332c72dc2131cce2a67fcbc6789fd3e73d9240c9c756cb73d1e28953bd8efd6b7c3828d3a90e6bd9ed72243cd7 languageName: node linkType: hard @@ -3719,17 +3719,17 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/transaction-update@npm:^0.1.25": - version: 0.1.25 - resolution: "@metamask-institutional/transaction-update@npm:0.1.25" +"@metamask-institutional/transaction-update@npm:^0.1.27": + version: 0.1.27 + resolution: "@metamask-institutional/transaction-update@npm:0.1.27" dependencies: - "@metamask-institutional/custody-keyring": "npm:^0.0.27" + "@metamask-institutional/custody-keyring": "npm:^1.0.1" "@metamask-institutional/sdk": "npm:^0.1.18" "@metamask-institutional/types": "npm:^1.0.3" - "@metamask-institutional/websocket-client": "npm:^0.1.27" + "@metamask-institutional/websocket-client": "npm:^0.1.29" "@metamask/obs-store": "npm:^8.0.0" ethereumjs-util: "npm:^7.1.5" - checksum: ee1c597a3e3ec3d226eb1a0cdda6ed3e098faab776eaeb30c5abbcf72efa4eceadbceefed51f6fc7fa92b45f02c0f0da732e2d600c13832d93f8ba449e4a31b5 + checksum: 1ff8db117aebf1c0131de0d89ca14d9a74ef8757187ad30e6ee7fd25859a2ce22a9a01f49efc33360bd9231ccce22ab89cc1bcea3d6a0ea915b2ee9d76cdc618 languageName: node linkType: hard @@ -3740,15 +3740,15 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/websocket-client@npm:^0.1.27": - version: 0.1.27 - resolution: "@metamask-institutional/websocket-client@npm:0.1.27" +"@metamask-institutional/websocket-client@npm:^0.1.29": + version: 0.1.29 + resolution: "@metamask-institutional/websocket-client@npm:0.1.29" dependencies: - "@metamask-institutional/custody-keyring": "npm:^0.0.27" + "@metamask-institutional/custody-keyring": "npm:^1.0.1" "@metamask-institutional/sdk": "npm:^0.1.18" "@metamask-institutional/types": "npm:^1.0.3" mock-socket: "npm:^9.2.1" - checksum: e35e36f0ceae33596848539f8a6dff7bb5867124b7bceece61abc21e603ef81db4d0d3f182edcda8e62552d4870e1f3f527bc489e6412a5ae8d88909e5c64af5 + checksum: 137d8d9c9a0672744022a92b04ed9b65d10cc34bba2bbcdb5b8ac7e7912ca25d6f8fc4d671ff973ee6dafbe58b6cbbad4aad3f35146363b03acdfa5d91d6b961 languageName: node linkType: hard @@ -24176,14 +24176,14 @@ __metadata: "@lavamoat/lavapack": "npm:^5.2.0" "@lavamoat/snow": "npm:^1.5.0" "@material-ui/core": "npm:^4.11.0" - "@metamask-institutional/custody-controller": "npm:^0.2.10" - "@metamask-institutional/custody-keyring": "npm:^0.0.27" - "@metamask-institutional/extension": "npm:^0.3.3" - "@metamask-institutional/institutional-features": "npm:^1.2.2" + "@metamask-institutional/custody-controller": "npm:^0.2.12" + "@metamask-institutional/custody-keyring": "npm:^1.0.1" + "@metamask-institutional/extension": "npm:^0.3.5" + "@metamask-institutional/institutional-features": "npm:^1.2.4" "@metamask-institutional/portfolio-dashboard": "npm:^1.4.0" "@metamask-institutional/rpc-allowlist": "npm:^1.0.0" "@metamask-institutional/sdk": "npm:^0.1.18" - "@metamask-institutional/transaction-update": "npm:^0.1.25" + "@metamask-institutional/transaction-update": "npm:^0.1.27" "@metamask/address-book-controller": "npm:^3.0.0" "@metamask/announcement-controller": "npm:^4.0.0" "@metamask/approval-controller": "npm:^3.4.0" From 8466d8896e4d54798e51911d70e595103b0d4308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Thu, 21 Sep 2023 14:21:51 +0100 Subject: [PATCH 003/219] [MMI] Updates custody-keyring package (#20982) * updates custody-keyring package * dedupe --- package.json | 2 +- ui/pages/institutional/custody/custody.js | 5 +++-- yarn.lock | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 5274641bc3f5..fd67921ef067 100644 --- a/package.json +++ b/package.json @@ -225,7 +225,7 @@ "@lavamoat/snow": "^1.5.0", "@material-ui/core": "^4.11.0", "@metamask-institutional/custody-controller": "^0.2.12", - "@metamask-institutional/custody-keyring": "^1.0.1", + "@metamask-institutional/custody-keyring": "^1.0.2", "@metamask-institutional/extension": "^0.3.5", "@metamask-institutional/institutional-features": "^1.2.4", "@metamask-institutional/portfolio-dashboard": "^1.4.0", diff --git a/ui/pages/institutional/custody/custody.js b/ui/pages/institutional/custody/custody.js index 5d078b03b5b5..70c60f1e214b 100644 --- a/ui/pages/institutional/custody/custody.js +++ b/ui/pages/institutional/custody/custody.js @@ -622,9 +622,10 @@ const CustodyPage = () => { history.push({ pathname: CUSTODY_ACCOUNT_DONE_ROUTE, state: { - imgSrc: selectedCustodian.iconUrl, + imgSrc: selectedCustodian && selectedCustodian.iconUrl, title: t('custodianAccountAddedTitle', [ - selectedCustodian.displayName, + (selectedCustodian && selectedCustodian.displayName) || + 'Custodian', ]), description: t('custodianAccountAddedDesc'), }, diff --git a/yarn.lock b/yarn.lock index a68f41b9756e..5b2f79c37f62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3641,9 +3641,9 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/custody-keyring@npm:^1.0.1": - version: 1.0.1 - resolution: "@metamask-institutional/custody-keyring@npm:1.0.1" +"@metamask-institutional/custody-keyring@npm:^1.0.1, @metamask-institutional/custody-keyring@npm:^1.0.2": + version: 1.0.2 + resolution: "@metamask-institutional/custody-keyring@npm:1.0.2" dependencies: "@ethereumjs/tx": "npm:^4.1.1" "@ethereumjs/util": "npm:^8.0.5" @@ -3653,7 +3653,7 @@ __metadata: "@metamask/obs-store": "npm:^8.0.0" crypto: "npm:^1.0.1" lodash.clonedeep: "npm:^4.5.0" - checksum: c9c763bd8416bd4434f4ae00d687f3bd33c6f4c6b7c299f0887e772e077b82291f59ecb6eebcac18cce56775887dfa8bc0a1be121d38ba35b5d5650bbb85ddf9 + checksum: eff487de17a9d7ea889a953f9874ee0a55b568928c97bf93a90c33af52acfc8958d708d8fb4e4cd909ec0e57757409ac86476b6ab89b72b72b4f6559eab3b06b languageName: node linkType: hard @@ -24177,7 +24177,7 @@ __metadata: "@lavamoat/snow": "npm:^1.5.0" "@material-ui/core": "npm:^4.11.0" "@metamask-institutional/custody-controller": "npm:^0.2.12" - "@metamask-institutional/custody-keyring": "npm:^1.0.1" + "@metamask-institutional/custody-keyring": "npm:^1.0.2" "@metamask-institutional/extension": "npm:^0.3.5" "@metamask-institutional/institutional-features": "npm:^1.2.4" "@metamask-institutional/portfolio-dashboard": "npm:^1.4.0" From 3140afbfad246268b2f12f4a67fdd2b686e7435f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Fri, 22 Sep 2023 09:48:02 +0100 Subject: [PATCH 004/219] [MMI] clean up MMI Portfolio Dashboard URLs (#20990) * clean up urls * test update * lint --- shared/constants/swaps.ts | 2 -- ui/components/app/wallet-overview/eth-overview.js | 9 +++++---- ui/components/app/wallet-overview/eth-overview.test.js | 10 +++++++--- ui/components/app/wallet-overview/token-overview.js | 9 +++++---- .../select-action-modal/select-action-modal.js | 10 ++++++---- ui/helpers/constants/common.ts | 1 - 6 files changed, 23 insertions(+), 18 deletions(-) diff --git a/shared/constants/swaps.ts b/shared/constants/swaps.ts index 73e2926e7143..1b60dc8f95fc 100644 --- a/shared/constants/swaps.ts +++ b/shared/constants/swaps.ts @@ -142,8 +142,6 @@ export const WETH_ZKSYNC_ERA_CONTRACT_ADDRESS = const SWAPS_TESTNET_CHAIN_ID = '0x539'; -export const MMI_SWAPS_URL = 'https://metamask-institutional.io/swap'; - export const SWAPS_API_V2_BASE_URL = 'https://swap.metaswap.codefi.network'; export const SWAPS_DEV_API_V2_BASE_URL = 'https://swap.dev-api.cx.metamask.io'; export const GAS_API_BASE_URL = 'https://gas-api.metaswap.codefi.network'; diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index 4c10a8edded0..15695dd237b5 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -14,7 +14,6 @@ import { getMmiPortfolioEnabled, getMmiPortfolioUrl, } from '../../../selectors/institutional/selectors'; -import { MMI_SWAPS_URL } from '../../../../shared/constants/swaps'; ///: END:ONLY_INCLUDE_IN import { I18nContext } from '../../../contexts/i18n'; import { @@ -112,7 +111,7 @@ const EthOverview = ({ className, showAddress }) => { onClick={() => { stakingEvent(); global.platform.openTab({ - url: 'https://metamask-institutional.io/stake', + url: `${mmiPortfolioUrl}/stake`, }); }} /> @@ -125,7 +124,9 @@ const EthOverview = ({ className, showAddress }) => { label={t('portfolio')} onClick={() => { portfolioEvent(); - window.open(mmiPortfolioUrl, '_blank'); + global.platform.openTab({ + url: mmiPortfolioUrl, + }); }} /> )} @@ -262,7 +263,7 @@ const EthOverview = ({ className, showAddress }) => { onClick={() => { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) global.platform.openTab({ - url: MMI_SWAPS_URL, + url: `${mmiPortfolioUrl}/swap`, }); ///: END:ONLY_INCLUDE_IN diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index 32e73fbee9a9..bad0339b6ed7 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -196,6 +196,12 @@ describe('EthOverview', () => { const mockedStoreWithCustodyKeyring = { metamask: { ...mockStore.metamask, + mmiConfiguration: { + portfolio: { + enabled: true, + url: 'https://metamask-institutional.io', + }, + }, keyrings: [ { type: 'Custody', @@ -224,9 +230,7 @@ describe('EthOverview', () => { await waitFor(() => expect(openTabSpy).toHaveBeenCalledWith({ - url: expect.stringContaining( - 'https://metamask-institutional.io/swap', - ), + url: 'https://metamask-institutional.io/swap', }), ); }); diff --git a/ui/components/app/wallet-overview/token-overview.js b/ui/components/app/wallet-overview/token-overview.js index cb7fcc4fd9a2..650404c86c5f 100644 --- a/ui/components/app/wallet-overview/token-overview.js +++ b/ui/components/app/wallet-overview/token-overview.js @@ -25,7 +25,6 @@ import { getMmiPortfolioEnabled, getMmiPortfolioUrl, } from '../../../selectors/institutional/selectors'; -import { MMI_SWAPS_URL } from '../../../../shared/constants/swaps'; ///: END:ONLY_INCLUDE_IN import { getIsSwapsChain, @@ -178,7 +177,7 @@ const TokenOverview = ({ className, token }) => { onClick={() => { stakingEvent(); global.platform.openTab({ - url: 'https://metamask-institutional.io/stake', + url: `${mmiPortfolioUrl}/stake`, }); }} /> @@ -195,7 +194,9 @@ const TokenOverview = ({ className, token }) => { data-testid="token-overview-mmi-portfolio" onClick={() => { portfolioEvent(); - window.open(mmiPortfolioUrl, '_blank'); + global.platform.openTab({ + url: mmiPortfolioUrl, + }); }} /> )} @@ -252,7 +253,7 @@ const TokenOverview = ({ className, token }) => { onClick={() => { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) global.platform.openTab({ - url: MMI_SWAPS_URL, + url: `${mmiPortfolioUrl}/swap`, }); ///: END:ONLY_INCLUDE_IN diff --git a/ui/components/multichain/select-action-modal/select-action-modal.js b/ui/components/multichain/select-action-modal/select-action-modal.js index 404549292110..3a5ca27c5fc9 100644 --- a/ui/components/multichain/select-action-modal/select-action-modal.js +++ b/ui/components/multichain/select-action-modal/select-action-modal.js @@ -49,9 +49,9 @@ import { startNewDraftTransaction } from '../../../ducks/send'; import { I18nContext } from '../../../contexts/i18n'; import { AssetType } from '../../../../shared/constants/transaction'; ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) -import { MMI_SWAPS_URL } from '../../../../shared/constants/swaps'; -import { MMI_STAKE_WEBSITE } from '../../../helpers/constants/common'; +import { getMmiPortfolioUrl } from '../../../selectors/institutional/selectors'; ///: END:ONLY_INCLUDE_IN + ///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask) import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import { isHardwareKeyring } from '../../../helpers/utils/hardware'; @@ -79,6 +79,8 @@ export const SelectActionModal = ({ onClose }) => { ///: END:ONLY_INCLUDE_IN ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + const mmiPortfolioUrl = useSelector(getMmiPortfolioUrl); + const stakingEvent = () => { trackEvent({ category: MetaMetricsEventCategory.Navigation, @@ -136,7 +138,7 @@ export const SelectActionModal = ({ onClose }) => { onClick={() => { stakingEvent(); global.platform.openTab({ - url: MMI_STAKE_WEBSITE, + url: `${mmiPortfolioUrl}/stake`, }); onClose(); }} @@ -152,7 +154,7 @@ export const SelectActionModal = ({ onClose }) => { onClick={() => { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) global.platform.openTab({ - url: MMI_SWAPS_URL, + url: `${mmiPortfolioUrl}/swap`, }); ///: END:ONLY_INCLUDE_IN diff --git a/ui/helpers/constants/common.ts b/ui/helpers/constants/common.ts index f59f451660e7..eb1b4946750f 100644 --- a/ui/helpers/constants/common.ts +++ b/ui/helpers/constants/common.ts @@ -7,7 +7,6 @@ const _contractAddressLink = ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) const _mmiWebSite = 'https://metamask.io/institutions/'; export const MMI_WEB_SITE = _mmiWebSite; -export const MMI_STAKE_WEBSITE = 'https://metamask-institutional.io/stake'; ///: END:ONLY_INCLUDE_IN // eslint-disable-next-line prefer-destructuring From e316f95092c1febaa86b8ac836c42fd8bbee9f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Oliv=C3=A9?= Date: Thu, 21 Sep 2023 10:39:14 +0200 Subject: [PATCH 005/219] Add Custody component unit tests (#20907) * Initial work * updated tests * Update test * Finished adding tests * prettier * Fixed tests * Changed custodian name to envName and fixed tests * Updated tests * Updated tests --- app/scripts/controllers/mmi-controller.js | 2 +- ui/components/ui/pulse-loader/pulse-loader.js | 2 +- .../institutional/find-by-custodian-name.ts | 2 +- .../confirm-transaction-base.test.js | 2 +- .../confirm-connect-custodian-modal.js | 6 +- .../__snapshots__/custody.test.js.snap | 145 +++--- ui/pages/institutional/custody/custody.js | 176 ++++---- .../institutional/custody/custody.test.js | 411 ++++++++++++++++-- ...active-replacement-token-page.test.js.snap | 1 + ui/selectors/institutional/selectors.js | 4 +- ui/selectors/institutional/selectors.test.js | 14 +- 11 files changed, 552 insertions(+), 213 deletions(-) diff --git a/app/scripts/controllers/mmi-controller.js b/app/scripts/controllers/mmi-controller.js index 355d4ad27352..bdf84c5f8877 100644 --- a/app/scripts/controllers/mmi-controller.js +++ b/app/scripts/controllers/mmi-controller.js @@ -345,7 +345,7 @@ export default class MMIController extends EventEmitter { // FIXME: status maps are not a thing anymore this.custodyController.storeCustodyStatusMap( - custodian.name, + custodian.envName, keyring.getStatusMap(), ); diff --git a/ui/components/ui/pulse-loader/pulse-loader.js b/ui/components/ui/pulse-loader/pulse-loader.js index d4d542d71c16..227a07e49475 100644 --- a/ui/components/ui/pulse-loader/pulse-loader.js +++ b/ui/components/ui/pulse-loader/pulse-loader.js @@ -2,7 +2,7 @@ import React from 'react'; export default function PulseLoader() { return ( -
+
diff --git a/ui/helpers/utils/institutional/find-by-custodian-name.ts b/ui/helpers/utils/institutional/find-by-custodian-name.ts index 52a05b3b7b27..bdd58d0cbcd2 100644 --- a/ui/helpers/utils/institutional/find-by-custodian-name.ts +++ b/ui/helpers/utils/institutional/find-by-custodian-name.ts @@ -24,7 +24,7 @@ export function findCustodianByDisplayName( } for (const custodian of custodians) { - const custodianName = custodian.name.toLowerCase(); + const custodianName = custodian.envName.toLowerCase(); if (formatedDisplayName.includes(custodianName)) { return custodian; diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js index 678bbf4540f3..882d32dc4dd9 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.test.js @@ -228,7 +228,7 @@ describe('Confirm Transaction Base', () => { mockedStore.metamask.mmiConfiguration = { custodians: [ { - name: 'saturn-dev', + envName: 'saturn-dev', displayName: 'Saturn Custody', isNoteToTraderSupported: true, }, diff --git a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.js b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.js index b3269b2ad607..d08e07d7fe1a 100644 --- a/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.js +++ b/ui/pages/institutional/confirm-connect-custodian-modal/confirm-connect-custodian-modal.js @@ -10,7 +10,7 @@ import { import { I18nContext } from '../../../contexts/i18n'; import { Button, - BUTTON_VARIANT, + ButtonVariant, Box, Text, Modal, @@ -78,7 +78,7 @@ const ConfirmConnectCustodianModal = ({
-
-`; - -exports[`CustodyPage renders CustodyPage 2`] = ` -
- -
-
- -

- Back -

-
-

- Custodial Accounts -

-
+

- Please choose the custodian you want to connect in order to add or refresh a token. -

-
+
+ +
+
+
+

-

    -
    -
    - Saturn Custody -

    - Saturn Custody -

    -
    - -
    -
-
+ Saturn Custody C +

- +
-
+ `; - -exports[`CustodyPage renders CustodyPage 3`] = `
`; diff --git a/ui/pages/institutional/custody/custody.js b/ui/pages/institutional/custody/custody.js index 70c60f1e214b..094d0a5f6016 100644 --- a/ui/pages/institutional/custody/custody.js +++ b/ui/pages/institutional/custody/custody.js @@ -7,7 +7,6 @@ import React, { } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { v4 as uuidv4 } from 'uuid'; import { isEqual } from 'lodash'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { mmiActionsFactory } from '../../../store/institutional/institution-background'; @@ -18,8 +17,8 @@ import { Label, IconName, IconSize, - BUTTON_SIZES, - BUTTON_VARIANT, + ButtonSize, + ButtonVariant, Box, Text, } from '../../../components/component-library'; @@ -55,6 +54,8 @@ import PulseLoader from '../../../components/ui/pulse-loader/pulse-loader'; import ConfirmConnectCustodianModal from '../confirm-connect-custodian-modal'; import { findCustodianByDisplayName } from '../../../helpers/utils/institutional/find-by-custodian-name'; +const GK8_DISPLAY_NAME = 'gk8'; + const CustodyPage = () => { const t = useI18nContext(); const history = useHistory(); @@ -88,26 +89,18 @@ const CustodyPage = () => { const [accounts, setAccounts] = useState(); const address = useSelector(getSelectedAddress); const connectRequest = connectRequests ? connectRequests[0] : undefined; + const isCheckBoxSelected = + accounts && Object.keys(selectedAccounts).length === accounts.length; const custodianButtons = useMemo(() => { const custodianItems = []; - const sortedCustodians = [...custodians].sort(function (a, b) { - const nameA = a.name.toLowerCase(); - const nameB = b.name.toLowerCase(); - - if (nameA < nameB) { - return -1; - } - if (nameA > nameB) { - return 1; - } - return 0; - }); + const sortedCustodians = [...custodians].sort((a, b) => + a.envName.toLowerCase().localeCompare(b.envName.toLowerCase()), + ); function shouldShowInProduction(custodian) { return ( - custodian && 'production' in custodian && !custodian.production && process.env.METAMASK_ENVIRONMENT === 'production' @@ -115,19 +108,59 @@ const CustodyPage = () => { } function isHidden(custodian) { - return custodian && 'hidden' in custodian && custodian.hidden; + return 'hidden' in custodian && custodian.hidden; } function isNotSelectedCustodian(custodian) { return ( - custodian && - 'name' in custodian && + 'envName' in custodian && connectRequest && Object.keys(connectRequest).length && - custodian.name !== selectedCustodianName + custodian.envName !== selectedCustodianName ); } + async function handleButtonClick(custodian) { + try { + const custodianByDisplayName = findCustodianByDisplayName( + custodian.displayName, + custodians, + ); + + const jwtListValue = await dispatch( + mmiActions.getCustodianJWTList(custodian.envName), + ); + + setSelectedCustodianName(custodian.envName); + setSelectedCustodianDisplayName(custodian.displayName); + setSelectedCustodianImage(custodian.iconUrl); + setApiUrl(custodian.apiUrl); + setCurrentJwt(jwtListValue[0] || ''); + setJwtList(jwtListValue); + + // open confirm Connect Custodian modal except for gk8 + if ( + custodianByDisplayName?.displayName?.toLocaleLowerCase() === + GK8_DISPLAY_NAME + ) { + setSelectedCustodianType(custodian.type); + } else { + setMatchedCustodian(custodianByDisplayName); + setIsConfirmConnectCustodianModalVisible(true); + } + + trackEvent({ + category: MetaMetricsEventCategory.MMI, + event: MetaMetricsEventName.CustodianSelected, + properties: { + custodian: custodian.envName, + }, + }); + } catch (error) { + console.error('Error:', error); + } + } + sortedCustodians.forEach((custodian) => { if ( shouldShowInProduction(custodian) || @@ -139,7 +172,7 @@ const CustodyPage = () => { custodianItems.push( { @@ -222,25 +218,27 @@ const CustodyPage = () => { const handleConnectError = useCallback( (e) => { - let errorMessage; - const detailedError = e.message.split(':'); - - if (detailedError.length > 1 && !isNaN(parseInt(detailedError[0], 10))) { - if (parseInt(detailedError[0], 10) === 401) { - // Authentication Error - errorMessage = - 'Authentication error. Please ensure you have entered the correct token'; + const getErrorMessage = (error) => { + const detailedError = error.message.split(':'); + const errorCode = parseInt(detailedError[0], 10); + + if (detailedError.length > 1 && !isNaN(errorCode)) { + switch (errorCode) { + case 401: + return 'Authentication error. Please ensure you have entered the correct token'; + default: + return null; + } } - } - if (/Network Error/u.test(e.message)) { - errorMessage = - 'Network error. Please ensure you have entered the correct API URL'; - } + if (/Network Error/u.test(error.message)) { + return 'Network error. Please ensure you have entered the correct API URL'; + } - if (!errorMessage) { - errorMessage = e.message; - } + return error.message; + }; + + const errorMessage = getErrorMessage(e); setConnectError( `Something went wrong connecting your custodian account. Error details: ${errorMessage}`, @@ -382,7 +380,12 @@ const CustodyPage = () => { return ( {connectError && ( - + {connectError} )} @@ -391,8 +394,10 @@ const CustodyPage = () => { {selectError} )} + {!accounts && !selectedCustodianType && ( { marginTop={4} > { ) : (
+
+
+
+
+ + + + + +
+
+
+
+

+ Custody test +

+

+ 0xca8f...f281 +

+
+
{ it('returns accounts with balance, address, and name from identity and accounts in state', () => { const accountsWithSendEther = selectors.accountsWithSendEtherInfoSelector(mockState); - expect(accountsWithSendEther).toHaveLength(4); + expect(accountsWithSendEther).toHaveLength(5); expect(accountsWithSendEther[0].balance).toStrictEqual( '0x346ba7725f412cbfdb', ); From fcefc0f5c5adf8aa17a687721634fb552013a39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Oliv=C3=A9?= Date: Mon, 2 Oct 2023 17:10:45 +0200 Subject: [PATCH 013/219] [MMI] Remove Whats-new-popup mmi code fences (#21146) ## Explanation Remove Whats-new-popup mmi code fences. Currently, those code fences are doing nothing, and we don't have any plans yet to add custom mmi popups. ## Pre-merge author checklist - [x] I've clearly explained: - [x] What problem this PR is solving - [x] How this problem was solved - [x] How reviewers can test my changes - [x] Sufficient automated test coverage has been added ## Pre-merge reviewer checklist - [x] Manual testing (e.g. pull and build branch, run in browser, test code being changed) - [x] **IF** this PR fixes a bug in the release milestone, add this PR to the release milestone If further QA is required (e.g. new feature, complex testing steps, large refactor), add the `Extension QA Board` label. In this case, a QA Engineer approval will be be required. --- .../app/whats-new-popup/whats-new-popup.js | 113 ++---------------- ui/pages/home/home.component.js | 12 +- 2 files changed, 11 insertions(+), 114 deletions(-) diff --git a/ui/components/app/whats-new-popup/whats-new-popup.js b/ui/components/app/whats-new-popup/whats-new-popup.js index 8c868e401588..3b1b18dd1b28 100644 --- a/ui/components/app/whats-new-popup/whats-new-popup.js +++ b/ui/components/app/whats-new-popup/whats-new-popup.js @@ -8,13 +8,7 @@ import { getCurrentLocale } from '../../../ducks/locale/locale'; import { I18nContext } from '../../../contexts/i18n'; import { useEqualityCheck } from '../../../hooks/useEqualityCheck'; import Popover from '../../ui/popover'; -import { - Text, - ButtonPrimary, - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - IconName, - ///: END:ONLY_INCLUDE_IN -} from '../../component-library'; +import { Text, ButtonPrimary } from '../../component-library'; import { updateViewedNotifications } from '../../../store/actions'; import { NOTIFICATION_BUY_SELL_BUTTON, @@ -30,12 +24,7 @@ import { EXPERIMENTAL_ROUTE, SECURITY_ROUTE, } from '../../../helpers/constants/routes'; -import { - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - Size, - ///: END:ONLY_INCLUDE_IN - TextVariant, -} from '../../../helpers/constants/design-system'; +import { TextVariant } from '../../../helpers/constants/design-system'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { @@ -167,30 +156,9 @@ const renderFirstNotification = ({ history, isLast, trackEvent, - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - mmiPortfolioUrl, - seenNotifications, - onClose, - ///: END:ONLY_INCLUDE_IN }) => { - const { - id, - date, - title, - description, - image, - actionText, - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - customButton, - hideDate, - ///: END:ONLY_INCLUDE_IN - } = notification; + const { id, date, title, description, image, actionText } = notification; const actionFunction = getActionFunctionById(id, history); - let showNotificationDate = true; - - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - showNotificationDate = !hideDate; - ///: END:ONLY_INCLUDE_IN const imageComponent = image && ( ); const placeImageBelowDescription = image?.placeImageBelowDescription; + return (
{renderDescription(description)}
- {showNotificationDate && ( -
{date}
- )} + +
{date}
{placeImageBelowDescription && imageComponent} {actionText && ( @@ -239,26 +207,6 @@ const renderFirstNotification = ({ {actionText} )} - { - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - customButton && customButton.name === 'mmi-portfolio' && ( - { - updateViewedNotifications(seenNotifications); - onClose(); - window.open(mmiPortfolioUrl, '_blank'); - }} - block - > - {customButton.text} - - ) - ///: END:ONLY_INCLUDE_IN - }
{ const observer = new window.IntersectionObserver( (entries, _observer) => { @@ -381,26 +325,10 @@ export default function WhatsNewPopup({ observer.observe(ref.current); }); - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - trackEvent({ - category: MetaMetricsEventCategory.MMI, - event: MetaMetricsEventName.MMIPortfolioDashboardModalOpen, - properties: { - action: 'Modal was opened', - }, - }); - ///: END:ONLY_INCLUDE_IN - return () => { observer.disconnect(); }; - }, [ - idRefMap, - setSeenNotifications, - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - trackEvent, - ///: END:ONLY_INCLUDE_IN - ]); + }, [idRefMap, setSeenNotifications]); // Display the swaps notification with full image // Displays the NFTs & OpenSea notifications 18,19 with full image @@ -436,15 +364,6 @@ export default function WhatsNewPopup({ completed_all: true, }, }); - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - trackEvent({ - category: MetaMetricsEventCategory.MMI, - event: MetaMetricsEventName.MMIPortfolioDashboardModalButton, - properties: { - action: 'Button was clicked', - }, - }); - ///: END:ONLY_INCLUDE_IN onClose(); }} popoverRef={popoverRef} @@ -457,24 +376,15 @@ export default function WhatsNewPopup({ const notification = getTranslatedUINotifications(t, locale)[id]; const isLast = index === notifications.length - 1; // Choose the appropriate rendering function based on the id - let renderNotification = + const renderNotification = notificationRenderers[id] || renderSubsequentNotification; - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - renderNotification = renderFirstNotification; - ///: END:ONLY_INCLUDE_IN - return renderNotification({ notification, idRefMap, history, isLast, trackEvent, - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - mmiPortfolioUrl, - seenNotifications, - onClose, - ///: END:ONLY_INCLUDE_IN }); })}
@@ -484,7 +394,4 @@ export default function WhatsNewPopup({ WhatsNewPopup.propTypes = { onClose: PropTypes.func.isRequired, - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - mmiPortfolioUrl: PropTypes.string.isRequired, - ///: END:ONLY_INCLUDE_IN }; diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index efa0acaeca70..85708fa9eceb 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -193,7 +193,6 @@ export default class Home extends PureComponent { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) institutionalConnectRequests: PropTypes.arrayOf(PropTypes.object), mmiPortfolioEnabled: PropTypes.bool, - mmiPortfolioUrl: PropTypes.string, modalOpen: PropTypes.bool, setWaitForConfirmDeepLinkDialog: PropTypes.func, waitForConfirmDeepLinkDialog: PropTypes.bool, @@ -772,7 +771,6 @@ export default class Home extends PureComponent { newNetworkAddedConfigurationId, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) mmiPortfolioEnabled, - mmiPortfolioUrl, ///: END:ONLY_INCLUDE_IN } = this.props; @@ -822,14 +820,7 @@ export default class Home extends PureComponent { exact />
- {showWhatsNew ? ( - - ) : null} + {showWhatsNew ? : null} { ///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask) } @@ -861,7 +852,6 @@ export default class Home extends PureComponent { ///: END:ONLY_INCLUDE_IN } From 87d410a29b168746f363fe430615bc140dbb0bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Oliv=C3=A9?= Date: Mon, 2 Oct 2023 17:21:42 +0200 Subject: [PATCH 014/219] =?UTF-8?q?[MMI]=C2=A0Add=20Confirm-approve-conten?= =?UTF-8?q?t.component=20unit=20test=20(#21148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Explanation Add Confirm-approve-content.component unit test ## Pre-merge author checklist - [x] I've clearly explained: - [x] What problem this PR is solving - [x] How this problem was solved - [x] How reviewers can test my changes - [x] Sufficient automated test coverage has been added ## Pre-merge reviewer checklist - [x] Manual testing (e.g. pull and build branch, run in browser, test code being changed) - [x] **IF** this PR fixes a bug in the release milestone, add this PR to the release milestone If further QA is required (e.g. new feature, complex testing steps, large refactor), add the `Extension QA Board` label. In this case, a QA Engineer approval will be be required. --- .../confirm-approve-content.component.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js index 20d27f86e00a..5f66cfd5d87c 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js +++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js @@ -361,4 +361,19 @@ describe('ConfirmApproveContent Component', () => { expect(getByText('This is a deceptive request')).toBeInTheDocument(); }); + + it('should render token contract address when isSetApproveForAll and isApprovalOrRejection are true', () => { + const { getByText } = renderComponent({ + ...props, + isSetApproveForAll: true, + isApprovalOrRejection: true, + tokenAddress: '0x', + }); + + const showViewTxDetails = getByText('View full transaction details'); + + fireEvent.click(showViewTxDetails); + + expect(getByText(/Token contract address: 0x/u)).toBeInTheDocument(); + }); }); From 4468168332c26041622e04f21d08bb0d8e7a60fc Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 2 Oct 2023 10:42:43 -0500 Subject: [PATCH 015/219] UX: Multichain: Add Account or Hardware Wallet button (#21081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Replaces the "Add / Import / Hardware" links with a single button that takes the user to another quick modal to add, import, or connect a hardware wallet. ## **Manual testing steps** _1. Open the account menu _2. Click the "Add account or hardware wallet" button _3. Click each option, ensure the correct modal displays _4. Click "back" on each screen, ensure it goes back logically in the nested cycle ## **Screenshots/Recordings** https://github.com/MetaMask/metamask-extension/assets/46655/7967d0ce-870c-4ccb-91bc-3f2e9cf55e79 ## **Related issues** _Fixes https://github.com/MetaMask/MetaMask-planning/issues/1374 ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've clearly explained: - [ ] What problem this PR is solving. - [ ] How this problem was solved. - [ ] How reviewers can test my changes. - [ ] I’ve indicated what issue this PR is linked to: Fixes #??? - [ ] I’ve included tests if applicable. - [ ] I’ve documented any added code. - [ ] 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)). - [ ] I’ve properly set the pull request status: - [ ] In case it's not yet "ready for review", I've set it to "draft". - [ ] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 6 + test/e2e/tests/account-details.spec.js | 6 + test/e2e/tests/add-account.spec.js | 14 +- test/e2e/tests/import-flow.spec.js | 20 +- test/e2e/user-actions-benchmark.js | 3 + .../account-list-menu/account-list-menu.js | 306 ++++++++++-------- .../account-list-menu.test.js | 115 +++++-- 7 files changed, 306 insertions(+), 164 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 8d3c090ca1c5..dd67a4b6d1ed 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -246,6 +246,9 @@ "addIPFSGateway": { "message": "Add your preferred IPFS gateway" }, + "addImportAccount": { + "message": "Add account or hardware wallet" + }, "addMemo": { "message": "Add memo" }, @@ -259,6 +262,9 @@ "message": "This network connection relies on third parties. This connection may be less reliable or enable third-parties to track activity. $1", "description": "$1 is Learn more link" }, + "addNewAccount": { + "message": "Add a new account" + }, "addNewToken": { "message": "Add new token" }, diff --git a/test/e2e/tests/account-details.spec.js b/test/e2e/tests/account-details.spec.js index eeee82694f54..b6601fb51b5c 100644 --- a/test/e2e/tests/account-details.spec.js +++ b/test/e2e/tests/account-details.spec.js @@ -105,6 +105,9 @@ describe('Show account details', function () { // Create and focus on different account await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement( '[data-testid="multichain-account-menu-popover-add-account"]', ); @@ -153,6 +156,9 @@ describe('Show account details', function () { // Create and focus on different account await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement( '[data-testid="multichain-account-menu-popover-add-account"]', ); diff --git a/test/e2e/tests/add-account.spec.js b/test/e2e/tests/add-account.spec.js index b44fbad027d6..5b2551b86294 100644 --- a/test/e2e/tests/add-account.spec.js +++ b/test/e2e/tests/add-account.spec.js @@ -41,6 +41,9 @@ describe('Add account', function () { await unlockWallet(driver); await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement( '[data-testid="multichain-account-menu-popover-add-account"]', ); @@ -82,6 +85,9 @@ describe('Add account', function () { // Create 2nd account await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement( '[data-testid="multichain-account-menu-popover-add-account"]', ); @@ -176,7 +182,9 @@ describe('Add account', function () { await unlockWallet(driver); await driver.clickElement('[data-testid="account-menu-icon"]'); - + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement( '[data-testid="multichain-account-menu-popover-add-account"]', ); @@ -191,7 +199,6 @@ describe('Add account', function () { }); await driver.clickElement('[data-testid="account-menu-icon"]'); - const menuItems = await driver.findElements( '.multichain-account-list-item', ); @@ -207,6 +214,9 @@ describe('Add account', function () { // Create 3rd account with private key await driver.clickElement('.mm-text-field'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement({ text: 'Import account', tag: 'button' }); await driver.fill('#private-key-box', testPrivateKey); diff --git a/test/e2e/tests/import-flow.spec.js b/test/e2e/tests/import-flow.spec.js index 23f51ec87f50..e92c8ffc2542 100644 --- a/test/e2e/tests/import-flow.spec.js +++ b/test/e2e/tests/import-flow.spec.js @@ -83,7 +83,10 @@ describe('Import flow', function () { // choose Create account from the account menu await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement({ text: 'Add account', tag: 'button' }); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement({ text: 'Add a new account', tag: 'button' }); // set account name await driver.fill('[placeholder="Account 2"]', '2nd account'); @@ -196,6 +199,9 @@ describe('Import flow', function () { await driver.press('#password', driver.Key.ENTER); await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement({ text: 'Import account', tag: 'button' }); // Imports Account 4 with private key @@ -222,6 +228,9 @@ describe('Import flow', function () { }); // Imports Account 5 with private key + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement({ text: 'Import account', tag: 'button' }); await driver.findClickableElement('#private-key-box'); await driver.fill('#private-key-box', testPrivateKey2); @@ -277,6 +286,9 @@ describe('Import flow', function () { // Imports an account with JSON file await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement({ text: 'Import account', tag: 'button' }); await driver.clickElement('.dropdown__select'); @@ -340,6 +352,9 @@ describe('Import flow', function () { // choose Import Account from the account menu await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement({ text: 'Import account', tag: 'button' }); // enter private key @@ -373,6 +388,9 @@ describe('Import flow', function () { // choose Connect hardware wallet from the account menu await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); await driver.clickElement({ text: 'Add hardware wallet', tag: 'button', diff --git a/test/e2e/user-actions-benchmark.js b/test/e2e/user-actions-benchmark.js index 07843ecdf32f..3c2d71045a68 100644 --- a/test/e2e/user-actions-benchmark.js +++ b/test/e2e/user-actions-benchmark.js @@ -34,6 +34,9 @@ async function loadNewAccount() { await driver.press('#password', driver.Key.ENTER); await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); const timestampBeforeAction = new Date(); await driver.clickElement( '[data-testid="multichain-account-menu-popover-add-account"]', diff --git a/ui/components/multichain/account-list-menu/account-list-menu.js b/ui/components/multichain/account-list-menu/account-list-menu.js index a51ba2e0d612..3edbe481063f 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.js +++ b/ui/components/multichain/account-list-menu/account-list-menu.js @@ -5,7 +5,6 @@ import Fuse from 'fuse.js'; import { useDispatch, useSelector } from 'react-redux'; import { IconName, - ButtonLink, TextFieldSearch, Box, Modal, @@ -13,6 +12,10 @@ import { ModalOverlay, ModalHeader, Text, + ButtonVariant, + ButtonLink, + ButtonSecondary, + ButtonSecondarySize, } from '../../component-library'; import { AccountListItem, CreateAccount, ImportAccount } from '..'; import { @@ -21,6 +24,7 @@ import { TextColor, Display, FlexDirection, + AlignItems, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -51,6 +55,17 @@ import { import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; +const ACTION_MODES = { + // Displays the search box and account list + LIST: '', + // Displays the Add, Import, Hardware accounts + MENU: 'menu', + // Displays the add account form controls + ADD: 'add', + // Displays the import account form controls + IMPORT: 'import', +}; + export const AccountListMenu = ({ onClose }) => { const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); @@ -65,7 +80,7 @@ export const AccountListMenu = ({ onClose }) => { ///: END:ONLY_INCLUDE_IN const [searchQuery, setSearchQuery] = useState(''); - const [actionMode, setActionMode] = useState(''); + const [actionMode, setActionMode] = useState(ACTION_MODES.LIST); let searchResults = accounts; if (searchQuery) { @@ -82,12 +97,21 @@ export const AccountListMenu = ({ onClose }) => { } let title = t('selectAnAccount'); - if (actionMode === 'add') { + if (actionMode === ACTION_MODES.ADD || actionMode === ACTION_MODES.MENU) { title = t('addAccount'); - } else if (actionMode === 'import') { + } else if (actionMode === ACTION_MODES.IMPORT) { title = t('importAccount'); } + let onBack = null; + if (actionMode !== ACTION_MODES.LIST) { + if (actionMode === ACTION_MODES.MENU) { + onBack = () => setActionMode(ACTION_MODES.LIST); + } else { + onBack = () => setActionMode(ACTION_MODES.MENU); + } + } + return ( @@ -100,27 +124,23 @@ export const AccountListMenu = ({ onClose }) => { flexDirection: FlexDirection.Column, }} > - setActionMode('')} - > + {title} - {actionMode === 'add' ? ( + {actionMode === ACTION_MODES.ADD ? ( { if (confirmed) { dispatch(toggleAccountMenu()); } else { - setActionMode(''); + setActionMode(ACTION_MODES.LIST); } }} /> ) : null} - {actionMode === 'import' ? ( + {actionMode === ACTION_MODES.IMPORT ? ( { if (confirmed) { dispatch(toggleAccountMenu()); } else { - setActionMode(''); + setActionMode(ACTION_MODES.LIST); } }} /> ) : null} - {actionMode === '' ? ( + {/* Add / Import / Hardware Menu */} + {actionMode === ACTION_MODES.MENU ? ( + + + { + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.AccountAddSelected, + properties: { + account_type: MetaMetricsEventAccountType.Default, + location: 'Main Menu', + }, + }); + setActionMode(ACTION_MODES.ADD); + }} + data-testid="multichain-account-menu-popover-add-account" + > + {t('addNewAccount')} + + + + { + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.AccountAddSelected, + properties: { + account_type: MetaMetricsEventAccountType.Imported, + location: 'Main Menu', + }, + }); + setActionMode(ACTION_MODES.IMPORT); + }} + > + {t('importAccount')} + + + + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.AccountAddSelected, + properties: { + account_type: MetaMetricsEventAccountType.Hardware, + location: 'Main Menu', + }, + }); + if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser( + CONNECT_HARDWARE_ROUTE, + ); + } else { + history.push(CONNECT_HARDWARE_ROUTE); + } + }} + > + {t('addHardwareWallet')} + + + { + ///: BEGIN:ONLY_INCLUDE_IN(keyring-snaps) + addSnapAccountEnabled ? ( + + { + dispatch(toggleAccountMenu()); + getEnvironmentType() === ENVIRONMENT_TYPE_POPUP + ? global.platform.openExtensionInBrowser( + ADD_SNAP_ACCOUNT_ROUTE, + null, + true, + ) + : history.push(ADD_SNAP_ACCOUNT_ROUTE); + }} + > + {t('settingAddSnapAccount')} + + + ) : null + ///: END:ONLY_INCLUDE_IN + } + { + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: + MetaMetricsEventName.ConnectCustodialAccountClicked, + }); + if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser( + CUSTODY_ACCOUNT_ROUTE, + ); + } else { + history.push(CUSTODY_ACCOUNT_ROUTE); + } + }} + > + {t('connectCustodialAccountMenu')} + + + ///: END:ONLY_INCLUDE_IN + } + + ) : null} + {actionMode === ACTION_MODES.LIST ? ( <> {/* Search box */} {accounts.length > 1 ? ( @@ -202,124 +343,25 @@ export const AccountListMenu = ({ onClose }) => { ); })} - {/* Add / Import / Hardware */} - - - { - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.AccountAddSelected, - properties: { - account_type: MetaMetricsEventAccountType.Default, - location: 'Main Menu', - }, - }); - setActionMode('add'); - }} - data-testid="multichain-account-menu-popover-add-account" - > - {t('addAccount')} - - - - { - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.AccountAddSelected, - properties: { - account_type: MetaMetricsEventAccountType.Imported, - location: 'Main Menu', - }, - }); - setActionMode('import'); - }} - > - {t('importAccount')} - - - - { - dispatch(toggleAccountMenu()); - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.AccountAddSelected, - properties: { - account_type: MetaMetricsEventAccountType.Hardware, - location: 'Main Menu', - }, - }); - if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser( - CONNECT_HARDWARE_ROUTE, - ); - } else { - history.push(CONNECT_HARDWARE_ROUTE); - } - }} - > - {t('addHardwareWallet')} - - - { - ///: BEGIN:ONLY_INCLUDE_IN(keyring-snaps) - addSnapAccountEnabled && ( - - { - dispatch(toggleAccountMenu()); - getEnvironmentType() === ENVIRONMENT_TYPE_POPUP - ? global.platform.openExtensionInBrowser( - ADD_SNAP_ACCOUNT_ROUTE, - null, - true, - ) - : history.push(ADD_SNAP_ACCOUNT_ROUTE); - }} - > - {t('settingAddSnapAccount')} - - - ) - ///: END:ONLY_INCLUDE_IN - } - { - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) - - { - dispatch(toggleAccountMenu()); - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: - MetaMetricsEventName.ConnectCustodialAccountClicked, - }); - if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser( - CUSTODY_ACCOUNT_ROUTE, - ); - } else { - history.push(CUSTODY_ACCOUNT_ROUTE); - } - }} - > - {t('connectCustodialAccountMenu')} - - - ///: END:ONLY_INCLUDE_IN - } + {/* Add / Import / Hardware button */} + + setActionMode(ACTION_MODES.MENU)} + data-testid="multichain-account-menu-popover-action-button" + > + {t('addImportAccount')} + ) : null} diff --git a/ui/components/multichain/account-list-menu/account-list-menu.test.js b/ui/components/multichain/account-list-menu/account-list-menu.test.js index ebdc008732db..3ea7532f404a 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.test.js +++ b/ui/components/multichain/account-list-menu/account-list-menu.test.js @@ -58,35 +58,7 @@ describe('AccountListMenu', () => { const { getByPlaceholderText, getByText } = render(); expect(getByPlaceholderText('Search accounts')).toBeInTheDocument(); - expect(getByText('Add account')).toBeInTheDocument(); - expect(getByText('Import account')).toBeInTheDocument(); - expect(getByText('Add hardware wallet')).toBeInTheDocument(); - }); - - it('shows the account creation UI when Add Account is clicked', () => { - const { getByText, getByPlaceholderText } = render(); - fireEvent.click(getByText('Add account')); - expect(getByText('Create')).toBeInTheDocument(); - expect(getByText('Cancel')).toBeInTheDocument(); - - fireEvent.click(getByText('Cancel')); - expect(getByPlaceholderText('Search accounts')).toBeInTheDocument(); - }); - - it('shows the account import UI when Import Account is clicked', () => { - const { getByText, getByPlaceholderText } = render(); - fireEvent.click(getByText('Import account')); - expect(getByText('Import')).toBeInTheDocument(); - expect(getByText('Cancel')).toBeInTheDocument(); - - fireEvent.click(getByText('Cancel')); - expect(getByPlaceholderText('Search accounts')).toBeInTheDocument(); - }); - - it('navigates to hardware wallet connection screen when clicked', () => { - const { getByText } = render(); - fireEvent.click(getByText('Add hardware wallet')); - expect(historyPushMock).toHaveBeenCalledWith(CONNECT_HARDWARE_ROUTE); + expect(getByText('Add account or hardware wallet')).toBeInTheDocument(); }); it('displays accounts for list and filters by search', () => { @@ -157,6 +129,77 @@ describe('AccountListMenu', () => { expect(searchBox).toBeInTheDocument(); }); + it('add / Import / Hardware button functions as it should', () => { + const { getByText } = render(); + + // Ensure the button is displaying + const button = document.querySelectorAll( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + expect(button).toHaveLength(1); + + // Click the button to ensure the options and close button display + button[0].click(); + expect(getByText('Add a new account')).toBeInTheDocument(); + expect(getByText('Import account')).toBeInTheDocument(); + expect(getByText('Add hardware wallet')).toBeInTheDocument(); + const header = document.querySelector('header'); + expect(header.innerHTML).toBe('Add account'); + expect( + document.querySelector('button[aria-label="Close"]'), + ).toBeInTheDocument(); + + const backButton = document.querySelector('button[aria-label="Back"]'); + expect(backButton).toBeInTheDocument(); + backButton.click(); + + expect(getByText('Select an account')).toBeInTheDocument(); + }); + + it('shows the account creation UI when Add Account is clicked', () => { + const { getByText, getByPlaceholderText } = render(); + + const button = document.querySelector( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + button.click(); + + fireEvent.click(getByText('Add a new account')); + expect(getByText('Create')).toBeInTheDocument(); + expect(getByText('Cancel')).toBeInTheDocument(); + + fireEvent.click(getByText('Cancel')); + expect(getByPlaceholderText('Search accounts')).toBeInTheDocument(); + }); + + it('shows the account import UI when Import Account is clicked', () => { + const { getByText, getByPlaceholderText } = render(); + + const button = document.querySelector( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + button.click(); + + fireEvent.click(getByText('Import account')); + expect(getByText('Import')).toBeInTheDocument(); + expect(getByText('Cancel')).toBeInTheDocument(); + + fireEvent.click(getByText('Cancel')); + expect(getByPlaceholderText('Search accounts')).toBeInTheDocument(); + }); + + it('navigates to hardware wallet connection screen when clicked', () => { + const { getByText } = render(); + + const button = document.querySelector( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + button.click(); + + fireEvent.click(getByText('Add hardware wallet')); + expect(historyPushMock).toHaveBeenCalledWith(CONNECT_HARDWARE_ROUTE); + }); + ///: BEGIN:ONLY_INCLUDE_IN(keyring-snaps) describe('addSnapAccountButton', () => { const renderWithState = (state, props = { onClose: () => jest.fn() }) => { @@ -181,6 +224,10 @@ describe('AccountListMenu', () => { it("doesn't render the add snap account button if it's disabled", async () => { const { getByText } = renderWithState({ addSnapAccountEnabled: false }); + const button = document.querySelector( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + button.click(); expect(() => getByText(messages.settingAddSnapAccount.message)).toThrow( `Unable to find an element with the text: ${messages.settingAddSnapAccount.message}`, ); @@ -188,6 +235,10 @@ describe('AccountListMenu', () => { it("renders the add snap account button if it's enabled", async () => { const { getByText } = renderWithState({ addSnapAccountEnabled: true }); + const button = document.querySelector( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + button.click(); const addSnapAccountButton = getByText( messages.settingAddSnapAccount.message, ); @@ -202,6 +253,12 @@ describe('AccountListMenu', () => { it('pushes history when clicking add snap account from extended view', async () => { const { getByText } = renderWithState({ addSnapAccountEnabled: true }); mockGetEnvironmentType.mockReturnValueOnce('fullscreen'); + + const button = document.querySelector( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + button.click(); + const addSnapAccountButton = getByText( messages.settingAddSnapAccount.message, ); From 0c6ebd6fdd1e42b302420c76fe3b5663e12ee5b2 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 2 Oct 2023 10:42:57 -0500 Subject: [PATCH 016/219] Fix #19371 - Provide conversion buttons for empty accounts (#21049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Provides conversion buttons to buy, receive, and learn more about NFTs when an account is empty. ## **Manual testing steps** - Start with a MetaMask account with assets in it on Mainnet - *Don't* see the Buy/Receive blocks - Create a new account with no assets - *See* the Buy/Receive blocks - Switch to a network that we don't support buying on (localhost) - See *only* the "Receive" block - Switch to NFTs tab - See the NFTs block if you have no NFTS - *Don't* see the NFTs block if you *do* have NFTs ## **Screenshots/Recordings** ### Token List SCR-20230928-qwwf ### NFTs SCR-20230928-qxau ## **Related issues** _Fixes https://github.com/MetaMask/metamask-extension/issues/19371 ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've clearly explained: - [ ] What problem this PR is solving. - [ ] How this problem was solved. - [ ] How reviewers can test my changes. - [ ] I’ve indicated what issue this PR is linked to: Fixes #??? - [ ] I’ve included tests if applicable. - [ ] I’ve documented any added code. - [ ] 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)). - [ ] I’ve properly set the pull request status: - [ ] In case it's not yet "ready for review", I've set it to "draft". - [ ] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 3 + app/images/token-list-buy-background.png | Bin 0 -> 3148 bytes app/images/token-list-nfts-background.png | Bin 0 -> 4011 bytes app/images/token-list-receive-background.png | Bin 0 -> 3449 bytes .../__snapshots__/asset-list.test.js.snap | 2 +- .../asset-list/asset-list.buy-receive.test.js | 64 +++++++++ ui/components/app/asset-list/asset-list.js | 132 ++++++++++++++---- .../app/asset-list/asset-list.test.js | 24 ++-- ui/components/app/nfts-tab/nfts-tab.js | 24 +++- ui/components/app/nfts-tab/nfts-tab.test.js | 22 +++ .../asset-list-conversion-button.js | 82 +++++++++++ .../asset-list-conversion-button.scss | 20 +++ .../asset-list-conversion-button.stories.js | 25 ++++ .../asset-list-conversion-button/index.js | 1 + ui/components/multichain/index.js | 1 + .../multichain/multichain-components.scss | 1 + .../multichain/receive-modal/index.js | 1 + .../multichain/receive-modal/receive-modal.js | 79 +++++++++++ .../receive-modal/receive-modal.stories.js | 33 +++++ .../receive-modal/receive-modal.test.js | 27 ++++ 20 files changed, 500 insertions(+), 41 deletions(-) create mode 100644 app/images/token-list-buy-background.png create mode 100644 app/images/token-list-nfts-background.png create mode 100644 app/images/token-list-receive-background.png create mode 100644 ui/components/app/asset-list/asset-list.buy-receive.test.js create mode 100644 ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.js create mode 100644 ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.scss create mode 100644 ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.stories.js create mode 100644 ui/components/multichain/asset-list-conversion-button/index.js create mode 100644 ui/components/multichain/receive-modal/index.js create mode 100644 ui/components/multichain/receive-modal/receive-modal.js create mode 100644 ui/components/multichain/receive-modal/receive-modal.stories.js create mode 100644 ui/components/multichain/receive-modal/receive-modal.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index dd67a4b6d1ed..f278ae748bf4 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2606,6 +2606,9 @@ "nftDisclaimer": { "message": "Disclaimer: MetaMask pulls the media file from the source url. This url sometimes is changed by the marketplace the NFT was minted on." }, + "nftLearnMore": { + "message": "Learn more about NFTs" + }, "nftOptions": { "message": "NFT Options" }, diff --git a/app/images/token-list-buy-background.png b/app/images/token-list-buy-background.png new file mode 100644 index 0000000000000000000000000000000000000000..65566931639729d9b676646f47a41d74fd9d9c99 GIT binary patch literal 3148 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 literal 0 HcmV?d00001 diff --git a/app/images/token-list-receive-background.png b/app/images/token-list-receive-background.png new file mode 100644 index 0000000000000000000000000000000000000000..70f0944fc7e0ff19db57865009e2d816a0f79cac GIT binary patch literal 3449 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 bg9N?
{ + const state = { + ...mockState, + metamask: { + ...mockState.metamask, + providerConfig: { chainId }, + cachedBalances: { + [CHAIN_IDS.MAINNET]: { + [selectedAddress]: balance, + }, + }, + selectedAddress, + }, + }; + const store = configureStore(state); + return renderWithProvider( + undefined} />, + store, + ); +}; + +describe('AssetList Buy/Receive', () => { + it('shows Buy and Receive when the account is empty', () => { + process.env.MULTICHAIN = 1; + const { queryByText } = render( + '0xc42edfcc21ed14dda456aa0756c153f7985d8813', + '0x0', + ); + expect(queryByText('Buy')).toBeInTheDocument(); + expect(queryByText('Receive')).toBeInTheDocument(); + }); + + it('shows only Receive when chainId is not buyable', () => { + process.env.MULTICHAIN = 1; + const { queryByText } = render( + '0xc42edfcc21ed14dda456aa0756c153f7985d8813', + '0x0', + '0x8675309', // Custom chain ID that isn't buyable + ); + expect(queryByText('Buy')).not.toBeInTheDocument(); + expect(queryByText('Receive')).toBeInTheDocument(); + }); + + it('shows neither when the account has a balance', () => { + process.env.MULTICHAIN = 1; + const { queryByText } = render(); + expect(queryByText('Buy')).not.toBeInTheDocument(); + expect(queryByText('Receive')).not.toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 48d82ee037b5..259579e380cc 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -14,6 +14,10 @@ import { getShouldHideZeroBalanceTokens, getTokenExchangeRates, getCurrentCurrency, + getIsBuyableChain, + getCurrentChainId, + getSwapsDefaultToken, + getSelectedAddress, } from '../../../selectors'; import { getConversionRate, @@ -33,7 +37,9 @@ import { TokenListItem, ImportTokenLink, BalanceOverview, + AssetListConversionButton, } from '../../multichain'; + import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; import { getTokenFiatAmount } from '../../../helpers/utils/token-util'; import { formatCurrency } from '../../../helpers/utils/confirm-tx.util'; @@ -43,15 +49,22 @@ import { } from '../../../../shared/modules/conversion.utils'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; +import useRamps from '../../../hooks/experiences/useRamps'; +import { Display } from '../../../helpers/constants/design-system'; + +import { ReceiveModal } from '../../multichain/receive-modal'; + const AssetList = ({ onClickAsset }) => { const [showDetectedTokens, setShowDetectedTokens] = useState(false); - const selectedAccountBalance = useSelector(getSelectedAccountCachedBalance); const nativeCurrency = useSelector(getNativeCurrency); const showFiat = useSelector(getShouldShowFiat); const trackEvent = useContext(MetaMetricsContext); const balance = useSelector(getSelectedAccountCachedBalance); const balanceIsLoading = !balance; + const selectedAddress = useSelector(getSelectedAddress); + + const [showReceiveModal, setShowReceiveModal] = useState(false); const { currency: primaryCurrency, @@ -109,6 +122,7 @@ const AssetList = ({ onClickAsset }) => { shouldHideZeroBalanceTokens, ); + // An array of string balances (ex: ["1.90", "22290.01", ...]) const dollarBalances = tokensWithBalances.map((token) => { const contractExchangeTokenKey = Object.keys(contractExchangeRates).find( (key) => isEqualCaseInsensitive(key, token.address), @@ -131,15 +145,32 @@ const AssetList = ({ onClickAsset }) => { return fiat; }); - const totalFiat = formatCurrency( - sumDecimals(nativeFiat, ...dollarBalances).toString(10), + // Total native and token fiat balance as a string (ex: "8.90") + const totalFiatBalance = sumDecimals(nativeFiat, ...dollarBalances).toString( + 10, + ); + + // Fiat balance formatted in user's desired currency (ex: "$8.90") + const formattedTotalFiatBalance = formatCurrency( + totalFiatBalance, currentCurrency, ); + const balanceIsZero = Number(totalFiatBalance) === 0; + const isBuyableChain = useSelector(getIsBuyableChain); + const shouldShowBuy = isBuyableChain && balanceIsZero; + const shouldShowReceive = balanceIsZero; + const { openBuyCryptoInPdapp } = useRamps(); + const chainId = useSelector(getCurrentChainId); + const defaultSwapsToken = useSelector(getSwapsDefaultToken); + return ( <> {process.env.MULTICHAIN ? ( - + ) : null} {detectedTokens.length > 0 && !isTokenDetectionInactiveOnNonMainnetSupportedNetwork && ( @@ -148,31 +179,74 @@ const AssetList = ({ onClickAsset }) => { margin={4} /> )} - onClickAsset(nativeCurrency)} - title={nativeCurrency} - primary={ - primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value - } - tokenSymbol={primaryCurrencyProperties.suffix} - secondary={showFiat ? secondaryCurrencyDisplay : undefined} - tokenImage={balanceIsLoading ? null : primaryTokenImage} - /> - { - onClickAsset(tokenAddress); - trackEvent({ - event: MetaMetricsEventName.TokenScreenOpened, - category: MetaMetricsEventCategory.Navigation, - properties: { - token_symbol: primaryCurrencyProperties.suffix, - location: 'Home', - }, - }); - }} - /> + {process.env.MULTICHAIN && (shouldShowBuy || shouldShowReceive) ? ( + + {shouldShowBuy ? ( + { + openBuyCryptoInPdapp(); + trackEvent({ + event: MetaMetricsEventName.NavBuyButtonClicked, + category: MetaMetricsEventCategory.Navigation, + properties: { + location: 'Home', + text: 'Buy', + chain_id: chainId, + token_symbol: defaultSwapsToken, + }, + }); + }} + /> + ) : null} + {shouldShowReceive ? ( + setShowReceiveModal(true)} + /> + ) : null} + {showReceiveModal ? ( + setShowReceiveModal(false)} + /> + ) : null} + + ) : ( + <> + onClickAsset(nativeCurrency)} + title={nativeCurrency} + primary={ + primaryCurrencyProperties.value ?? + secondaryCurrencyProperties.value + } + tokenSymbol={primaryCurrencyProperties.suffix} + secondary={showFiat ? secondaryCurrencyDisplay : undefined} + tokenImage={balanceIsLoading ? null : primaryTokenImage} + /> + { + onClickAsset(tokenAddress); + trackEvent({ + event: MetaMetricsEventName.TokenScreenOpened, + category: MetaMetricsEventCategory.Navigation, + properties: { + token_symbol: primaryCurrencyProperties.suffix, + location: 'Home', + }, + }); + }} + /> + + )} 0 ? 0 : 4}> diff --git a/ui/components/app/asset-list/asset-list.test.js b/ui/components/app/asset-list/asset-list.test.js index cb12394ac4f3..9ba19e0cf8f1 100644 --- a/ui/components/app/asset-list/asset-list.test.js +++ b/ui/components/app/asset-list/asset-list.test.js @@ -49,16 +49,20 @@ jest.mock('../../../hooks/useTokenTracker', () => { }; }); -const render = () => { +const render = ( + selectedAddress = mockState.metamask.selectedAddress, + balance = ETH_BALANCE, + chainId = CHAIN_IDS.MAINNET, +) => { const state = { ...mockState, metamask: { ...mockState.metamask, - providerConfig: { chainId: CHAIN_IDS.MAINNET }, + providerConfig: { chainId }, conversionRate: CONVERSION_RATE, cachedBalances: { [CHAIN_IDS.MAINNET]: { - [mockState.metamask.selectedAddress]: ETH_BALANCE, + [selectedAddress]: balance, }, }, contractExchangeRates: { @@ -66,6 +70,7 @@ const render = () => { [LINK_CONTRACT]: 0.00423239, [WBTC_CONTRACT]: 16.66575, }, + selectedAddress, }, }; const store = configureStore(state); @@ -81,10 +86,13 @@ describe('AssetList', () => { expect(screen.getByText('Refresh list')).toBeInTheDocument(); }); - it('calculates the correct fiat account total', () => { - process.env.MULTICHAIN = 1; - const { container } = render(); - expect(container).toMatchSnapshot(); - expect(screen.getByText('$63,356.88 USD')).toBeInTheDocument(); + describe('token fiat value calculations', () => { + it('calculates the correct fiat account total', () => { + process.env.MULTICHAIN = 1; + const { container } = render(); + expect(container).toMatchSnapshot(); + expect(screen.getByText('$63,356.88 USD')).toBeInTheDocument(); + jest.resetModules(); + }); }); }); diff --git a/ui/components/app/nfts-tab/nfts-tab.js b/ui/components/app/nfts-tab/nfts-tab.js index 9b4af2f201bd..80adfc62a8be 100644 --- a/ui/components/app/nfts-tab/nfts-tab.js +++ b/ui/components/app/nfts-tab/nfts-tab.js @@ -24,6 +24,7 @@ import { import { Box, ButtonLink, IconName, Text } from '../../component-library'; import NFTsDetectionNoticeNFTsTab from '../nfts-detection-notice-nfts-tab/nfts-detection-notice-nfts-tab'; import NftsItems from '../nfts-items'; +import { AssetListConversionButton } from '../../multichain'; export default function NftsTab() { const useNftDetection = useSelector(getUseNftDetection); @@ -46,14 +47,16 @@ export default function NftsTab() { checkAndUpdateAllNftsOwnershipStatus(); }; + const hasAnyNfts = Object.keys(collections).length > 0; + const showNftBanner = process.env.MULTICHAIN && hasAnyNfts === false; + if (nftsLoading) { return
{t('loadingNFTs')}
; } return ( - {Object.keys(collections).length > 0 || - previouslyOwnedCollection.nfts.length > 0 ? ( + {hasAnyNfts > 0 || previouslyOwnedCollection.nfts.length > 0 ? ( {isMainnet && !useNftDetection ? ( - + ) : null} + {showNftBanner ? ( + + + global.platform.openTab({ url: ZENDESK_URLS.NFT_TOKENS }) + } + /> + + ) : null} { expect(historyPushMock).toHaveBeenCalledWith(SECURITY_ROUTE); }); }); + + describe('nft conversion banner', () => { + it('shows the NFT conversion banner when there are no NFTs', () => { + process.env.MULTICHAIN = 1; + const { queryByText } = render({ + selectedAddress: ACCOUNT_1, + nfts: [], + }); + + expect(queryByText('Learn more about NFTs')).toBeInTheDocument(); + }); + + it('does not show the NFT conversion banner when there are NFTs', () => { + process.env.MULTICHAIN = 1; + const { queryByText } = render({ + selectedAddress: ACCOUNT_1, + nfts: NFTS, + }); + + expect(queryByText('Learn more about NFTs')).not.toBeInTheDocument(); + }); + }); }); diff --git a/ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.js b/ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.js new file mode 100644 index 000000000000..8869daafe2ae --- /dev/null +++ b/ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.js @@ -0,0 +1,82 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { IconName, Box, IconSize, Icon, Text } from '../../component-library'; +import { + AlignItems, + BorderRadius, + Display, +} from '../../../helpers/constants/design-system'; + +import { useI18nContext } from '../../../hooks/useI18nContext'; + +const ASSET_LIST_CONVERSION_BUTTON_VARIANTS = { + buy: { + color: 'var(--color-info-default)', + backgroundImage: 'url(/images/token-list-buy-background.png)', + text: 'buy', + icon: IconName.Add, + }, + receive: { + color: 'var(--color-flask-default)', + backgroundImage: 'url(/images/token-list-receive-background.png)', + text: 'receive', + icon: IconName.Arrow2Down, + }, + nft: { + color: 'var(--color-error-alternative)', + backgroundImage: 'url(/images/token-list-nfts-background.png)', + text: 'nftLearnMore', + icon: IconName.Book, + }, +}; + +export const AssetListConversionButton = ({ onClick, variant }) => { + const t = useI18nContext(); + const { color, backgroundImage, text, icon } = + ASSET_LIST_CONVERSION_BUTTON_VARIANTS[variant]; + + return ( + + + + + + + + + {t(text)} + + + + ); +}; + +AssetListConversionButton.propTypes = { + /** + * Executes when the button is clicked + */ + onClick: PropTypes.func.isRequired, + /** + * Text within the button body + */ + variant: PropTypes.oneOf(['buy', 'receive', 'nft']), +}; diff --git a/ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.scss b/ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.scss new file mode 100644 index 000000000000..fea379efe2a5 --- /dev/null +++ b/ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.scss @@ -0,0 +1,20 @@ +.asset-list-conversion-button { + min-width: 157px; + height: 56px; + flex-grow: 1; + + + &__contents { + flex-grow: 1; + + &__button-wrapper { + background: var(--brand-colors-white-white000); + width: 24px; + height: 24px; + } + + &__text { + color: var(--brand-colors-white-white000); + } + } +} diff --git a/ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.stories.js b/ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.stories.js new file mode 100644 index 000000000000..4bf7248073ba --- /dev/null +++ b/ui/components/multichain/asset-list-conversion-button/asset-list-conversion-button.stories.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { AssetListConversionButton } from '.'; + +export default { + title: 'Components/Multichain/AssetListConversionButton', + component: AssetListConversionButton, + argTypes: { + variant: { + control: 'text', + }, + }, + args: { + variant: 'buy', + onClick: () => undefined, + onClose: () => undefined, + }, +}; + +export const DefaultStory = (args) => ; +DefaultStory.storyName = 'Default'; + +export const ReceiveStory = (args) => ( + +); +ReceiveStory.storyName = 'Receive'; diff --git a/ui/components/multichain/asset-list-conversion-button/index.js b/ui/components/multichain/asset-list-conversion-button/index.js new file mode 100644 index 000000000000..c8c6aa20e7e7 --- /dev/null +++ b/ui/components/multichain/asset-list-conversion-button/index.js @@ -0,0 +1 @@ +export { AssetListConversionButton } from './asset-list-conversion-button'; diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js index 38ab4e6db18d..a2d8c5d7db17 100644 --- a/ui/components/multichain/index.js +++ b/ui/components/multichain/index.js @@ -23,3 +23,4 @@ export { AccountDetailsMenuItem, ViewExplorerMenuItem } from './menu-items'; export { ImportTokensModal } from './import-tokens-modal'; export { SelectActionModal } from './select-action-modal'; export { SelectActionModalItem } from './select-action-modal-item'; +export { AssetListConversionButton } from './asset-list-conversion-button'; diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss index 59f12129f5e7..124bc2af08bf 100644 --- a/ui/components/multichain/multichain-components.scss +++ b/ui/components/multichain/multichain-components.scss @@ -22,3 +22,4 @@ @import 'nft-item/nft-item'; @import 'import-tokens-modal/import-tokens-modal'; @import 'select-action-modal-item/select-action-modal-item'; +@import 'asset-list-conversion-button/asset-list-conversion-button'; diff --git a/ui/components/multichain/receive-modal/index.js b/ui/components/multichain/receive-modal/index.js new file mode 100644 index 000000000000..7249a755ab7f --- /dev/null +++ b/ui/components/multichain/receive-modal/index.js @@ -0,0 +1 @@ +export { ReceiveModal } from './receive-modal'; diff --git a/ui/components/multichain/receive-modal/receive-modal.js b/ui/components/multichain/receive-modal/receive-modal.js new file mode 100644 index 000000000000..03b59c845062 --- /dev/null +++ b/ui/components/multichain/receive-modal/receive-modal.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + AvatarAccount, + AvatarAccountSize, + AvatarAccountVariant, + Box, + Modal, + ModalContent, + ModalHeader, + ModalOverlay, + Text, +} from '../../component-library'; +import QrView from '../../ui/qr-code'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { getMetaMaskAccountsOrdered, getUseBlockie } from '../../../selectors'; +import { + AlignItems, + BlockSize, + Display, + FlexDirection, + JustifyContent, + TextAlign, + TextVariant, +} from '../../../helpers/constants/design-system'; + +export const ReceiveModal = ({ address, onClose }) => { + const t = useI18nContext(); + const useBlockie = useSelector(getUseBlockie); + const accounts = useSelector(getMetaMaskAccountsOrdered); + const { name } = accounts.find((account) => account.address === address); + + return ( + + + + + {t('receive')} + + + + + + {name} + + + + + + + + ); +}; + +ReceiveModal.propTypes = { + address: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, +}; diff --git a/ui/components/multichain/receive-modal/receive-modal.stories.js b/ui/components/multichain/receive-modal/receive-modal.stories.js new file mode 100644 index 000000000000..412de6d22fac --- /dev/null +++ b/ui/components/multichain/receive-modal/receive-modal.stories.js @@ -0,0 +1,33 @@ +import React from 'react'; + +import { Provider } from 'react-redux'; + +import testData from '../../../../.storybook/test-data'; +import configureStore from '../../../store/store'; + +import { ReceiveModal } from '.'; + +const store = configureStore(testData); + +export default { + title: 'Components/Multichain/ReceiveModal', + component: ReceiveModal, + argTypes: { + address: { + control: 'text', + }, + onClose: { + action: 'onClose', + }, + }, + args: { + address: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', + onClose: () => undefined, + }, +}; + +export const DefaultStory = (args) => ; +DefaultStory.decorators = [ + (story) => {story()}, +]; +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/receive-modal/receive-modal.test.js b/ui/components/multichain/receive-modal/receive-modal.test.js new file mode 100644 index 000000000000..e31e9e593020 --- /dev/null +++ b/ui/components/multichain/receive-modal/receive-modal.test.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { toChecksumHexAddress } from '@metamask/controller-utils'; +import mockState from '../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import { ReceiveModal } from '.'; + +describe('ReceiveModal', () => { + const address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; + + const render = () => + renderWithProvider( + , + configureStore(mockState), + ); + + it('should show the correct account address and name', () => { + render(); + // Check for the copy button + expect( + screen.queryByText(toChecksumHexAddress(address)), + ).toBeInTheDocument(); + // Check for the title + expect(screen.queryByText('Test Account')).toBeInTheDocument(); + }); +}); From 360ab36c877f356f412a37657afe785da3e444a1 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Mon, 2 Oct 2023 17:49:24 +0200 Subject: [PATCH 017/219] fix: fix contract address display in confirmation page (#21042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Sometimes the confirmation transaction page displays the destination wallet address rather than the contract address. Here is related github issue: https://github.com/MetaMask/metamask-extension/issues/19502. ## **Manual testing steps** _1. Go to : https://metamask.github.io/test-dapp/ or you can use etherscan on a contract address _2. Deploy ERC1155 smart contract _3. Call safeBatchTransferFrom function and notice the confirmation page does displays the destination address and not the contract address (side by side with the contract function name) ## **Screenshots/Recordings** ### **Before** Calling mintBatch fct displays contract address correctly ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/302d0e4c-2da7-4898-a621-72a4590e3592) Calling safeBatchTransferFrom fct displays the destination address instead: ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/2c51d863-9890-4491-ba0d-6051c3003cb8) ### **After** Mint batch and safeTransferFromBatch display contract address ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/7ff35d8b-fedc-4d2c-9eab-9539be211c6a) ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/96e9eb9e-7b9b-4923-a256-707d14f8d891) And if you are sending a transfer transaction (contractInteraction) to an address already imported in your wallet, it shows : ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/6e08d800-0ac7-4c80-b0fe-7a5878388ae7) ## **Related issues** [_Fixes #MMAssets-33_](https://consensyssoftware.atlassian.net/browse/MMASSETS-33) ## **Pre-merge author checklist** - [x ] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ x] I've clearly explained: - [ ] What problem this PR is solving. - [ ] How this problem was solved. - [ ] How reviewers can test my changes. - [x ] I’ve indicated what issue this PR is linked to: Fixes #??? - [ ] I’ve included tests if applicable. - [ x] I’ve documented any added code. - [ 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)). - [ ] I’ve properly set the pull request status: - [ ] In case it's not yet "ready for review", I've set it to "draft". - [ ] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/data/mock-state.json | 8 ++++++++ .../confirm-page-container-container.test.js | 2 ++ ...m-page-container-content.component.test.js | 19 +++++++++++++++++-- ...onfirm-page-container-summary.component.js | 13 ++++++++----- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 95a1e6caff3d..6c2064108972 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -20,6 +20,14 @@ "warning": null, "customTokenAmount": "10" }, + "confirmTransaction": { + "txData": { + "txParams": { + "gas": "0x153e2", + "value": "0x0" + } + } + }, "history": { "mostRecentOverviewPage": "/mostRecentOverviewPage" }, diff --git a/ui/components/app/confirm-page-container/confirm-page-container-container.test.js b/ui/components/app/confirm-page-container/confirm-page-container-container.test.js index 6432fad41027..a6afdcfc6368 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-container.test.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-container.test.js @@ -319,6 +319,8 @@ describe('Confirm Page Container Container Test', () => { }; mockState.metamask.addressBook = addressBook; + mockState.confirmTransaction.txData.txParams.to = + '0x7a1A4Ad9cc746a70ee58568466f7996dD0aCE4E8'; const store = configureMockStore()(mockState); diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js index f81301c195ec..5261d935c185 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.test.js @@ -7,6 +7,7 @@ import { INSUFFICIENT_FUNDS_ERROR_KEY, TRANSACTION_ERROR_KEY, } from '../../../../helpers/constants/error-keys'; +import { shortenAddress } from '../../../../helpers/utils/util'; import ConfirmPageContainerContent from './confirm-page-container-content.component'; describe('Confirm Page Container Content', () => { @@ -25,6 +26,17 @@ describe('Confirm Page Container Content', () => { }, }, }, + identities: {}, + tokenList: {}, + }, + confirmTransaction: { + txData: { + txParams: { + gas: '0x153e2', + value: '0x0', + to: '0x0BC30598F0F386371eB3d2195AcAA14C7566534b', + }, + }, }, }; @@ -106,7 +118,7 @@ describe('Confirm Page Container Content', () => { expect(props.onCancel).toHaveBeenCalledTimes(1); }); - it('render contract address name from addressBook in title for contract', async () => { + it('render contract address in the content component', async () => { props.disabled = false; props.toAddress = '0x06195827297c7A80a443b6894d3BDB8824b43896'; props.transactionType = TransactionType.contractInteraction; @@ -114,8 +126,11 @@ describe('Confirm Page Container Content', () => { , store, ); + const expectedAddress = shortenAddress( + mockStore.confirmTransaction.txData.txParams.to, + ); - expect(queryByText('Address Book Account 1')).toBeInTheDocument(); + expect(queryByText(`${expectedAddress}`)).toBeInTheDocument(); }); it('render simple title without address name for simple send', async () => { diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index 46a3f068f42e..2421f11ee39b 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -9,7 +9,7 @@ import { TransactionType } from '../../../../../../shared/constants/transaction' import { toChecksumHexAddress } from '../../../../../../shared/modules/hexstring-utils'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import useAddressDetails from '../../../../../hooks/useAddressDetails'; -import { getIpfsGateway } from '../../../../../selectors'; +import { getIpfsGateway, txDataSelector } from '../../../../../selectors'; import Identicon from '../../../../ui/identicon'; import InfoTooltip from '../../../../ui/info-tooltip'; @@ -25,7 +25,6 @@ const ConfirmPageContainerSummary = (props) => { subtitleComponent, className, tokenAddress, - toAddress, nonce, origin, image, @@ -36,6 +35,10 @@ const ConfirmPageContainerSummary = (props) => { const t = useI18nContext(); const ipfsGateway = useSelector(getIpfsGateway); + const txData = useSelector(txDataSelector); + const { txParams = {} } = txData; + const { to: txParamsToAddress } = txParams; + const contractInitiatedTransactionType = [ TransactionType.contractInteraction, TransactionType.tokenMethodTransfer, @@ -48,14 +51,15 @@ const ConfirmPageContainerSummary = (props) => { if (isContractTypeTransaction) { // If the transaction is TOKEN_METHOD_TRANSFER or TOKEN_METHOD_TRANSFER_FROM // the contract address is passed down as tokenAddress, if it is anyother - // type of contract interaction it is passed as toAddress + // type of contract interaction it is "to" from txParams + contractAddress = transactionType === TransactionType.tokenMethodTransfer || transactionType === TransactionType.tokenMethodTransferFrom || transactionType === TransactionType.tokenMethodSafeTransferFrom || transactionType === TransactionType.tokenMethodSetApprovalForAll ? tokenAddress - : toAddress; + : txParamsToAddress; } const { toName, isTrusted } = useAddressDetails(contractAddress); @@ -146,7 +150,6 @@ ConfirmPageContainerSummary.propTypes = { subtitleComponent: PropTypes.node, className: PropTypes.string, tokenAddress: PropTypes.string, - toAddress: PropTypes.string, nonce: PropTypes.string, origin: PropTypes.string.isRequired, transactionType: PropTypes.string, From dc01cc0f6ba87f05ecb35f7afbc7e79676b933a5 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Mon, 2 Oct 2023 17:49:36 +0200 Subject: [PATCH 018/219] fix: display warning when sending zero tokens (#21091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR display a warning when the user attempts to send zero amount in tokens/native currency. ## **Manual testing steps** _1. Click on any token _2. Click Send _3. Enter zero as amount _4. Click next and notice the display of warning. ## **Screenshots/Recordings** ### **Before** ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/7f37a546-0c27-4e78-99b3-a739eff27986) ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/28e9d00f-b1a3-4230-b908-a013400b0175) ### **After** Sending zero ETH ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/8b81d6d1-6e7e-40a5-b80d-6410e929c56d) Sending zero token (DAI) ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/c4b03fdd-587b-46a7-8c55-8f39cf946c65) Not displayed when sending amount in ETH !== 0 ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/14e12b85-3cef-4c49-b2f6-39e1d086a30f) Not displayed when sending amount of tokens different than zero ![image](https://github.com/MetaMask/metamask-extension/assets/10994169/ce179f3f-7925-490f-849b-bbde4eaa1a40) ## **Related issues** [_Fixes #MMASSETS-44](https://consensyssoftware.atlassian.net/browse/MMASSETS-44) ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've clearly explained: - [x] What problem this PR is solving. - [x] How this problem was solved. - [x] How reviewers can test my changes. - [x] I’ve indicated what issue this PR is linked to: Fixes #??? - [x] I’ve included tests if applicable. - [x] I’ve documented any added code. - [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)). - [ ] I’ve properly set the pull request status: - [ ] In case it's not yet "ready for review", I've set it to "draft". - [ ] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 3 + .../transaction-alerts/transaction-alerts.js | 35 ++- .../transaction-alerts.stories.js | 31 +++ .../transaction-alerts.test.js | 227 +++++++++++++++++- .../confirm-send-ether.test.js.snap | 15 ++ .../confirm-token-transaction-base.js | 1 + .../confirm-transaction-base.component.js | 3 + 7 files changed, 311 insertions(+), 4 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f278ae748bf4..77c8e3ec8f06 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3948,6 +3948,9 @@ "message": "Warning: you are about to send to a token contract which could result in a loss of funds. $1", "description": "$1 is a clickable link with text defined by the 'learnMoreUpperCase' key. The link will open to a support article regarding the known contract address warning" }, + "sendingZeroAmount": { + "message": "You are sending 0 $1." + }, "sepolia": { "message": "Sepolia test network" }, diff --git a/ui/components/app/transaction-alerts/transaction-alerts.js b/ui/components/app/transaction-alerts/transaction-alerts.js index 0738fdc104b4..c72d58bf1b68 100644 --- a/ui/components/app/transaction-alerts/transaction-alerts.js +++ b/ui/components/app/transaction-alerts/transaction-alerts.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; - import { PriorityLevels } from '../../../../shared/constants/gas'; import { submittedPendingTransactionsSelector } from '../../../selectors'; import { useGasFeeContext } from '../../../contexts/gasFee'; @@ -16,16 +15,44 @@ import { isSuspiciousResponse } from '../../../../shared/modules/security-provid import BlockaidBannerAlert from '../security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert'; ///: END:ONLY_INCLUDE_IN import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message'; +import { getNativeCurrency } from '../../../ducks/metamask/metamask'; +import { TransactionType } from '../../../../shared/constants/transaction'; +import { parseStandardTokenTransactionData } from '../../../../shared/modules/transaction.utils'; +import { getTokenValueParam } from '../../../../shared/lib/metamask-controller-utils'; const TransactionAlerts = ({ userAcknowledgedGasMissing, setUserAcknowledgedGasMissing, txData, + tokenSymbol, }) => { const { estimateUsed, hasSimulationError, supportsEIP1559, isNetworkBusy } = useGasFeeContext(); const pendingTransactions = useSelector(submittedPendingTransactionsSelector); const t = useI18nContext(); + const nativeCurrency = useSelector(getNativeCurrency); + const transactionData = txData.txParams.data; + const currentTokenSymbol = tokenSymbol || nativeCurrency; + let currentTokenAmount; + + if (txData.type === TransactionType.simpleSend) { + currentTokenAmount = txData.txParams.value; + } + if (txData.type === TransactionType.tokenMethodTransfer) { + const tokenData = parseStandardTokenTransactionData(transactionData); + currentTokenAmount = getTokenValueParam(tokenData); + } + + // isSendingZero is true when either sending native tokens where the value is in txParams + // or sending tokens where the value is in the txData + // We want to only display this warning in the cases where txType is simpleSend || transfer and not contractInteractions + const hasProperTxType = + txData.type === TransactionType.simpleSend || + txData.type === TransactionType.tokenMethodTransfer; + + const isSendingZero = + hasProperTxType && + (currentTokenAmount === '0x0' || currentTokenAmount === '0'); return (
@@ -85,6 +112,11 @@ const TransactionAlerts = ({ {t('networkIsBusy')} ) : null} + {isSendingZero && ( + + {t('sendingZeroAmount', [currentTokenSymbol])} + + )}
); }; @@ -93,6 +125,7 @@ TransactionAlerts.propTypes = { userAcknowledgedGasMissing: PropTypes.bool, setUserAcknowledgedGasMissing: PropTypes.func, txData: PropTypes.object, + tokenSymbol: PropTypes.string, }; export default TransactionAlerts; diff --git a/ui/components/app/transaction-alerts/transaction-alerts.stories.js b/ui/components/app/transaction-alerts/transaction-alerts.stories.js index 72426dd9202e..a53cd7a23ff0 100644 --- a/ui/components/app/transaction-alerts/transaction-alerts.stories.js +++ b/ui/components/app/transaction-alerts/transaction-alerts.stories.js @@ -98,6 +98,11 @@ export default { }, args: { userAcknowledgedGasMissing: false, + txData: { + txParams: { + value: '0x1', + }, + }, }, }; @@ -121,6 +126,15 @@ export const DefaultStory = (args) => ( ); DefaultStory.storyName = 'Default'; +DefaultStory.args = { + ...DefaultStory.args, + txData: { + txParams: { + value: '0x0', + }, + type: 'simpleSend', + }, +}; export const SimulationError = (args) => ( @@ -170,3 +184,20 @@ export const BusyNetwork = (args) => ( ); BusyNetwork.storyName = 'BusyNetwork'; + +export const SendingZeroAmount = (args) => ( + + + + + +); +SendingZeroAmount.storyName = 'SendingZeroAmount'; +SendingZeroAmount.args = { + txData: { + txParams: { + value: '0x0', + }, + type: 'simpleSend', + }, +}; diff --git a/ui/components/app/transaction-alerts/transaction-alerts.test.js b/ui/components/app/transaction-alerts/transaction-alerts.test.js index 8bbda90e37a2..0918801b77ab 100644 --- a/ui/components/app/transaction-alerts/transaction-alerts.test.js +++ b/ui/components/app/transaction-alerts/transaction-alerts.test.js @@ -1,10 +1,14 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; +import sinon from 'sinon'; import { SECURITY_PROVIDER_MESSAGE_SEVERITY } from '../../../../shared/constants/security-provider'; import { renderWithProvider } from '../../../../test/jest'; import { submittedPendingTransactionsSelector } from '../../../selectors/transactions'; import { useGasFeeContext } from '../../../contexts/gasFee'; 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 TransactionAlerts from './transaction-alerts'; jest.mock('../../../selectors/transactions', () => { @@ -20,12 +24,13 @@ function render({ componentProps = {}, useGasFeeContextValue = {}, submittedPendingTransactionsSelectorValue = null, + mockedStore = mockState, }) { useGasFeeContext.mockReturnValue(useGasFeeContextValue); submittedPendingTransactionsSelector.mockReturnValue( submittedPendingTransactionsSelectorValue, ); - const store = configureStore({}); + const store = configureStore(mockedStore); return renderWithProvider(, store); } @@ -44,6 +49,9 @@ describe('TransactionAlerts', () => { operator: '0x92a3b9773b1763efa556f55ccbeb20441962d9b2', }, }, + txParams: { + value: '0x1', + }, }, }, }); @@ -59,6 +67,9 @@ describe('TransactionAlerts', () => { reason: 'Some reason...', reason_header: 'Some reason header...', }, + txParams: { + value: '0x1', + }, }, }, }); @@ -79,6 +90,9 @@ describe('TransactionAlerts', () => { securityProviderResponse: { flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITY.NOT_MALICIOUS, }, + txParams: { + value: '0x1', + }, }, }, }); @@ -100,6 +114,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, hasSimulationError: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( @@ -116,6 +137,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, hasSimulationError: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect(getByText('I want to proceed anyway')).toBeInTheDocument(); }); @@ -127,7 +155,14 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, hasSimulationError: true, }, - componentProps: { setUserAcknowledgedGasMissing }, + componentProps: { + setUserAcknowledgedGasMissing, + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); fireEvent.click(getByText('I want to proceed anyway')); expect(setUserAcknowledgedGasMissing).toHaveBeenCalled(); @@ -141,7 +176,14 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, hasSimulationError: true, }, - componentProps: { userAcknowledgedGasMissing: true }, + componentProps: { + userAcknowledgedGasMissing: true, + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText('I want to proceed anyway'), @@ -156,6 +198,13 @@ describe('TransactionAlerts', () => { useGasFeeContextValue: { supportsEIP1559: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText( @@ -170,6 +219,13 @@ describe('TransactionAlerts', () => { const { getByText } = render({ useGasFeeContextValue: { supportsEIP1559: true }, submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }], + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( getByText('You have (1) pending transaction.'), @@ -185,6 +241,13 @@ describe('TransactionAlerts', () => { { some: 'transaction' }, { some: 'transaction' }, ], + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( getByText('You have (2) pending transactions.'), @@ -197,6 +260,13 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ useGasFeeContextValue: { supportsEIP1559: true }, submittedPendingTransactionsSelectorValue: [], + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText('You have (0) pending transactions.'), @@ -211,6 +281,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, balanceError: false, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect(queryByText('Insufficient funds.')).not.toBeInTheDocument(); }); @@ -223,6 +300,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, estimateUsed: 'low', }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( getByText('Future transactions will queue after this one.'), @@ -237,6 +321,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, estimateUsed: 'something_else', }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText('Future transactions will queue after this one.'), @@ -251,6 +342,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, isNetworkBusy: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( getByText( @@ -267,6 +365,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: true, isNetworkBusy: false, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText( @@ -285,6 +390,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: false, hasSimulationError: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( @@ -300,6 +412,13 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ useGasFeeContextValue: { supportsEIP1559: false }, submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }], + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText('You have (1) pending transaction.'), @@ -314,6 +433,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: false, balanceError: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect(queryByText('Insufficient funds.')).not.toBeInTheDocument(); }); @@ -326,6 +452,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: false, estimateUsed: 'low', }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText( @@ -342,6 +475,13 @@ describe('TransactionAlerts', () => { supportsEIP1559: false, isNetworkBusy: true, }, + componentProps: { + txData: { + txParams: { + value: '0x1', + }, + }, + }, }); expect( queryByText( @@ -351,4 +491,85 @@ describe('TransactionAlerts', () => { }); }); }); + + describe('when sending zero amount it should display a warning', () => { + it('should display alert if sending zero tokens', () => { + // Mock + const testTokenData = { + args: 'decoded-param', + }; + const testTokenValue = '0'; + + const parseStandardTokenTransactionDataStub = sinon.stub( + txUtil, + 'parseStandardTokenTransactionData', + ); + const getTokenValueStub = sinon.stub( + metamaskControllerUtils, + 'getTokenValueParam', + ); + + parseStandardTokenTransactionDataStub.callsFake(() => testTokenData); + getTokenValueStub.callsFake(() => testTokenValue); + // render + const { getByText } = render({ + componentProps: { + txData: { + txParams: { + value: '0x0', + }, + type: 'transfer', + }, + tokenSymbol: 'DAI', + }, + }); + // assert + expect(getByText('You are sending 0 DAI.')).toBeInTheDocument(); + }); + + it('should display alert if sending zero of native currency', () => { + const { getByText } = render({ + componentProps: { + txData: { + txParams: { + value: '0x0', + }, + type: 'simpleSend', + }, + tokenSymbol: undefined, + }, + }); + expect(getByText('You are sending 0 ETH.')).toBeInTheDocument(); + }); + }); + + describe('when sending amount different than zero should not display alert', () => { + it('should not display alerts if sending amount different than zero in native currency', () => { + const { queryByText } = render({ + componentProps: { + txData: { + txParams: { + value: '0x5af3107a4000', + }, + }, + tokenSymbol: undefined, + }, + }); + expect(queryByText('You are sending 0 ETH.')).not.toBeInTheDocument(); + }); + + it('should not display alerts if sending amount different than zero in tokens', () => { + const { queryByText } = render({ + componentProps: { + txData: { + txParams: { + value: '0x0', + }, + }, + tokenSymbol: 'DAI', + }, + }); + expect(queryByText('You are sending 0 DAI.')).not.toBeInTheDocument(); + }); + }); }); diff --git a/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap b/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap index 7e905833d834..bf7f3f28c1f8 100644 --- a/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap +++ b/ui/pages/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap @@ -329,6 +329,21 @@ exports[`ConfirmSendEther should render correct information for for confirm send

+
+ +
+

+ You are sending 0 ETH. +

+
+
); } diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js index f610f6b92214..6cd2da349f3b 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -145,6 +145,7 @@ export default class ConfirmTransactionBase extends Component { isNoteToTraderSupported: PropTypes.bool, isMainBetaFlask: PropTypes.bool, displayAccountBalanceHeader: PropTypes.bool, + tokenSymbol: PropTypes.string, }; state = { @@ -324,6 +325,7 @@ export default class ConfirmTransactionBase extends Component { nativeCurrency, isBuyableChain, useCurrencyRateCheck, + tokenSymbol, } = this.props; const { t } = this.context; const { userAcknowledgedGasMissing } = this.state; @@ -461,6 +463,7 @@ export default class ConfirmTransactionBase extends Component { networkName={networkName} type={txData.type} isBuyableChain={isBuyableChain} + tokenSymbol={tokenSymbol} /> Date: Mon, 2 Oct 2023 18:31:36 +0100 Subject: [PATCH 019/219] [MMI] Deletes unnecessary logic for mmi (#20834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Explanation MMI no longer needs this bit of logic 🧹 ## Pre-merge author checklist - [x] I've clearly explained: - [x] What problem this PR is solving - [x] How this problem was solved - [x] How reviewers can test my changes - [x] Sufficient automated test coverage has been added --- app/scripts/controllers/mmi-controller.js | 43 +----------- .../rpc-method-middleware/handlers/index.js | 2 - .../handlers/institutional/mmi-open-swaps.js | 68 ------------------- app/scripts/metamask-controller.js | 3 - 4 files changed, 2 insertions(+), 114 deletions(-) delete mode 100644 app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-swaps.js diff --git a/app/scripts/controllers/mmi-controller.js b/app/scripts/controllers/mmi-controller.js index d6e980e1be95..969b26427a13 100644 --- a/app/scripts/controllers/mmi-controller.js +++ b/app/scripts/controllers/mmi-controller.js @@ -1,7 +1,6 @@ import EventEmitter from 'events'; import log from 'loglevel'; import { captureException } from '@sentry/browser'; -import { isEqual } from 'lodash'; import { CUSTODIAN_TYPES } from '@metamask-institutional/custody-keyring'; import { updateCustodianTransactions, @@ -11,17 +10,10 @@ import { REFRESH_TOKEN_CHANGE_EVENT, INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, } from '@metamask-institutional/sdk'; -import { - handleMmiPortfolio, - setDashboardCookie, -} from '@metamask-institutional/portfolio-dashboard'; +import { handleMmiPortfolio } from '@metamask-institutional/portfolio-dashboard'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import { CHAIN_IDS } from '../../../shared/constants/network'; -import { - BUILD_QUOTE_ROUTE, - CONNECT_HARDWARE_ROUTE, -} from '../../../ui/helpers/constants/routes'; -import { previousValueComparator } from '../lib/util'; +import { CONNECT_HARDWARE_ROUTE } from '../../../ui/helpers/constants/routes'; import { getPermissionBackgroundApiMethods } from './permissions'; export default class MMIController extends EventEmitter { @@ -63,17 +55,6 @@ export default class MMIController extends EventEmitter { }); } - this.preferencesController.store.subscribe( - previousValueComparator(async (prevState, currState) => { - const { identities: prevIdentities } = prevState; - const { identities: currIdentities } = currState; - if (isEqual(prevIdentities, currIdentities)) { - return; - } - await this.prepareMmiPortfolio(); - }, this.preferencesController.store.getState()), - ); - this.signatureController.hub.on( 'personal_sign:signed', async ({ signature, messageId }) => { @@ -584,20 +565,6 @@ export default class MMIController extends EventEmitter { }); } - async prepareMmiPortfolio() { - if (!process.env.IN_TEST) { - try { - const mmiDashboardData = await this.handleMmiDashboardData(); - const cookieSetUrls = - this.mmiConfigurationController.store.mmiConfiguration?.portfolio - ?.cookieSetUrls || []; - setDashboardCookie(mmiDashboardData, cookieSetUrls); - } catch (error) { - console.error(error); - } - } - } - async newUnsignedMessage(msgParams, req, version) { // The code path triggered by deferSetAsSigned: true is for custodial accounts const accountDetails = this.custodyController.getAccountDetails( @@ -673,12 +640,6 @@ export default class MMIController extends EventEmitter { return true; } - async handleMmiOpenSwaps(origin, address, chainId) { - await this.setAccountAndNetwork(origin, address, chainId); - this.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE); - return true; - } - async handleMmiOpenAddHardwareWallet() { await this.appStateController.getUnlockPromise(true); this.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/index.js b/app/scripts/lib/rpc-method-middleware/handlers/index.js index 126da76f9de4..a4c02fa71d66 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/index.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/index.js @@ -11,7 +11,6 @@ import watchAsset from './watch-asset'; import mmiSupported from './institutional/mmi-supported'; import mmiAuthenticate from './institutional/mmi-authenticate'; import mmiPortfolio from './institutional/mmi-portfolio'; -import mmiOpenSwaps from './institutional/mmi-open-swaps'; import mmiCheckIfTokenIsPresent from './institutional/mmi-check-if-token-is-present'; import mmiSetAccountAndNetwork from './institutional/mmi-set-account-and-network'; import mmiOpenAddHardwareWallet from './institutional/mmi-open-add-hardware-wallet'; @@ -30,7 +29,6 @@ const handlers = [ mmiAuthenticate, mmiSupported, mmiPortfolio, - mmiOpenSwaps, mmiCheckIfTokenIsPresent, mmiSetAccountAndNetwork, mmiOpenAddHardwareWallet, diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-swaps.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-swaps.js deleted file mode 100644 index f0cdbd026c24..000000000000 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-open-swaps.js +++ /dev/null @@ -1,68 +0,0 @@ -import { ethErrors } from 'eth-rpc-errors'; -import { RPC_ALLOWED_ORIGINS } from '@metamask-institutional/rpc-allowlist'; -import { MESSAGE_TYPE } from '../../../../../../shared/constants/app'; - -const mmiOpenSwaps = { - methodNames: [MESSAGE_TYPE.MMI_OPEN_SWAPS], - implementation: mmiOpenSwapsHandler, - hookNames: { - handleMmiOpenSwaps: true, - }, -}; -export default mmiOpenSwaps; - -/** - * @typedef {object} MmiOpenSwapsOptions - * @property {Function} handleMmiOpenSwaps - The metmaskinsititutional_open_swaps method implementation. - */ - -/** - * @typedef {object} MmiOpenSwapsParam - * @property {string} service - The service to which we are authenticating, e.g. 'codefi-compliance' - * @property {object} token - The token used to authenticate - */ - -/** - * @param {import('json-rpc-engine').JsonRpcRequest} req - The JSON-RPC request object. - * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. - * @param {Function} _next - The json-rpc-engine 'next' callback. - * @param {Function} end - The json-rpc-engine 'end' callback. - * @param {WatchAssetOptions} options - */ -async function mmiOpenSwapsHandler( - req, - res, - _next, - end, - { handleMmiOpenSwaps }, -) { - try { - let validUrl = false; - // if (!RPC_ALLOWED_ORIGINS[MESSAGE_TYPE.MMI_PORTFOLIO].includes(req.origin)) { - RPC_ALLOWED_ORIGINS[MESSAGE_TYPE.MMI_PORTFOLIO].forEach((regexp) => { - // eslint-disable-next-line require-unicode-regexp - if (regexp.test(req.origin)) { - validUrl = true; - } - }); - // eslint-disable-next-line no-negated-condition - if (!validUrl) { - throw new Error('Unauthorized'); - } - - if (!req.params?.[0] || typeof req.params[0] !== 'object') { - return end( - ethErrors.rpc.invalidParams({ - message: `Expected single, object parameter. Received:\n${JSON.stringify( - req.params, - )}`, - }), - ); - } - const { address, network } = req.params[0]; - res.result = await handleMmiOpenSwaps(req.origin, address, network); - return end(); - } catch (error) { - return end(error); - } -} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 27ebd056f8f3..684c64249864 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4347,9 +4347,6 @@ export default class MetamaskController extends EventEmitter { handleMmiDashboardData: this.mmiController.handleMmiDashboardData.bind( this.mmiController, ), - handleMmiOpenSwaps: this.mmiController.handleMmiOpenSwaps.bind( - this.mmiController, - ), handleMmiSetAccountAndNetwork: this.mmiController.setAccountAndNetwork.bind(this.mmiController), handleMmiOpenAddHardwareWallet: From ab0fdeee13645fb65148bbe4976871058d0e065b Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Tue, 3 Oct 2023 10:58:15 +0100 Subject: [PATCH 020/219] Add privacy setting to disable external name sources (#21045) --- app/_locales/en/messages.json | 9 +++ app/scripts/controllers/preferences.js | 16 ++++ app/scripts/controllers/preferences.test.js | 17 +++++ app/scripts/metamask-controller.js | 15 +++- .../__snapshots__/security-tab.test.js.snap | 75 +++++++++++++++++++ .../security-tab/security-tab.component.js | 56 ++++++++++++++ .../security-tab/security-tab.container.js | 14 ++++ ui/store/actions.ts | 10 +++ yarn.lock | 20 ++--- 9 files changed, 219 insertions(+), 13 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 77c8e3ec8f06..79a76e98dbdf 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1616,6 +1616,12 @@ "externalExtension": { "message": "External extension" }, + "externalNameSourcesSetting": { + "message": "Suggest address names" + }, + "externalNameSourcesSettingDescription": { + "message": "We pull data from third parties like Etherscan, Infura, and Lens Protocol, to suggest names for addresses on signatures requests. Turning on name suggestions exposes your IP address to these third parties." + }, "failed": { "message": "Failed" }, @@ -3973,6 +3979,9 @@ "settingsSearchMatchingNotFound": { "message": "No matching results found." }, + "settingsSubHeadingSignatures": { + "message": "Signature requests" + }, "show": { "message": "Show" }, diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index ac209b576480..908e3d985855 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -106,6 +106,9 @@ export default class PreferencesController { snapsAddSnapAccountModalDismissed: false, ///: END:ONLY_INCLUDE_IN isLineaMainnetReleased: false, + ///: BEGIN:ONLY_INCLUDE_IN(petnames) + useExternalNameSources: true, + ///: END:ONLY_INCLUDE_IN ...opts.initState, }; @@ -261,6 +264,19 @@ export default class PreferencesController { } ///: END:ONLY_INCLUDE_IN + ///: BEGIN:ONLY_INCLUDE_IN(petnames) + /** + * Setter for the `useExternalNameSources` property + * + * @param {boolean} useExternalNameSources - Whether or not to use external name providers in the name controller. + */ + setUseExternalNameSources(useExternalNameSources) { + this.store.updateState({ + useExternalNameSources, + }); + } + ///: END:ONLY_INCLUDE_IN + /** * Setter for the `advancedGasFee` property * diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index 106871aa5d71..a46730c8c9ab 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -419,4 +419,21 @@ describe('preferences controller', () => { }); }); }); + + ///: BEGIN:ONLY_INCLUDE_IN(petnames) + describe('setUseExternalNameSources', () => { + it('should default to true', () => { + expect( + preferencesController.store.getState().useExternalNameSources, + ).toStrictEqual(true); + }); + + it('should set the useExternalNameSources property in state', () => { + preferencesController.setUseExternalNameSources(false); + expect( + preferencesController.store.getState().useExternalNameSources, + ).toStrictEqual(false); + }); + }); + ///: END:ONLY_INCLUDE_IN }); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 684c64249864..6a0a2edc2f19 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1537,6 +1537,9 @@ export default class MetamaskController extends EventEmitter { ); ///: BEGIN:ONLY_INCLUDE_IN(petnames) + const isExternalNameSourcesEnabled = () => + this.preferencesController.store.getState().useExternalNameSources; + this.nameController = new NameController({ messenger: this.controllerMessenger.getRestricted({ name: 'NameController', @@ -1548,9 +1551,9 @@ export default class MetamaskController extends EventEmitter { this.ensController, ), }), - new EtherscanNameProvider({}), - new TokenNameProvider({}), - new LensNameProvider(), + new EtherscanNameProvider({ isEnabled: isExternalNameSourcesEnabled }), + new TokenNameProvider({ isEnabled: isExternalNameSourcesEnabled }), + new LensNameProvider({ isEnabled: isExternalNameSourcesEnabled }), new SnapsNameProvider({ messenger: this.controllerMessenger.getRestricted({ name: 'SnapsNameProvider', @@ -2404,6 +2407,12 @@ export default class MetamaskController extends EventEmitter { preferencesController, ), ///: END:ONLY_INCLUDE_IN + ///: BEGIN:ONLY_INCLUDE_IN(petnames) + setUseExternalNameSources: + preferencesController.setUseExternalNameSources.bind( + preferencesController, + ), + ///: END:ONLY_INCLUDE_IN setIpfsGateway: preferencesController.setIpfsGateway.bind( preferencesController, ), diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index 6aabc18d4813..7c52b3c8e7cc 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -1313,6 +1313,81 @@ exports[`Security Tab should match snapshot 1`] = `
+ + Signature requests + +
+
+
+ + Suggest address names + +
+ We pull data from third parties like Etherscan, Infura, and Lens Protocol, to suggest names for addresses on signatures requests. Turning on name suggestions exposes your IP address to these third parties. +
+
+
+