mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Network tab refactor (#12502)
This commit is contained in:
parent
e3e6da1a75
commit
524725b24b
@ -665,12 +665,6 @@
|
||||
"on": {
|
||||
"message": "በርቷል"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "ኤክስፕሎረር URL አግድ (አማራጭ)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "ምልክት (አማራጭ)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "መነሻ"
|
||||
},
|
||||
|
@ -661,12 +661,6 @@
|
||||
"on": {
|
||||
"message": "تشغيل"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "العنوان الإلكتروني لمستكشف البلوكات (اختياري)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "الرمز (اختياري)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "الأصل"
|
||||
},
|
||||
|
@ -664,12 +664,6 @@
|
||||
"on": {
|
||||
"message": "Включено"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Блокиране на Explorer URL (по избор)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Символ (по избор)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Произход"
|
||||
},
|
||||
|
@ -668,12 +668,6 @@
|
||||
"on": {
|
||||
"message": "চালু"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "এক্সপ্লোরার URL ব্লক করুন (ঐচ্ছিক)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "প্রতীক (ঐচ্ছিক)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "উৎস"
|
||||
},
|
||||
|
@ -652,12 +652,6 @@
|
||||
"on": {
|
||||
"message": "Activat"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Bloqueja l'URL d'Explorer (opcional)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Símbol (opcional)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Origen"
|
||||
},
|
||||
|
@ -652,12 +652,6 @@
|
||||
"on": {
|
||||
"message": "Til"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Blok-stifinder-URL (valgfrit)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Symbol (valgfrit)"
|
||||
},
|
||||
"parameters": {
|
||||
"message": "Parametre"
|
||||
},
|
||||
|
@ -644,9 +644,6 @@
|
||||
"on": {
|
||||
"message": "An"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Block-Explorer-URL (optional)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Ursprung"
|
||||
},
|
||||
|
@ -665,12 +665,6 @@
|
||||
"on": {
|
||||
"message": "Ενεργό"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Διεύθυνση URL Εξερευνητή Μπλοκ (προαιρετικό)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Σύμβολο (προαιρετικό)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Προέλευση"
|
||||
},
|
||||
|
@ -1767,11 +1767,8 @@
|
||||
"optional": {
|
||||
"message": "Optional"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Block Explorer URL (optional)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Currency Symbol (optional)"
|
||||
"optionalWithParanthesis": {
|
||||
"message": "(Optional)"
|
||||
},
|
||||
"or": {
|
||||
"message": "or"
|
||||
|
@ -1321,12 +1321,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "Conéctese solo con sitios de confianza."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Dirección URL del explorador de bloques (opcional)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Símbolo de moneda (opcional)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Origen"
|
||||
},
|
||||
|
@ -1321,12 +1321,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "Conéctese solo con sitios de confianza."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Dirección URL del explorador de bloques (opcional)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Símbolo de moneda (opcional)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Origen"
|
||||
},
|
||||
|
@ -658,12 +658,6 @@
|
||||
"on": {
|
||||
"message": "Sees"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Blokeeri Exploreri URL (valikuline)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Sümbol (valikuline)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Päritolu"
|
||||
},
|
||||
|
@ -668,12 +668,6 @@
|
||||
"on": {
|
||||
"message": "روشن"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "بلاک کردن مرورگر URL (انتخابی)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "سمبول (انتخابی)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "مبدأ"
|
||||
},
|
||||
|
@ -665,12 +665,6 @@
|
||||
"on": {
|
||||
"message": "Käytössä"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Estä Explorerin URL-osoite (valinnainen)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Symboli (valinnainen)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Alkuperä"
|
||||
},
|
||||
|
@ -602,12 +602,6 @@
|
||||
"on": {
|
||||
"message": "Naka-on"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Block Explorer URL (opsyonal)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbolo (opsyonal)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Pinanggalingan"
|
||||
},
|
||||
|
@ -650,12 +650,6 @@
|
||||
"on": {
|
||||
"message": "Activé"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "URL de l'explorateur de blocs (facultatif)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Symbole (facultatif)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Origine"
|
||||
},
|
||||
|
@ -665,12 +665,6 @@
|
||||
"on": {
|
||||
"message": "פועל"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "חסום כתובת URL של אקספלורר (אופציונלי)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "סמל (אופציונלי)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "מקור"
|
||||
},
|
||||
|
@ -1321,12 +1321,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "केवल उन साइटों से कनेक्ट करें, जिन पर आप भरोसा करते हैं।"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "ब्लॉक एक्सप्लोरर URL (वैकल्पिक)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "मुद्रा प्रतीक (वैकल्पिक)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "उत्पत्ति"
|
||||
},
|
||||
|
@ -661,12 +661,6 @@
|
||||
"on": {
|
||||
"message": "Uključi"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Blokiraj Explorerov URL (neobavezno)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbol (neobavezno)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Podrijetlo"
|
||||
},
|
||||
|
@ -661,12 +661,6 @@
|
||||
"on": {
|
||||
"message": "Be"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Explorer URL letiltása (nem kötelező)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Szimbólum (opcionális)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Eredet"
|
||||
},
|
||||
|
@ -1321,12 +1321,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "Hanya hubungkan ke situs yang Anda percayai."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "URL Block Explorer (opsional)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbol Mata Uang (opsional)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Asal"
|
||||
},
|
||||
|
@ -1080,12 +1080,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "Connettiti solo con siti di cui ti fidi."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "URL del Block Explorer (opzionale)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbolo (opzionale)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Origine"
|
||||
},
|
||||
|
@ -1321,12 +1321,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "信頼するサイトにのみ接続します。"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "ブロック エクスプローラーの URL (オプション)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "通貨記号 (オプション)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "起点"
|
||||
},
|
||||
|
@ -668,12 +668,6 @@
|
||||
"on": {
|
||||
"message": "ಆನ್"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "ಅನ್ವೇಷಕ URL ಅನ್ನು ನಿರ್ಬಂಧಿಸಿ (ಐಚ್ಛಿಕ)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "ಚಿಹ್ನೆ (ಐಚ್ಛಿಕ)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "ಮೂಲ"
|
||||
},
|
||||
|
@ -1321,12 +1321,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "신뢰하는 사이트만 연결하세요."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "블록 탐색기 URL(선택 사항)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "통화 기호(선택 사항)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "원본"
|
||||
},
|
||||
|
@ -668,12 +668,6 @@
|
||||
"on": {
|
||||
"message": "Įjungta"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Blokuoti naršyklės URL (pasirinktinai)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbolis (nebūtinas)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Kilmė"
|
||||
},
|
||||
|
@ -664,12 +664,6 @@
|
||||
"on": {
|
||||
"message": "Iesl."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Bloķēt Explorer URL (pēc izvēles)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbols (neobligāti)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Avots"
|
||||
},
|
||||
|
@ -645,12 +645,6 @@
|
||||
"on": {
|
||||
"message": "Hidupkan"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Sekat URL Explorer (pilihan)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbol (pilihan)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Asal"
|
||||
},
|
||||
|
@ -655,12 +655,6 @@
|
||||
"on": {
|
||||
"message": "På"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Blokker Explorer URL (valgfritt)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Symbol (valgfritt)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Opprinnelse"
|
||||
},
|
||||
|
@ -1321,12 +1321,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "URL ng Block Explorer (opsyonal)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbolo ng Currency (opsyonal)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Pinagmulan"
|
||||
},
|
||||
|
@ -662,12 +662,6 @@
|
||||
"on": {
|
||||
"message": "Włączone"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Adres URL przeglądarki łańcucha bloków (opcjonalnie)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Symbol (opcjonalnie)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Pochodzenie"
|
||||
},
|
||||
|
@ -1321,12 +1321,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "Conecte-se somente com sites em quem você confia."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "URL do Block Explorer (opcional)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Símbolo de moeda (opcional)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Origem"
|
||||
},
|
||||
|
@ -655,12 +655,6 @@
|
||||
"on": {
|
||||
"message": "Activat"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "URL explorator bloc (opțional)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbol (opțional)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Origine"
|
||||
},
|
||||
|
@ -1321,12 +1321,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "Подключайтесь только к сайтам, которым доверяете."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "URL-адрес проводника блока (необязательно)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Символ валюты (необязательно)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Источник"
|
||||
},
|
||||
|
@ -637,12 +637,6 @@
|
||||
"on": {
|
||||
"message": "Zapnuté"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Blokovať URL Explorera (voliteľné)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Symbol (voliteľné)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Pôvod"
|
||||
},
|
||||
|
@ -656,12 +656,6 @@
|
||||
"on": {
|
||||
"message": "Vklopljeno"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Blokiraj URL Explorerja (poljubno)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbol (nezahtevano)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Izvor"
|
||||
},
|
||||
|
@ -659,12 +659,6 @@
|
||||
"on": {
|
||||
"message": "Укључено"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Blokirajte URL Explorer-a (opciono)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbol (opciono)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Извор"
|
||||
},
|
||||
|
@ -652,12 +652,6 @@
|
||||
"on": {
|
||||
"message": "På"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Block Explorer URL (valfritt)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Symbol (frivillig)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Ursprung"
|
||||
},
|
||||
|
@ -646,12 +646,6 @@
|
||||
"on": {
|
||||
"message": "Imewashwa"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "URL ya Block Explorer URL (hiari)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Ishara (hiari)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Asili"
|
||||
},
|
||||
|
@ -1071,12 +1071,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "URL ng Block Explorer (opsyonal)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Simbolo ng Currency (opsyonal)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Pinagmulan"
|
||||
},
|
||||
|
@ -668,12 +668,6 @@
|
||||
"on": {
|
||||
"message": "Увімкнути"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Блокувати Explorer URL (не обов'язково)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Символ (не обов'язково)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Походження"
|
||||
},
|
||||
|
@ -1321,12 +1321,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "Chỉ kết nối với các trang web mà bạn tin tưởng."
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "URL trình khám phá khối (không bắt buộc)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Ký hiệu tiền tệ (không bắt buộc)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Nguồn gốc"
|
||||
},
|
||||
|
@ -1074,12 +1074,6 @@
|
||||
"onlyConnectTrust": {
|
||||
"message": "只连接您信任的网站。"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "区块浏览器 URL(选填)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "符号(选填)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "来源"
|
||||
},
|
||||
|
@ -668,12 +668,6 @@
|
||||
"on": {
|
||||
"message": "開啟"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "區塊鏈瀏覽器 URL(非必要)"
|
||||
},
|
||||
"optionalCurrencySymbol": {
|
||||
"message": "Symbol (可選)"
|
||||
},
|
||||
"origin": {
|
||||
"message": "來源"
|
||||
},
|
||||
|
@ -32,13 +32,7 @@ describe('Stores custom RPC history', function () {
|
||||
|
||||
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
||||
|
||||
await driver.findVisibleElement('.settings-page__content');
|
||||
|
||||
await driver.findElement('.settings-page__sub-header-text');
|
||||
|
||||
await driver.clickElement(
|
||||
'.add-network-form__header-add-network-button',
|
||||
);
|
||||
await driver.findElement('.networks-tab__sub-header-text');
|
||||
|
||||
const customRpcInputs = await driver.findElements('input[type="text"]');
|
||||
const networkNameInput = customRpcInputs[0];
|
||||
@ -54,7 +48,10 @@ describe('Stores custom RPC history', function () {
|
||||
await chainIdInput.clear();
|
||||
await chainIdInput.sendKeys(chainId.toString());
|
||||
|
||||
await driver.clickElement('.add-network-form__footer-submit-button');
|
||||
await driver.clickElement(
|
||||
'.networks-tab__add-network-form-footer .btn-primary',
|
||||
);
|
||||
|
||||
await driver.findElement({ text: networkName, tag: 'span' });
|
||||
},
|
||||
);
|
||||
@ -79,13 +76,7 @@ describe('Stores custom RPC history', function () {
|
||||
|
||||
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
||||
|
||||
await driver.findVisibleElement('.settings-page__content');
|
||||
|
||||
await driver.findElement('.settings-page__sub-header-text');
|
||||
|
||||
await driver.clickElement(
|
||||
'.add-network-form__header-add-network-button',
|
||||
);
|
||||
await driver.findElement('.networks-tab__sub-header-text');
|
||||
|
||||
const customRpcInputs = await driver.findElements('input[type="text"]');
|
||||
const rpcUrlInput = customRpcInputs[1];
|
||||
@ -94,7 +85,7 @@ describe('Stores custom RPC history', function () {
|
||||
await rpcUrlInput.sendKeys(duplicateRpcUrl);
|
||||
await driver.findElement({
|
||||
text: 'This URL is currently used by the localhost network.',
|
||||
tag: 'p',
|
||||
tag: 'h6',
|
||||
});
|
||||
},
|
||||
);
|
||||
@ -120,15 +111,7 @@ describe('Stores custom RPC history', function () {
|
||||
|
||||
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
||||
|
||||
// await driver.findElement('.add-network-form__sub-header-text');
|
||||
// wait for the full screen to be visible
|
||||
await driver.findVisibleElement('.settings-page__content');
|
||||
|
||||
await driver.findElement('.settings-page__sub-header-text');
|
||||
|
||||
await driver.clickElement(
|
||||
'.add-network-form__header-add-network-button',
|
||||
);
|
||||
await driver.findElement('.networks-tab__sub-header-text');
|
||||
|
||||
const customRpcInputs = await driver.findElements('input[type="text"]');
|
||||
const rpcUrlInput = customRpcInputs[1];
|
||||
@ -141,7 +124,7 @@ describe('Stores custom RPC history', function () {
|
||||
await chainIdInput.sendKeys(duplicateChainId);
|
||||
await driver.findElement({
|
||||
text: 'This Chain ID is currently used by the localhost network.',
|
||||
tag: 'p',
|
||||
tag: 'h6',
|
||||
});
|
||||
},
|
||||
);
|
||||
@ -211,6 +194,10 @@ describe('Stores custom RPC history', function () {
|
||||
await driver.clickElement({ text: 'Add Network', tag: 'button' });
|
||||
|
||||
await driver.findVisibleElement('.settings-page__content');
|
||||
// // cancel new custom rpc
|
||||
await driver.clickElement(
|
||||
'.networks-tab__add-network-form-footer button.btn-secondary',
|
||||
);
|
||||
|
||||
const networkListItems = await driver.findClickableElements(
|
||||
'.networks-tab__networks-list-name',
|
||||
|
@ -14,7 +14,7 @@ import { COLORS, SIZES } from '../../../helpers/constants/design-system';
|
||||
import { getShowTestNetworks } from '../../../selectors';
|
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
||||
import { NETWORKS_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { ADD_NETWORK_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { Dropdown, DropdownMenuItem } from './dropdown';
|
||||
|
||||
// classes from nodes of the toggle element.
|
||||
@ -129,9 +129,9 @@ class NetworkDropdown extends Component {
|
||||
size="large"
|
||||
onClick={() => {
|
||||
if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) {
|
||||
global.platform.openExtensionInBrowser(NETWORKS_ROUTE);
|
||||
global.platform.openExtensionInBrowser(ADD_NETWORK_ROUTE);
|
||||
} else {
|
||||
this.props.history.push(NETWORKS_ROUTE);
|
||||
this.props.history.push(ADD_NETWORK_ROUTE);
|
||||
}
|
||||
this.props.hideNetworkDropdown();
|
||||
}}
|
||||
|
@ -117,7 +117,7 @@ FormField.propTypes = {
|
||||
titleDetail: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
error: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.number,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
detailText: PropTypes.string,
|
||||
autoFocus: PropTypes.bool,
|
||||
numeric: PropTypes.bool,
|
||||
|
@ -1 +1 @@
|
||||
export { default } from './networks-tab.container';
|
||||
export { default } from './networks-tab';
|
||||
|
@ -1,8 +1,5 @@
|
||||
@import 'network-form/index.scss';
|
||||
|
||||
.networks-tab {
|
||||
&__content {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-width: 739px;
|
||||
@ -27,13 +24,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__subheader {
|
||||
@include H4;
|
||||
|
||||
padding: 16px 4px;
|
||||
border-bottom: 1px solid $alto;
|
||||
height: 72px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
&__subheader--break {
|
||||
margin-inline-start: 10px;
|
||||
}
|
||||
|
||||
&__sub-header-text {
|
||||
@include H4;
|
||||
|
||||
color: $ui-4;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&__network-form {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
max-width: 343px;
|
||||
max-height: 465px;
|
||||
margin-top: 24px;
|
||||
|
||||
.page-container__footer {
|
||||
border-top: none;
|
||||
@ -53,29 +72,39 @@
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
width: 90%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-network-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 465px;
|
||||
}
|
||||
|
||||
&__network-form-body {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
width: 95%;
|
||||
|
||||
&__view-only {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-network-form-body {
|
||||
display: grid;
|
||||
grid-template-columns: 48% 48%;
|
||||
// row-gap: 10%;
|
||||
column-gap: 5%;
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__network-form-row {
|
||||
@media screen and (max-width: $break-small) {
|
||||
width: 93%;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
@include H7;
|
||||
|
||||
background-color: #fefae8;
|
||||
border: 1px solid #ffd33d;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
width: 93%;
|
||||
}
|
||||
width: 99%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,12 +129,14 @@
|
||||
&__networks-list {
|
||||
flex: 0.5 0 auto;
|
||||
max-width: 343px;
|
||||
margin-top: 24px;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-width: 100vw;
|
||||
width: 100vw;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,13 +254,12 @@
|
||||
font-weight: 300;
|
||||
color: #cdcdcd;
|
||||
}
|
||||
}
|
||||
|
||||
.network-form {
|
||||
&__footer {
|
||||
&__network-form-footer {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 0.75rem 0;
|
||||
width: 95%;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
width: 93%;
|
||||
@ -247,4 +277,19 @@
|
||||
margin-right: 3.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-network-form-footer {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
padding: 0.75rem 0;
|
||||
width: 60%;
|
||||
|
||||
.btn-secondary {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
export { default } from './network-form.component';
|
@ -1,76 +0,0 @@
|
||||
.add-network-form {
|
||||
&__body {
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
&__subheader {
|
||||
@include H4;
|
||||
|
||||
padding: 16px 4px;
|
||||
border-bottom: 1px solid $alto;
|
||||
height: 72px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
&__subheader--break {
|
||||
margin-inline-start: 10px;
|
||||
}
|
||||
|
||||
&__sub-header-text {
|
||||
@include H4;
|
||||
|
||||
color: $ui-4;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&--warning {
|
||||
@include H7;
|
||||
|
||||
background-color: $Yellow-000;
|
||||
border: 1px solid $alert-1;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__form-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&__form-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__network-form-row {
|
||||
padding-bottom: 30px;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
padding: 0 0 0.75rem 0;
|
||||
width: 60%;
|
||||
|
||||
&-cancel-button {
|
||||
margin-right: 1.25rem;
|
||||
}
|
||||
|
||||
&-submit-button {
|
||||
margin-left: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,761 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import validUrl from 'valid-url';
|
||||
import log from 'loglevel';
|
||||
import TextField from '../../../../components/ui/text-field';
|
||||
import Button from '../../../../components/ui/button';
|
||||
import Tooltip from '../../../../components/ui/tooltip';
|
||||
import {
|
||||
isPrefixedFormattedHexString,
|
||||
isSafeChainId,
|
||||
} from '../../../../../shared/modules/network.utils';
|
||||
import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils';
|
||||
import { decimalToHex } from '../../../../helpers/utils/conversions.util';
|
||||
|
||||
const FORM_STATE_KEYS = [
|
||||
'rpcUrl',
|
||||
'chainId',
|
||||
'ticker',
|
||||
'networkName',
|
||||
'blockExplorerUrl',
|
||||
];
|
||||
|
||||
export default class NetworkForm extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
metricsEvent: PropTypes.func,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
editRpc: PropTypes.func,
|
||||
showConfirmDeleteNetworkModal: PropTypes.func,
|
||||
rpcUrl: PropTypes.string,
|
||||
chainId: PropTypes.string,
|
||||
ticker: PropTypes.string,
|
||||
viewOnly: PropTypes.bool,
|
||||
networkName: PropTypes.string,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
setRpcTarget: PropTypes.func.isRequired,
|
||||
isCurrentRpcTarget: PropTypes.bool,
|
||||
blockExplorerUrl: PropTypes.string,
|
||||
rpcPrefs: PropTypes.object,
|
||||
networksToRender: PropTypes.array.isRequired,
|
||||
onAddNetwork: PropTypes.func,
|
||||
setNewNetworkAdded: PropTypes.func,
|
||||
addNewNetwork: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
rpcUrl: '',
|
||||
chainId: '',
|
||||
ticker: '',
|
||||
networkName: '',
|
||||
blockExplorerUrl: '',
|
||||
};
|
||||
|
||||
state = {
|
||||
rpcUrl: this.props.rpcUrl,
|
||||
chainId: this.getDisplayChainId(this.props.chainId),
|
||||
ticker: this.props.ticker,
|
||||
networkName: this.props.networkName,
|
||||
blockExplorerUrl: this.props.blockExplorerUrl,
|
||||
errors: {},
|
||||
isSubmitting: false,
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { addNewNetwork: prevAddMode } = prevProps;
|
||||
const { addNewNetwork } = this.props;
|
||||
|
||||
if (!prevAddMode && addNewNetwork) {
|
||||
this.setState({
|
||||
rpcUrl: '',
|
||||
chainId: '',
|
||||
ticker: '',
|
||||
networkName: '',
|
||||
blockExplorerUrl: '',
|
||||
errors: {},
|
||||
isSubmitting: false,
|
||||
});
|
||||
} else {
|
||||
for (const key of FORM_STATE_KEYS) {
|
||||
if (prevProps[key] !== this.props[key]) {
|
||||
this.resetForm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.setState({
|
||||
rpcUrl: '',
|
||||
chainId: '',
|
||||
ticker: '',
|
||||
networkName: '',
|
||||
blockExplorerUrl: '',
|
||||
errors: {},
|
||||
});
|
||||
// onClear will push the network settings route unless was pass false.
|
||||
// Since we call onClear to cause this component to be unmounted, the
|
||||
// route will already have been updated, and we avoid setting it twice.
|
||||
this.props.onClear(false);
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
const {
|
||||
rpcUrl,
|
||||
chainId,
|
||||
ticker,
|
||||
networkName,
|
||||
blockExplorerUrl,
|
||||
} = this.props;
|
||||
|
||||
this.setState({
|
||||
rpcUrl,
|
||||
chainId: this.getDisplayChainId(chainId),
|
||||
ticker,
|
||||
networkName,
|
||||
blockExplorerUrl,
|
||||
errors: {},
|
||||
isSubmitting: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to convert the given chainId to a decimal string, for display
|
||||
* purposes.
|
||||
*
|
||||
* Should be called with the props chainId whenever it is used to set the
|
||||
* component's state.
|
||||
*
|
||||
* @param {unknown} chainId - The chainId to convert.
|
||||
* @returns {string} The props chainId in decimal, or the original value if
|
||||
* it can't be converted.
|
||||
*/
|
||||
getDisplayChainId(chainId) {
|
||||
if (!chainId || typeof chainId !== 'string' || !chainId.startsWith('0x')) {
|
||||
return chainId;
|
||||
}
|
||||
return parseInt(chainId, 16).toString(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes a given id with '0x' if the prefix does not exist
|
||||
*
|
||||
* @param {string} chainId - The chainId to prefix
|
||||
* @returns {string} The chainId, prefixed with '0x'
|
||||
*/
|
||||
prefixChainId(chainId) {
|
||||
let prefixedChainId = chainId;
|
||||
if (!chainId.startsWith('0x')) {
|
||||
prefixedChainId = `0x${parseInt(chainId, 10).toString(16)}`;
|
||||
}
|
||||
return prefixedChainId;
|
||||
}
|
||||
|
||||
onSubmit = async () => {
|
||||
this.setState({
|
||||
isSubmitting: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const {
|
||||
setRpcTarget,
|
||||
rpcUrl: propsRpcUrl,
|
||||
editRpc,
|
||||
rpcPrefs = {},
|
||||
onAddNetwork,
|
||||
setNewNetworkAdded,
|
||||
addNewNetwork,
|
||||
} = this.props;
|
||||
const {
|
||||
networkName,
|
||||
rpcUrl,
|
||||
chainId: stateChainId,
|
||||
ticker,
|
||||
blockExplorerUrl,
|
||||
} = this.state;
|
||||
|
||||
const formChainId = stateChainId.trim().toLowerCase();
|
||||
const chainId = this.prefixChainId(formChainId);
|
||||
|
||||
if (!(await this.validateChainIdOnSubmit(formChainId, chainId, rpcUrl))) {
|
||||
this.setState({
|
||||
isSubmitting: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// After this point, isSubmitting will be reset in componentDidUpdate
|
||||
if (propsRpcUrl && rpcUrl !== propsRpcUrl) {
|
||||
await editRpc(propsRpcUrl, rpcUrl, chainId, ticker, networkName, {
|
||||
...rpcPrefs,
|
||||
blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl,
|
||||
});
|
||||
} else {
|
||||
await setRpcTarget(rpcUrl, chainId, ticker, networkName, {
|
||||
...rpcPrefs,
|
||||
blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl,
|
||||
});
|
||||
}
|
||||
|
||||
if (addNewNetwork) {
|
||||
setNewNetworkAdded(networkName);
|
||||
onAddNetwork();
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
isSubmitting: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
const { addNewNetwork, onClear } = this.props;
|
||||
|
||||
if (addNewNetwork) {
|
||||
onClear();
|
||||
} else {
|
||||
this.resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
onDelete = () => {
|
||||
const { showConfirmDeleteNetworkModal, rpcUrl, onClear } = this.props;
|
||||
showConfirmDeleteNetworkModal({
|
||||
target: rpcUrl,
|
||||
onConfirm: () => {
|
||||
this.resetForm();
|
||||
onClear();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
isSubmitting() {
|
||||
return this.state.isSubmitting;
|
||||
}
|
||||
|
||||
stateIsUnchanged() {
|
||||
const {
|
||||
rpcUrl,
|
||||
chainId: propsChainId,
|
||||
ticker,
|
||||
networkName,
|
||||
blockExplorerUrl,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
rpcUrl: stateRpcUrl,
|
||||
chainId: stateChainId,
|
||||
ticker: stateTicker,
|
||||
networkName: stateNetworkName,
|
||||
blockExplorerUrl: stateBlockExplorerUrl,
|
||||
} = this.state;
|
||||
|
||||
// These added conditions are in case the saved chainId is invalid, which
|
||||
// was possible in versions <8.1 of the extension.
|
||||
// Basically, we always want to be able to overwrite an invalid chain ID.
|
||||
const chainIdIsUnchanged =
|
||||
typeof propsChainId === 'string' &&
|
||||
propsChainId.toLowerCase().startsWith('0x') &&
|
||||
stateChainId === this.getDisplayChainId(propsChainId);
|
||||
|
||||
return (
|
||||
stateRpcUrl === rpcUrl &&
|
||||
chainIdIsUnchanged &&
|
||||
stateTicker === ticker &&
|
||||
stateNetworkName === networkName &&
|
||||
stateBlockExplorerUrl === blockExplorerUrl
|
||||
);
|
||||
}
|
||||
|
||||
renderFormTextField({
|
||||
className,
|
||||
fieldKey,
|
||||
textFieldId,
|
||||
onChange,
|
||||
value,
|
||||
optionalTextFieldKey,
|
||||
tooltipText,
|
||||
autoFocus = false,
|
||||
}) {
|
||||
const { errors } = this.state;
|
||||
const { viewOnly } = this.props;
|
||||
const errorMessage = errors[fieldKey]?.msg || '';
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="networks-tab__network-form-label">
|
||||
<div className="networks-tab__network-form-label-text">
|
||||
{this.context.t(optionalTextFieldKey || fieldKey)}
|
||||
</div>
|
||||
{!viewOnly && tooltipText ? (
|
||||
<Tooltip
|
||||
position="top"
|
||||
title={tooltipText}
|
||||
wrapperClassName="networks-tab__network-form-label-tooltip"
|
||||
>
|
||||
<i className="fa fa-info-circle" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
<TextField
|
||||
type="text"
|
||||
id={textFieldId}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={value}
|
||||
disabled={viewOnly}
|
||||
error={errorMessage}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
setStateWithValue = (stateKey, validator) => {
|
||||
return (e) => {
|
||||
validator?.(e.target.value, stateKey);
|
||||
this.setState({ [stateKey]: e.target.value });
|
||||
};
|
||||
};
|
||||
|
||||
setErrorTo = (errorKey, errorVal) => {
|
||||
this.setState({
|
||||
errors: {
|
||||
...this.state.errors,
|
||||
[errorKey]: errorVal,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
setErrorEmpty = (errorKey) => {
|
||||
this.setState({
|
||||
errors: {
|
||||
...this.state.errors,
|
||||
[errorKey]: {
|
||||
msg: '',
|
||||
key: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
hasError = (errorKey, errorKeyVal) => {
|
||||
return this.state.errors[errorKey]?.key === errorKeyVal;
|
||||
};
|
||||
|
||||
hasErrors = () => {
|
||||
const { errors } = this.state;
|
||||
return Object.keys(errors).some((key) => {
|
||||
const error = errors[key];
|
||||
// Do not factor in duplicate chain id error for submission disabling
|
||||
if (key === 'chainId' && error.key === 'chainIdExistsErrorMsg') {
|
||||
return false;
|
||||
}
|
||||
return error.key && error.msg;
|
||||
});
|
||||
};
|
||||
|
||||
validateChainIdOnChange = (selfRpcUrl, chainIdArg = '') => {
|
||||
const { t } = this.context;
|
||||
const { networksToRender } = this.props;
|
||||
const chainId = chainIdArg.trim();
|
||||
|
||||
let errorKey = '';
|
||||
let errorMessage = '';
|
||||
let radix = 10;
|
||||
let hexChainId = chainId;
|
||||
|
||||
if (!hexChainId.startsWith('0x')) {
|
||||
try {
|
||||
hexChainId = `0x${decimalToHex(hexChainId)}`;
|
||||
} catch (err) {
|
||||
this.setErrorTo('chainId', {
|
||||
key: 'invalidHexNumber',
|
||||
msg: t('invalidHexNumber'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const [matchingChainId] = networksToRender.filter(
|
||||
(e) => e.chainId === hexChainId && e.rpcUrl !== selfRpcUrl,
|
||||
);
|
||||
|
||||
if (chainId === '') {
|
||||
this.setErrorEmpty('chainId');
|
||||
return;
|
||||
} else if (matchingChainId) {
|
||||
errorKey = 'chainIdExistsErrorMsg';
|
||||
errorMessage = t('chainIdExistsErrorMsg', [
|
||||
matchingChainId.label ?? matchingChainId.labelKey,
|
||||
]);
|
||||
} else if (chainId.startsWith('0x')) {
|
||||
radix = 16;
|
||||
if (!/^0x[0-9a-f]+$/iu.test(chainId)) {
|
||||
errorKey = 'invalidHexNumber';
|
||||
errorMessage = t('invalidHexNumber');
|
||||
} else if (!isPrefixedFormattedHexString(chainId)) {
|
||||
errorMessage = t('invalidHexNumberLeadingZeros');
|
||||
}
|
||||
} else if (!/^[0-9]+$/u.test(chainId)) {
|
||||
errorKey = 'invalidNumber';
|
||||
errorMessage = t('invalidNumber');
|
||||
} else if (chainId.startsWith('0')) {
|
||||
errorKey = 'invalidNumberLeadingZeros';
|
||||
errorMessage = t('invalidNumberLeadingZeros');
|
||||
} else if (!isSafeChainId(parseInt(chainId, radix))) {
|
||||
errorKey = 'invalidChainIdTooBig';
|
||||
errorMessage = t('invalidChainIdTooBig');
|
||||
}
|
||||
|
||||
this.setErrorTo('chainId', {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the chain ID by checking it against the `eth_chainId` return
|
||||
* value from the given RPC URL.
|
||||
* Assumes that all strings are non-empty and correctly formatted.
|
||||
*
|
||||
* @param {string} formChainId - Non-empty, hex or decimal number string from
|
||||
* the form.
|
||||
* @param {string} parsedChainId - The parsed, hex string chain ID.
|
||||
* @param {string} rpcUrl - The RPC URL from the form.
|
||||
*/
|
||||
validateChainIdOnSubmit = async (formChainId, parsedChainId, rpcUrl) => {
|
||||
const { t } = this.context;
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
let endpointChainId;
|
||||
let providerError;
|
||||
|
||||
try {
|
||||
endpointChainId = await jsonRpcRequest(rpcUrl, 'eth_chainId');
|
||||
} catch (err) {
|
||||
log.warn('Failed to fetch the chainId from the endpoint.', err);
|
||||
providerError = err;
|
||||
}
|
||||
|
||||
if (providerError || typeof endpointChainId !== 'string') {
|
||||
errorKey = 'failedToFetchChainId';
|
||||
errorMessage = t('failedToFetchChainId');
|
||||
} else if (parsedChainId !== endpointChainId) {
|
||||
// Here, we are in an error state. The endpoint should always return a
|
||||
// hexadecimal string. If the user entered a decimal string, we attempt
|
||||
// to convert the endpoint's return value to decimal before rendering it
|
||||
// in an error message in the form.
|
||||
if (!formChainId.startsWith('0x')) {
|
||||
try {
|
||||
endpointChainId = parseInt(endpointChainId, 16).toString(10);
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
'Failed to convert endpoint chain ID to decimal',
|
||||
endpointChainId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
errorKey = 'endpointReturnedDifferentChainId';
|
||||
errorMessage = t('endpointReturnedDifferentChainId', [
|
||||
endpointChainId.length <= 12
|
||||
? endpointChainId
|
||||
: `${endpointChainId.slice(0, 9)}...`,
|
||||
]);
|
||||
}
|
||||
|
||||
if (errorKey) {
|
||||
this.setErrorTo('chainId', {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setErrorEmpty('chainId');
|
||||
return true;
|
||||
};
|
||||
|
||||
isValidWhenAppended = (url) => {
|
||||
const appendedRpc = `http://${url}`;
|
||||
return validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/u);
|
||||
};
|
||||
|
||||
validateBlockExplorerURL = (url, stateKey) => {
|
||||
const { t } = this.context;
|
||||
if (!validUrl.isWebUri(url) && url !== '') {
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
|
||||
if (this.isValidWhenAppended(url)) {
|
||||
errorKey = 'urlErrorMsg';
|
||||
errorMessage = t('urlErrorMsg');
|
||||
} else {
|
||||
errorKey = 'invalidBlockExplorerURL';
|
||||
errorMessage = t('invalidBlockExplorerURL');
|
||||
}
|
||||
|
||||
this.setErrorTo(stateKey, {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
} else {
|
||||
this.setErrorEmpty(stateKey);
|
||||
}
|
||||
};
|
||||
|
||||
validateUrlRpcUrl = (url, stateKey) => {
|
||||
const { t } = this.context;
|
||||
const { networksToRender } = this.props;
|
||||
const { chainId: stateChainId } = this.state;
|
||||
const isValidUrl = validUrl.isWebUri(url);
|
||||
const chainIdFetchFailed = this.hasError('chainId', 'failedToFetchChainId');
|
||||
const [matchingRPCUrl] = networksToRender.filter((e) => e.rpcUrl === url);
|
||||
|
||||
if (!isValidUrl && url !== '') {
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
if (this.isValidWhenAppended(url)) {
|
||||
errorKey = 'urlErrorMsg';
|
||||
errorMessage = t('urlErrorMsg');
|
||||
} else {
|
||||
errorKey = 'invalidRPC';
|
||||
errorMessage = t('invalidRPC');
|
||||
}
|
||||
this.setErrorTo(stateKey, {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
} else if (matchingRPCUrl) {
|
||||
this.setErrorTo(stateKey, {
|
||||
key: 'urlExistsErrorMsg',
|
||||
msg: t('urlExistsErrorMsg', [
|
||||
matchingRPCUrl.label ?? matchingRPCUrl.labelKey,
|
||||
]),
|
||||
});
|
||||
} else {
|
||||
this.setErrorEmpty(stateKey);
|
||||
}
|
||||
|
||||
// Re-validate the chain id if it could not be found with previous rpc url
|
||||
if (stateChainId && isValidUrl && chainIdFetchFailed) {
|
||||
const formChainId = stateChainId.trim().toLowerCase();
|
||||
const chainId = this.prefixChainId(formChainId);
|
||||
this.validateChainIdOnSubmit(formChainId, chainId, url);
|
||||
}
|
||||
};
|
||||
|
||||
renderAddNetworkForm() {
|
||||
const { t } = this.context;
|
||||
const {
|
||||
networkName,
|
||||
rpcUrl,
|
||||
chainId = '',
|
||||
ticker,
|
||||
blockExplorerUrl,
|
||||
} = this.state;
|
||||
|
||||
const isSubmitDisabled =
|
||||
this.hasErrors() || this.isSubmitting() || !rpcUrl || !chainId;
|
||||
|
||||
return (
|
||||
<div className="add-network-form__body">
|
||||
<div className="add-network-form__subheader">
|
||||
<span className="add-network-form__sub-header-text">
|
||||
{t('networks')}
|
||||
</span>
|
||||
<span>{' > '}</span>
|
||||
<div className="add-network-form__subheader--break">
|
||||
{t('addANetwork')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="add-network-form__content">
|
||||
<div className="add-network-form__content--warning">
|
||||
{t('onlyAddTrustedNetworks')}
|
||||
</div>
|
||||
<div className="add-network-form__form-column">
|
||||
<div className="add-network-form__form-row">
|
||||
{this.renderFormTextField({
|
||||
className: 'add-network-form__network-form-row',
|
||||
fieldKey: 'networkName',
|
||||
textFieldId: 'network-name',
|
||||
onChange: this.setStateWithValue('networkName'),
|
||||
value: networkName,
|
||||
autoFocus: true,
|
||||
})}
|
||||
{this.renderFormTextField({
|
||||
className: 'add-network-form__network-form-row',
|
||||
fieldKey: 'rpcUrl',
|
||||
textFieldId: 'rpc-url',
|
||||
onChange: this.setStateWithValue(
|
||||
'rpcUrl',
|
||||
this.validateUrlRpcUrl,
|
||||
),
|
||||
value: rpcUrl,
|
||||
})}
|
||||
</div>
|
||||
<div className="add-network-form__form-row">
|
||||
{this.renderFormTextField({
|
||||
className: 'add-network-form__network-form-row',
|
||||
fieldKey: 'chainId',
|
||||
textFieldId: 'chainId',
|
||||
onChange: this.setStateWithValue(
|
||||
'chainId',
|
||||
this.validateChainIdOnChange.bind(this, rpcUrl),
|
||||
),
|
||||
value: chainId,
|
||||
tooltipText: t('networkSettingsChainIdDescription'),
|
||||
})}
|
||||
{this.renderFormTextField({
|
||||
className: 'add-network-form__network-form-row',
|
||||
fieldKey: 'symbol',
|
||||
textFieldId: 'network-ticker',
|
||||
onChange: this.setStateWithValue('ticker'),
|
||||
value: ticker,
|
||||
optionalTextFieldKey: 'optionalCurrencySymbol',
|
||||
})}
|
||||
</div>
|
||||
<div className="add-network-form__form-row">
|
||||
{this.renderFormTextField({
|
||||
className: 'add-network-form__network-form-row',
|
||||
fieldKey: 'blockExplorerUrl',
|
||||
textFieldId: 'block-explorer-url',
|
||||
onChange: this.setStateWithValue(
|
||||
'blockExplorerUrl',
|
||||
this.validateBlockExplorerURL,
|
||||
),
|
||||
value: blockExplorerUrl,
|
||||
optionalTextFieldKey: 'optionalBlockExplorerUrl',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="add-network-form__footer">
|
||||
<Button
|
||||
type="secondary"
|
||||
onClick={this.onCancel}
|
||||
className="add-network-form__footer-cancel-button"
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={isSubmitDisabled}
|
||||
onClick={this.onSubmit}
|
||||
className="add-network-form__footer-submit-button"
|
||||
>
|
||||
{t('save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderNetworkForm() {
|
||||
const { t } = this.context;
|
||||
const { viewOnly, isCurrentRpcTarget } = this.props;
|
||||
const {
|
||||
networkName,
|
||||
rpcUrl,
|
||||
chainId = '',
|
||||
ticker,
|
||||
blockExplorerUrl,
|
||||
} = this.state;
|
||||
|
||||
const deletable = !isCurrentRpcTarget && !viewOnly;
|
||||
|
||||
const isSubmitDisabled =
|
||||
this.hasErrors() ||
|
||||
this.isSubmitting() ||
|
||||
this.stateIsUnchanged() ||
|
||||
!rpcUrl ||
|
||||
!chainId;
|
||||
|
||||
return (
|
||||
<div className="networks-tab__network-form">
|
||||
{this.renderFormTextField({
|
||||
className: 'networks-tab__network-form-row',
|
||||
fieldKey: 'networkName',
|
||||
textFieldId: 'network-name',
|
||||
onChange: this.setStateWithValue('networkName'),
|
||||
value: networkName,
|
||||
})}
|
||||
{this.renderFormTextField({
|
||||
className: 'networks-tab__network-form-row',
|
||||
fieldKey: 'rpcUrl',
|
||||
textFieldId: 'rpc-url',
|
||||
onChange: this.setStateWithValue('rpcUrl', this.validateUrlRpcUrl),
|
||||
value: rpcUrl,
|
||||
})}
|
||||
{this.renderFormTextField({
|
||||
className: 'networks-tab__network-form-row',
|
||||
fieldKey: 'chainId',
|
||||
textFieldId: 'chainId',
|
||||
onChange: this.setStateWithValue(
|
||||
'chainId',
|
||||
this.validateChainIdOnChange.bind(this, rpcUrl),
|
||||
),
|
||||
value: chainId,
|
||||
tooltipText: viewOnly ? null : t('networkSettingsChainIdDescription'),
|
||||
})}
|
||||
{this.renderFormTextField({
|
||||
className: 'networks-tab__network-form-row',
|
||||
fieldKey: 'symbol',
|
||||
textFieldId: 'network-ticker',
|
||||
onChange: this.setStateWithValue('ticker'),
|
||||
value: ticker,
|
||||
optionalTextFieldKey: 'optionalCurrencySymbol',
|
||||
})}
|
||||
{this.renderFormTextField({
|
||||
className: 'networks-tab__network-form-row',
|
||||
fieldKey: 'blockExplorerUrl',
|
||||
textFieldId: 'block-explorer-url',
|
||||
onChange: this.setStateWithValue(
|
||||
'blockExplorerUrl',
|
||||
this.validateBlockExplorerURL,
|
||||
),
|
||||
value: blockExplorerUrl,
|
||||
optionalTextFieldKey: 'optionalBlockExplorerUrl',
|
||||
})}
|
||||
<div className="network-form__footer">
|
||||
{!viewOnly && (
|
||||
<>
|
||||
{deletable && (
|
||||
<Button type="danger" onClick={this.onDelete}>
|
||||
{t('delete')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="secondary"
|
||||
onClick={this.onCancel}
|
||||
disabled={this.stateIsUnchanged()}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={isSubmitDisabled}
|
||||
onClick={this.onSubmit}
|
||||
>
|
||||
{t('save')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { addNewNetwork } = this.props;
|
||||
return addNewNetwork
|
||||
? this.renderAddNetworkForm()
|
||||
: this.renderNetworkForm();
|
||||
}
|
||||
}
|
1
ui/pages/settings/networks-tab/networks-form/index.js
Normal file
1
ui/pages/settings/networks-tab/networks-form/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './networks-form';
|
575
ui/pages/settings/networks-tab/networks-form/networks-form.js
Normal file
575
ui/pages/settings/networks-tab/networks-form/networks-form.js
Normal file
@ -0,0 +1,575 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import validUrl from 'valid-url';
|
||||
import log from 'loglevel';
|
||||
import classnames from 'classnames';
|
||||
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||
import {
|
||||
isPrefixedFormattedHexString,
|
||||
isSafeChainId,
|
||||
} from '../../../../../shared/modules/network.utils';
|
||||
import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils';
|
||||
import ActionableMessage from '../../../../components/ui/actionable-message';
|
||||
import Button from '../../../../components/ui/button';
|
||||
import FormField from '../../../../components/ui/form-field';
|
||||
import { decimalToHex } from '../../../../helpers/utils/conversions.util';
|
||||
import {
|
||||
setSelectedSettingsRpcUrl,
|
||||
updateAndSetCustomRpc,
|
||||
editRpc,
|
||||
showModal,
|
||||
setNewNetworkAdded,
|
||||
} from '../../../../store/actions';
|
||||
import {
|
||||
DEFAULT_ROUTE,
|
||||
NETWORKS_ROUTE,
|
||||
} from '../../../../helpers/constants/routes';
|
||||
|
||||
/**
|
||||
* Attempts to convert the given chainId to a decimal string, for display
|
||||
* purposes.
|
||||
*
|
||||
* Should be called with the props chainId whenever it is used to set the
|
||||
* component's state.
|
||||
*
|
||||
* @param {unknown} chainId - The chainId to convert.
|
||||
* @returns {string} The props chainId in decimal, or the original value if
|
||||
* it can't be converted.
|
||||
*/
|
||||
const getDisplayChainId = (chainId) => {
|
||||
if (!chainId || typeof chainId !== 'string' || !chainId.startsWith('0x')) {
|
||||
return chainId;
|
||||
}
|
||||
return parseInt(chainId, 16).toString(10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Prefixes a given id with '0x' if the prefix does not exist
|
||||
*
|
||||
* @param {string} chainId - The chainId to prefix
|
||||
* @returns {string} The chainId, prefixed with '0x'
|
||||
*/
|
||||
const prefixChainId = (chainId) => {
|
||||
let prefixedChainId = chainId;
|
||||
if (!chainId.startsWith('0x')) {
|
||||
prefixedChainId = `0x${parseInt(chainId, 10).toString(16)}`;
|
||||
}
|
||||
return prefixedChainId;
|
||||
};
|
||||
|
||||
const isValidWhenAppended = (url) => {
|
||||
const appendedRpc = `http://${url}`;
|
||||
return validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/u);
|
||||
};
|
||||
|
||||
const NetworksForm = ({
|
||||
addNewNetwork,
|
||||
isCurrentRpcTarget,
|
||||
networksToRender,
|
||||
selectedNetwork,
|
||||
}) => {
|
||||
const t = useI18nContext();
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const { label, labelKey, viewOnly, rpcPrefs } = selectedNetwork;
|
||||
const selectedNetworkName = label || (labelKey && t(labelKey));
|
||||
const [networkName, setNetworkName] = useState(selectedNetworkName || '');
|
||||
const [rpcUrl, setRpcUrl] = useState(selectedNetwork?.rpcUrl || '');
|
||||
const [chainId, setChainId] = useState(selectedNetwork?.chainId || '');
|
||||
const [ticker, setTicker] = useState(selectedNetwork?.ticker || '');
|
||||
const [blockExplorerUrl, setBlockExplorerUrl] = useState(
|
||||
selectedNetwork?.blockExplorerUrl || '',
|
||||
);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setNetworkName(selectedNetworkName || '');
|
||||
setRpcUrl(selectedNetwork.rpcUrl);
|
||||
setChainId(getDisplayChainId(selectedNetwork.chainId));
|
||||
setTicker(selectedNetwork?.ticker);
|
||||
setBlockExplorerUrl(selectedNetwork?.blockExplorerUrl);
|
||||
setErrors({});
|
||||
setIsSubmitting(false);
|
||||
}, [selectedNetwork, selectedNetworkName]);
|
||||
|
||||
const stateIsUnchanged = () => {
|
||||
// These added conditions are in case the saved chainId is invalid, which
|
||||
// was possible in versions <8.1 of the extension.
|
||||
// Basically, we always want to be able to overwrite an invalid chain ID.
|
||||
const chainIdIsUnchanged =
|
||||
typeof selectedNetwork.chainId === 'string' &&
|
||||
selectedNetwork.chainId.toLowerCase().startsWith('0x') &&
|
||||
chainId === getDisplayChainId(selectedNetwork.chainId);
|
||||
return (
|
||||
rpcUrl === selectedNetwork.rpcUrl &&
|
||||
chainIdIsUnchanged &&
|
||||
ticker === selectedNetwork.ticker &&
|
||||
networkName === selectedNetworkName &&
|
||||
blockExplorerUrl === selectedNetwork.blockExplorerUrl
|
||||
);
|
||||
};
|
||||
|
||||
const prevAddNewNetwork = useRef();
|
||||
const prevNetworkName = useRef();
|
||||
const prevChainId = useRef();
|
||||
const prevRpcUrl = useRef();
|
||||
const prevTicker = useRef();
|
||||
const prevBlockExplorerUrl = useRef();
|
||||
useEffect(() => {
|
||||
if (!prevAddNewNetwork.current && addNewNetwork) {
|
||||
setNetworkName('');
|
||||
setRpcUrl('');
|
||||
setChainId('');
|
||||
setTicker('');
|
||||
setBlockExplorerUrl('');
|
||||
setErrors({});
|
||||
setIsSubmitting(false);
|
||||
} else if (
|
||||
prevNetworkName.current !== selectedNetworkName ||
|
||||
prevRpcUrl.current !== selectedNetwork.rpcUrl ||
|
||||
prevChainId.current !== selectedNetwork.chainId ||
|
||||
prevTicker.current !== selectedNetwork.ticker ||
|
||||
prevBlockExplorerUrl.current !== selectedNetwork.blockExplorerUrl
|
||||
) {
|
||||
resetForm(selectedNetwork);
|
||||
}
|
||||
}, [
|
||||
selectedNetwork,
|
||||
selectedNetworkName,
|
||||
addNewNetwork,
|
||||
setNetworkName,
|
||||
setRpcUrl,
|
||||
setChainId,
|
||||
setTicker,
|
||||
setBlockExplorerUrl,
|
||||
setErrors,
|
||||
setIsSubmitting,
|
||||
resetForm,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setNetworkName('');
|
||||
setRpcUrl('');
|
||||
setChainId('');
|
||||
setTicker('');
|
||||
setBlockExplorerUrl('');
|
||||
setErrors({});
|
||||
dispatch(setSelectedSettingsRpcUrl(''));
|
||||
};
|
||||
}, [
|
||||
setNetworkName,
|
||||
setRpcUrl,
|
||||
setChainId,
|
||||
setTicker,
|
||||
setBlockExplorerUrl,
|
||||
setErrors,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
const setErrorTo = (errorKey, errorVal) => {
|
||||
setErrors({ ...errors, [errorKey]: errorVal });
|
||||
};
|
||||
|
||||
const setErrorEmpty = (errorKey) => {
|
||||
setErrors({
|
||||
...errors,
|
||||
[errorKey]: {
|
||||
msg: '',
|
||||
key: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const hasError = (errorKey, errorKeyVal) => {
|
||||
return errors[errorKey]?.key === errorKeyVal;
|
||||
};
|
||||
|
||||
const hasErrors = () => {
|
||||
return Object.keys(errors).some((key) => {
|
||||
const error = errors[key];
|
||||
// Do not factor in duplicate chain id error for submission disabling
|
||||
if (key === 'chainId' && error.key === 'chainIdExistsErrorMsg') {
|
||||
return false;
|
||||
}
|
||||
return error.key && error.msg;
|
||||
});
|
||||
};
|
||||
|
||||
const validateChainIdOnChange = (chainArg = '') => {
|
||||
const formChainId = chainArg.trim();
|
||||
let errorKey = '';
|
||||
let errorMessage = '';
|
||||
let radix = 10;
|
||||
let hexChainId = formChainId;
|
||||
|
||||
if (!hexChainId.startsWith('0x')) {
|
||||
try {
|
||||
hexChainId = `0x${decimalToHex(hexChainId)}`;
|
||||
} catch (err) {
|
||||
setErrorTo('chainId', {
|
||||
key: 'invalidHexNumber',
|
||||
msg: t('invalidHexNumber'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const [matchingChainId] = networksToRender.filter(
|
||||
(e) => e.chainId === hexChainId && e.rpcUrl !== rpcUrl,
|
||||
);
|
||||
|
||||
if (formChainId === '') {
|
||||
setErrorEmpty('chainId');
|
||||
return;
|
||||
} else if (matchingChainId) {
|
||||
errorKey = 'chainIdExistsErrorMsg';
|
||||
errorMessage = t('chainIdExistsErrorMsg', [
|
||||
matchingChainId.label ?? matchingChainId.labelKey,
|
||||
]);
|
||||
} else if (formChainId.startsWith('0x')) {
|
||||
radix = 16;
|
||||
if (!/^0x[0-9a-f]+$/iu.test(formChainId)) {
|
||||
errorKey = 'invalidHexNumber';
|
||||
errorMessage = t('invalidHexNumber');
|
||||
} else if (!isPrefixedFormattedHexString(formChainId)) {
|
||||
errorMessage = t('invalidHexNumberLeadingZeros');
|
||||
}
|
||||
} else if (!/^[0-9]+$/u.test(formChainId)) {
|
||||
errorKey = 'invalidNumber';
|
||||
errorMessage = t('invalidNumber');
|
||||
} else if (formChainId.startsWith('0')) {
|
||||
errorKey = 'invalidNumberLeadingZeros';
|
||||
errorMessage = t('invalidNumberLeadingZeros');
|
||||
} else if (!isSafeChainId(parseInt(formChainId, radix))) {
|
||||
errorKey = 'invalidChainIdTooBig';
|
||||
errorMessage = t('invalidChainIdTooBig');
|
||||
}
|
||||
|
||||
setErrorTo('chainId', {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the chain ID by checking it against the `eth_chainId` return
|
||||
* value from the given RPC URL.
|
||||
* Assumes that all strings are non-empty and correctly formatted.
|
||||
*
|
||||
* @param {string} formChainId - Non-empty, hex or decimal number string from
|
||||
* the form.
|
||||
* @param {string} parsedChainId - The parsed, hex string chain ID.
|
||||
* @param {string} formRpcUrl - The RPC URL from the form.
|
||||
*/
|
||||
const validateChainIdOnSubmit = async (
|
||||
formChainId,
|
||||
parsedChainId,
|
||||
formRpcUrl,
|
||||
) => {
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
let endpointChainId;
|
||||
let providerError;
|
||||
|
||||
try {
|
||||
endpointChainId = await jsonRpcRequest(formRpcUrl, 'eth_chainId');
|
||||
} catch (err) {
|
||||
log.warn('Failed to fetch the chainId from the endpoint.', err);
|
||||
providerError = err;
|
||||
}
|
||||
|
||||
if (providerError || typeof endpointChainId !== 'string') {
|
||||
errorKey = 'failedToFetchChainId';
|
||||
errorMessage = t('failedToFetchChainId');
|
||||
} else if (parsedChainId !== endpointChainId) {
|
||||
// Here, we are in an error state. The endpoint should always return a
|
||||
// hexadecimal string. If the user entered a decimal string, we attempt
|
||||
// to convert the endpoint's return value to decimal before rendering it
|
||||
// in an error message in the form.
|
||||
if (!formChainId.startsWith('0x')) {
|
||||
try {
|
||||
endpointChainId = parseInt(endpointChainId, 16).toString(10);
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
'Failed to convert endpoint chain ID to decimal',
|
||||
endpointChainId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
errorKey = 'endpointReturnedDifferentChainId';
|
||||
errorMessage = t('endpointReturnedDifferentChainId', [
|
||||
endpointChainId.length <= 12
|
||||
? endpointChainId
|
||||
: `${endpointChainId.slice(0, 9)}...`,
|
||||
]);
|
||||
}
|
||||
|
||||
if (errorKey) {
|
||||
setErrorTo('chainId', {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
setErrorEmpty('chainId');
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateBlockExplorerURL = (url) => {
|
||||
if (!validUrl.isWebUri(url) && url !== '') {
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
|
||||
if (isValidWhenAppended(url)) {
|
||||
errorKey = 'urlErrorMsg';
|
||||
errorMessage = t('urlErrorMsg');
|
||||
} else {
|
||||
errorKey = 'invalidBlockExplorerURL';
|
||||
errorMessage = t('invalidBlockExplorerURL');
|
||||
}
|
||||
|
||||
setErrorTo('blockExplorerUrl', {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
} else {
|
||||
setErrorEmpty('blockExplorerUrl');
|
||||
}
|
||||
};
|
||||
|
||||
const validateUrlRpcUrl = (url) => {
|
||||
const isValidUrl = validUrl.isWebUri(url);
|
||||
const chainIdFetchFailed = hasError('chainId', 'failedToFetchChainId');
|
||||
const [matchingRPCUrl] = networksToRender.filter((e) => e.rpcUrl === url);
|
||||
|
||||
if (!isValidUrl && url !== '') {
|
||||
let errorKey;
|
||||
let errorMessage;
|
||||
if (isValidWhenAppended(url)) {
|
||||
errorKey = 'urlErrorMsg';
|
||||
errorMessage = t('urlErrorMsg');
|
||||
} else {
|
||||
errorKey = 'invalidRPC';
|
||||
errorMessage = t('invalidRPC');
|
||||
}
|
||||
setErrorTo('rpcUrl', {
|
||||
key: errorKey,
|
||||
msg: errorMessage,
|
||||
});
|
||||
} else if (matchingRPCUrl) {
|
||||
setErrorTo('rpcUrl', {
|
||||
key: 'urlExistsErrorMsg',
|
||||
msg: t('urlExistsErrorMsg', [
|
||||
matchingRPCUrl.label ?? matchingRPCUrl.labelKey,
|
||||
]),
|
||||
});
|
||||
} else {
|
||||
setErrorEmpty('rpcUrl');
|
||||
}
|
||||
|
||||
// Re-validate the chain id if it could not be found with previous rpc url
|
||||
if (chainId && isValidUrl && chainIdFetchFailed) {
|
||||
const formChainId = chainId.trim().toLowerCase();
|
||||
const prefixedChainId = prefixChainId(formChainId);
|
||||
validateChainIdOnSubmit(formChainId, prefixedChainId, url);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const formChainId = chainId.trim().toLowerCase();
|
||||
const prefixedChainId = prefixChainId(formChainId);
|
||||
|
||||
if (
|
||||
!(await validateChainIdOnSubmit(formChainId, prefixedChainId, rpcUrl))
|
||||
) {
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// After this point, isSubmitting will be reset in componentDidUpdate
|
||||
if (selectedNetwork.rpcUrl && rpcUrl !== selectedNetwork.rpcUrl) {
|
||||
await dispatch(
|
||||
editRpc(
|
||||
selectedNetwork.rpcUrl,
|
||||
rpcUrl,
|
||||
prefixedChainId,
|
||||
ticker,
|
||||
networkName,
|
||||
{
|
||||
...rpcPrefs,
|
||||
blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl,
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await dispatch(
|
||||
updateAndSetCustomRpc(rpcUrl, prefixedChainId, ticker, networkName, {
|
||||
...rpcPrefs,
|
||||
blockExplorerUrl: blockExplorerUrl || rpcPrefs?.blockExplorerUrl,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (addNewNetwork) {
|
||||
dispatch(setNewNetworkAdded(networkName));
|
||||
history.push(DEFAULT_ROUTE);
|
||||
}
|
||||
} catch (error) {
|
||||
setIsSubmitting(false);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (addNewNetwork) {
|
||||
dispatch(setSelectedSettingsRpcUrl(''));
|
||||
history.push(NETWORKS_ROUTE);
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
dispatch(
|
||||
showModal({
|
||||
name: 'CONFIRM_DELETE_NETWORK',
|
||||
target: selectedNetwork.rpcUrl,
|
||||
onConfirm: () => {
|
||||
resetForm();
|
||||
dispatch(setSelectedSettingsRpcUrl(''));
|
||||
history.push(NETWORKS_ROUTE);
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
const deletable = !isCurrentRpcTarget && !viewOnly && !addNewNetwork;
|
||||
const stateUnchanged = stateIsUnchanged();
|
||||
const isSubmitDisabled =
|
||||
hasErrors() || isSubmitting || stateUnchanged || !rpcUrl || !chainId;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames({
|
||||
'networks-tab__network-form': !addNewNetwork,
|
||||
'networks-tab__add-network-form': addNewNetwork,
|
||||
})}
|
||||
>
|
||||
{addNewNetwork ? (
|
||||
<ActionableMessage
|
||||
type="warning"
|
||||
message={t('onlyAddTrustedNetworks')}
|
||||
iconFillColor="#f8c000"
|
||||
useIcon
|
||||
withRightButton
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={classnames({
|
||||
'networks-tab__network-form-body': !addNewNetwork,
|
||||
'networks-tab__network-form-body__view-only': viewOnly,
|
||||
'networks-tab__add-network-form-body': addNewNetwork,
|
||||
})}
|
||||
>
|
||||
<FormField
|
||||
autoFocus
|
||||
error={errors.networkName?.msg || ''}
|
||||
onChange={setNetworkName}
|
||||
titleText={t('networkName')}
|
||||
value={networkName}
|
||||
disabled={viewOnly}
|
||||
/>
|
||||
<FormField
|
||||
error={errors.rpcUrl?.msg || ''}
|
||||
onChange={(value) => {
|
||||
setRpcUrl(value);
|
||||
validateUrlRpcUrl(value);
|
||||
}}
|
||||
titleText={t('rpcUrl')}
|
||||
value={rpcUrl}
|
||||
disabled={viewOnly}
|
||||
/>
|
||||
<FormField
|
||||
error={errors.chainId?.msg || ''}
|
||||
onChange={(value) => {
|
||||
setChainId(value);
|
||||
validateChainIdOnChange(value);
|
||||
}}
|
||||
titleText={t('chainId')}
|
||||
value={chainId}
|
||||
disabled={viewOnly}
|
||||
tooltipText={viewOnly ? null : t('networkSettingsChainIdDescription')}
|
||||
/>
|
||||
<FormField
|
||||
error={errors.ticker?.msg || ''}
|
||||
onChange={setTicker}
|
||||
titleText={t('currencySymbol')}
|
||||
titleUnit={t('optionalWithParanthesis')}
|
||||
value={ticker}
|
||||
disabled={viewOnly}
|
||||
/>
|
||||
<FormField
|
||||
error={errors.blockExplorerUrl?.msg || ''}
|
||||
onChange={(value) => {
|
||||
setBlockExplorerUrl(value);
|
||||
validateBlockExplorerURL(value);
|
||||
}}
|
||||
titleText={t('blockExplorerUrl')}
|
||||
titleUnit={t('optionalWithParanthesis')}
|
||||
value={blockExplorerUrl}
|
||||
disabled={viewOnly}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classnames({
|
||||
'networks-tab__network-form-footer': !addNewNetwork,
|
||||
'networks-tab__add-network-form-footer': addNewNetwork,
|
||||
})}
|
||||
>
|
||||
{!viewOnly && (
|
||||
<>
|
||||
{deletable && (
|
||||
<Button type="danger" onClick={onDelete}>
|
||||
{t('delete')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="secondary"
|
||||
onClick={onCancel}
|
||||
disabled={stateUnchanged}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={isSubmitDisabled}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{t('save')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NetworksForm.propTypes = {
|
||||
addNewNetwork: PropTypes.bool,
|
||||
isCurrentRpcTarget: PropTypes.bool,
|
||||
networksToRender: PropTypes.array.isRequired,
|
||||
selectedNetwork: PropTypes.object,
|
||||
};
|
||||
|
||||
NetworksForm.defaultProps = {
|
||||
selectedNetwork: {},
|
||||
};
|
||||
|
||||
export default NetworksForm;
|
@ -3,11 +3,11 @@ import configureMockStore from 'redux-mock-store';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||
import { defaultNetworksData } from '../networks-tab.constants';
|
||||
import NetworkForm from '.';
|
||||
import NetworksForm from '.';
|
||||
|
||||
const renderComponent = (props) => {
|
||||
const store = configureMockStore([])({ metamask: {} });
|
||||
return renderWithProvider(<NetworkForm {...props} />, store);
|
||||
return renderWithProvider(<NetworksForm {...props} />, store);
|
||||
};
|
||||
|
||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||
@ -16,41 +16,39 @@ const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||
}));
|
||||
|
||||
const propNewNetwork = {
|
||||
onClear: () => undefined,
|
||||
setRpcTarget: () => undefined,
|
||||
networksToRender: defaultNetworks,
|
||||
onAddNetwork: () => undefined,
|
||||
setNewNetworkAdded: () => undefined,
|
||||
addNewNetwork: true,
|
||||
};
|
||||
|
||||
const propNetworkDisplay = {
|
||||
editRpc: () => undefined,
|
||||
showConfirmDeleteNetworkModal: () => undefined,
|
||||
rpcUrl: 'http://localhost:8545',
|
||||
chainId: '1337',
|
||||
ticker: 'ETH',
|
||||
viewOnly: false,
|
||||
networkName: 'LocalHost',
|
||||
onClear: () => undefined,
|
||||
setRpcTarget: () => undefined,
|
||||
selectedNetwork: {
|
||||
rpcUrl: 'http://localhost:8545',
|
||||
chainId: '1337',
|
||||
ticker: 'ETH',
|
||||
label: 'LocalHost',
|
||||
blockExplorerUrl: '',
|
||||
viewOnly: false,
|
||||
rpcPrefs: {},
|
||||
},
|
||||
isCurrentRpcTarget: false,
|
||||
blockExplorerUrl: '',
|
||||
rpcPrefs: {},
|
||||
networksToRender: defaultNetworks,
|
||||
onAddNetwork: () => undefined,
|
||||
setNewNetworkAdded: () => undefined,
|
||||
addNewNetwork: false,
|
||||
};
|
||||
|
||||
describe('NetworkForm Component', () => {
|
||||
it('should render Add new network form correctly', () => {
|
||||
const { queryByText } = renderComponent(propNewNetwork);
|
||||
const { queryByText, queryAllByText } = renderComponent(propNewNetwork);
|
||||
expect(
|
||||
queryByText(
|
||||
'A malicious network provider can lie about the state of the blockchain and record your network activity. Only add custom networks you trust.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(queryByText('Network Name')).toBeInTheDocument();
|
||||
expect(queryByText('New RPC URL')).toBeInTheDocument();
|
||||
expect(queryByText('Chain ID')).toBeInTheDocument();
|
||||
expect(queryByText('Currency Symbol (optional)')).toBeInTheDocument();
|
||||
expect(queryByText('Block Explorer URL (optional)')).toBeInTheDocument();
|
||||
expect(queryByText('Currency Symbol')).toBeInTheDocument();
|
||||
expect(queryByText('Block Explorer URL')).toBeInTheDocument();
|
||||
expect(queryAllByText('(Optional)')).toHaveLength(2);
|
||||
expect(queryByText('Cancel')).toBeInTheDocument();
|
||||
expect(queryByText('Save')).toBeInTheDocument();
|
||||
});
|
||||
@ -62,35 +60,50 @@ describe('NetworkForm Component', () => {
|
||||
expect(queryByText('Network Name')).toBeInTheDocument();
|
||||
expect(queryByText('New RPC URL')).toBeInTheDocument();
|
||||
expect(queryByText('Chain ID')).toBeInTheDocument();
|
||||
expect(queryByText('Currency Symbol (optional)')).toBeInTheDocument();
|
||||
expect(queryByText('Block Explorer URL (optional)')).toBeInTheDocument();
|
||||
expect(queryByText('Currency Symbol')).toBeInTheDocument();
|
||||
expect(queryByText('Block Explorer URL')).toBeInTheDocument();
|
||||
expect(queryByText('Delete')).toBeInTheDocument();
|
||||
expect(queryByText('Cancel')).toBeInTheDocument();
|
||||
expect(queryByText('Save')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
getByDisplayValue(propNetworkDisplay.networkName),
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.label),
|
||||
).toBeInTheDocument();
|
||||
expect(getByDisplayValue(propNetworkDisplay.rpcUrl)).toBeInTheDocument();
|
||||
expect(getByDisplayValue(propNetworkDisplay.chainId)).toBeInTheDocument();
|
||||
expect(getByDisplayValue(propNetworkDisplay.ticker)).toBeInTheDocument();
|
||||
expect(
|
||||
getByDisplayValue(propNetworkDisplay.blockExplorerUrl),
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.rpcUrl),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.change(getByDisplayValue(propNetworkDisplay.networkName), {
|
||||
target: { value: 'LocalHost 8545' },
|
||||
});
|
||||
expect(
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.chainId),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.ticker),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.blockExplorerUrl),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.change(
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.label),
|
||||
{
|
||||
target: { value: 'LocalHost 8545' },
|
||||
},
|
||||
);
|
||||
expect(getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
|
||||
fireEvent.change(getByDisplayValue(propNetworkDisplay.chainId), {
|
||||
target: { value: '1' },
|
||||
});
|
||||
fireEvent.change(
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.chainId),
|
||||
{
|
||||
target: { value: '1' },
|
||||
},
|
||||
);
|
||||
expect(
|
||||
queryByText('This Chain ID is currently used by the mainnet network.'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(getByDisplayValue(propNetworkDisplay.rpcUrl), {
|
||||
target: { value: 'test' },
|
||||
});
|
||||
fireEvent.change(
|
||||
getByDisplayValue(propNetworkDisplay.selectedNetwork.rpcUrl),
|
||||
{
|
||||
target: { value: 'test' },
|
||||
},
|
||||
);
|
||||
expect(
|
||||
queryByText('URLs require the appropriate HTTP/HTTPS prefix.'),
|
||||
).toBeInTheDocument();
|
@ -0,0 +1 @@
|
||||
export { default } from './networks-list-item';
|
@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||
import { NETWORK_TYPE_RPC } from '../../../../../shared/constants/network';
|
||||
import { SIZES } from '../../../../helpers/constants/design-system';
|
||||
import ColorIndicator from '../../../../components/ui/color-indicator';
|
||||
import LockIcon from '../../../../components/ui/lock-icon';
|
||||
import { NETWORKS_FORM_ROUTE } from '../../../../helpers/constants/routes';
|
||||
import { setSelectedSettingsRpcUrl } from '../../../../store/actions';
|
||||
import { getEnvironmentType } from '../../../../../app/scripts/lib/util';
|
||||
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app';
|
||||
import { getProvider } from '../../../../selectors';
|
||||
|
||||
const NetworksListItem = ({ network, networkIsSelected, selectedRpcUrl }) => {
|
||||
const t = useI18nContext();
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const environmentType = getEnvironmentType();
|
||||
const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
|
||||
const provider = useSelector(getProvider);
|
||||
const {
|
||||
label,
|
||||
labelKey,
|
||||
rpcUrl,
|
||||
providerType: currentProviderType,
|
||||
} = network;
|
||||
|
||||
const listItemNetworkIsSelected = selectedRpcUrl && selectedRpcUrl === rpcUrl;
|
||||
const listItemUrlIsProviderUrl = rpcUrl === provider.rpcUrl;
|
||||
const listItemTypeIsProviderNonRpcType =
|
||||
provider.type !== NETWORK_TYPE_RPC && currentProviderType === provider.type;
|
||||
const listItemNetworkIsCurrentProvider =
|
||||
!networkIsSelected &&
|
||||
(listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType);
|
||||
const displayNetworkListItemAsSelected =
|
||||
listItemNetworkIsSelected || listItemNetworkIsCurrentProvider;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`settings-network-list-item:${rpcUrl}`}
|
||||
className="networks-tab__networks-list-item"
|
||||
onClick={() => {
|
||||
dispatch(setSelectedSettingsRpcUrl(rpcUrl));
|
||||
if (!isFullScreen) {
|
||||
history.push(NETWORKS_FORM_ROUTE);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ColorIndicator
|
||||
color={labelKey}
|
||||
type={ColorIndicator.TYPES.FILLED}
|
||||
size={SIZES.LG}
|
||||
/>
|
||||
<div
|
||||
className={classnames('networks-tab__networks-list-name', {
|
||||
'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected,
|
||||
'networks-tab__networks-list-name--disabled':
|
||||
currentProviderType !== NETWORK_TYPE_RPC &&
|
||||
!displayNetworkListItemAsSelected,
|
||||
})}
|
||||
>
|
||||
{label || t(labelKey)}
|
||||
{currentProviderType !== NETWORK_TYPE_RPC && (
|
||||
<LockIcon width="14px" height="17px" fill="#cdcdcd" />
|
||||
)}
|
||||
</div>
|
||||
<div className="networks-tab__networks-list-arrow" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NetworksListItem.propTypes = {
|
||||
network: PropTypes.object.isRequired,
|
||||
networkIsSelected: PropTypes.bool,
|
||||
selectedRpcUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
export default NetworksListItem;
|
@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||
import { defaultNetworksData } from '../networks-tab.constants';
|
||||
import NetworksListItem from '.';
|
||||
|
||||
const mockState = {
|
||||
metamask: {
|
||||
provider: {
|
||||
chainId: '0x4',
|
||||
nickname: '',
|
||||
rpcPrefs: {},
|
||||
rpcUrl: 'https://rinkeby.infura.io/v3/undefined',
|
||||
ticker: 'ETH',
|
||||
type: 'rinkeby',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (props) => {
|
||||
const store = configureMockStore([])(mockState);
|
||||
return renderWithProvider(<NetworksListItem {...props} />, store);
|
||||
};
|
||||
|
||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||
...network,
|
||||
viewOnly: true,
|
||||
}));
|
||||
|
||||
const MainnetProps = {
|
||||
network: defaultNetworks[0],
|
||||
networkIsSelected: false,
|
||||
selectedRpcUrl: 'http://localhost:8545',
|
||||
};
|
||||
const testNetProps = {
|
||||
network: defaultNetworks[1],
|
||||
networkIsSelected: false,
|
||||
selectedRpcUrl: 'http://localhost:8545',
|
||||
};
|
||||
|
||||
describe('NetworksListItem Component', () => {
|
||||
it('should render a Mainnet network item correctly', () => {
|
||||
const { queryByText } = renderComponent(MainnetProps);
|
||||
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a test network item correctly', () => {
|
||||
const { queryByText } = renderComponent(testNetProps);
|
||||
expect(queryByText('Ropsten Test Network')).toBeInTheDocument();
|
||||
});
|
||||
});
|
1
ui/pages/settings/networks-tab/networks-list/index.js
Normal file
1
ui/pages/settings/networks-tab/networks-list/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './networks-list';
|
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import NetworksListItem from '../networks-list-item';
|
||||
|
||||
const NetworksList = ({
|
||||
networkIsSelected,
|
||||
networksToRender,
|
||||
networkDefaultedToProvider,
|
||||
selectedRpcUrl,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classnames('networks-tab__networks-list', {
|
||||
'networks-tab__networks-list--selection':
|
||||
networkIsSelected && !networkDefaultedToProvider,
|
||||
})}
|
||||
>
|
||||
{networksToRender.map((network) => (
|
||||
<NetworksListItem
|
||||
key={`settings-network-list:${network.rpcUrl}`}
|
||||
network={network}
|
||||
networkIsSelected={networkIsSelected}
|
||||
selectedRpcUrl={selectedRpcUrl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NetworksList.propTypes = {
|
||||
networkDefaultedToProvider: PropTypes.bool,
|
||||
networkIsSelected: PropTypes.bool,
|
||||
networksToRender: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedRpcUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
export default NetworksList;
|
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||
import { defaultNetworksData } from '../networks-tab.constants';
|
||||
import NetworksList from '.';
|
||||
|
||||
const mockState = {
|
||||
metamask: {
|
||||
provider: {
|
||||
chainId: '0x4',
|
||||
nickname: '',
|
||||
rpcPrefs: {},
|
||||
rpcUrl: 'https://rinkeby.infura.io/v3/undefined',
|
||||
ticker: 'ETH',
|
||||
type: 'rinkeby',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (props) => {
|
||||
const store = configureMockStore([])(mockState);
|
||||
return renderWithProvider(<NetworksList {...props} />, store);
|
||||
};
|
||||
|
||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||
...network,
|
||||
viewOnly: true,
|
||||
}));
|
||||
|
||||
const props = {
|
||||
networkDefaultedToProvider: false,
|
||||
networkIsSelected: false,
|
||||
networksToRender: defaultNetworks,
|
||||
selectedRpcUrl: 'http://localhost:8545',
|
||||
};
|
||||
|
||||
describe('NetworksList Component', () => {
|
||||
it('should render a list of networks correctly', () => {
|
||||
const { queryByText } = renderComponent(props);
|
||||
|
||||
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
|
||||
expect(queryByText('Ropsten Test Network')).toBeInTheDocument();
|
||||
expect(queryByText('Rinkeby Test Network')).toBeInTheDocument();
|
||||
expect(queryByText('Goerli Test Network')).toBeInTheDocument();
|
||||
expect(queryByText('Kovan Test Network')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1 @@
|
||||
export { default } from './networks-tab-content';
|
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import NetworksForm from '../networks-form';
|
||||
import NetworksList from '../networks-list';
|
||||
import { getProvider } from '../../../../selectors';
|
||||
|
||||
const NetworksTabContent = ({
|
||||
networkDefaultedToProvider,
|
||||
networkIsSelected,
|
||||
networksToRender,
|
||||
selectedNetwork,
|
||||
shouldRenderNetworkForm,
|
||||
}) => {
|
||||
const provider = useSelector(getProvider);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NetworksList
|
||||
networkDefaultedToProvider={networkDefaultedToProvider}
|
||||
networkIsSelected={networkIsSelected}
|
||||
networksToRender={networksToRender}
|
||||
selectedRpcUrl={selectedNetwork.rpcUrl}
|
||||
/>
|
||||
{shouldRenderNetworkForm ? (
|
||||
<NetworksForm
|
||||
isCurrentRpcTarget={provider.rpcUrl === selectedNetwork.rpcUrl}
|
||||
networksToRender={networksToRender}
|
||||
selectedNetwork={selectedNetwork}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
NetworksTabContent.propTypes = {
|
||||
networkDefaultedToProvider: PropTypes.bool,
|
||||
networkIsSelected: PropTypes.bool,
|
||||
networksToRender: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedNetwork: PropTypes.object,
|
||||
shouldRenderNetworkForm: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default NetworksTabContent;
|
@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||
import { defaultNetworksData } from '../networks-tab.constants';
|
||||
import NetworksTabContent from '.';
|
||||
|
||||
const mockState = {
|
||||
metamask: {
|
||||
provider: {
|
||||
chainId: '0x539',
|
||||
nickname: '',
|
||||
rpcPrefs: {},
|
||||
rpcUrl: 'http://localhost:8545',
|
||||
ticker: 'ETH',
|
||||
type: 'localhost',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (props) => {
|
||||
const store = configureMockStore([])(mockState);
|
||||
return renderWithProvider(<NetworksTabContent {...props} />, store);
|
||||
};
|
||||
|
||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||
...network,
|
||||
viewOnly: true,
|
||||
}));
|
||||
|
||||
const props = {
|
||||
networkDefaultedToProvider: false,
|
||||
networkIsSelected: true,
|
||||
networksToRender: defaultNetworks,
|
||||
selectedNetwork: {
|
||||
rpcUrl: 'http://localhost:8545',
|
||||
chainId: '1337',
|
||||
ticker: 'ETH',
|
||||
label: 'LocalHost',
|
||||
blockExplorerUrl: '',
|
||||
viewOnly: false,
|
||||
rpcPrefs: {},
|
||||
},
|
||||
shouldRenderNetworkForm: true,
|
||||
};
|
||||
|
||||
describe('NetworksTabContent Component', () => {
|
||||
it('should render networks tab content correctly', () => {
|
||||
const { queryByText, getByDisplayValue } = renderComponent(props);
|
||||
|
||||
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
|
||||
expect(queryByText('Ropsten Test Network')).toBeInTheDocument();
|
||||
expect(queryByText('Rinkeby Test Network')).toBeInTheDocument();
|
||||
expect(queryByText('Goerli Test Network')).toBeInTheDocument();
|
||||
expect(queryByText('Kovan Test Network')).toBeInTheDocument();
|
||||
|
||||
expect(queryByText('Network Name')).toBeInTheDocument();
|
||||
expect(queryByText('New RPC URL')).toBeInTheDocument();
|
||||
expect(queryByText('Chain ID')).toBeInTheDocument();
|
||||
expect(queryByText('Currency Symbol')).toBeInTheDocument();
|
||||
expect(queryByText('Block Explorer URL')).toBeInTheDocument();
|
||||
expect(queryByText('Cancel')).toBeInTheDocument();
|
||||
expect(queryByText('Save')).toBeInTheDocument();
|
||||
|
||||
expect(getByDisplayValue(props.selectedNetwork.label)).toBeInTheDocument();
|
||||
expect(getByDisplayValue(props.selectedNetwork.rpcUrl)).toBeInTheDocument();
|
||||
expect(
|
||||
getByDisplayValue(props.selectedNetwork.chainId),
|
||||
).toBeInTheDocument();
|
||||
expect(getByDisplayValue(props.selectedNetwork.ticker)).toBeInTheDocument();
|
||||
expect(
|
||||
getByDisplayValue(props.selectedNetwork.blockExplorerUrl),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.change(getByDisplayValue(props.selectedNetwork.label), {
|
||||
target: { value: 'LocalHost 8545' },
|
||||
});
|
||||
expect(getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
|
||||
fireEvent.change(getByDisplayValue(props.selectedNetwork.chainId), {
|
||||
target: { value: '1' },
|
||||
});
|
||||
expect(
|
||||
queryByText('This Chain ID is currently used by the mainnet network.'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(getByDisplayValue(props.selectedNetwork.rpcUrl), {
|
||||
target: { value: 'test' },
|
||||
});
|
||||
expect(
|
||||
queryByText('URLs require the appropriate HTTP/HTTPS prefix.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1 @@
|
||||
export { default } from './networks-tab-subheader';
|
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||
import { ADD_NETWORK_ROUTE } from '../../../../helpers/constants/routes';
|
||||
import Button from '../../../../components/ui/button';
|
||||
|
||||
const NetworksFormSubheader = ({ addNewNetwork }) => {
|
||||
const t = useI18nContext();
|
||||
const history = useHistory();
|
||||
return addNewNetwork ? (
|
||||
<div className="networks-tab__subheader">
|
||||
<span className="networks-tab__sub-header-text">{t('networks')}</span>
|
||||
<span>{' > '}</span>
|
||||
<div className="networks-tab__subheader--break">{t('addANetwork')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="settings-page__sub-header">
|
||||
<span className="settings-page__sub-header-text">{t('networks')}</span>
|
||||
<div className="networks-tab__add-network-header-button-wrapper">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
history.push(ADD_NETWORK_ROUTE);
|
||||
}}
|
||||
>
|
||||
{t('addANetwork')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NetworksFormSubheader.propTypes = {
|
||||
addNewNetwork: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default NetworksFormSubheader;
|
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { renderWithProvider } from '../../../../../test/jest/rendering';
|
||||
import NetworksTabSubheader from '.';
|
||||
|
||||
const mockState = {
|
||||
metamask: {
|
||||
provider: {
|
||||
chainId: '0x539',
|
||||
nickname: '',
|
||||
rpcPrefs: {},
|
||||
rpcUrl: 'http://localhost:8545',
|
||||
ticker: 'ETH',
|
||||
type: 'localhost',
|
||||
},
|
||||
frequentRpcListDetail: [],
|
||||
},
|
||||
appState: {
|
||||
networksTabSelectedRpcUrl: 'http://localhost:8545',
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (props) => {
|
||||
const store = configureMockStore([])(mockState);
|
||||
return renderWithProvider(<NetworksTabSubheader {...props} />, store);
|
||||
};
|
||||
|
||||
describe('NetworksTabSubheader Component', () => {
|
||||
it('should render network subheader correctly', () => {
|
||||
const { queryByText, getByRole } = renderComponent({
|
||||
addNewNetwork: false,
|
||||
});
|
||||
|
||||
expect(queryByText('Networks')).toBeInTheDocument();
|
||||
expect(queryByText('Add a network')).toBeInTheDocument();
|
||||
expect(getByRole('button', { text: 'Add a network' })).toBeDefined();
|
||||
});
|
||||
it('should render add network form subheader correctly', () => {
|
||||
const { queryByText } = renderComponent({
|
||||
addNewNetwork: true,
|
||||
});
|
||||
expect(queryByText('Networks')).toBeInTheDocument();
|
||||
expect(queryByText('>')).toBeInTheDocument();
|
||||
expect(queryByText('Add a network')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,259 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { NETWORK_TYPE_RPC } from '../../../../shared/constants/network';
|
||||
import Button from '../../../components/ui/button';
|
||||
import LockIcon from '../../../components/ui/lock-icon';
|
||||
import {
|
||||
NETWORKS_ROUTE,
|
||||
NETWORKS_FORM_ROUTE,
|
||||
DEFAULT_ROUTE,
|
||||
ADD_NETWORK_ROUTE,
|
||||
} from '../../../helpers/constants/routes';
|
||||
import ColorIndicator from '../../../components/ui/color-indicator';
|
||||
import { SIZES } from '../../../helpers/constants/design-system';
|
||||
import NetworkForm from './network-form';
|
||||
|
||||
export default class NetworksTab extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
metricsEvent: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
editRpc: PropTypes.func.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
networkIsSelected: PropTypes.bool,
|
||||
networksToRender: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedNetwork: PropTypes.object,
|
||||
setRpcTarget: PropTypes.func.isRequired,
|
||||
setSelectedSettingsRpcUrl: PropTypes.func.isRequired,
|
||||
showConfirmDeleteNetworkModal: PropTypes.func.isRequired,
|
||||
providerUrl: PropTypes.string,
|
||||
providerType: PropTypes.string,
|
||||
networkDefaultedToProvider: PropTypes.bool,
|
||||
history: PropTypes.object.isRequired,
|
||||
shouldRenderNetworkForm: PropTypes.bool.isRequired,
|
||||
isFullScreen: PropTypes.bool.isRequired,
|
||||
setNewNetworkAdded: PropTypes.func.isRequired,
|
||||
addNewNetwork: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.setSelectedSettingsRpcUrl('');
|
||||
}
|
||||
|
||||
isCurrentPath(pathname) {
|
||||
return this.props.location.pathname === pathname;
|
||||
}
|
||||
|
||||
renderSubHeader() {
|
||||
const { history } = this.props;
|
||||
|
||||
return (
|
||||
<div className="settings-page__sub-header">
|
||||
<span className="settings-page__sub-header-text">
|
||||
{this.context.t('networks')}
|
||||
</span>
|
||||
<div className="networks-tab__add-network-header-button-wrapper">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
history.push(ADD_NETWORK_ROUTE);
|
||||
}}
|
||||
className="add-network-form__header-add-network-button"
|
||||
>
|
||||
{this.context.t('addANetwork')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderNetworkListItem(network, selectRpcUrl) {
|
||||
const {
|
||||
setSelectedSettingsRpcUrl,
|
||||
networkIsSelected,
|
||||
providerUrl,
|
||||
providerType,
|
||||
history,
|
||||
isFullScreen,
|
||||
} = this.props;
|
||||
const {
|
||||
label,
|
||||
labelKey,
|
||||
rpcUrl,
|
||||
providerType: currentProviderType,
|
||||
} = network;
|
||||
|
||||
const listItemNetworkIsSelected = selectRpcUrl && selectRpcUrl === rpcUrl;
|
||||
const listItemUrlIsProviderUrl = rpcUrl === providerUrl;
|
||||
const listItemTypeIsProviderNonRpcType =
|
||||
providerType !== NETWORK_TYPE_RPC && currentProviderType === providerType;
|
||||
const listItemNetworkIsCurrentProvider =
|
||||
!networkIsSelected &&
|
||||
(listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType);
|
||||
const displayNetworkListItemAsSelected =
|
||||
listItemNetworkIsSelected || listItemNetworkIsCurrentProvider;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`settings-network-list-item:${rpcUrl}`}
|
||||
className="networks-tab__networks-list-item"
|
||||
onClick={() => {
|
||||
setSelectedSettingsRpcUrl(rpcUrl);
|
||||
if (!isFullScreen) {
|
||||
history.push(NETWORKS_FORM_ROUTE);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ColorIndicator
|
||||
color={labelKey}
|
||||
type={ColorIndicator.TYPES.FILLED}
|
||||
size={SIZES.LG}
|
||||
/>
|
||||
<div
|
||||
className={classnames('networks-tab__networks-list-name', {
|
||||
'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected,
|
||||
'networks-tab__networks-list-name--disabled':
|
||||
currentProviderType !== NETWORK_TYPE_RPC &&
|
||||
!displayNetworkListItemAsSelected,
|
||||
})}
|
||||
>
|
||||
{label || this.context.t(labelKey)}
|
||||
{currentProviderType !== NETWORK_TYPE_RPC && (
|
||||
<LockIcon width="14px" height="17px" fill="#cdcdcd" />
|
||||
)}
|
||||
</div>
|
||||
<div className="networks-tab__networks-list-arrow" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderNetworksList() {
|
||||
const {
|
||||
networksToRender,
|
||||
selectedNetwork,
|
||||
networkIsSelected,
|
||||
networkDefaultedToProvider,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('networks-tab__networks-list', {
|
||||
'networks-tab__networks-list--selection':
|
||||
networkIsSelected && !networkDefaultedToProvider,
|
||||
})}
|
||||
>
|
||||
{networksToRender.map((network) =>
|
||||
this.renderNetworkListItem(network, selectedNetwork.rpcUrl),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderNetworksTabContent() {
|
||||
const { t } = this.context;
|
||||
const {
|
||||
setRpcTarget,
|
||||
showConfirmDeleteNetworkModal,
|
||||
setSelectedSettingsRpcUrl,
|
||||
selectedNetwork: {
|
||||
labelKey,
|
||||
label,
|
||||
rpcUrl,
|
||||
chainId,
|
||||
ticker,
|
||||
viewOnly,
|
||||
rpcPrefs,
|
||||
blockExplorerUrl,
|
||||
},
|
||||
editRpc,
|
||||
providerUrl,
|
||||
networksToRender,
|
||||
history,
|
||||
isFullScreen,
|
||||
shouldRenderNetworkForm,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderNetworksList()}
|
||||
{shouldRenderNetworkForm ? (
|
||||
<NetworkForm
|
||||
setRpcTarget={setRpcTarget}
|
||||
editRpc={editRpc}
|
||||
networkName={label || (labelKey && t(labelKey)) || ''}
|
||||
rpcUrl={rpcUrl}
|
||||
chainId={chainId}
|
||||
networksToRender={networksToRender}
|
||||
ticker={ticker}
|
||||
onClear={(shouldUpdateHistory = true) => {
|
||||
setSelectedSettingsRpcUrl('');
|
||||
if (shouldUpdateHistory) {
|
||||
history.push(NETWORKS_ROUTE);
|
||||
}
|
||||
}}
|
||||
showConfirmDeleteNetworkModal={showConfirmDeleteNetworkModal}
|
||||
viewOnly={viewOnly}
|
||||
isCurrentRpcTarget={providerUrl === rpcUrl}
|
||||
rpcPrefs={rpcPrefs}
|
||||
blockExplorerUrl={blockExplorerUrl}
|
||||
isFullScreen={isFullScreen}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
history,
|
||||
isFullScreen,
|
||||
shouldRenderNetworkForm,
|
||||
setRpcTarget,
|
||||
networksToRender,
|
||||
setNewNetworkAdded,
|
||||
selectedNetwork: { rpcPrefs },
|
||||
addNewNetwork,
|
||||
} = this.props;
|
||||
return addNewNetwork ? (
|
||||
<NetworkForm
|
||||
setRpcTarget={setRpcTarget}
|
||||
onClear={(shouldUpdateHistory = true) => {
|
||||
if (shouldUpdateHistory) {
|
||||
history.push(NETWORKS_ROUTE);
|
||||
}
|
||||
}}
|
||||
onAddNetwork={() => {
|
||||
history.push(DEFAULT_ROUTE);
|
||||
}}
|
||||
rpcPrefs={rpcPrefs}
|
||||
networksToRender={networksToRender}
|
||||
setNewNetworkAdded={setNewNetworkAdded}
|
||||
addNewNetwork={addNewNetwork}
|
||||
/>
|
||||
) : (
|
||||
<div className="networks-tab__body">
|
||||
{isFullScreen ? this.renderSubHeader() : null}
|
||||
<div className="networks-tab__content">
|
||||
{this.renderNetworksTabContent()}
|
||||
{!isFullScreen && !shouldRenderNetworkForm ? (
|
||||
<div className="networks-tab__networks-list-popup-footer">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
global.platform.openExtensionInBrowser(ADD_NETWORK_ROUTE);
|
||||
}}
|
||||
>
|
||||
{this.context.t('addNetwork')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import {
|
||||
setSelectedSettingsRpcUrl,
|
||||
updateAndSetCustomRpc,
|
||||
displayWarning,
|
||||
editRpc,
|
||||
showModal,
|
||||
setNewNetworkAdded,
|
||||
} from '../../../store/actions';
|
||||
import {
|
||||
ADD_NETWORK_ROUTE,
|
||||
NETWORKS_FORM_ROUTE,
|
||||
} from '../../../helpers/constants/routes';
|
||||
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
|
||||
import { NETWORK_TYPE_RPC } from '../../../../shared/constants/network';
|
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
import NetworksTab from './networks-tab.component';
|
||||
import { defaultNetworksData } from './networks-tab.constants';
|
||||
|
||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||
...network,
|
||||
viewOnly: true,
|
||||
}));
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const {
|
||||
location: { pathname },
|
||||
} = ownProps;
|
||||
|
||||
const environmentType = getEnvironmentType();
|
||||
const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
|
||||
const shouldRenderNetworkForm =
|
||||
isFullScreen || Boolean(pathname.match(NETWORKS_FORM_ROUTE));
|
||||
const addNewNetwork = Boolean(pathname.match(ADD_NETWORK_ROUTE));
|
||||
|
||||
const { frequentRpcListDetail, provider } = state.metamask;
|
||||
const { networksTabSelectedRpcUrl } = state.appState;
|
||||
const frequentRpcNetworkListDetails = frequentRpcListDetail.map((rpc) => {
|
||||
return {
|
||||
label: rpc.nickname,
|
||||
iconColor: '#6A737D',
|
||||
providerType: NETWORK_TYPE_RPC,
|
||||
rpcUrl: rpc.rpcUrl,
|
||||
chainId: rpc.chainId,
|
||||
ticker: rpc.ticker,
|
||||
blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '',
|
||||
};
|
||||
});
|
||||
|
||||
const networksToRender = [
|
||||
...defaultNetworks,
|
||||
...frequentRpcNetworkListDetails,
|
||||
];
|
||||
let selectedNetwork =
|
||||
networksToRender.find(
|
||||
(network) => network.rpcUrl === networksTabSelectedRpcUrl,
|
||||
) || {};
|
||||
const networkIsSelected = Boolean(selectedNetwork.rpcUrl);
|
||||
|
||||
let networkDefaultedToProvider = false;
|
||||
if (!networkIsSelected) {
|
||||
selectedNetwork =
|
||||
networksToRender.find((network) => {
|
||||
return (
|
||||
network.rpcUrl === provider.rpcUrl ||
|
||||
(network.providerType !== NETWORK_TYPE_RPC &&
|
||||
network.providerType === provider.type)
|
||||
);
|
||||
}) || {};
|
||||
networkDefaultedToProvider = true;
|
||||
}
|
||||
|
||||
return {
|
||||
selectedNetwork,
|
||||
networksToRender,
|
||||
networkIsSelected,
|
||||
providerType: provider.type,
|
||||
providerUrl: provider.rpcUrl,
|
||||
networkDefaultedToProvider,
|
||||
isFullScreen,
|
||||
shouldRenderNetworkForm,
|
||||
addNewNetwork,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
setSelectedSettingsRpcUrl: (newRpcUrl) =>
|
||||
dispatch(setSelectedSettingsRpcUrl(newRpcUrl)),
|
||||
setRpcTarget: (newRpc, chainId, ticker, nickname, rpcPrefs) => {
|
||||
return dispatch(
|
||||
updateAndSetCustomRpc(newRpc, chainId, ticker, nickname, rpcPrefs),
|
||||
);
|
||||
},
|
||||
showConfirmDeleteNetworkModal: ({ target, onConfirm }) => {
|
||||
return dispatch(
|
||||
showModal({ name: 'CONFIRM_DELETE_NETWORK', target, onConfirm }),
|
||||
);
|
||||
},
|
||||
displayWarning: (warning) => dispatch(displayWarning(warning)),
|
||||
editRpc: (oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs) => {
|
||||
return dispatch(
|
||||
editRpc(oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs),
|
||||
);
|
||||
},
|
||||
setNewNetworkAdded: (newNetwork) => {
|
||||
dispatch(setNewNetworkAdded(newNetwork));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(NetworksTab);
|
129
ui/pages/settings/networks-tab/networks-tab.js
Normal file
129
ui/pages/settings/networks-tab/networks-tab.js
Normal file
@ -0,0 +1,129 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import {
|
||||
ADD_NETWORK_ROUTE,
|
||||
NETWORKS_FORM_ROUTE,
|
||||
} from '../../../helpers/constants/routes';
|
||||
import { setSelectedSettingsRpcUrl } from '../../../store/actions';
|
||||
import Button from '../../../components/ui/button';
|
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
|
||||
import {
|
||||
getFrequentRpcListDetail,
|
||||
getNetworksTabSelectedRpcUrl,
|
||||
getProvider,
|
||||
} from '../../../selectors';
|
||||
import { NETWORK_TYPE_RPC } from '../../../../shared/constants/network';
|
||||
import { defaultNetworksData } from './networks-tab.constants';
|
||||
import NetworksTabContent from './networks-tab-content';
|
||||
import NetworksForm from './networks-form';
|
||||
import NetworksFormSubheader from './networks-tab-subheader';
|
||||
|
||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||
...network,
|
||||
viewOnly: true,
|
||||
}));
|
||||
|
||||
const NetworksTab = ({ addNewNetwork }) => {
|
||||
const t = useI18nContext();
|
||||
const dispatch = useDispatch();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const environmentType = getEnvironmentType();
|
||||
const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
|
||||
const shouldRenderNetworkForm =
|
||||
isFullScreen || Boolean(pathname.match(NETWORKS_FORM_ROUTE));
|
||||
|
||||
const frequentRpcListDetail = useSelector(getFrequentRpcListDetail);
|
||||
const provider = useSelector(getProvider);
|
||||
const networksTabSelectedRpcUrl = useSelector(getNetworksTabSelectedRpcUrl);
|
||||
|
||||
const frequentRpcNetworkListDetails = frequentRpcListDetail.map((rpc) => {
|
||||
return {
|
||||
label: rpc.nickname,
|
||||
iconColor: '#6A737D',
|
||||
providerType: NETWORK_TYPE_RPC,
|
||||
rpcUrl: rpc.rpcUrl,
|
||||
chainId: rpc.chainId,
|
||||
ticker: rpc.ticker,
|
||||
blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '',
|
||||
};
|
||||
});
|
||||
|
||||
const networksToRender = [
|
||||
...defaultNetworks,
|
||||
...frequentRpcNetworkListDetails,
|
||||
];
|
||||
let selectedNetwork =
|
||||
networksToRender.find(
|
||||
(network) => network.rpcUrl === networksTabSelectedRpcUrl,
|
||||
) || {};
|
||||
const networkIsSelected = Boolean(selectedNetwork.rpcUrl);
|
||||
|
||||
let networkDefaultedToProvider = false;
|
||||
if (!networkIsSelected) {
|
||||
selectedNetwork =
|
||||
networksToRender.find((network) => {
|
||||
return (
|
||||
network.rpcUrl === provider.rpcUrl ||
|
||||
(network.providerType !== NETWORK_TYPE_RPC &&
|
||||
network.providerType === provider.type)
|
||||
);
|
||||
}) || {};
|
||||
networkDefaultedToProvider = true;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(setSelectedSettingsRpcUrl(''));
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className="networks-tab__body">
|
||||
{isFullScreen ? (
|
||||
<NetworksFormSubheader addNewNetwork={addNewNetwork} />
|
||||
) : null}
|
||||
<div className="networks-tab__content">
|
||||
{addNewNetwork ? (
|
||||
<NetworksForm
|
||||
networksToRender={networksToRender}
|
||||
addNewNetwork={addNewNetwork}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<NetworksTabContent
|
||||
networkDefaultedToProvider={networkDefaultedToProvider}
|
||||
networkIsSelected={networkIsSelected}
|
||||
networksToRender={networksToRender}
|
||||
providerUrl={provider.rpcUrl}
|
||||
selectedNetwork={selectedNetwork}
|
||||
shouldRenderNetworkForm={shouldRenderNetworkForm}
|
||||
/>
|
||||
{!isFullScreen && !shouldRenderNetworkForm ? (
|
||||
<div className="networks-tab__networks-list-popup-footer">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
global.platform.openExtensionInBrowser(ADD_NETWORK_ROUTE);
|
||||
}}
|
||||
>
|
||||
{t('addNetwork')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NetworksTab.propTypes = {
|
||||
addNewNetwork: PropTypes.bool,
|
||||
};
|
||||
export default NetworksTab;
|
53
ui/pages/settings/networks-tab/networks-tab.test.js
Normal file
53
ui/pages/settings/networks-tab/networks-tab.test.js
Normal file
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { renderWithProvider } from '../../../../test/jest/rendering';
|
||||
import NetworksTab from '.';
|
||||
|
||||
const mockState = {
|
||||
metamask: {
|
||||
provider: {
|
||||
chainId: '0x539',
|
||||
nickname: '',
|
||||
rpcPrefs: {},
|
||||
rpcUrl: 'http://localhost:8545',
|
||||
ticker: 'ETH',
|
||||
type: 'localhost',
|
||||
},
|
||||
frequentRpcListDetail: [],
|
||||
},
|
||||
appState: {
|
||||
networksTabSelectedRpcUrl: 'http://localhost:8545',
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (props) => {
|
||||
const store = configureMockStore([])(mockState);
|
||||
return renderWithProvider(<NetworksTab {...props} />, store);
|
||||
};
|
||||
|
||||
describe('NetworksTab Component', () => {
|
||||
it('should render networks tab content correctly', () => {
|
||||
const { queryByText } = renderComponent({
|
||||
addNewNetwork: false,
|
||||
});
|
||||
|
||||
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
|
||||
expect(queryByText('Ropsten Test Network')).toBeInTheDocument();
|
||||
expect(queryByText('Rinkeby Test Network')).toBeInTheDocument();
|
||||
expect(queryByText('Goerli Test Network')).toBeInTheDocument();
|
||||
expect(queryByText('Kovan Test Network')).toBeInTheDocument();
|
||||
expect(queryByText('Add Network')).toBeInTheDocument();
|
||||
});
|
||||
it('should render add network form correctly', () => {
|
||||
const { queryByText } = renderComponent({
|
||||
addNewNetwork: true,
|
||||
});
|
||||
expect(queryByText('Network Name')).toBeInTheDocument();
|
||||
expect(queryByText('New RPC URL')).toBeInTheDocument();
|
||||
expect(queryByText('Chain ID')).toBeInTheDocument();
|
||||
expect(queryByText('Currency Symbol')).toBeInTheDocument();
|
||||
expect(queryByText('Block Explorer URL')).toBeInTheDocument();
|
||||
expect(queryByText('Cancel')).toBeInTheDocument();
|
||||
expect(queryByText('Save')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -231,7 +231,11 @@ class SettingsPage extends PureComponent {
|
||||
<Route exact path={ABOUT_US_ROUTE} component={InfoTab} />
|
||||
<Route exact path={ADVANCED_ROUTE} component={AdvancedTab} />
|
||||
<Route exact path={ALERTS_ROUTE} component={AlertsTab} />
|
||||
<Route exact path={ADD_NETWORK_ROUTE} component={NetworksTab} />
|
||||
<Route
|
||||
exact
|
||||
path={ADD_NETWORK_ROUTE}
|
||||
render={() => <NetworksTab addNewNetwork />}
|
||||
/>
|
||||
<Route path={NETWORKS_ROUTE} component={NetworksTab} />
|
||||
<Route exact path={SECURITY_ROUTE} component={SecurityTab} />
|
||||
<Route exact path={EXPERIMENTAL_ROUTE} component={ExperimentalTab} />
|
||||
|
@ -688,3 +688,15 @@ export function doesAddressRequireLedgerHidConnection(state, address) {
|
||||
export function getNewNetworkAdded(state) {
|
||||
return state.appState.newNetworkAdded;
|
||||
}
|
||||
|
||||
export function getNetworksTabSelectedRpcUrl(state) {
|
||||
return state.appState.networksTabSelectedRpcUrl;
|
||||
}
|
||||
|
||||
export function getProvider(state) {
|
||||
return state.metamask.provider;
|
||||
}
|
||||
|
||||
export function getFrequentRpcListDetail(state) {
|
||||
return state.metamask.frequentRpcListDetail;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user