Network tab refactor (#12502)

feature/default_network_editable
Niranjana Binoy 3 years ago committed by GitHub
parent e3e6da1a75
commit 524725b24b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      app/_locales/am/messages.json
  2. 6
      app/_locales/ar/messages.json
  3. 6
      app/_locales/bg/messages.json
  4. 6
      app/_locales/bn/messages.json
  5. 6
      app/_locales/ca/messages.json
  6. 6
      app/_locales/da/messages.json
  7. 3
      app/_locales/de/messages.json
  8. 6
      app/_locales/el/messages.json
  9. 7
      app/_locales/en/messages.json
  10. 6
      app/_locales/es/messages.json
  11. 6
      app/_locales/es_419/messages.json
  12. 6
      app/_locales/et/messages.json
  13. 6
      app/_locales/fa/messages.json
  14. 6
      app/_locales/fi/messages.json
  15. 6
      app/_locales/fil/messages.json
  16. 6
      app/_locales/fr/messages.json
  17. 6
      app/_locales/he/messages.json
  18. 6
      app/_locales/hi/messages.json
  19. 6
      app/_locales/hr/messages.json
  20. 6
      app/_locales/hu/messages.json
  21. 6
      app/_locales/id/messages.json
  22. 6
      app/_locales/it/messages.json
  23. 6
      app/_locales/ja/messages.json
  24. 6
      app/_locales/kn/messages.json
  25. 6
      app/_locales/ko/messages.json
  26. 6
      app/_locales/lt/messages.json
  27. 6
      app/_locales/lv/messages.json
  28. 6
      app/_locales/ms/messages.json
  29. 6
      app/_locales/no/messages.json
  30. 6
      app/_locales/ph/messages.json
  31. 6
      app/_locales/pl/messages.json
  32. 6
      app/_locales/pt_BR/messages.json
  33. 6
      app/_locales/ro/messages.json
  34. 6
      app/_locales/ru/messages.json
  35. 6
      app/_locales/sk/messages.json
  36. 6
      app/_locales/sl/messages.json
  37. 6
      app/_locales/sr/messages.json
  38. 6
      app/_locales/sv/messages.json
  39. 6
      app/_locales/sw/messages.json
  40. 6
      app/_locales/tl/messages.json
  41. 6
      app/_locales/uk/messages.json
  42. 6
      app/_locales/vi/messages.json
  43. 6
      app/_locales/zh_CN/messages.json
  44. 6
      app/_locales/zh_TW/messages.json
  45. 39
      test/e2e/tests/custom-rpc-history.spec.js
  46. 6
      ui/components/app/dropdowns/network-dropdown.js
  47. 2
      ui/components/ui/form-field/form-field.js
  48. 2
      ui/pages/settings/networks-tab/index.js
  49. 91
      ui/pages/settings/networks-tab/index.scss
  50. 1
      ui/pages/settings/networks-tab/network-form/index.js
  51. 76
      ui/pages/settings/networks-tab/network-form/index.scss
  52. 761
      ui/pages/settings/networks-tab/network-form/network-form.component.js
  53. 1
      ui/pages/settings/networks-tab/networks-form/index.js
  54. 575
      ui/pages/settings/networks-tab/networks-form/networks-form.js
  55. 89
      ui/pages/settings/networks-tab/networks-form/networks-form.test.js
  56. 1
      ui/pages/settings/networks-tab/networks-list-item/index.js
  57. 81
      ui/pages/settings/networks-tab/networks-list-item/networks-list-item.js
  58. 51
      ui/pages/settings/networks-tab/networks-list-item/networks-list-item.test.js
  59. 1
      ui/pages/settings/networks-tab/networks-list/index.js
  60. 38
      ui/pages/settings/networks-tab/networks-list/networks-list.js
  61. 47
      ui/pages/settings/networks-tab/networks-list/networks-list.test.js
  62. 1
      ui/pages/settings/networks-tab/networks-tab-content/index.js
  63. 43
      ui/pages/settings/networks-tab/networks-tab-content/networks-tab-content.js
  64. 92
      ui/pages/settings/networks-tab/networks-tab-content/networks-tab-content.test.js
  65. 1
      ui/pages/settings/networks-tab/networks-tab-subheader/index.js
  66. 39
      ui/pages/settings/networks-tab/networks-tab-subheader/networks-tab-subheader.js
  67. 46
      ui/pages/settings/networks-tab/networks-tab-subheader/networks-tab-subheader.test.js
  68. 259
      ui/pages/settings/networks-tab/networks-tab.component.js
  69. 117
      ui/pages/settings/networks-tab/networks-tab.container.js
  70. 129
      ui/pages/settings/networks-tab/networks-tab.js
  71. 53
      ui/pages/settings/networks-tab/networks-tab.test.js
  72. 6
      ui/pages/settings/settings.component.js
  73. 12
      ui/selectors/selectors.js

@ -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;
}
}
&__network-form-row {
@media screen and (max-width: $break-small) {
width: 93%;
}
&__add-network-form {
display: flex;
flex-direction: column;
max-height: 465px;
}
&--warning {
@include H7;
&__network-form-body {
display: grid;
grid-template-columns: 100%;
width: 95%;
background-color: #fefae8;
border: 1px solid #ffd33d;
border-radius: 5px;
box-sizing: border-box;
padding: 12px;
margin: 12px 0;
&__view-only {
width: 100%;
}
}
@media screen and (max-width: $break-small) {
width: 93%;
}
&__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: 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();
}
}

@ -0,0 +1 @@
export { default } from './networks-form';

@ -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.selectedNetwork.rpcUrl),
).toBeInTheDocument();
expect(
getByDisplayValue(propNetworkDisplay.selectedNetwork.chainId),
).toBeInTheDocument();
expect(getByDisplayValue(propNetworkDisplay.rpcUrl)).toBeInTheDocument();
expect(getByDisplayValue(propNetworkDisplay.chainId)).toBeInTheDocument();
expect(getByDisplayValue(propNetworkDisplay.ticker)).toBeInTheDocument();
expect(
getByDisplayValue(propNetworkDisplay.blockExplorerUrl),
getByDisplayValue(propNetworkDisplay.selectedNetwork.ticker),
).toBeInTheDocument();
fireEvent.change(getByDisplayValue(propNetworkDisplay.networkName), {
target: { value: 'LocalHost 8545' },
});
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();
});
});

@ -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);

@ -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;

@ -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…
Cancel
Save