From ee4bf2d2641b7d2d0732f0e16b891f4c3ae52d44 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 14 Aug 2023 11:08:59 -0500 Subject: [PATCH] Fix #19829: UX: Multichain: Move "Import Tokens" to Modal (#19553) * Move Import Tokens to Modal * Better dimensions for long token name * Add padding above tabs --- app/_locales/am/messages.json | 3 - app/_locales/ar/messages.json | 3 - app/_locales/bg/messages.json | 3 - app/_locales/bn/messages.json | 3 - app/_locales/ca/messages.json | 3 - app/_locales/cs/messages.json | 3 - app/_locales/da/messages.json | 3 - app/_locales/de/messages.json | 6 - app/_locales/el/messages.json | 6 - app/_locales/en/messages.json | 6 - app/_locales/es/messages.json | 6 - app/_locales/es_419/messages.json | 6 - app/_locales/et/messages.json | 3 - app/_locales/fa/messages.json | 3 - app/_locales/fi/messages.json | 3 - app/_locales/fil/messages.json | 3 - app/_locales/fr/messages.json | 6 - app/_locales/he/messages.json | 3 - app/_locales/hi/messages.json | 6 - app/_locales/hr/messages.json | 3 - app/_locales/ht/messages.json | 3 - app/_locales/hu/messages.json | 3 - app/_locales/id/messages.json | 6 - app/_locales/it/messages.json | 6 - app/_locales/ja/messages.json | 6 - app/_locales/kn/messages.json | 3 - app/_locales/ko/messages.json | 6 - app/_locales/lt/messages.json | 3 - app/_locales/lv/messages.json | 3 - app/_locales/ms/messages.json | 3 - app/_locales/no/messages.json | 3 - app/_locales/ph/messages.json | 3 - app/_locales/pl/messages.json | 3 - app/_locales/pt/messages.json | 6 - app/_locales/pt_BR/messages.json | 6 - app/_locales/ro/messages.json | 3 - app/_locales/ru/messages.json | 6 - app/_locales/sk/messages.json | 3 - app/_locales/sl/messages.json | 3 - app/_locales/sr/messages.json | 3 - app/_locales/sv/messages.json | 3 - app/_locales/sw/messages.json | 3 - app/_locales/ta/messages.json | 3 - app/_locales/tl/messages.json | 6 - app/_locales/tr/messages.json | 6 - app/_locales/uk/messages.json | 3 - app/_locales/vi/messages.json | 6 - app/_locales/zh_CN/messages.json | 6 - app/_locales/zh_TW/messages.json | 6 - test/e2e/metamask-ui.spec.js | 11 +- test/e2e/tests/add-hide-token.spec.js | 6 +- .../tests/custom-token-add-approve.spec.js | 18 +- test/e2e/tests/import-tokens.spec.js | 12 +- test/e2e/tests/token-details.spec.js | 18 +- ui/components/app/app-components.scss | 1 + ui/components/app/import-token/index.scss | 1 + .../app}/import-token/token-list/index.js | 0 .../app}/import-token/token-list/index.scss | 2 - .../token-list-placeholder/index.js | 0 .../token-list-placeholder.component.js | 35 + .../token-list-placeholder.stories.js | 2 +- .../token-list/token-list.component.js | 2 +- .../token-list/token-list.container.js | 0 .../app}/import-token/token-search/index.js | 0 .../token-search/token-search.component.js | 26 +- .../token-search/token-search.stories.js | 4 +- .../import-token-link/import-token-link.js | 7 +- .../import-token-link.test.js | 9 +- .../import-tokens-modal-confirm.js | 108 +++ .../import-tokens-modal-confirm.stories.js | 100 +++ .../import-tokens-modal.js | 649 +++++++++++++++++ .../import-tokens-modal.scss | 67 ++ .../import-tokens-modal.stories.js | 62 ++ .../import-tokens-modal.test.js | 200 ++++++ .../multichain/import-tokens-modal/index.js | 1 + ui/components/multichain/index.js | 1 + .../multichain/multichain-components.scss | 1 + .../page-container.component.js | 16 +- ui/components/ui/popover/index.scss | 4 + ui/ducks/app/app.ts | 14 + .../confirm-import-token.js | 150 ---- .../confirm-import-token.stories.js | 46 -- .../confirm-import-token.test.js | 128 ---- ui/pages/confirm-import-token/index.js | 3 - ui/pages/confirm-import-token/index.scss | 50 -- ui/pages/import-token/README.mdx | 31 - .../import-token/import-token.component.js | 659 ------------------ .../import-token/import-token.container.js | 68 -- ui/pages/import-token/import-token.stories.js | 120 ---- ui/pages/import-token/import-token.test.js | 178 ----- ui/pages/import-token/index.js | 3 - ui/pages/import-token/index.scss | 79 --- .../token-list-placeholder/index.scss | 27 - .../token-list-placeholder.component.js | 31 - ui/pages/pages.scss | 2 - ui/pages/routes/routes.component.js | 24 +- ui/pages/routes/routes.container.js | 3 + .../__snapshots__/security-tab.test.js.snap | 1 + .../security-tab/security-tab.component.js | 1 + ui/store/actionConstants.ts | 2 + ui/store/actions.ts | 12 + 101 files changed, 1339 insertions(+), 1860 deletions(-) create mode 100644 ui/components/app/import-token/index.scss rename ui/{pages => components/app}/import-token/token-list/index.js (100%) rename ui/{pages => components/app}/import-token/token-list/index.scss (96%) rename ui/{pages => components/app}/import-token/token-list/token-list-placeholder/index.js (100%) create mode 100644 ui/components/app/import-token/token-list/token-list-placeholder/token-list-placeholder.component.js rename ui/{pages => components/app}/import-token/token-list/token-list-placeholder/token-list-placeholder.stories.js (79%) rename ui/{pages => components/app}/import-token/token-list/token-list.component.js (96%) rename ui/{pages => components/app}/import-token/token-list/token-list.container.js (100%) rename ui/{pages => components/app}/import-token/token-search/index.js (100%) rename ui/{pages => components/app}/import-token/token-search/token-search.component.js (69%) rename ui/{pages => components/app}/import-token/token-search/token-search.stories.js (76%) create mode 100644 ui/components/multichain/import-tokens-modal/import-tokens-modal-confirm.js create mode 100644 ui/components/multichain/import-tokens-modal/import-tokens-modal-confirm.stories.js create mode 100644 ui/components/multichain/import-tokens-modal/import-tokens-modal.js create mode 100644 ui/components/multichain/import-tokens-modal/import-tokens-modal.scss create mode 100644 ui/components/multichain/import-tokens-modal/import-tokens-modal.stories.js create mode 100644 ui/components/multichain/import-tokens-modal/import-tokens-modal.test.js create mode 100644 ui/components/multichain/import-tokens-modal/index.js delete mode 100644 ui/pages/confirm-import-token/confirm-import-token.js delete mode 100644 ui/pages/confirm-import-token/confirm-import-token.stories.js delete mode 100644 ui/pages/confirm-import-token/confirm-import-token.test.js delete mode 100644 ui/pages/confirm-import-token/index.js delete mode 100644 ui/pages/confirm-import-token/index.scss delete mode 100644 ui/pages/import-token/README.mdx delete mode 100644 ui/pages/import-token/import-token.component.js delete mode 100644 ui/pages/import-token/import-token.container.js delete mode 100644 ui/pages/import-token/import-token.stories.js delete mode 100644 ui/pages/import-token/import-token.test.js delete mode 100644 ui/pages/import-token/index.js delete mode 100644 ui/pages/import-token/index.scss delete mode 100644 ui/pages/import-token/token-list/token-list-placeholder/index.scss delete mode 100644 ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.component.js diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index cea1b3cc3..a815821b9 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -609,9 +609,6 @@ "searchResults": { "message": "ውጤቶችን ፈልግ" }, - "searchTokens": { - "message": "ተለዋጭ ስሞችን ፈልግ" - }, "securityAndPrivacy": { "message": "ደህንነት እና ግላዊነት" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 1be520429..db4a5a6fe 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -621,9 +621,6 @@ "searchResults": { "message": "نتائج البحث" }, - "searchTokens": { - "message": "البحث عن العملات الرمزية" - }, "securityAndPrivacy": { "message": "الأمن والخصوصية" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 3743fcbf0..5e41be222 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -620,9 +620,6 @@ "searchResults": { "message": "Резултати от търсенето" }, - "searchTokens": { - "message": "Търсене на маркери" - }, "securityAndPrivacy": { "message": "Сигурност и поверителност" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 941fb82da..dfa33cf7c 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -618,9 +618,6 @@ "searchResults": { "message": "অনুসন্ধানের ফলাফলগুলি" }, - "searchTokens": { - "message": "টোকেনগুলি অনুসন্ধান করুন" - }, "securityAndPrivacy": { "message": "নিরাপত্তা এবং গোপনীয়তা" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index d5abf0507..fd33ec6f3 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -605,9 +605,6 @@ "searchResults": { "message": "Resultats de Cerca" }, - "searchTokens": { - "message": "Tokens per cercar" - }, "securityAndPrivacy": { "message": "Seguretat i privacitat" }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 1a617a7b7..b0702badb 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -289,9 +289,6 @@ "search": { "message": "Hledat" }, - "searchTokens": { - "message": "Hledat tokeny" - }, "seedPhraseReq": { "message": "klíčové fráze mají 12 slov" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 85f449875..fb5b26f48 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -605,9 +605,6 @@ "searchResults": { "message": "Søg Resultater" }, - "searchTokens": { - "message": "Søg efter tokens" - }, "securityAndPrivacy": { "message": "Sikkerhed & Privatliv" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 9379779b6..01948c981 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "Benutzerdefiniertes Netzwerk hinzufügen" }, - "addCustomToken": { - "message": "Kunden-Token hinzufügen" - }, "addEthereumChainConfirmationDescription": { "message": "Dadurch kann dieses Netzwerk innerhalb MetaMask verwendet werden." }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "Suchergebnisse" }, - "searchTokens": { - "message": "Token suchen" - }, "secretRecoveryPhrase": { "message": "Geheime Wiederherstellungsphrase" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 5875c30e2..691927f66 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "Προσθήκη προσαρμοσμένου δικτύου" }, - "addCustomToken": { - "message": "Προσθήκη Προσαρμοσμένου Token" - }, "addEthereumChainConfirmationDescription": { "message": "Αυτό θα επιτρέψει σε αυτό το δίκτυο να χρησιμοποιηθεί στο MetaMask." }, @@ -2977,9 +2974,6 @@ "searchAccounts": { "message": "Αναζήτηση Λογαριασμών" }, - "searchTokens": { - "message": "Αναζήτηση Tokens" - }, "secretRecoveryPhrase": { "message": "Μυστική Φράση Ανάκτησης" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 18e28b7d9..a5073b434 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -192,9 +192,6 @@ "addCustomNetwork": { "message": "Add custom network" }, - "addCustomToken": { - "message": "Add custom token" - }, "addEthereumChainConfirmationDescription": { "message": "This will allow this network to be used within MetaMask." }, @@ -3653,9 +3650,6 @@ "searchResults": { "message": "Search results" }, - "searchTokens": { - "message": "Search tokens" - }, "secretRecoveryPhrase": { "message": "Secret Recovery Phrase" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index be5afad23..2f094077c 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "Agregar red personalizada" }, - "addCustomToken": { - "message": "Añadir token personalizado" - }, "addEthereumChainConfirmationDescription": { "message": "Esto permitirá que la red se utilice en MetaMask." }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "Resultados de la búsqueda" }, - "searchTokens": { - "message": "Buscar tokens" - }, "secretRecoveryPhrase": { "message": "Frase secreta de recuperación" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 6e209ec6b..1c3adff3a 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -100,9 +100,6 @@ "addContact": { "message": "Agregar contacto" }, - "addCustomToken": { - "message": "Añadir token personalizado" - }, "addEthereumChainConfirmationDescription": { "message": "Esto permitirá que la red se utilice en MetaMask." }, @@ -1882,9 +1879,6 @@ "searchResults": { "message": "Resultados de la búsqueda" }, - "searchTokens": { - "message": "Buscar tokens" - }, "secretRecoveryPhrase": { "message": "Frase secreta de recuperación" }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index 073f73d75..e0b6889e9 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -614,9 +614,6 @@ "searchResults": { "message": "Otsingutulemused" }, - "searchTokens": { - "message": "Lubade otsimine" - }, "securityAndPrivacy": { "message": "Turvalisus ja privaatsus" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index cf24c0c61..54db00df5 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -624,9 +624,6 @@ "searchResults": { "message": "نتایج جستجو" }, - "searchTokens": { - "message": "رمزیاب های جستجو" - }, "securityAndPrivacy": { "message": "امنیت و حریم خصوصی" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 478620170..9bf17fcef 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -621,9 +621,6 @@ "searchResults": { "message": "Hakutulokset" }, - "searchTokens": { - "message": "Hae tietueita" - }, "securityAndPrivacy": { "message": "Turva & yksityisyys" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index b99f5a5ea..bb8270894 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -548,9 +548,6 @@ "searchResults": { "message": "Mga Resulta ng Paghahanap" }, - "searchTokens": { - "message": "Maghanap ng Mga Token" - }, "securityAndPrivacy": { "message": "Seguridad at Privacy" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 340f707c6..1e990f8d7 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "Ajouter un réseau personnalisé" }, - "addCustomToken": { - "message": "Ajouter un jeton personnalisé" - }, "addEthereumChainConfirmationDescription": { "message": "Cela permettra d’utiliser ce réseau dans MetaMask." }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "Résultats de la recherche" }, - "searchTokens": { - "message": "Rechercher des jetons" - }, "secretRecoveryPhrase": { "message": "Phrase secrète de récupération" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 15dd5adac..ab70fe6ee 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -621,9 +621,6 @@ "searchResults": { "message": "תוצאות חיפוש" }, - "searchTokens": { - "message": "חיפוש טוקנים" - }, "securityAndPrivacy": { "message": "אבטחה ופרטיות" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 1a69af91b..d80b47528 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "कस्टम नेटवर्क जोड़ें" }, - "addCustomToken": { - "message": "कस्टम टोकन जोड़ें" - }, "addEthereumChainConfirmationDescription": { "message": "इससे इस नेटवर्क को MetaMask के अंदर उपयोग करने की अनुमति मिलेगी।" }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "खोज परिणाम" }, - "searchTokens": { - "message": "टोकन खोजें" - }, "secretRecoveryPhrase": { "message": "सीक्रेट रिकवरी फ्रेज" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index de36eb424..aa20baa31 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -617,9 +617,6 @@ "searchResults": { "message": "Rezultati pretraživanja" }, - "searchTokens": { - "message": "Pretraži tokene" - }, "securityAndPrivacy": { "message": "Sigurnost i privatnost" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 601e049be..d14e5f7a7 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -452,9 +452,6 @@ "searchResults": { "message": "Rezilta rechèch" }, - "searchTokens": { - "message": "Rechèch Tokens" - }, "seedPhraseReq": { "message": "Seed fraz yo se 12 long mo" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index a1373e469..ca77ac427 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -617,9 +617,6 @@ "searchResults": { "message": "Keresési eredmények" }, - "searchTokens": { - "message": "Keresés a tokenek között" - }, "securityAndPrivacy": { "message": "Biztonság és adatvédelem" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 7a7fc5eb2..62e53b537 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "Tambahkan jaringan khusus" }, - "addCustomToken": { - "message": "Tambahkan token kustom" - }, "addEthereumChainConfirmationDescription": { "message": "Tindakan ini akan membantu jaringan ini agar dapat digunakan dengan MetaMask." }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "Cari hasil" }, - "searchTokens": { - "message": "Cari token" - }, "secretRecoveryPhrase": { "message": "Frasa Pemulihan Rahasia" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index a301f751f..4545daa82 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -155,9 +155,6 @@ "addContact": { "message": "Aggiungi contatto" }, - "addCustomToken": { - "message": "Aggiungi token personalizzato" - }, "addEthereumChainConfirmationDescription": { "message": "Ciò consentirà a questa rete di essere utilizzata all'interno di MetaMask." }, @@ -1380,9 +1377,6 @@ "searchResults": { "message": "Risultati Ricerca" }, - "searchTokens": { - "message": "Cerca Tokens" - }, "securityAndPrivacy": { "message": "Sicurezza & Privacy" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 58225cb1e..1c4c4827e 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "カスタムネットワークを追加" }, - "addCustomToken": { - "message": "カスタムトークンを追加" - }, "addEthereumChainConfirmationDescription": { "message": "これにより、このネットワークはMetaMask内で使用できるようになります。" }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "検索結果" }, - "searchTokens": { - "message": "トークンを検索" - }, "secretRecoveryPhrase": { "message": "シークレットリカバリーフレーズ" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index c4138b3a8..62a51a898 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -624,9 +624,6 @@ "searchResults": { "message": "ಹುಡುಕಾಟ ಫಲಿತಾಂಶಗಳು" }, - "searchTokens": { - "message": "ಟೋಕನ್‌ಗಳನ್ನು ಹುಡುಕಿ" - }, "securityAndPrivacy": { "message": "ಭದ್ರತೆ ಮತ್ತು ಗೌಪ್ಯತೆ" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index fccf6041d..741f9de5a 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "커스텀 네트워크 추가" }, - "addCustomToken": { - "message": "커스텀 토큰 추가" - }, "addEthereumChainConfirmationDescription": { "message": "이렇게 하면 MetaMask 내에서 이 네트워크를 사용할 수 있습니다." }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "검색 결과" }, - "searchTokens": { - "message": "토큰 검색" - }, "secretRecoveryPhrase": { "message": "비밀 복구 구문" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index 36d58197b..f1294dbde 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -624,9 +624,6 @@ "searchResults": { "message": "Paieškos rezultatai" }, - "searchTokens": { - "message": "Ieškoti žetonų" - }, "securityAndPrivacy": { "message": "Sauga ir privatumas" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 70e2a3333..a2c9cc425 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -620,9 +620,6 @@ "searchResults": { "message": "Meklēšanas rezultāti" }, - "searchTokens": { - "message": "Meklēt marķierus" - }, "securityAndPrivacy": { "message": "Drošība un konfidencialitāte" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index e1ff1b6e3..e94e1ccae 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -604,9 +604,6 @@ "searchResults": { "message": "Hasil Carian" }, - "searchTokens": { - "message": "Cari Token" - }, "securityAndPrivacy": { "message": "Keselamatan & Privasi" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index e53ecfc02..820dbcf35 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -608,9 +608,6 @@ "searchResults": { "message": "Søkeresultater" }, - "searchTokens": { - "message": "Søk i sjetonger" - }, "securityAndPrivacy": { "message": "Sikkerhet og personvern" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 33e995e54..755965d2a 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -1211,9 +1211,6 @@ "searchResults": { "message": "Mga Resulta ng Paghahanap" }, - "searchTokens": { - "message": "Maghanap ng Mga Token" - }, "securityAndPrivacy": { "message": "Seguridad at Privacy" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 32664ebda..9ec3a0677 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -618,9 +618,6 @@ "searchResults": { "message": "Wyniki wyszukiwania" }, - "searchTokens": { - "message": "Szukaj tokenów" - }, "securityAndPrivacy": { "message": "Bezpieczeństwo i prywatność" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index c3d06b188..540d8d39a 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "Adicionar rede personalizada" }, - "addCustomToken": { - "message": "Adicionar token personalizado" - }, "addEthereumChainConfirmationDescription": { "message": "Isso permitirá que essa rede seja usada dentro da MetaMask." }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "Resultados da busca" }, - "searchTokens": { - "message": "Buscar tokens" - }, "secretRecoveryPhrase": { "message": "Frase Secreta de Recuperação" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index b023dbb67..bde907241 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -100,9 +100,6 @@ "addContact": { "message": "Adicionar contato" }, - "addCustomToken": { - "message": "Adicionar token personalizado" - }, "addEthereumChainConfirmationDescription": { "message": "Isso permitirá que essa rede seja usada dentro da MetaMask." }, @@ -1882,9 +1879,6 @@ "searchResults": { "message": "Resultados da busca" }, - "searchTokens": { - "message": "Buscar tokens" - }, "secretRecoveryPhrase": { "message": "Frase de Recuperação Secreta" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index d39e5b4e6..7b0ae2062 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -611,9 +611,6 @@ "searchResults": { "message": "Rezultate căutare" }, - "searchTokens": { - "message": "Căutați token-uri" - }, "securityAndPrivacy": { "message": "Securitate și confidențialitate" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 2e0f11573..22c7cff59 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "Добавить пользовательскую сеть" }, - "addCustomToken": { - "message": "Добавить пользовательский токен" - }, "addEthereumChainConfirmationDescription": { "message": "Это позволит использовать эту сеть в MetaMask." }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "Результаты поиска" }, - "searchTokens": { - "message": "Поиск токенов" - }, "secretRecoveryPhrase": { "message": "Секретная фраза для восстановления" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 3dcf16933..277713b1c 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -596,9 +596,6 @@ "searchResults": { "message": "Výsledky vyhľadávania" }, - "searchTokens": { - "message": "Hledat tokeny" - }, "securityAndPrivacy": { "message": "Bezpečnosť a súkromie" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 92c40f8d4..b81cc3630 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -612,9 +612,6 @@ "searchResults": { "message": "Rezultati iskanja" }, - "searchTokens": { - "message": "Iskanje žetonov" - }, "securityAndPrivacy": { "message": "Varnost in zasebnost" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index e061fa55a..54a833755 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -615,9 +615,6 @@ "searchResults": { "message": "Rezultati pretrage" }, - "searchTokens": { - "message": "Pretražite tokene" - }, "securityAndPrivacy": { "message": "Bezbednost i privatnost" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index 9f02c1ce2..637c22227 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -608,9 +608,6 @@ "searchResults": { "message": "Sökresultat" }, - "searchTokens": { - "message": "Sök tokens" - }, "securityAndPrivacy": { "message": "Säkerhet och integritet" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 0ddfe8ac6..1b8fc27d0 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -602,9 +602,6 @@ "searchResults": { "message": "Matokeo ya Utafutaji" }, - "searchTokens": { - "message": "Tafuta Vianzio" - }, "securityAndPrivacy": { "message": "Ulinzi na Faragha" }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index eb6f2c499..909ff25ec 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -359,9 +359,6 @@ "search": { "message": "தேடல்" }, - "searchTokens": { - "message": "தேடல் டோக்கன்ஸ்" - }, "seedPhraseReq": { "message": "விதை வாக்கியங்கள் 12 வார்த்தைகள் நீண்டவை" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index cff8ca4b2..82311d906 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "Magdagdag ng custom na network" }, - "addCustomToken": { - "message": "Magdagdag ng Custom na Token" - }, "addEthereumChainConfirmationDescription": { "message": "Magpapahintulot ito sa network na ito na gamitin sa loob ng MetaMask." }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "Mga Resulta ng Paghahanap" }, - "searchTokens": { - "message": "Maghanap ng Mga Token" - }, "secretRecoveryPhrase": { "message": "Lihim na recovery phrase" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index b181d5d95..b6ad12c28 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "Özel ağ ekle" }, - "addCustomToken": { - "message": "Özel token ekle" - }, "addEthereumChainConfirmationDescription": { "message": "Bu, bu ağın MetaMas dahilinde kullanılmasına olanak tanıyacaktır." }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "Arama sonuçları" }, - "searchTokens": { - "message": "Token ara" - }, "secretRecoveryPhrase": { "message": "Gizli Kurtarma İfadesi" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index e9505d2b2..bba5b27f1 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -624,9 +624,6 @@ "searchResults": { "message": "Результати пошуку" }, - "searchTokens": { - "message": "Шукати токени" - }, "securityAndPrivacy": { "message": "Безпека й конфіденційність" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 982edf876..ea52fc82b 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "Thêm mạng tùy chỉnh" }, - "addCustomToken": { - "message": "Thêm token tùy chỉnh" - }, "addEthereumChainConfirmationDescription": { "message": "Thao tác này sẽ cho phép sử dụng mạng này trong MetaMask." }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "Kết quả tìm kiếm" }, - "searchTokens": { - "message": "Tìm kiếm token" - }, "secretRecoveryPhrase": { "message": "Cụm Mật Khẩu Khôi Phục Bí Mật" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 303e62866..2aade1fdf 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -186,9 +186,6 @@ "addCustomNetwork": { "message": "添加自定义网络" }, - "addCustomToken": { - "message": "添加自定义代币" - }, "addEthereumChainConfirmationDescription": { "message": "这将允许在 MetaMask 中使用此网络。" }, @@ -2980,9 +2977,6 @@ "searchResults": { "message": "搜索结果" }, - "searchTokens": { - "message": "搜索代币" - }, "secretRecoveryPhrase": { "message": "助记词" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 276d101e5..8a95e870f 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -42,9 +42,6 @@ "addContact": { "message": "新增合約" }, - "addCustomToken": { - "message": "Add Custom Token" - }, "addEthereumChainConfirmationDescription": { "message": "這會允許在 MetaMask 內使用這個網路。" }, @@ -1133,9 +1130,6 @@ "searchResults": { "message": "搜尋結果" }, - "searchTokens": { - "message": "搜尋代幣" - }, "secureWallet": { "message": "Secure Wallet" }, diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index ac5ac1861..86964698f 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -257,13 +257,18 @@ describe('MetaMask', function () { }); await driver.delay(regularDelayMs); - await driver.fill('#custom-address', tokenAddress); + await driver.fill( + '[data-testid="import-tokens-modal-custom-address"]', + tokenAddress, + ); await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'Add custom token', tag: 'button' }); + await driver.clickElement({ text: 'Next', tag: 'button' }); await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.clickElement( + '[data-testid="import-tokens-modal-import-button"]', + ); await driver.delay(regularDelayMs); }); diff --git a/test/e2e/tests/add-hide-token.spec.js b/test/e2e/tests/add-hide-token.spec.js index b1ead17a8..533611ff2 100644 --- a/test/e2e/tests/add-hide-token.spec.js +++ b/test/e2e/tests/add-hide-token.spec.js @@ -108,13 +108,15 @@ describe('Add existing token using search', function () { await driver.press('#password', driver.Key.ENTER); await driver.clickElement({ text: 'Import tokens', tag: 'button' }); - await driver.fill('#search-tokens', 'BAT'); + await driver.fill('input[placeholder="Search"]', 'BAT'); await driver.clickElement({ text: 'BAT', tag: 'span', }); await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.clickElement( + '[data-testid="import-tokens-modal-import-button"]', + ); await driver.waitForSelector({ css: '.token-overview__primary-balance', diff --git a/test/e2e/tests/custom-token-add-approve.spec.js b/test/e2e/tests/custom-token-add-approve.spec.js index 2cae7ff50..f5f58e5d8 100644 --- a/test/e2e/tests/custom-token-add-approve.spec.js +++ b/test/e2e/tests/custom-token-add-approve.spec.js @@ -51,20 +51,24 @@ describe('Create token, approve token and approve token without gas', function ( text: 'Custom token', tag: 'button', }); - await driver.fill('#custom-address', contractAddress); - await driver.waitForSelector('#custom-decimals'); + await driver.fill( + '[data-testid="import-tokens-modal-custom-address"]', + contractAddress, + ); + await driver.waitForSelector( + '[data-testid="import-tokens-modal-custom-decimals"]', + ); await driver.delay(2000); await driver.clickElement({ - text: 'Add custom token', + text: 'Next', tag: 'button', }); await driver.delay(2000); - await driver.clickElement({ - text: 'Import tokens', - tag: 'button', - }); + await driver.clickElement( + '[data-testid="import-tokens-modal-import-button"]', + ); // renders balance for newly created token await driver.clickElement('.app-header__logo-container'); diff --git a/test/e2e/tests/import-tokens.spec.js b/test/e2e/tests/import-tokens.spec.js index d86d5f097..01adbb2ac 100644 --- a/test/e2e/tests/import-tokens.spec.js +++ b/test/e2e/tests/import-tokens.spec.js @@ -37,14 +37,20 @@ describe('Import flow', function () { await driver.delay(regularDelayMs); await driver.clickElement('[data-testid="import-token-button"]'); - await driver.fill('input[placeholder="Search tokens"]', 'cha'); + await driver.fill('input[placeholder="Search"]', 'cha'); await driver.clickElement('.token-list__token'); await driver.clickElement('.token-list__token:nth-of-type(2)'); await driver.clickElement('.token-list__token:nth-of-type(3)'); - await driver.clickElement({ css: 'button', text: 'Next' }); - await driver.clickElement({ css: 'button', text: 'Import' }); + await driver.clickElement({ + css: '.import-tokens-modal button', + text: 'Next', + }); + await driver.clickElement({ + css: '.import-tokens-modal button', + text: 'Import', + }); await driver.clickElement('.asset-breadcrumb'); diff --git a/test/e2e/tests/token-details.spec.js b/test/e2e/tests/token-details.spec.js index 07c4a8570..e365b760d 100644 --- a/test/e2e/tests/token-details.spec.js +++ b/test/e2e/tests/token-details.spec.js @@ -30,11 +30,19 @@ describe('Token Details', function () { const tokenAddress = '0x2EFA2Cb29C2341d8E5Ba7D3262C9e9d6f1Bf3711'; const tokenSymbol = 'AAVE'; - await driver.fill('#custom-address', tokenAddress); - await driver.waitForSelector('#custom-symbol-helper-text'); - await driver.fill('#custom-symbol', tokenSymbol); - await driver.clickElement({ text: 'Add custom token', tag: 'button' }); - await driver.clickElement({ text: 'Import tokens', tag: 'button' }); + await driver.fill( + '[data-testid="import-tokens-modal-custom-address"]', + tokenAddress, + ); + await driver.waitForSelector('p.mm-box--color-error-default'); + await driver.fill( + '[data-testid="import-tokens-modal-custom-symbol"]', + tokenSymbol, + ); + await driver.clickElement({ text: 'Next', tag: 'button' }); + await driver.clickElement( + '[data-testid="import-tokens-modal-import-button"]', + ); await driver.clickElement('[aria-label="Asset options"]'); await driver.clickElement({ text: 'Token details', tag: 'div' }); diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index b3a9ed194..017e7deb6 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -11,6 +11,7 @@ @import 'confirm-data/index'; @import 'confirmation-warning-modal/index'; @import 'custom-nonce/index'; +@import 'import-token/index'; @import 'nfts-items/index'; @import 'nfts-tab/index'; @import 'nft-details/index'; diff --git a/ui/components/app/import-token/index.scss b/ui/components/app/import-token/index.scss new file mode 100644 index 000000000..24b210c93 --- /dev/null +++ b/ui/components/app/import-token/index.scss @@ -0,0 +1 @@ +@import 'token-list/index'; diff --git a/ui/pages/import-token/token-list/index.js b/ui/components/app/import-token/token-list/index.js similarity index 100% rename from ui/pages/import-token/token-list/index.js rename to ui/components/app/import-token/token-list/index.js diff --git a/ui/pages/import-token/token-list/index.scss b/ui/components/app/import-token/token-list/index.scss similarity index 96% rename from ui/pages/import-token/token-list/index.scss rename to ui/components/app/import-token/token-list/index.scss index b776d093c..e00298ada 100644 --- a/ui/pages/import-token/token-list/index.scss +++ b/ui/components/app/import-token/token-list/index.scss @@ -1,5 +1,3 @@ -@import 'token-list-placeholder/index'; - .token-list { &__title { @include H7; diff --git a/ui/pages/import-token/token-list/token-list-placeholder/index.js b/ui/components/app/import-token/token-list/token-list-placeholder/index.js similarity index 100% rename from ui/pages/import-token/token-list/token-list-placeholder/index.js rename to ui/components/app/import-token/token-list/token-list-placeholder/index.js diff --git a/ui/components/app/import-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/components/app/import-token/token-list/token-list-placeholder/token-list-placeholder.component.js new file mode 100644 index 000000000..7cbf662b1 --- /dev/null +++ b/ui/components/app/import-token/token-list/token-list-placeholder/token-list-placeholder.component.js @@ -0,0 +1,35 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ZENDESK_URLS from '../../../../../helpers/constants/zendesk-url'; +import { ButtonLink, Text, Box } from '../../../../component-library'; +import { + Display, + FlexDirection, + TextAlign, + TextColor, + AlignItems, +} from '../../../../../helpers/constants/design-system'; + +export default class TokenListPlaceholder extends Component { + static contextTypes = { + t: PropTypes.func, + }; + + render() { + return ( + + + {this.context.t('addAcquiredTokens')} + + + {this.context.t('learnMoreUpperCase')} + + + ); + } +} diff --git a/ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.stories.js b/ui/components/app/import-token/token-list/token-list-placeholder/token-list-placeholder.stories.js similarity index 79% rename from ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.stories.js rename to ui/components/app/import-token/token-list/token-list-placeholder/token-list-placeholder.stories.js index 72386103d..2cbe8f608 100644 --- a/ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.stories.js +++ b/ui/components/app/import-token/token-list/token-list-placeholder/token-list-placeholder.stories.js @@ -2,7 +2,7 @@ import React from 'react'; import TokenListPlaceholder from './token-list-placeholder.component'; export default { - title: 'Pages/ImportToken/TokenList/TokenListPlaceholder', + title: 'Components/App/TokenList/TokenListPlaceholder', }; export const DefaultStory = () => { diff --git a/ui/pages/import-token/token-list/token-list.component.js b/ui/components/app/import-token/token-list/token-list.component.js similarity index 96% rename from ui/pages/import-token/token-list/token-list.component.js rename to ui/components/app/import-token/token-list/token-list.component.js index fed54e5ad..e57d06be2 100644 --- a/ui/pages/import-token/token-list/token-list.component.js +++ b/ui/components/app/import-token/token-list/token-list.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { checkExistingAddresses } from '../../../helpers/utils/util'; +import { checkExistingAddresses } from '../../../../helpers/utils/util'; import TokenListPlaceholder from './token-list-placeholder'; export default class TokenList extends Component { diff --git a/ui/pages/import-token/token-list/token-list.container.js b/ui/components/app/import-token/token-list/token-list.container.js similarity index 100% rename from ui/pages/import-token/token-list/token-list.container.js rename to ui/components/app/import-token/token-list/token-list.container.js diff --git a/ui/pages/import-token/token-search/index.js b/ui/components/app/import-token/token-search/index.js similarity index 100% rename from ui/pages/import-token/token-search/index.js rename to ui/components/app/import-token/token-search/index.js diff --git a/ui/pages/import-token/token-search/token-search.component.js b/ui/components/app/import-token/token-search/token-search.component.js similarity index 69% rename from ui/pages/import-token/token-search/token-search.component.js rename to ui/components/app/import-token/token-search/token-search.component.js index b6546cd78..853d226ae 100644 --- a/ui/pages/import-token/token-search/token-search.component.js +++ b/ui/components/app/import-token/token-search/token-search.component.js @@ -1,10 +1,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Fuse from 'fuse.js'; -import InputAdornment from '@material-ui/core/InputAdornment'; -import TextField from '../../../components/ui/text-field'; -import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; -import SearchIcon from '../../../components/ui/icon/search-icon'; +import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils'; +import { TextFieldSearch } from '../../../component-library'; +import { BlockSize } from '../../../../helpers/constants/design-system'; export default class TokenSearch extends Component { static contextTypes = { @@ -57,30 +56,19 @@ export default class TokenSearch extends Component { this.props.onSearch({ searchQuery, results }); } - renderAdornment() { - return ( - - - - ); - } - render() { const { error } = this.props; const { searchQuery } = this.state; return ( - this.handleSearch(e.target.value)} error={error} - fullWidth autoFocus - autoComplete="off" - startAdornment={this.renderAdornment()} + autoComplete={false} + width={BlockSize.Full} /> ); } diff --git a/ui/pages/import-token/token-search/token-search.stories.js b/ui/components/app/import-token/token-search/token-search.stories.js similarity index 76% rename from ui/pages/import-token/token-search/token-search.stories.js rename to ui/components/app/import-token/token-search/token-search.stories.js index 8b0156474..57ec57cea 100644 --- a/ui/pages/import-token/token-search/token-search.stories.js +++ b/ui/components/app/import-token/token-search/token-search.stories.js @@ -1,9 +1,9 @@ import React from 'react'; -import testData from '../../../../.storybook/test-data'; +import testData from '../../../../../.storybook/test-data'; import TokenSearch from './token-search.component'; export default { - title: 'Pages/ImportToken/TokenSearch', + title: 'Components/App/ImportToken/TokenSearch', argTypes: { error: { diff --git a/ui/components/multichain/import-token-link/import-token-link.js b/ui/components/multichain/import-token-link/import-token-link.js index 699db9136..914e8110f 100644 --- a/ui/components/multichain/import-token-link/import-token-link.js +++ b/ui/components/multichain/import-token-link/import-token-link.js @@ -1,6 +1,5 @@ import React, { useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { ButtonLink, IconName, Box } from '../../component-library'; @@ -10,8 +9,7 @@ import { 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 { detectNewTokens, showImportTokensModal } from '../../../store/actions'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory, @@ -25,7 +23,6 @@ import { export const ImportTokenLink = ({ className, ...props }) => { const trackEvent = useContext(MetaMetricsContext); const t = useI18nContext(); - const history = useHistory(); const dispatch = useDispatch(); const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported); @@ -48,7 +45,7 @@ export const ImportTokenLink = ({ className, ...props }) => { data-testid="import-token-button" startIconName={IconName.Add} onClick={() => { - history.push(IMPORT_TOKEN_ROUTE); + dispatch(showImportTokensModal()); trackEvent({ event: MetaMetricsEventName.TokenImportButtonClicked, category: MetaMetricsEventCategory.Navigation, diff --git a/ui/components/multichain/import-token-link/import-token-link.test.js b/ui/components/multichain/import-token-link/import-token-link.test.js index a4763f6de..f4008cfc3 100644 --- a/ui/components/multichain/import-token-link/import-token-link.test.js +++ b/ui/components/multichain/import-token-link/import-token-link.test.js @@ -19,7 +19,12 @@ jest.mock('react-router-dom', () => { }); jest.mock('../../../store/actions.ts', () => ({ - detectNewTokens: jest.fn().mockReturnValue({ type: '' }), + detectNewTokens: jest + .fn() + .mockImplementation(() => ({ type: 'DETECT_TOKENS' })), + showImportTokensModal: jest + .fn() + .mockImplementation(() => ({ type: 'UI_IMPORT_TOKENS_POPOVER_OPEN' })), })); describe('Import Token Link', () => { @@ -90,6 +95,6 @@ describe('Import Token Link', () => { const importToken = screen.getByTestId('import-token-button'); fireEvent.click(importToken); - expect(mockPushHistory).toHaveBeenCalledWith('/import-token'); + expect(screen.getByText('Import tokens')).toBeInTheDocument(); }); }); diff --git a/ui/components/multichain/import-tokens-modal/import-tokens-modal-confirm.js b/ui/components/multichain/import-tokens-modal/import-tokens-modal-confirm.js new file mode 100644 index 000000000..43d4808f8 --- /dev/null +++ b/ui/components/multichain/import-tokens-modal/import-tokens-modal-confirm.js @@ -0,0 +1,108 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + Box, + ButtonPrimary, + ButtonSecondary, + Text, +} from '../../component-library'; +import { + AlignItems, + Display, + Size, + TextAlign, + TextColor, + TextVariant, +} from '../../../helpers/constants/design-system'; +import TokenBalance from '../../ui/token-balance/token-balance'; +import Identicon from '../../ui/identicon'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { getPendingTokens } from '../../../ducks/metamask/metamask'; + +export const ImportTokensModalConfirm = ({ onBackClick, onImportClick }) => { + const t = useI18nContext(); + const pendingTokens = useSelector(getPendingTokens); + + return ( + + {t('likeToImportTokens')} + + + + {t('token')} + + + {t('balance')} + + + + {Object.entries(pendingTokens).map(([address, token]) => { + const { name, symbol } = token; + return ( + + + + + {name} + + {symbol} + + + + + + + + ); + })} + + + + {t('back')} + + + {t('import')} + + + + + ); +}; + +ImportTokensModalConfirm.propTypes = { + onBackClick: PropTypes.func.isRequired, + onImportClick: PropTypes.func.isRequired, +}; diff --git a/ui/components/multichain/import-tokens-modal/import-tokens-modal-confirm.stories.js b/ui/components/multichain/import-tokens-modal/import-tokens-modal-confirm.stories.js new file mode 100644 index 000000000..2df36b65d --- /dev/null +++ b/ui/components/multichain/import-tokens-modal/import-tokens-modal-confirm.stories.js @@ -0,0 +1,100 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { ImportTokensModalConfirm } from './import-tokens-modal-confirm'; + +const createStore = ( + chainId = CHAIN_IDS.MAINNET, + useTokenDetection = true, + tokenRepetition = 1, +) => { + return configureStore({ + ...testData, + metamask: { + ...testData.metamask, + useTokenDetection, + providerConfig: { chainId }, + pendingTokens: { + '0x0000000de40dfa9b17854cbc7869d80f9f98d823': { + address: '0x0000000de40dfa9b17854cbc7869d80f9f98d823', + aggregators: ['CoinGecko', 'Sonarwatch', 'Coinmarketcap'], + decimals: 18, + fees: {}, + iconUrl: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0x0000000de40dfa9b17854cbc7869d80f9f98d823.png', + name: 'delta theta'.repeat(tokenRepetition), + occurrences: 3, + symbol: 'DLTA', + type: 'erc20', + unlisted: false, + }, + '0x00d8318e44780edeefcf3020a5448f636788883c': { + address: '0x00d8318e44780edeefcf3020a5448f636788883c', + aggregators: ['CoinGecko', 'Sonarwatch', 'Coinmarketcap'], + decimals: 18, + fees: {}, + iconUrl: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0x00d8318e44780edeefcf3020a5448f636788883c.png', + name: 'dAppstore'.repeat(tokenRepetition), + occurrences: 3, + symbol: 'DAPPX', + type: 'erc20', + unlisted: false, + }, + '0x00e679ba63b509182c349f5614f0a07cdd0ce0c5': { + address: '0x00e679ba63b509182c349f5614f0a07cdd0ce0c5', + aggregators: ['CoinGecko', 'Sonarwatch', 'Coinmarketcap'], + decimals: 18, + iconUrl: + 'https://static.metafi.codefi.network/api/v1/tokenIcons/1/0x00e679ba63b509182c349f5614f0a07cdd0ce0c5.png', + name: 'Damex Token'.repeat(tokenRepetition), + occurrences: 3, + symbol: 'DAMEX', + type: 'erc20', + unlisted: false, + }, + }, + }, + }); +}; + +export default { + title: 'Components/Multichain/ImportTokensModal/ImportTokensModalConfirm', + component: ImportTokensModalConfirm, + argTypes: { + onBackClick: { + action: 'onClose', + }, + onImportClick: { + action: 'onClose', + }, + }, +}; + +export const DefaultStory = (args) => ; +DefaultStory.decorators = [ + (Story) => ( + + + + ), +]; + +DefaultStory.storyName = 'Default'; + +export const LongValueStory = (args) => ( +
+ +
+); +LongValueStory.decorators = [ + (Story) => ( + + + + ), +]; + +LongValueStory.storyName = 'LongValueStory'; diff --git a/ui/components/multichain/import-tokens-modal/import-tokens-modal.js b/ui/components/multichain/import-tokens-modal/import-tokens-modal.js new file mode 100644 index 000000000..2e8ef6b10 --- /dev/null +++ b/ui/components/multichain/import-tokens-modal/import-tokens-modal.js @@ -0,0 +1,649 @@ +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { getTokenTrackerLink } from '@metamask/etherscan-link/dist/token-tracker-link'; +import { Tab, Tabs } from '../../ui/tabs'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + getCurrentChainId, + getIsDynamicTokenListAvailable, + getIsMainnet, + getIsTokenDetectionInactiveOnMainnet, + getIsTokenDetectionSupported, + getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, + getMetaMaskIdentities, + getRpcPrefsForCurrentProvider, + getSelectedAddress, + getTokenDetectionSupportNetworkByChainId, + getTokenList, +} from '../../../selectors'; +import { + addImportedTokens, + clearPendingTokens, + getTokenStandardAndDetails, + setPendingTokens, + showImportNftsModal, +} from '../../../store/actions'; +import { + BannerAlert, + ButtonLink, + ButtonPrimary, + Text, + FormTextField, + Box, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, +} from '../../component-library'; +import TokenSearch from '../../app/import-token/token-search'; +import TokenList from '../../app/import-token/token-list'; + +import { + FontWeight, + Severity, + Size, + TextAlign, + TextColor, +} from '../../../helpers/constants/design-system'; + +import { ASSET_ROUTE, SECURITY_ROUTE } from '../../../helpers/constants/routes'; +import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; +import { isValidHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { addHexPrefix } from '../../../../app/scripts/lib/util'; +import { STATIC_MAINNET_TOKEN_LIST } from '../../../../shared/constants/tokens'; +import { + AssetType, + TokenStandard, +} from '../../../../shared/constants/transaction'; +import { + checkExistingAddresses, + getURLHostName, +} from '../../../helpers/utils/util'; +import { tokenInfoGetter } from '../../../helpers/utils/token-util'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { getPendingTokens } from '../../../ducks/metamask/metamask'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, + MetaMetricsTokenEventSource, +} from '../../../../shared/constants/metametrics'; +import { ImportTokensModalConfirm } from './import-tokens-modal-confirm'; + +export const ImportTokensModal = ({ onClose }) => { + const t = useI18nContext(); + const history = useHistory(); + const dispatch = useDispatch(); + + const [mode, setMode] = useState(''); + + const [tokenSelectorError, setTokenSelectorError] = useState(null); + const [selectedTokens, setSelectedTokens] = useState({}); + const [searchResults, setSearchResults] = useState([]); + + // Determine if we should show the search tab + const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported); + const isTokenDetectionInactiveOnMainnet = useSelector( + getIsTokenDetectionInactiveOnMainnet, + ); + const showSearchTab = + isTokenDetectionSupported || + isTokenDetectionInactiveOnMainnet || + Boolean(process.env.IN_TEST); + + const tokenList = useSelector(getTokenList); + const useTokenDetection = useSelector( + ({ metamask }) => metamask.useTokenDetection, + ); + const networkName = useSelector(getTokenDetectionSupportNetworkByChainId); + + // Custom token stuff + const tokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector( + getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, + ); + const isDynamicTokenListAvailable = useSelector( + getIsDynamicTokenListAvailable, + ); + const selectedAddress = useSelector(getSelectedAddress); + const isMainnet = useSelector(getIsMainnet); + const identities = useSelector(getMetaMaskIdentities); + const tokens = useSelector((state) => state.metamask.tokens); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + + const [customAddress, setCustomAddress] = useState(''); + const [customAddressError, setCustomAddressError] = useState(null); + const [nftAddressError, setNftAddressError] = useState(null); + const [symbolAutoFilled, setSymbolAutoFilled] = useState(false); + const [decimalAutoFilled, setDecimalAutoFilled] = useState(false); + const [mainnetTokenWarning, setMainnetTokenWarning] = useState(null); + const [customSymbol, setCustomSymbol] = useState(''); + const [customSymbolError, setCustomSymbolError] = useState(null); + const [customDecimals, setCustomDecimals] = useState(0); + const [customDecimalsError, setCustomDecimalsError] = useState(null); + const [tokenStandard, setTokenStandard] = useState(TokenStandard.none); + const [forceEditSymbol, setForceEditSymbol] = useState(false); + + const chainId = useSelector(getCurrentChainId); + const blockExplorerTokenLink = getTokenTrackerLink( + customAddress, + chainId, + null, + null, + { blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null }, + ); + const blockExplorerLabel = rpcPrefs?.blockExplorerUrl + ? getURLHostName(blockExplorerTokenLink) + : t('etherscan'); + + // Min and Max decimal values + const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000'; + const MIN_DECIMAL_VALUE = 0; + const MAX_DECIMAL_VALUE = 36; + + const infoGetter = useRef(tokenInfoGetter()); + + // CONFIRMATION MODE + const trackEvent = useContext(MetaMetricsContext); + const pendingTokens = useSelector(getPendingTokens); + + const handleAddTokens = useCallback(async () => { + const addedTokenValues = Object.values(pendingTokens); + await dispatch(addImportedTokens(addedTokenValues)); + + const firstTokenAddress = addedTokenValues?.[0].address?.toLowerCase(); + + addedTokenValues.forEach((pendingToken) => { + trackEvent({ + event: MetaMetricsEventName.TokenAdded, + category: MetaMetricsEventCategory.Wallet, + sensitiveProperties: { + token_symbol: pendingToken.symbol, + token_contract_address: pendingToken.address, + token_decimal_precision: pendingToken.decimals, + unlisted: pendingToken.unlisted, + source_connection_method: pendingToken.isCustom + ? MetaMetricsTokenEventSource.Custom + : MetaMetricsTokenEventSource.List, + token_standard: TokenStandard.ERC20, + asset_type: AssetType.token, + }, + }); + }); + + dispatch(clearPendingTokens()); + + if (firstTokenAddress) { + history.push(`${ASSET_ROUTE}/${firstTokenAddress}`); + } + }, [dispatch, history, pendingTokens, trackEvent]); + + useEffect(() => { + const pendingTokenKeys = Object.keys(pendingTokens); + + if (pendingTokenKeys.length === 0) { + return; + } + + let initialSelectedTokens = {}; + let initialCustomToken = {}; + + pendingTokenKeys.forEach((tokenAddress) => { + const token = pendingTokens[tokenAddress]; + const { isCustom } = token; + + if (isCustom) { + initialCustomToken = { ...token }; + } else { + initialSelectedTokens = { + ...selectedTokens, + [tokenAddress]: { ...token }, + }; + } + }); + + setSelectedTokens(initialSelectedTokens); + setCustomAddress(initialCustomToken.address); + setCustomSymbol(initialCustomToken.symbol); + setCustomDecimals(initialCustomToken.decimals); + }, [pendingTokens]); + + const handleCustomSymbolChange = (value) => { + const symbol = value.trim(); + const symbolLength = symbol.length; + let symbolError = null; + + if (symbolLength <= 0 || symbolLength >= 12) { + symbolError = t('symbolBetweenZeroTwelve'); + } + + setCustomSymbol(symbol); + setCustomSymbolError(symbolError); + }; + + const handleCustomDecimalsChange = (value) => { + let decimals; + let decimalsError = null; + + if (value) { + decimals = Number(value.trim()); + decimalsError = + value < MIN_DECIMAL_VALUE || value > MAX_DECIMAL_VALUE + ? t('decimalsMustZerotoTen') + : null; + } else { + decimals = ''; + decimalsError = t('tokenDecimalFetchFailed'); + } + + setCustomDecimals(decimals); + setCustomDecimalsError(decimalsError); + }; + + const attemptToAutoFillTokenParams = async (address) => { + const { symbol = '', decimals } = await infoGetter.current( + address, + tokenList, + ); + + setSymbolAutoFilled(Boolean(symbol)); + setDecimalAutoFilled(Boolean(decimals)); + + handleCustomSymbolChange(symbol || ''); + handleCustomDecimalsChange(decimals); + }; + + const handleToggleToken = (token) => { + const { address } = token; + const selectedTokensCopy = { ...selectedTokens }; + + if (address in selectedTokensCopy) { + delete selectedTokensCopy[address]; + } else { + selectedTokensCopy[address] = token; + } + + setSelectedTokens(selectedTokensCopy); + setTokenSelectorError(null); + }; + + const hasError = () => { + return ( + tokenSelectorError || + customAddressError || + customSymbolError || + customDecimalsError || + nftAddressError + ); + }; + + const hasSelected = () => { + return customAddress || Object.keys(selectedTokens).length > 0; + }; + + const handleNext = () => { + if (hasError()) { + return; + } + + if (!hasSelected()) { + setTokenSelectorError(t('mustSelectOne')); + return; + } + + const tokenAddressList = Object.keys(tokenList); + const customToken = customAddress + ? { + address: customAddress, + symbol: customSymbol, + decimals: customDecimals, + standard: tokenStandard, + } + : null; + + dispatch( + setPendingTokens({ customToken, selectedTokens, tokenAddressList }), + ); + setMode('confirm'); + }; + + const handleCustomAddressChange = async (value) => { + const address = value.trim(); + + setCustomAddress(address); + setCustomAddressError(null); + setNftAddressError(null); + setSymbolAutoFilled(false); + setDecimalAutoFilled(false); + setMainnetTokenWarning(null); + + const addressIsValid = isValidHexAddress(address, { + allowNonPrefixed: false, + }); + const standardAddress = addHexPrefix(address).toLowerCase(); + + const isMainnetToken = Object.keys(STATIC_MAINNET_TOKEN_LIST).some( + (key) => key.toLowerCase() === address.toLowerCase(), + ); + + let standard; + if (addressIsValid) { + try { + ({ standard } = await getTokenStandardAndDetails( + standardAddress, + selectedAddress, + null, + )); + } catch (error) { + // ignore + } + } + + const addressIsEmpty = address.length === 0 || address === EMPTY_ADDRESS; + + switch (true) { + case !addressIsValid && !addressIsEmpty: + setCustomAddressError(t('invalidAddress')); + setCustomSymbol(''); + setCustomDecimals(0); + setCustomSymbolError(null); + setCustomDecimalsError(null); + break; + + case standard === TokenStandard.ERC1155 || + standard === TokenStandard.ERC721: + setNftAddressError( + t('nftAddressError', [ + { + dispatch(showImportNftsModal()); + onClose(); + }} + color={TextColor.primaryDefault} + key="nftAddressError" + > + {t('importNFTPage')} + , + ]), + ); + break; + + case isMainnetToken && !isMainnet: + setMainnetTokenWarning(t('mainnetToken')); + setCustomSymbol(''); + setCustomDecimals(0); + setCustomSymbolError(null); + setCustomDecimalsError(null); + break; + + case Boolean(identities[standardAddress]): + setCustomAddressError(t('personalAddressDetected')); + break; + + case checkExistingAddresses(address, tokens): + setCustomAddressError(t('tokenAlreadyAdded')); + break; + + default: + if (!addressIsEmpty) { + attemptToAutoFillTokenParams(address); + if (standard) { + setTokenStandard(standard); + } + } + } + }; + + // Determines whether to show the Search/Import or Confirm action + const isConfirming = mode === 'confirm'; + + return ( + { + dispatch(clearPendingTokens()); + onClose(); + }} + className="import-tokens-modal" + > + + + setMode('') : null} + onClose={() => { + dispatch(clearPendingTokens()); + onClose(); + }} + > + {t('importTokensCamelCase')} + + + {isConfirming ? ( + { + dispatch(clearPendingTokens()); + setMode(''); + }} + onImportClick={async () => { + await handleAddTokens(); + onClose(); + }} + /> + ) : ( + <> + + {showSearchTab ? ( + + + {useTokenDetection ? null : ( + + + {t('enhancedTokenDetectionAlertMessage', [ + networkName, + { + history.push( + `${SECURITY_ROUTE}#advanced-settings-autodetect-tokens`, + ); + onClose(); + }} + > + {t('enableFromSettings')} + , + ])} + + + )} + + setSearchResults(results) + } + error={tokenSelectorError} + tokenList={tokenList} + /> + + handleToggleToken(token)} + /> + + + + ) : null} + + + {tokenDetectionInactiveOnNonMainnetSupportedNetwork ? ( + + {t( + 'customTokenWarningInTokenDetectionNetworkWithTDOFF', + [ + + {t('tokenScamSecurityRisk')} + , + + history.push( + `${SECURITY_ROUTE}#advanced-settings-autodetect-tokens`, + ) + } + > + {t('inYourSettings')} + , + ], + )} + + ) : ( + + {t( + isDynamicTokenListAvailable + ? 'customTokenWarningInTokenDetectionNetwork' + : 'customTokenWarningInNonTokenDetectionNetwork', + [ + + {t('learnScamRisk')} + , + ], + )} + + )} + + handleCustomAddressChange(e.target.value) + } + helpText={ + customAddressError || + mainnetTokenWarning || + nftAddressError + } + error={ + customAddressError || + mainnetTokenWarning || + nftAddressError + } + autoFocus + marginTop={6} + inputProps={{ + 'data-testid': 'import-tokens-modal-custom-address', + }} + /> + + {t('tokenSymbol')} + {symbolAutoFilled && !forceEditSymbol && ( + setForceEditSymbol(true)} + textAlign={TextAlign.End} + paddingInlineEnd={1} + paddingInlineStart={1} + color={TextColor.primaryDefault} + > + {t('edit')} + + )} + + } + value={customSymbol} + onChange={(e) => handleCustomSymbolChange(e.target.value)} + helpText={customSymbolError} + error={customSymbolError} + disabled={symbolAutoFilled && !forceEditSymbol} + marginTop={6} + inputProps={{ + 'data-testid': 'import-tokens-modal-custom-symbol', + }} + /> + + handleCustomDecimalsChange(e.target.value) + } + helpText={customDecimalsError} + error={customDecimalsError} + disabled={decimalAutoFilled} + min={MIN_DECIMAL_VALUE} + max={MAX_DECIMAL_VALUE} + marginTop={6} + inputProps={{ + 'data-testid': 'import-tokens-modal-custom-decimals', + }} + /> + {customDecimals === '' && ( + + + {t('tokenDecimalFetchFailed')} + + {t('verifyThisTokenDecimalOn', [ + + {blockExplorerLabel} + , + ])} + + )} + + + + + handleNext()} + size={Size.LG} + disabled={Boolean(hasError()) || !hasSelected()} + block + > + {t('next')} + + + + )} + + + + ); +}; + +ImportTokensModal.propTypes = { + onClose: PropTypes.func.isRequired, +}; diff --git a/ui/components/multichain/import-tokens-modal/import-tokens-modal.scss b/ui/components/multichain/import-tokens-modal/import-tokens-modal.scss new file mode 100644 index 000000000..f1240f786 --- /dev/null +++ b/ui/components/multichain/import-tokens-modal/import-tokens-modal.scss @@ -0,0 +1,67 @@ +.import-tokens-modal { + $self: &; + + .tabs { + ul { + border-bottom: 0; + } + + li { + width: 50%; + } + } + + &__autodetect { + vertical-align: unset; + } + + &__search-list { + max-width: 100%; + overflow: auto; + max-height: 200px; + } + + &__custom-token-form { + input[type='number']::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; + } + + input[type='number']:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; + } + & #{$self}__decimal-warning { + margin-top: 5px; + } + } + + &__token-name { + flex: 1; + } + + &__token-balance { + flex: 0 0 30%; + } + + &__confirm-token-list { + flex-flow: column nowrap; + + &-item { + flex-flow: row nowrap; + + &-wrapper { + flex-grow: 1; + + &__text { + max-width: 130px; + } + } + } + } + + &__nft-address-error-link { + display: contents; + font-size: inherit; + } +} diff --git a/ui/components/multichain/import-tokens-modal/import-tokens-modal.stories.js b/ui/components/multichain/import-tokens-modal/import-tokens-modal.stories.js new file mode 100644 index 000000000..04505d00b --- /dev/null +++ b/ui/components/multichain/import-tokens-modal/import-tokens-modal.stories.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { ImportTokensModal } from './import-tokens-modal'; + +const createStore = (chainId = CHAIN_IDS.MAINNET, useTokenDetection = true) => { + return configureStore({ + ...testData, + metamask: { + ...testData.metamask, + useTokenDetection, + providerConfig: { chainId }, + }, + }); +}; + +export default { + title: 'Components/Multichain/ImportTokensModal', + component: ImportTokensModal, + argTypes: { + onClose: { + action: 'onClose', + }, + }, +}; + +export const DefaultStory = (args) => ; +DefaultStory.decorators = [ + (Story) => ( + + + + ), +]; + +DefaultStory.storyName = 'Default'; + +export const CustomImportOnlyStory = (args) => ; +CustomImportOnlyStory.decorators = [ + (Story) => ( + + + + ), +]; + +CustomImportOnlyStory.storyName = 'Custom Import Only'; + +export const TokenDetectionDisabledStory = (args) => ( + +); +TokenDetectionDisabledStory.decorators = [ + (Story) => ( + + + + ), +]; + +TokenDetectionDisabledStory.storyName = 'Token Detection Disabled'; diff --git a/ui/components/multichain/import-tokens-modal/import-tokens-modal.test.js b/ui/components/multichain/import-tokens-modal/import-tokens-modal.test.js new file mode 100644 index 000000000..e61320662 --- /dev/null +++ b/ui/components/multichain/import-tokens-modal/import-tokens-modal.test.js @@ -0,0 +1,200 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; + +import configureStore from '../../../store/store'; +import { + setPendingTokens, + clearPendingTokens, + getTokenStandardAndDetails, +} from '../../../store/actions'; +import mockState from '../../../../test/data/mock-state.json'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { ImportTokensModal } from '.'; + +jest.mock('../../../store/actions', () => ({ + getTokenStandardAndDetails: jest + .fn() + .mockImplementation(() => Promise.resolve({ standard: 'ERC20' })), + setPendingTokens: jest + .fn() + .mockImplementation(() => ({ type: 'SET_PENDING_TOKENS' })), + clearPendingTokens: jest + .fn() + .mockImplementation(() => ({ type: 'CLEAR_PENDING_TOKENS' })), +})); + +describe('ImportTokensModal', () => { + const render = (metamaskStateChanges = {}, onClose = jest.fn()) => { + const store = configureStore({ + ...mockState, + metamask: { + ...mockState.metamask, + ...metamaskStateChanges, + }, + }); + return renderWithProvider(, store); + }; + + describe('Search', () => { + it('renders expected elements', () => { + const { getByText, getByPlaceholderText } = render(); + expect( + getByText(`Add the tokens you've acquired using MetaMask`), + ).toBeInTheDocument(); + expect(getByText('Next')).toBeDisabled(); + expect(getByPlaceholderText('Search')).toBeInTheDocument(); + }); + + it('shows the token detection notice when setting is off', () => { + const { getByText } = render({ useTokenDetection: false }); + expect(getByText('Enable it from Settings.')).toBeInTheDocument(); + }); + }); + + describe('Custom Token', () => { + it('add custom token button is disabled when no fields are populated', () => { + const { getByText } = render(); + const customTokenButton = getByText('Custom token'); + fireEvent.click(customTokenButton); + const submit = getByText('Next'); + + expect(submit).toBeDisabled(); + }); + + it('edits token address', () => { + const { getByText, getByTestId } = render(); + const customTokenButton = getByText('Custom token'); + fireEvent.click(customTokenButton); + + const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; + const event = { target: { value: tokenAddress } }; + fireEvent.change( + getByTestId('import-tokens-modal-custom-address'), + event, + ); + + expect( + getByTestId('import-tokens-modal-custom-address').value, + ).toStrictEqual(tokenAddress); + }); + + it('edits token symbol', () => { + const { getByText, getByTestId } = render(); + const customTokenButton = getByText('Custom token'); + fireEvent.click(customTokenButton); + + const tokenSymbol = 'META'; + const event = { target: { value: tokenSymbol } }; + fireEvent.change(getByTestId('import-tokens-modal-custom-symbol'), event); + + expect( + getByTestId('import-tokens-modal-custom-symbol').value, + ).toStrictEqual(tokenSymbol); + }); + + it('edits token decimal precision', () => { + const { getByText, getByTestId } = render(); + const customTokenButton = getByText('Custom token'); + fireEvent.click(customTokenButton); + + const tokenPrecision = '2'; + const event = { target: { value: tokenPrecision } }; + fireEvent.change( + getByTestId('import-tokens-modal-custom-decimals'), + event, + ); + + expect( + getByTestId('import-tokens-modal-custom-decimals').value, + ).toStrictEqual(tokenPrecision); + }); + + it('adds custom tokens successfully', async () => { + const { getByText, getByTestId } = render({ tokens: [], tokenList: {} }); + const customTokenButton = getByText('Custom token'); + fireEvent.click(customTokenButton); + + expect(getByText('Next')).toBeDisabled(); + + const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; + await fireEvent.change( + getByTestId('import-tokens-modal-custom-address'), + { + target: { value: tokenAddress }, + }, + ); + expect(getByText('Next')).not.toBeDisabled(); + + const tokenSymbol = 'META'; + + fireEvent.change(getByTestId('import-tokens-modal-custom-symbol'), { + target: { value: tokenSymbol }, + }); + + expect(getByTestId('import-tokens-modal-custom-symbol').value).toBe( + 'META', + ); + + const tokenPrecision = '2'; + + fireEvent.change(getByTestId('import-tokens-modal-custom-decimals'), { + target: { value: tokenPrecision }, + }); + + expect(getByText('Next')).not.toBeDisabled(); + + fireEvent.click(getByText('Next')); + + expect(setPendingTokens).toHaveBeenCalledWith({ + customToken: { + address: tokenAddress, + decimals: Number(tokenPrecision), + standard: TokenStandard.ERC20, + symbol: tokenSymbol, + }, + selectedTokens: {}, + tokenAddressList: [], + }); + + expect(getByText('Import')).toBeInTheDocument(); + }); + + it('cancels out of import token flow', () => { + const onClose = jest.fn(); + render({}, onClose); + + fireEvent.click(document.querySelector('button[aria-label="Close"]')); + + expect(clearPendingTokens).toHaveBeenCalled(); + expect(onClose).toHaveBeenCalled(); + }); + + it('sets and error when a token is an NFT', async () => { + getTokenStandardAndDetails.mockImplementation(() => + Promise.resolve({ standard: TokenStandard.ERC721 }), + ); + + const { getByText, getByTestId } = render(); + const customTokenButton = getByText('Custom token'); + fireEvent.click(customTokenButton); + + const submit = getByText('Next'); + expect(submit).toBeDisabled(); + + const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; + await fireEvent.change( + getByTestId('import-tokens-modal-custom-address'), + { + target: { value: tokenAddress }, + }, + ); + + expect(submit).toBeDisabled(); + + // The last part of this error message won't be found by getByText because it is wrapped as a link. + const errorMessage = getByText('This token is an NFT. Add on the'); + expect(errorMessage).toBeInTheDocument(); + }); + }); +}); diff --git a/ui/components/multichain/import-tokens-modal/index.js b/ui/components/multichain/import-tokens-modal/index.js new file mode 100644 index 000000000..ecaf2735e --- /dev/null +++ b/ui/components/multichain/import-tokens-modal/index.js @@ -0,0 +1 @@ +export { ImportTokensModal } from './import-tokens-modal'; diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js index 95a9b191b..e1e3359fc 100644 --- a/ui/components/multichain/index.js +++ b/ui/components/multichain/index.js @@ -18,3 +18,4 @@ export { CreateAccount } from './create-account'; export { ImportAccount } from './import-account'; export { ImportNftsModal } from './import-nfts-modal'; export { AccountDetailsMenuItem, ViewExplorerMenuItem } from './menu-items'; +export { ImportTokensModal } from './import-tokens-modal'; diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss index c67217a0f..7f6282a35 100644 --- a/ui/components/multichain/multichain-components.scss +++ b/ui/components/multichain/multichain-components.scss @@ -19,3 +19,4 @@ @import 'network-list-menu/'; @import 'product-tour-popover/product-tour-popover'; @import 'nft-item/nft-item'; +@import 'import-tokens-modal/import-tokens-modal' diff --git a/ui/components/ui/page-container/page-container.component.js b/ui/components/ui/page-container/page-container.component.js index d360bbc1b..315a557e6 100644 --- a/ui/components/ui/page-container/page-container.component.js +++ b/ui/components/ui/page-container/page-container.component.js @@ -88,19 +88,6 @@ export default class PageContainer extends PureComponent { return null; } - getTabSubmitText() { - const { tabsComponent } = this.props; - const { activeTabIndex } = this.state; - if (tabsComponent) { - let { children } = tabsComponent.props; - children = children.filter(Boolean); - if (children[activeTabIndex]?.key === 'custom-tab') { - return this.context.t('addCustomToken'); - } - } - return null; - } - render() { const { title, @@ -118,7 +105,6 @@ export default class PageContainer extends PureComponent { headerCloseText, hideCancel, } = this.props; - const tabSubmitText = this.getTabSubmitText(); return (
diff --git a/ui/components/ui/popover/index.scss b/ui/components/ui/popover/index.scss index 2b53cf892..409758812 100644 --- a/ui/components/ui/popover/index.scss +++ b/ui/components/ui/popover/index.scss @@ -90,4 +90,8 @@ transform: rotate(45deg); box-shadow: var(--shadow-size-lg) var(--color-shadow-default); } + + &-container .page-container { + width: auto; + } } diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index e34edcf2d..0ad619c0a 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -28,6 +28,7 @@ interface AppState { networkDropdownOpen: boolean; importNftsModalOpen: boolean; showIpfsModalOpen: boolean; + importTokensModalOpen: boolean; accountDetail: { subview?: string; accountExport?: string; @@ -98,6 +99,7 @@ const initialState: AppState = { networkDropdownOpen: false, importNftsModalOpen: false, showIpfsModalOpen: false, + importTokensModalOpen: false, accountDetail: { privateKey: '', }, @@ -191,6 +193,18 @@ export default function reduceApp( showIpfsModalOpen: false, }; + case actionConstants.IMPORT_TOKENS_POPOVER_OPEN: + return { + ...appState, + importTokensModalOpen: true, + }; + + case actionConstants.IMPORT_TOKENS_POPOVER_CLOSE: + return { + ...appState, + importTokensModalOpen: false, + }; + // alert methods case actionConstants.ALERT_OPEN: return { diff --git a/ui/pages/confirm-import-token/confirm-import-token.js b/ui/pages/confirm-import-token/confirm-import-token.js deleted file mode 100644 index e45ade9da..000000000 --- a/ui/pages/confirm-import-token/confirm-import-token.js +++ /dev/null @@ -1,150 +0,0 @@ -import React, { useCallback, useContext, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { - ASSET_ROUTE, - IMPORT_TOKEN_ROUTE, -} from '../../helpers/constants/routes'; -import Button from '../../components/ui/button'; -import Identicon from '../../components/ui/identicon'; -import TokenBalance from '../../components/ui/token-balance'; -import { I18nContext } from '../../contexts/i18n'; -import { MetaMetricsContext } from '../../contexts/metametrics'; -import { getMostRecentOverviewPage } from '../../ducks/history/history'; -import { getPendingTokens } from '../../ducks/metamask/metamask'; -import { addImportedTokens, clearPendingTokens } from '../../store/actions'; -import { - MetaMetricsEventCategory, - MetaMetricsEventName, - MetaMetricsTokenEventSource, -} from '../../../shared/constants/metametrics'; -import { - AssetType, - TokenStandard, -} from '../../../shared/constants/transaction'; - -const getTokenName = (name, symbol) => { - return name === undefined ? symbol : `${name} (${symbol})`; -}; - -const ConfirmImportToken = () => { - const t = useContext(I18nContext); - const dispatch = useDispatch(); - const history = useHistory(); - const trackEvent = useContext(MetaMetricsContext); - - const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage); - const pendingTokens = useSelector(getPendingTokens); - - const handleAddTokens = useCallback(async () => { - const addedTokenValues = Object.values(pendingTokens); - await dispatch(addImportedTokens(addedTokenValues)); - - const firstTokenAddress = addedTokenValues?.[0].address?.toLowerCase(); - - addedTokenValues.forEach((pendingToken) => { - trackEvent({ - event: MetaMetricsEventName.TokenAdded, - category: MetaMetricsEventCategory.Wallet, - sensitiveProperties: { - token_symbol: pendingToken.symbol, - token_contract_address: pendingToken.address, - token_decimal_precision: pendingToken.decimals, - unlisted: pendingToken.unlisted, - source: pendingToken.isCustom - ? MetaMetricsTokenEventSource.Custom - : MetaMetricsTokenEventSource.List, - token_standard: TokenStandard.ERC20, - asset_type: AssetType.token, - }, - }); - }); - - dispatch(clearPendingTokens()); - - if (firstTokenAddress) { - history.push(`${ASSET_ROUTE}/${firstTokenAddress}`); - } else { - history.push(mostRecentOverviewPage); - } - }, [dispatch, history, mostRecentOverviewPage, pendingTokens, trackEvent]); - - useEffect(() => { - if (Object.keys(pendingTokens).length === 0) { - history.push(mostRecentOverviewPage); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( -
-
-
- {t('importTokensCamelCase')} -
-
- {t('likeToImportTokens')} -
-
-
-
-
-
{t('token')}
-
{t('balance')}
-
-
- {Object.entries(pendingTokens).map(([address, token]) => { - const { name, symbol } = token; - - return ( -
-
- -
- {getTokenName(name, symbol)} -
-
-
- -
-
- ); - })} -
-
-
-
-
- - -
-
-
- ); -}; - -export default ConfirmImportToken; diff --git a/ui/pages/confirm-import-token/confirm-import-token.stories.js b/ui/pages/confirm-import-token/confirm-import-token.stories.js deleted file mode 100644 index c9bdb091b..000000000 --- a/ui/pages/confirm-import-token/confirm-import-token.stories.js +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { useEffect } from 'react'; - -import { store, getNewState } from '../../../.storybook/preview'; -import { tokens } from '../../../.storybook/initial-states/approval-screens/add-token'; -import { updateMetamaskState } from '../../store/actions'; -import ConfirmAddToken from '.'; - -export default { - title: 'Pages/ConfirmImportToken', - - argTypes: { - pendingTokens: { - control: 'object', - table: { category: 'Data' }, - }, - }, -}; - -const PageSet = ({ children, pendingTokens }) => { - const { metamask: state } = store.getState(); - - useEffect(() => { - store.dispatch( - updateMetamaskState( - getNewState(state, { - pendingTokens, - }), - ), - ); - }, [state, pendingTokens]); - - return children; -}; - -export const DefaultStory = ({ pendingTokens }) => { - return ( - - - - ); -}; -DefaultStory.args = { - pendingTokens: { ...tokens }, -}; -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/confirm-import-token/confirm-import-token.test.js b/ui/pages/confirm-import-token/confirm-import-token.test.js deleted file mode 100644 index c756e8983..000000000 --- a/ui/pages/confirm-import-token/confirm-import-token.test.js +++ /dev/null @@ -1,128 +0,0 @@ -import React from 'react'; -import reactRouterDom from 'react-router-dom'; -import { fireEvent, screen } from '@testing-library/react'; -import { - ASSET_ROUTE, - IMPORT_TOKEN_ROUTE, -} from '../../helpers/constants/routes'; -import { addImportedTokens, clearPendingTokens } from '../../store/actions'; -import configureStore from '../../store/store'; -import { renderWithProvider } from '../../../test/jest'; -import ConfirmImportToken from '.'; - -const MOCK_PENDING_TOKENS = { - '0x6b175474e89094c44da98b954eedeac495271d0f': { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', - symbol: 'META', - decimals: 18, - image: 'metamark.svg', - }, - '0xB8c77482e45F1F44dE1745F52C74426C631bDD52': { - address: '0xB8c77482e45F1F44dE1745F52C74426C631bDD52', - symbol: '0X', - decimals: 18, - image: '0x.svg', - }, -}; - -jest.mock('../../store/actions', () => ({ - addImportedTokens: jest.fn().mockReturnValue({ type: 'test' }), - clearPendingTokens: jest - .fn() - .mockReturnValue({ type: 'CLEAR_PENDING_TOKENS' }), -})); - -const renderComponent = (mockPendingTokens = MOCK_PENDING_TOKENS) => { - const store = configureStore({ - metamask: { - pendingTokens: { ...mockPendingTokens }, - providerConfig: { chainId: '0x1' }, - }, - history: { - mostRecentOverviewPage: '/', - }, - }); - - return renderWithProvider(, store); -}; - -describe('ConfirmImportToken Component', () => { - const mockHistoryPush = jest.fn(); - - beforeEach(() => { - jest - .spyOn(reactRouterDom, 'useHistory') - .mockImplementation() - .mockReturnValue({ push: mockHistoryPush }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should render', () => { - renderComponent(); - - const [title, importTokensBtn] = screen.queryAllByText('Import tokens'); - - expect(title).toBeInTheDocument(title); - expect( - screen.getByText('Would you like to import these tokens?'), - ).toBeInTheDocument(); - expect(screen.getByText('Token')).toBeInTheDocument(); - expect(screen.getByText('Balance')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument(); - expect(importTokensBtn).toBeInTheDocument(); - }); - - it('should render the list of tokens', () => { - renderComponent(); - - Object.values(MOCK_PENDING_TOKENS).forEach((token) => { - expect(screen.getByText(token.symbol)).toBeInTheDocument(); - }); - }); - - it('should go to "IMPORT_TOKEN_ROUTE" route when clicking the "Back" button', async () => { - renderComponent(); - - const backBtn = screen.getByRole('button', { name: 'Back' }); - - await fireEvent.click(backBtn); - expect(mockHistoryPush).toHaveBeenCalledTimes(1); - expect(mockHistoryPush).toHaveBeenCalledWith(IMPORT_TOKEN_ROUTE); - }); - - it('should dispatch clearPendingTokens and redirect to the first token page when clicking the "Import tokens" button', async () => { - const mockFirstPendingTokenAddress = - '0xe83cccfabd4ed148903bf36d4283ee7c8b3494d1'; - const mockPendingTokens = { - [mockFirstPendingTokenAddress]: { - address: mockFirstPendingTokenAddress, - symbol: 'CVL', - decimals: 18, - image: 'CVL_token.svg', - }, - '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': { - address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', - symbol: 'GLA', - decimals: 18, - image: 'gladius.svg', - }, - }; - renderComponent(mockPendingTokens); - - const importTokensBtn = screen.getByRole('button', { - name: 'Import tokens', - }); - - await fireEvent.click(importTokensBtn); - - expect(addImportedTokens).toHaveBeenCalled(); - expect(clearPendingTokens).toHaveBeenCalled(); - expect(mockHistoryPush).toHaveBeenCalledTimes(1); - expect(mockHistoryPush).toHaveBeenCalledWith( - `${ASSET_ROUTE}/${mockFirstPendingTokenAddress}`, - ); - }); -}); diff --git a/ui/pages/confirm-import-token/index.js b/ui/pages/confirm-import-token/index.js deleted file mode 100644 index 4443efa6b..000000000 --- a/ui/pages/confirm-import-token/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import ConfirmImportToken from './confirm-import-token'; - -export default ConfirmImportToken; diff --git a/ui/pages/confirm-import-token/index.scss b/ui/pages/confirm-import-token/index.scss deleted file mode 100644 index 4dbc659ec..000000000 --- a/ui/pages/confirm-import-token/index.scss +++ /dev/null @@ -1,50 +0,0 @@ -.confirm-import-token { - padding: 16px; - - &__header { - @include H7; - - display: flex; - } - - &__token { - flex: 1; - min-width: 0; - } - - &__balance { - flex: 0 0 30%; - min-width: 0; - } - - &__token-list { - display: flex; - flex-flow: column nowrap; - } - - &__token-list-item { - display: flex; - flex-flow: row nowrap; - align-items: center; - margin-top: 8px; - box-sizing: border-box; - } - - &__data { - display: flex; - align-items: center; - padding: 8px; - } - - &__name { - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__token-icon { - margin-right: 12px; - flex: 0 0 auto; - } -} diff --git a/ui/pages/import-token/README.mdx b/ui/pages/import-token/README.mdx deleted file mode 100644 index 040a0e925..000000000 --- a/ui/pages/import-token/README.mdx +++ /dev/null @@ -1,31 +0,0 @@ -import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; - -import ImportToken from './import-token.component'; - -import testData from '../../../.storybook/test-data'; -import configureStore from '../../store/store'; - -export const PersonalAddress = () => {configureStore(testData).getState().metamask.selectedAddress}; - -# ImportToken - -The `ImportToken` component allows a user to import custom tokens in one of two ways: - -1. By searching for one -2. By importing one by `Token Contract Address` - - - - - -## Example inputs - -An example input that works, to enable the `Add custom token` button is `0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`. - -### Personal address error - -To show the personal address detected error, input the address in the `Token Contract Address` field. - -## Props - - diff --git a/ui/pages/import-token/import-token.component.js b/ui/pages/import-token/import-token.component.js deleted file mode 100644 index 20a00d1aa..000000000 --- a/ui/pages/import-token/import-token.component.js +++ /dev/null @@ -1,659 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { getTokenTrackerLink } from '@metamask/etherscan-link'; -import ZENDESK_URLS from '../../helpers/constants/zendesk-url'; -import { - checkExistingAddresses, - getURLHostName, -} from '../../helpers/utils/util'; -import { tokenInfoGetter } from '../../helpers/utils/token-util'; -import { - CONFIRM_IMPORT_TOKEN_ROUTE, - SECURITY_ROUTE, -} from '../../helpers/constants/routes'; -import TextField from '../../components/ui/text-field'; -import PageContainer from '../../components/ui/page-container'; -import { Tabs, Tab } from '../../components/ui/tabs'; -import { addHexPrefix } from '../../../app/scripts/lib/util'; -import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; -import ActionableMessage from '../../components/ui/actionable-message/actionable-message'; -import Typography from '../../components/ui/typography'; -import { - TypographyVariant, - FONT_WEIGHT, -} from '../../helpers/constants/design-system'; -import Button from '../../components/ui/button'; -import { TokenStandard } from '../../../shared/constants/transaction'; -import { STATIC_MAINNET_TOKEN_LIST } from '../../../shared/constants/tokens'; -import TokenSearch from './token-search'; -import TokenList from './token-list'; - -const emptyAddr = '0x0000000000000000000000000000000000000000'; - -const MIN_DECIMAL_VALUE = 0; -const MAX_DECIMAL_VALUE = 36; - -class ImportToken extends Component { - static contextTypes = { - t: PropTypes.func, - }; - - static propTypes = { - /** - * History object of the router. - */ - history: PropTypes.object, - - /** - * Set the state of `pendingTokens`, called when adding a token. - */ - setPendingTokens: PropTypes.func, - - /** - * The current list of pending tokens to be added. - */ - pendingTokens: PropTypes.object, - - /** - * Clear the list of pending tokens. Called when closing the modal. - */ - clearPendingTokens: PropTypes.func, - - /** - * Clear the list of pending tokens. Called when closing the modal. - */ - showImportNftsModal: PropTypes.func, - - /** - * The list of already added tokens. - */ - tokens: PropTypes.array, - - /** - * The identities/accounts that are currently added to the wallet. - */ - identities: PropTypes.object, - - /** - * Boolean flag that shows/hides the search tab. - */ - showSearchTab: PropTypes.bool.isRequired, - - /** - * The most recent overview page route, which is 'navigated' to when closing the modal. - */ - mostRecentOverviewPage: PropTypes.string.isRequired, - - /** - * The active chainId in use. - */ - chainId: PropTypes.string, - - /** - * The rpc preferences to use for the current provider. - */ - rpcPrefs: PropTypes.object, - - /** - * The list of tokens available for search. - */ - tokenList: PropTypes.object, - - /** - * Boolean flag indicating whether token detection is enabled or not. - * When disabled, shows an information alert in the search tab informing the - * user of the availability of this feature. - */ - useTokenDetection: PropTypes.bool, - - /** - * Function called to fetch information about the token standard and - * details, see `actions.js`. - */ - getTokenStandardAndDetails: PropTypes.func, - - /** - * The currently selected active address. - */ - selectedAddress: PropTypes.string, - isDynamicTokenListAvailable: PropTypes.bool.isRequired, - tokenDetectionInactiveOnNonMainnetSupportedNetwork: - PropTypes.bool.isRequired, - networkName: PropTypes.string.isRequired, - }; - - static defaultProps = { - tokenList: {}, - }; - - state = { - customAddress: '', - customSymbol: '', - customDecimals: 0, - searchResults: [], - selectedTokens: {}, - standard: TokenStandard.NONE, - tokenSelectorError: null, - customAddressError: null, - customSymbolError: null, - customDecimalsError: null, - nftAddressError: null, - forceEditSymbol: false, - symbolAutoFilled: false, - decimalAutoFilled: false, - mainnetTokenWarning: null, - }; - - componentDidMount() { - this.tokenInfoGetter = tokenInfoGetter(); - const { pendingTokens = {} } = this.props; - const pendingTokenKeys = Object.keys(pendingTokens); - - if (pendingTokenKeys.length > 0) { - let selectedTokens = {}; - let customToken = {}; - - pendingTokenKeys.forEach((tokenAddress) => { - const token = pendingTokens[tokenAddress]; - const { isCustom } = token; - - if (isCustom) { - customToken = { ...token }; - } else { - selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } }; - } - }); - - const { - address: customAddress = '', - symbol: customSymbol = '', - decimals: customDecimals = 0, - } = customToken; - - this.setState({ - selectedTokens, - customAddress, - customSymbol, - customDecimals, - }); - } - } - - handleToggleToken(token) { - const { address } = token; - const { selectedTokens = {} } = this.state; - const selectedTokensCopy = { ...selectedTokens }; - - if (address in selectedTokensCopy) { - delete selectedTokensCopy[address]; - } else { - selectedTokensCopy[address] = token; - } - - this.setState({ - selectedTokens: selectedTokensCopy, - tokenSelectorError: null, - }); - } - - hasError() { - const { - tokenSelectorError, - customAddressError, - customSymbolError, - customDecimalsError, - nftAddressError, - } = this.state; - - return ( - tokenSelectorError || - customAddressError || - customSymbolError || - customDecimalsError || - nftAddressError - ); - } - - hasSelected() { - const { customAddress = '', selectedTokens = {} } = this.state; - return customAddress || Object.keys(selectedTokens).length > 0; - } - - handleNext() { - if (this.hasError()) { - return; - } - - if (!this.hasSelected()) { - this.setState({ tokenSelectorError: this.context.t('mustSelectOne') }); - return; - } - - const { setPendingTokens, history, tokenList } = this.props; - const tokenAddressList = Object.keys(tokenList); - const { - customAddress: address, - customSymbol: symbol, - customDecimals: decimals, - selectedTokens, - standard, - } = this.state; - - const customToken = { - address, - symbol, - decimals, - standard, - }; - - setPendingTokens({ customToken, selectedTokens, tokenAddressList }); - history.push(CONFIRM_IMPORT_TOKEN_ROUTE); - } - - async attemptToAutoFillTokenParams(address) { - const { tokenList } = this.props; - const { symbol = '', decimals } = await this.tokenInfoGetter( - address, - tokenList, - ); - - const symbolAutoFilled = Boolean(symbol); - const decimalAutoFilled = Boolean(decimals); - this.setState({ symbolAutoFilled, decimalAutoFilled }); - this.handleCustomSymbolChange(symbol || ''); - this.handleCustomDecimalsChange(decimals); - } - - async handleCustomAddressChange(value) { - const customAddress = value.trim(); - this.setState({ - customAddress, - customAddressError: null, - nftAddressError: null, - tokenSelectorError: null, - symbolAutoFilled: false, - decimalAutoFilled: false, - mainnetTokenWarning: null, - }); - - const addressIsValid = isValidHexAddress(customAddress, { - allowNonPrefixed: false, - }); - const standardAddress = addHexPrefix(customAddress).toLowerCase(); - - const isMainnetToken = Object.keys(STATIC_MAINNET_TOKEN_LIST).some( - (key) => key.toLowerCase() === customAddress.toLowerCase(), - ); - - const isMainnetNetwork = this.props.chainId === '0x1'; - - let standard; - if (addressIsValid) { - try { - ({ standard } = await this.props.getTokenStandardAndDetails( - standardAddress, - this.props.selectedAddress, - )); - } catch (error) { - // ignore - } - } - - const addressIsEmpty = - customAddress.length === 0 || customAddress === emptyAddr; - - switch (true) { - case !addressIsValid && !addressIsEmpty: - this.setState({ - customAddressError: this.context.t('invalidAddress'), - customSymbol: '', - customDecimals: 0, - customSymbolError: null, - customDecimalsError: null, - }); - - break; - case standard === 'ERC1155' || standard === 'ERC721': - this.setState({ - nftAddressError: this.context.t('nftAddressError', [ - { - this.props.showImportNftsModal(); - }} - key="nftAddressError" - > - {this.context.t('importNFTPage')} - , - ]), - }); - break; - case isMainnetToken && !isMainnetNetwork: - this.setState({ - mainnetTokenWarning: this.context.t('mainnetToken'), - customSymbol: '', - customDecimals: 0, - customSymbolError: null, - customDecimalsError: null, - }); - - break; - case Boolean(this.props.identities[standardAddress]): - this.setState({ - customAddressError: this.context.t('personalAddressDetected'), - }); - - break; - case checkExistingAddresses(customAddress, this.props.tokens): - this.setState({ - customAddressError: this.context.t('tokenAlreadyAdded'), - }); - - break; - default: - if (!addressIsEmpty) { - this.attemptToAutoFillTokenParams(customAddress); - if (standard) { - this.setState({ standard }); - } - } - } - } - - handleCustomSymbolChange(value) { - const customSymbol = value.trim(); - const symbolLength = customSymbol.length; - let customSymbolError = null; - - if (symbolLength <= 0 || symbolLength >= 12) { - customSymbolError = this.context.t('symbolBetweenZeroTwelve'); - } - - this.setState({ customSymbol, customSymbolError }); - } - - handleCustomDecimalsChange(value) { - let customDecimals; - let customDecimalsError = null; - - if (value) { - customDecimals = Number(value.trim()); - customDecimalsError = - value < MIN_DECIMAL_VALUE || value > MAX_DECIMAL_VALUE - ? this.context.t('decimalsMustZerotoTen') - : null; - } else { - customDecimals = ''; - customDecimalsError = this.context.t('tokenDecimalFetchFailed'); - } - - this.setState({ customDecimals, customDecimalsError }); - } - - renderCustomTokenForm() { - const { t } = this.context; - const { - customAddress, - customSymbol, - customDecimals, - customAddressError, - customSymbolError, - customDecimalsError, - forceEditSymbol, - symbolAutoFilled, - decimalAutoFilled, - mainnetTokenWarning, - nftAddressError, - } = this.state; - - const { - chainId, - rpcPrefs, - isDynamicTokenListAvailable, - tokenDetectionInactiveOnNonMainnetSupportedNetwork, - history, - } = this.props; - const blockExplorerTokenLink = getTokenTrackerLink( - customAddress, - chainId, - null, - null, - { blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null }, - ); - const blockExplorerLabel = rpcPrefs?.blockExplorerUrl - ? getURLHostName(blockExplorerTokenLink) - : t('etherscan'); - - return ( -
- {tokenDetectionInactiveOnNonMainnetSupportedNetwork ? ( - - {t('tokenScamSecurityRisk')} - , - , - ])} - withRightButton - useIcon - iconFillColor="var(--color-warning-default)" - /> - ) : ( - - {t('learnScamRisk')} - , - ], - )} - withRightButton - useIcon - iconFillColor={ - isDynamicTokenListAvailable - ? 'var(--color-warning-default)' - : 'var(--color-info-default)' - } - /> - )} - this.handleCustomAddressChange(e.target.value)} - error={customAddressError || mainnetTokenWarning || nftAddressError} - fullWidth - autoFocus - margin="normal" - /> - - - {t('tokenSymbol')} - - {symbolAutoFilled && !forceEditSymbol && ( -
this.setState({ forceEditSymbol: true })} - > - {t('edit')} -
- )} -
- } - type="text" - value={customSymbol} - onChange={(e) => this.handleCustomSymbolChange(e.target.value)} - error={customSymbolError} - fullWidth - margin="normal" - disabled={symbolAutoFilled && !forceEditSymbol} - /> - this.handleCustomDecimalsChange(e.target.value)} - error={customDecimals ? customDecimalsError : null} - fullWidth - margin="normal" - disabled={decimalAutoFilled} - min={MIN_DECIMAL_VALUE} - max={MAX_DECIMAL_VALUE} - /> - {customDecimals === '' && ( - - - {t('tokenDecimalFetchFailed')} - - - {t('verifyThisTokenDecimalOn', [ - , - ])} - - - } - type="warning" - withRightButton - className="import-token__decimal-warning" - /> - )} - - ); - } - - renderSearchToken() { - const { t } = this.context; - const { tokenList, history, useTokenDetection, networkName } = this.props; - const { tokenSelectorError, selectedTokens, searchResults } = this.state; - return ( -
- {!useTokenDetection && ( - - history.push(`${SECURITY_ROUTE}#token-description`) - } - > - {t('enableFromSettings')} - , - ])} - withRightButton - useIcon - iconFillColor="var(--color-primary-default)" - className="import-token__token-detection-announcement" - /> - )} - - this.setState({ searchResults: results }) - } - error={tokenSelectorError} - tokenList={tokenList} - /> -
- this.handleToggleToken(token)} - /> -
-
- ); - } - - renderTabs() { - const { t } = this.context; - const { showSearchTab } = this.props; - const tabs = []; - - if (showSearchTab) { - tabs.push( - - {this.renderSearchToken()} - , - ); - } - tabs.push( - - {this.renderCustomTokenForm()} - , - ); - - return {tabs}; - } - - render() { - const { history, clearPendingTokens, mostRecentOverviewPage } = this.props; - - return ( - this.handleNext()} - hideCancel - disabled={Boolean(this.hasError()) || !this.hasSelected()} - onClose={() => { - clearPendingTokens(); - history.push(mostRecentOverviewPage); - }} - /> - ); - } -} - -export default ImportToken; diff --git a/ui/pages/import-token/import-token.container.js b/ui/pages/import-token/import-token.container.js deleted file mode 100644 index 9d238d646..000000000 --- a/ui/pages/import-token/import-token.container.js +++ /dev/null @@ -1,68 +0,0 @@ -import { connect } from 'react-redux'; - -import { - setPendingTokens, - clearPendingTokens, - getTokenStandardAndDetails, - showImportNftsModal, -} from '../../store/actions'; -import { getMostRecentOverviewPage } from '../../ducks/history/history'; -import { getProviderConfig } from '../../ducks/metamask/metamask'; -import { - getRpcPrefsForCurrentProvider, - getIsTokenDetectionSupported, - getTokenDetectionSupportNetworkByChainId, - getIsTokenDetectionInactiveOnMainnet, - getIsDynamicTokenListAvailable, - getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, - getTokenList, -} from '../../selectors/selectors'; -import ImportToken from './import-token.component'; - -const mapStateToProps = (state) => { - const { - metamask: { - identities, - tokens, - pendingTokens, - useTokenDetection, - selectedAddress, - }, - } = state; - const { chainId } = getProviderConfig(state); - - const isTokenDetectionInactiveOnMainnet = - getIsTokenDetectionInactiveOnMainnet(state); - const showSearchTab = - getIsTokenDetectionSupported(state) || - isTokenDetectionInactiveOnMainnet || - Boolean(process.env.IN_TEST); - - return { - identities, - mostRecentOverviewPage: getMostRecentOverviewPage(state), - tokens, - pendingTokens, - showSearchTab, - chainId, - rpcPrefs: getRpcPrefsForCurrentProvider(state), - tokenList: getTokenList(state), - useTokenDetection, - selectedAddress, - isDynamicTokenListAvailable: getIsDynamicTokenListAvailable(state), - networkName: getTokenDetectionSupportNetworkByChainId(state), - tokenDetectionInactiveOnNonMainnetSupportedNetwork: - getIstokenDetectionInactiveOnNonMainnetSupportedNetwork(state), - }; -}; -const mapDispatchToProps = (dispatch) => { - return { - setPendingTokens: (tokens) => dispatch(setPendingTokens(tokens)), - clearPendingTokens: () => dispatch(clearPendingTokens()), - showImportNftsModal: () => dispatch(showImportNftsModal()), - getTokenStandardAndDetails: (address, selectedAddress) => - getTokenStandardAndDetails(address, selectedAddress, null), - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ImportToken); diff --git a/ui/pages/import-token/import-token.stories.js b/ui/pages/import-token/import-token.stories.js deleted file mode 100644 index b5368a430..000000000 --- a/ui/pages/import-token/import-token.stories.js +++ /dev/null @@ -1,120 +0,0 @@ -import React from 'react'; - -import { Provider } from 'react-redux'; -import { action } from '@storybook/addon-actions'; -import { DEFAULT_ROUTE } from '../../helpers/constants/routes'; -import configureStore from '../../store/store'; -import testData from '../../../.storybook/test-data'; -import ImportToken from './import-token.component'; -import README from './README.mdx'; - -const store = configureStore(testData); -const { metamask } = store.getState(); - -const { - networkConfigurations, - identities, - pendingTokens, - selectedAddress, - tokenList, - tokens, -} = metamask; - -export default { - title: 'Pages/ImportToken', - - decorators: [(story) => {story()}], - component: ImportToken, - parameters: { - docs: { - page: README, - }, - }, - argTypes: { - history: { - control: { - type: 'object', - }, - }, - setPendingTokens: { - action: 'setPendingTokens', - }, - pendingTokens: { - control: { - type: 'object', - }, - }, - clearPendingTokens: { - action: 'clearPendingTokens', - }, - tokens: { - control: { - type: 'object', - }, - }, - identities: { - control: { - type: 'object', - }, - }, - showSearchTab: { - control: { - type: 'boolean', - }, - }, - mostRecentOverviewPage: { - control: { - type: 'text', - }, - }, - chainId: { - control: { - type: 'text', - }, - }, - rpcPrefs: { - control: { - type: 'object', - }, - }, - tokenList: { - control: { - type: 'object', - }, - }, - useTokenDetection: { - control: { - type: 'boolean', - }, - }, - getTokenStandardAndDetails: { - action: 'getTokenStandardAndDetails', - }, - selectedAddress: { - control: { - type: 'text', - }, - }, - }, - args: { - history: { - push: action('history.push()'), - }, - pendingTokens, - tokens, - identities, - showSearchTab: true, - mostRecentOverviewPage: DEFAULT_ROUTE, - chainId: networkConfigurations['test-networkConfigurationId-1'].chainId, - rpcPrefs: networkConfigurations['test-networkConfigurationId-1'].rpcPrefs, - tokenList, - useTokenDetection: false, - selectedAddress, - }, -}; - -export const DefaultStory = (args) => { - return ; -}; - -DefaultStory.storyName = 'Default'; diff --git a/ui/pages/import-token/import-token.test.js b/ui/pages/import-token/import-token.test.js deleted file mode 100644 index b2fb6b5a2..000000000 --- a/ui/pages/import-token/import-token.test.js +++ /dev/null @@ -1,178 +0,0 @@ -import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { renderWithProvider } from '../../../test/lib/render-helpers'; -import configureStore from '../../store/store'; -import { - setPendingTokens, - clearPendingTokens, - getTokenStandardAndDetails, -} from '../../store/actions'; -import ImportToken from './import-token.container'; - -jest.mock('../../store/actions', () => ({ - getTokenStandardAndDetails: jest - .fn() - .mockImplementation(() => Promise.resolve({ standard: 'ERC20' })), - setPendingTokens: jest - .fn() - .mockImplementation(() => ({ type: 'SET_PENDING_TOKENS' })), - clearPendingTokens: jest - .fn() - .mockImplementation(() => ({ type: 'CLEAR_PENDING_TOKENS' })), -})); - -describe('Import Token', () => { - const historyStub = jest.fn(); - const props = { - history: { - push: historyStub, - }, - showSearchTab: true, - tokenList: {}, - }; - - const render = () => { - const baseStore = { - metamask: { - tokens: [], - providerConfig: { chainId: '0x1' }, - networkConfigurations: {}, - identities: {}, - selectedAddress: '0x1231231', - useTokenDetection: true, - }, - history: { - mostRecentOverviewPage: '/', - }, - }; - - const store = configureStore(baseStore); - - return renderWithProvider(, store); - }; - - describe('Import Token', () => { - it('add custom token button is disabled when no fields are populated', () => { - const { getByText } = render(); - const customTokenButton = getByText('Custom token'); - fireEvent.click(customTokenButton); - const submit = getByText('Add custom token'); - - expect(submit).toBeDisabled(); - }); - - it('edits token address', () => { - const { getByText } = render(); - const customTokenButton = getByText('Custom token'); - fireEvent.click(customTokenButton); - - const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; - const event = { target: { value: tokenAddress } }; - fireEvent.change(document.getElementById('custom-address'), event); - - expect(document.getElementById('custom-address').value).toStrictEqual( - tokenAddress, - ); - }); - - it('edits token symbol', () => { - const { getByText } = render(); - const customTokenButton = getByText('Custom token'); - fireEvent.click(customTokenButton); - - const tokenSymbol = 'META'; - const event = { target: { value: tokenSymbol } }; - fireEvent.change(document.getElementById('custom-symbol'), event); - - expect(document.getElementById('custom-symbol').value).toStrictEqual( - tokenSymbol, - ); - }); - - it('edits token decimal precision', () => { - const { getByText } = render(); - const customTokenButton = getByText('Custom token'); - fireEvent.click(customTokenButton); - - const tokenPrecision = '2'; - const event = { target: { value: tokenPrecision } }; - fireEvent.change(document.getElementById('custom-decimals'), event); - - expect(document.getElementById('custom-decimals').value).toStrictEqual( - tokenPrecision, - ); - }); - - it('adds custom tokens successfully', async () => { - const { getByText } = render(); - const customTokenButton = getByText('Custom token'); - fireEvent.click(customTokenButton); - - const submit = getByText('Add custom token'); - expect(submit).toBeDisabled(); - - const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; - fireEvent.change(document.getElementById('custom-address'), { - target: { value: tokenAddress }, - }); - expect(submit).not.toBeDisabled(); - - const tokenSymbol = 'META'; - fireEvent.change(document.getElementById('custom-symbol'), { - target: { value: tokenSymbol }, - }); - - const tokenPrecision = '2'; - await fireEvent.change(document.getElementById('custom-decimals'), { - target: { value: tokenPrecision }, - }); - - expect(submit).not.toBeDisabled(); - fireEvent.click(submit); - expect(setPendingTokens).toHaveBeenCalledWith({ - customToken: { - address: tokenAddress, - decimals: Number(tokenPrecision), - standard: 'ERC20', - symbol: tokenSymbol, - }, - selectedTokens: {}, - tokenAddressList: [], - }); - expect(historyStub).toHaveBeenCalledWith('/confirm-import-token'); - }); - - it('cancels out of import token flow', () => { - const { getByRole } = render(); - const closeButton = getByRole('button', { name: 'close' }); - fireEvent.click(closeButton); - - expect(clearPendingTokens).toHaveBeenCalled(); - expect(historyStub).toHaveBeenCalledWith('/'); - }); - - it('sets and error when a token is an NFT', async () => { - getTokenStandardAndDetails.mockImplementation(() => - Promise.resolve({ standard: 'ERC721' }), - ); - - const { getByText } = render(); - const customTokenButton = getByText('Custom token'); - fireEvent.click(customTokenButton); - - const submit = getByText('Add custom token'); - expect(submit).toBeDisabled(); - - const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; - await fireEvent.change(document.getElementById('custom-address'), { - target: { value: tokenAddress }, - }); - - expect(submit).toBeDisabled(); - - // The last part of this error message won't be found by getByText because it is wrapped as a link. - const errorMessage = getByText('This token is an NFT. Add on the'); - expect(errorMessage).toBeInTheDocument(); - }); - }); -}); diff --git a/ui/pages/import-token/index.js b/ui/pages/import-token/index.js deleted file mode 100644 index 8fa4ee8c8..000000000 --- a/ui/pages/import-token/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import ImportToken from './import-token.container'; - -export default ImportToken; diff --git a/ui/pages/import-token/index.scss b/ui/pages/import-token/index.scss deleted file mode 100644 index be2cc1cc5..000000000 --- a/ui/pages/import-token/index.scss +++ /dev/null @@ -1,79 +0,0 @@ -@import 'token-list/index'; - -.import-token { - $self: &; - - &__custom-token-form { - padding: 8px 16px 16px; - - input[type='number']::-webkit-inner-spin-button { - -webkit-appearance: none; - display: none; - } - - input[type='number']:hover::-webkit-inner-spin-button { - -webkit-appearance: none; - display: none; - } - & #{$self}__decimal-warning { - margin-top: 5px; - } - } - - &__search-token { - padding: 16px 16px 16px 16px; - } - - &__token-list { - margin-top: 16px; - } - - &__custom-symbol { - &__label-wrapper { - display: flex; - flex-flow: row nowrap; - } - - &__label { - flex: 0 0 auto; - } - - &__edit { - flex: 1 1 auto; - text-align: right; - color: var(--color-primary-default); - padding-right: 4px; - cursor: pointer; - } - } - - &__link { - @include H7; - - display: inline; - color: var(--color-primary-default); - padding-left: 0; - } - - &__token-detection-announcement { - margin-bottom: 16px; - margin-top: 0; - } - - &__close { - color: var(--color-icon-default); - background: none; - flex: 0; - align-self: flex-start; - padding-right: 0; - } - - &__nft-address-error-link { - color: var(--color-primary-default); - cursor: pointer; - - &:hover { - color: var(--color-primary-default); - } - } -} diff --git a/ui/pages/import-token/token-list/token-list-placeholder/index.scss b/ui/pages/import-token/token-list/token-list-placeholder/index.scss deleted file mode 100644 index e97741621..000000000 --- a/ui/pages/import-token/token-list/token-list-placeholder/index.scss +++ /dev/null @@ -1,27 +0,0 @@ -.token-list-placeholder { - display: flex; - align-items: center; - padding-top: 36px; - flex-direction: column; - line-height: 22px; - - img { - opacity: 0.5; - } - - &__text { - color: var(--color-text-alternative); - width: 50%; - text-align: center; - margin-top: 8px; - opacity: 0.5; - - @include screen-sm-max { - width: 60%; - } - } - - &__link { - margin-top: 0.5rem; - } -} diff --git a/ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.component.js deleted file mode 100644 index ffc13bbec..000000000 --- a/ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.component.js +++ /dev/null @@ -1,31 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import Button from '../../../../components/ui/button'; -import IconTokenSearch from '../../../../components/ui/icon/icon-token-search'; -import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; - -export default class TokenListPlaceholder extends Component { - static contextTypes = { - t: PropTypes.func, - }; - - render() { - return ( -
- -
- {this.context.t('addAcquiredTokens')} -
- -
- ); - } -} diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index 72c4b2b49..96f1f29c8 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -1,6 +1,5 @@ /** Please import your files in alphabetical order **/ @import 'asset/asset'; -@import 'confirm-import-token/index'; @import 'confirm-add-suggested-token/index'; @import 'confirm-add-suggested-nft/index'; @import 'confirm-approve/index'; @@ -15,7 +14,6 @@ @import 'desktop-pairing/index'; @import 'error/index'; @import 'home/index'; -@import 'import-token/index'; ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) @import "institutional/connect-custody/index"; @import "institutional/institutional-entity-done-page/index"; diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index d6280a79c..c66efe967 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -7,6 +7,7 @@ import IdleTimer from 'react-idle-timer'; ///: BEGIN:ONLY_INCLUDE_IN(desktop) import browserAPI from 'webextension-polyfill'; ///: END:ONLY_INCLUDE_IN + import SendTransactionScreen from '../send'; import Swaps from '../swaps'; import ConfirmTransaction from '../confirm-transaction'; @@ -18,8 +19,6 @@ import Lock from '../lock'; import PermissionsConnect from '../permissions-connect'; import RestoreVaultPage from '../keychains/restore-vault'; import RevealSeedConfirmation from '../keychains/reveal-seed'; -import ImportTokenPage from '../import-token'; -import ConfirmImportTokenPage from '../confirm-import-token'; import ConfirmAddSuggestedTokenPage from '../confirm-add-suggested-token'; import CreateAccountPage from '../create-account/create-account.component'; import ConfirmAddSuggestedNftPage from '../confirm-add-suggested-nft'; @@ -33,6 +32,7 @@ import { NetworkListMenu, AccountDetails, ImportNftsModal, + ImportTokensModal, } from '../../components/multichain'; import UnlockPage from '../unlock-page'; import Alerts from '../../components/app/alerts'; @@ -59,7 +59,6 @@ import CustodyPage from '../institutional/custody'; ///: END:ONLY_INCLUDE_IN import { - IMPORT_TOKEN_ROUTE, ASSET_ROUTE, CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, CONFIRM_ADD_SUGGESTED_NFT_ROUTE, @@ -76,7 +75,6 @@ import { UNLOCK_ROUTE, BUILD_QUOTE_ROUTE, CONFIRMATION_V_NEXT_ROUTE, - CONFIRM_IMPORT_TOKEN_ROUTE, ONBOARDING_ROUTE, ONBOARDING_UNLOCK_ROUTE, TOKEN_DETAILS, @@ -162,6 +160,8 @@ export default class Routes extends Component { hideImportNftsModal: PropTypes.func.isRequired, isIpfsModalOpen: PropTypes.bool.isRequired, hideIpfsModal: PropTypes.func.isRequired, + isImportTokensModalOpen: PropTypes.bool.isRequired, + hideImportTokensModal: PropTypes.func.isRequired, }; static contextTypes = { @@ -279,16 +279,6 @@ export default class Routes extends Component { exact /> - - hideIpfsModal()} /> ) : null} + {isImportTokensModalOpen ? ( + hideImportTokensModal()} /> + ) : null} {isLoading ? : null} {!isLoading && isNetworkLoading ? : null} diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index 03c0e7f46..d964dfc54 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -14,6 +14,7 @@ import { isCurrentProviderCustom, } from '../../selectors'; import { + hideImportTokensModal, lockMetamask, hideImportNftsModal, hideIpfsModal, @@ -64,6 +65,7 @@ function mapStateToProps(state) { completedOnboarding, isAccountMenuOpen: state.metamask.isAccountMenuOpen, isNetworkMenuOpen: state.metamask.isNetworkMenuOpen, + isImportTokensModalOpen: state.appState.importTokensModalOpen, accountDetailsAddress: state.appState.accountDetailsAddress, isImportNftsModalOpen: state.appState.importNftsModalOpen, isIpfsModalOpen: state.appState.showIpfsModalOpen, @@ -83,6 +85,7 @@ function mapDispatchToProps(dispatch) { toggleNetworkMenu: () => dispatch(toggleNetworkMenu()), hideImportNftsModal: () => dispatch(hideImportNftsModal()), hideIpfsModal: () => dispatch(hideIpfsModal()), + hideImportTokensModal: () => dispatch(hideImportTokensModal()), }; } diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index f9e256234..1190abd2a 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -627,6 +627,7 @@ exports[`Security Tab should match snapshot 1`] = `
{t('autoDetectTokens')} diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 3cbc24497..0a48192b5 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -13,6 +13,8 @@ export const IMPORT_NFTS_MODAL_OPEN = 'UI_IMPORT_NFTS_MODAL_OPEN'; export const IMPORT_NFTS_MODAL_CLOSE = 'UI_IMPORT_NFTS_MODAL_CLOSE'; export const SHOW_IPFS_MODAL_OPEN = 'UI_IPFS_MODAL_OPEN'; export const SHOW_IPFS_MODAL_CLOSE = 'UI_IPFS_MODAL_CLOSE'; +export const IMPORT_TOKENS_POPOVER_OPEN = 'UI_IMPORT_TOKENS_POPOVER_OPEN'; +export const IMPORT_TOKENS_POPOVER_CLOSE = 'UI_IMPORT_TOKENS_POPOVER_CLOSE'; // remote state export const UPDATE_METAMASK_STATE = 'UPDATE_METAMASK_STATE'; export const SELECTED_ADDRESS_CHANGED = 'SELECTED_ADDRESS_CHANGED'; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index b7751a7ed..6d92e5b1b 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -2381,6 +2381,18 @@ export function hideNetworkDropdown() { }; } +export function showImportTokensModal(): Action { + return { + type: actionConstants.IMPORT_TOKENS_POPOVER_OPEN, + }; +} + +export function hideImportTokensModal(): Action { + return { + type: actionConstants.IMPORT_TOKENS_POPOVER_CLOSE, + }; +} + type ModalPayload = { name: string } & Record; export function showModal(payload: ModalPayload): PayloadAction {