mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
* Move Import Tokens to Modal * Better dimensions for long token name * Add padding above tabs
This commit is contained in:
parent
d6eecf8584
commit
ee4bf2d264
3
app/_locales/am/messages.json
generated
3
app/_locales/am/messages.json
generated
@ -609,9 +609,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "ውጤቶችን ፈልግ"
|
"message": "ውጤቶችን ፈልግ"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "ተለዋጭ ስሞችን ፈልግ"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "ደህንነት እና ግላዊነት"
|
"message": "ደህንነት እና ግላዊነት"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/ar/messages.json
generated
3
app/_locales/ar/messages.json
generated
@ -621,9 +621,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "نتائج البحث"
|
"message": "نتائج البحث"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "البحث عن العملات الرمزية"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "الأمن والخصوصية"
|
"message": "الأمن والخصوصية"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/bg/messages.json
generated
3
app/_locales/bg/messages.json
generated
@ -620,9 +620,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Резултати от търсенето"
|
"message": "Резултати от търсенето"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Търсене на маркери"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Сигурност и поверителност"
|
"message": "Сигурност и поверителност"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/bn/messages.json
generated
3
app/_locales/bn/messages.json
generated
@ -618,9 +618,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "অনুসন্ধানের ফলাফলগুলি"
|
"message": "অনুসন্ধানের ফলাফলগুলি"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "টোকেনগুলি অনুসন্ধান করুন"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "নিরাপত্তা এবং গোপনীয়তা"
|
"message": "নিরাপত্তা এবং গোপনীয়তা"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/ca/messages.json
generated
3
app/_locales/ca/messages.json
generated
@ -605,9 +605,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Resultats de Cerca"
|
"message": "Resultats de Cerca"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Tokens per cercar"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Seguretat i privacitat"
|
"message": "Seguretat i privacitat"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/cs/messages.json
generated
3
app/_locales/cs/messages.json
generated
@ -289,9 +289,6 @@
|
|||||||
"search": {
|
"search": {
|
||||||
"message": "Hledat"
|
"message": "Hledat"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Hledat tokeny"
|
|
||||||
},
|
|
||||||
"seedPhraseReq": {
|
"seedPhraseReq": {
|
||||||
"message": "klíčové fráze mají 12 slov"
|
"message": "klíčové fráze mají 12 slov"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/da/messages.json
generated
3
app/_locales/da/messages.json
generated
@ -605,9 +605,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Søg Resultater"
|
"message": "Søg Resultater"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Søg efter tokens"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Sikkerhed & Privatliv"
|
"message": "Sikkerhed & Privatliv"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/de/messages.json
generated
6
app/_locales/de/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Benutzerdefiniertes Netzwerk hinzufügen"
|
"message": "Benutzerdefiniertes Netzwerk hinzufügen"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Kunden-Token hinzufügen"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Dadurch kann dieses Netzwerk innerhalb MetaMask verwendet werden."
|
"message": "Dadurch kann dieses Netzwerk innerhalb MetaMask verwendet werden."
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Suchergebnisse"
|
"message": "Suchergebnisse"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Token suchen"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Geheime Wiederherstellungsphrase"
|
"message": "Geheime Wiederherstellungsphrase"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/el/messages.json
generated
6
app/_locales/el/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Προσθήκη προσαρμοσμένου δικτύου"
|
"message": "Προσθήκη προσαρμοσμένου δικτύου"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Προσθήκη Προσαρμοσμένου Token"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Αυτό θα επιτρέψει σε αυτό το δίκτυο να χρησιμοποιηθεί στο MetaMask."
|
"message": "Αυτό θα επιτρέψει σε αυτό το δίκτυο να χρησιμοποιηθεί στο MetaMask."
|
||||||
},
|
},
|
||||||
@ -2977,9 +2974,6 @@
|
|||||||
"searchAccounts": {
|
"searchAccounts": {
|
||||||
"message": "Αναζήτηση Λογαριασμών"
|
"message": "Αναζήτηση Λογαριασμών"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Αναζήτηση Tokens"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Μυστική Φράση Ανάκτησης"
|
"message": "Μυστική Φράση Ανάκτησης"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/en/messages.json
generated
6
app/_locales/en/messages.json
generated
@ -192,9 +192,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Add custom network"
|
"message": "Add custom network"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Add custom token"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "This will allow this network to be used within MetaMask."
|
"message": "This will allow this network to be used within MetaMask."
|
||||||
},
|
},
|
||||||
@ -3653,9 +3650,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Search results"
|
"message": "Search results"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Search tokens"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Secret Recovery Phrase"
|
"message": "Secret Recovery Phrase"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/es/messages.json
generated
6
app/_locales/es/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Agregar red personalizada"
|
"message": "Agregar red personalizada"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Añadir token personalizado"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Esto permitirá que la red se utilice en MetaMask."
|
"message": "Esto permitirá que la red se utilice en MetaMask."
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Resultados de la búsqueda"
|
"message": "Resultados de la búsqueda"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Buscar tokens"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Frase secreta de recuperación"
|
"message": "Frase secreta de recuperación"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/es_419/messages.json
generated
6
app/_locales/es_419/messages.json
generated
@ -100,9 +100,6 @@
|
|||||||
"addContact": {
|
"addContact": {
|
||||||
"message": "Agregar contacto"
|
"message": "Agregar contacto"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Añadir token personalizado"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Esto permitirá que la red se utilice en MetaMask."
|
"message": "Esto permitirá que la red se utilice en MetaMask."
|
||||||
},
|
},
|
||||||
@ -1882,9 +1879,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Resultados de la búsqueda"
|
"message": "Resultados de la búsqueda"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Buscar tokens"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Frase secreta de recuperación"
|
"message": "Frase secreta de recuperación"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/et/messages.json
generated
3
app/_locales/et/messages.json
generated
@ -614,9 +614,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Otsingutulemused"
|
"message": "Otsingutulemused"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Lubade otsimine"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Turvalisus ja privaatsus"
|
"message": "Turvalisus ja privaatsus"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/fa/messages.json
generated
3
app/_locales/fa/messages.json
generated
@ -624,9 +624,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "نتایج جستجو"
|
"message": "نتایج جستجو"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "رمزیاب های جستجو"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "امنیت و حریم خصوصی"
|
"message": "امنیت و حریم خصوصی"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/fi/messages.json
generated
3
app/_locales/fi/messages.json
generated
@ -621,9 +621,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Hakutulokset"
|
"message": "Hakutulokset"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Hae tietueita"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Turva & yksityisyys"
|
"message": "Turva & yksityisyys"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/fil/messages.json
generated
3
app/_locales/fil/messages.json
generated
@ -548,9 +548,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Mga Resulta ng Paghahanap"
|
"message": "Mga Resulta ng Paghahanap"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Maghanap ng Mga Token"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Seguridad at Privacy"
|
"message": "Seguridad at Privacy"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/fr/messages.json
generated
6
app/_locales/fr/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Ajouter un réseau personnalisé"
|
"message": "Ajouter un réseau personnalisé"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Ajouter un jeton personnalisé"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Cela permettra d’utiliser ce réseau dans MetaMask."
|
"message": "Cela permettra d’utiliser ce réseau dans MetaMask."
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Résultats de la recherche"
|
"message": "Résultats de la recherche"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Rechercher des jetons"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Phrase secrète de récupération"
|
"message": "Phrase secrète de récupération"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/he/messages.json
generated
3
app/_locales/he/messages.json
generated
@ -621,9 +621,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "תוצאות חיפוש"
|
"message": "תוצאות חיפוש"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "חיפוש טוקנים"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "אבטחה ופרטיות"
|
"message": "אבטחה ופרטיות"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/hi/messages.json
generated
6
app/_locales/hi/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "कस्टम नेटवर्क जोड़ें"
|
"message": "कस्टम नेटवर्क जोड़ें"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "कस्टम टोकन जोड़ें"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "इससे इस नेटवर्क को MetaMask के अंदर उपयोग करने की अनुमति मिलेगी।"
|
"message": "इससे इस नेटवर्क को MetaMask के अंदर उपयोग करने की अनुमति मिलेगी।"
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "खोज परिणाम"
|
"message": "खोज परिणाम"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "टोकन खोजें"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "सीक्रेट रिकवरी फ्रेज"
|
"message": "सीक्रेट रिकवरी फ्रेज"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/hr/messages.json
generated
3
app/_locales/hr/messages.json
generated
@ -617,9 +617,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Rezultati pretraživanja"
|
"message": "Rezultati pretraživanja"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Pretraži tokene"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Sigurnost i privatnost"
|
"message": "Sigurnost i privatnost"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/ht/messages.json
generated
3
app/_locales/ht/messages.json
generated
@ -452,9 +452,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Rezilta rechèch"
|
"message": "Rezilta rechèch"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Rechèch Tokens"
|
|
||||||
},
|
|
||||||
"seedPhraseReq": {
|
"seedPhraseReq": {
|
||||||
"message": "Seed fraz yo se 12 long mo"
|
"message": "Seed fraz yo se 12 long mo"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/hu/messages.json
generated
3
app/_locales/hu/messages.json
generated
@ -617,9 +617,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Keresési eredmények"
|
"message": "Keresési eredmények"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Keresés a tokenek között"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Biztonság és adatvédelem"
|
"message": "Biztonság és adatvédelem"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/id/messages.json
generated
6
app/_locales/id/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Tambahkan jaringan khusus"
|
"message": "Tambahkan jaringan khusus"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Tambahkan token kustom"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Tindakan ini akan membantu jaringan ini agar dapat digunakan dengan MetaMask."
|
"message": "Tindakan ini akan membantu jaringan ini agar dapat digunakan dengan MetaMask."
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Cari hasil"
|
"message": "Cari hasil"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Cari token"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Frasa Pemulihan Rahasia"
|
"message": "Frasa Pemulihan Rahasia"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/it/messages.json
generated
6
app/_locales/it/messages.json
generated
@ -155,9 +155,6 @@
|
|||||||
"addContact": {
|
"addContact": {
|
||||||
"message": "Aggiungi contatto"
|
"message": "Aggiungi contatto"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Aggiungi token personalizzato"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Ciò consentirà a questa rete di essere utilizzata all'interno di MetaMask."
|
"message": "Ciò consentirà a questa rete di essere utilizzata all'interno di MetaMask."
|
||||||
},
|
},
|
||||||
@ -1380,9 +1377,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Risultati Ricerca"
|
"message": "Risultati Ricerca"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Cerca Tokens"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Sicurezza & Privacy"
|
"message": "Sicurezza & Privacy"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/ja/messages.json
generated
6
app/_locales/ja/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "カスタムネットワークを追加"
|
"message": "カスタムネットワークを追加"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "カスタムトークンを追加"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "これにより、このネットワークはMetaMask内で使用できるようになります。"
|
"message": "これにより、このネットワークはMetaMask内で使用できるようになります。"
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "検索結果"
|
"message": "検索結果"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "トークンを検索"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "シークレットリカバリーフレーズ"
|
"message": "シークレットリカバリーフレーズ"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/kn/messages.json
generated
3
app/_locales/kn/messages.json
generated
@ -624,9 +624,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "ಹುಡುಕಾಟ ಫಲಿತಾಂಶಗಳು"
|
"message": "ಹುಡುಕಾಟ ಫಲಿತಾಂಶಗಳು"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "ಟೋಕನ್ಗಳನ್ನು ಹುಡುಕಿ"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "ಭದ್ರತೆ ಮತ್ತು ಗೌಪ್ಯತೆ"
|
"message": "ಭದ್ರತೆ ಮತ್ತು ಗೌಪ್ಯತೆ"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/ko/messages.json
generated
6
app/_locales/ko/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "커스텀 네트워크 추가"
|
"message": "커스텀 네트워크 추가"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "커스텀 토큰 추가"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "이렇게 하면 MetaMask 내에서 이 네트워크를 사용할 수 있습니다."
|
"message": "이렇게 하면 MetaMask 내에서 이 네트워크를 사용할 수 있습니다."
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "검색 결과"
|
"message": "검색 결과"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "토큰 검색"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "비밀 복구 구문"
|
"message": "비밀 복구 구문"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/lt/messages.json
generated
3
app/_locales/lt/messages.json
generated
@ -624,9 +624,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Paieškos rezultatai"
|
"message": "Paieškos rezultatai"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Ieškoti žetonų"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Sauga ir privatumas"
|
"message": "Sauga ir privatumas"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/lv/messages.json
generated
3
app/_locales/lv/messages.json
generated
@ -620,9 +620,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Meklēšanas rezultāti"
|
"message": "Meklēšanas rezultāti"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Meklēt marķierus"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Drošība un konfidencialitāte"
|
"message": "Drošība un konfidencialitāte"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/ms/messages.json
generated
3
app/_locales/ms/messages.json
generated
@ -604,9 +604,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Hasil Carian"
|
"message": "Hasil Carian"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Cari Token"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Keselamatan & Privasi"
|
"message": "Keselamatan & Privasi"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/no/messages.json
generated
3
app/_locales/no/messages.json
generated
@ -608,9 +608,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Søkeresultater"
|
"message": "Søkeresultater"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Søk i sjetonger"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Sikkerhet og personvern"
|
"message": "Sikkerhet og personvern"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/ph/messages.json
generated
3
app/_locales/ph/messages.json
generated
@ -1211,9 +1211,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Mga Resulta ng Paghahanap"
|
"message": "Mga Resulta ng Paghahanap"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Maghanap ng Mga Token"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Seguridad at Privacy"
|
"message": "Seguridad at Privacy"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/pl/messages.json
generated
3
app/_locales/pl/messages.json
generated
@ -618,9 +618,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Wyniki wyszukiwania"
|
"message": "Wyniki wyszukiwania"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Szukaj tokenów"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Bezpieczeństwo i prywatność"
|
"message": "Bezpieczeństwo i prywatność"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/pt/messages.json
generated
6
app/_locales/pt/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Adicionar rede personalizada"
|
"message": "Adicionar rede personalizada"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Adicionar token personalizado"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Isso permitirá que essa rede seja usada dentro da MetaMask."
|
"message": "Isso permitirá que essa rede seja usada dentro da MetaMask."
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Resultados da busca"
|
"message": "Resultados da busca"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Buscar tokens"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Frase Secreta de Recuperação"
|
"message": "Frase Secreta de Recuperação"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/pt_BR/messages.json
generated
6
app/_locales/pt_BR/messages.json
generated
@ -100,9 +100,6 @@
|
|||||||
"addContact": {
|
"addContact": {
|
||||||
"message": "Adicionar contato"
|
"message": "Adicionar contato"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Adicionar token personalizado"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Isso permitirá que essa rede seja usada dentro da MetaMask."
|
"message": "Isso permitirá que essa rede seja usada dentro da MetaMask."
|
||||||
},
|
},
|
||||||
@ -1882,9 +1879,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Resultados da busca"
|
"message": "Resultados da busca"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Buscar tokens"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Frase de Recuperação Secreta"
|
"message": "Frase de Recuperação Secreta"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/ro/messages.json
generated
3
app/_locales/ro/messages.json
generated
@ -611,9 +611,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Rezultate căutare"
|
"message": "Rezultate căutare"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Căutați token-uri"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Securitate și confidențialitate"
|
"message": "Securitate și confidențialitate"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/ru/messages.json
generated
6
app/_locales/ru/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Добавить пользовательскую сеть"
|
"message": "Добавить пользовательскую сеть"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Добавить пользовательский токен"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Это позволит использовать эту сеть в MetaMask."
|
"message": "Это позволит использовать эту сеть в MetaMask."
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Результаты поиска"
|
"message": "Результаты поиска"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Поиск токенов"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Секретная фраза для восстановления"
|
"message": "Секретная фраза для восстановления"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/sk/messages.json
generated
3
app/_locales/sk/messages.json
generated
@ -596,9 +596,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Výsledky vyhľadávania"
|
"message": "Výsledky vyhľadávania"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Hledat tokeny"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Bezpečnosť a súkromie"
|
"message": "Bezpečnosť a súkromie"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/sl/messages.json
generated
3
app/_locales/sl/messages.json
generated
@ -612,9 +612,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Rezultati iskanja"
|
"message": "Rezultati iskanja"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Iskanje žetonov"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Varnost in zasebnost"
|
"message": "Varnost in zasebnost"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/sr/messages.json
generated
3
app/_locales/sr/messages.json
generated
@ -615,9 +615,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Rezultati pretrage"
|
"message": "Rezultati pretrage"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Pretražite tokene"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Bezbednost i privatnost"
|
"message": "Bezbednost i privatnost"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/sv/messages.json
generated
3
app/_locales/sv/messages.json
generated
@ -608,9 +608,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Sökresultat"
|
"message": "Sökresultat"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Sök tokens"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Säkerhet och integritet"
|
"message": "Säkerhet och integritet"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/sw/messages.json
generated
3
app/_locales/sw/messages.json
generated
@ -602,9 +602,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Matokeo ya Utafutaji"
|
"message": "Matokeo ya Utafutaji"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Tafuta Vianzio"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Ulinzi na Faragha"
|
"message": "Ulinzi na Faragha"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/ta/messages.json
generated
3
app/_locales/ta/messages.json
generated
@ -359,9 +359,6 @@
|
|||||||
"search": {
|
"search": {
|
||||||
"message": "தேடல்"
|
"message": "தேடல்"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "தேடல் டோக்கன்ஸ்"
|
|
||||||
},
|
|
||||||
"seedPhraseReq": {
|
"seedPhraseReq": {
|
||||||
"message": "விதை வாக்கியங்கள் 12 வார்த்தைகள் நீண்டவை"
|
"message": "விதை வாக்கியங்கள் 12 வார்த்தைகள் நீண்டவை"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/tl/messages.json
generated
6
app/_locales/tl/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Magdagdag ng custom na network"
|
"message": "Magdagdag ng custom na network"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Magdagdag ng Custom na Token"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Magpapahintulot ito sa network na ito na gamitin sa loob ng MetaMask."
|
"message": "Magpapahintulot ito sa network na ito na gamitin sa loob ng MetaMask."
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Mga Resulta ng Paghahanap"
|
"message": "Mga Resulta ng Paghahanap"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Maghanap ng Mga Token"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Lihim na recovery phrase"
|
"message": "Lihim na recovery phrase"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/tr/messages.json
generated
6
app/_locales/tr/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Özel ağ ekle"
|
"message": "Özel ağ ekle"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Özel token ekle"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Bu, bu ağın MetaMas dahilinde kullanılmasına olanak tanıyacaktır."
|
"message": "Bu, bu ağın MetaMas dahilinde kullanılmasına olanak tanıyacaktır."
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Arama sonuçları"
|
"message": "Arama sonuçları"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Token ara"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Gizli Kurtarma İfadesi"
|
"message": "Gizli Kurtarma İfadesi"
|
||||||
},
|
},
|
||||||
|
3
app/_locales/uk/messages.json
generated
3
app/_locales/uk/messages.json
generated
@ -624,9 +624,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Результати пошуку"
|
"message": "Результати пошуку"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Шукати токени"
|
|
||||||
},
|
|
||||||
"securityAndPrivacy": {
|
"securityAndPrivacy": {
|
||||||
"message": "Безпека й конфіденційність"
|
"message": "Безпека й конфіденційність"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/vi/messages.json
generated
6
app/_locales/vi/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "Thêm mạng tùy chỉnh"
|
"message": "Thêm mạng tùy chỉnh"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Thêm token tùy chỉnh"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "Thao tác này sẽ cho phép sử dụng mạng này trong MetaMask."
|
"message": "Thao tác này sẽ cho phép sử dụng mạng này trong MetaMask."
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "Kết quả tìm kiếm"
|
"message": "Kết quả tìm kiếm"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "Tìm kiếm token"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "Cụm Mật Khẩu Khôi Phục Bí Mật"
|
"message": "Cụm Mật Khẩu Khôi Phục Bí Mật"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/zh_CN/messages.json
generated
6
app/_locales/zh_CN/messages.json
generated
@ -186,9 +186,6 @@
|
|||||||
"addCustomNetwork": {
|
"addCustomNetwork": {
|
||||||
"message": "添加自定义网络"
|
"message": "添加自定义网络"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "添加自定义代币"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "这将允许在 MetaMask 中使用此网络。"
|
"message": "这将允许在 MetaMask 中使用此网络。"
|
||||||
},
|
},
|
||||||
@ -2980,9 +2977,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "搜索结果"
|
"message": "搜索结果"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "搜索代币"
|
|
||||||
},
|
|
||||||
"secretRecoveryPhrase": {
|
"secretRecoveryPhrase": {
|
||||||
"message": "助记词"
|
"message": "助记词"
|
||||||
},
|
},
|
||||||
|
6
app/_locales/zh_TW/messages.json
generated
6
app/_locales/zh_TW/messages.json
generated
@ -42,9 +42,6 @@
|
|||||||
"addContact": {
|
"addContact": {
|
||||||
"message": "新增合約"
|
"message": "新增合約"
|
||||||
},
|
},
|
||||||
"addCustomToken": {
|
|
||||||
"message": "Add Custom Token"
|
|
||||||
},
|
|
||||||
"addEthereumChainConfirmationDescription": {
|
"addEthereumChainConfirmationDescription": {
|
||||||
"message": "這會允許在 MetaMask 內使用這個網路。"
|
"message": "這會允許在 MetaMask 內使用這個網路。"
|
||||||
},
|
},
|
||||||
@ -1133,9 +1130,6 @@
|
|||||||
"searchResults": {
|
"searchResults": {
|
||||||
"message": "搜尋結果"
|
"message": "搜尋結果"
|
||||||
},
|
},
|
||||||
"searchTokens": {
|
|
||||||
"message": "搜尋代幣"
|
|
||||||
},
|
|
||||||
"secureWallet": {
|
"secureWallet": {
|
||||||
"message": "Secure Wallet"
|
"message": "Secure Wallet"
|
||||||
},
|
},
|
||||||
|
@ -257,13 +257,18 @@ describe('MetaMask', function () {
|
|||||||
});
|
});
|
||||||
await driver.delay(regularDelayMs);
|
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.delay(regularDelayMs);
|
||||||
|
|
||||||
await driver.clickElement({ text: 'Add custom token', tag: 'button' });
|
await driver.clickElement({ text: 'Next', tag: 'button' });
|
||||||
await driver.delay(regularDelayMs);
|
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);
|
await driver.delay(regularDelayMs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,13 +108,15 @@ describe('Add existing token using search', function () {
|
|||||||
await driver.press('#password', driver.Key.ENTER);
|
await driver.press('#password', driver.Key.ENTER);
|
||||||
|
|
||||||
await driver.clickElement({ text: 'Import tokens', tag: 'button' });
|
await driver.clickElement({ text: 'Import tokens', tag: 'button' });
|
||||||
await driver.fill('#search-tokens', 'BAT');
|
await driver.fill('input[placeholder="Search"]', 'BAT');
|
||||||
await driver.clickElement({
|
await driver.clickElement({
|
||||||
text: 'BAT',
|
text: 'BAT',
|
||||||
tag: 'span',
|
tag: 'span',
|
||||||
});
|
});
|
||||||
await driver.clickElement({ text: 'Next', tag: 'button' });
|
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({
|
await driver.waitForSelector({
|
||||||
css: '.token-overview__primary-balance',
|
css: '.token-overview__primary-balance',
|
||||||
|
@ -51,20 +51,24 @@ describe('Create token, approve token and approve token without gas', function (
|
|||||||
text: 'Custom token',
|
text: 'Custom token',
|
||||||
tag: 'button',
|
tag: 'button',
|
||||||
});
|
});
|
||||||
await driver.fill('#custom-address', contractAddress);
|
await driver.fill(
|
||||||
await driver.waitForSelector('#custom-decimals');
|
'[data-testid="import-tokens-modal-custom-address"]',
|
||||||
|
contractAddress,
|
||||||
|
);
|
||||||
|
await driver.waitForSelector(
|
||||||
|
'[data-testid="import-tokens-modal-custom-decimals"]',
|
||||||
|
);
|
||||||
await driver.delay(2000);
|
await driver.delay(2000);
|
||||||
|
|
||||||
await driver.clickElement({
|
await driver.clickElement({
|
||||||
text: 'Add custom token',
|
text: 'Next',
|
||||||
tag: 'button',
|
tag: 'button',
|
||||||
});
|
});
|
||||||
|
|
||||||
await driver.delay(2000);
|
await driver.delay(2000);
|
||||||
await driver.clickElement({
|
await driver.clickElement(
|
||||||
text: 'Import tokens',
|
'[data-testid="import-tokens-modal-import-button"]',
|
||||||
tag: 'button',
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// renders balance for newly created token
|
// renders balance for newly created token
|
||||||
await driver.clickElement('.app-header__logo-container');
|
await driver.clickElement('.app-header__logo-container');
|
||||||
|
@ -37,14 +37,20 @@ describe('Import flow', function () {
|
|||||||
await driver.delay(regularDelayMs);
|
await driver.delay(regularDelayMs);
|
||||||
|
|
||||||
await driver.clickElement('[data-testid="import-token-button"]');
|
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');
|
||||||
await driver.clickElement('.token-list__token:nth-of-type(2)');
|
await driver.clickElement('.token-list__token:nth-of-type(2)');
|
||||||
await driver.clickElement('.token-list__token:nth-of-type(3)');
|
await driver.clickElement('.token-list__token:nth-of-type(3)');
|
||||||
|
|
||||||
await driver.clickElement({ css: 'button', text: 'Next' });
|
await driver.clickElement({
|
||||||
await driver.clickElement({ css: 'button', text: 'Import' });
|
css: '.import-tokens-modal button',
|
||||||
|
text: 'Next',
|
||||||
|
});
|
||||||
|
await driver.clickElement({
|
||||||
|
css: '.import-tokens-modal button',
|
||||||
|
text: 'Import',
|
||||||
|
});
|
||||||
|
|
||||||
await driver.clickElement('.asset-breadcrumb');
|
await driver.clickElement('.asset-breadcrumb');
|
||||||
|
|
||||||
|
@ -30,11 +30,19 @@ describe('Token Details', function () {
|
|||||||
const tokenAddress = '0x2EFA2Cb29C2341d8E5Ba7D3262C9e9d6f1Bf3711';
|
const tokenAddress = '0x2EFA2Cb29C2341d8E5Ba7D3262C9e9d6f1Bf3711';
|
||||||
const tokenSymbol = 'AAVE';
|
const tokenSymbol = 'AAVE';
|
||||||
|
|
||||||
await driver.fill('#custom-address', tokenAddress);
|
await driver.fill(
|
||||||
await driver.waitForSelector('#custom-symbol-helper-text');
|
'[data-testid="import-tokens-modal-custom-address"]',
|
||||||
await driver.fill('#custom-symbol', tokenSymbol);
|
tokenAddress,
|
||||||
await driver.clickElement({ text: 'Add custom token', tag: 'button' });
|
);
|
||||||
await driver.clickElement({ text: 'Import tokens', tag: 'button' });
|
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('[aria-label="Asset options"]');
|
||||||
await driver.clickElement({ text: 'Token details', tag: 'div' });
|
await driver.clickElement({ text: 'Token details', tag: 'div' });
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
@import 'confirm-data/index';
|
@import 'confirm-data/index';
|
||||||
@import 'confirmation-warning-modal/index';
|
@import 'confirmation-warning-modal/index';
|
||||||
@import 'custom-nonce/index';
|
@import 'custom-nonce/index';
|
||||||
|
@import 'import-token/index';
|
||||||
@import 'nfts-items/index';
|
@import 'nfts-items/index';
|
||||||
@import 'nfts-tab/index';
|
@import 'nfts-tab/index';
|
||||||
@import 'nft-details/index';
|
@import 'nft-details/index';
|
||||||
|
1
ui/components/app/import-token/index.scss
Normal file
1
ui/components/app/import-token/index.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import 'token-list/index';
|
@ -1,5 +1,3 @@
|
|||||||
@import 'token-list-placeholder/index';
|
|
||||||
|
|
||||||
.token-list {
|
.token-list {
|
||||||
&__title {
|
&__title {
|
||||||
@include H7;
|
@include H7;
|
@ -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 (
|
||||||
|
<Box
|
||||||
|
display={Display.Flex}
|
||||||
|
alignItems={AlignItems.center}
|
||||||
|
flexDirection={FlexDirection.Column}
|
||||||
|
textAlign={TextAlign.Center}
|
||||||
|
>
|
||||||
|
<Text color={TextColor.textAlternative}>
|
||||||
|
{this.context.t('addAcquiredTokens')}
|
||||||
|
</Text>
|
||||||
|
<ButtonLink href={ZENDESK_URLS.ADD_CUSTOM_TOKENS} externalLink>
|
||||||
|
{this.context.t('learnMoreUpperCase')}
|
||||||
|
</ButtonLink>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import TokenListPlaceholder from './token-list-placeholder.component';
|
import TokenListPlaceholder from './token-list-placeholder.component';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Pages/ImportToken/TokenList/TokenListPlaceholder',
|
title: 'Components/App/TokenList/TokenListPlaceholder',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DefaultStory = () => {
|
export const DefaultStory = () => {
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { checkExistingAddresses } from '../../../helpers/utils/util';
|
import { checkExistingAddresses } from '../../../../helpers/utils/util';
|
||||||
import TokenListPlaceholder from './token-list-placeholder';
|
import TokenListPlaceholder from './token-list-placeholder';
|
||||||
|
|
||||||
export default class TokenList extends Component {
|
export default class TokenList extends Component {
|
@ -1,10 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils';
|
||||||
import TextField from '../../../components/ui/text-field';
|
import { TextFieldSearch } from '../../../component-library';
|
||||||
import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
|
import { BlockSize } from '../../../../helpers/constants/design-system';
|
||||||
import SearchIcon from '../../../components/ui/icon/search-icon';
|
|
||||||
|
|
||||||
export default class TokenSearch extends Component {
|
export default class TokenSearch extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -57,30 +56,19 @@ export default class TokenSearch extends Component {
|
|||||||
this.props.onSearch({ searchQuery, results });
|
this.props.onSearch({ searchQuery, results });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAdornment() {
|
|
||||||
return (
|
|
||||||
<InputAdornment position="start" style={{ marginRight: '12px' }}>
|
|
||||||
<SearchIcon color="var(--color-icon-muted)" />
|
|
||||||
</InputAdornment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { error } = this.props;
|
const { error } = this.props;
|
||||||
const { searchQuery } = this.state;
|
const { searchQuery } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextFieldSearch
|
||||||
id="search-tokens"
|
placeholder={this.context.t('search')}
|
||||||
placeholder={this.context.t('searchTokens')}
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => this.handleSearch(e.target.value)}
|
onChange={(e) => this.handleSearch(e.target.value)}
|
||||||
error={error}
|
error={error}
|
||||||
fullWidth
|
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="off"
|
autoComplete={false}
|
||||||
startAdornment={this.renderAdornment()}
|
width={BlockSize.Full}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import testData from '../../../../.storybook/test-data';
|
import testData from '../../../../../.storybook/test-data';
|
||||||
import TokenSearch from './token-search.component';
|
import TokenSearch from './token-search.component';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Pages/ImportToken/TokenSearch',
|
title: 'Components/App/ImportToken/TokenSearch',
|
||||||
|
|
||||||
argTypes: {
|
argTypes: {
|
||||||
error: {
|
error: {
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { ButtonLink, IconName, Box } from '../../component-library';
|
import { ButtonLink, IconName, Box } from '../../component-library';
|
||||||
@ -10,8 +9,7 @@ import {
|
|||||||
Size,
|
Size,
|
||||||
} from '../../../helpers/constants/design-system';
|
} from '../../../helpers/constants/design-system';
|
||||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
import { IMPORT_TOKEN_ROUTE } from '../../../helpers/constants/routes';
|
import { detectNewTokens, showImportTokensModal } from '../../../store/actions';
|
||||||
import { detectNewTokens } from '../../../store/actions';
|
|
||||||
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
||||||
import {
|
import {
|
||||||
MetaMetricsEventCategory,
|
MetaMetricsEventCategory,
|
||||||
@ -25,7 +23,6 @@ import {
|
|||||||
export const ImportTokenLink = ({ className, ...props }) => {
|
export const ImportTokenLink = ({ className, ...props }) => {
|
||||||
const trackEvent = useContext(MetaMetricsContext);
|
const trackEvent = useContext(MetaMetricsContext);
|
||||||
const t = useI18nContext();
|
const t = useI18nContext();
|
||||||
const history = useHistory();
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported);
|
const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported);
|
||||||
@ -48,7 +45,7 @@ export const ImportTokenLink = ({ className, ...props }) => {
|
|||||||
data-testid="import-token-button"
|
data-testid="import-token-button"
|
||||||
startIconName={IconName.Add}
|
startIconName={IconName.Add}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
history.push(IMPORT_TOKEN_ROUTE);
|
dispatch(showImportTokensModal());
|
||||||
trackEvent({
|
trackEvent({
|
||||||
event: MetaMetricsEventName.TokenImportButtonClicked,
|
event: MetaMetricsEventName.TokenImportButtonClicked,
|
||||||
category: MetaMetricsEventCategory.Navigation,
|
category: MetaMetricsEventCategory.Navigation,
|
||||||
|
@ -19,7 +19,12 @@ jest.mock('react-router-dom', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('../../../store/actions.ts', () => ({
|
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', () => {
|
describe('Import Token Link', () => {
|
||||||
@ -90,6 +95,6 @@ describe('Import Token Link', () => {
|
|||||||
const importToken = screen.getByTestId('import-token-button');
|
const importToken = screen.getByTestId('import-token-button');
|
||||||
fireEvent.click(importToken);
|
fireEvent.click(importToken);
|
||||||
|
|
||||||
expect(mockPushHistory).toHaveBeenCalledWith('/import-token');
|
expect(screen.getByText('Import tokens')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 (
|
||||||
|
<Box paddingTop={4} paddingBottom={4}>
|
||||||
|
<Text textAlign={TextAlign.Center}>{t('likeToImportTokens')}</Text>
|
||||||
|
<Box marginTop={4} marginBottom={4}>
|
||||||
|
<Box display={Display.Flex}>
|
||||||
|
<Text
|
||||||
|
variant={TextVariant.bodySm}
|
||||||
|
className="import-tokens-modal__token-name"
|
||||||
|
>
|
||||||
|
{t('token')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
variant={TextVariant.bodySm}
|
||||||
|
className="import-tokens-modal__token-balance"
|
||||||
|
>
|
||||||
|
{t('balance')}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
display={Display.Flex}
|
||||||
|
className="import-tokens-modal__confirm-token-list"
|
||||||
|
>
|
||||||
|
{Object.entries(pendingTokens).map(([address, token]) => {
|
||||||
|
const { name, symbol } = token;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={address}
|
||||||
|
marginBottom={4}
|
||||||
|
display={Display.Flex}
|
||||||
|
className="import-tokens-modal__confirm-token-list-item"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
display={Display.Flex}
|
||||||
|
alignItems={AlignItems.center}
|
||||||
|
className="import-tokens-modal__confirm-token-list-item-wrapper"
|
||||||
|
>
|
||||||
|
<Identicon diameter={36} address={address} />
|
||||||
|
<Box
|
||||||
|
marginInlineStart={4}
|
||||||
|
className="import-tokens-modal__confirm-token-list-item-wrapper__text"
|
||||||
|
>
|
||||||
|
<Text ellipsis>{name}</Text>
|
||||||
|
<Text
|
||||||
|
variant={TextVariant.bodySm}
|
||||||
|
color={TextColor.textAlternative}
|
||||||
|
>
|
||||||
|
{symbol}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
className="import-tokens-modal__token-balance"
|
||||||
|
alignItems={AlignItems.flexStart}
|
||||||
|
>
|
||||||
|
<TokenBalance token={token} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
<Box display={Display.Flex} gap={2} marginTop={4}>
|
||||||
|
<ButtonSecondary size={Size.LG} onClick={onBackClick} block>
|
||||||
|
{t('back')}
|
||||||
|
</ButtonSecondary>
|
||||||
|
<ButtonPrimary
|
||||||
|
size={Size.LG}
|
||||||
|
onClick={onImportClick}
|
||||||
|
block
|
||||||
|
data-testid="import-tokens-modal-import-button"
|
||||||
|
>
|
||||||
|
{t('import')}
|
||||||
|
</ButtonPrimary>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ImportTokensModalConfirm.propTypes = {
|
||||||
|
onBackClick: PropTypes.func.isRequired,
|
||||||
|
onImportClick: PropTypes.func.isRequired,
|
||||||
|
};
|
@ -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) => <ImportTokensModalConfirm {...args} />;
|
||||||
|
DefaultStory.decorators = [
|
||||||
|
(Story) => (
|
||||||
|
<Provider store={createStore()}>
|
||||||
|
<Story />
|
||||||
|
</Provider>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default';
|
||||||
|
|
||||||
|
export const LongValueStory = (args) => (
|
||||||
|
<div style={{ width: '300px' }}>
|
||||||
|
<ImportTokensModalConfirm {...args} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
LongValueStory.decorators = [
|
||||||
|
(Story) => (
|
||||||
|
<Provider store={createStore(CHAIN_IDS.MAINNET, true, 5)}>
|
||||||
|
<Story />
|
||||||
|
</Provider>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
LongValueStory.storyName = 'LongValueStory';
|
@ -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', [
|
||||||
|
<ButtonLink
|
||||||
|
className="import-tokens-modal__nft-address-error-link"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(showImportNftsModal());
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
color={TextColor.primaryDefault}
|
||||||
|
key="nftAddressError"
|
||||||
|
>
|
||||||
|
{t('importNFTPage')}
|
||||||
|
</ButtonLink>,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
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 (
|
||||||
|
<Modal
|
||||||
|
isOpen
|
||||||
|
onClose={() => {
|
||||||
|
dispatch(clearPendingTokens());
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="import-tokens-modal"
|
||||||
|
>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader
|
||||||
|
onBack={isConfirming ? () => setMode('') : null}
|
||||||
|
onClose={() => {
|
||||||
|
dispatch(clearPendingTokens());
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('importTokensCamelCase')}
|
||||||
|
</ModalHeader>
|
||||||
|
<Box marginTop={6}>
|
||||||
|
{isConfirming ? (
|
||||||
|
<ImportTokensModalConfirm
|
||||||
|
onBackClick={() => {
|
||||||
|
dispatch(clearPendingTokens());
|
||||||
|
setMode('');
|
||||||
|
}}
|
||||||
|
onImportClick={async () => {
|
||||||
|
await handleAddTokens();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Tabs t={t}>
|
||||||
|
{showSearchTab ? (
|
||||||
|
<Tab tabKey="search" name={t('search')}>
|
||||||
|
<Box paddingTop={4} paddingBottom={4}>
|
||||||
|
{useTokenDetection ? null : (
|
||||||
|
<BannerAlert severity={Severity.Info} marginBottom={4}>
|
||||||
|
<Text>
|
||||||
|
{t('enhancedTokenDetectionAlertMessage', [
|
||||||
|
networkName,
|
||||||
|
<ButtonLink
|
||||||
|
key="token-detection-announcement"
|
||||||
|
className="import-tokens-modal__autodetect"
|
||||||
|
onClick={() => {
|
||||||
|
history.push(
|
||||||
|
`${SECURITY_ROUTE}#advanced-settings-autodetect-tokens`,
|
||||||
|
);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('enableFromSettings')}
|
||||||
|
</ButtonLink>,
|
||||||
|
])}
|
||||||
|
</Text>
|
||||||
|
</BannerAlert>
|
||||||
|
)}
|
||||||
|
<TokenSearch
|
||||||
|
onSearch={({ results = [] }) =>
|
||||||
|
setSearchResults(results)
|
||||||
|
}
|
||||||
|
error={tokenSelectorError}
|
||||||
|
tokenList={tokenList}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
marginTop={4}
|
||||||
|
className="import-tokens-modal__search-list"
|
||||||
|
>
|
||||||
|
<TokenList
|
||||||
|
results={searchResults}
|
||||||
|
selectedTokens={selectedTokens}
|
||||||
|
onToggleToken={(token) => handleToggleToken(token)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Tab>
|
||||||
|
) : null}
|
||||||
|
<Tab tabKey="customToken" name={t('customToken')}>
|
||||||
|
<Box
|
||||||
|
padding={[2, 4, 4, 4]}
|
||||||
|
className="import-tokens-modal__custom-token-form"
|
||||||
|
>
|
||||||
|
{tokenDetectionInactiveOnNonMainnetSupportedNetwork ? (
|
||||||
|
<BannerAlert severity={Severity.Warning}>
|
||||||
|
{t(
|
||||||
|
'customTokenWarningInTokenDetectionNetworkWithTDOFF',
|
||||||
|
[
|
||||||
|
<ButtonLink
|
||||||
|
key="import-token-security-risk"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href={ZENDESK_URLS.TOKEN_SAFETY_PRACTICES}
|
||||||
|
>
|
||||||
|
{t('tokenScamSecurityRisk')}
|
||||||
|
</ButtonLink>,
|
||||||
|
<ButtonLink
|
||||||
|
type="link"
|
||||||
|
key="import-token-token-detection-announcement"
|
||||||
|
onClick={() =>
|
||||||
|
history.push(
|
||||||
|
`${SECURITY_ROUTE}#advanced-settings-autodetect-tokens`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('inYourSettings')}
|
||||||
|
</ButtonLink>,
|
||||||
|
],
|
||||||
|
)}
|
||||||
|
</BannerAlert>
|
||||||
|
) : (
|
||||||
|
<BannerAlert
|
||||||
|
severity={
|
||||||
|
isDynamicTokenListAvailable
|
||||||
|
? Severity.Warning
|
||||||
|
: Severity.Info
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
isDynamicTokenListAvailable
|
||||||
|
? 'customTokenWarningInTokenDetectionNetwork'
|
||||||
|
: 'customTokenWarningInNonTokenDetectionNetwork',
|
||||||
|
[
|
||||||
|
<ButtonLink
|
||||||
|
key="import-token-fake-token-warning"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href={ZENDESK_URLS.TOKEN_SAFETY_PRACTICES}
|
||||||
|
>
|
||||||
|
{t('learnScamRisk')}
|
||||||
|
</ButtonLink>,
|
||||||
|
],
|
||||||
|
)}
|
||||||
|
</BannerAlert>
|
||||||
|
)}
|
||||||
|
<FormTextField
|
||||||
|
label={t('tokenContractAddress')}
|
||||||
|
value={customAddress}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleCustomAddressChange(e.target.value)
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
customAddressError ||
|
||||||
|
mainnetTokenWarning ||
|
||||||
|
nftAddressError
|
||||||
|
}
|
||||||
|
error={
|
||||||
|
customAddressError ||
|
||||||
|
mainnetTokenWarning ||
|
||||||
|
nftAddressError
|
||||||
|
}
|
||||||
|
autoFocus
|
||||||
|
marginTop={6}
|
||||||
|
inputProps={{
|
||||||
|
'data-testid': 'import-tokens-modal-custom-address',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormTextField
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
{t('tokenSymbol')}
|
||||||
|
{symbolAutoFilled && !forceEditSymbol && (
|
||||||
|
<ButtonLink
|
||||||
|
onClick={() => setForceEditSymbol(true)}
|
||||||
|
textAlign={TextAlign.End}
|
||||||
|
paddingInlineEnd={1}
|
||||||
|
paddingInlineStart={1}
|
||||||
|
color={TextColor.primaryDefault}
|
||||||
|
>
|
||||||
|
{t('edit')}
|
||||||
|
</ButtonLink>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
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',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormTextField
|
||||||
|
label={t('decimal')}
|
||||||
|
type="number"
|
||||||
|
value={customDecimals}
|
||||||
|
onChange={(e) =>
|
||||||
|
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 === '' && (
|
||||||
|
<BannerAlert severity={Severity.Warning}>
|
||||||
|
<Text fontWeight={FontWeight.Bold}>
|
||||||
|
{t('tokenDecimalFetchFailed')}
|
||||||
|
</Text>
|
||||||
|
{t('verifyThisTokenDecimalOn', [
|
||||||
|
<ButtonLink
|
||||||
|
key="import-token-verify-token-decimal"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href={blockExplorerTokenLink}
|
||||||
|
>
|
||||||
|
{blockExplorerLabel}
|
||||||
|
</ButtonLink>,
|
||||||
|
])}
|
||||||
|
</BannerAlert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
<Box paddingTop={6} paddingBottom={6}>
|
||||||
|
<ButtonPrimary
|
||||||
|
onClick={() => handleNext()}
|
||||||
|
size={Size.LG}
|
||||||
|
disabled={Boolean(hasError()) || !hasSelected()}
|
||||||
|
block
|
||||||
|
>
|
||||||
|
{t('next')}
|
||||||
|
</ButtonPrimary>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ImportTokensModal.propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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) => <ImportTokensModal {...args} />;
|
||||||
|
DefaultStory.decorators = [
|
||||||
|
(Story) => (
|
||||||
|
<Provider store={createStore()}>
|
||||||
|
<Story />
|
||||||
|
</Provider>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default';
|
||||||
|
|
||||||
|
export const CustomImportOnlyStory = (args) => <ImportTokensModal {...args} />;
|
||||||
|
CustomImportOnlyStory.decorators = [
|
||||||
|
(Story) => (
|
||||||
|
<Provider store={createStore(CHAIN_IDS.GOERLI)}>
|
||||||
|
<Story />
|
||||||
|
</Provider>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
CustomImportOnlyStory.storyName = 'Custom Import Only';
|
||||||
|
|
||||||
|
export const TokenDetectionDisabledStory = (args) => (
|
||||||
|
<ImportTokensModal {...args} />
|
||||||
|
);
|
||||||
|
TokenDetectionDisabledStory.decorators = [
|
||||||
|
(Story) => (
|
||||||
|
<Provider store={createStore(CHAIN_IDS.MAINNET, false)}>
|
||||||
|
<Story />
|
||||||
|
</Provider>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
TokenDetectionDisabledStory.storyName = 'Token Detection Disabled';
|
@ -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(<ImportTokensModal onClose={onClose} />, 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
1
ui/components/multichain/import-tokens-modal/index.js
Normal file
1
ui/components/multichain/import-tokens-modal/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { ImportTokensModal } from './import-tokens-modal';
|
@ -18,3 +18,4 @@ export { CreateAccount } from './create-account';
|
|||||||
export { ImportAccount } from './import-account';
|
export { ImportAccount } from './import-account';
|
||||||
export { ImportNftsModal } from './import-nfts-modal';
|
export { ImportNftsModal } from './import-nfts-modal';
|
||||||
export { AccountDetailsMenuItem, ViewExplorerMenuItem } from './menu-items';
|
export { AccountDetailsMenuItem, ViewExplorerMenuItem } from './menu-items';
|
||||||
|
export { ImportTokensModal } from './import-tokens-modal';
|
||||||
|
@ -19,3 +19,4 @@
|
|||||||
@import 'network-list-menu/';
|
@import 'network-list-menu/';
|
||||||
@import 'product-tour-popover/product-tour-popover';
|
@import 'product-tour-popover/product-tour-popover';
|
||||||
@import 'nft-item/nft-item';
|
@import 'nft-item/nft-item';
|
||||||
|
@import 'import-tokens-modal/import-tokens-modal'
|
||||||
|
@ -88,19 +88,6 @@ export default class PageContainer extends PureComponent {
|
|||||||
return null;
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
@ -118,7 +105,6 @@ export default class PageContainer extends PureComponent {
|
|||||||
headerCloseText,
|
headerCloseText,
|
||||||
hideCancel,
|
hideCancel,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const tabSubmitText = this.getTabSubmitText();
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
<PageContainerHeader
|
<PageContainerHeader
|
||||||
@ -139,7 +125,7 @@ export default class PageContainer extends PureComponent {
|
|||||||
cancelText={cancelText}
|
cancelText={cancelText}
|
||||||
hideCancel={hideCancel}
|
hideCancel={hideCancel}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
submitText={tabSubmitText || submitText}
|
submitText={submitText}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,4 +90,8 @@
|
|||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
box-shadow: var(--shadow-size-lg) var(--color-shadow-default);
|
box-shadow: var(--shadow-size-lg) var(--color-shadow-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-container .page-container {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ interface AppState {
|
|||||||
networkDropdownOpen: boolean;
|
networkDropdownOpen: boolean;
|
||||||
importNftsModalOpen: boolean;
|
importNftsModalOpen: boolean;
|
||||||
showIpfsModalOpen: boolean;
|
showIpfsModalOpen: boolean;
|
||||||
|
importTokensModalOpen: boolean;
|
||||||
accountDetail: {
|
accountDetail: {
|
||||||
subview?: string;
|
subview?: string;
|
||||||
accountExport?: string;
|
accountExport?: string;
|
||||||
@ -98,6 +99,7 @@ const initialState: AppState = {
|
|||||||
networkDropdownOpen: false,
|
networkDropdownOpen: false,
|
||||||
importNftsModalOpen: false,
|
importNftsModalOpen: false,
|
||||||
showIpfsModalOpen: false,
|
showIpfsModalOpen: false,
|
||||||
|
importTokensModalOpen: false,
|
||||||
accountDetail: {
|
accountDetail: {
|
||||||
privateKey: '',
|
privateKey: '',
|
||||||
},
|
},
|
||||||
@ -191,6 +193,18 @@ export default function reduceApp(
|
|||||||
showIpfsModalOpen: false,
|
showIpfsModalOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case actionConstants.IMPORT_TOKENS_POPOVER_OPEN:
|
||||||
|
return {
|
||||||
|
...appState,
|
||||||
|
importTokensModalOpen: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case actionConstants.IMPORT_TOKENS_POPOVER_CLOSE:
|
||||||
|
return {
|
||||||
|
...appState,
|
||||||
|
importTokensModalOpen: false,
|
||||||
|
};
|
||||||
|
|
||||||
// alert methods
|
// alert methods
|
||||||
case actionConstants.ALERT_OPEN:
|
case actionConstants.ALERT_OPEN:
|
||||||
return {
|
return {
|
||||||
|
@ -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 (
|
|
||||||
<div className="page-container">
|
|
||||||
<div className="page-container__header">
|
|
||||||
<div className="page-container__title">
|
|
||||||
{t('importTokensCamelCase')}
|
|
||||||
</div>
|
|
||||||
<div className="page-container__subtitle">
|
|
||||||
{t('likeToImportTokens')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="page-container__content">
|
|
||||||
<div className="confirm-import-token">
|
|
||||||
<div className="confirm-import-token__header">
|
|
||||||
<div className="confirm-import-token__token">{t('token')}</div>
|
|
||||||
<div className="confirm-import-token__balance">{t('balance')}</div>
|
|
||||||
</div>
|
|
||||||
<div className="confirm-import-token__token-list">
|
|
||||||
{Object.entries(pendingTokens).map(([address, token]) => {
|
|
||||||
const { name, symbol } = token;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="confirm-import-token__token-list-item"
|
|
||||||
key={address}
|
|
||||||
>
|
|
||||||
<div className="confirm-import-token__token confirm-import-token__data">
|
|
||||||
<Identicon
|
|
||||||
className="confirm-import-token__token-icon"
|
|
||||||
diameter={48}
|
|
||||||
address={address}
|
|
||||||
/>
|
|
||||||
<div className="confirm-import-token__name">
|
|
||||||
{getTokenName(name, symbol)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="confirm-import-token__balance">
|
|
||||||
<TokenBalance token={token} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="page-container__footer">
|
|
||||||
<footer>
|
|
||||||
<Button
|
|
||||||
type="secondary"
|
|
||||||
large
|
|
||||||
className="page-container__footer-button"
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(clearPendingTokens());
|
|
||||||
history.push(IMPORT_TOKEN_ROUTE);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('back')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
large
|
|
||||||
className="page-container__footer-button"
|
|
||||||
onClick={handleAddTokens}
|
|
||||||
>
|
|
||||||
{t('importTokensCamelCase')}
|
|
||||||
</Button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConfirmImportToken;
|
|
@ -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 (
|
|
||||||
<PageSet pendingTokens={pendingTokens}>
|
|
||||||
<ConfirmAddToken />
|
|
||||||
</PageSet>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
DefaultStory.args = {
|
|
||||||
pendingTokens: { ...tokens },
|
|
||||||
};
|
|
||||||
DefaultStory.storyName = 'Default';
|
|
@ -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(<ConfirmImportToken />, 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}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,3 +0,0 @@
|
|||||||
import ConfirmImportToken from './confirm-import-token';
|
|
||||||
|
|
||||||
export default ConfirmImportToken;
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 = () => <code>{configureStore(testData).getState().metamask.selectedAddress}</code>;
|
|
||||||
|
|
||||||
# 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`
|
|
||||||
|
|
||||||
<Canvas>
|
|
||||||
<Story id="pages-swaps-importtoken--default-story" />
|
|
||||||
</Canvas>
|
|
||||||
|
|
||||||
## 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 <PersonalAddress/> in the `Token Contract Address` field.
|
|
||||||
|
|
||||||
## Props
|
|
||||||
|
|
||||||
<ArgsTable of={ImportToken} />
|
|
@ -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', [
|
|
||||||
<a
|
|
||||||
className="import-token__nft-address-error-link"
|
|
||||||
onClick={() => {
|
|
||||||
this.props.showImportNftsModal();
|
|
||||||
}}
|
|
||||||
key="nftAddressError"
|
|
||||||
>
|
|
||||||
{this.context.t('importNFTPage')}
|
|
||||||
</a>,
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
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 (
|
|
||||||
<div className="import-token__custom-token-form">
|
|
||||||
{tokenDetectionInactiveOnNonMainnetSupportedNetwork ? (
|
|
||||||
<ActionableMessage
|
|
||||||
type="warning"
|
|
||||||
message={t('customTokenWarningInTokenDetectionNetworkWithTDOFF', [
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
key="import-token-security-risk"
|
|
||||||
className="import-token__link"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
href={ZENDESK_URLS.TOKEN_SAFETY_PRACTICES}
|
|
||||||
>
|
|
||||||
{t('tokenScamSecurityRisk')}
|
|
||||||
</Button>,
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
key="import-token-token-detection-announcement"
|
|
||||||
className="import-token__link"
|
|
||||||
onClick={() =>
|
|
||||||
history.push(`${SECURITY_ROUTE}#token-description`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('inYourSettings')}
|
|
||||||
</Button>,
|
|
||||||
])}
|
|
||||||
withRightButton
|
|
||||||
useIcon
|
|
||||||
iconFillColor="var(--color-warning-default)"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ActionableMessage
|
|
||||||
type={isDynamicTokenListAvailable ? 'warning' : 'default'}
|
|
||||||
message={t(
|
|
||||||
isDynamicTokenListAvailable
|
|
||||||
? 'customTokenWarningInTokenDetectionNetwork'
|
|
||||||
: 'customTokenWarningInNonTokenDetectionNetwork',
|
|
||||||
[
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
key="import-token-fake-token-warning"
|
|
||||||
className="import-token__link"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
href={ZENDESK_URLS.TOKEN_SAFETY_PRACTICES}
|
|
||||||
>
|
|
||||||
{t('learnScamRisk')}
|
|
||||||
</Button>,
|
|
||||||
],
|
|
||||||
)}
|
|
||||||
withRightButton
|
|
||||||
useIcon
|
|
||||||
iconFillColor={
|
|
||||||
isDynamicTokenListAvailable
|
|
||||||
? 'var(--color-warning-default)'
|
|
||||||
: 'var(--color-info-default)'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TextField
|
|
||||||
id="custom-address"
|
|
||||||
label={t('tokenContractAddress')}
|
|
||||||
type="text"
|
|
||||||
value={customAddress}
|
|
||||||
onChange={(e) => this.handleCustomAddressChange(e.target.value)}
|
|
||||||
error={customAddressError || mainnetTokenWarning || nftAddressError}
|
|
||||||
fullWidth
|
|
||||||
autoFocus
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
id="custom-symbol"
|
|
||||||
label={
|
|
||||||
<div className="import-token__custom-symbol__label-wrapper">
|
|
||||||
<span className="import-token__custom-symbol__label">
|
|
||||||
{t('tokenSymbol')}
|
|
||||||
</span>
|
|
||||||
{symbolAutoFilled && !forceEditSymbol && (
|
|
||||||
<div
|
|
||||||
className="import-token__custom-symbol__edit"
|
|
||||||
onClick={() => this.setState({ forceEditSymbol: true })}
|
|
||||||
>
|
|
||||||
{t('edit')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
type="text"
|
|
||||||
value={customSymbol}
|
|
||||||
onChange={(e) => this.handleCustomSymbolChange(e.target.value)}
|
|
||||||
error={customSymbolError}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
disabled={symbolAutoFilled && !forceEditSymbol}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
id="custom-decimals"
|
|
||||||
label={t('decimal')}
|
|
||||||
type="number"
|
|
||||||
value={customDecimals}
|
|
||||||
onChange={(e) => this.handleCustomDecimalsChange(e.target.value)}
|
|
||||||
error={customDecimals ? customDecimalsError : null}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
disabled={decimalAutoFilled}
|
|
||||||
min={MIN_DECIMAL_VALUE}
|
|
||||||
max={MAX_DECIMAL_VALUE}
|
|
||||||
/>
|
|
||||||
{customDecimals === '' && (
|
|
||||||
<ActionableMessage
|
|
||||||
message={
|
|
||||||
<>
|
|
||||||
<Typography
|
|
||||||
variant={TypographyVariant.H7}
|
|
||||||
fontWeight={FONT_WEIGHT.BOLD}
|
|
||||||
>
|
|
||||||
{t('tokenDecimalFetchFailed')}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant={TypographyVariant.H7}
|
|
||||||
fontWeight={FONT_WEIGHT.NORMAL}
|
|
||||||
>
|
|
||||||
{t('verifyThisTokenDecimalOn', [
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
key="import-token-verify-token-decimal"
|
|
||||||
className="import-token__link"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
href={blockExplorerTokenLink}
|
|
||||||
>
|
|
||||||
{blockExplorerLabel}
|
|
||||||
</Button>,
|
|
||||||
])}
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
type="warning"
|
|
||||||
withRightButton
|
|
||||||
className="import-token__decimal-warning"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSearchToken() {
|
|
||||||
const { t } = this.context;
|
|
||||||
const { tokenList, history, useTokenDetection, networkName } = this.props;
|
|
||||||
const { tokenSelectorError, selectedTokens, searchResults } = this.state;
|
|
||||||
return (
|
|
||||||
<div className="import-token__search-token">
|
|
||||||
{!useTokenDetection && (
|
|
||||||
<ActionableMessage
|
|
||||||
message={t('enhancedTokenDetectionAlertMessage', [
|
|
||||||
networkName,
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
key="token-detection-announcement"
|
|
||||||
className="import-token__link"
|
|
||||||
onClick={() =>
|
|
||||||
history.push(`${SECURITY_ROUTE}#token-description`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('enableFromSettings')}
|
|
||||||
</Button>,
|
|
||||||
])}
|
|
||||||
withRightButton
|
|
||||||
useIcon
|
|
||||||
iconFillColor="var(--color-primary-default)"
|
|
||||||
className="import-token__token-detection-announcement"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TokenSearch
|
|
||||||
onSearch={({ results = [] }) =>
|
|
||||||
this.setState({ searchResults: results })
|
|
||||||
}
|
|
||||||
error={tokenSelectorError}
|
|
||||||
tokenList={tokenList}
|
|
||||||
/>
|
|
||||||
<div className="import-token__token-list">
|
|
||||||
<TokenList
|
|
||||||
results={searchResults}
|
|
||||||
selectedTokens={selectedTokens}
|
|
||||||
onToggleToken={(token) => this.handleToggleToken(token)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTabs() {
|
|
||||||
const { t } = this.context;
|
|
||||||
const { showSearchTab } = this.props;
|
|
||||||
const tabs = [];
|
|
||||||
|
|
||||||
if (showSearchTab) {
|
|
||||||
tabs.push(
|
|
||||||
<Tab name={t('search')} key="search-tab" tabKey="search">
|
|
||||||
{this.renderSearchToken()}
|
|
||||||
</Tab>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
tabs.push(
|
|
||||||
<Tab name={t('customToken')} key="custom-tab" tabKey="customToken">
|
|
||||||
{this.renderCustomTokenForm()}
|
|
||||||
</Tab>,
|
|
||||||
);
|
|
||||||
|
|
||||||
return <Tabs>{tabs}</Tabs>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { history, clearPendingTokens, mostRecentOverviewPage } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer
|
|
||||||
title={this.context.t('importTokensCamelCase')}
|
|
||||||
tabsComponent={this.renderTabs()}
|
|
||||||
onSubmit={() => this.handleNext()}
|
|
||||||
hideCancel
|
|
||||||
disabled={Boolean(this.hasError()) || !this.hasSelected()}
|
|
||||||
onClose={() => {
|
|
||||||
clearPendingTokens();
|
|
||||||
history.push(mostRecentOverviewPage);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ImportToken;
|
|
@ -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);
|
|
@ -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) => <Provider store={store}>{story()}</Provider>],
|
|
||||||
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 <ImportToken {...args} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
DefaultStory.storyName = 'Default';
|
|
@ -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(<ImportToken {...props} />, 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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,3 +0,0 @@
|
|||||||
import ImportToken from './import-token.container';
|
|
||||||
|
|
||||||
export default ImportToken;
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 (
|
|
||||||
<div className="token-list-placeholder">
|
|
||||||
<IconTokenSearch size={64} color="var(--color-icon-muted)" />
|
|
||||||
<div className="token-list-placeholder__text">
|
|
||||||
{this.context.t('addAcquiredTokens')}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
className="token-list-placeholder__link"
|
|
||||||
href={ZENDESK_URLS.ADD_CUSTOM_TOKENS}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{this.context.t('learnMoreUpperCase')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
/** Please import your files in alphabetical order **/
|
/** Please import your files in alphabetical order **/
|
||||||
@import 'asset/asset';
|
@import 'asset/asset';
|
||||||
@import 'confirm-import-token/index';
|
|
||||||
@import 'confirm-add-suggested-token/index';
|
@import 'confirm-add-suggested-token/index';
|
||||||
@import 'confirm-add-suggested-nft/index';
|
@import 'confirm-add-suggested-nft/index';
|
||||||
@import 'confirm-approve/index';
|
@import 'confirm-approve/index';
|
||||||
@ -15,7 +14,6 @@
|
|||||||
@import 'desktop-pairing/index';
|
@import 'desktop-pairing/index';
|
||||||
@import 'error/index';
|
@import 'error/index';
|
||||||
@import 'home/index';
|
@import 'home/index';
|
||||||
@import 'import-token/index';
|
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
||||||
@import "institutional/connect-custody/index";
|
@import "institutional/connect-custody/index";
|
||||||
@import "institutional/institutional-entity-done-page/index";
|
@import "institutional/institutional-entity-done-page/index";
|
||||||
|
@ -7,6 +7,7 @@ import IdleTimer from 'react-idle-timer';
|
|||||||
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
||||||
import browserAPI from 'webextension-polyfill';
|
import browserAPI from 'webextension-polyfill';
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
|
||||||
import SendTransactionScreen from '../send';
|
import SendTransactionScreen from '../send';
|
||||||
import Swaps from '../swaps';
|
import Swaps from '../swaps';
|
||||||
import ConfirmTransaction from '../confirm-transaction';
|
import ConfirmTransaction from '../confirm-transaction';
|
||||||
@ -18,8 +19,6 @@ import Lock from '../lock';
|
|||||||
import PermissionsConnect from '../permissions-connect';
|
import PermissionsConnect from '../permissions-connect';
|
||||||
import RestoreVaultPage from '../keychains/restore-vault';
|
import RestoreVaultPage from '../keychains/restore-vault';
|
||||||
import RevealSeedConfirmation from '../keychains/reveal-seed';
|
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 ConfirmAddSuggestedTokenPage from '../confirm-add-suggested-token';
|
||||||
import CreateAccountPage from '../create-account/create-account.component';
|
import CreateAccountPage from '../create-account/create-account.component';
|
||||||
import ConfirmAddSuggestedNftPage from '../confirm-add-suggested-nft';
|
import ConfirmAddSuggestedNftPage from '../confirm-add-suggested-nft';
|
||||||
@ -33,6 +32,7 @@ import {
|
|||||||
NetworkListMenu,
|
NetworkListMenu,
|
||||||
AccountDetails,
|
AccountDetails,
|
||||||
ImportNftsModal,
|
ImportNftsModal,
|
||||||
|
ImportTokensModal,
|
||||||
} from '../../components/multichain';
|
} from '../../components/multichain';
|
||||||
import UnlockPage from '../unlock-page';
|
import UnlockPage from '../unlock-page';
|
||||||
import Alerts from '../../components/app/alerts';
|
import Alerts from '../../components/app/alerts';
|
||||||
@ -59,7 +59,6 @@ import CustodyPage from '../institutional/custody';
|
|||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IMPORT_TOKEN_ROUTE,
|
|
||||||
ASSET_ROUTE,
|
ASSET_ROUTE,
|
||||||
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
|
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
|
||||||
CONFIRM_ADD_SUGGESTED_NFT_ROUTE,
|
CONFIRM_ADD_SUGGESTED_NFT_ROUTE,
|
||||||
@ -76,7 +75,6 @@ import {
|
|||||||
UNLOCK_ROUTE,
|
UNLOCK_ROUTE,
|
||||||
BUILD_QUOTE_ROUTE,
|
BUILD_QUOTE_ROUTE,
|
||||||
CONFIRMATION_V_NEXT_ROUTE,
|
CONFIRMATION_V_NEXT_ROUTE,
|
||||||
CONFIRM_IMPORT_TOKEN_ROUTE,
|
|
||||||
ONBOARDING_ROUTE,
|
ONBOARDING_ROUTE,
|
||||||
ONBOARDING_UNLOCK_ROUTE,
|
ONBOARDING_UNLOCK_ROUTE,
|
||||||
TOKEN_DETAILS,
|
TOKEN_DETAILS,
|
||||||
@ -162,6 +160,8 @@ export default class Routes extends Component {
|
|||||||
hideImportNftsModal: PropTypes.func.isRequired,
|
hideImportNftsModal: PropTypes.func.isRequired,
|
||||||
isIpfsModalOpen: PropTypes.bool.isRequired,
|
isIpfsModalOpen: PropTypes.bool.isRequired,
|
||||||
hideIpfsModal: PropTypes.func.isRequired,
|
hideIpfsModal: PropTypes.func.isRequired,
|
||||||
|
isImportTokensModalOpen: PropTypes.bool.isRequired,
|
||||||
|
hideImportTokensModal: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -279,16 +279,6 @@ export default class Routes extends Component {
|
|||||||
exact
|
exact
|
||||||
/>
|
/>
|
||||||
<Authenticated path={SWAPS_ROUTE} component={Swaps} />
|
<Authenticated path={SWAPS_ROUTE} component={Swaps} />
|
||||||
<Authenticated
|
|
||||||
path={IMPORT_TOKEN_ROUTE}
|
|
||||||
component={ImportTokenPage}
|
|
||||||
exact
|
|
||||||
/>
|
|
||||||
<Authenticated
|
|
||||||
path={CONFIRM_IMPORT_TOKEN_ROUTE}
|
|
||||||
component={ConfirmImportTokenPage}
|
|
||||||
exact
|
|
||||||
/>
|
|
||||||
<Authenticated
|
<Authenticated
|
||||||
path={CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE}
|
path={CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE}
|
||||||
component={ConfirmAddSuggestedTokenPage}
|
component={ConfirmAddSuggestedTokenPage}
|
||||||
@ -515,12 +505,15 @@ export default class Routes extends Component {
|
|||||||
isNetworkMenuOpen,
|
isNetworkMenuOpen,
|
||||||
toggleNetworkMenu,
|
toggleNetworkMenu,
|
||||||
accountDetailsAddress,
|
accountDetailsAddress,
|
||||||
|
isImportTokensModalOpen,
|
||||||
location,
|
location,
|
||||||
isImportNftsModalOpen,
|
isImportNftsModalOpen,
|
||||||
hideImportNftsModal,
|
hideImportNftsModal,
|
||||||
isIpfsModalOpen,
|
isIpfsModalOpen,
|
||||||
hideIpfsModal,
|
hideIpfsModal,
|
||||||
|
hideImportTokensModal,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const loadMessage =
|
const loadMessage =
|
||||||
loadingMessage || isNetworkLoading
|
loadingMessage || isNetworkLoading
|
||||||
? this.getConnectingLabel(loadingMessage)
|
? this.getConnectingLabel(loadingMessage)
|
||||||
@ -584,6 +577,9 @@ export default class Routes extends Component {
|
|||||||
{isIpfsModalOpen ? (
|
{isIpfsModalOpen ? (
|
||||||
<ToggleIpfsModal onClose={() => hideIpfsModal()} />
|
<ToggleIpfsModal onClose={() => hideIpfsModal()} />
|
||||||
) : null}
|
) : null}
|
||||||
|
{isImportTokensModalOpen ? (
|
||||||
|
<ImportTokensModal onClose={() => hideImportTokensModal()} />
|
||||||
|
) : null}
|
||||||
<Box className="main-container-wrapper">
|
<Box className="main-container-wrapper">
|
||||||
{isLoading ? <Loading loadingMessage={loadMessage} /> : null}
|
{isLoading ? <Loading loadingMessage={loadMessage} /> : null}
|
||||||
{!isLoading && isNetworkLoading ? <LoadingNetwork /> : null}
|
{!isLoading && isNetworkLoading ? <LoadingNetwork /> : null}
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
isCurrentProviderCustom,
|
isCurrentProviderCustom,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import {
|
import {
|
||||||
|
hideImportTokensModal,
|
||||||
lockMetamask,
|
lockMetamask,
|
||||||
hideImportNftsModal,
|
hideImportNftsModal,
|
||||||
hideIpfsModal,
|
hideIpfsModal,
|
||||||
@ -64,6 +65,7 @@ function mapStateToProps(state) {
|
|||||||
completedOnboarding,
|
completedOnboarding,
|
||||||
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
|
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
|
||||||
isNetworkMenuOpen: state.metamask.isNetworkMenuOpen,
|
isNetworkMenuOpen: state.metamask.isNetworkMenuOpen,
|
||||||
|
isImportTokensModalOpen: state.appState.importTokensModalOpen,
|
||||||
accountDetailsAddress: state.appState.accountDetailsAddress,
|
accountDetailsAddress: state.appState.accountDetailsAddress,
|
||||||
isImportNftsModalOpen: state.appState.importNftsModalOpen,
|
isImportNftsModalOpen: state.appState.importNftsModalOpen,
|
||||||
isIpfsModalOpen: state.appState.showIpfsModalOpen,
|
isIpfsModalOpen: state.appState.showIpfsModalOpen,
|
||||||
@ -83,6 +85,7 @@ function mapDispatchToProps(dispatch) {
|
|||||||
toggleNetworkMenu: () => dispatch(toggleNetworkMenu()),
|
toggleNetworkMenu: () => dispatch(toggleNetworkMenu()),
|
||||||
hideImportNftsModal: () => dispatch(hideImportNftsModal()),
|
hideImportNftsModal: () => dispatch(hideImportNftsModal()),
|
||||||
hideIpfsModal: () => dispatch(hideIpfsModal()),
|
hideIpfsModal: () => dispatch(hideIpfsModal()),
|
||||||
|
hideImportTokensModal: () => dispatch(hideImportTokensModal()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -627,6 +627,7 @@ exports[`Security Tab should match snapshot 1`] = `
|
|||||||
<div
|
<div
|
||||||
class="mm-box settings-page__content-row mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between"
|
class="mm-box settings-page__content-row mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between"
|
||||||
data-testid="advanced-setting-gas-fee-estimation"
|
data-testid="advanced-setting-gas-fee-estimation"
|
||||||
|
id="advanced-settings-autodetect-tokens"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="settings-page__content-item"
|
class="settings-page__content-item"
|
||||||
|
@ -531,6 +531,7 @@ export default class SecurityTab extends PureComponent {
|
|||||||
display={Display.Flex}
|
display={Display.Flex}
|
||||||
flexDirection={FlexDirection.Row}
|
flexDirection={FlexDirection.Row}
|
||||||
justifyContent={JustifyContent.spaceBetween}
|
justifyContent={JustifyContent.spaceBetween}
|
||||||
|
id="advanced-settings-autodetect-tokens"
|
||||||
>
|
>
|
||||||
<div className="settings-page__content-item">
|
<div className="settings-page__content-item">
|
||||||
<span>{t('autoDetectTokens')}</span>
|
<span>{t('autoDetectTokens')}</span>
|
||||||
|
@ -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 IMPORT_NFTS_MODAL_CLOSE = 'UI_IMPORT_NFTS_MODAL_CLOSE';
|
||||||
export const SHOW_IPFS_MODAL_OPEN = 'UI_IPFS_MODAL_OPEN';
|
export const SHOW_IPFS_MODAL_OPEN = 'UI_IPFS_MODAL_OPEN';
|
||||||
export const SHOW_IPFS_MODAL_CLOSE = 'UI_IPFS_MODAL_CLOSE';
|
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
|
// remote state
|
||||||
export const UPDATE_METAMASK_STATE = 'UPDATE_METAMASK_STATE';
|
export const UPDATE_METAMASK_STATE = 'UPDATE_METAMASK_STATE';
|
||||||
export const SELECTED_ADDRESS_CHANGED = 'SELECTED_ADDRESS_CHANGED';
|
export const SELECTED_ADDRESS_CHANGED = 'SELECTED_ADDRESS_CHANGED';
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user