diff --git a/.metamaskrc.dist b/.metamaskrc.dist index d5b8114a8f16..17350a99aaf5 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -7,6 +7,7 @@ PUBNUB_PUB_KEY= PUBNUB_SUB_KEY= PORTFOLIO_URL= TRANSACTION_SECURITY_PROVIDER= +MULTICHAIN= ; Set this to test changes to the phishing warning page. PHISHING_WARNING_PAGE_URL= diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 65bb7fb4c39a..3f133fcba5e3 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -1179,9 +1179,12 @@ const state = { accounts: [ '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', - '0x9d0ba4ddac06032527b140912ec808ab9451b788', ], }, + { + type: KeyringType.ledger, + accounts: ['0x9d0ba4ddac06032527b140912ec808ab9451b788'], + }, ], networkConfigurations: { 'test-networkConfigurationId-1': { diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f30379c39cc..aca89dc0deeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [10.26.2] +### Changed +- Sign in with Ethereum: re-enable warning UI for mismatched domains / disable domain binding ([#18200](https://github.com/MetaMask/metamask-extension/pull/18200)) + ## [10.26.1] ### Fixed - Fix main build by modifying desktop build steps ([#18112](https://github.com/MetaMask/metamask-extension/pull/18112)) @@ -3532,7 +3536,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/v10.26.1...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.26.2...HEAD +[10.26.2]: https://github.com/MetaMask/metamask-extension/compare/v10.26.1...v10.26.2 [10.26.1]: https://github.com/MetaMask/metamask-extension/compare/v10.26.0...v10.26.1 [10.26.0]: https://github.com/MetaMask/metamask-extension/compare/v10.25.0...v10.26.0 [10.25.0]: https://github.com/MetaMask/metamask-extension/compare/v10.24.2...v10.25.0 diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index a92a7de6aee1..a920e0116afe 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -329,9 +329,6 @@ "message": "$1 erlauben, bis zu dem folgenden Betrag abzuheben und auszugeben:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Gehen Sie immer mit Sorgfalt vor, bevor Sie irgendwelche Anfragen genehmigen." - }, "amount": { "message": "Betrag" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Verwenden Sie die OpenSea's API, um NFT-Daten abzurufen. Die NFT-Auto-Erkennung basiert auf der OpenSea's API und wird nicht verfügbar sein, wenn diese deaktiviert ist." }, - "enableOpenSeaSecurityProvider": { - "message": "Sicherheitsanbieter aktivieren" - }, "enableSmartTransactions": { "message": "Intelligente Transaktionen ermöglichen" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Allgemein" }, - "getWarningsFromOpenSea": { - "message": "Erhalten Sie Warnungen von OpenSea, wenn Sie eine bekannte bösartige Anfrage erhalten." - }, "goBack": { "message": "Zurück" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Nicht genügend Gas" }, - "notNow": { - "message": "Nicht jetzt" - }, "notifications": { "message": "Benachrichtigungen" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "OpenSea-Sicherheitsanbieter" - }, - "openSeaDescription": { - "message": "OpenSea ist der erset Sicherheitsanbieter für diese Funktion. Mehr Anbieter folgen in Kürze!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Nicht verbunden" }, - "staySafeWithOpenSea": { - "message": "Mit OpenSea sicher bleiben" - }, "step1LatticeWallet": { "message": "Verbinden Sie Ihr Lattice1" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index bb17e4e33696..6a97d9d71ceb 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -329,9 +329,6 @@ "message": "Επιτρέψτε στο $1 να κάνει ανάληψη και να ξοδέψει μέχρι το ακόλουθο ποσό:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Φροντίστε πάντα να κάνετε τη δική σας επιμελή έρευνα προτού εγκρίνετε οποιαδήποτε αιτήματα." - }, "amount": { "message": "Ποσό" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Χρησιμοποιήστε το API OpenSea για λήψη δεδομένων NFT. Η αυτόματη ανίχνευση NFT βασίζεται στο API του OpenSea, και δεν θα είναι διαθέσιμη όταν αυτό είναι απενεργοποιημένο." }, - "enableOpenSeaSecurityProvider": { - "message": "Ενεργοποίηση παρόχου ασφάλειας" - }, "enableSmartTransactions": { "message": "Ενεργοποίηση Έξυπνων Συναλλαγών" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Γενικά" }, - "getWarningsFromOpenSea": { - "message": "Λάβετε προειδοποιήσεις από το OpenSea όποτε λαβαίνετε ένα αναγνωρισμένο κακόβουλο αίτημα." - }, "goBack": { "message": "Μετάβαση Πίσω" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Δεν Υπάρχει Αρκετό τέλος συναλλαγής" }, - "notNow": { - "message": "Όχι τώρα" - }, "notifications": { "message": "Ειδοποιήσεις" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Πάροχος ασφάλειας OpenSea" - }, - "openSeaDescription": { - "message": "Το OpenSea είναι ο πρώτος πάροχος ασφάλειας για αυτή τη λειτουργία. Σύντομα θα προστεθούν και άλλοι πάροχοι!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Δεν έχει συνδεθεί" }, - "staySafeWithOpenSea": { - "message": "Μείνετε ασφαλείς με το OpenSea" - }, "step1LatticeWallet": { "message": "Συνδέστε το Lattice1 σας" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 1dfc9ec31738..1aca00b814d9 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -171,6 +171,9 @@ "addANickname": { "message": "Add a nickname" }, + "addAccount": { + "message": "Add account" + }, "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, @@ -339,9 +342,6 @@ "message": "Allow $1 to withdraw and spend up to the following amount:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Always be sure to do your own due diligence before approving any requests." - }, "amount": { "message": "Amount" }, @@ -1246,9 +1246,6 @@ "enableOpenSeaAPIDescription": { "message": "Use OpenSea's API to fetch NFT data. NFT auto-detection relies on OpenSea's API, and will not be available when this is turned off." }, - "enableOpenSeaSecurityProvider": { - "message": "Enable security provider" - }, "enableSmartTransactions": { "message": "Enable smart transactions" }, @@ -1552,9 +1549,6 @@ "general": { "message": "General" }, - "getWarningsFromOpenSea": { - "message": "Get warnings from OpenSea whenever you receive a known malicious request." - }, "goBack": { "message": "Go back" }, @@ -1573,6 +1567,9 @@ "hardware": { "message": "Hardware" }, + "hardwareWallet": { + "message": "Hardware wallet" + }, "hardwareWalletConnected": { "message": "Hardware wallet connected" }, @@ -2326,9 +2323,6 @@ "notEnoughGas": { "message": "Not enough gas" }, - "notNow": { - "message": "Not now" - }, "notifications": { "message": "Notifications" }, @@ -2689,12 +2683,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "OpenSea security provider" - }, - "openSeaDescription": { - "message": "OpenSea is the first security provider for this feature. More providers coming soon!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3703,9 +3691,6 @@ "statusNotConnected": { "message": "Not connected" }, - "staySafeWithOpenSea": { - "message": "Stay safe with OpenSea" - }, "step1LatticeWallet": { "message": "Connect your Lattice1" }, @@ -4611,6 +4596,9 @@ "message": "View $1 on Etherscan", "description": "$1 is the action type. e.g (Account, Transaction, Swap)" }, + "viewOnExplorer": { + "message": "View on explorer" + }, "viewOnOpensea": { "message": "View on Opensea" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index e987868a466d..a2dc1a9fe61b 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -329,9 +329,6 @@ "message": "Permitir que se retire $1 y gastar hasta el siguiente importe:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Asegúrese siempre de hacer su propia diligencia debida antes de aprobar cualquier solicitud." - }, "amount": { "message": "Importe" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Utilice la API de OpenSea para obtener los datos de NFT. La autodetección de NFT depende de la API de OpenSea y no estará disponible si la API está desactivada." }, - "enableOpenSeaSecurityProvider": { - "message": "Habilitar proveedor de seguridad" - }, "enableSmartTransactions": { "message": "Habilitar transacciones inteligentes" }, @@ -1519,9 +1513,6 @@ "general": { "message": "General" }, - "getWarningsFromOpenSea": { - "message": "Reciba advertencias de OpenSea cada vez que reciba una solicitud maliciosa conocida." - }, "goBack": { "message": "Volver" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "No hay gas suficiente" }, - "notNow": { - "message": "Por ahora no" - }, "notifications": { "message": "Notificaciones" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Proveedor de seguridad de OpenSea" - }, - "openSeaDescription": { - "message": "OpenSea es el primer proveedor de seguridad para esta función. ¡Más proveedores próximamente!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "No conectado" }, - "staySafeWithOpenSea": { - "message": "Manténgase seguro con OpenSea" - }, "step1LatticeWallet": { "message": "Conecte su Lattice1" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index e2f21611a001..1906a3a1f8f8 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -329,9 +329,6 @@ "message": "Permettre à $1 de retirer et de dépenser jusqu’au montant suivant :", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Vous devez faire preuve de diligence raisonnable avant d’approuver toute demande." - }, "amount": { "message": "Montant" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Utilisez l’API OpenSea pour récupérer les données de NFT. La détection automatique de NFT repose sur l’API OpenSea et ne sera pas disponible si elle est désactivée." }, - "enableOpenSeaSecurityProvider": { - "message": "Activer le fournisseur de services de sécurité" - }, "enableSmartTransactions": { "message": "Activer les transactions intelligentes" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Général" }, - "getWarningsFromOpenSea": { - "message": "OpenSea vous avertira dès que vous recevrez une requête malveillante." - }, "goBack": { "message": "Retour" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Pas assez de gaz" }, - "notNow": { - "message": "Pas maintenant" - }, "notifications": { "message": "Notifications" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Fournisseur de services de sécurité OpenSea" - }, - "openSeaDescription": { - "message": "OpenSea est le premier fournisseur de services de sécurité à offrir cette fonctionnalité. D’autres fournisseurs seront bientôt ajoutés !" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Non connecté" }, - "staySafeWithOpenSea": { - "message": "Restez en sécurité avec OpenSea" - }, "step1LatticeWallet": { "message": "Connectez votre Lattice1" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 2c2da2a6ebf0..03307179f588 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -329,9 +329,6 @@ "message": "$1 को निम्नलिखित तक राशि निकालने और खर्च करने की अनुमति दें:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "किसी भी अनुरोध को मंजूरी देने से पहले हमेशा अपनी खुद की उचित कर्मठता सुनिश्चित करें।" - }, "amount": { "message": "राशि" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "NFT डेटा लाने के लिए OpenSea के API का उपयोग करें। NFT ऑटो-डिटेक्शन OpenSea के API पर निर्भर करता है, और इसके बंद होने पर उपलब्ध नहीं होगा।" }, - "enableOpenSeaSecurityProvider": { - "message": "सुरक्षा प्रदाता सक्षम करें" - }, "enableSmartTransactions": { "message": "स्मार्ट लेनदेन को सक्षम करें" }, @@ -1519,9 +1513,6 @@ "general": { "message": "सामान्य" }, - "getWarningsFromOpenSea": { - "message": "जब भी आप एक ज्ञात दुर्भावनापूर्ण अनुरोध प्राप्त करते हैं, OpenSea से चेतावनी प्राप्त करें।" - }, "goBack": { "message": "वापस जाएं" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "पर्याप्त गैस नहीं" }, - "notNow": { - "message": "अभी नहीं" - }, "notifications": { "message": "सूचनाएं" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (बीटा)" }, - "openSeaAltText": { - "message": "OpenSea सुरक्षा प्रदाता" - }, - "openSeaDescription": { - "message": "OpenSea इस सुविधा के लिए पहला सुरक्षा प्रदाता है।अधिक प्रदाता जल्द ही आ रहे हैं!" - }, "openSeaNew": { "message": "ओपनसी" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "कनेक्ट नहीं है" }, - "staySafeWithOpenSea": { - "message": "OpenSea के साथ सुरक्षित रहें" - }, "step1LatticeWallet": { "message": "अपना Lattice1 कनेक्ट करें" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 370ffdf72441..07aae2ce06e4 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -329,9 +329,6 @@ "message": "Izinkan $1 untuk ditarik dan digunakan hingga jumlah berikut:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Selalu pastikan untuk melakukan uji tuntas Anda sendiri sebelum menyetujui permintaan apa pun." - }, "amount": { "message": "Jumlah" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Gunakan API OpenSea untuk mengambil data NFT. Deteksi otomatis NFT bergantung pada API OpenSea, dan tidak akan tersedia saat API ditutup." }, - "enableOpenSeaSecurityProvider": { - "message": "Aktifkan penyedia keamanan" - }, "enableSmartTransactions": { "message": "Aktifkan transaksi pintar" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Umum" }, - "getWarningsFromOpenSea": { - "message": "Dapatkan peringatan dari OpenSea setiap kali Anda menerima permintaan berbahaya yang diketahui." - }, "goBack": { "message": "Kembali" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Gas tidak cukup" }, - "notNow": { - "message": "Tidak sekarang" - }, "notifications": { "message": "Notifikasi" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Penyedia keamanan OpenSea" - }, - "openSeaDescription": { - "message": "OpenSea merupakan penyedia keamanan pertama untuk fitur ini. Penyedia lainnya akan segera hadir!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Tidak terhubung" }, - "staySafeWithOpenSea": { - "message": "Tetap aman bersama OpenSea" - }, "step1LatticeWallet": { "message": "Hubungkan Lattice1 Anda" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 57d53796f4ca..3320c1442012 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -329,9 +329,6 @@ "message": "$1に以下の額までの引き出しと使用を許可します。", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "リクエストを承認する前に、必ず独自のデューデリジェンスを行ってください。" - }, "amount": { "message": "金額" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "OpenSea APIを使用してNFTデータを取得します。NFT自動検出はOpenSea APIを使用するため、この設定をオフにすると利用できなくなります。" }, - "enableOpenSeaSecurityProvider": { - "message": "セキュリティプロバイダーを有効にする" - }, "enableSmartTransactions": { "message": "スマートトランザクションを有効にする" }, @@ -1519,9 +1513,6 @@ "general": { "message": "一般" }, - "getWarningsFromOpenSea": { - "message": "既知の悪質なリクエストを受けた際に OpenSea から警告を受けられます。" - }, "goBack": { "message": "戻る" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "ガスが不足しています" }, - "notNow": { - "message": "また後で" - }, "notifications": { "message": "通知" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (ベータ)" }, - "openSeaAltText": { - "message": "OpenSea セキュリティプロバイダー" - }, - "openSeaDescription": { - "message": "OpenSea は、この機能を提供する最初のセキュリティプロバイダーです。他のプロバイダーも近日追加予定です!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "未接続" }, - "staySafeWithOpenSea": { - "message": "OpenSea で安全を確保" - }, "step1LatticeWallet": { "message": "Lattice1を接続する" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 2826089ef4ab..01f792772edd 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -329,9 +329,6 @@ "message": "$1에서 다음 금액까지 인출 및 지출하도록 허용:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "모든 요청을 승인하기 전에 주의 깊게 직접 확인하세요." - }, "amount": { "message": "금액" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "OpenSea의 API를 사용하여 NFT 데이터를 가져옵니다. NFT 자동 감지는 OpenSea의 API에 의존하며 이 API가 꺼져 있으면 사용할 수 없습니다." }, - "enableOpenSeaSecurityProvider": { - "message": "보안 업체 활성화" - }, "enableSmartTransactions": { "message": "스마트 트랜잭션 활성화" }, @@ -1519,9 +1513,6 @@ "general": { "message": "일반" }, - "getWarningsFromOpenSea": { - "message": "알려진 악성 요청을 받을 때마다 OpenSea로부터 경고 알림을 받으세요." - }, "goBack": { "message": "뒤로 가기" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "가스 부족" }, - "notNow": { - "message": "나중에" - }, "notifications": { "message": "알림" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (베타)" }, - "openSeaAltText": { - "message": "OpenSea 보안 업체" - }, - "openSeaDescription": { - "message": "이 기능의 1차 보안 업체는 OpenSea입니다. 곧 다른 보안 업체도 마련하겠습니다!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "연결되지 않음" }, - "staySafeWithOpenSea": { - "message": "OpenSea로 보안을 유지하세요" - }, "step1LatticeWallet": { "message": "Lattice1을 연결하세요." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 46fc9910facf..544cc1998a2d 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -329,9 +329,6 @@ "message": "Permitir que $1 saque e gaste até o seguinte valor:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Certifique-se sempre de fazer sua devida diligência antes de aprovar qualquer solicitação." - }, "amount": { "message": "Valor" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Use a API OpenSea para recuperar dados de NFTs. A detecção automática de NFTs depende da API OpenSea e não estará disponível quando essa opção estiver desativada." }, - "enableOpenSeaSecurityProvider": { - "message": "Habilitar provedor de segurança" - }, "enableSmartTransactions": { "message": "Ativar transações inteligentes" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Geral" }, - "getWarningsFromOpenSea": { - "message": "Receba avisos do OpenSea sempre que receber uma solicitação maliciosa conhecida." - }, "goBack": { "message": "Voltar" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Não há gás suficiente" }, - "notNow": { - "message": "Agora não" - }, "notifications": { "message": "Notificações" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Provedor de segurança OpenSea" - }, - "openSeaDescription": { - "message": "O OpenSea é o primeiro provedor de segurança para este recurso. Mais provedores chegarão em breve!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Não conectado" }, - "staySafeWithOpenSea": { - "message": "Fique seguro com o OpenSea" - }, "step1LatticeWallet": { "message": "Conecte seu Lattice1" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index d906e91fec79..89293012dc4a 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -329,9 +329,6 @@ "message": "Разрешить $1 снять и потратить до следующей суммы:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Всегда обязательно проводите собственную комплексную проверку, прежде чем утверждать какие-либо запросы." - }, "amount": { "message": "Сумма" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Используйте API OpenSea для получения данных NFT. Для автоматического обнаружения NFT используется API OpenSea, и такое обнаружение будет недоступно, если этот API отключен." }, - "enableOpenSeaSecurityProvider": { - "message": "Включить поставщика услуг безопасности" - }, "enableSmartTransactions": { "message": "Включить смарт-транзакции" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Общее" }, - "getWarningsFromOpenSea": { - "message": "Получайте предупреждения от OpenSea всякий раз, когда вы получаете заведомо вредоносный запрос." - }, "goBack": { "message": "Назад" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Недостаточно газа" }, - "notNow": { - "message": "Не сейчас" - }, "notifications": { "message": "Уведомления" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (бета-версия)" }, - "openSeaAltText": { - "message": "Поставщик услуг безопасности OpenSea" - }, - "openSeaDescription": { - "message": "OpenSea является первым поставщиком услуг безопасности для этой функции. Скоро появятся новые поставщики!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Не подключено" }, - "staySafeWithOpenSea": { - "message": "Оставайтесь в безопасности с OpenSea" - }, "step1LatticeWallet": { "message": "Подключите Lattice1" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 334c05393a01..ef944e308713 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -329,9 +329,6 @@ "message": "Payagan ang $1 na mag-withdraw at gastusin ang sumusunod na halaga:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Laging siguraduhin na gawin ang iyong sariling angkop na pagsusumikap bago aprubahan ang anumang mga kahilingan." - }, "amount": { "message": "Halaga" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Gamitin ang API ng Opensea upang kunin ang NFT data. ang NFT auto-detection ay umaasa sa API ng OpenSea, at hindi magiging available kapag ito ay isinara." }, - "enableOpenSeaSecurityProvider": { - "message": "Paganahin ang tagapagbigay ng seguridad" - }, "enableSmartTransactions": { "message": "Payagan ang mga smart transaction" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Pangkalahatan" }, - "getWarningsFromOpenSea": { - "message": "Makakuha ng mga babala mula sa OpenSea sa tuwing makakatanggap ka ng kilalang malisyosong kahilingan." - }, "goBack": { "message": "Bumalik" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Hindi Sapat ang Gas" }, - "notNow": { - "message": "Hindi ngayon" - }, "notifications": { "message": "Mga Abiso" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea na (Beta)" }, - "openSeaAltText": { - "message": "Tagapagbigay ng seguridad ng OpenSea" - }, - "openSeaDescription": { - "message": "Ang OpenSea ay ang unang tagapagbigay ng seguridad para sa tampok na ito. Marami pang mga provider ang paparating!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Hindi konektado" }, - "staySafeWithOpenSea": { - "message": "Manatiling ligtas sa OpenSea" - }, "step1LatticeWallet": { "message": "Ikonekta ang iyong Lattice1" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 3b85a7dc8da7..eb3841356acd 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -329,9 +329,6 @@ "message": "$1 için şu tutara kadar para çekme ve harcama izni ver:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Talepleri onaylamadan önce her zaman bunu gerekli özeni göstererek yaptığınızdan emin olun." - }, "amount": { "message": "Tutar" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "NFT verilerini almak için OpenSea API'sini kullanın. NFT otomatik algılama OpenSea API'ye dayalıdır ve bu kapatılırsa mevcut olmayacaktır." }, - "enableOpenSeaSecurityProvider": { - "message": "Güvenlik sağlayıcısını etkinleştir" - }, "enableSmartTransactions": { "message": "Akıllı işlemleri etkinleştir" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Genel" }, - "getWarningsFromOpenSea": { - "message": "Bilinen bir kötü amaçlı talep aldığınızda OpenSea'den uyarı alın." - }, "goBack": { "message": "Geri git" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Yeterli gaz yok" }, - "notNow": { - "message": "Şimdi değil" - }, "notifications": { "message": "Bildirimler" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "OpenSea güvenlik sağlayıcısı" - }, - "openSeaDescription": { - "message": "OpenSea bu özellik için birinci güvenlik sağlayıcısıdır. Daha fazla sağlayıcı çok yakında gelecektir!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Bağlanmadı" }, - "staySafeWithOpenSea": { - "message": "OpenSea ile güvende kalın" - }, "step1LatticeWallet": { "message": "Lattice1'inizi bağlayın" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 00897f945df9..1aebc6b67c6c 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -329,9 +329,6 @@ "message": "Cho phép $1 rút và chi tiêu tối đa số tiền sau đây:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Nhớ luôn tự thẩm định trước khi phê duyệt bất kỳ yêu cầu nào." - }, "amount": { "message": "Số tiền" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Sử dụng API của OpenSea để tìm nạp dữ liệu NFT. Tính năng tự động phát hiện NFT dựa vào API của OpenSea và sẽ không khả dụng nếu tính năng này bị tắt." }, - "enableOpenSeaSecurityProvider": { - "message": "Kích hoạt nhà cung cấp bảo mật" - }, "enableSmartTransactions": { "message": "Bật giao dịch thông minh" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Chung" }, - "getWarningsFromOpenSea": { - "message": "Nhận cảnh báo từ OpenSea bất cứ khi nào bạn nhận được một yêu cầu độc hại đã biết." - }, "goBack": { "message": "Quay Lại" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Không đủ gas" }, - "notNow": { - "message": "Không phải bây giờ" - }, "notifications": { "message": "Thông báo" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Nhà cung cấp bảo mật OpenSea" - }, - "openSeaDescription": { - "message": "OpenSea là nhà cung cấp bảo mật đầu tiên cho tính năng này. Sắp có thêm nhiều nhà cung cấp khác!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Chưa kết nối" }, - "staySafeWithOpenSea": { - "message": "Đảm bảo an toàn với OpenSea" - }, "step1LatticeWallet": { "message": "Kết nối với Lattice1" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 096dcd1e4c29..78d5099ef13e 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -329,9 +329,6 @@ "message": "允许 $1 提取和消费最高以下金额:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "在批准任何请求之前,始终确保执行自身尽职调查。" - }, "amount": { "message": "数额" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "使用 OpenSea 的 API 获取 NFT 数据。NFT 自动检测依赖于 OpenSea 的 API,在后者关闭时自动检测将不可用。" }, - "enableOpenSeaSecurityProvider": { - "message": "启用安全服务提供商" - }, "enableSmartTransactions": { "message": "启用智能交易" }, @@ -1519,9 +1513,6 @@ "general": { "message": "常规" }, - "getWarningsFromOpenSea": { - "message": "每当收到已知恶意请求,即从 OpenSea 获取警告。" - }, "goBack": { "message": "返回" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "燃料不足" }, - "notNow": { - "message": "暂时不" - }, "notifications": { "message": "通知" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea(测试版)" }, - "openSeaAltText": { - "message": "OpenSea 安全服务提供商" - }, - "openSeaDescription": { - "message": "OpenSea 是该功能的第一个安全服务提供商。即将推出更多提供商!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "未连接" }, - "staySafeWithOpenSea": { - "message": "通过 OpenSea 确保安全" - }, "step1LatticeWallet": { "message": "关联您的 Lattice1" }, diff --git a/app/images/open-sea-security-provider.svg b/app/images/open-sea-security-provider.svg deleted file mode 100644 index ac79c0026adb..000000000000 --- a/app/images/open-sea-security-provider.svg +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/manifest/v2/_base.json b/app/manifest/v2/_base.json index f962de618d51..39b289197617 100644 --- a/app/manifest/v2/_base.json +++ b/app/manifest/v2/_base.json @@ -60,19 +60,5 @@ }, "manifest_version": 2, "name": "__MSG_appName__", - "permissions": [ - "storage", - "unlimitedStorage", - "clipboardWrite", - "http://localhost:8545/", - "https://*.infura.io/", - "https://*.codefi.network/", - "https://chainid.network/chains.json", - "https://lattice.gridplus.io/*", - "activeTab", - "webRequest", - "*://*.eth/", - "notifications" - ], "short_name": "__MSG_appName__" } diff --git a/app/manifest/v2/chrome.json b/app/manifest/v2/chrome.json index a152130d89b2..9c0e95ec5d6f 100644 --- a/app/manifest/v2/chrome.json +++ b/app/manifest/v2/chrome.json @@ -4,5 +4,19 @@ "matches": ["https://metamask.io/*"], "ids": ["*"] }, - "minimum_chrome_version": "80" + "minimum_chrome_version": "80", + "permissions": [ + "storage", + "unlimitedStorage", + "clipboardWrite", + "http://localhost:8545/", + "https://*.infura.io/", + "https://*.codefi.network/", + "https://chainid.network/chains.json", + "https://lattice.gridplus.io/*", + "activeTab", + "webRequest", + "*://*.eth/", + "notifications" + ] } diff --git a/app/manifest/v2/firefox.json b/app/manifest/v2/firefox.json index d50b26a27ff8..5adf0471356d 100644 --- a/app/manifest/v2/firefox.json +++ b/app/manifest/v2/firefox.json @@ -4,5 +4,20 @@ "id": "webextension@metamask.io", "strict_min_version": "78.0" } - } + }, + "permissions": [ + "storage", + "unlimitedStorage", + "clipboardWrite", + "http://localhost:8545/", + "https://*.infura.io/", + "https://*.codefi.network/", + "https://chainid.network/chains.json", + "https://lattice.gridplus.io/*", + "activeTab", + "tabs", + "webRequest", + "*://*.eth/", + "notifications" + ] } diff --git a/app/manifest/v3/_base.json b/app/manifest/v3/_base.json index 1b9456fd8d93..3beeb73790d4 100644 --- a/app/manifest/v3/_base.json +++ b/app/manifest/v3/_base.json @@ -65,15 +65,5 @@ }, "manifest_version": 3, "name": "__MSG_appName__", - "permissions": [ - "activeTab", - "alarms", - "clipboardWrite", - "notifications", - "scripting", - "storage", - "unlimitedStorage", - "webRequest" - ], "short_name": "__MSG_appName__" } diff --git a/app/manifest/v3/chrome.json b/app/manifest/v3/chrome.json index 486692539eb4..dbb0ee22cca8 100644 --- a/app/manifest/v3/chrome.json +++ b/app/manifest/v3/chrome.json @@ -6,5 +6,19 @@ "matches": ["https://metamask.io/*"], "ids": ["*"] }, - "minimum_chrome_version": "80" + "minimum_chrome_version": "80", + "permissions": [ + "storage", + "unlimitedStorage", + "clipboardWrite", + "http://localhost:8545/", + "https://*.infura.io/", + "https://*.codefi.network/", + "https://chainid.network/chains.json", + "https://lattice.gridplus.io/*", + "activeTab", + "webRequest", + "*://*.eth/", + "notifications" + ] } diff --git a/app/manifest/v3/firefox.json b/app/manifest/v3/firefox.json index 5f0e5672fdbe..67ecf7b09891 100644 --- a/app/manifest/v3/firefox.json +++ b/app/manifest/v3/firefox.json @@ -22,5 +22,20 @@ "default_title": "MetaMask", "default_popup": "popup.html" }, - "manifest_version": 2 + "manifest_version": 2, + "permissions": [ + "storage", + "unlimitedStorage", + "clipboardWrite", + "http://localhost:8545/", + "https://*.infura.io/", + "https://*.codefi.network/", + "https://chainid.network/chains.json", + "https://lattice.gridplus.io/*", + "tabs", + "activeTab", + "webRequest", + "*://*.eth/", + "notifications" + ] } diff --git a/app/scripts/controllers/network/create-network-client.test.js b/app/scripts/controllers/network/create-network-client.test.js new file mode 100644 index 000000000000..7e674392b75c --- /dev/null +++ b/app/scripts/controllers/network/create-network-client.test.js @@ -0,0 +1,7 @@ +import { NetworkClientType } from './create-network-client'; +import { testsForProviderType } from './provider-api-tests/shared-tests'; + +describe('createNetworkClient', () => { + testsForProviderType(NetworkClientType.Infura); + testsForProviderType(NetworkClientType.Custom); +}); diff --git a/app/scripts/controllers/network/create-network-client.ts b/app/scripts/controllers/network/create-network-client.ts new file mode 100644 index 000000000000..6e96b71f5c9a --- /dev/null +++ b/app/scripts/controllers/network/create-network-client.ts @@ -0,0 +1,191 @@ +import { + createAsyncMiddleware, + createScaffoldMiddleware, + JsonRpcEngine, + mergeMiddleware, + JsonRpcMiddleware, +} from 'json-rpc-engine'; +import { + createBlockCacheMiddleware, + createBlockRefMiddleware, + createBlockRefRewriteMiddleware, + createBlockTrackerInspectorMiddleware, + createInflightCacheMiddleware, + createFetchMiddleware, + createRetryOnEmptyMiddleware, +} from '@metamask/eth-json-rpc-middleware'; +import { + providerFromEngine, + providerFromMiddleware, + SafeEventEmitterProvider, +} from '@metamask/eth-json-rpc-provider'; +import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura'; +import type { Hex } from '@metamask/utils/dist'; +import { PollingBlockTracker } from 'eth-block-tracker/dist'; +import { SECOND } from '../../../../shared/constants/time'; +import { + BUILT_IN_INFURA_NETWORKS, + BuiltInInfuraNetwork, +} from '../../../../shared/constants/network'; + +export enum NetworkClientType { + Custom = 'custom', + Infura = 'infura', +} + +type CustomNetworkConfiguration = { + chainId: Hex; + rpcUrl: string; + type: NetworkClientType.Custom; +}; + +type InfuraNetworkConfiguration = { + network: BuiltInInfuraNetwork; + infuraProjectId: string; + type: NetworkClientType.Infura; +}; + +/** + * Create a JSON RPC network client for a specific network. + * + * @param networkConfig - The network configuration. + * @returns + */ +export function createNetworkClient( + networkConfig: CustomNetworkConfiguration | InfuraNetworkConfiguration, +): { provider: SafeEventEmitterProvider; blockTracker: PollingBlockTracker } { + const rpcApiMiddleware = + networkConfig.type === NetworkClientType.Infura + ? createInfuraMiddleware({ + network: networkConfig.network, + projectId: networkConfig.infuraProjectId, + maxAttempts: 5, + source: 'metamask', + }) + : createFetchMiddleware({ + btoa: global.btoa, + fetch: global.fetch, + rpcUrl: networkConfig.rpcUrl, + }); + + const rpcProvider = providerFromMiddleware(rpcApiMiddleware); + + const blockTrackerOpts = + process.env.IN_TEST && networkConfig.type === 'custom' + ? { pollingInterval: SECOND } + : {}; + const blockTracker = new PollingBlockTracker({ + ...blockTrackerOpts, + provider: rpcProvider, + }); + + const networkMiddleware = + networkConfig.type === NetworkClientType.Infura + ? createInfuraNetworkMiddleware({ + blockTracker, + network: networkConfig.network, + rpcProvider, + rpcApiMiddleware, + }) + : createCustomNetworkMiddleware({ + blockTracker, + chainId: networkConfig.chainId, + rpcApiMiddleware, + }); + + const engine = new JsonRpcEngine(); + + engine.push(networkMiddleware); + + const provider = providerFromEngine(engine); + + return { provider, blockTracker }; +} + +function createInfuraNetworkMiddleware({ + blockTracker, + network, + rpcProvider, + rpcApiMiddleware, +}: { + blockTracker: PollingBlockTracker; + network: BuiltInInfuraNetwork; + rpcProvider: SafeEventEmitterProvider; + rpcApiMiddleware: JsonRpcMiddleware; +}) { + return mergeMiddleware([ + createNetworkAndChainIdMiddleware({ network }), + createBlockCacheMiddleware({ blockTracker }), + createInflightCacheMiddleware(), + createBlockRefMiddleware({ blockTracker, provider: rpcProvider }), + createRetryOnEmptyMiddleware({ blockTracker, provider: rpcProvider }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + rpcApiMiddleware, + ]); +} + +function createNetworkAndChainIdMiddleware({ + network, +}: { + network: BuiltInInfuraNetwork; +}) { + if (!BUILT_IN_INFURA_NETWORKS[network]) { + throw new Error(`createInfuraClient - unknown network "${network}"`); + } + + const { chainId, networkId } = BUILT_IN_INFURA_NETWORKS[network]; + + return createScaffoldMiddleware({ + eth_chainId: chainId, + net_version: networkId, + }); +} + +const createChainIdMiddleware = ( + chainId: string, +): JsonRpcMiddleware => { + return (req, res, next, end) => { + if (req.method === 'eth_chainId') { + res.result = chainId; + return end(); + } + return next(); + }; +}; + +function createCustomNetworkMiddleware({ + blockTracker, + chainId, + rpcApiMiddleware, +}: { + blockTracker: PollingBlockTracker; + chainId: string; + rpcApiMiddleware: any; +}) { + const testMiddlewares = process.env.IN_TEST + ? [createEstimateGasDelayTestMiddleware()] + : []; + + return mergeMiddleware([ + ...testMiddlewares, + createChainIdMiddleware(chainId), + createBlockRefRewriteMiddleware({ blockTracker }), + createBlockCacheMiddleware({ blockTracker }), + createInflightCacheMiddleware(), + createBlockTrackerInspectorMiddleware({ blockTracker }), + rpcApiMiddleware, + ]); +} + +/** + * For use in tests only. + * Adds a delay to `eth_estimateGas` calls. + */ +function createEstimateGasDelayTestMiddleware() { + return createAsyncMiddleware(async (req, _, next) => { + if (req.method === 'eth_estimateGas') { + await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); + } + return next(); + }); +} diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js deleted file mode 100644 index 76461eb6226e..000000000000 --- a/app/scripts/controllers/network/createInfuraClient.js +++ /dev/null @@ -1,49 +0,0 @@ -import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine'; -import { - createBlockRefMiddleware, - createRetryOnEmptyMiddleware, - createBlockCacheMiddleware, - createInflightCacheMiddleware, - createBlockTrackerInspectorMiddleware, - providerFromMiddleware, -} from '@metamask/eth-json-rpc-middleware'; - -import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura'; -import { PollingBlockTracker } from 'eth-block-tracker'; - -import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; - -export default function createInfuraClient({ network, projectId }) { - const infuraMiddleware = createInfuraMiddleware({ - network, - projectId, - maxAttempts: 5, - source: 'metamask', - }); - const infuraProvider = providerFromMiddleware(infuraMiddleware); - const blockTracker = new PollingBlockTracker({ provider: infuraProvider }); - - const networkMiddleware = mergeMiddleware([ - createNetworkAndChainIdMiddleware({ network }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockRefMiddleware({ blockTracker, provider: infuraProvider }), - createRetryOnEmptyMiddleware({ blockTracker, provider: infuraProvider }), - createBlockTrackerInspectorMiddleware({ blockTracker }), - infuraMiddleware, - ]); - return { networkMiddleware, blockTracker }; -} - -function createNetworkAndChainIdMiddleware({ network }) { - if (!BUILT_IN_NETWORKS[network]) { - throw new Error(`createInfuraClient - unknown network "${network}"`); - } - - const { chainId, networkId } = BUILT_IN_NETWORKS[network]; - - return createScaffoldMiddleware({ - eth_chainId: chainId, - net_version: networkId, - }); -} diff --git a/app/scripts/controllers/network/createInfuraClient.test.js b/app/scripts/controllers/network/createInfuraClient.test.js deleted file mode 100644 index d1b1a7ccfc62..000000000000 --- a/app/scripts/controllers/network/createInfuraClient.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import { testsForProviderType } from './provider-api-tests/shared-tests'; - -describe('createInfuraClient', () => { - testsForProviderType('infura'); -}); diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js deleted file mode 100644 index b8cf0e7aaeb0..000000000000 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ /dev/null @@ -1,61 +0,0 @@ -import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine'; -import { - createFetchMiddleware, - createBlockRefRewriteMiddleware, - createBlockCacheMiddleware, - createInflightCacheMiddleware, - createBlockTrackerInspectorMiddleware, - providerFromMiddleware, -} from '@metamask/eth-json-rpc-middleware'; -import { PollingBlockTracker } from 'eth-block-tracker'; -import { SECOND } from '../../../../shared/constants/time'; - -export default function createJsonRpcClient({ rpcUrl, chainId }) { - const blockTrackerOpts = process.env.IN_TEST - ? { pollingInterval: SECOND } - : {}; - const fetchMiddleware = createFetchMiddleware({ rpcUrl }); - const blockProvider = providerFromMiddleware(fetchMiddleware); - const blockTracker = new PollingBlockTracker({ - ...blockTrackerOpts, - provider: blockProvider, - }); - const testMiddlewares = process.env.IN_TEST - ? [createEstimateGasDelayTestMiddleware()] - : []; - - const networkMiddleware = mergeMiddleware([ - ...testMiddlewares, - createChainIdMiddleware(chainId), - createBlockRefRewriteMiddleware({ blockTracker }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockTrackerInspectorMiddleware({ blockTracker }), - fetchMiddleware, - ]); - - return { networkMiddleware, blockTracker }; -} - -function createChainIdMiddleware(chainId) { - return (req, res, next, end) => { - if (req.method === 'eth_chainId') { - res.result = chainId; - return end(); - } - return next(); - }; -} - -/** - * For use in tests only. - * Adds a delay to `eth_estimateGas` calls. - */ -function createEstimateGasDelayTestMiddleware() { - return createAsyncMiddleware(async (req, _, next) => { - if (req.method === 'eth_estimateGas') { - await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); - } - return next(); - }); -} diff --git a/app/scripts/controllers/network/createJsonRpcClient.test.js b/app/scripts/controllers/network/createJsonRpcClient.test.js deleted file mode 100644 index 1c3443d25814..000000000000 --- a/app/scripts/controllers/network/createJsonRpcClient.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import { testsForProviderType } from './provider-api-tests/shared-tests'; - -describe('createJsonRpcClient', () => { - testsForProviderType('custom'); -}); diff --git a/app/scripts/controllers/network/network-controller.js b/app/scripts/controllers/network/network-controller.js index 20869f227e55..06d236e26c83 100644 --- a/app/scripts/controllers/network/network-controller.js +++ b/app/scripts/controllers/network/network-controller.js @@ -1,8 +1,6 @@ import { strict as assert } from 'assert'; import EventEmitter from 'events'; import { ComposedStore, ObservableStore } from '@metamask/obs-store'; -import { JsonRpcEngine } from 'json-rpc-engine'; -import { providerFromEngine } from '@metamask/eth-json-rpc-middleware'; import log from 'loglevel'; import { createSwappableProxy, @@ -24,8 +22,7 @@ import { isSafeChainId, } from '../../../../shared/modules/network.utils'; import { EVENT } from '../../../../shared/constants/metametrics'; -import createInfuraClient from './createInfuraClient'; -import createJsonRpcClient from './createJsonRpcClient'; +import { createNetworkClient } from './create-network-client'; /** * @typedef {object} NetworkConfiguration @@ -428,7 +425,10 @@ export default class NetworkController extends EventEmitter { // infura type-based endpoints const isInfura = INFURA_PROVIDER_TYPES.includes(type); if (isInfura) { - this._configureInfuraProvider(type, this._infuraProjectId); + this._configureInfuraProvider({ + type, + infuraProjectId: this._infuraProjectId, + }); // url-based rpc endpoints } else if (type === NETWORK_TYPES.RPC) { this._configureStandardProvider(rpcUrl, chainId); @@ -439,25 +439,23 @@ export default class NetworkController extends EventEmitter { } } - _configureInfuraProvider(type, projectId) { + _configureInfuraProvider({ type, infuraProjectId }) { log.info('NetworkController - configureInfuraProvider', type); - const networkClient = createInfuraClient({ + const { provider, blockTracker } = createNetworkClient({ network: type, - projectId, + infuraProjectId, + type: 'infura', }); - this._setNetworkClient(networkClient); + this._setProviderAndBlockTracker({ provider, blockTracker }); } _configureStandardProvider(rpcUrl, chainId) { log.info('NetworkController - configureStandardProvider', rpcUrl); - const networkClient = createJsonRpcClient({ rpcUrl, chainId }); - this._setNetworkClient(networkClient); - } - - _setNetworkClient({ networkMiddleware, blockTracker }) { - const engine = new JsonRpcEngine(); - engine.push(networkMiddleware); - const provider = providerFromEngine(engine); + const { provider, blockTracker } = createNetworkClient({ + chainId, + rpcUrl, + type: 'custom', + }); this._setProviderAndBlockTracker({ provider, blockTracker }); } diff --git a/app/scripts/controllers/network/network-controller.test.js b/app/scripts/controllers/network/network-controller.test.js index a3743ae56819..cd4e9a78254f 100644 --- a/app/scripts/controllers/network/network-controller.test.js +++ b/app/scripts/controllers/network/network-controller.test.js @@ -3,15 +3,15 @@ import { isMatch } from 'lodash'; import { v4 } from 'uuid'; import nock from 'nock'; import sinon from 'sinon'; -import * as ethJsonRpcMiddlewareModule from '@metamask/eth-json-rpc-middleware'; +import * as ethJsonRpcProvider from '@metamask/eth-json-rpc-provider'; import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; import { EVENT } from '../../../../shared/constants/metametrics'; import NetworkController from './network-controller'; -jest.mock('@metamask/eth-json-rpc-middleware', () => { +jest.mock('@metamask/eth-json-rpc-provider', () => { return { __esModule: true, - ...jest.requireActual('@metamask/eth-json-rpc-middleware'), + ...jest.requireActual('@metamask/eth-json-rpc-provider'), }; }); @@ -1760,7 +1760,7 @@ describe('NetworkController', () => { }, ]; jest - .spyOn(ethJsonRpcMiddlewareModule, 'providerFromEngine') + .spyOn(ethJsonRpcProvider, 'providerFromEngine') .mockImplementationOnce(() => fakeProviders[0]) .mockImplementationOnce(() => fakeProviders[1]); await withoutCallingLookupNetwork({ @@ -1876,7 +1876,7 @@ describe('NetworkController', () => { }, ]; jest - .spyOn(ethJsonRpcMiddlewareModule, 'providerFromEngine') + .spyOn(ethJsonRpcProvider, 'providerFromEngine') .mockImplementationOnce(() => fakeProviders[0]) .mockImplementationOnce(() => fakeProviders[1]); await withoutCallingLookupNetwork({ diff --git a/app/scripts/controllers/network/provider-api-tests/helpers.js b/app/scripts/controllers/network/provider-api-tests/helpers.js index a5d92b91e5d2..afa91a7bba6c 100644 --- a/app/scripts/controllers/network/provider-api-tests/helpers.js +++ b/app/scripts/controllers/network/provider-api-tests/helpers.js @@ -1,10 +1,7 @@ import nock from 'nock'; import sinon from 'sinon'; -import { JsonRpcEngine } from 'json-rpc-engine'; -import { providerFromEngine } from '@metamask/eth-json-rpc-middleware'; import EthQuery from 'eth-query'; -import createInfuraClient from '../createInfuraClient'; -import createJsonRpcClient from '../createJsonRpcClient'; +import { createNetworkClient } from '../create-network-client'; /** * @typedef {import('nock').Scope} NockScope @@ -414,20 +411,21 @@ export async function withNetworkClient( delete process.env.IN_TEST; const clientUnderTest = providerType === 'infura' - ? createInfuraClient({ + ? createNetworkClient({ network: infuraNetwork, - projectId: MOCK_INFURA_PROJECT_ID, + infuraProjectId: MOCK_INFURA_PROJECT_ID, + type: 'infura', }) - : createJsonRpcClient({ rpcUrl: customRpcUrl, chainId: customChainId }); + : createNetworkClient({ + chainId: customChainId, + rpcUrl: customRpcUrl, + type: 'custom', + }); process.env.IN_TEST = inTest; - const { networkMiddleware, blockTracker } = clientUnderTest; + const { provider, blockTracker } = clientUnderTest; - const engine = new JsonRpcEngine(); - engine.push(networkMiddleware); - const provider = providerFromEngine(engine); const ethQuery = new EthQuery(provider); - const curriedMakeRpcCall = (request) => makeRpcCall(ethQuery, request); const makeRpcCallsInSeries = async (requests) => { const responses = []; diff --git a/app/scripts/controllers/network/provider-api-tests/shared-tests.js b/app/scripts/controllers/network/provider-api-tests/shared-tests.js index d615956707b9..04412d3f0841 100644 --- a/app/scripts/controllers/network/provider-api-tests/shared-tests.js +++ b/app/scripts/controllers/network/provider-api-tests/shared-tests.js @@ -291,9 +291,9 @@ export function testsForProviderType(providerType) { // tests on the core side. { name: 'net_listening', numberOfParameters: 0 }, - - { name: 'eth_subscribe', numberOfParameters: 1 }, - { name: 'eth_unsubscribe', numberOfParameters: 1 }, + // TODO: Methods to add back when we add testing for subscribe middleware + // { name: 'eth_subscribe', numberOfParameters: 1 }, + // { name: 'eth_unsubscribe', numberOfParameters: 1 }, { name: 'custom_rpc_method', numberOfParameters: 1 }, { name: 'net_peerCount', numberOfParameters: 0 }, { name: 'parity_nextNonce', numberOfParameters: 1 }, diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 455fcdc1e0ee..c33e0f12adf4 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -67,7 +67,6 @@ export default class PreferencesController { : LedgerTransportTypes.u2f, transactionSecurityCheckEnabled: false, theme: ThemeType.os, - openSeaTransactionSecurityProviderPopoverHasBeenShown: false, ...opts.initState, }; @@ -204,16 +203,6 @@ export default class PreferencesController { }); } - /** - * Setter for the `openSeaTransactionSecurityProviderPopoverHasBeenShown` property - * - */ - setOpenSeaTransactionSecurityProviderPopoverHasBeenShown() { - this.store.updateState({ - openSeaTransactionSecurityProviderPopoverHasBeenShown: true, - }); - } - /** * Add new methodData to state, to avoid requesting this information again through Infura * diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index f129de94011f..21267195c322 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -330,28 +330,4 @@ describe('preferences controller', function () { ); }); }); - - describe('setOpenSeaTransactionSecurityProviderPopoverHasBeenShown', function () { - it('should default to value "false"', function () { - const state = preferencesController.store.getState(); - assert.equal( - state.openSeaTransactionSecurityProviderPopoverHasBeenShown, - false, - ); - }); - - it('should set the openSeaTransactionSecurityProviderPopoverHasBeenShown to true', function () { - const state = preferencesController.store.getState(); - assert.equal( - state.openSeaTransactionSecurityProviderPopoverHasBeenShown, - false, - ); - preferencesController.setOpenSeaTransactionSecurityProviderPopoverHasBeenShown(); - assert.equal( - preferencesController.store.getState() - .openSeaTransactionSecurityProviderPopoverHasBeenShown, - true, - ); - }); - }); }); diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js deleted file mode 100644 index bb0a2b0c1e04..000000000000 --- a/app/scripts/lib/personal-message-manager.js +++ /dev/null @@ -1,370 +0,0 @@ -import EventEmitter from 'events'; -import { ObservableStore } from '@metamask/obs-store'; -import { bufferToHex } from 'ethereumjs-util'; -import { ethErrors } from 'eth-rpc-errors'; -import log from 'loglevel'; -import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from '../../../shared/modules/random-id'; -import { EVENT } from '../../../shared/constants/metametrics'; -import { detectSIWE } from '../../../shared/modules/siwe'; -import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; -import { addHexPrefix } from './util'; - -const hexRe = /^[0-9A-Fa-f]+$/gu; - -/** - * Represents, and contains data about, an 'personal_sign' type signature request. These are created when a - * signature for an personal_sign call is requested. - * - * @see {@link https://web3js.readthedocs.io/en/1.0/web3-eth-personal.html#sign} - * @typedef {object} PersonalMessage - * @property {number} id An id to track and identify the message object - * @property {object} msgParams The parameters to pass to the personal_sign method once the signature request is - * approved. - * @property {object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. - * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request - * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' - * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will - * always have a 'personal_sign' type. - */ - -export default class PersonalMessageManager extends EventEmitter { - /** - * Controller in charge of managing - storing, adding, removing, updating - PersonalMessage. - * - * @param options - * @param options.metricsEvent - * @param options.securityProviderRequest - */ - constructor({ metricsEvent, securityProviderRequest }) { - super(); - this.memStore = new ObservableStore({ - unapprovedPersonalMsgs: {}, - unapprovedPersonalMsgCount: 0, - }); - - this.resetState = () => { - this.memStore.updateState({ - unapprovedPersonalMsgs: {}, - unapprovedPersonalMsgCount: 0, - }); - }; - - this.messages = []; - this.metricsEvent = metricsEvent; - this.securityProviderRequest = securityProviderRequest; - } - - /** - * A getter for the number of 'unapproved' PersonalMessages in this.messages - * - * @returns {number} The number of 'unapproved' PersonalMessages in this.messages - */ - get unapprovedPersonalMsgCount() { - return Object.keys(this.getUnapprovedMsgs()).length; - } - - /** - * A getter for the 'unapproved' PersonalMessages in this.messages - * - * @returns {object} An index of PersonalMessage ids to PersonalMessages, for all 'unapproved' PersonalMessages in - * this.messages - */ - getUnapprovedMsgs() { - return this.messages - .filter((msg) => msg.status === 'unapproved') - .reduce((result, msg) => { - result[msg.id] = msg; - return result; - }, {}); - } - - /** - * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to - * this.memStore. - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @returns {promise} When the message has been signed or rejected - */ - addUnapprovedMessageAsync(msgParams, req) { - return new Promise((resolve, reject) => { - if (!msgParams.from) { - reject( - new Error('MetaMask Message Signature: from field is required.'), - ); - return; - } - this.addUnapprovedMessage(msgParams, req).then((msgId) => { - this.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - resolve(data.rawSig); - return; - case 'rejected': - reject( - ethErrors.provider.userRejectedRequest( - 'MetaMask Message Signature: User denied message signature.', - ), - ); - return; - case 'errored': - reject(new Error(`MetaMask Message Signature: ${data.error}`)); - return; - default: - reject( - new Error( - `MetaMask Message Signature: Unknown problem: ${JSON.stringify( - msgParams, - )}`, - ), - ); - } - }); - }); - }); - } - - /** - * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to - * this.memStore. - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @returns {number} The id of the newly created PersonalMessage. - */ - async addUnapprovedMessage(msgParams, req) { - log.debug( - `PersonalMessageManager addUnapprovedMessage: ${JSON.stringify( - msgParams, - )}`, - ); - // add origin from request - if (req) { - msgParams.origin = req.origin; - } - msgParams.data = this.normalizeMsgData(msgParams.data); - - // check for SIWE message - const siwe = detectSIWE(msgParams); - msgParams.siwe = siwe; - - // create txData obj with parameters and meta data - const time = new Date().getTime(); - const msgId = createId(); - const msgData = { - id: msgId, - msgParams, - time, - status: 'unapproved', - type: MESSAGE_TYPE.PERSONAL_SIGN, - }; - this.addMsg(msgData); - - const securityProviderResponse = await this.securityProviderRequest( - msgData, - msgData.type, - ); - - msgData.securityProviderResponse = securityProviderResponse; - - // signal update - this.emit('update'); - return msgId; - } - - /** - * Adds a passed PersonalMessage to this.messages, and calls this._saveMsgList() to save the unapproved PersonalMessages from that - * list to this.memStore. - * - * @param {Message} msg - The PersonalMessage to add to this.messages - */ - addMsg(msg) { - this.messages.push(msg); - this._saveMsgList(); - } - - /** - * Returns a specified PersonalMessage. - * - * @param {number} msgId - The id of the PersonalMessage to get - * @returns {PersonalMessage|undefined} The PersonalMessage with the id that matches the passed msgId, or undefined - * if no PersonalMessage has that id. - */ - getMsg(msgId) { - return this.messages.find((msg) => msg.id === msgId); - } - - /** - * Approves a PersonalMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise - * with any the message params modified for proper signing. - * - * @param {object} msgParams - The msgParams to be used when eth_sign is called, plus data added by MetaMask. - * @param {object} msgParams.metamaskId - Added to msgParams for tracking and identification within MetaMask. - * @returns {Promise} Promises the msgParams object with metamaskId removed. - */ - approveMessage(msgParams) { - this.setMsgStatusApproved(msgParams.metamaskId); - return this.prepMsgForSigning(msgParams); - } - - /** - * Sets a PersonalMessage status to 'approved' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the PersonalMessage to approve. - */ - setMsgStatusApproved(msgId) { - this._setMsgStatus(msgId, 'approved'); - } - - /** - * Sets a PersonalMessage status to 'signed' via a call to this._setMsgStatus and updates that PersonalMessage in - * this.messages by adding the raw signature data of the signature request to the PersonalMessage - * - * @param {number} msgId - The id of the PersonalMessage to sign. - * @param {buffer} rawSig - The raw data of the signature request - */ - setMsgStatusSigned(msgId, rawSig) { - const msg = this.getMsg(msgId); - msg.rawSig = rawSig; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'signed'); - } - - /** - * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams - * - * @param {object} msgParams - The msgParams to modify - * @returns {Promise} Promises the msgParams with the metamaskId property removed - */ - async prepMsgForSigning(msgParams) { - delete msgParams.metamaskId; - return msgParams; - } - - /** - * Sets a PersonalMessage status to 'rejected' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the PersonalMessage to reject. - * @param reason - */ - rejectMsg(msgId, reason = undefined) { - if (reason) { - const msg = this.getMsg(msgId); - this.metricsEvent({ - event: reason, - category: EVENT.CATEGORIES.TRANSACTIONS, - properties: { - action: 'Sign Request', - type: msg.type, - }, - }); - } - this._setMsgStatus(msgId, 'rejected'); - } - - /** - * Sets a Message status to 'errored' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the Message to error - * @param error - */ - errorMessage(msgId, error) { - const msg = this.getMsg(msgId); - msg.error = error; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'errored'); - } - - /** - * Clears all unapproved messages from memory. - */ - clearUnapproved() { - this.messages = this.messages.filter((msg) => msg.status !== 'unapproved'); - this._saveMsgList(); - } - - /** - * Updates the status of a PersonalMessage in this.messages via a call to this._updateMsg - * - * @private - * @param {number} msgId - The id of the PersonalMessage to update. - * @param {string} status - The new status of the PersonalMessage. - * @throws A 'PersonalMessageManager - PersonalMessage not found for id: "${msgId}".' if there is no PersonalMessage - * in this.messages with an id equal to the passed msgId - * @fires An event with a name equal to `${msgId}:${status}`. The PersonalMessage is also fired. - * @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along - * with the PersonalMessage - */ - _setMsgStatus(msgId, status) { - const msg = this.getMsg(msgId); - if (!msg) { - throw new Error( - `PersonalMessageManager - Message not found for id: "${msgId}".`, - ); - } - msg.status = status; - this._updateMsg(msg); - this.emit(`${msgId}:${status}`, msg); - if (status === 'rejected' || status === 'signed') { - this.emit(`${msgId}:finished`, msg); - } - } - - /** - * Sets a PersonalMessage in this.messages to the passed PersonalMessage if the ids are equal. Then saves the - * unapprovedPersonalMsgs index to storage via this._saveMsgList - * - * @private - * @param {PersonalMessage} msg - A PersonalMessage that will replace an existing PersonalMessage (with the same - * id) in this.messages - */ - _updateMsg(msg) { - const index = this.messages.findIndex((message) => message.id === msg.id); - if (index !== -1) { - this.messages[index] = msg; - } - this._saveMsgList(); - } - - /** - * Saves the unapproved PersonalMessages, and their count, to this.memStore - * - * @private - * @fires 'updateBadge' - */ - _saveMsgList() { - const unapprovedPersonalMsgs = this.getUnapprovedMsgs(); - const unapprovedPersonalMsgCount = Object.keys( - unapprovedPersonalMsgs, - ).length; - this.memStore.updateState({ - unapprovedPersonalMsgs, - unapprovedPersonalMsgCount, - }); - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } - - /** - * A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex. - * - * @param {any} data - The buffer data to convert to a hex - * @returns {string} A hex string conversion of the buffer data - */ - normalizeMsgData(data) { - try { - const stripped = stripHexPrefix(data); - if (stripped.match(hexRe)) { - return addHexPrefix(stripped); - } - } catch (e) { - log.debug(`Message was not hex encoded, interpreting as utf8.`); - } - - return bufferToHex(Buffer.from(data, 'utf8')); - } -} diff --git a/app/scripts/lib/personal-message-manager.test.js b/app/scripts/lib/personal-message-manager.test.js deleted file mode 100644 index e565c0229b69..000000000000 --- a/app/scripts/lib/personal-message-manager.test.js +++ /dev/null @@ -1,182 +0,0 @@ -import { TransactionStatus } from '../../../shared/constants/transaction'; -import PersonalMessageManager from './personal-message-manager'; - -describe('Personal Message Manager', () => { - let messageManager; - - beforeEach(() => { - messageManager = new PersonalMessageManager({ - metricsEvent: jest.fn(), - securityProviderRequest: jest.fn(), - }); - }); - - describe('#getMsgList', () => { - it('when new should return empty array', () => { - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(0); - }); - }); - - describe('#addMsg', () => { - it('adds a Msg returned in getMsgList', () => { - const Msg = { - id: 1, - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].id).toStrictEqual(1); - }); - }); - - describe('#setMsgStatusApproved', () => { - it('sets the Msg status to approved', () => { - const Msg = { - id: 1, - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - messageManager.setMsgStatusApproved(1); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].status).toStrictEqual(TransactionStatus.approved); - }); - }); - - describe('#rejectMsg', () => { - it('sets the Msg status to rejected', () => { - const Msg = { - id: 1, - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - messageManager.rejectMsg(1); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].status).toStrictEqual(TransactionStatus.rejected); - }); - }); - - describe('#_updateMsg', () => { - it('replaces the Msg with the same id', () => { - messageManager.addMsg({ - id: '1', - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - messageManager._updateMsg({ - id: '1', - status: 'blah', - hash: 'foo', - metamaskNetworkId: 'unit test', - }); - const result = messageManager.getMsg('1'); - expect(result.hash).toStrictEqual('foo'); - }); - }); - - describe('#getUnapprovedMsgs', () => { - it('returns unapproved Msgs in a hash', () => { - messageManager.addMsg({ - id: '1', - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - const result = messageManager.getUnapprovedMsgs(); - expect(typeof result).toStrictEqual('object'); - expect(result['1'].status).toStrictEqual(TransactionStatus.unapproved); - expect(result['2']).toBeUndefined(); - }); - }); - - describe('#getMsg', () => { - it('returns a Msg with the requested id', () => { - messageManager.addMsg({ - id: '1', - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - expect(messageManager.getMsg('1').status).toStrictEqual( - TransactionStatus.unapproved, - ); - expect(messageManager.getMsg('2').status).toStrictEqual( - TransactionStatus.approved, - ); - }); - }); - - describe('#normalizeMsgData', () => { - it('converts text to a utf8 hex string', () => { - const input = 'hello'; - const output = messageManager.normalizeMsgData(input); - expect(output).toStrictEqual('0x68656c6c6f'); - }); - - it('tolerates a hex prefix', () => { - const input = '0x12'; - const output = messageManager.normalizeMsgData(input); - expect(output).toStrictEqual('0x12'); - }); - - it('tolerates normal hex', () => { - const input = '12'; - const output = messageManager.normalizeMsgData(input); - expect(output).toStrictEqual('0x12'); - }); - }); - - describe('#addUnapprovedMessage', () => { - const origin = 'http://localhost:8080'; - const from = '0xFb2C15004343904e5f4082578c4e8e11105cF7e3'; - const msgParams = { - from, - data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e', - }; - - it('should detect SIWE messages', async () => { - const request = { origin }; - const nonSiweMsgParams = { - from, - data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', - }; - // siwe message - const msgId = await messageManager.addUnapprovedMessage( - msgParams, - request, - ); - const result = messageManager.getMsg(msgId); - expect(result.msgParams.siwe.isSIWEMessage).toStrictEqual(true); - // non-siwe message - const msgId2 = await messageManager.addUnapprovedMessage( - nonSiweMsgParams, - request, - ); - const result2 = messageManager.getMsg(msgId2); - expect(result2.msgParams.siwe.isSIWEMessage).toStrictEqual(false); - }); - }); -}); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0a395106a9d2..6a2c1107d07c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1861,10 +1861,6 @@ export default class MetamaskController extends EventEmitter { preferencesController.setTransactionSecurityCheckEnabled.bind( preferencesController, ), - setOpenSeaTransactionSecurityProviderPopoverHasBeenShown: - preferencesController.setOpenSeaTransactionSecurityProviderPopoverHasBeenShown.bind( - preferencesController, - ), // AssetsContractController getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this), diff --git a/development/build/config.js b/development/build/config.js index 292a5cc0b146..e07707caa05c 100644 --- a/development/build/config.js +++ b/development/build/config.js @@ -7,6 +7,7 @@ const commonConfigurationPropertyNames = ['PUBNUB_PUB_KEY', 'PUBNUB_SUB_KEY']; const configurationPropertyNames = [ ...commonConfigurationPropertyNames, + 'MULTICHAIN', 'INFURA_PROJECT_ID', 'PHISHING_WARNING_PAGE_URL', 'PORTFOLIO_URL', diff --git a/development/build/scripts.js b/development/build/scripts.js index d28ef4db4e25..7d66260504c2 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -1109,6 +1109,7 @@ async function getEnvironmentVariables({ buildTarget, buildType, version }) { const iconNames = await generateIconNames(); return { ICON_NAMES: iconNames, + MULTICHAIN: config.MULTICHAIN === '1', CONF: devMode ? config : {}, IN_TEST: testing, INFURA_PROJECT_ID: getInfuraProjectId({ diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 17687c27bbdf..7d933f82d451 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -871,8 +871,9 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -881,57 +882,19 @@ "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "ethereumjs-wallet>randombytes": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, @@ -947,6 +910,12 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "json-rpc-engine": true, + "json-rpc-engine>@metamask/safe-event-emitter": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -2610,6 +2579,11 @@ "document.createElement": true } }, + "btoa": { + "packages": { + "browserify>buffer": true + } + }, "classnames": { "globals": { "classNames": "write", diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index ceadebd907b7..46ce15ef8d00 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -925,8 +925,9 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -935,57 +936,19 @@ "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "ethereumjs-wallet>randombytes": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, @@ -1001,6 +964,12 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "json-rpc-engine": true, + "json-rpc-engine>@metamask/safe-event-emitter": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -3051,6 +3020,11 @@ "document.createElement": true } }, + "btoa": { + "packages": { + "browserify>buffer": true + } + }, "classnames": { "globals": { "classNames": "write", diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index ceadebd907b7..46ce15ef8d00 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -925,8 +925,9 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -935,57 +936,19 @@ "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "ethereumjs-wallet>randombytes": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, @@ -1001,6 +964,12 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "json-rpc-engine": true, + "json-rpc-engine>@metamask/safe-event-emitter": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -3051,6 +3020,11 @@ "document.createElement": true } }, + "btoa": { + "packages": { + "browserify>buffer": true + } + }, "classnames": { "globals": { "classNames": "write", diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 17687c27bbdf..7d933f82d451 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -871,8 +871,9 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -881,57 +882,19 @@ "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "ethereumjs-wallet>randombytes": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, @@ -947,6 +910,12 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "json-rpc-engine": true, + "json-rpc-engine>@metamask/safe-event-emitter": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -2610,6 +2579,11 @@ "document.createElement": true } }, + "btoa": { + "packages": { + "browserify>buffer": true + } + }, "classnames": { "globals": { "classNames": "write", diff --git a/package.json b/package.json index 62e4af2ab549..e76f1c517b2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "10.26.1", + "version": "10.26.2", "private": true, "repository": { "type": "git", @@ -228,12 +228,13 @@ "@metamask/approval-controller": "^1.0.0", "@metamask/assets-controllers": "^4.0.1", "@metamask/base-controller": "^1.0.0", - "@metamask/contract-metadata": "^2.2.0", + "@metamask/contract-metadata": "^2.3.1", "@metamask/controller-utils": "^1.0.0", "@metamask/design-tokens": "^1.9.0", "@metamask/desktop": "^0.3.0", - "@metamask/eth-json-rpc-infura": "^7.0.0", - "@metamask/eth-json-rpc-middleware": "^10.0.0", + "@metamask/eth-json-rpc-infura": "^8.0.0", + "@metamask/eth-json-rpc-middleware": "^11.0.0", + "@metamask/eth-json-rpc-provider": "^1.0.0", "@metamask/eth-keyring-controller": "^10.0.1", "@metamask/eth-ledger-bridge-keyring": "^0.13.0", "@metamask/eth-token-tracker": "^4.0.0", @@ -285,7 +286,7 @@ "debounce-stream": "^2.0.0", "deep-freeze-strict": "1.1.1", "end-of-stream": "^1.4.4", - "eth-block-tracker": "^6.0.0", + "eth-block-tracker": "^7.0.0", "eth-ens-namehash": "^2.0.8", "eth-json-rpc-filters": "^6.0.0", "eth-lattice-keyring": "^0.12.3", @@ -510,7 +511,7 @@ "redux-mock-store": "^1.5.4", "remote-redux-devtools": "^0.5.16", "require-from-string": "^2.0.2", - "resolve-url-loader": "^3.1.2", + "resolve-url-loader": "^3.1.5", "sass": "^1.32.4", "sass-loader": "^10.1.1", "selenium-webdriver": "^4.3.1", diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 16bafb208dda..0774126239b1 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -1,4 +1,4 @@ -import { capitalize } from 'lodash'; +import { capitalize, pick } from 'lodash'; /** * A type representing any valid value for 'type' for setProviderType and other * methods that add or manipulate networks in MetaMask state. @@ -301,6 +301,13 @@ export const BUILT_IN_NETWORKS = { }, } as const; +export const BUILT_IN_INFURA_NETWORKS = pick( + BUILT_IN_NETWORKS, + INFURA_PROVIDER_TYPES, +); + +export type BuiltInInfuraNetwork = keyof typeof BUILT_IN_INFURA_NETWORKS; + export const NETWORK_TO_NAME_MAP = { [NETWORK_TYPES.MAINNET]: MAINNET_DISPLAY_NAME, [NETWORK_TYPES.GOERLI]: GOERLI_DISPLAY_NAME, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index a56197f4e694..6fffc84a9958 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -245,8 +245,6 @@ function defaultFixture() { useTokenDetection: false, useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, - transactionSecurityCheckEnabled: true, - openSeaTransactionSecurityProviderPopoverHasBeenShown: true, }, SmartTransactionsController: { smartTransactionsState: { @@ -360,8 +358,6 @@ function onboardingFixture() { useTokenDetection: false, useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, - transactionSecurityCheckEnabled: true, - openSeaTransactionSecurityProviderPopoverHasBeenShown: true, }, SmartTransactionsController: { smartTransactionsState: { diff --git a/test/e2e/tests/add-account.spec.js b/test/e2e/tests/add-account.spec.js index dc10beb5653a..226c8bda950b 100644 --- a/test/e2e/tests/add-account.spec.js +++ b/test/e2e/tests/add-account.spec.js @@ -242,7 +242,7 @@ describe('Add account', function () { // enter private key', await driver.fill('#private-key-box', testPrivateKey); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should show the correct account name const importedAccountName = await driver.findElement( diff --git a/test/e2e/tests/failing-contract.spec.js b/test/e2e/tests/failing-contract.spec.js index b3f9bb5c0085..3391d878ee65 100644 --- a/test/e2e/tests/failing-contract.spec.js +++ b/test/e2e/tests/failing-contract.spec.js @@ -63,7 +63,7 @@ describe('Failing contract interaction ', function () { // dismiss warning and confirm the transaction await driver.clickElement({ text: 'I want to proceed anyway', - tag: 'span', + tag: 'button', }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.waitUntilXWindowHandles(2); @@ -149,7 +149,7 @@ describe('Failing contract interaction on non-EIP1559 network', function () { // dismiss warning and confirm the transaction await driver.clickElement({ text: 'I want to proceed anyway', - tag: 'span', + tag: 'button', }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.waitUntilXWindowHandles(2); diff --git a/test/e2e/tests/from-import-ui.spec.js b/test/e2e/tests/from-import-ui.spec.js index 6a931835152b..e56882c05728 100644 --- a/test/e2e/tests/from-import-ui.spec.js +++ b/test/e2e/tests/from-import-ui.spec.js @@ -212,7 +212,7 @@ describe('MetaMask Import UI', function () { // enter private key', await driver.fill('#private-key-box', testPrivateKey1); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should show the correct account name const importedAccountName = await driver.findElement( @@ -239,7 +239,7 @@ describe('MetaMask Import UI', function () { await driver.clickElement({ text: 'Import account', tag: 'div' }); // enter private key await driver.fill('#private-key-box', testPrivateKey2); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should see new account in account menu const importedAccount2Name = await driver.findElement( @@ -330,7 +330,7 @@ describe('MetaMask Import UI', function () { await driver.fill('#json-password-box', 'foobarbazqux'); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should show the correct account name const importedAccountName = await driver.findElement( @@ -392,7 +392,7 @@ describe('MetaMask Import UI', function () { // enter private key', await driver.fill('#private-key-box', testPrivateKey); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // error should occur await driver.waitForSelector({ diff --git a/types/eth-json-rpc-filters/index.d.ts b/types/eth-json-rpc-filters/index.d.ts new file mode 100644 index 000000000000..f7515bccd652 --- /dev/null +++ b/types/eth-json-rpc-filters/index.d.ts @@ -0,0 +1 @@ +declare module 'eth-json-rpc-filters'; diff --git a/types/eth-json-rpc-filters/subscriptionManager.d.ts b/types/eth-json-rpc-filters/subscriptionManager.d.ts new file mode 100644 index 000000000000..a79ff8ee4483 --- /dev/null +++ b/types/eth-json-rpc-filters/subscriptionManager.d.ts @@ -0,0 +1 @@ +declare module 'eth-json-rpc-filters/subscriptionManager'; diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 427e60690984..e83606e8bfe7 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -12,21 +12,26 @@ import { getNativeCurrencyImage, getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, + getTokenList, } from '../../../selectors'; import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; -import Typography from '../../ui/typography/typography'; import Box from '../../ui/box/box'; import { Color, - TypographyVariant, - FONT_WEIGHT, - JustifyContent, + TextVariant, + TEXT_ALIGN, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics'; import DetectedToken from '../detected-token/detected-token'; +import { + DetectedTokensBanner, + MultichainTokenListItem, + MultichainImportTokenLink, +} from '../../multichain'; +import { Text } from '../../component-library'; import DetectedTokensLink from './detetcted-tokens-link/detected-tokens-link'; const AssetList = ({ onClickAsset }) => { @@ -69,20 +74,38 @@ const AssetList = ({ onClickAsset }) => { const istokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector( getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, ); - + const tokenList = useSelector(getTokenList); + const tokenData = Object.values(tokenList).find( + (token) => token.symbol === primaryCurrencyProperties.suffix, + ); + const title = tokenData?.name || primaryCurrencyProperties.suffix; return ( <> - onClickAsset(nativeCurrency)} - data-testid="wallet-balance" - primary={ - primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value - } - tokenSymbol={primaryCurrencyProperties.suffix} - secondary={showFiat ? secondaryCurrencyDisplay : undefined} - tokenImage={balanceIsLoading ? null : primaryTokenImage} - identiconBorder - /> + {process.env.MULTICHAIN ? ( + onClickAsset(nativeCurrency)} + title={title} + primary={ + primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value + } + tokenSymbol={primaryCurrencyProperties.suffix} + secondary={showFiat ? secondaryCurrencyDisplay : undefined} + tokenImage={balanceIsLoading ? null : primaryTokenImage} + /> + ) : ( + onClickAsset(nativeCurrency)} + data-testid="wallet-balance" + primary={ + primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value + } + tokenSymbol={primaryCurrencyProperties.suffix} + secondary={showFiat ? secondaryCurrencyDisplay : undefined} + tokenImage={balanceIsLoading ? null : primaryTokenImage} + identiconBorder + /> + )} + { onClickAsset(tokenAddress); @@ -98,19 +121,36 @@ const AssetList = ({ onClickAsset }) => { /> {detectedTokens.length > 0 && !istokenDetectionInactiveOnNonMainnetSupportedNetwork && ( - + <> + {process.env.MULTICHAIN ? ( + setShowDetectedTokens(true)} + margin={4} + /> + ) : ( + + )} + )} 0 ? 0 : 4}> - - - {t('missingToken')} - - - + {process.env.MULTICHAIN ? ( + + ) : ( + <> + + {t('missingToken')} + + + + + )} {showDetectedTokens && ( diff --git a/ui/components/app/open-sea-whats-new-popover/index.js b/ui/components/app/open-sea-whats-new-popover/index.js deleted file mode 100644 index abd91185ed53..000000000000 --- a/ui/components/app/open-sea-whats-new-popover/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './open-sea-whats-new-popover'; diff --git a/ui/components/app/open-sea-whats-new-popover/index.scss b/ui/components/app/open-sea-whats-new-popover/index.scss deleted file mode 100644 index ff307b957902..000000000000 --- a/ui/components/app/open-sea-whats-new-popover/index.scss +++ /dev/null @@ -1,6 +0,0 @@ -.open-sea-whats-new-popover { - &__enable-security-provider-button { - width: fit-content; - font-weight: 400; - } -} diff --git a/ui/components/app/open-sea-whats-new-popover/open-sea-whats-new-popover.js b/ui/components/app/open-sea-whats-new-popover/open-sea-whats-new-popover.js deleted file mode 100644 index 30bfda33a30f..000000000000 --- a/ui/components/app/open-sea-whats-new-popover/open-sea-whats-new-popover.js +++ /dev/null @@ -1,124 +0,0 @@ -import React, { useContext } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { I18nContext } from '../../../contexts/i18n'; -import Popover from '../../ui/popover'; -import { - DISPLAY, - FLEX_DIRECTION, - FONT_WEIGHT, - TextColor, - TextVariant, -} from '../../../helpers/constants/design-system'; -import Button from '../../ui/button'; -import Box from '../../ui/box'; -import { - setOpenSeaTransactionSecurityProviderPopoverHasBeenShown, - setTransactionSecurityCheckEnabled, -} from '../../../store/actions'; -import { getHasTheOpenSeaTransactionSecurityProviderPopoverBeenShown } from '../../../selectors'; -import { Text } from '../../component-library'; -import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes'; - -export default function OpenSeaWhatsNewPopover() { - const t = useContext(I18nContext); - const dispatch = useDispatch(); - const history = useHistory(); - - const hasThePopoverBeenShown = useSelector( - getHasTheOpenSeaTransactionSecurityProviderPopoverBeenShown, - ); - - return ( - process.env.TRANSACTION_SECURITY_PROVIDER && - !hasThePopoverBeenShown && ( - - {t('staySafeWithOpenSea')} - - } - footer={ - <> - - - - - - - - } - footerClassName="smart-transactions-popover__footer" - className="smart-transactions-popover" - onClose={() => - dispatch(setOpenSeaTransactionSecurityProviderPopoverHasBeenShown()) - } - > - - - {t('openSeaAltText')} - - - {t('getWarningsFromOpenSea')} - - - {t('openSeaDescription')} - - - {t('alwaysBeSureTo')} - - - - ) - ); -} diff --git a/ui/components/app/signature-request-original/index.scss b/ui/components/app/signature-request-original/index.scss index 01182eda8edb..281fae02fa07 100644 --- a/ui/components/app/signature-request-original/index.scss +++ b/ui/components/app/signature-request-original/index.scss @@ -62,11 +62,11 @@ } &__body { - width: 100%; - height: 100%; display: flex; flex-flow: column; - flex: 1 1 auto; + flex: 1 1 0; + min-height: 0; + max-width: 100%; } &__origin { @@ -92,18 +92,18 @@ } &__rows { - height: 100%; overflow-y: auto; overflow-x: hidden; border-top: 1px solid var(--color-border-default); display: flex; flex-flow: column; + flex: 1 1 0; + min-height: 0; } &__row { display: flex; flex-flow: column; - flex: 1 0 auto; } &__row-title { diff --git a/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap b/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap index 8a8ddd8a5aba..d1e6c7708ee2 100644 --- a/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap +++ b/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap @@ -369,7 +369,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xCD2...D826
@@ -445,7 +445,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xDea...beeF
@@ -580,7 +580,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xbBb...BBbB
@@ -656,7 +656,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xB0B...Ea57
@@ -732,7 +732,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xB0B...0000
@@ -1144,7 +1144,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xCD2...D826
@@ -1220,7 +1220,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xDea...beeF
@@ -1355,7 +1355,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xbBb...BBbB
@@ -1431,7 +1431,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xB0B...Ea57
@@ -1507,7 +1507,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xB0B...0000
diff --git a/ui/components/app/token-cell/token-cell.js b/ui/components/app/token-cell/token-cell.js index 1a18f9c838c7..60d8f304a879 100644 --- a/ui/components/app/token-cell/token-cell.js +++ b/ui/components/app/token-cell/token-cell.js @@ -3,9 +3,12 @@ import PropTypes from 'prop-types'; import React from 'react'; import { useSelector } from 'react-redux'; import AssetListItem from '../asset-list-item'; -import { getSelectedAddress } from '../../../selectors'; +import { getSelectedAddress, getTokenList } from '../../../selectors'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; +import { MultichainTokenListItem } from '../../multichain'; +import { ButtonLink, Text } from '../../component-library'; +import { TextColor } from '../../../helpers/constants/design-system'; export default function TokenCell({ address, @@ -19,39 +22,58 @@ export default function TokenCell({ }) { const userAddress = useSelector(getSelectedAddress); const t = useI18nContext(); - + const tokenList = useSelector(getTokenList); + const tokenData = Object.values(tokenList).find( + (token) => token.symbol === symbol, + ); + const title = tokenData?.name || symbol; + const tokenImage = tokenData?.iconUrl || image; const formattedFiat = useTokenFiatAmount(address, string, symbol); const warning = balanceError ? ( - + {t('troubleTokenBalances')} - event.stopPropagation()} - style={{ color: 'var(--color-warning-default)' }} + textProps={{ + color: TextColor.warningDefault, + }} > {t('here')} - - + + ) : null; return ( - + <> + {process.env.MULTICHAIN ? ( + onClick(address)} + tokenSymbol={symbol} + tokenImage={tokenImage} + primary={`${string || 0}`} + secondary={formattedFiat} + title={title} + /> + ) : ( + onClick(address)} + tokenAddress={address} + tokenSymbol={symbol} + tokenDecimals={decimals} + tokenImage={image} + warning={warning} + primary={`${string || 0}`} + secondary={formattedFiat} + isERC721={isERC721} + /> + )} + ); } diff --git a/ui/components/app/transaction-decoding/components/decoding/address/address.component.js b/ui/components/app/transaction-decoding/components/decoding/address/address.component.js index 9b576b5b3384..21220448ab5c 100644 --- a/ui/components/app/transaction-decoding/components/decoding/address/address.component.js +++ b/ui/components/app/transaction-decoding/components/decoding/address/address.component.js @@ -58,7 +58,7 @@ const Address = ({
setShowNicknamePopovers(true)} > {recipientToRender} diff --git a/ui/components/app/transaction-decoding/components/decoding/address/index.scss b/ui/components/app/transaction-decoding/components/decoding/address/index.scss index ca821dae80e2..71bf88fadc64 100644 --- a/ui/components/app/transaction-decoding/components/decoding/address/index.scss +++ b/ui/components/app/transaction-decoding/components/decoding/address/index.scss @@ -1,12 +1,13 @@ -.tx-insight-content { - .tx-insight-component-address { - display: flex; - align-items: center; - cursor: pointer; - overflow: visible; +.tx-insight-component-address { + display: flex; + align-items: center; + overflow: visible; + + &__sender-icon { + padding-right: 5px; + } - &__sender-icon { - padding-right: 5px; - } + &__name { + cursor: pointer; } } diff --git a/ui/components/component-library/button-base/README.mdx b/ui/components/component-library/button-base/README.mdx index 2390323792a3..f18d98636efa 100644 --- a/ui/components/component-library/button-base/README.mdx +++ b/ui/components/component-library/button-base/README.mdx @@ -102,7 +102,9 @@ import { ButtonBase } from '../../component-library'; When an `externalLink` prop is passed it will change the element to an anchor(`a`) tag and add the `target="_blank"` and `rel="noopener noreferrer"` attributes. + + ```jsx import { ButtonBase } from '../../component-library'; @@ -168,7 +170,7 @@ import { ICON_NAMES } from '../icon'; ### RTL -For RTL language support use the `textProps` prop to pass a `textDirection` prop. +For RTL language support use the `textDirection` prop. @@ -187,11 +189,28 @@ import { ButtonBase, ICON_NAMES } from '../../component-library'; Button Demo ; ``` + +### Ellipsis + +Use the boolean `ellipsis` prop to change the if the `ButtonBase` component to have an ellipsis. + +Note: this should only be used for dynamic/user generated content or addresses. Generally, button text should be succinct and only contain one or two words. + + + + + +```jsx +import { ButtonBase } from '../../component-library'; + + + This is long text example without ellipsis + This is long text example with ellipsis +; +``` diff --git a/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap b/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap index 6dc5bfbcf11d..9e3221f01a85 100644 --- a/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap +++ b/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap @@ -3,17 +3,13 @@ exports[`ButtonBase should render anchor element correctly by href and externalLink, href target and rel exist 1`] = ` `; @@ -21,14 +17,10 @@ exports[`ButtonBase should render anchor element correctly by href and externalL exports[`ButtonBase should render button element correctly and match snapshot 1`] = `
`; diff --git a/ui/components/component-library/button-base/button-base.js b/ui/components/component-library/button-base/button-base.js index 1a870284ad29..c79a757e91b5 100644 --- a/ui/components/component-library/button-base/button-base.js +++ b/ui/components/component-library/button-base/button-base.js @@ -15,6 +15,7 @@ import { Size, BorderRadius, BackgroundColor, + IconColor, } from '../../../helpers/constants/design-system'; import { BUTTON_BASE_SIZES } from './button-base.constants'; @@ -24,6 +25,7 @@ export const ButtonBase = ({ children, className, href, + ellipsis = false, externalLink, size = BUTTON_BASE_SIZES.MD, startIconName, @@ -34,6 +36,7 @@ export const ButtonBase = ({ disabled, iconLoadingProps, textProps, + color = TextColor.textDefault, ...props }) => { const Tag = href ? 'a' : as; @@ -42,13 +45,14 @@ export const ButtonBase = ({ props.rel = 'noopener noreferrer'; } return ( - - - {startIconName && ( - - )} - {children} - {endIconName && ( - - )} - + {startIconName && ( + + )} + {/* + * If children is a string and doesn't need truncation or loading + * prevent html bloat by rendering just the string + * otherwise render with wrapper to allow truncation or loading + */} + {typeof children === 'string' && !ellipsis && !loading ? ( + children + ) : ( + + {children} + + )} + {endIconName && ( + + )} {loading && ( )} - + ); }; @@ -126,6 +150,10 @@ ButtonBase.propTypes = { * When an `href` prop is passed, ButtonBase will automatically change the root element to be an `a` (anchor) tag */ href: PropTypes.string, + /** + * Used for long strings that can be cut off... + */ + ellipsis: PropTypes.bool, /** * Boolean indicating if the link targets external content, it will cause the link to open in a new tab */ diff --git a/ui/components/component-library/button-base/button-base.scss b/ui/components/component-library/button-base/button-base.scss index 42e6d24a9db7..6b4e249c2542 100644 --- a/ui/components/component-library/button-base/button-base.scss +++ b/ui/components/component-library/button-base/button-base.scss @@ -6,18 +6,13 @@ vertical-align: middle; user-select: none; - &:active, - &:hover { - color: var(--color-text-default); - } - &--block { display: block; width: 100%; } - &__content { - height: 100%; + &--ellipsis { + max-width: 100%; } &--size-sm { @@ -36,10 +31,6 @@ cursor: not-allowed; } - &--loading &__content { - color: transparent; - } - &--disabled, &:disabled { opacity: 0.3; diff --git a/ui/components/component-library/button-base/button-base.stories.js b/ui/components/component-library/button-base/button-base.stories.js index ffb512783106..0a4afed2c247 100644 --- a/ui/components/component-library/button-base/button-base.stories.js +++ b/ui/components/component-library/button-base/button-base.stories.js @@ -1,6 +1,7 @@ import React from 'react'; import { AlignItems, + Color, DISPLAY, FLEX_DIRECTION, Size, @@ -199,11 +200,18 @@ export const Rtl = (args) => ( {...args} startIconName={ICON_NAMES.ADD_SQUARE} endIconName={ICON_NAMES.ARROW_2_RIGHT} - textProps={{ - textDirection: TEXT_DIRECTIONS.RIGHT_TO_LEFT, - }} + textDirection={TEXT_DIRECTIONS.RIGHT_TO_LEFT} > Button Demo ); + +export const Ellipsis = (args) => ( + + Example without ellipsis + + Example with ellipsis + + +); diff --git a/ui/components/component-library/button-link/README.mdx b/ui/components/component-library/button-link/README.mdx index 292190b07731..4850161adc80 100644 --- a/ui/components/component-library/button-link/README.mdx +++ b/ui/components/component-library/button-link/README.mdx @@ -67,7 +67,7 @@ import { ButtonLink, Text, TextVariant } from '../../component-library'; - Inherits the font-size of the parent element and example with textProps override for a success color. + Inherits the font-size of the parent element and example with override for a success color. Learn more ``` @@ -103,7 +103,7 @@ import { ButtonLink } from '../../component-library'; ### External Link -When an `externalLink` prop is passed it adds the `target="_blank"` and `rel="noopener noreferrer"` attributes. +When an `externalLink` prop is passed it adds the `target="_blank"` and `rel="noopener noreferrer"` attributes. `rel="noreferrer noopener"` is used in links to prevent security vulnerabilities that can be exploited by malicious websites. It disables the window.opener property and prevents the new page from sending the referrer information, providing an additional layer of security. diff --git a/ui/components/component-library/button-link/__snapshots__/button-link.test.js.snap b/ui/components/component-library/button-link/__snapshots__/button-link.test.js.snap index f62717f6cd73..1a5c0af51d6b 100644 --- a/ui/components/component-library/button-link/__snapshots__/button-link.test.js.snap +++ b/ui/components/component-library/button-link/__snapshots__/button-link.test.js.snap @@ -3,14 +3,10 @@ exports[`ButtonLink should render button element correctly 1`] = `
`; diff --git a/ui/components/component-library/button-link/button-link.js b/ui/components/component-library/button-link/button-link.js index aa77a788ad7b..c7d7dd14fe47 100644 --- a/ui/components/component-library/button-link/button-link.js +++ b/ui/components/component-library/button-link/button-link.js @@ -3,26 +3,25 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { ButtonBase } from '../button-base'; -import { Text } from '../text'; import { BackgroundColor, Color, Size, - TextVariant, } from '../../../helpers/constants/design-system'; import { BUTTON_LINK_SIZES } from './button-link.constants'; export const ButtonLink = ({ className, danger, + disabled, size = Size.auto, - textProps, ...props }) => { return ( ); }; @@ -62,15 +54,15 @@ ButtonLink.propTypes = { * Boolean to change button type to Danger when true */ danger: PropTypes.bool, + /** + * Boolean to disable button + */ + disabled: PropTypes.bool, /** * Possible size values: 'SIZES.AUTO'(auto), 'SIZES.SM'(32px), 'SIZES.MD'(40px), 'SIZES.LG'(48px), 'SIZES.INHERIT'(inherits parents font-size) * Default value is 'SIZES.AUTO'. */ size: PropTypes.oneOf(Object.values(BUTTON_LINK_SIZES)), - /** - * textProps accepts all the props from Text component - */ - textProps: PropTypes.shape(Text.PropTypes), /** * ButtonLink accepts all the props from ButtonBase */ diff --git a/ui/components/component-library/button-link/button-link.scss b/ui/components/component-library/button-link/button-link.scss index 5c8a4c10b89f..30e40042c672 100644 --- a/ui/components/component-library/button-link/button-link.scss +++ b/ui/components/component-library/button-link/button-link.scss @@ -1,17 +1,14 @@ .mm-button-link { &:hover { - color: var(--color-primary-default); opacity: 0.5; } &:active { - color: var(--color-primary-default); opacity: 0.5; } &--disabled { &:hover { - color: var(--color-primary-default); opacity: 0.3; } diff --git a/ui/components/component-library/button-link/button-link.stories.js b/ui/components/component-library/button-link/button-link.stories.js index b97d8de36735..bda5a27f4083 100644 --- a/ui/components/component-library/button-link/button-link.stories.js +++ b/ui/components/component-library/button-link/button-link.stories.js @@ -162,12 +162,12 @@ export const SizeStory = (args) => ( - Inherits the font-size of the parent element and example with textProps - override for a success color.{' '} + Inherits the font-size of the parent element and example with override for + a success color.{' '} Learn more diff --git a/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap b/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap index 8b0c03a89fca..8768e1c7c56d 100644 --- a/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap +++ b/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap @@ -3,14 +3,10 @@ exports[`ButtonPrimary should render button element correctly 1`] = `
`; diff --git a/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap b/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap index c6f9a31779c5..fcc63172f02d 100644 --- a/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap +++ b/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap @@ -3,14 +3,10 @@ exports[`ButtonSecondary should render button element correctly 1`] = `
`; diff --git a/ui/components/component-library/button/__snapshots__/button.test.js.snap b/ui/components/component-library/button/__snapshots__/button.test.js.snap index a0685c51a84e..7d795a179e20 100644 --- a/ui/components/component-library/button/__snapshots__/button.test.js.snap +++ b/ui/components/component-library/button/__snapshots__/button.test.js.snap @@ -3,14 +3,10 @@ exports[`Button should render button element correctly 1`] = `
`; @@ -18,34 +14,22 @@ exports[`Button should render button element correctly 1`] = ` exports[`Button should render with different button types 1`] = `
`; diff --git a/ui/components/component-library/component-library-components.scss b/ui/components/component-library/component-library-components.scss index 6a10d8f99656..4068f6af5cf6 100644 --- a/ui/components/component-library/component-library-components.scss +++ b/ui/components/component-library/component-library-components.scss @@ -29,3 +29,6 @@ @import 'form-text-field/form-text-field'; @import 'banner-alert/banner-alert'; @import 'banner-tip/banner-tip'; +@import 'modal-content/modal-content'; +@import 'modal-overlay/modal-overlay'; + diff --git a/ui/components/component-library/index.js b/ui/components/component-library/index.js index fbf0282f809b..33e8ab60480e 100644 --- a/ui/components/component-library/index.js +++ b/ui/components/component-library/index.js @@ -31,6 +31,8 @@ export { Text, TEXT_DIRECTIONS, INVISIBLE_CHARACTER } from './text'; export { Input, INPUT_TYPES } from './input'; export { TextField, TEXT_FIELD_TYPES, TEXT_FIELD_SIZES } from './text-field'; export { TextFieldSearch } from './text-field-search'; +export { ModalContent, ModalContentSize } from './modal-content'; +export { ModalOverlay } from './modal-overlay'; // Molecules export { BannerBase } from './banner-base'; diff --git a/ui/components/component-library/modal-content/README.mdx b/ui/components/component-library/modal-content/README.mdx new file mode 100644 index 000000000000..3d755d954388 --- /dev/null +++ b/ui/components/component-library/modal-content/README.mdx @@ -0,0 +1,115 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; + +import { ModalContent } from './modal-content'; + +# ModalContent + +`ModalContent` is the container for the modal dialog's content + + + + + +## Props + +The `ModalContent` accepts all props below as well as all [Box](/docs/components-ui-box--default-story#props) component props + + + +### Children + +Use the `children` prop to render the content of `ModalContent` + + + + + +```jsx +import { ModalContent, Text } from '../../component-library'; + + + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Distinctio, + reiciendis assumenda dolorum mollitia saepe, optio at aliquam molestias + omnis quae corporis nesciunt natus, quas tempore ut ullam eaque fuga. Velit. + +; +``` + +### Size + +Currently the `ModalContent` supports a single size, this decision was made after we ran an audit on all modal sizes in the extension codebase and found that all modals could be made to fit the `ModalContentSize.Sm`(360px) size. + +If you do require a larger modal size you can use the Box props or add a className to override the default size. + + + + + +```jsx +import { BLOCK_SIZES } from '../../../helpers/constants/design-system'; +import { ModalContent,s Text } from '../../component-library'; + + + ModalContentSize.Sm default and only size 360px max-width + + + + Using width Box props and responsive array props
[ + BLOCK_SIZES.FULL, BLOCK_SIZES.THREE_FOURTHS, BLOCK_SIZES.HALF, + BLOCK_SIZES.ONE_THIRD, ] +
+
+ + Adding a className and setting a max width (max-width: 800px) + +``` + +### Modal Content Ref + +Use the `modalContentRef` prop to pass a ref to the `ModalContent` component. This is primarily used with the `closeOnOutsideClick` prop on the `Modal` component. It allows the `Modal` to close when the user clicks outside of the `ModalContent` component. + + + + + +```jsx +import React, { useEffect, useRef, useState } from 'react'; +import { ModalContent, Text } from '../../component-library'; + +const [show, setShow] = useState(false); +const modalContentRef = useRef(null); + +const handleClickOutside = (event: MouseEvent) => { + if ( + modalContentRef?.current && + !modalContentRef.current.contains(event.target as Node) + ) { + setShow(false); + } +}; + +useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; +}, []); + + +{show && ( + + Click outside of this ModalContent to close + +)} +``` diff --git a/ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap b/ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap new file mode 100644 index 000000000000..c8fa28c96d0a --- /dev/null +++ b/ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalContent should match snapshot 1`] = ` +
+
+ test +
+
+`; diff --git a/ui/components/component-library/modal-content/index.ts b/ui/components/component-library/modal-content/index.ts new file mode 100644 index 000000000000..ef6fb6595300 --- /dev/null +++ b/ui/components/component-library/modal-content/index.ts @@ -0,0 +1,3 @@ +export { ModalContent } from './modal-content'; +export { ModalContentSize } from './modal-content.types'; +export type { ModalContentProps } from './modal-content.types'; diff --git a/ui/components/component-library/modal-content/modal-content.scss b/ui/components/component-library/modal-content/modal-content.scss new file mode 100644 index 000000000000..3c3bc00fb64f --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.scss @@ -0,0 +1,17 @@ +.mm-modal-content { + --modal-content-size: var(--size, 360px); + + // Currently there is only use case for one size of ModalContent in the extension + // See audit https://www.figma.com/file/hxYqloYgmVcgsoiVqmGZ8K/Modal-Audit?node-id=481%3A244&t=XITeuRB1pRc09hiG-1 + // Not to say there won't be more in the future, but to prevent redundant code there is only one for now + &--size-sm { + --size: 360px; + + max-width: var(--modal-content-size); + } + + position: relative; + box-shadow: var(--shadow-size-lg) var(--color-shadow-default); + max-height: calc(100% - 32px); // allow for 16px padding on top and bottom + overflow: auto; +} diff --git a/ui/components/component-library/modal-content/modal-content.stories.tsx b/ui/components/component-library/modal-content/modal-content.stories.tsx new file mode 100644 index 000000000000..020e170262d1 --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.stories.tsx @@ -0,0 +1,144 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { + DISPLAY, + JustifyContent, + AlignItems, + BLOCK_SIZES, + TextVariant, + TEXT_ALIGN, +} from '../../../helpers/constants/design-system'; + +import Box from '../../ui/box'; + +import { Button, Text } from '..'; + +import { ModalContent } from './modal-content'; +import { ModalContentSize } from './modal-content.types'; + +import README from './README.mdx'; + +export default { + title: 'Components/ComponentLibrary/ModalContent', + component: ModalContent, + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + className: { + control: 'text', + }, + children: { + control: 'text', + }, + size: { + control: 'select', + options: Object.values(ModalContentSize).map((value) => + value.toLowerCase(), + ), + }, + }, + args: { + children: 'Modal Content', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const DefaultStory = Template.bind({}); +DefaultStory.storyName = 'Default'; + +/* + * !!TODO: Replace with ModalHeader component + */ +const ModalHeader = () => ( + <> + + + + Modal Header + + + + +); + +export const Children: ComponentStory = (args) => ( + + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Distinctio, + reiciendis assumenda dolorum mollitia saepe, optio at aliquam molestias + omnis quae corporis nesciunt natus, quas tempore ut ullam eaque fuga. + Velit. + + +); + +export const Size: ComponentStory = (args) => ( + <> + + ModalContentSize.Sm default and only size 360px max-width + + + + Using width Box props and responsive array props
[ + BLOCK_SIZES.FULL, BLOCK_SIZES.THREE_FOURTHS, BLOCK_SIZES.HALF, + BLOCK_SIZES.ONE_THIRD, ] +
+
+ + Adding a className and setting a max width (max-width: 800px) + + +); + +export const ModalContentRef: ComponentStory = (args) => { + const [show, setShow] = useState(false); + const modalContentRef = useRef(null); + const handleClickOutside = (event: MouseEvent) => { + if ( + modalContentRef?.current && + !modalContentRef.current.contains(event.target as Node) + ) { + setShow(false); + } + }; + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + return ( + <> + + {show && ( + + Click outside of this ModalContent to close + + )} + + ); +}; diff --git a/ui/components/component-library/modal-content/modal-content.test.tsx b/ui/components/component-library/modal-content/modal-content.test.tsx new file mode 100644 index 000000000000..64b6a96dfe49 --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.test.tsx @@ -0,0 +1,39 @@ +/* eslint-disable jest/require-top-level-describe */ +import { render } from '@testing-library/react'; +import React from 'react'; + +import { ModalContent } from './modal-content'; +import { ModalContentSize } from './modal-content.types'; + +describe('ModalContent', () => { + it('should render with text inside the ModalContent', () => { + const { getByText } = render(test); + expect(getByText('test')).toBeDefined(); + expect(getByText('test')).toHaveClass('mm-modal-content'); + }); + it('should match snapshot', () => { + const { container } = render(test); + expect(container).toMatchSnapshot(); + }); + it('should render with and additional className', () => { + const { getByText } = render( + test, + ); + expect(getByText('test')).toHaveClass('test-class'); + }); + it('should render with size sm', () => { + const { getByText } = render( + <> + default + sm + , + ); + expect(getByText('sm')).toHaveClass('mm-modal-content--size-sm'); + expect(getByText('default')).toHaveClass('mm-modal-content--size-sm'); + }); + it('should render with a ref', () => { + const ref = React.createRef(); + render(test); + expect(ref.current).toBeDefined(); + }); +}); diff --git a/ui/components/component-library/modal-content/modal-content.tsx b/ui/components/component-library/modal-content/modal-content.tsx new file mode 100644 index 000000000000..064a4b4e71cc --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import classnames from 'classnames'; + +import { + BackgroundColor, + BorderRadius, + BLOCK_SIZES, +} from '../../../helpers/constants/design-system'; + +import Box from '../../ui/box/box'; + +import { ModalContentProps, ModalContentSize } from './modal-content.types'; + +export const ModalContent = ({ + className = '', + children, + size = ModalContentSize.Sm, + width, + modalContentRef, // Would have preferred to forwardRef but it's not trivial in TypeScript. Will update once we have an established pattern + ...props +}: ModalContentProps) => ( + + {children} + +); diff --git a/ui/components/component-library/modal-content/modal-content.types.ts b/ui/components/component-library/modal-content/modal-content.types.ts new file mode 100644 index 000000000000..801a04b36323 --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.types.ts @@ -0,0 +1,40 @@ +import React from 'react'; +import type { BoxProps, BoxWidth, BoxWidthArray } from '../../ui/box/box.d'; +import { Size } from '../../../helpers/constants/design-system'; + +/* + * ModalContent sizes + * Currently there is only use case for one size of ModalContent in the extension + * See audit https://www.figma.com/file/hxYqloYgmVcgsoiVqmGZ8K/Modal-Audit?node-id=481%3A244&t=XITeuRB1pRc09hiG-1 + * Not to say there won't be more in the future, but to prevent redundant code there is only one for now + */ +export enum ModalContentSize { + Sm = Size.SM, +} + +export interface ModalContentProps extends BoxProps { + /** + * The additional className of the ModalContent component + */ + className?: string; + /** + * The content of the ModalContent component + */ + children?: React.ReactNode; + /** + * The size of ModalContent + * Currently only one size is supported ModalContentSize.Sm 360px + * See docs for more info + */ + size?: ModalContentSize; + /** + * To override the default width of the ModalContent component + * Accepts all BLOCK_SIZES from design-system + */ + width?: BoxWidth | BoxWidthArray; + /** + * The ref of the ModalContent component + * Used with Modal and closeOnOutsideClick prop + */ + modalContentRef?: React.RefObject; +} diff --git a/ui/components/component-library/modal-overlay/README.mdx b/ui/components/component-library/modal-overlay/README.mdx new file mode 100644 index 000000000000..cb42da5d003f --- /dev/null +++ b/ui/components/component-library/modal-overlay/README.mdx @@ -0,0 +1,40 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; + +import { ModalOverlay } from './modal-overlay'; + +# ModalOverlay + +`ModalOverlay` is a transparent overlay that covers the entire screen. It is used to dim the background when a modal is open. + + + + + +## Props + +The `ModalOverlay` accepts all props below as well as all [Box](/docs/components-ui-box--default-story#props) component props + + + +### On Click + +Use the `onClick` prop to handle clicks on the overlay + + + + + +```jsx +import React, { useState } from 'react'; +import { ModalOverlay } from '../../component-library'; + +const [open, setOpen] = useState(false); +const handleOnClick = () => { + setOpen(!open); +}; + +; +{ + open && ; +} +``` diff --git a/ui/components/component-library/modal-overlay/__snapshots__/modal-overlay.test.tsx.snap b/ui/components/component-library/modal-overlay/__snapshots__/modal-overlay.test.tsx.snap new file mode 100644 index 000000000000..10f22ae8d9b9 --- /dev/null +++ b/ui/components/component-library/modal-overlay/__snapshots__/modal-overlay.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalOverlay should match snapshot 1`] = ` +
+
+
+`; diff --git a/ui/components/component-library/modal-overlay/index.ts b/ui/components/component-library/modal-overlay/index.ts new file mode 100644 index 000000000000..bc283f8985c2 --- /dev/null +++ b/ui/components/component-library/modal-overlay/index.ts @@ -0,0 +1,2 @@ +export { ModalOverlay } from './modal-overlay'; +export type { ModalOverlayProps } from './modal-overlay.types'; diff --git a/ui/components/component-library/modal-overlay/modal-overlay.scss b/ui/components/component-library/modal-overlay/modal-overlay.scss new file mode 100644 index 000000000000..afa528a1853c --- /dev/null +++ b/ui/components/component-library/modal-overlay/modal-overlay.scss @@ -0,0 +1,7 @@ +.mm-modal-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} diff --git a/ui/components/component-library/modal-overlay/modal-overlay.stories.tsx b/ui/components/component-library/modal-overlay/modal-overlay.stories.tsx new file mode 100644 index 000000000000..fb2cec5970de --- /dev/null +++ b/ui/components/component-library/modal-overlay/modal-overlay.stories.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { ModalOverlay } from './modal-overlay'; + +import README from './README.mdx'; + +export default { + title: 'Components/ComponentLibrary/ModalOverlay', + component: ModalOverlay, + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + className: { + control: 'text', + }, + onClick: { + action: 'onClick', + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const DefaultStory = Template.bind({}); +DefaultStory.storyName = 'Default'; + +export const OnClick: ComponentStory = (args) => { + const [open, setOpen] = useState(false); + const handleOnClick = () => { + setOpen(!open); + }; + return ( + <> + + {open && } + + ); +}; diff --git a/ui/components/component-library/modal-overlay/modal-overlay.test.tsx b/ui/components/component-library/modal-overlay/modal-overlay.test.tsx new file mode 100644 index 000000000000..e3347176676b --- /dev/null +++ b/ui/components/component-library/modal-overlay/modal-overlay.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import { ModalOverlay } from './modal-overlay'; + +describe('ModalOverlay', () => { + it('should render ModalOverlay without error', () => { + const { getByTestId } = render( + , + ); + expect(getByTestId('modal-overlay')).toBeDefined(); + expect(getByTestId('modal-overlay')).toHaveClass('mm-modal-overlay'); + }); + it('should match snapshot', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it('should render with and additional className', () => { + const { getByTestId } = render( + , + ); + expect(getByTestId('modal-overlay')).toHaveClass('test-class'); + }); + it('should fire the onClick function when clicked', () => { + const onClick = jest.fn(); + const { getByTestId } = render( + , + ); + getByTestId('modal-overlay').click(); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/ui/components/component-library/modal-overlay/modal-overlay.tsx b/ui/components/component-library/modal-overlay/modal-overlay.tsx new file mode 100644 index 000000000000..76b9586cba88 --- /dev/null +++ b/ui/components/component-library/modal-overlay/modal-overlay.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import classnames from 'classnames'; + +import { + BackgroundColor, + BLOCK_SIZES, +} from '../../../helpers/constants/design-system'; + +import Box from '../../ui/box/box'; + +import { ModalOverlayProps } from './modal-overlay.types'; + +export const ModalOverlay: React.FC = ({ + onClick, + className = '', + ...props +}) => ( + +); + +export default ModalOverlay; diff --git a/ui/components/component-library/modal-overlay/modal-overlay.types.ts b/ui/components/component-library/modal-overlay/modal-overlay.types.ts new file mode 100644 index 000000000000..a1045bc96783 --- /dev/null +++ b/ui/components/component-library/modal-overlay/modal-overlay.types.ts @@ -0,0 +1,13 @@ +import { BoxProps } from '../../ui/box/box.d'; + +export interface ModalOverlayProps extends BoxProps { + /** + * onClick handler for the overlay + * Not necessary when used with Modal and closeOnClickOutside is true + */ + onClick?: (event: React.MouseEvent) => void; + /** + * Additional className to add to the ModalOverlay + */ + className?: string; +} diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js new file mode 100644 index 000000000000..2d02363b6c99 --- /dev/null +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js @@ -0,0 +1,147 @@ +import React, { useContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { getAccountLink } from '@metamask/etherscan-link'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + getRpcPrefsForCurrentProvider, + getBlockExplorerLinkText, + getCurrentChainId, +} from '../../../selectors'; +import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; +import { Menu, MenuItem } from '../../ui/menu'; +import { ICON_NAMES, Text } from '../../component-library'; +import { EVENT_NAMES, EVENT } from '../../../../shared/constants/metametrics'; +import { getURLHostName } from '../../../helpers/utils/util'; +import { showModal } from '../../../store/actions'; +import { TextVariant } from '../../../helpers/constants/design-system'; + +export const AccountListItemMenu = ({ + anchorElement, + blockExplorerUrlSubTitle, + onClose, + closeMenu, + isRemovable, + identity, +}) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const dispatch = useDispatch(); + const history = useHistory(); + + const chainId = useSelector(getCurrentChainId); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const addressLink = getAccountLink(identity.address, chainId, rpcPrefs); + + const blockExplorerLinkText = useSelector(getBlockExplorerLinkText); + const openBlockExplorer = () => { + trackEvent({ + event: EVENT_NAMES.EXTERNAL_LINK_CLICKED, + category: EVENT.CATEGORIES.NAVIGATION, + properties: { + link_type: EVENT.EXTERNAL_LINK_TYPES.ACCOUNT_TRACKER, + location: 'Account Options', + url_domain: getURLHostName(addressLink), + }, + }); + global.platform.openTab({ + url: addressLink, + }); + onClose(); + }; + + const routeToAddBlockExplorerUrl = () => { + history.push(`${NETWORKS_ROUTE}#blockExplorerUrl`); + }; + + return ( + + + {t('viewOnExplorer')} + + { + dispatch(showModal({ name: 'ACCOUNT_DETAILS' })); + trackEvent({ + event: EVENT_NAMES.NAV_ACCOUNT_DETAILS_OPENED, + category: EVENT.CATEGORIES.NAVIGATION, + properties: { + location: 'Account Options', + }, + }); + onClose(); + closeMenu?.(); + }} + iconName={ICON_NAMES.SCAN_BARCODE} + > + {t('accountDetails')} + + {isRemovable ? ( + { + dispatch( + showModal({ + name: 'CONFIRM_REMOVE_ACCOUNT', + identity, + }), + ); + onClose(); + }} + iconName={ICON_NAMES.TRASH} + > + {t('removeAccount')} + + ) : null} + + ); +}; + +AccountListItemMenu.propTypes = { + /** + * Element that the menu should display next to + */ + anchorElement: PropTypes.instanceOf(window.Element), + /** + * Function that executes when the menu is closed + */ + onClose: PropTypes.func.isRequired, + /** + * Function that closes the menu + */ + closeMenu: PropTypes.func, + /** + * Domain of the block explorer + */ + blockExplorerUrlSubTitle: PropTypes.string, + /** + * Represents if the account should be removable + */ + isRemovable: PropTypes.bool.isRequired, + /** + * Identity of the account + */ + /** + * Identity of the account + */ + identity: PropTypes.shape({ + name: PropTypes.string.isRequired, + address: PropTypes.string.isRequired, + balance: PropTypes.string.isRequired, + }).isRequired, +}; diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js new file mode 100644 index 000000000000..5ae50a1778ad --- /dev/null +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { AccountListItemMenu } from '.'; + +export default { + title: 'Components/Multichain/AccountListItemMenu', + component: AccountListItemMenu, + argTypes: { + anchorElement: { + control: 'window.Element', + }, + onClose: { + action: 'onClose', + }, + closeMenu: { + action: 'closeMenu', + }, + blockExplorerUrlSubTitle: { + control: 'text', + }, + isRemovable: { + control: 'boolean', + }, + identity: { + control: 'object', + }, + }, + args: { + anchorElement: null, + identity: { + address: '"0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e"', + name: 'Account 1', + balance: '0x152387ad22c3f0', + tokenBalance: '32.09 ETH', + }, + isRemovable: true, + blockExplorerUrlSubTitle: 'etherscan.io', + }, +}; + +export const DefaultStory = (args) => ; +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js new file mode 100644 index 000000000000..f5b91104bd37 --- /dev/null +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js @@ -0,0 +1,54 @@ +/* eslint-disable jest/require-top-level-describe */ +import React from 'react'; +import { renderWithProvider, fireEvent } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import { AccountListItemMenu } from '.'; + +const identity = { + ...mockState.metamask.identities[ + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' + ], + balance: '0x152387ad22c3f0', +}; + +const DEFAULT_PROPS = { + identity, + onClose: jest.fn(), + onHide: jest.fn(), + isRemovable: false, +}; + +const render = (props = {}) => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + }, + }); + const allProps = { ...DEFAULT_PROPS, ...props }; + return renderWithProvider(, store); +}; + +describe('AccountListItem', () => { + it('renders the URL for explorer', () => { + const blockExplorerDomain = 'etherscan.io'; + const { getByText, getByTestId } = render({ + blockExplorerUrlSubTitle: blockExplorerDomain, + }); + expect(getByText(blockExplorerDomain)).toBeInTheDocument(); + + Object.defineProperty(global, 'platform', { + value: { + openTab: jest.fn(), + }, + }); + const openExplorerTabSpy = jest.spyOn(global.platform, 'openTab'); + fireEvent.click(getByTestId('account-list-menu-open-explorer')); + expect(openExplorerTabSpy).toHaveBeenCalled(); + }); + + it('renders remove icon with isRemovable', () => { + const { getByTestId } = render({ isRemovable: true }); + expect(getByTestId('account-list-menu-remove')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/multichain/account-list-item-menu/index.js b/ui/components/multichain/account-list-item-menu/index.js new file mode 100644 index 000000000000..6c0f7c65a38f --- /dev/null +++ b/ui/components/multichain/account-list-item-menu/index.js @@ -0,0 +1 @@ +export { AccountListItemMenu } from './account-list-item-menu'; diff --git a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap new file mode 100644 index 000000000000..c44eb702947e --- /dev/null +++ b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountListItem renders AccountListItem component and shows account name, address, and balance 1`] = ` +
+ +
+`; diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js new file mode 100644 index 000000000000..bbfd277a4570 --- /dev/null +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -0,0 +1,253 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { useSelector } from 'react-redux'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { getRpcPrefsForCurrentProvider } from '../../../selectors'; +import { getURLHostName, shortenAddress } from '../../../helpers/utils/util'; + +import { AccountListItemMenu } from '..'; +import Box from '../../ui/box/box'; +import { + AvatarAccount, + ButtonIcon, + Text, + ICON_NAMES, + ICON_SIZES, + AvatarFavicon, + Tag, +} from '../../component-library'; +import { + Color, + TEXT_ALIGN, + AlignItems, + DISPLAY, + TextVariant, + FLEX_DIRECTION, + BorderRadius, + JustifyContent, + Size, + BorderColor, +} from '../../../helpers/constants/design-system'; +import { HardwareKeyringNames } from '../../../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../../../shared/constants/keyring'; +import UserPreferencedCurrencyDisplay from '../../app/user-preferenced-currency-display/user-preferenced-currency-display.component'; +import { SECONDARY, PRIMARY } from '../../../helpers/constants/common'; +import { findKeyringForAddress } from '../../../ducks/metamask/metamask'; +import Tooltip from '../../ui/tooltip/tooltip'; + +const MAXIMUM_CURRENCY_DECIMALS = 3; +const MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP = 17; + +function getLabel(keyring = {}, t) { + const { type } = keyring; + switch (type) { + case KeyringType.qr: + return HardwareKeyringNames.qr; + case KeyringType.imported: + return t('imported'); + case KeyringType.trezor: + return HardwareKeyringNames.trezor; + case KeyringType.ledger: + return HardwareKeyringNames.ledger; + case KeyringType.lattice: + return HardwareKeyringNames.lattice; + default: + return null; + } +} + +export const AccountListItem = ({ + identity, + selected = false, + onClick, + closeMenu, + connectedAvatar, + connectedAvatarName, +}) => { + const t = useI18nContext(); + const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false); + const ref = useRef(false); + const keyring = useSelector((state) => + findKeyringForAddress(state, identity.address), + ); + const label = getLabel(keyring, t); + + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const { blockExplorerUrl } = rpcPrefs; + const blockExplorerUrlSubTitle = getURLHostName(blockExplorerUrl); + + return ( + { + e.preventDefault(); + // Without this check, the account will be selected after + // the account options menu closes + if (!accountOptionsMenuOpen) { + onClick(); + } + }} + > + {selected && ( + + )} + + + + + + {identity.name.length > MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP ? ( + + {identity.name} + + ) : ( + identity.name + )} + + + {connectedAvatar ? ( + + ) : null} + + + + + + + + + {shortenAddress(identity.address)} + + + + + + {label ? ( + + ) : null} + +
+ { + e.stopPropagation(); + setAccountOptionsMenuOpen(true); + }} + as="div" + tabIndex={0} + onKeyPress={(e) => { + if (e.key === 'Enter') { + setAccountOptionsMenuOpen(true); + } + }} + data-testid="account-list-item-menu-button" + /> + {accountOptionsMenuOpen ? ( + setAccountOptionsMenuOpen(false)} + isRemovable={keyring?.type !== KeyringType.hdKeyTree} + closeMenu={closeMenu} + /> + ) : null} +
+
+ ); +}; + +AccountListItem.propTypes = { + /** + * Identity of the account + */ + identity: PropTypes.shape({ + name: PropTypes.string.isRequired, + address: PropTypes.string.isRequired, + balance: PropTypes.string.isRequired, + }).isRequired, + /** + * Represents if this account is currently selected + */ + selected: PropTypes.bool, + /** + * Function to execute when the item is clicked + */ + onClick: PropTypes.func.isRequired, + /** + * Function that closes the menu + */ + closeMenu: PropTypes.func, + /** + * File location of the avatar icon + */ + connectedAvatar: PropTypes.string, + /** + * Text used as the avatar alt text + */ + connectedAvatarName: PropTypes.string, +}; + +AccountListItem.displayName = 'AccountListItem'; diff --git a/ui/components/multichain/account-list-item/account-list-item.stories.js b/ui/components/multichain/account-list-item/account-list-item.stories.js new file mode 100644 index 000000000000..627f80bc9127 --- /dev/null +++ b/ui/components/multichain/account-list-item/account-list-item.stories.js @@ -0,0 +1,129 @@ +import React from 'react'; + +import { Provider } from 'react-redux'; + +import testData from '../../../../.storybook/test-data'; +import configureStore from '../../../store/store'; +import { AccountListItem } from '.'; + +const store = configureStore(testData); + +const [chaosAddress, simpleAddress, hardwareAddress] = Object.keys( + testData.metamask.identities, +); + +const SIMPLE_IDENTITY = { + ...testData.metamask.identities[simpleAddress], + balance: '0x152387ad22c3f0', +}; + +const HARDWARE_IDENTITY = { + ...testData.metamask.identities[hardwareAddress], + balance: '0x152387ad22c3f0', +}; + +const CHAOS_IDENTITY = { + ...testData.metamask.identities[chaosAddress], + balance: '0x152387ad22c3f0', +}; + +const CONTAINER_STYLES = { + style: { + width: '328px', + border: '1px solid var(--color-border-muted)', + }, +}; + +const onClick = () => console.log('Clicked account!'); + +export default { + title: 'Components/Multichain/AccountListItem', + component: AccountListItem, + argTypes: { + identity: { + control: 'object', + }, + selected: { + control: 'boolean', + }, + onClick: { + action: 'onClick', + }, + closeMenu: { + action: 'closeMenu', + }, + connectedAvatar: { + control: 'text', + }, + connectedAvatarName: { + control: 'text', + }, + }, + args: { + identity: SIMPLE_IDENTITY, + onClick, + }, +}; + +export const DefaultStory = (args) => ( +
+ +
+); + +export const SelectedItem = (args) => ( +
+ +
+); +SelectedItem.args = { selected: true }; + +export const HardwareItem = (args) => ( +
+ +
+); +HardwareItem.args = { identity: HARDWARE_IDENTITY }; +HardwareItem.decorators = [ + (story) => {story()}, +]; + +export const SelectedHardwareItem = (args) => ( +
+ +
+); +SelectedHardwareItem.args = { identity: HARDWARE_IDENTITY, selected: true }; +SelectedHardwareItem.decorators = [ + (story) => {story()}, +]; + +export const ChaosDataItem = (args) => ( +
+ +
+); +ChaosDataItem.args = { identity: CHAOS_IDENTITY }; + +export const ConnectedSiteItem = (args) => ( +
+ +
+); +ConnectedSiteItem.args = { + connectedAvatar: 'https://uniswap.org/favicon.ico', + connectedAvatarName: 'Uniswap', +}; + +export const ConnectedSiteChaosItem = (args) => ( +
+ +
+); +ConnectedSiteChaosItem.args = { + identity: CHAOS_IDENTITY, + connectedAvatar: 'https://uniswap.org/favicon.ico', + connectedAvatarName: 'Uniswap', +}; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/account-list-item/account-list-item.test.js b/ui/components/multichain/account-list-item/account-list-item.test.js new file mode 100644 index 000000000000..b8e3e292e360 --- /dev/null +++ b/ui/components/multichain/account-list-item/account-list-item.test.js @@ -0,0 +1,103 @@ +/* eslint-disable jest/require-top-level-describe */ +import React from 'react'; +import { screen, fireEvent } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import { shortenAddress } from '../../../helpers/utils/util'; +import { AccountListItem } from '.'; + +const identity = { + ...mockState.metamask.identities[ + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' + ], + balance: '0x152387ad22c3f0', +}; + +const DEFAULT_PROPS = { + identity, + onClick: jest.fn(), +}; + +const render = (props = {}) => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + }, + }); + const allProps = { ...DEFAULT_PROPS, ...props }; + return renderWithProvider(, store); +}; + +describe('AccountListItem', () => { + it('renders AccountListItem component and shows account name, address, and balance', () => { + const { container } = render(); + expect(screen.getByText(identity.name)).toBeInTheDocument(); + expect( + screen.getByText(shortenAddress(identity.address)), + ).toBeInTheDocument(); + expect(document.querySelector('[title="0.006 ETH"]')).toBeInTheDocument(); + + expect(container).toMatchSnapshot(); + }); + + it('renders selected block when account is selected', () => { + render({ selected: true }); + expect( + document.querySelector('.multichain-account-list-item--selected'), + ).toBeInTheDocument(); + }); + + it('renders the account name tooltip for long names', () => { + render({ + selected: true, + identity: { + ...identity, + name: 'This is a super long name that requires tooltip', + }, + }); + expect( + document.querySelector('.multichain-account-list-item__tooltip'), + ).toBeInTheDocument(); + }); + + it('renders the tree-dot menu to lauch the details menu', () => { + render(); + const optionsButton = document.querySelector( + '[aria-label="Test Account Options"]', + ); + expect(optionsButton).toBeInTheDocument(); + fireEvent.click(optionsButton); + expect(document.querySelector('.menu__background')).toBeInTheDocument(); + }); + + it('executes the action when the item is clicked', () => { + const onClick = jest.fn(); + render({ onClick }); + const item = document.querySelector('.multichain-account-list-item'); + fireEvent.click(item); + expect(onClick).toHaveBeenCalled(); + }); + + it('clicking the three-dot menu opens up options', () => { + const onClick = jest.fn(); + render({ onClick }); + const item = document.querySelector( + '[data-testid="account-list-item-menu-button"]', + ); + fireEvent.click(item); + expect( + document.querySelector('[data-testid="account-list-menu-open-explorer"]'), + ).toBeInTheDocument(); + }); + + it('renders connected site icon', () => { + const connectedAvatarName = 'Uniswap'; + const { getByAltText } = render({ + connectedAvatar: 'https://uniswap.org/favicon.ico', + connectedAvatarName, + }); + + expect(getByAltText(`${connectedAvatarName} logo`)).toBeInTheDocument(); + }); +}); diff --git a/ui/components/multichain/account-list-item/index.js b/ui/components/multichain/account-list-item/index.js new file mode 100644 index 000000000000..0926db843bc8 --- /dev/null +++ b/ui/components/multichain/account-list-item/index.js @@ -0,0 +1 @@ +export { AccountListItem } from './account-list-item'; diff --git a/ui/components/multichain/account-list-item/index.scss b/ui/components/multichain/account-list-item/index.scss new file mode 100644 index 000000000000..5d74ee152a41 --- /dev/null +++ b/ui/components/multichain/account-list-item/index.scss @@ -0,0 +1,32 @@ +.multichain-account-list-item { + position: relative; + width: 100%; + + &:not(.account-list-item--selected) { + &:hover, + &:focus-within { + background: var(--color-background-default-hover); + } + } + + &__selected-indicator { + width: 4px; + height: calc(100% - 8px); + position: absolute; + top: 4px; + left: 4px; + } + + &__content { + overflow: hidden; + flex: 1; + } + + .currency-display-component { + flex-wrap: nowrap; + } + + &__tooltip { + display: inline; + } +} diff --git a/ui/components/multichain/account-list-menu/account-list-menu.js b/ui/components/multichain/account-list-menu/account-list-menu.js new file mode 100644 index 000000000000..c054b115d01d --- /dev/null +++ b/ui/components/multichain/account-list-menu/account-list-menu.js @@ -0,0 +1,196 @@ +import React, { useState, useContext } from 'react'; +import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; +import Fuse from 'fuse.js'; +import { useDispatch, useSelector } from 'react-redux'; +import Box from '../../ui/box/box'; +import { + ButtonLink, + ICON_NAMES, + TextFieldSearch, + Text, +} from '../../component-library'; +import { AccountListItem } from '..'; +import { + BLOCK_SIZES, + Size, + TextColor, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import Popover from '../../ui/popover'; +import { + getSelectedAccount, + getMetaMaskAccountsOrdered, + getConnectedSubjectsForAllAddresses, + getOriginOfCurrentTab, +} from '../../../selectors'; +import { toggleAccountMenu, setSelectedAccount } from '../../../store/actions'; +import { EVENT_NAMES, EVENT } from '../../../../shared/constants/metametrics'; +import { + IMPORT_ACCOUNT_ROUTE, + NEW_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, +} from '../../../helpers/constants/routes'; +import { getEnvironmentType } from '../../../../app/scripts/lib/util'; +import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; + +export const AccountListMenu = ({ onClose }) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const accounts = useSelector(getMetaMaskAccountsOrdered); + const selectedAccount = useSelector(getSelectedAccount); + const connectedSites = useSelector(getConnectedSubjectsForAllAddresses); + const currentTabOrigin = useSelector(getOriginOfCurrentTab); + const history = useHistory(); + const dispatch = useDispatch(); + + const [searchQuery, setSearchQuery] = useState(''); + + let searchResults = accounts; + if (searchQuery) { + const fuse = new Fuse(accounts, { + threshold: 0.2, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: ['name', 'address'], + }); + fuse.setCollection(accounts); + searchResults = fuse.search(searchQuery); + } + + return ( + + + {/* Search box */} + + setSearchQuery(e.target.value)} + /> + + {/* Account list block */} + + {searchResults.length === 0 && searchQuery !== '' ? ( + + {t('noAccountsFound')} + + ) : null} + {searchResults.map((account) => { + const connectedSite = connectedSites[account.address]?.find( + ({ origin }) => origin === currentTabOrigin, + ); + + return ( + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.NAV_ACCOUNT_SWITCHED, + properties: { + location: 'Main Menu', + }, + }); + dispatch(setSelectedAccount(account.address)); + }} + identity={account} + key={account.address} + selected={selectedAccount.address === account.address} + closeMenu={onClose} + connectedAvatar={connectedSite?.iconUrl} + connectedAvatarName={connectedSite?.name} + /> + ); + })} + + {/* Add / Import / Hardware */} + + + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.ACCOUNT_ADD_SELECTED, + properties: { + account_type: EVENT.ACCOUNT_TYPES.DEFAULT, + location: 'Main Menu', + }, + }); + history.push(NEW_ACCOUNT_ROUTE); + }} + > + {t('addAccount')} + + + + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.ACCOUNT_ADD_SELECTED, + properties: { + account_type: EVENT.ACCOUNT_TYPES.IMPORTED, + location: 'Main Menu', + }, + }); + history.push(IMPORT_ACCOUNT_ROUTE); + }} + > + {t('importAccount')} + + + + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.ACCOUNT_ADD_SELECTED, + properties: { + account_type: EVENT.ACCOUNT_TYPES.HARDWARE, + location: 'Main Menu', + }, + }); + if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser( + CONNECT_HARDWARE_ROUTE, + ); + } else { + history.push(CONNECT_HARDWARE_ROUTE); + } + }} + > + {t('hardwareWallet')} + + + + + + ); +}; + +AccountListMenu.propTypes = { + /** + * Function that executes when the menu closes + */ + onClose: PropTypes.func.isRequired, +}; diff --git a/ui/components/multichain/account-list-menu/account-list-menu.stories.js b/ui/components/multichain/account-list-menu/account-list-menu.stories.js new file mode 100644 index 000000000000..30abe820e7d6 --- /dev/null +++ b/ui/components/multichain/account-list-menu/account-list-menu.stories.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { AccountListMenu } from '.'; + +export default { + title: 'Components/Multichain/AccountListMenu', + component: AccountListMenu, + argTypes: { + onClose: { + action: 'onClose', + }, + }, +}; + +export const DefaultStory = (args) => ; 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 new file mode 100644 index 000000000000..2d34652b79a2 --- /dev/null +++ b/ui/components/multichain/account-list-menu/account-list-menu.test.js @@ -0,0 +1,103 @@ +/* eslint-disable jest/require-top-level-describe */ +import React from 'react'; +import reactRouterDom from 'react-router-dom'; +import { fireEvent, renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import { + NEW_ACCOUNT_ROUTE, + IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, +} from '../../../helpers/constants/routes'; +import { AccountListMenu } from '.'; + +const render = (props = { onClose: () => jest.fn() }) => { + const store = configureStore({ + activeTab: { + id: 113, + title: 'E2E Test Dapp', + origin: 'https://metamask.github.io', + protocol: 'https:', + url: 'https://metamask.github.io/test-dapp/', + }, + metamask: { + ...mockState.metamask, + }, + }); + return renderWithProvider(, store); +}; + +describe('AccountListMenu', () => { + const historyPushMock = jest.fn(); + + jest + .spyOn(reactRouterDom, 'useHistory') + .mockImplementation() + .mockReturnValue({ push: historyPushMock }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('displays important controls', () => { + const { getByPlaceholderText, getByText } = render(); + + expect(getByPlaceholderText('Search accounts')).toBeInTheDocument(); + expect(getByText('Add account')).toBeInTheDocument(); + expect(getByText('Import account')).toBeInTheDocument(); + expect(getByText('Hardware wallet')).toBeInTheDocument(); + }); + + it('navigates to new account screen when clicked', () => { + const { getByText } = render(); + fireEvent.click(getByText('Add account')); + expect(historyPushMock).toHaveBeenCalledWith(NEW_ACCOUNT_ROUTE); + }); + + it('navigates to import account screen when clicked', () => { + const { getByText } = render(); + fireEvent.click(getByText('Import account')); + expect(historyPushMock).toHaveBeenCalledWith(IMPORT_ACCOUNT_ROUTE); + }); + + it('navigates to hardware wallet connection screen when clicked', () => { + const { getByText } = render(); + fireEvent.click(getByText('Hardware wallet')); + expect(historyPushMock).toHaveBeenCalledWith(CONNECT_HARDWARE_ROUTE); + }); + + it('displays accounts for list and filters by search', () => { + render(); + const listItems = document.querySelectorAll( + '.multichain-account-list-item', + ); + expect(listItems).toHaveLength(4); + + const searchBox = document.querySelector('input[type=search]'); + fireEvent.change(searchBox, { + target: { value: 'Le' }, + }); + + const filteredListItems = document.querySelectorAll( + '.multichain-account-list-item', + ); + expect(filteredListItems).toHaveLength(1); + }); + + it('displays the "no accounts" message when search finds nothing', () => { + const { getByTestId } = render(); + + const searchBox = document.querySelector('input[type=search]'); + fireEvent.change(searchBox, { + target: { value: 'adslfkjlx' }, + }); + + const filteredListItems = document.querySelectorAll( + '.multichain-account-list-item', + ); + expect(filteredListItems).toHaveLength(0); + expect( + getByTestId('multichain-account-menu-no-results'), + ).toBeInTheDocument(); + }); +}); diff --git a/ui/components/multichain/account-list-menu/index.js b/ui/components/multichain/account-list-menu/index.js new file mode 100644 index 000000000000..c7c28c3b86c4 --- /dev/null +++ b/ui/components/multichain/account-list-menu/index.js @@ -0,0 +1 @@ +export { AccountListMenu } from './account-list-menu'; diff --git a/ui/components/multichain/account-list-menu/index.scss b/ui/components/multichain/account-list-menu/index.scss new file mode 100644 index 000000000000..5bbb6f0f112a --- /dev/null +++ b/ui/components/multichain/account-list-menu/index.scss @@ -0,0 +1,6 @@ +.multichain-account-menu { + &__list { + max-height: 200px; + overflow: auto; + } +} diff --git a/ui/components/multichain/detected-token-banner/__snapshots__/detected-token-banner.test.js.snap b/ui/components/multichain/detected-token-banner/__snapshots__/detected-token-banner.test.js.snap new file mode 100644 index 000000000000..cbf587cb2064 --- /dev/null +++ b/ui/components/multichain/detected-token-banner/__snapshots__/detected-token-banner.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DetectedTokensBanner should render correctly 1`] = ` +
+
+ +
+

+ 3 new tokens found in this account +

+ +
+
+
+`; diff --git a/ui/components/multichain/detected-token-banner/detected-token-banner.js b/ui/components/multichain/detected-token-banner/detected-token-banner.js new file mode 100644 index 000000000000..790630ef71d2 --- /dev/null +++ b/ui/components/multichain/detected-token-banner/detected-token-banner.js @@ -0,0 +1,60 @@ +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { getDetectedTokensInCurrentNetwork } from '../../../selectors'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics'; +import { BannerAlert } from '../../component-library'; + +export const DetectedTokensBanner = ({ + className, + actionButtonOnClick, + ...props +}) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + + const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork); + const detectedTokensDetails = detectedTokens.map( + ({ address, symbol }) => `${symbol} - ${address}`, + ); + + const handleOnClick = () => { + actionButtonOnClick(); + trackEvent({ + event: EVENT_NAMES.TOKEN_IMPORT_CLICKED, + category: EVENT.CATEGORIES.WALLET, + properties: { + source: EVENT.SOURCE.TOKEN.DETECTED, + tokens: detectedTokensDetails, + }, + }); + }; + return ( + + {detectedTokens.length === 1 + ? t('numberOfNewTokensDetectedSingular') + : t('numberOfNewTokensDetectedPlural', [detectedTokens.length])} + + ); +}; + +DetectedTokensBanner.propTypes = { + /** + * Handler to be passed to the DetectedTokenBanner component + */ + actionButtonOnClick: PropTypes.func.isRequired, + /** + * An additional className to the DetectedTokenBanner component + */ + className: PropTypes.string, +}; diff --git a/ui/components/multichain/detected-token-banner/detected-token-banner.stories.js b/ui/components/multichain/detected-token-banner/detected-token-banner.stories.js new file mode 100644 index 000000000000..080b476effd3 --- /dev/null +++ b/ui/components/multichain/detected-token-banner/detected-token-banner.stories.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { DetectedTokensBanner } from './detected-token-banner'; + +export default { + title: 'Components/Multichain/DetectedTokensBanner', + component: DetectedTokensBanner, + argTypes: { + actionButtonOnClick: { action: 'setShowDetectedTokens' }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/detected-token-banner/detected-token-banner.test.js b/ui/components/multichain/detected-token-banner/detected-token-banner.test.js new file mode 100644 index 000000000000..e7c78b47b3b6 --- /dev/null +++ b/ui/components/multichain/detected-token-banner/detected-token-banner.test.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { renderWithProvider, screen, fireEvent } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; + +import { DetectedTokensBanner } from './detected-token-banner'; + +describe('DetectedTokensBanner', () => { + let setShowDetectedTokensSpy; + + const args = {}; + + beforeEach(() => { + setShowDetectedTokensSpy = jest.fn(); + args.actionButtonOnClick = setShowDetectedTokensSpy; + }); + + it('should render correctly', () => { + const store = configureStore(testData); + const { getByTestId, container } = renderWithProvider( + , + store, + ); + + expect(getByTestId('detected-token-banner')).toBeDefined(); + expect(container).toMatchSnapshot(); + }); + it('should render number of tokens detected link', () => { + const store = configureStore(testData); + renderWithProvider(, store); + + expect( + screen.getByText('3 new tokens found in this account'), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Import tokens')); + expect(setShowDetectedTokensSpy).toHaveBeenCalled(); + }); +}); diff --git a/ui/components/multichain/detected-token-banner/index.js b/ui/components/multichain/detected-token-banner/index.js new file mode 100644 index 000000000000..0fe2b61deaa6 --- /dev/null +++ b/ui/components/multichain/detected-token-banner/index.js @@ -0,0 +1 @@ +export { DetectedTokensBanner } from './detected-token-banner'; diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js new file mode 100644 index 000000000000..d34e427510d9 --- /dev/null +++ b/ui/components/multichain/index.js @@ -0,0 +1,6 @@ +export { AccountListItem } from './account-list-item'; +export { AccountListItemMenu } from './account-list-item-menu'; +export { AccountListMenu } from './account-list-menu'; +export { DetectedTokensBanner } from './detected-token-banner'; +export { MultichainImportTokenLink } from './multichain-import-token-link'; +export { MultichainTokenListItem } from './multichain-token-list-item'; diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss new file mode 100644 index 000000000000..e8ee40ffa49e --- /dev/null +++ b/ui/components/multichain/multichain-components.scss @@ -0,0 +1,9 @@ +/** +* Please import your styles in order of atomicity. +* The most atomic styles should be imported first. +* This will help improve specificity and reduce the chance of +* unintended overrides. +**/ +@import 'account-list-item/index'; +@import 'account-list-menu/index'; +@import 'multichain-token-list-item/multichain-token-list-item'; diff --git a/ui/components/multichain/multichain-import-token-link/__snapshots__/multichain-import-token-link.test.js.snap b/ui/components/multichain/multichain-import-token-link/__snapshots__/multichain-import-token-link.test.js.snap new file mode 100644 index 000000000000..925788efa313 --- /dev/null +++ b/ui/components/multichain/multichain-import-token-link/__snapshots__/multichain-import-token-link.test.js.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Import Token Link should match snapshot for goerli chainId 1`] = ` +
+ +
+`; + +exports[`Import Token Link should match snapshot for mainnet chainId 1`] = ` +
+ +
+`; diff --git a/ui/components/multichain/multichain-import-token-link/index.js b/ui/components/multichain/multichain-import-token-link/index.js new file mode 100644 index 000000000000..c9899d1191b9 --- /dev/null +++ b/ui/components/multichain/multichain-import-token-link/index.js @@ -0,0 +1 @@ +export { MultichainImportTokenLink } from './multichain-import-token-link'; diff --git a/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js new file mode 100644 index 000000000000..fc6dae84a9c6 --- /dev/null +++ b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js @@ -0,0 +1,87 @@ +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import Box from '../../ui/box/box'; +import { ButtonLink, ICON_NAMES } from '../../component-library'; +import { + AlignItems, + DISPLAY, + Size, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { IMPORT_TOKEN_ROUTE } from '../../../helpers/constants/routes'; +import { detectNewTokens } from '../../../store/actions'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics'; +import { + getIsTokenDetectionSupported, + getIsTokenDetectionInactiveOnMainnet, +} from '../../../selectors'; + +export const MultichainImportTokenLink = ({ className, ...props }) => { + const trackEvent = useContext(MetaMetricsContext); + const t = useI18nContext(); + const history = useHistory(); + + const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported); + const isTokenDetectionInactiveOnMainnet = useSelector( + getIsTokenDetectionInactiveOnMainnet, + ); + + const isTokenDetectionAvailable = + isTokenDetectionSupported || + isTokenDetectionInactiveOnMainnet || + Boolean(process.env.IN_TEST); + return ( + + + { + history.push(IMPORT_TOKEN_ROUTE); + trackEvent({ + event: EVENT_NAMES.TOKEN_IMPORT_BUTTON_CLICKED, + category: EVENT.CATEGORIES.NAVIGATION, + properties: { + location: 'Home', + }, + }); + }} + > + {isTokenDetectionAvailable + ? t('importTokensCamelCase') + : t('importTokensCamelCase').charAt(0).toUpperCase() + + t('importTokensCamelCase').slice(1)} + + + + detectNewTokens()} + > + {t('refreshList')} + + + + ); +}; + +MultichainImportTokenLink.propTypes = { + /** + * An additional className to apply to the TokenList. + */ + className: PropTypes.string, +}; diff --git a/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.stories.js b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.stories.js new file mode 100644 index 000000000000..1cb7b4395148 --- /dev/null +++ b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { MultichainImportTokenLink } from './multichain-import-token-link'; + +export default { + title: 'Components/Multichain/MultichainImportTokenLink', + component: MultichainImportTokenLink, +}; + +export const DefaultStory = () => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.test.js b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.test.js new file mode 100644 index 000000000000..dd376c52ba0c --- /dev/null +++ b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.test.js @@ -0,0 +1,101 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { fireEvent, screen } from '@testing-library/react'; +import { detectNewTokens } from '../../../store/actions'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { MultichainImportTokenLink } from './multichain-import-token-link'; + +const mockPushHistory = jest.fn(); + +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + return { + ...original, + useLocation: jest.fn(() => ({ search: '' })), + useHistory: () => ({ + push: mockPushHistory, + }), + }; +}); + +jest.mock('../../../store/actions.ts', () => ({ + detectNewTokens: jest.fn(), +})); + +describe('Import Token Link', () => { + it('should match snapshot for goerli chainId', () => { + const mockState = { + metamask: { + provider: { + chainId: '0x5', + }, + }, + }; + + const store = configureMockStore()(mockState); + + const { container } = renderWithProvider( + , + store, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot for mainnet chainId', () => { + const mockState = { + metamask: { + provider: { + chainId: '0x1', + }, + }, + }; + + const store = configureMockStore()(mockState); + + const { container } = renderWithProvider( + , + store, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should detectNewTokens when clicking refresh', () => { + const mockState = { + metamask: { + provider: { + chainId: '0x5', + }, + }, + }; + + const store = configureMockStore()(mockState); + + renderWithProvider(, store); + + const refreshList = screen.getByTestId('refresh-list-button'); + fireEvent.click(refreshList); + + expect(detectNewTokens).toHaveBeenCalled(); + }); + + it('should push import token route', () => { + const mockState = { + metamask: { + provider: { + chainId: '0x5', + }, + }, + }; + + const store = configureMockStore()(mockState); + + renderWithProvider(, store); + + const importToken = screen.getByTestId('import-token-button'); + fireEvent.click(importToken); + + expect(mockPushHistory).toHaveBeenCalledWith('/import-token'); + }); +}); diff --git a/ui/components/multichain/multichain-token-list-item/__snapshots__/multichain-token-list-item.test.js.snap b/ui/components/multichain/multichain-token-list-item/__snapshots__/multichain-token-list-item.test.js.snap new file mode 100644 index 000000000000..b4af82ccb0a6 --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/__snapshots__/multichain-token-list-item.test.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MultichainTokenListItem should render correctly 1`] = ` + +`; diff --git a/ui/components/multichain/multichain-token-list-item/index.js b/ui/components/multichain/multichain-token-list-item/index.js new file mode 100644 index 000000000000..bccfac8ab4e9 --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/index.js @@ -0,0 +1 @@ +export { MultichainTokenListItem } from './multichain-token-list-item'; diff --git a/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js new file mode 100644 index 000000000000..82ed7752403a --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js @@ -0,0 +1,162 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import classnames from 'classnames'; +import { + BLOCK_SIZES, + BorderColor, + DISPLAY, + FLEX_DIRECTION, + FONT_WEIGHT, + JustifyContent, + Size, + TextColor, + TextVariant, + TEXT_ALIGN, +} from '../../../helpers/constants/design-system'; +import { + AvatarNetwork, + AvatarToken, + BadgeWrapper, + Text, +} from '../../component-library'; +import Box from '../../ui/box/box'; +import { getNativeCurrencyImage } from '../../../selectors'; +import Tooltip from '../../ui/tooltip'; +import { useI18nContext } from '../../../hooks/useI18nContext'; + +export const MultichainTokenListItem = ({ + className, + onClick, + tokenSymbol, + tokenImage, + primary, + secondary, + title, +}) => { + const t = useI18nContext(); + const primaryTokenImage = useSelector(getNativeCurrencyImage); + const dataTheme = document.documentElement.getAttribute('data-theme'); + return ( + + { + e.preventDefault(); + onClick(); + }} + > + + } + marginRight={3} + > + + + + + + + + {title === 'ETH' ? t('networkNameEthereum') : title} + + + + + {secondary} + + + + {Number(primary).toFixed(3)} {tokenSymbol}{' '} + + + + + ); +}; + +MultichainTokenListItem.propTypes = { + /** + * An additional className to apply to the TokenList. + */ + className: PropTypes.string, + /** + * The onClick handler to be passed to the MultichainTokenListItem component + */ + onClick: PropTypes.func, + /** + * tokenSymbol represents the symbol of the Token + */ + tokenSymbol: PropTypes.string, + /** + * title represents the name of the token and if name is not available then Symbol + */ + title: PropTypes.string, + /** + * tokenImage represnts the image of the token icon + */ + tokenImage: PropTypes.string, + /** + * primary represents the balance + */ + primary: PropTypes.string, + /** + * secondary represents the balance in dollars + */ + secondary: PropTypes.string, +}; diff --git a/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.scss b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.scss new file mode 100644 index 000000000000..f6863f8b32b2 --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.scss @@ -0,0 +1,8 @@ +.multichain-token-list-item { + &__container-cell { + &:hover, + &:focus-within { + background-color: var(--color-background-default-hover); + } + } +} diff --git a/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.stories.js b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.stories.js new file mode 100644 index 000000000000..168e6ed1aa0f --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.stories.js @@ -0,0 +1,78 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import testData from '../../../../.storybook/test-data'; +import configureStore from '../../../store/store'; +import { MultichainTokenListItem } from './multichain-token-list-item'; + +export default { + title: 'Components/Multichain/MultichainTokenListItem', + component: MultichainTokenListItem, + argTypes: { + tokenSymbol: { + control: 'text', + }, + tokenImage: { + control: 'text', + }, + primary: { + control: 'text', + }, + secondary: { + control: 'text', + }, + title: { + control: 'text', + }, + onClick: { + action: 'onClick', + }, + }, + args: { + secondary: '$9.80 USD', + primary: '88.00687889', + tokenImage: './images/eth_logo.svg', + tokenSymbol: 'ETH', + title: 'Ethereum', + }, +}; + +const customNetworkData = { + ...testData, + metamask: { ...testData.metamask, nativeCurrency: '' }, +}; +const customNetworkStore = configureStore(customNetworkData); + +const Template = (args) => { + return ; +}; + +export const DefaultStory = Template.bind({}); + +export const ChaosStory = (args) => ( +
+ +
+); +ChaosStory.storyName = 'ChaosStory'; + +ChaosStory.args = { + title: 'Really long, long name', + secondary: '$94556756776.80 USD', + primary: '34449765768526.00', +}; + +export const NoImagesStory = Template.bind({}); + +NoImagesStory.decorators = [ + (Story) => ( + + + + ), +]; + +NoImagesStory.args = { + tokenImage: '', +}; diff --git a/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.test.js b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.test.js new file mode 100644 index 000000000000..6f04982984ae --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.test.js @@ -0,0 +1,57 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; + +import { fireEvent } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { MultichainTokenListItem } from './multichain-token-list-item'; + +const state = { + metamask: { + provider: { + ticker: 'ETH', + nickname: '', + chainId: '0x1', + type: 'mainnet', + }, + useTokenDetection: false, + nativeCurrency: 'ETH', + }, +}; + +describe('MultichainTokenListItem', () => { + const props = { + onClick: jest.fn(), + }; + it('should render correctly', () => { + const store = configureMockStore()(state); + const { getByTestId, container } = renderWithProvider( + , + store, + ); + expect(getByTestId('multichain-token-list-item')).toBeDefined(); + expect(container).toMatchSnapshot(); + }); + + it('should render with custom className', () => { + const store = configureMockStore()(state); + const { getByTestId } = renderWithProvider( + , + store, + ); + expect(getByTestId('multichain-token-list-item')).toHaveClass( + 'multichain-token-list-item-test', + ); + }); + + it('handles click action and fires onClick', () => { + const store = configureMockStore()(state); + const { queryByTestId } = renderWithProvider( + , + store, + ); + + fireEvent.click(queryByTestId('multichain-token-list-button')); + + expect(props.onClick).toHaveBeenCalled(); + }); +}); diff --git a/ui/components/ui/menu/menu-item.js b/ui/components/ui/menu/menu-item.js index c3c409cc43ec..abdebf108bf4 100644 --- a/ui/components/ui/menu/menu-item.js +++ b/ui/components/ui/menu/menu-item.js @@ -2,7 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Icon, ICON_SIZES } from '../../component-library'; +import { Text, Icon, ICON_SIZES } from '../../component-library'; +import { TextVariant } from '../../../helpers/constants/design-system'; const MenuItem = ({ children, @@ -22,7 +23,7 @@ const MenuItem = ({ ) : null}
{children}
- {subtitle ?
{subtitle}
: null} + {subtitle ? {subtitle} : null}
); diff --git a/ui/components/ui/page-container/index.scss b/ui/components/ui/page-container/index.scss index cec98678aa43..1e37799af474 100644 --- a/ui/components/ui/page-container/index.scss +++ b/ui/components/ui/page-container/index.scss @@ -190,7 +190,7 @@ margin-right: 0; &:first-of-type { - margin-bottom: 0; + margin-right: 4px; } } } diff --git a/ui/components/ui/popover/index.scss b/ui/components/ui/popover/index.scss index acb22a510096..0a22355eb844 100644 --- a/ui/components/ui/popover/index.scss +++ b/ui/components/ui/popover/index.scss @@ -22,6 +22,10 @@ &-header { position: relative; + + &__title--center { + flex: 1; + } } &-bg { diff --git a/ui/components/ui/popover/popover.component.js b/ui/components/ui/popover/popover.component.js index 1defc3741e73..23a06f759472 100644 --- a/ui/components/ui/popover/popover.component.js +++ b/ui/components/ui/popover/popover.component.js @@ -15,6 +15,8 @@ import { Size, BorderColor, IconColor, + TEXT_ALIGN, + BLOCK_SIZES, } from '../../../helpers/constants/design-system'; import { ButtonIcon, @@ -76,9 +78,10 @@ const Popover = ({ {onBack ? ( @@ -90,7 +93,13 @@ const Popover = ({ size={Size.SM} /> ) : null} - + {title} {onClose ? ( diff --git a/ui/components/ui/tooltip/index.scss b/ui/components/ui/tooltip/index.scss index 8ae602e33f7b..3bc86f9102b2 100644 --- a/ui/components/ui/tooltip/index.scss +++ b/ui/components/ui/tooltip/index.scss @@ -26,19 +26,19 @@ } } - &[x-placement^=top] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { + &[x-placement^='top'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { border-top-color: var(--color-background-default); } - &[x-placement^=right] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { + &[x-placement^='right'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { border-right-color: var(--color-background-default); } - &[x-placement^=left] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { + &[x-placement^='left'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { border-left-color: var(--color-background-default); } - &[x-placement^=bottom] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { + &[x-placement^='bottom'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { border-bottom-color: var(--color-background-default); } } diff --git a/ui/css/index.scss b/ui/css/index.scss index d861b721019e..a26c7ae09cf6 100644 --- a/ui/css/index.scss +++ b/ui/css/index.scss @@ -11,6 +11,7 @@ @import '../components/component-library/component-library-components.scss'; @import '../components/app/app-components'; @import '../components/ui/ui-components'; +@import '../components/multichain/multichain-components.scss'; @import '../pages/pages'; @import './errors.scss'; @import './loading.scss'; diff --git a/ui/helpers/constants/design-system.ts b/ui/helpers/constants/design-system.ts index c39e95f51378..bc17859c0975 100644 --- a/ui/helpers/constants/design-system.ts +++ b/ui/helpers/constants/design-system.ts @@ -130,6 +130,7 @@ export enum TextColor { lineaTestnetInverse = 'lineatestnet-inverse', goerliInverse = 'goerli-inverse', sepoliaInverse = 'sepolia-inverse', + transparent = 'transparent', } export enum IconColor { @@ -302,6 +303,7 @@ export const TEXT_ALIGN = { RIGHT: 'right', JUSTIFY: 'justify', END: 'end', + START: 'start', }; export const TEXT_TRANSFORM = { diff --git a/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap b/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap index e795ab35d5f7..fbf3c10c168d 100644 --- a/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap +++ b/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap @@ -368,7 +368,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
0xCD2...D826
@@ -444,7 +444,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
0xDea...beeF
@@ -579,7 +579,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
0xbBb...BBbB
@@ -655,7 +655,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
0xB0B...Ea57
@@ -731,7 +731,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
0xB0B...0000
diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 1d8c8330d7e6..cdc6663178cb 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -54,7 +54,6 @@ import { ONBOARDING_SECURE_YOUR_WALLET_ROUTE, } from '../../helpers/constants/routes'; import ZENDESK_URLS from '../../helpers/constants/zendesk-url'; -import OpenSeaWhatsNewPopover from '../../components/app/open-sea-whats-new-popover/open-sea-whats-new-popover'; ///: BEGIN:ONLY_INCLUDE_IN(main) import { SUPPORT_LINK } from '../../../shared/lib/ui-utils'; ///: END:ONLY_INCLUDE_IN @@ -632,7 +631,6 @@ export default class Home extends PureComponent { />
{showWhatsNew ? : null} - {showWhatsNew ? : null} {!showWhatsNew && showRecoveryPhraseReminder ? ( - - Secret Recovery Phrase (SRP) - + Secret Recovery Phrase (SRP) provides - - non-custodial wallet - + non-custodial wallet . That means you're the owner of your SRP. @@ -112,23 +104,15 @@ exports[`Reveal Seed Page should match snapshot 1`] = ` class="box box--margin-top-auto box--display-flex box--gap-4 box--flex-direction-row" >
diff --git a/ui/pages/onboarding-flow/secure-your-wallet/skip-srp-backup-popover.js b/ui/pages/onboarding-flow/secure-your-wallet/skip-srp-backup-popover.js index 4e9c69e895ab..bd64f93474f7 100644 --- a/ui/pages/onboarding-flow/secure-your-wallet/skip-srp-backup-popover.js +++ b/ui/pages/onboarding-flow/secure-your-wallet/skip-srp-backup-popover.js @@ -87,7 +87,7 @@ export default function SkipSRPBackup({ handleClose }) { color={IconColor.errorDefault} /> {t('skipAccountSecurity')} @@ -102,7 +102,7 @@ export default function SkipSRPBackup({ handleClose }) { /> {t('skipAccountSecurityDetails')} diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index c6cb8d89bf08..8cb160764239 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -90,6 +90,7 @@ import { SEND_STAGES } from '../../ducks/send'; import DeprecatedTestNetworks from '../../components/ui/deprecated-test-networks/deprecated-test-networks'; import NewNetworkInfo from '../../components/ui/new-network-info/new-network-info'; import { ThemeType } from '../../../shared/constants/preferences'; +import { AccountListMenu } from '../../components/multichain'; export default class Routes extends Component { static propTypes = { @@ -125,6 +126,8 @@ export default class Routes extends Component { forgottenPassword: PropTypes.bool, isCurrentProviderCustom: PropTypes.bool, completedOnboarding: PropTypes.bool, + isAccountMenuOpen: PropTypes.bool, + toggleAccountMenu: PropTypes.func, }; static contextTypes = { @@ -427,6 +430,8 @@ export default class Routes extends Component { shouldShowSeedPhraseReminder, isCurrentProviderCustom, completedOnboarding, + isAccountMenuOpen, + toggleAccountMenu, } = this.props; const loadMessage = loadingMessage || isNetworkLoading @@ -483,7 +488,10 @@ export default class Routes extends Component { )} {this.showOnboardingHeader() && } {completedOnboarding ? : null} - + {process.env.MULTICHAIN ? null : } + {process.env.MULTICHAIN && isAccountMenuOpen ? ( + toggleAccountMenu()} /> + ) : null}
{isLoading ? : null} {!isLoading && isNetworkLoading ? : null} diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index baa22c162504..c132bcfa87c7 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -18,6 +18,7 @@ import { setCurrentCurrency, setLastActiveTime, setMouseUserState, + toggleAccountMenu, } from '../../store/actions'; import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; @@ -55,6 +56,7 @@ function mapStateToProps(state) { forgottenPassword: state.metamask.forgottenPassword, isCurrentProviderCustom: isCurrentProviderCustom(state), completedOnboarding, + isAccountMenuOpen: state.metamask.isAccountMenuOpen, }; } @@ -67,6 +69,7 @@ function mapDispatchToProps(dispatch) { setLastActiveTime: () => dispatch(setLastActiveTime()), pageChanged: (path) => dispatch(pageChanged(path)), prepareToLeaveSwaps: () => dispatch(prepareToLeaveSwaps()), + toggleAccountMenu: () => dispatch(toggleAccountMenu()), }; } diff --git a/ui/selectors/permissions.js b/ui/selectors/permissions.js index 27f5c4c88f6a..d4b3d90de780 100644 --- a/ui/selectors/permissions.js +++ b/ui/selectors/permissions.js @@ -100,6 +100,24 @@ export function getConnectedSubjectsForSelectedAddress(state) { return connectedSubjects; } +export function getConnectedSubjectsForAllAddresses(state) { + const subjects = getPermissionSubjects(state); + const subjectMetadata = getSubjectMetadata(state); + + const accountsToConnections = {}; + Object.entries(subjects).forEach(([subjectKey, subjectValue]) => { + const exposedAccounts = getAccountsFromSubject(subjectValue); + exposedAccounts.forEach((address) => { + if (!accountsToConnections[address]) { + accountsToConnections[address] = []; + } + accountsToConnections[address].push(subjectMetadata[subjectKey] || {}); + }); + }); + + return accountsToConnections; +} + export function getSubjectsWithPermission(state, permissionName) { const subjects = getPermissionSubjects(state); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 4fc7ba94870d..dea043e15076 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1363,9 +1363,3 @@ export function getIsDesktopEnabled(state) { return state.metamask.desktopEnabled; } ///: END:ONLY_INCLUDE_IN - -export function getHasTheOpenSeaTransactionSecurityProviderPopoverBeenShown( - state, -) { - return state.metamask.openSeaTransactionSecurityProviderPopoverHasBeenShown; -} diff --git a/ui/store/actions.ts b/ui/store/actions.ts index a71cdb62e63c..913e7de0dd70 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4575,12 +4575,11 @@ export function hideBetaHeader() { export function setTransactionSecurityCheckEnabled( transactionSecurityCheckEnabled: boolean, ): ThunkAction { - return async (dispatch) => { + return async () => { try { await submitRequestToBackground('setTransactionSecurityCheckEnabled', [ transactionSecurityCheckEnabled, ]); - await forceUpdateMetamaskState(dispatch); } catch (error) { logErrorWithMessage(error); } @@ -4591,19 +4590,6 @@ export function setFirstTimeUsedNetwork(chainId: string) { return submitRequestToBackground('setFirstTimeUsedNetwork', [chainId]); } -export function setOpenSeaTransactionSecurityProviderPopoverHasBeenShown(): ThunkAction< - void, - MetaMaskReduxState, - unknown, - AnyAction -> { - return async () => { - await submitRequestToBackground( - 'setOpenSeaTransactionSecurityProviderPopoverHasBeenShown', - ); - }; -} - // QR Hardware Wallets export async function submitQRHardwareCryptoHDKey(cbor: Hex) { await submitRequestToBackground('submitQRHardwareCryptoHDKey', [cbor]); diff --git a/yarn.lock b/yarn.lock index 3f1224f76bcc..6ea77ae8ff53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3704,10 +3704,10 @@ __metadata: languageName: node linkType: hard -"@metamask/contract-metadata@npm:^2.1.0, @metamask/contract-metadata@npm:^2.2.0": - version: 2.2.0 - resolution: "@metamask/contract-metadata@npm:2.2.0" - checksum: 31b94274c7c0a2d03377f3fa5e46574cde6607fd3a3b571cb7ffa4f7ce6dc90e2bfe2a4b2eb9eb4ebcd07cb7d6918da893356b699e91bc6e005b472ca7279370 +"@metamask/contract-metadata@npm:^2.1.0, @metamask/contract-metadata@npm:^2.3.1": + version: 2.3.1 + resolution: "@metamask/contract-metadata@npm:2.3.1" + checksum: 95dcc27f661a3e380c0cca8c6d90fb1777e31ab3fe16909fd5c67844125510e3f8e9eca05c9069fde34c77df3b66e56111c7a908a07623e88052ef147eff4315 languageName: node linkType: hard @@ -3849,35 +3849,43 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-json-rpc-infura@npm:^7.0.0": - version: 7.0.0 - resolution: "@metamask/eth-json-rpc-infura@npm:7.0.0" +"@metamask/eth-json-rpc-infura@npm:^8.0.0": + version: 8.0.0 + resolution: "@metamask/eth-json-rpc-infura@npm:8.0.0" dependencies: "@metamask/utils": ^3.0.1 - eth-json-rpc-middleware: ^8.1.0 + eth-json-rpc-middleware: ^9.0.0 eth-rpc-errors: ^4.0.3 json-rpc-engine: ^6.1.0 node-fetch: ^2.6.7 - checksum: 6230cb289b66db39d27f08ffc72cfb79274e632e4e14eb52ca72d19167d17bbf22c58718d80f801818058631dec0638bcc21ef4b229fa8d53e7f9328be870fc6 + checksum: e8c3a4b75d4f2bb09f68d7d2ac6b992f264df893921b50a05c35968ee684b7bba90180870eebecfc89b7fbf40d11de2a545ab68f1d511f569ce0a6519c64b0aa languageName: node linkType: hard -"@metamask/eth-json-rpc-middleware@npm:^10.0.0": - version: 10.0.0 - resolution: "@metamask/eth-json-rpc-middleware@npm:10.0.0" +"@metamask/eth-json-rpc-middleware@npm:^11.0.0": + version: 11.0.0 + resolution: "@metamask/eth-json-rpc-middleware@npm:11.0.0" dependencies: + "@metamask/eth-json-rpc-provider": ^1.0.0 "@metamask/eth-sig-util": ^5.0.0 - "@metamask/safe-event-emitter": ^2.0.0 "@metamask/utils": ^3.0.3 - btoa: ^1.2.1 clone: ^2.1.1 - eth-block-tracker: ^5.0.1 + eth-block-tracker: ^7.0.0 eth-rpc-errors: ^4.0.3 json-rpc-engine: ^6.1.0 - json-stable-stringify: ^1.0.1 - node-fetch: ^2.6.7 pify: ^3.0.0 - checksum: c754b3a39f175698070b4d07076e692d3080738bd25157c3b93114d286c975ee6895d5793b4188ca3d0abbcdef04bfde9e2d4835251a6b725b002d3750bf98de + safe-stable-stringify: ^2.3.2 + checksum: c866d07a199ab480ceeb7ab8df61c08284640b5ac13aee3dd81dae9e0e5575f4a425d95728070ab5402c0c6cd5f9237fb5f4f22dbcdc99fe0b50bb47df561830 + languageName: node + linkType: hard + +"@metamask/eth-json-rpc-provider@npm:^1.0.0": + version: 1.0.0 + resolution: "@metamask/eth-json-rpc-provider@npm:1.0.0" + dependencies: + "@metamask/safe-event-emitter": ^2.0.0 + json-rpc-engine: ^6.1.0 + checksum: 27865d84d90030db1a9e5a66bc0b0ae079706fb7be635ec1e9bd4f64771e819aae78f0a026c6629d3a1a2eb277fcd51977315c049c47a70df1dd95d1d4106982 languageName: node linkType: hard @@ -14648,13 +14656,6 @@ __metadata: languageName: node linkType: hard -"emojis-list@npm:^2.0.0": - version: 2.1.0 - resolution: "emojis-list@npm:2.1.0" - checksum: fb61fa6356dfcc9fbe6db8e334c29da365a34d3d82a915cb59621883d3023d804fd5edad5acd42b8eec016936e81d3b38e2faf921b32e073758374253afe1272 - languageName: node - linkType: hard - "emojis-list@npm:^3.0.0": version: 3.0.0 resolution: "emojis-list@npm:3.0.0" @@ -15570,15 +15571,16 @@ __metadata: languageName: node linkType: hard -"eth-block-tracker@npm:^6.0.0": - version: 6.0.0 - resolution: "eth-block-tracker@npm:6.0.0" +"eth-block-tracker@npm:^7.0.0": + version: 7.0.0 + resolution: "eth-block-tracker@npm:7.0.0" dependencies: + "@metamask/eth-json-rpc-provider": ^1.0.0 "@metamask/safe-event-emitter": ^2.0.0 "@metamask/utils": ^3.0.1 json-rpc-random-id: ^1.0.1 pify: ^3.0.0 - checksum: ad1199b822a9a3ff2673ecc92ca7cda0a37828e5bfd1927fd917a8085a99904fc29d3ef2392068bcfb14e47589df097940ef28f3e9025d1681e56a89b07e284e + checksum: b76f6ba022947eec0161e5592bc5386e8f05bff8a2c3e0e10c76bce21bc51900ef1cb153eb8bf31858fb0027e929c6a85a159bdf84aa1e3ef77b24e53e82ba84 languageName: node linkType: hard @@ -15647,21 +15649,22 @@ __metadata: languageName: node linkType: hard -"eth-json-rpc-middleware@npm:^8.1.0": - version: 8.1.0 - resolution: "eth-json-rpc-middleware@npm:8.1.0" +"eth-json-rpc-middleware@npm:^9.0.0": + version: 9.0.1 + resolution: "eth-json-rpc-middleware@npm:9.0.1" dependencies: + "@metamask/eth-sig-util": ^5.0.0 "@metamask/safe-event-emitter": ^2.0.0 + "@metamask/utils": ^3.0.3 btoa: ^1.2.1 clone: ^2.1.1 eth-block-tracker: ^5.0.1 eth-rpc-errors: ^4.0.3 - eth-sig-util: ^1.4.2 json-rpc-engine: ^6.1.0 json-stable-stringify: ^1.0.1 node-fetch: ^2.6.7 pify: ^3.0.0 - checksum: ec10bbc04e3b7696f82db2db528b052c8f6de811c90a12d4eb32f23cbe6ea198d86656afa5b53c52de06b631fef633cf29409bb56c04a16f173da94ee1d89ab6 + checksum: 9512829a6958df6ef739b891a0c0804b51a140407fd2e3ddaaa6b18d975796646cfcf7f7305a18beb7903db09e0c7a91b06dc5434b6bd2d6cdb85d992d9fd3ab languageName: node linkType: hard @@ -23288,17 +23291,6 @@ __metadata: languageName: node linkType: hard -"loader-utils@npm:1.2.3": - version: 1.2.3 - resolution: "loader-utils@npm:1.2.3" - dependencies: - big.js: ^5.2.2 - emojis-list: ^2.0.0 - json5: ^1.0.1 - checksum: 385407fc2683b6d664276fd41df962296de4a15030bb24389de77b175570c3b56bd896869376ba14cf8b33a9e257e17a91d395739ba7e23b5b68a8749a41df7e - languageName: node - linkType: hard - "loader-utils@npm:^1.1.0, loader-utils@npm:^1.2.3": version: 1.4.0 resolution: "loader-utils@npm:1.4.0" @@ -23311,13 +23303,13 @@ __metadata: linkType: hard "loader-utils@npm:^2.0.0": - version: 2.0.0 - resolution: "loader-utils@npm:2.0.0" + version: 2.0.4 + resolution: "loader-utils@npm:2.0.4" dependencies: big.js: ^5.2.2 emojis-list: ^3.0.0 json5: ^2.1.2 - checksum: 6856423131b50b6f5f259da36f498cfd7fc3c3f8bb17777cf87fdd9159e797d4ba4288d9a96415fd8da62c2906960e88f74711dee72d03a9003bddcd0d364a51 + checksum: a5281f5fff1eaa310ad5e1164095689443630f3411e927f95031ab4fb83b4a98f388185bb1fe949e8ab8d4247004336a625e9255c22122b815bb9a4c5d8fc3b7 languageName: node linkType: hard @@ -24277,7 +24269,7 @@ __metadata: "@metamask/assets-controllers": ^4.0.1 "@metamask/auto-changelog": ^2.1.0 "@metamask/base-controller": ^1.0.0 - "@metamask/contract-metadata": ^2.2.0 + "@metamask/contract-metadata": ^2.3.1 "@metamask/controller-utils": ^1.0.0 "@metamask/design-tokens": ^1.9.0 "@metamask/desktop": ^0.3.0 @@ -24286,8 +24278,9 @@ __metadata: "@metamask/eslint-config-mocha": ^9.0.0 "@metamask/eslint-config-nodejs": ^9.0.0 "@metamask/eslint-config-typescript": ^9.0.1 - "@metamask/eth-json-rpc-infura": ^7.0.0 - "@metamask/eth-json-rpc-middleware": ^10.0.0 + "@metamask/eth-json-rpc-infura": ^8.0.0 + "@metamask/eth-json-rpc-middleware": ^11.0.0 + "@metamask/eth-json-rpc-provider": ^1.0.0 "@metamask/eth-keyring-controller": ^10.0.1 "@metamask/eth-ledger-bridge-keyring": ^0.13.0 "@metamask/eth-token-tracker": ^4.0.0 @@ -24420,7 +24413,7 @@ __metadata: eslint-plugin-react: ^7.23.1 eslint-plugin-react-hooks: ^4.2.0 eslint-plugin-storybook: ^0.6.4 - eth-block-tracker: ^6.0.0 + eth-block-tracker: ^7.0.0 eth-ens-namehash: ^2.0.8 eth-json-rpc-filters: ^6.0.0 eth-lattice-keyring: ^0.12.3 @@ -24536,7 +24529,7 @@ __metadata: remove-trailing-slash: ^0.1.1 require-from-string: ^2.0.2 reselect: ^3.0.1 - resolve-url-loader: ^3.1.2 + resolve-url-loader: ^3.1.5 safe-event-emitter: ^1.0.1 sass: ^1.32.4 sass-loader: ^10.1.1 @@ -27730,14 +27723,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:7.0.21": - version: 7.0.21 - resolution: "postcss@npm:7.0.21" +"postcss@npm:7.0.36": + version: 7.0.36 + resolution: "postcss@npm:7.0.36" dependencies: chalk: ^2.4.2 source-map: ^0.6.1 supports-color: ^6.1.0 - checksum: 5c11d58a4ffd54ddaf2f2f18ef7be10fc44405559ee56b52e41db8305d1b184d162138994dcce506ab77eef7283887a72d1b81cd1036c7fee106f50af0ef86d3 + checksum: 4cfc0989b9ad5d0e8971af80d87f9c5beac5c84cb89ff22ad69852edf73c0a2fa348e7e0a135b5897bf893edad0fe86c428769050431ad9b532f072ff530828d languageName: node linkType: hard @@ -30027,21 +30020,21 @@ __metadata: languageName: node linkType: hard -"resolve-url-loader@npm:^3.1.2": - version: 3.1.2 - resolution: "resolve-url-loader@npm:3.1.2" +"resolve-url-loader@npm:^3.1.5": + version: 3.1.5 + resolution: "resolve-url-loader@npm:3.1.5" dependencies: adjust-sourcemap-loader: 3.0.0 camelcase: 5.3.1 compose-function: 3.0.3 convert-source-map: 1.7.0 es6-iterator: 2.0.3 - loader-utils: 1.2.3 - postcss: 7.0.21 + loader-utils: ^1.2.3 + postcss: 7.0.36 rework: 1.0.1 rework-visit: 1.0.0 source-map: 0.6.1 - checksum: 02e559af8d10a8fda8d2cb1c61290b932787309309839288820438b4f25339a8c8cbd52598af89c1c1d277133d74914407e7a760e49acd966425a038798a6e70 + checksum: eb52911eff20723f07409cc12138d254fa0dd4a4f3b1ba11ee1b29912afb03f1272aaddb523658be1e3a946e0d1bf6f603d0e107753ab83d48ad2116cf04b7f6 languageName: node linkType: hard @@ -30471,10 +30464,10 @@ __metadata: languageName: node linkType: hard -"safe-stable-stringify@npm:^2.1.0": - version: 2.3.1 - resolution: "safe-stable-stringify@npm:2.3.1" - checksum: a0a0bad0294c3e2a9d1bf3cf2b1096dfb83c162d09a5e4891e488cce082120bd69161d2a92aae7fc48255290f17700decae9c89a07fe139794e61b5c8b411377 +"safe-stable-stringify@npm:^2.1.0, safe-stable-stringify@npm:^2.3.2": + version: 2.4.2 + resolution: "safe-stable-stringify@npm:2.4.2" + checksum: 0324ba2e40f78cae63e31a02b1c9bdf1b786621f9e8760845608eb9e81aef401944ac2078e5c9c1533cf516aea34d08fa8052ca853637ced84b791caaf1e394e languageName: node linkType: hard @@ -35064,8 +35057,8 @@ __metadata: linkType: hard "webpack@npm:>=4.43.0 <6.0.0, webpack@npm:^5.75.0, webpack@npm:^5.9.0": - version: 5.75.0 - resolution: "webpack@npm:5.75.0" + version: 5.76.2 + resolution: "webpack@npm:5.76.2" dependencies: "@types/eslint-scope": ^3.7.3 "@types/estree": ^0.0.51 @@ -35096,7 +35089,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 2bcc5f3c195f375944e8af2f00bf2feea39cb9fda5f763b0d1b00077f1c51783db25c94d3fae96a07dead9fa085e6ae7474417e5ab31719c9776ea5969ceb83a + checksum: 86db98299a175c371031449c26077e87b33acd8f45de7f7945ed4b9b37c8ca11bc5169af9c44743efccd4d55e08042a3aa3a3bc42aff831309a0821ffbcd395e languageName: node linkType: hard