From 9c80abe8b6fb6875e764b1184eb209c169b2ec0a Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 12 Apr 2019 14:28:59 +0800 Subject: [PATCH 01/21] add PermissionsController remove provider approval controller integrate rpc-cap create PermissionsController move provider approval functionality to permissions controller add permissions approval ui, settings page add permissions activity and history move some functionality to metamask-inpage-provider rename siteMetadata -> domainMetadata add accountsChange notification to inpage provider move functionality to inpage provider update inpage provider Remove 'Connections' settings page (#7369) add hooks for exposing accounts in settings rename unused messages in non-English locales Add external extension id to metadata (#7396) update inpage provider, rpc-cap add eth_requestAccounts handling to background prevent notifying connections if extension is locked update inpage provider Fix lint errors add migration review fixes transaction controller review updates removed unused messages --- app/_locales/am/messages.json | 6 - app/_locales/ar/messages.json | 6 - app/_locales/bg/messages.json | 6 - app/_locales/bn/messages.json | 6 - app/_locales/ca/messages.json | 6 - app/_locales/cs/messages.json | 3 - app/_locales/da/messages.json | 6 - app/_locales/de/messages.json | 6 - app/_locales/el/messages.json | 6 - app/_locales/en/messages.json | 8 +- app/_locales/es/messages.json | 6 - app/_locales/es_419/messages.json | 6 - app/_locales/et/messages.json | 6 - app/_locales/fa/messages.json | 6 - app/_locales/fi/messages.json | 6 - app/_locales/fil/messages.json | 3 - app/_locales/fr/messages.json | 6 - app/_locales/he/messages.json | 6 - app/_locales/hi/messages.json | 6 - app/_locales/hn/messages.json | 3 - app/_locales/hr/messages.json | 6 - app/_locales/ht/messages.json | 3 - app/_locales/hu/messages.json | 6 - app/_locales/id/messages.json | 6 - app/_locales/it/messages.json | 6 - app/_locales/ja/messages.json | 3 - app/_locales/kn/messages.json | 6 - app/_locales/ko/messages.json | 6 - app/_locales/lt/messages.json | 6 - app/_locales/lv/messages.json | 6 - app/_locales/ms/messages.json | 6 - app/_locales/nl/messages.json | 3 - app/_locales/no/messages.json | 6 - app/_locales/ph/messages.json | 3 - app/_locales/pl/messages.json | 6 - app/_locales/pt/messages.json | 3 - app/_locales/pt_BR/messages.json | 6 - app/_locales/ro/messages.json | 6 - app/_locales/ru/messages.json | 6 - app/_locales/sk/messages.json | 6 - app/_locales/sl/messages.json | 6 - app/_locales/sr/messages.json | 6 - app/_locales/sv/messages.json | 6 - app/_locales/sw/messages.json | 6 - app/_locales/ta/messages.json | 3 - app/_locales/th/messages.json | 3 - app/_locales/tr/messages.json | 3 - app/_locales/uk/messages.json | 6 - app/_locales/vi/messages.json | 3 - app/_locales/zh_CN/messages.json | 6 - app/_locales/zh_TW/messages.json | 6 - ...proval-check.svg => permissions-check.svg} | 0 app/scripts/background.js | 7 +- app/scripts/contentscript.js | 94 +--- app/scripts/controllers/permissions/index.js | 420 ++++++++++++++++++ .../permissions/loggerMiddleware.js | 160 +++++++ .../permissions/methodMiddleware.js | 90 ++++ .../permissions/permissions-safe-methods.json | 49 ++ .../permissions/restrictedMethods.js | 20 + app/scripts/controllers/preferences.js | 14 +- app/scripts/controllers/provider-approval.js | 177 -------- app/scripts/controllers/transactions/index.js | 79 ++-- app/scripts/createStandardProvider.js | 73 --- app/scripts/inpage.js | 132 +----- app/scripts/lib/auto-reload.js | 5 +- app/scripts/lib/createOriginMiddleware.js | 1 + app/scripts/lib/message-manager.js | 4 +- app/scripts/lib/personal-message-manager.js | 4 +- app/scripts/lib/typed-message-manager.js | 4 +- app/scripts/metamask-controller.js | 281 +++++++----- app/scripts/migrations/029.js | 1 - app/scripts/migrations/040.js | 23 + docs/porting_to_new_environment.md | 13 +- package.json | 8 +- .../preferences-controller-test.js | 3 +- .../app/controllers/provider-approval-test.js | 330 -------------- .../transactions/tx-controller-test.js | 5 +- yarn.lock | 53 ++- 78 files changed, 1080 insertions(+), 1242 deletions(-) rename app/images/{provider-approval-check.svg => permissions-check.svg} (100%) create mode 100644 app/scripts/controllers/permissions/index.js create mode 100644 app/scripts/controllers/permissions/loggerMiddleware.js create mode 100644 app/scripts/controllers/permissions/methodMiddleware.js create mode 100644 app/scripts/controllers/permissions/permissions-safe-methods.json create mode 100644 app/scripts/controllers/permissions/restrictedMethods.js delete mode 100644 app/scripts/controllers/provider-approval.js delete mode 100644 app/scripts/createStandardProvider.js create mode 100644 app/scripts/migrations/040.js delete mode 100644 test/unit/app/controllers/provider-approval-test.js diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index bbb076896568..d93c7cea0be9 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "የግንኙነት ተግባቦት" }, - "clearApprovalData": { - "message": "የግላዊነት ውሂብን አጥራ" - }, "reject": { "message": "አይቀበሉ" }, @@ -248,9 +245,6 @@ "connect": { "message": "ይገናኙ" }, - "connectRequest": { - "message": "የግንኙነት ጥያቄ" - }, "connectingTo": { "message": "ከ $1ጋር መገናኘት" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 2a9451d619e6..45a712b1a7cb 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "التفاعل على العقد" }, - "clearApprovalData": { - "message": "مسح بيانات الخصوصية" - }, "reject": { "message": "رفض" }, @@ -248,9 +245,6 @@ "connect": { "message": "اتصال" }, - "connectRequest": { - "message": "طلب اتصال" - }, "connectingTo": { "message": "جارِ الاتصال بـ $1" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index bd66c78c7112..6bb502105785 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Взаимодействие с договор" }, - "clearApprovalData": { - "message": "Изчистване на данните за поверителност" - }, "reject": { "message": "Отхвърляне" }, @@ -248,9 +245,6 @@ "connect": { "message": "Свързване" }, - "connectRequest": { - "message": "Свържете заявка" - }, "connectingTo": { "message": "Свързване с $1" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 7580e8e534b1..8ba2889f33c1 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "কন্ট্র্যাক্ট বাক্যালাপ" }, - "clearApprovalData": { - "message": "গোপনীয়তার ডেটা মুছে পরিস্কার করুন" - }, "reject": { "message": "প্রত্যাখ্যান" }, @@ -248,9 +245,6 @@ "connect": { "message": "সংযুক্ত করুন" }, - "connectRequest": { - "message": "সংযোগের অনুরোধ" - }, "connectingTo": { "message": " $1 এর সাথে সংযোগ করছে" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index 07ec4c914d56..16368abd9e4c 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Contractar Interacció" }, - "clearApprovalData": { - "message": "Elimina les dades de privacitat" - }, "reject": { "message": "Rebutja" }, @@ -245,9 +242,6 @@ "connect": { "message": "Connecta" }, - "connectRequest": { - "message": "Sol·licitud de connexió" - }, "connectingTo": { "message": "Connectant a $1 " }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 452e5fb5e066..0a9a0758c50b 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "Naozaj chcete vymazať schválené webové stránky?" }, - "clearApprovalData": { - "message": "Jasné údaje o schválení" - }, "reject": { "message": "Odmítnout" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index d47cc30468ab..5e10540a86bf 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Kontraktinteraktion" }, - "clearApprovalData": { - "message": "Ryd fortrolighedsdata" - }, "reject": { "message": "Afvis" }, @@ -248,9 +245,6 @@ "connect": { "message": "Få forbindelse" }, - "connectRequest": { - "message": "Tilslutningsanmodning" - }, "connectingTo": { "message": "Forbinder til $1" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 5ea773ba375f..c5e5e8995d5b 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Vertragsinteraktion" }, - "clearApprovalData": { - "message": "Genehmigungsdaten löschen" - }, "reject": { "message": "Ablehnen" }, @@ -236,9 +233,6 @@ "connect": { "message": "Verbinden" }, - "connectRequest": { - "message": "Verbindungsanfrage" - }, "connectingTo": { "message": "Verbindung mit $1 wird hergestellt" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 9d6725632c41..f22eb30a8106 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Αλληλεπίδραση Σύμβασης" }, - "clearApprovalData": { - "message": "Εκκαθάριση Δεδομένων Απορρήτου" - }, "reject": { "message": "Απόρριψη" }, @@ -245,9 +242,6 @@ "connect": { "message": "Σύνδεση" }, - "connectRequest": { - "message": "Αίτημα Σύνδεσης" - }, "connectingTo": { "message": "Σύνδεση με $1" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 2afe8e0c313d..eaf5d8de5b0b 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -18,7 +18,7 @@ "message": "Chart only available on Ethereum networks." }, "confirmClear": { - "message": "Are you sure you want to clear approved websites?" + "message": "Are you sure you want to remove all dapp/website permissions?" }, "connections": { "message": "Connections" @@ -44,9 +44,6 @@ "contractInteraction": { "message": "Contract Interaction" }, - "clearApprovalData": { - "message": "Remove all sites" - }, "reject": { "message": "Reject" }, @@ -305,9 +302,6 @@ "connect": { "message": "Connect" }, - "connectRequest": { - "message": "Connect Request" - }, "connectingTo": { "message": "Connecting to $1" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 5aeb4ebe028a..5e8f2e816e38 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Interacción con contrato" }, - "clearApprovalData": { - "message": "Borrar datos de aprobación" - }, "reject": { "message": "Rechazar" }, @@ -205,9 +202,6 @@ "connect": { "message": "Conectar" }, - "connectRequest": { - "message": "Petición para conectar" - }, "connectingTo": { "message": "Conectánodse a $1" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index e75b4c27bf01..c4c9d5ba71e5 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Interacción contractual" }, - "clearApprovalData": { - "message": "Borrar datos de privacidad" - }, "reject": { "message": "Rechazar" }, @@ -245,9 +242,6 @@ "connect": { "message": "Conectar" }, - "connectRequest": { - "message": "Solicitud de conexión" - }, "connectingTo": { "message": "Conexión con $1" }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index 3b5f4ced8ffb..3f5d8027520c 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Lepingu suhtlus" }, - "clearApprovalData": { - "message": "Tühjenda privaatsusandmed" - }, "reject": { "message": "Lükka tagasi" }, @@ -248,9 +245,6 @@ "connect": { "message": "Ühendamine" }, - "connectRequest": { - "message": "Ühenduse taotlus" - }, "connectingTo": { "message": "Ühenduse loomine $1" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index 6d9741344078..bfd6fcffb64e 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "تعامل قرارداد" }, - "clearApprovalData": { - "message": "حذف اطلاعات حریم خصوصی" - }, "reject": { "message": "عدم پذیرش" }, @@ -248,9 +245,6 @@ "connect": { "message": "اتصال" }, - "connectRequest": { - "message": "درخواست اتصال" - }, "connectingTo": { "message": "در حال اتصال به 1$1" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 352600b26228..a5975c5fcb26 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Sopimustoiminta" }, - "clearApprovalData": { - "message": "Tyhjennä yksityisyystiedot" - }, "reject": { "message": "Hylkää" }, @@ -245,9 +242,6 @@ "connect": { "message": "Muodosta yhteys" }, - "connectRequest": { - "message": "Yhdistämispyyntö" - }, "connectingTo": { "message": "Yhdistetään summaan $1 " }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index 8c453bca5b0d..3914f084fda0 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Paggamit sa Contract" }, - "clearApprovalData": { - "message": "I-clear ang Privacy Data" - }, "reject": { "message": "Tanggihan" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index ddcddb6be321..4fcc1a68791e 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Interaction avec un contrat" }, - "clearApprovalData": { - "message": "Effacer les données d'approbation" - }, "reject": { "message": "Rejeter" }, @@ -236,9 +233,6 @@ "connect": { "message": "Connecter" }, - "connectRequest": { - "message": "Demande de connexion" - }, "connectingTo": { "message": "Connexion à $1" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 62e69e1d5077..88850dd264a2 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "אינטראקציית חוזה" }, - "clearApprovalData": { - "message": "נקה נתוני פרטיות" - }, "reject": { "message": "דחה" }, @@ -248,9 +245,6 @@ "connect": { "message": "התחברות" }, - "connectRequest": { - "message": "חבר/י בקשה" - }, "connectingTo": { "message": "מתחבר ל- $1 " }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 92fa40af68a4..4f747474ace5 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "कॉन्ट्रैक्ट की बातचीत" }, - "clearApprovalData": { - "message": "गोपनीयता डेटा रिक्त करें" - }, "reject": { "message": "अस्‍वीकार करें" }, @@ -248,9 +245,6 @@ "connect": { "message": "कनेक्ट करें" }, - "connectRequest": { - "message": "संपर्क अनुरोध" - }, "connectingTo": { "message": "$1 से कनेक्ट हो रहा है" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index 1dd4fd117578..c959c6fc34b2 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "क्या आप वाकई अनुमोदित वेबसाइटों को साफ़ करना चाहते हैं?" }, - "clearApprovalData": { - "message": "अनुमोदन डेटा साफ़ करें" - }, "approve": { "message": "मंजूर" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 2e224f36f01f..6bfd8452753f 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Ugovorna interakcija" }, - "clearApprovalData": { - "message": "Očisti podatke o privatnosti" - }, "appName": { "message": "MetaMask", "description": "The name of the application" @@ -248,9 +245,6 @@ "connect": { "message": "Povežite se" }, - "connectRequest": { - "message": "Zahtjev za povezivanjem" - }, "connectingTo": { "message": "Povezivanje na $1" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 4724090171af..80505e5746be 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "Èske ou sèten ou vle klè sitwèb apwouve?" }, - "clearApprovalData": { - "message": "Klè Done sou vi prive" - }, "providerRequestInfo": { "message": "Domèn ki nan lis anba a ap mande pou jwenn aksè a blòkchou Ethereum ak pou wè kont ou ye kounye a. Toujou double tcheke ke ou sou sit ki kòrèk la anvan apwouve aksè." }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index f1554299ba60..99b7ed1e7bdc 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Szerződéses interakció" }, - "clearApprovalData": { - "message": "Adatvédelmi adatok törlése" - }, "appName": { "message": "MetaMask", "description": "The name of the application" @@ -248,9 +245,6 @@ "connect": { "message": "Csatlakozás" }, - "connectRequest": { - "message": "Csatlakozási kérelem" - }, "connectingTo": { "message": "Kapcsolódás: $1" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index eaddf42ebb47..21d252d1845b 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Interaksi Kontrak" }, - "clearApprovalData": { - "message": "Bersihkan Data Privasi" - }, "appName": { "message": "MetaMask", "description": "The name of the application" @@ -248,9 +245,6 @@ "connect": { "message": "Sambungkan" }, - "connectRequest": { - "message": "Permintaan Sambungan" - }, "connectingTo": { "message": "Menghubungkan ke $1" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index e51b5a477e1c..40b70612b5d1 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Interazione Contratto" }, - "clearApprovalData": { - "message": "Cancella i dati di approvazione" - }, "reject": { "message": "Annulla" }, @@ -227,9 +224,6 @@ "connect": { "message": "Connetti" }, - "connectRequest": { - "message": "Richiesta Connessione" - }, "connectingTo": { "message": "Connessione in corso a $1" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 4967db8a6604..2618686c4969 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "コントラクトへのアクセス" }, - "clearApprovalData": { - "message": "承認データのクリア" - }, "reject": { "message": "拒否" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index e4c8b38fe673..ea3200f0fb31 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "ಒಪ್ಪಂದದ ಸಂವಹನ" }, - "clearApprovalData": { - "message": "ಗೌಪ್ಯತೆ ಡೇಟಾವನ್ನು ತೆರವುಗೊಳಿಸಿ" - }, "appName": { "message": "MetaMask", "description": "The name of the application" @@ -248,9 +245,6 @@ "connect": { "message": "ಸಂಪರ್ಕಿಸು" }, - "connectRequest": { - "message": "ವಿನಂತಿಯನ್ನು ಸಂಪರ್ಕಪಡಿಸಿ" - }, "connectingTo": { "message": "$1 ಗೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗುತ್ತಿದೆ" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 39c68a85e612..c8ec7e07f27e 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "계약 상호 작용" }, - "clearApprovalData": { - "message": "승인 데이터 삭제" - }, "reject": { "message": "거부" }, @@ -245,9 +242,6 @@ "connect": { "message": "연결" }, - "connectRequest": { - "message": "연결 요청" - }, "connectingTo": { "message": "$1에 연결" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index 961eedc9137c..e1e6defea938 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Sutartinė sąveika" }, - "clearApprovalData": { - "message": "Išvalyti asmeninius duomenis" - }, "reject": { "message": "Atmesti" }, @@ -248,9 +245,6 @@ "connect": { "message": "Prisijungti" }, - "connectRequest": { - "message": "Prijungimo užklausa" - }, "connectingTo": { "message": "Jungiamasi prie $1" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 18d80c900f4b..5a5b5908e437 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Līguma mijiedarbības" }, - "clearApprovalData": { - "message": "Notīrīt konfidencialitātes datus" - }, "appName": { "message": "MetaMask", "description": "The name of the application" @@ -248,9 +245,6 @@ "connect": { "message": "Pievienošana" }, - "connectRequest": { - "message": "Savienojuma pieprasījums" - }, "connectingTo": { "message": "Pieslēdzas $1" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index e573eabc4f82..639f37cd2a76 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Interaksi Kontrak" }, - "clearApprovalData": { - "message": "Kosongkan Data Privasi" - }, "reject": { "message": "Tolak" }, @@ -245,9 +242,6 @@ "connect": { "message": "Sambung" }, - "connectRequest": { - "message": "Sambungkan Permintaan" - }, "connectingTo": { "message": "Menyambungkan kepada $1" }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index 265542aedecd..c8ddfa3ab8ea 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "Weet je zeker dat je goedgekeurde websites wilt wissen?" }, - "clearApprovalData": { - "message": "Gegevens over goedkeuring wissen" - }, "reject": { "message": "Afwijzen" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index b0bf0d6dec46..62875b91f644 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Kontraktssamhandling" }, - "clearApprovalData": { - "message": "Tøm personvernsdata" - }, "reject": { "message": "Avslå" }, @@ -245,9 +242,6 @@ "connect": { "message": "Koble til" }, - "connectRequest": { - "message": "Kontaktforespørsel " - }, "connectingTo": { "message": "Forbinder til $1 " }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 6b66773f68fd..5c451cd688e7 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "Sigurado ka bang gusto mong i-clear ang mga naaprubahang website?" }, - "clearApprovalData": { - "message": "Tanggalin ang data ng pag-apruba" - }, "appName": { "message": "MetaMask", "description": "The name of the application" diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 3fa36bd39809..875d2385b098 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Interakcja z kontraktem" }, - "clearApprovalData": { - "message": "Usuń dane poufne" - }, "reject": { "message": "Odrzuć" }, @@ -245,9 +242,6 @@ "connect": { "message": "Połącz" }, - "connectRequest": { - "message": "Potwierdź żądanie" - }, "connectingTo": { "message": "Łączenie z $1" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 51936934953d..6851c8489a4b 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "Tem certeza de que deseja limpar sites aprovados?" }, - "clearApprovalData": { - "message": "Limpar dados de aprovação" - }, "reject": { "message": "Rejeitar" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 44ed790efc8b..7916a05b6c69 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Interação do Contrato" }, - "clearApprovalData": { - "message": "Limpar Dados de Privacidade" - }, "appName": { "message": "MetaMask", "description": "The name of the application" @@ -242,9 +239,6 @@ "connect": { "message": "Conectar-se" }, - "connectRequest": { - "message": "Solicitação de Conexão" - }, "connectingTo": { "message": "Conectando a $1" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index f891a40dfbe0..0d20037290ac 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Interacțiune contract" }, - "clearApprovalData": { - "message": "Ștergeți datele confidențiale" - }, "reject": { "message": "Respingeți" }, @@ -248,9 +245,6 @@ "connect": { "message": "Conectează-te" }, - "connectRequest": { - "message": "Solicitare de conectare" - }, "connectingTo": { "message": "Se conectează la $1" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 660b2799e406..a5d60718dfd8 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "Вы уверены, что хотите очистить утвержденные веб-сайты?Tem certeza de que deseja limpar sites aprovados?" }, - "clearApprovalData": { - "message": "Четкие данные об утверждении" - }, "reject": { "message": "Отклонить" }, @@ -707,9 +704,6 @@ "connect": { "message": "Подключиться" }, - "connectRequest": { - "message": "Запрос на подключение" - }, "connectingTo": { "message": "Подключение к $1" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 5238e076df7b..c97be718e73b 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Zmluvná interakcia" }, - "clearApprovalData": { - "message": "Jasné údaje o schválení" - }, "reject": { "message": "Odmítnout" }, @@ -239,9 +236,6 @@ "connect": { "message": "Pripojenie" }, - "connectRequest": { - "message": "Požiadavka na pripojenie" - }, "connectingTo": { "message": "Pripája sa k $1" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 8530dd43dbaf..0110f49a7899 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Interakcija s pogodbo" }, - "clearApprovalData": { - "message": "Počisti podatke o odobritvi" - }, "reject": { "message": "Zavrni" }, @@ -248,9 +245,6 @@ "connect": { "message": "Poveži" }, - "connectRequest": { - "message": "Zahteva za povezavo" - }, "connectingTo": { "message": "Povezovanje na $1" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index d3fd1cbd4c64..ffedda639878 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Ugovorna interakcija" }, - "clearApprovalData": { - "message": "Obrišite privatne podatke" - }, "reject": { "message": "Одбиј" }, @@ -245,9 +242,6 @@ "connect": { "message": "Повезивање" }, - "connectRequest": { - "message": "Zahtev za povezivanjem" - }, "connectingTo": { "message": "Povezuje se na $1" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index 1b7ecc06e3c2..934cfe086194 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Kontraktinteraktion" }, - "clearApprovalData": { - "message": "Rensa personlig data" - }, "appName": { "message": "MetaMask", "description": "The name of the application" @@ -242,9 +239,6 @@ "connect": { "message": "Ansluta" }, - "connectRequest": { - "message": "Anslutningsförfrågan" - }, "connectingTo": { "message": "Ansluter till $1" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index c8c2e36eecbb..6ebb31f66db7 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Mwingiliono wa Mkataba" }, - "clearApprovalData": { - "message": "Futa Data za Faragha" - }, "appName": { "message": "MetaMask", "description": "The name of the application" @@ -242,9 +239,6 @@ "connect": { "message": "Unganisha" }, - "connectRequest": { - "message": "Unganisha Ombi" - }, "connectingTo": { "message": "Inaunganisha kwenye $1" }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index 698349c24e5f..c671370399f6 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "அங்கீகரிக்கப்பட்ட வலைத்தளங்களை நிச்சயமாக நீக்க விரும்புகிறீர்களா?" }, - "clearApprovalData": { - "message": "ஒப்புதல் தரவை அழி" - }, "approve": { "message": "ஒப்புதல்" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index f07f3421eca8..14b8acc6484a 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "คุณแน่ใจหรือไม่ว่าต้องการล้างเว็บไซต์ที่ผ่านการอนุมัติ" }, - "clearApprovalData": { - "message": "ล้างข้อมูลการอนุมัติ" - }, "reject": { "message": "ปฏิเสธ" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index c6027a42d0fa..3dc50dbbfb2c 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "Onaylanmış web sitelerini silmek istediğinizden emin misiniz?" }, - "clearApprovalData": { - "message": "Onay verilerini temizle" - }, "reject": { "message": "Reddetmek" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 49d04a096f5e..0a444f24a15b 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "Контрактна взаємодія" }, - "clearApprovalData": { - "message": "Очистити приватні дані" - }, "reject": { "message": "Відхилити" }, @@ -248,9 +245,6 @@ "connect": { "message": "Під’єднатися" }, - "connectRequest": { - "message": "Запит на з'єднання" - }, "connectingTo": { "message": "Під'єднуємось до $1" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index bc017c9472cd..e6e71dbdbdae 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -2,9 +2,6 @@ "confirmClear": { "message": "Bạn có chắc chắn muốn xóa các trang web được phê duyệt không?" }, - "clearApprovalData": { - "message": "Xóa dữ liệu phê duyệt" - }, "reject": { "message": "Từ chối" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index cade6ee71abe..0d4eeda8d930 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "合约交互" }, - "clearApprovalData": { - "message": "清除批准数据" - }, "reject": { "message": "拒绝" }, @@ -248,9 +245,6 @@ "connect": { "message": "连接" }, - "connectRequest": { - "message": "关联请求" - }, "connectingTo": { "message": "正在连接 $1" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 3d0e299a6060..cc123c34527b 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -11,9 +11,6 @@ "contractInteraction": { "message": "合約互動" }, - "clearApprovalData": { - "message": "清除批准數據" - }, "reject": { "message": "拒絕" }, @@ -245,9 +242,6 @@ "connect": { "message": "連線" }, - "connectRequest": { - "message": "連線請求" - }, "connectingTo": { "message": "連線到$1" }, diff --git a/app/images/provider-approval-check.svg b/app/images/permissions-check.svg similarity index 100% rename from app/images/provider-approval-check.svg rename to app/images/permissions-check.svg diff --git a/app/scripts/background.js b/app/scripts/background.js index 18bdfdfb95ac..3f152fbb7b49 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -75,7 +75,6 @@ initialize().catch(log.error) // setup metamask mesh testing container const { submitMeshMetricsEntry } = setupMetamaskMeshMetrics() - /** * An object representing a transaction, in whatever state it is in. * @typedef TransactionMeta @@ -411,7 +410,7 @@ function setupController (initState, initLangCode) { controller.messageManager.on('updateBadge', updateBadge) controller.personalMessageManager.on('updateBadge', updateBadge) controller.typedMessageManager.on('updateBadge', updateBadge) - controller.providerApprovalController.memStore.on('update', updateBadge) + controller.permissionsController.permissions.subscribe(updateBadge) /** * Updates the Web Extension's "badge" number, on the little fox in the toolbar. @@ -423,8 +422,8 @@ function setupController (initState, initLangCode) { const unapprovedMsgCount = controller.messageManager.unapprovedMsgCount const unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount const unapprovedTypedMsgs = controller.typedMessageManager.unapprovedTypedMessagesCount - const pendingProviderRequests = controller.providerApprovalController.memStore.getState().providerRequests.length - const count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs + pendingProviderRequests + const pendingPermissionRequests = Object.keys(controller.permissionsController.permissions.state.permissionsRequests).length + const count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs + pendingPermissionRequests if (count) { label = String(count) } diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 6ea2db740dd9..14614177eecc 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -2,7 +2,6 @@ const fs = require('fs') const path = require('path') const pump = require('pump') const log = require('loglevel') -const Dnode = require('dnode') const querystring = require('querystring') const { Writable } = require('readable-stream') const LocalMessageDuplexStream = require('post-message-stream') @@ -21,7 +20,7 @@ const inpageBundle = inpageContent + inpageSuffix // If we create a FireFox-only code path using that API, // MetaMask will be much faster loading and performant on Firefox. -if (shouldInjectWeb3()) { +if (shouldInjectProvider()) { injectScript(inpageBundle) start() } @@ -40,7 +39,7 @@ function injectScript (content) { container.insertBefore(scriptTag, container.children[0]) container.removeChild(scriptTag) } catch (e) { - console.error('MetaMask script injection failed', e) + console.error('MetaMask provider injection failed.', e) } } @@ -132,12 +131,6 @@ async function setupStreams () { // connect "phishing" channel to warning system const phishingStream = extensionMux.createStream('phishing') phishingStream.once('data', redirectToPhishingWarning) - - // connect "publicApi" channel to submit page metadata - const publicApiStream = extensionMux.createStream('publicApi') - const background = await setupPublicApi(publicApiStream) - - return { background } } function forwardTrafficBetweenMuxers (channelName, muxA, muxB) { @@ -151,39 +144,6 @@ function forwardTrafficBetweenMuxers (channelName, muxA, muxB) { ) } -async function setupPublicApi (outStream) { - const api = { - getSiteMetadata: (cb) => cb(null, getSiteMetadata()), - } - const dnode = Dnode(api) - pump( - outStream, - dnode, - outStream, - (err) => { - // report any error - if (err) { - log.error(err) - } - } - ) - const background = await new Promise(resolve => dnode.once('remote', resolve)) - return background -} - -/** - * Gets site metadata and returns it - * - */ -function getSiteMetadata () { - // get metadata - const metadata = { - name: getSiteName(window), - icon: getSiteIcon(window), - } - return metadata -} - /** * Error handler for page to extension stream disconnections * @@ -199,11 +159,11 @@ function logStreamDisconnectWarning (remoteLabel, err) { } /** - * Determines if Web3 should be injected + * Determines if the provider should be injected * - * @returns {boolean} {@code true} if Web3 should be injected + * @returns {boolean} {@code true} if the provider should be injected */ -function shouldInjectWeb3 () { +function shouldInjectProvider () { return doctypeCheck() && suffixCheck() && documentElementCheck() && !blacklistedDomainCheck() } @@ -226,8 +186,8 @@ function doctypeCheck () { * Returns whether or not the extension (suffix) of the current document is prohibited * * This checks {@code window.location.pathname} against a set of file extensions - * that should not have web3 injected into them. This check is indifferent of query parameters - * in the location. + * that we should not inject the provider into. This check is indifferent of + * query parameters in the location. * * @returns {boolean} whether or not the extension of the current document is prohibited */ @@ -300,46 +260,6 @@ function redirectToPhishingWarning () { })}` } - -/** - * Extracts a name for the site from the DOM - */ -function getSiteName (window) { - const document = window.document - const siteName = document.querySelector('head > meta[property="og:site_name"]') - if (siteName) { - return siteName.content - } - - const metaTitle = document.querySelector('head > meta[name="title"]') - if (metaTitle) { - return metaTitle.content - } - - return document.title -} - -/** - * Extracts an icon for the site from the DOM - */ -function getSiteIcon (window) { - const document = window.document - - // Use the site's favicon if it exists - const shortcutIcon = document.querySelector('head > link[rel="shortcut icon"]') - if (shortcutIcon) { - return shortcutIcon.href - } - - // Search through available icons in no particular order - const icon = Array.from(document.querySelectorAll('head > link[rel="icon"]')).find((icon) => Boolean(icon.href)) - if (icon) { - return icon.href - } - - return null -} - /** * Returns a promise that resolves when the DOM is loaded (does not wait for images to load) */ diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js new file mode 100644 index 000000000000..fb6929e89d64 --- /dev/null +++ b/app/scripts/controllers/permissions/index.js @@ -0,0 +1,420 @@ +const JsonRpcEngine = require('json-rpc-engine') +const asMiddleware = require('json-rpc-engine/src/asMiddleware') +const ObservableStore = require('obs-store') +const RpcCap = require('rpc-cap').CapabilitiesController +const { ethErrors } = require('eth-json-rpc-errors') + +const getRestrictedMethods = require('./restrictedMethods') +const createMethodMiddleware = require('./methodMiddleware') +const createLoggerMiddleware = require('./loggerMiddleware') + +// Methods that do not require any permissions to use: +const SAFE_METHODS = require('./permissions-safe-methods.json') + +// some constants +const METADATA_STORE_KEY = 'domainMetadata' +const LOG_STORE_KEY = 'permissionsLog' +const HISTORY_STORE_KEY = 'permissionsHistory' +const WALLET_METHOD_PREFIX = 'wallet_' +const CAVEAT_NAMES = { + exposedAccounts: 'exposedAccounts', +} +const ACCOUNTS_CHANGED_NOTIFICATION = 'wallet_accountsChanged' + +class PermissionsController { + + constructor ( + { + openPopup, closePopup, notifyDomain, notifyAllDomains, keyringController, + } = {}, + restoredPermissions = {}, + restoredState = {}) { + this.store = new ObservableStore({ + [METADATA_STORE_KEY]: restoredState[METADATA_STORE_KEY] || {}, + [LOG_STORE_KEY]: restoredState[LOG_STORE_KEY] || [], + [HISTORY_STORE_KEY]: restoredState[HISTORY_STORE_KEY] || {}, + }) + this.notifyDomain = notifyDomain + this.notifyAllDomains = notifyAllDomains + this._openPopup = openPopup + this._closePopup = closePopup + this.keyringController = keyringController + this._restrictedMethods = getRestrictedMethods(this) + this._initializePermissions(restoredPermissions) + } + + createMiddleware ({ origin, extensionId }) { + + if (extensionId) { + this.store.updateState({ + [METADATA_STORE_KEY]: { + ...this.store.getState()[METADATA_STORE_KEY], + [origin]: { extensionId }, + }, + }) + } + + const engine = new JsonRpcEngine() + + engine.push(createMethodMiddleware({ + store: this.store, + storeKey: METADATA_STORE_KEY, + getAccounts: this.getAccounts.bind(this, origin), + requestAccountsPermission: this._requestPermissions.bind( + this, origin, { eth_accounts: {} } + ), + })) + + engine.push(createLoggerMiddleware({ + walletPrefix: WALLET_METHOD_PREFIX, + restrictedMethods: Object.keys(this._restrictedMethods), + store: this.store, + logStoreKey: LOG_STORE_KEY, + historyStoreKey: HISTORY_STORE_KEY, + })) + + engine.push(this.permissions.providerMiddlewareFunction.bind( + this.permissions, { origin } + )) + return asMiddleware(engine) + } + + /** + * Returns the accounts that should be exposed for the given origin domain, + * if any. This method exists for when a trusted context needs to know + * which accounts are exposed to a given domain. + * + * @param {string} origin - The origin string. + */ + getAccounts (origin) { + return new Promise((resolve, _) => { + + const req = { method: 'eth_accounts' } + const res = {} + this.permissions.providerMiddlewareFunction( + { origin }, req, res, () => {}, _end + ) + + function _end () { + if (res.error || !Array.isArray(res.result)) { + resolve([]) + } else { + resolve(res.result) + } + } + }) + } + + /** + * Submits a permissions request to rpc-cap. Internal use only. + * + * @param {string} origin - The origin string. + * @param {IRequestedPermissions} permissions - The requested permissions. + */ + _requestPermissions (origin, permissions) { + return new Promise((resolve, reject) => { + + const req = { method: 'wallet_requestPermissions', params: [permissions] } + const res = {} + this.permissions.providerMiddlewareFunction( + { origin }, req, res, () => {}, _end + ) + + function _end (err) { + if (err || res.error) { + reject(err || res.error) + } else { + resolve(res.result) + } + } + }) + } + + /** + * User approval callback. The request can fail if the request is invalid. + * + * @param {object} approved the approved request object + */ + async approvePermissionsRequest (approved, accounts) { + + const { id } = approved.metadata + const approval = this.pendingApprovals[id] + + try { + + // attempt to finalize the request and resolve it + await this.finalizePermissionsRequest(approved.permissions, accounts) + approval.resolve(approved.permissions) + + } catch (err) { + + // if finalization fails, reject the request + approval.reject(ethErrors.rpc.invalidRequest({ + message: err.message, data: err, + })) + } + + this._closePopup && this._closePopup() + delete this.pendingApprovals[id] + } + + /** + * User rejection callback. + * + * @param {string} id the id of the rejected request + */ + async rejectPermissionsRequest (id) { + const approval = this.pendingApprovals[id] + approval.reject(ethErrors.provider.userRejectedRequest()) + this._closePopup && this._closePopup() + delete this.pendingApprovals[id] + } + + /** + * Grants the given origin the eth_accounts permission for the given account(s). + * This method should ONLY be called as a result of direct user action in the UI, + * with the intention of supporting legacy dapps that don't support EIP 1102. + * + * @param {string} origin - The origin to expose the account(s) to. + * @param {Array} accounts - The account(s) to expose. + */ + async legacyExposeAccounts (origin, accounts) { + + const permissions = { + eth_accounts: {}, + } + + await this.finalizePermissionsRequest(permissions, accounts) + + let error + try { + error = await new Promise((resolve) => { + this.permissions.grantNewPermissions(origin, permissions, {}, err => resolve(err)) + }) + } catch (err) { + error = err + } + + if (error) { + if (error.code === 4001) { + throw error + } else { + throw ethErrors.rpc.internal({ + message: `Failed to add 'eth_accounts' to '${origin}'.`, + data: { + originalError: error, + accounts, + }, + }) + } + } + } + + /** + * Update the accounts exposed to the given origin. + * Throws error if the update fails. + * + * @param {string} origin - The origin to change the exposed accounts for. + * @param {string[]} accounts - The new account(s) to expose. + */ + async updateExposedAccounts (origin, accounts) { + + await this.validateExposedAccounts(accounts) + + this.permissions.updateCaveatFor( + origin, 'eth_accounts', CAVEAT_NAMES.exposedAccounts, accounts + ) + + this.notifyDomain(origin, { + method: ACCOUNTS_CHANGED_NOTIFICATION, + result: accounts, + }) + } + + /** + * Finalizes a permissions request. + * Throws if request validation fails. + * + * @param {Object} requestedPermissions - The requested permissions. + * @param {string[]} accounts - The accounts to expose, if any. + */ + async finalizePermissionsRequest (requestedPermissions, accounts) { + + const { eth_accounts: ethAccounts } = requestedPermissions + + if (ethAccounts) { + + await this.validateExposedAccounts(accounts) + + if (!ethAccounts.caveats) { + ethAccounts.caveats = [] + } + + // caveat names are unique, and we will only construct this caveat here + ethAccounts.caveats = ethAccounts.caveats.filter(c => ( + c.name !== CAVEAT_NAMES.exposedAccounts + )) + + ethAccounts.caveats.push( + { + type: 'filterResponse', + value: accounts, + name: CAVEAT_NAMES.exposedAccounts, + }, + ) + } + } + + /** + * Validate an array of accounts representing accounts to be exposed + * to a domain. Throws error if validation fails. + * + * @param {string[]} accounts - An array of addresses. + */ + async validateExposedAccounts (accounts) { + + if (!Array.isArray(accounts) || accounts.length === 0) { + throw new Error('Must provide non-empty array of account(s).') + } + + // assert accounts exist + const allAccounts = await this.keyringController.getAccounts() + accounts.forEach(acc => { + if (!allAccounts.includes(acc)) { + throw new Error(`Unknown account: ${acc}`) + } + }) + } + + /** + * Removes the given permissions for the given domain. + * @param {object} domains { origin: [permissions] } + */ + removePermissionsFor (domains) { + + Object.entries(domains).forEach(([origin, perms]) => { + + this.permissions.removePermissionsFor( + origin, + perms.map(methodName => { + + if (methodName === 'eth_accounts') { + this.notifyDomain( + origin, + { method: ACCOUNTS_CHANGED_NOTIFICATION, result: [] } + ) + } + + return { parentCapability: methodName } + }) + ) + }) + } + + /** + * Gets all caveats for the given origin and permission, or returns null + * if none exist. + * + * @param {string} permission - The name of the target permission. + * @param {string} origin - The origin that has the permission. + */ + getCaveatsFor (permission, origin) { + return this.permissions.getCaveats(origin, permission) || null + } + + /** + * Gets the caveat with the given name for the given permission of the + * given origin. + * + * @param {string} permission - The name of the target permission. + * @param {string} origin - The origin that has the permission. + * @param {string} caveatName - The name of the caveat to retrieve. + */ + getCaveat (permission, origin, caveatName) { + return this.permissions.getCaveat(origin, permission, caveatName) + } + + /** + * Removes all known domains and their related permissions. + */ + clearPermissions () { + this.permissions.clearDomains() + this.notifyAllDomains({ + method: ACCOUNTS_CHANGED_NOTIFICATION, + result: [], + }) + } + + /** + * Clears the permissions log. + */ + clearLog () { + this.store.updateState({ + [LOG_STORE_KEY]: [], + }) + } + + /** + * Clears the permissions history. + */ + clearHistory () { + this.store.updateState({ + [HISTORY_STORE_KEY]: {}, + }) + } + + /** + * A convenience method for retrieving a login object + * or creating a new one if needed. + * + * @param {string} origin = The origin string representing the domain. + */ + _initializePermissions (restoredState) { + + // these permission requests are almost certainly stale + const initState = { ...restoredState, permissionsRequests: [] } + + this.pendingApprovals = {} + + this.permissions = new RpcCap({ + + // Supports passthrough methods: + safeMethods: SAFE_METHODS, + + // optional prefix for internal methods + methodPrefix: WALLET_METHOD_PREFIX, + + restrictedMethods: this._restrictedMethods, + + /** + * A promise-returning callback used to determine whether to approve + * permissions requests or not. + * + * Currently only returns a boolean, but eventually should return any + * specific parameters or amendments to the permissions. + * + * @param {string} req - The internal rpc-cap user request object. + */ + requestUserApproval: async (req) => { + const { metadata: { id } } = req + + this._openPopup && this._openPopup() + + return new Promise((resolve, reject) => { + this.pendingApprovals[id] = { resolve, reject } + }) + }, + }, initState) + } +} + +module.exports = { + PermissionsController, + addInternalMethodPrefix: prefix, + CAVEAT_NAMES, +} + + +function prefix (method) { + return WALLET_METHOD_PREFIX + method +} diff --git a/app/scripts/controllers/permissions/loggerMiddleware.js b/app/scripts/controllers/permissions/loggerMiddleware.js new file mode 100644 index 000000000000..f2a7a4b22e48 --- /dev/null +++ b/app/scripts/controllers/permissions/loggerMiddleware.js @@ -0,0 +1,160 @@ + +const clone = require('clone') +const { isValidAddress } = require('ethereumjs-util') + +const LOG_LIMIT = 100 + +/** + * Create middleware for logging requests and responses to restricted and + * permissions-related methods. + */ +module.exports = function createLoggerMiddleware ({ + walletPrefix, restrictedMethods, store, logStoreKey, historyStoreKey, +}) { + return (req, res, next, _end) => { + + let activityEntry, requestedMethods + const { origin, method } = req + const isInternal = method.startsWith(walletPrefix) + + if (isInternal || restrictedMethods.includes(method)) { + activityEntry = logActivity(req, isInternal) + if (method === `${walletPrefix}requestPermissions`) { + requestedMethods = getRequestedMethods(req) + } + } else { + return next() + } + + next(cb => { + const time = Date.now() + addResponse(activityEntry, res, time) + if (!res.error && requestedMethods) { + logHistory(requestedMethods, origin, res.result, time) + } + cb() + }) + } + + function logActivity (request, isInternal) { + const activityEntry = { + id: request.id, + method: request.method, + methodType: isInternal ? 'internal' : 'restricted', + origin: request.origin, + request: cloneObj(request), + requestTime: Date.now(), + response: null, + responseTime: null, + success: null, + } + commitActivity(activityEntry) + return activityEntry + } + + function addResponse (activityEntry, response, time) { + if (!response) { + return + } + activityEntry.response = cloneObj(response) + activityEntry.responseTime = time + activityEntry.success = !response.error + } + + function commitActivity (entry) { + const logs = store.getState()[logStoreKey] + if (logs.length > LOG_LIMIT - 2) { + logs.pop() + } + logs.push(entry) + store.updateState({ [logStoreKey]: logs }) + } + + function getRequestedMethods (request) { + if ( + !request.params || + typeof request.params[0] !== 'object' || + Array.isArray(request.params[0]) + ) { + return null + } + return Object.keys(request.params[0]) + } + + function logHistory (requestedMethods, origin, result, time) { + + let accounts + const entries = result + ? result.map(perm => { + if (perm.parentCapability === 'eth_accounts') { + accounts = getAccountsFromPermission(perm) + } + return perm.parentCapability + }) + .reduce((acc, m) => { + if (requestedMethods.includes(m)) { + acc[m] = { + lastApproved: time, + } + if (m === 'eth_accounts') { + acc[m].accounts = accounts + } + } + return acc + }, {}) + : {} + + if (Object.keys(entries).length > 0) { + commitHistory(origin, entries, accounts) + } + } + + function commitHistory (origin, entries, accounts) { + + const history = store.getState()[historyStoreKey] + + if (Array.isArray(accounts)) { + if (history[origin] && history[origin]['eth_accounts']) { + history[origin]['eth_accounts']['accounts'] + .filter(acc => !accounts.includes(acc)) + .forEach(acc => accounts.unshift(acc)) + } + accounts.sort() + } + + history[origin] = { + ...history[origin], + ...entries, + } + store.updateState({ [historyStoreKey]: history }) + } +} + +// the call to clone is set to disallow circular references +// we attempt cloning at a depth of 3 and 2, then return a +// shallow copy of the object +function cloneObj (obj) { + for (let i = 3; i > 1; i--) { + try { + return clone(obj, false, i) + } catch (_) {} + } + return { ...obj } +} + +function getAccountsFromPermission (perm) { + if (perm.parentCapability !== 'eth_accounts' || !perm.caveats) { + return [] + } + const accounts = {} + for (const c of perm.caveats) { + if (c.type === 'filterResponse' && Array.isArray(c.value)) { + for (const v of c.value) { + if (isValidAddress(v)) { + accounts[v] = true + } + } + } + } + return Object.keys(accounts) +} diff --git a/app/scripts/controllers/permissions/methodMiddleware.js b/app/scripts/controllers/permissions/methodMiddleware.js new file mode 100644 index 000000000000..e0b451c9f631 --- /dev/null +++ b/app/scripts/controllers/permissions/methodMiddleware.js @@ -0,0 +1,90 @@ + +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') +const { ethErrors } = require('eth-json-rpc-errors') + +/** + * Create middleware for handling certain methods and preprocessing permissions requests. + */ +module.exports = function createMethodMiddleware ({ + store, storeKey, getAccounts, requestAccountsPermission, +}) { + return createAsyncMiddleware(async (req, res, next) => { + + if (typeof req.method !== 'string') { + res.error = ethErrors.rpc.invalidRequest({ data: req}) + return + } + + switch (req.method) { + + // intercepting eth_accounts requests for backwards compatibility, + // i.e. return an empty array instead of an error + case 'eth_accounts': + + res.result = await getAccounts() + return + + case 'eth_requestAccounts': + + // first, just try to get accounts + let accounts = await getAccounts() + if (accounts.length > 0) { + res.result = accounts + return + } + + // if no accounts, request the accounts permission + try { + await requestAccountsPermission() + } catch (err) { + res.error = err + return + } + + // get the accounts again + accounts = await getAccounts() + if (accounts.length > 0) { + res.result = accounts + } else { + // this should never happen + res.error = ethErrors.rpc.internal( + 'Accounts unexpectedly unavailable. Please report this bug.' + ) + } + + return + + // custom method for getting metadata from the requesting domain + case 'wallet_sendDomainMetadata': + + const storeState = store.getState()[storeKey] + const extensionId = storeState[req.origin] + ? storeState[req.origin].extensionId + : undefined + + if ( + req.domainMetadata && + typeof req.domainMetadata.name === 'string' + ) { + + store.updateState({ + [storeKey]: { + ...storeState, + [req.origin]: { + extensionId, + ...req.domainMetadata, + }, + }, + }) + } + + res.result = true + return + + default: + break + } + + next() + }) +} diff --git a/app/scripts/controllers/permissions/permissions-safe-methods.json b/app/scripts/controllers/permissions/permissions-safe-methods.json new file mode 100644 index 000000000000..17b46b531346 --- /dev/null +++ b/app/scripts/controllers/permissions/permissions-safe-methods.json @@ -0,0 +1,49 @@ +[ + "web3_sha3", + "net_listening", + "net_peerCount", + "net_version", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_sign", + "personal_sign", + "eth_signTypedData", + "eth_signTypedData_v1", + "eth_signTypedData_v3", + "eth_submitHashrate", + "eth_submitWork", + "eth_syncing", + "eth_uninstallFilter" +] \ No newline at end of file diff --git a/app/scripts/controllers/permissions/restrictedMethods.js b/app/scripts/controllers/permissions/restrictedMethods.js new file mode 100644 index 000000000000..2051d95a8c73 --- /dev/null +++ b/app/scripts/controllers/permissions/restrictedMethods.js @@ -0,0 +1,20 @@ + +module.exports = function getRestrictedMethods (permissionsController) { + return { + + 'eth_accounts': { + description: 'View Ethereum accounts', + method: (_, res, __, end) => { + permissionsController.keyringController.getAccounts() + .then((accounts) => { + res.result = accounts + end() + }) + .catch((err) => { + res.error = err + end(err) + }) + }, + }, + } +} diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 22c6e999db19..409ce687667c 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,4 +1,5 @@ const ObservableStore = require('obs-store') +const { addInternalMethodPrefix } = require('./permissions') const normalizeAddress = require('eth-sig-util').normalize const { isValidAddress, sha3, bufferToHex } = require('ethereumjs-util') const extend = require('xtend') @@ -57,7 +58,6 @@ class PreferencesController { useNativeCurrencyAsPrimaryCurrency: true, }, completedOnboarding: false, - migratedPrivacyMode: false, metaMetricsId: null, metaMetricsSendCount: 0, }, opts.initState) @@ -187,7 +187,10 @@ class PreferencesController { * @param {Function} - end */ async requestWatchAsset (req, res, next, end) { - if (req.method === 'metamask_watchAsset' || req.method === 'wallet_watchAsset') { + if ( + req.method === 'metamask_watchAsset' || + req.method === addInternalMethodPrefix('watchAsset') + ) { const { type, options } = req.params switch (type) { case 'ERC20': @@ -644,13 +647,6 @@ class PreferencesController { return Promise.resolve(true) } - unsetMigratedPrivacyMode () { - this.store.updateState({ - migratedPrivacyMode: false, - }) - return Promise.resolve() - } - // // PRIVATE METHODS // diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js deleted file mode 100644 index e54c7196f34a..000000000000 --- a/app/scripts/controllers/provider-approval.js +++ /dev/null @@ -1,177 +0,0 @@ -const ObservableStore = require('obs-store') -const SafeEventEmitter = require('safe-event-emitter') -const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') -const { errors: rpcErrors } = require('eth-json-rpc-errors') - -/** - * A controller that services user-approved requests for a full Ethereum provider API - */ -class ProviderApprovalController extends SafeEventEmitter { - /** - * Creates a ProviderApprovalController - * - * @param {Object} [config] - Options to configure controller - */ - constructor ({ closePopup, initState, keyringController, openPopup, preferencesController } = {}) { - super() - this.closePopup = closePopup - this.keyringController = keyringController - this.openPopup = openPopup - this.preferencesController = preferencesController - this.memStore = new ObservableStore({ - providerRequests: [], - }) - - const defaultState = { approvedOrigins: {} } - this.store = new ObservableStore(Object.assign(defaultState, initState)) - } - - /** - * Called when a user approves access to a full Ethereum provider API - * - * @param {object} opts - opts for the middleware contains the origin for the middleware - */ - createMiddleware ({ senderUrl, extensionId, getSiteMetadata }) { - return createAsyncMiddleware(async (req, res, next) => { - // only handle requestAccounts - if (req.method !== 'eth_requestAccounts') { - return next() - } - // if already approved or privacy mode disabled, return early - const isUnlocked = this.keyringController.memStore.getState().isUnlocked - const origin = senderUrl.hostname - if (this.shouldExposeAccounts(origin) && isUnlocked) { - res.result = [this.preferencesController.getSelectedAddress()] - return - } - // register the provider request - const metadata = { hostname: senderUrl.hostname, origin } - if (extensionId) { - metadata.extensionId = extensionId - } else { - const siteMetadata = await getSiteMetadata(origin) - Object.assign(metadata, { siteTitle: siteMetadata.name, siteImage: siteMetadata.icon}) - } - this._handleProviderRequest(metadata) - // wait for resolution of request - const approved = await new Promise(resolve => this.once(`resolvedRequest:${origin}`, ({ approved }) => resolve(approved))) - if (approved) { - res.result = [this.preferencesController.getSelectedAddress()] - } else { - throw rpcErrors.eth.userRejectedRequest('User denied account authorization') - } - }) - } - - /** - * @typedef {Object} SiteMetadata - * @param {string} hostname - The hostname of the site - * @param {string} origin - The origin of the site - * @param {string} [siteTitle] - The title of the site - * @param {string} [siteImage] - The icon for the site - * @param {string} [extensionId] - The extension ID of the extension - */ - /** - * Called when a tab requests access to a full Ethereum provider API - * - * @param {SiteMetadata} siteMetadata - The metadata for the site requesting full provider access - */ - _handleProviderRequest (siteMetadata) { - const { providerRequests } = this.memStore.getState() - const origin = siteMetadata.origin - this.memStore.updateState({ - providerRequests: [ - ...providerRequests, - siteMetadata, - ], - }) - const isUnlocked = this.keyringController.memStore.getState().isUnlocked - const { approvedOrigins } = this.store.getState() - const originAlreadyHandled = approvedOrigins[origin] - if (originAlreadyHandled && isUnlocked) { - return - } - this.openPopup && this.openPopup() - } - - /** - * Called when a user approves access to a full Ethereum provider API - * - * @param {string} origin - origin of the domain that had provider access approved - */ - approveProviderRequestByOrigin (origin) { - if (this.closePopup) { - this.closePopup() - } - - const { approvedOrigins } = this.store.getState() - const { providerRequests } = this.memStore.getState() - const providerRequest = providerRequests.find((request) => request.origin === origin) - const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) - this.store.updateState({ - approvedOrigins: { - ...approvedOrigins, - [origin]: { - siteTitle: providerRequest ? providerRequest.siteTitle : null, - siteImage: providerRequest ? providerRequest.siteImage : null, - hostname: providerRequest ? providerRequest.hostname : null, - }, - }, - }) - this.memStore.updateState({ providerRequests: remainingProviderRequests }) - this.emit(`resolvedRequest:${origin}`, { approved: true }) - } - - /** - * Called when a tab rejects access to a full Ethereum provider API - * - * @param {string} origin - origin of the domain that had provider access approved - */ - rejectProviderRequestByOrigin (origin) { - if (this.closePopup) { - this.closePopup() - } - - const { approvedOrigins } = this.store.getState() - const { providerRequests } = this.memStore.getState() - const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) - - // We're cloning and deleting keys here because we don't want to keep unneeded keys - const _approvedOrigins = Object.assign({}, approvedOrigins) - delete _approvedOrigins[origin] - - this.store.putState({ approvedOrigins: _approvedOrigins }) - this.memStore.putState({ providerRequests: remainingProviderRequests }) - this.emit(`resolvedRequest:${origin}`, { approved: false }) - } - - /** - * Clears any approvals for user-approved origins - */ - clearApprovedOrigins () { - this.store.updateState({ - approvedOrigins: {}, - }) - } - - /** - * Determines if a given origin should have accounts exposed - * - * @param {string} origin - Domain origin to check for approval status - * @returns {boolean} - True if the origin has been approved - */ - shouldExposeAccounts (origin) { - return Boolean(this.store.getState().approvedOrigins[origin]) - } - - /** - * Returns a merged state representation - * @return {object} - * @private - */ - _getMergedState () { - return Object.assign({}, this.memStore.getState(), this.store.getState()) - } -} - -module.exports = ProviderApprovalController diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 31f8cc103972..0dd2c047b41f 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -3,7 +3,7 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const EthQuery = require('ethjs-query') -const { errors: rpcErrors } = require('eth-json-rpc-errors') +const { ethErrors } = require('eth-json-rpc-errors') const abi = require('human-standard-token-abi') const abiDecoder = require('abi-decoder') abiDecoder.addABI(abi) @@ -54,6 +54,7 @@ const { hexToBn, bnToHex, BnMultiplyByFraction } = require('../../lib/util') @param {Object} opts.blockTracker - An instance of eth-blocktracker @param {Object} opts.provider - A network provider. @param {Function} opts.signTransaction - function the signs an ethereumjs-tx + @param {object} opts.getPermittedAccounts - get accounts that an origin has permissions for @param {Function} [opts.getGasPrice] - optional gas price calculator @param {Function} opts.signTransaction - ethTx signer that returns a rawTx @param {Number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state @@ -66,6 +67,7 @@ class TransactionController extends EventEmitter { this.networkStore = opts.networkStore || new ObservableStore({}) this.preferencesStore = opts.preferencesStore || new ObservableStore({}) this.provider = opts.provider + this.getPermittedAccounts = opts.getPermittedAccounts this.blockTracker = opts.blockTracker this.signEthTx = opts.signTransaction this.getGasPrice = opts.getGasPrice @@ -133,7 +135,7 @@ class TransactionController extends EventEmitter { /** Adds a tx to the txlist @emits ${txMeta.id}:unapproved -*/ + */ addTx (txMeta) { this.txStateManager.addTx(txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta) @@ -148,18 +150,18 @@ class TransactionController extends EventEmitter { } /** - add a new unapproved transaction to the pipeline - - @returns {Promise} the hash of the transaction after being submitted to the network - @param txParams {object} - txParams for the transaction - @param opts {object} - with the key origin to put the origin on the txMeta + * Add a new unapproved transaction to the pipeline + * + * @returns {Promise} the hash of the transaction after being submitted to the network + * @param txParams {object} - txParams for the transaction + * @param opts {object} - with the key origin to put the origin on the txMeta */ - async newUnapprovedTransaction (txParams, opts = {}) { + log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) - const initialTxMeta = await this.addUnapprovedTransaction(txParams) - initialTxMeta.origin = opts.origin - this.txStateManager.updateTx(initialTxMeta, '#newUnapprovedTransaction - adding the origin') + + const initialTxMeta = await this.addUnapprovedTransaction(txParams, opts.origin) + // listen for tx completion (success, fail) return new Promise((resolve, reject) => { this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => { @@ -167,30 +169,48 @@ class TransactionController extends EventEmitter { case 'submitted': return resolve(finishedTxMeta.hash) case 'rejected': - return reject(cleanErrorStack(rpcErrors.eth.userRejectedRequest('MetaMask Tx Signature: User denied transaction signature.'))) + return reject(cleanErrorStack(ethErrors.provider.userRejectedRequest('MetaMask Tx Signature: User denied transaction signature.'))) case 'failed': - return reject(cleanErrorStack(rpcErrors.internal(finishedTxMeta.err.message))) + return reject(cleanErrorStack(ethErrors.rpc.internal(finishedTxMeta.err.message))) default: - return reject(cleanErrorStack(rpcErrors.internal(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`))) + return reject(cleanErrorStack(ethErrors.rpc.internal(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`))) } }) }) } /** - Validates and generates a txMeta with defaults and puts it in txStateManager - store - - @returns {txMeta} - */ + * Validates and generates a txMeta with defaults and puts it in txStateManager + * store. + * + * @returns {txMeta} + */ + async addUnapprovedTransaction (txParams, origin) { - async addUnapprovedTransaction (txParams) { // validate const normalizedTxParams = txUtils.normalizeTxParams(txParams) - // Assert the from address is the selected address - if (normalizedTxParams.from !== this.getSelectedAddress()) { - throw new Error(`Transaction from address isn't valid for this account`) + + if (origin === 'metamask') { + // Assert the from address is the selected address + if (normalizedTxParams.from !== this.getSelectedAddress()) { + throw ethErrors.rpc.internal({ + message: `Internally initiated transaction is using invalid account.`, + data: { + origin, + fromAddress: normalizedTxParams.from, + selectedAddress: this.getSelectedAddress(), + } + }) + } + } else { + // Assert that the origin has permissions to initiate transactions from + // the specified address + const permittedAddresses = await this.getPermittedAccounts(origin) + if (!permittedAddresses.includes(normalizedTxParams.from)) { + throw ethErrors.provider.unauthorized({ data: { origin }}) + } } + txUtils.validateTxParams(normalizedTxParams) /** `generateTxMeta` adds the default txMeta properties to the passed object. @@ -201,6 +221,7 @@ class TransactionController extends EventEmitter { let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams, type: TRANSACTION_TYPE_STANDARD, + origin, }) const { transactionCategory, getCodeResponse } = await this._determineTransactionCategory(txParams) txMeta.transactionCategory = transactionCategory @@ -222,15 +243,16 @@ class TransactionController extends EventEmitter { txMeta.loadingDefaults = false // save txMeta - this.txStateManager.updateTx(txMeta) + this.txStateManager.updateTx(txMeta, 'Added new unapproved transaction.') return txMeta } + /** - adds the tx gas defaults: gas && gasPrice - @param txMeta {Object} - the txMeta object - @returns {Promise} resolves with txMeta -*/ + * Adds the tx gas defaults: gas && gasPrice + * @param txMeta {Object} - the txMeta object + * @returns {Promise} resolves with txMeta + */ async addTxGasDefaults (txMeta, getCodeResponse) { const txParams = txMeta.txParams // ensure value @@ -416,6 +438,7 @@ class TransactionController extends EventEmitter { this.inProcessOfSigning.delete(txId) } } + /** adds the chain id and signs the transaction and set the status to signed @param txId {number} - the tx's Id diff --git a/app/scripts/createStandardProvider.js b/app/scripts/createStandardProvider.js deleted file mode 100644 index 2059b9b3a748..000000000000 --- a/app/scripts/createStandardProvider.js +++ /dev/null @@ -1,73 +0,0 @@ -class StandardProvider { - _isConnected - _provider - - constructor (provider) { - this._provider = provider - this._subscribe() - // indicate that we've connected, mostly just for standard compliance - setTimeout(() => { - this._onConnect() - }) - } - - _onClose () { - if (this._isConnected === undefined || this._isConnected) { - this._provider.emit('close', { - code: 1011, - reason: 'Network connection error', - }) - } - this._isConnected = false - } - - _onConnect () { - !this._isConnected && this._provider.emit('connect') - this._isConnected = true - } - - _subscribe () { - this._provider.on('data', (error, { method, params }) => { - if (!error && method === 'eth_subscription') { - this._provider.emit('notification', params.result) - } - }) - } - - /** - * Initiate an RPC method call - * - * @param {string} method - RPC method name to call - * @param {string[]} params - Array of RPC method parameters - * @returns {Promise<*>} Promise resolving to the result if successful - */ - send (method, params = []) { - return new Promise((resolve, reject) => { - try { - this._provider.sendAsync({ id: 1, jsonrpc: '2.0', method, params }, (error, response) => { - error = error || response.error - error ? reject(error) : resolve(response) - }) - } catch (error) { - reject(error) - } - }) - } -} - -/** - * Converts a legacy provider into an EIP-1193-compliant standard provider - * @param {Object} provider - Legacy provider to convert - * @returns {Object} Standard provider - */ -export default function createStandardProvider (provider) { - const standardProvider = new StandardProvider(provider) - const sendLegacy = provider.send - provider.send = (methodOrPayload, callbackOrArgs) => { - if (typeof methodOrPayload === 'string' && !callbackOrArgs || Array.isArray(callbackOrArgs)) { - return standardProvider.send(methodOrPayload, callbackOrArgs) - } - return sendLegacy.call(provider, methodOrPayload, callbackOrArgs) - } - return provider -} diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 0f752f1afb75..d81d8587b901 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -1,6 +1,5 @@ /*global Web3*/ - // need to make sure we aren't affected by overlapping namespaces // and that we dont affect the app with our namespace // mostly a fix for web3's BigNumber if AMD's "define" is defined... @@ -32,17 +31,14 @@ const restoreContextAfterImports = () => { } cleanContextForImports() -require('web3/dist/web3.min.js') + const log = require('loglevel') const LocalMessageDuplexStream = require('post-message-stream') -const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('metamask-inpage-provider') -const ObjectMultiplex = require('obj-multiplex') -const pump = require('pump') -const promisify = require('pify') -const createStandardProvider = require('./createStandardProvider').default -let warned = false +// TODO:deprecate:2020-01-13 +require('web3/dist/web3.min.js') +const setupDappAutoReload = require('./lib/auto-reload.js') restoreContextAfterImports() @@ -64,106 +60,6 @@ const inpageProvider = new MetamaskInpageProvider(metamaskStream) // set a high max listener count to avoid unnecesary warnings inpageProvider.setMaxListeners(100) -const pageMux = new ObjectMultiplex() -const onboardingStream = pageMux.createStream('onboarding') -pump( - pageMux, - metamaskStream, - error => log.error('MetaMask muxed in-page traffic failed', error) -) - -let warnedOfAutoRefreshDeprecation = false -// augment the provider with its enable method -inpageProvider.enable = function ({ force } = {}) { - if ( - !warnedOfAutoRefreshDeprecation && - inpageProvider.autoRefreshOnNetworkChange - ) { - console.warn(`MetaMask: MetaMask will soon stop reloading pages on network change. -If you rely upon this behavior, add a 'networkChanged' event handler to trigger the reload manually: https://metamask.github.io/metamask-docs/API_Reference/Ethereum_Provider#ethereum.on(eventname%2C-callback) -Set 'ethereum.autoRefreshOnNetworkChange' to 'false' to silence this warning: https://metamask.github.io/metamask-docs/API_Reference/Ethereum_Provider#ethereum.autorefreshonnetworkchange' -`) - warnedOfAutoRefreshDeprecation = true - } - return new Promise((resolve, reject) => { - inpageProvider.sendAsync({ method: 'eth_requestAccounts', params: [force] }, (error, response) => { - if (error || response.error) { - reject(error || response.error) - } else { - resolve(response.result) - } - }) - }) -} - -// give the dapps control of a refresh they can toggle this off on the window.ethereum -// this will be default true so it does not break any old apps. -inpageProvider.autoRefreshOnNetworkChange = true - - -// publicConfig isn't populated until we get a message from background. -// Using this getter will ensure the state is available -const getPublicConfigWhenReady = async () => { - const store = inpageProvider.publicConfigStore - let state = store.getState() - // if state is missing, wait for first update - if (!state.networkVersion) { - state = await new Promise(resolve => store.once('update', resolve)) - console.log('new state', state) - } - return state -} - -// add metamask-specific convenience methods -inpageProvider._metamask = new Proxy({ - /** - * Synchronously determines if this domain is currently enabled, with a potential false negative if called to soon - * - * @returns {boolean} - returns true if this domain is currently enabled - */ - isEnabled: function () { - const { isEnabled } = inpageProvider.publicConfigStore.getState() - return Boolean(isEnabled) - }, - - /** - * Asynchronously determines if this domain is currently enabled - * - * @returns {Promise} - Promise resolving to true if this domain is currently enabled - */ - isApproved: async function () { - const { isEnabled } = await getPublicConfigWhenReady() - return Boolean(isEnabled) - }, - - /** - * Determines if MetaMask is unlocked by the user - * - * @returns {Promise} - Promise resolving to true if MetaMask is currently unlocked - */ - isUnlocked: async function () { - const { isUnlocked } = await getPublicConfigWhenReady() - return Boolean(isUnlocked) - }, - - /** - * Registers a page as having initated onboarding. This facilitates MetaMask focusing the initiating tab after onboarding. - * - * @returns {Promise} - Promise resolving to undefined - */ - registerOnboarding: async () => { - await promisify(onboardingStream.write({ type: 'registerOnboarding' })) - }, -}, { - get: function (obj, prop) { - !warned && console.warn('Heads up! ethereum._metamask exposes methods that have ' + - 'not been standardized yet. This means that these methods may not be implemented ' + - 'in other dapp browsers and may be removed from MetaMask in the future.') - warned = true - return obj[prop] - }, -}) - // Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound // `sendAsync` method on the prototype, causing `this` reference issues const proxiedInpageProvider = new Proxy(inpageProvider, { @@ -172,12 +68,12 @@ const proxiedInpageProvider = new Proxy(inpageProvider, { deleteProperty: () => true, }) -window.ethereum = createStandardProvider(proxiedInpageProvider) - // -// setup web3 +// TODO:deprecate:2020-01-13 // +// setup web3 + if (typeof window.web3 !== 'undefined') { throw new Error(`MetaMask detected another web3. MetaMask will not work reliably with another web3 extension. @@ -192,9 +88,13 @@ web3.setProvider = function () { } log.debug('MetaMask - injected web3') -setupDappAutoReload(web3, inpageProvider.publicConfigStore) +proxiedInpageProvider._web3Ref = web3.eth -// set web3 defaultAccount -inpageProvider.publicConfigStore.subscribe(function (state) { - web3.eth.defaultAccount = state.selectedAddress -}) +// setup dapp auto reload AND proxy web3 +setupDappAutoReload(web3, inpageProvider._publicConfigStore) + +// +// end deprecate:2020-01-13 +// + +window.ethereum = proxiedInpageProvider diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index 343191f4c126..e2b9c17bd33b 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -1,3 +1,6 @@ + +// TODO:deprecate:2020-01-13 + module.exports = setupDappAutoReload function setupDappAutoReload (web3, observable) { @@ -13,7 +16,7 @@ function setupDappAutoReload (web3, observable) { lastTimeUsed = Date.now() // show warning once on web3 access if (!hasBeenWarned && key !== 'currentProvider') { - console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider\nhttps://medium.com/metamask/4a899ad6e59e') + console.warn(`MetaMask: On 2020-01-13, MetaMask will no longer inject web3. For more information, see: https://medium.com/metamask/no-longer-injecting-web3-js-4a899ad6e59e`) hasBeenWarned = true } // return value normally diff --git a/app/scripts/lib/createOriginMiddleware.js b/app/scripts/lib/createOriginMiddleware.js index 98bb0e3b3996..4ff9a33862ae 100644 --- a/app/scripts/lib/createOriginMiddleware.js +++ b/app/scripts/lib/createOriginMiddleware.js @@ -1,3 +1,4 @@ + module.exports = createOriginMiddleware /** diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index c66b3a9e1afe..a947188e5325 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -1,7 +1,7 @@ const EventEmitter = require('events') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const { errors: rpcErrors } = require('eth-json-rpc-errors') +const { ethErrors } = require('eth-json-rpc-errors') const createId = require('./random-id') /** @@ -85,7 +85,7 @@ module.exports = class MessageManager extends EventEmitter { case 'signed': return resolve(data.rawSig) case 'rejected': - return reject(rpcErrors.eth.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) + return reject(ethErrors.provider.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) default: return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) } diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index c538ef17f023..fb30cc03ce9e 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -1,7 +1,7 @@ const EventEmitter = require('events') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const { errors: rpcErrors } = require('eth-json-rpc-errors') +const { ethErrors } = require('eth-json-rpc-errors') const createId = require('./random-id') const hexRe = /^[0-9A-Fa-f]+$/g const log = require('loglevel') @@ -91,7 +91,7 @@ module.exports = class PersonalMessageManager extends EventEmitter { case 'signed': return resolve(data.rawSig) case 'rejected': - return reject(rpcErrors.eth.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) + return reject(ethErrors.provider.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) default: return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) } diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index 7a73e7761378..5e3c480387d0 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -2,7 +2,7 @@ const EventEmitter = require('events') const ObservableStore = require('obs-store') const createId = require('./random-id') const assert = require('assert') -const { errors: rpcErrors } = require('eth-json-rpc-errors') +const { ethErrors } = require('eth-json-rpc-errors') const sigUtil = require('eth-sig-util') const log = require('loglevel') const jsonschema = require('jsonschema') @@ -81,7 +81,7 @@ module.exports = class TypedMessageManager extends EventEmitter { case 'signed': return resolve(data.rawSig) case 'rejected': - return reject(rpcErrors.eth.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) + return reject(ethErrors.provider.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) case 'errored': return reject(new Error(`MetaMask Message Signature: ${data.error}`)) default: diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 9b38451a4e4b..68680b1cb1ab 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -8,11 +8,9 @@ const assert = require('assert').strict const EventEmitter = require('events') const pump = require('pump') const Dnode = require('dnode') -const pify = require('pify') const extension = require('extensionizer') const ObservableStore = require('obs-store') const ComposableObservableStore = require('./lib/ComposableObservableStore') -const createDnodeRemoteGetter = require('./lib/createDnodeRemoteGetter') const asStream = require('obs-store/lib/asStream') const AccountTracker = require('./lib/account-tracker') const RpcEngine = require('json-rpc-engine') @@ -20,8 +18,8 @@ const debounce = require('debounce') const createEngineStream = require('json-rpc-middleware-stream/engineStream') const createFilterMiddleware = require('eth-json-rpc-filters') const createSubscriptionManager = require('eth-json-rpc-filters/subscriptionManager') -const createOriginMiddleware = require('./lib/createOriginMiddleware') const createLoggerMiddleware = require('./lib/createLoggerMiddleware') +const createOriginMiddleware = require('./lib/createOriginMiddleware') const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware') const {setupMultiplex} = require('./lib/stream-utils.js') const KeyringController = require('eth-keyring-controller') @@ -41,8 +39,8 @@ const TypedMessageManager = require('./lib/typed-message-manager') const TransactionController = require('./controllers/transactions') const TokenRatesController = require('./controllers/token-rates') const DetectTokensController = require('./controllers/detect-tokens') -const ProviderApprovalController = require('./controllers/provider-approval') const ABTestController = require('./controllers/ab-test') +const { PermissionsController } = require('./controllers/permissions/') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') @@ -58,6 +56,7 @@ const TrezorKeyring = require('eth-trezor-keyring') const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring') const EthQuery = require('eth-query') const ethUtil = require('ethereumjs-util') +const nanoid = require('nanoid') const contractMap = require('eth-contract-metadata') const { AddressBookController, @@ -93,13 +92,18 @@ module.exports = class MetamaskController extends EventEmitter { // observable state store this.store = new ComposableObservableStore(initState) + // external connections by origin + // Do not modify directly. Use the associated methods. + this.connections = {} + // lock to ensure only one vault created at once this.createVaultMutex = new Mutex() - // network store + // next, we will initialize the controllers + // controller initializaiton order matters + this.networkController = new NetworkController(initState.NetworkController) - // preferences controller this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, @@ -107,17 +111,14 @@ module.exports = class MetamaskController extends EventEmitter { network: this.networkController, }) - // app-state controller this.appStateController = new AppStateController({ preferencesStore: this.preferencesController.store, onInactiveTimeout: () => this.setLocked(), initState: initState.AppStateController, }) - // currency controller this.currencyRateController = new CurrencyRateController(undefined, initState.CurrencyController) - // infura controller this.infuraController = new InfuraController({ initState: initState.InfuraController, }) @@ -125,7 +126,7 @@ module.exports = class MetamaskController extends EventEmitter { this.phishingController = new PhishingController() - // rpc provider + // now we can initialize the RPC provider, which other controllers require this.initializeProvider() this.provider = this.networkController.getProviderAndBlockTracker().provider this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker @@ -154,7 +155,7 @@ module.exports = class MetamaskController extends EventEmitter { initState: initState.IncomingTransactionsController, }) - // account tracker watches balances, nonces, and any code at their address. + // account tracker watches balances, nonces, and any code at their address this.accountTracker = new AccountTracker({ provider: this.provider, blockTracker: this.blockTracker, @@ -188,7 +189,6 @@ module.exports = class MetamaskController extends EventEmitter { this.accountTracker._updateAccounts() }) - // key mgmt const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, @@ -196,16 +196,26 @@ module.exports = class MetamaskController extends EventEmitter { getNetwork: this.networkController.getNetworkState.bind(this.networkController), encryptor: opts.encryptor || undefined, }) - this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s)) - // detect tokens controller + this.permissionsController = new PermissionsController({ + keyringController: this.keyringController, + openPopup: opts.openPopup, + closePopup: opts.closePopup, + notifyDomain: this.notifyConnections.bind(this), + notifyAllDomains: this.notifyAllConnections.bind(this), + }, initState.PermissionsController, initState.PermissionsMetadata) + this.detectTokensController = new DetectTokensController({ preferences: this.preferencesController, network: this.networkController, keyringMemStore: this.keyringController.memStore, }) + this.abTestController = new ABTestController({ + initState: initState.ABTestController, + }) + this.addressBookController = new AddressBookController(undefined, initState.AddressBookController) this.threeBoxController = new ThreeBoxController({ @@ -217,9 +227,9 @@ module.exports = class MetamaskController extends EventEmitter { version, }) - // tx mgmt this.txController = new TransactionController({ initState: initState.TransactionController || initState.TransactionManager, + getPermittedAccounts: this.permissionsController.getAccounts.bind(this.permissionsController), networkStore: this.networkController.networkStore, preferencesStore: this.preferencesController.store, txHistoryLimit: 40, @@ -270,18 +280,6 @@ module.exports = class MetamaskController extends EventEmitter { this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen }) - this.providerApprovalController = new ProviderApprovalController({ - closePopup: opts.closePopup, - initState: initState.ProviderApprovalController, - keyringController: this.keyringController, - openPopup: opts.openPopup, - preferencesController: this.preferencesController, - }) - - this.abTestController = new ABTestController({ - initState: initState.ABTestController, - }) - this.store.updateStructure({ AppStateController: this.appStateController.store, TransactionController: this.txController.store, @@ -294,10 +292,11 @@ module.exports = class MetamaskController extends EventEmitter { InfuraController: this.infuraController.store, CachedBalancesController: this.cachedBalancesController.store, OnboardingController: this.onboardingController.store, - ProviderApprovalController: this.providerApprovalController.store, IncomingTransactionsController: this.incomingTransactionsController.store, - ThreeBoxController: this.threeBoxController.store, ABTestController: this.abTestController.store, + PermissionsController: this.permissionsController.permissions, + PermissionsMetadata: this.permissionsController.store, + ThreeBoxController: this.threeBoxController.store, }) this.memStore = new ComposableObservableStore(null, { @@ -318,11 +317,9 @@ module.exports = class MetamaskController extends EventEmitter { ShapeshiftController: this.shapeshiftController, InfuraController: this.infuraController.store, OnboardingController: this.onboardingController.store, - // ProviderApprovalController - ProviderApprovalController: this.providerApprovalController.store, - ProviderApprovalControllerMemStore: this.providerApprovalController.memStore, IncomingTransactionsController: this.incomingTransactionsController.store, - // ThreeBoxController + PermissionsController: this.permissionsController.permissions, + PermissionsMetadata: this.permissionsController.store, ThreeBoxController: this.threeBoxController.store, ABTestController: this.abTestController.store, // ENS Controller @@ -343,20 +340,15 @@ module.exports = class MetamaskController extends EventEmitter { version, // account mgmt getAccounts: async ({ origin }) => { - // Expose no accounts if this origin has not been approved, preventing - // account-requring RPC methods from completing successfully - const exposeAccounts = this.providerApprovalController.shouldExposeAccounts(origin) - if (origin !== 'metamask' && !exposeAccounts) { - return [] - } - const isUnlocked = this.keyringController.memStore.getState().isUnlocked - const selectedAddress = this.preferencesController.getSelectedAddress() - // only show address if account is unlocked - if (isUnlocked && selectedAddress) { - return [selectedAddress] - } else { - return [] + if (origin === 'metamask') { + const selectedAddress = this.preferencesController.getSelectedAddress() + return selectedAddress ? [selectedAddress] : [] + } else if ( + this.keyringController.memStore.getState().isUnlocked + ) { + return await this.permissionsController.getAccounts(origin) } + return [] // changing this is a breaking change }, // tx signing processTransaction: this.newUnapprovedTransaction.bind(this), @@ -377,7 +369,7 @@ module.exports = class MetamaskController extends EventEmitter { * Constructor helper: initialize a public config store. * This store is used to make some config info available to Dapps synchronously. */ - createPublicConfigStore ({ checkIsEnabled }) { + createPublicConfigStore () { // subset of state for metamask inpage provider const publicConfigStore = new ObservableStore() @@ -390,23 +382,16 @@ module.exports = class MetamaskController extends EventEmitter { } function updatePublicConfigStore (memState) { - const publicState = selectPublicState(memState) - publicConfigStore.putState(publicState) + publicConfigStore.putState(selectPublicState(memState)) } - function selectPublicState ({ isUnlocked, selectedAddress, network, provider }) { - const isEnabled = checkIsEnabled() - const isReady = isUnlocked && isEnabled - const result = { + function selectPublicState ({ isUnlocked, network, provider }) { + return { isUnlocked, - isEnabled, - selectedAddress: isReady ? selectedAddress : null, networkVersion: network, chainId: selectChainId({ network, provider }), } - return result } - return publicConfigStore } @@ -438,13 +423,13 @@ module.exports = class MetamaskController extends EventEmitter { */ getApi () { const keyringController = this.keyringController - const preferencesController = this.preferencesController - const txController = this.txController const networkController = this.networkController - const providerApprovalController = this.providerApprovalController const onboardingController = this.onboardingController + const permissionsController = this.permissionsController + const preferencesController = this.preferencesController const threeBoxController = this.threeBoxController const abTestController = this.abTestController + const txController = this.txController return { // etc @@ -502,7 +487,6 @@ module.exports = class MetamaskController extends EventEmitter { setPreference: nodeify(preferencesController.setPreference, preferencesController), completeOnboarding: nodeify(preferencesController.completeOnboarding, preferencesController), addKnownMethodData: nodeify(preferencesController.addKnownMethodData, preferencesController), - unsetMigratedPrivacyMode: nodeify(preferencesController.unsetMigratedPrivacyMode, preferencesController), // BlacklistController whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this), @@ -550,11 +534,6 @@ module.exports = class MetamaskController extends EventEmitter { signTypedMessage: nodeify(this.signTypedMessage, this), cancelTypedMessage: this.cancelTypedMessage.bind(this), - // provider approval - approveProviderRequestByOrigin: providerApprovalController.approveProviderRequestByOrigin.bind(providerApprovalController), - rejectProviderRequestByOrigin: providerApprovalController.rejectProviderRequestByOrigin.bind(providerApprovalController), - clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController), - // onboarding controller setSeedPhraseBackedUp: nodeify(onboardingController.setSeedPhraseBackedUp, onboardingController), @@ -568,10 +547,22 @@ module.exports = class MetamaskController extends EventEmitter { // a/b test controller getAssignedABTestGroupName: nodeify(abTestController.getAssignedABTestGroupName, abTestController), + + // permissions + approvePermissionsRequest: nodeify(permissionsController.approvePermissionsRequest, permissionsController), + clearPermissions: permissionsController.clearPermissions.bind(permissionsController), + clearPermissionsHistory: permissionsController.clearHistory.bind(permissionsController), + clearPermissionsLog: permissionsController.clearLog.bind(permissionsController), + getApprovedAccounts: nodeify(permissionsController.getAccounts.bind(permissionsController)), + rejectPermissionsRequest: nodeify(permissionsController.rejectPermissionsRequest, permissionsController), + removePermissionsFor: permissionsController.removePermissionsFor.bind(permissionsController), + getCaveatsFor: permissionsController.getCaveatsFor.bind(permissionsController), + getCaveat: permissionsController.getCaveat.bind(permissionsController), + updateExposedAccounts: nodeify(permissionsController.updateExposedAccounts, permissionsController), + legacyExposeAccounts: nodeify(permissionsController.legacyExposeAccounts, permissionsController), } } - //============================================================================= // VAULT / KEYRING RELATED METHODS //============================================================================= @@ -1351,10 +1342,10 @@ module.exports = class MetamaskController extends EventEmitter { // setup multiplexing const mux = setupMultiplex(connectionStream) - // connect features - const publicApi = this.setupPublicApi(mux.createStream('publicApi')) - this.setupProviderConnection(mux.createStream('provider'), senderUrl, extensionId, publicApi) - this.setupPublicConfig(mux.createStream('publicConfig'), senderUrl) + + // messages between inpage and background + this.setupProviderConnection(mux.createStream('provider'), senderUrl, extensionId) + this.setupPublicConfig(mux.createStream('publicConfig')) } /** @@ -1432,13 +1423,15 @@ module.exports = class MetamaskController extends EventEmitter { * resource is an extension. * @param {object} publicApi - The public API */ - setupProviderConnection (outStream, senderUrl, extensionId, publicApi) { - const getSiteMetadata = publicApi && publicApi.getSiteMetadata - const engine = this.setupProviderEngine(senderUrl, extensionId, getSiteMetadata) + setupProviderConnection (outStream, senderUrl, extensionId) { + const origin = senderUrl.hostname + const engine = this.setupProviderEngine(senderUrl, extensionId) // setup connection const providerStream = createEngineStream({ engine }) + const connectionId = this.addConnection(origin, { engine }) + pump( outStream, providerStream, @@ -1450,6 +1443,7 @@ module.exports = class MetamaskController extends EventEmitter { mid.destroy() } }) + connectionId && this.removeConnection(origin, connectionId) if (err) { log.error(err) } @@ -1460,7 +1454,8 @@ module.exports = class MetamaskController extends EventEmitter { /** * A method for creating a provider that is safely restricted for the requesting domain. **/ - setupProviderEngine (senderUrl, extensionId, getSiteMetadata) { + setupProviderEngine (senderUrl, extensionId) { + const origin = senderUrl.hostname // setup json rpc engine stack const engine = new RpcEngine() @@ -1474,20 +1469,17 @@ module.exports = class MetamaskController extends EventEmitter { const subscriptionManager = createSubscriptionManager({ provider, blockTracker }) subscriptionManager.events.on('notification', (message) => engine.emit('notification', message)) - // metadata + // append origin to each request engine.push(createOriginMiddleware({ origin })) + // logging engine.push(createLoggerMiddleware({ origin })) // filter and subscription polyfills engine.push(filterMiddleware) engine.push(subscriptionManager.middleware) + // permissions + engine.push(this.permissionsController.createMiddleware({ origin, extensionId })) // watch asset engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) - // requestAccounts - engine.push(this.providerApprovalController.createMiddleware({ - senderUrl, - extensionId, - getSiteMetadata, - })) // forward to metamask primary provider engine.push(providerAsMiddleware(provider)) return engine @@ -1502,13 +1494,9 @@ module.exports = class MetamaskController extends EventEmitter { * this is a good candidate for deprecation. * * @param {*} outStream - The stream to provide public config over. - * @param {URL} senderUrl - The URL of requesting resource */ - setupPublicConfig (outStream, senderUrl) { - const configStore = this.createPublicConfigStore({ - // check the providerApprovalController's approvedOrigins - checkIsEnabled: () => this.providerApprovalController.shouldExposeAccounts(senderUrl.hostname), - }) + setupPublicConfig (outStream) { + const configStore = this.createPublicConfigStore({}) const configStream = asStream(configStore) pump( @@ -1524,6 +1512,8 @@ module.exports = class MetamaskController extends EventEmitter { ) } + // manage external connections + onMessage (message, sender, sendResponse) { if (!message || !message.type) { log.debug(`Ignoring invalid message: '${JSON.stringify(message)}'`) @@ -1562,39 +1552,98 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * A method for providing our public api over a stream. - * This includes a method for setting site metadata like title and image + * Adds a reference to a connection by origin. Ignores the 'metamask' origin. + * Caller must ensure that the returned id is stored such that the reference + * can be deleted later. * - * @param {*} outStream - The stream to provide the api over. + * @param {string} origin - The connection's origin string. + * @param {Object} options - Data associated with the connection + * @param {Object} options.engine - The connection's JSON Rpc Engine + * @returns {string} - The connection's id (so that it can be deleted later) */ - setupPublicApi (outStream) { - const dnode = Dnode() - // connect dnode api to remote connection - pump( - outStream, - dnode, - outStream, - (err) => { - // report any error - if (err) { - log.error(err) - } - } - ) + addConnection (origin, { engine }) { - const getRemote = createDnodeRemoteGetter(dnode) + if (origin === 'metamask') { + return null + } - const publicApi = { - // wrap with an await remote - getSiteMetadata: async () => { - const remote = await getRemote() - return await pify(remote.getSiteMetadata)() - }, + if (!this.connections[origin]) { + this.connections[origin] = {} + } + + const id = nanoid() + this.connections[origin][id] = { + engine, + } + + return id + } + + /** + * Deletes a reference to a connection, by origin and id. + * Ignores unknown origins. + * + * @param {string} origin - The connection's origin string. + * @param {string} id - The connection's id, as returned from addConnection. + */ + removeConnection (origin, id) { + + const connections = this.connections[origin] + if (!connections) { + return } - return publicApi + delete connections[id] + + if (Object.keys(connections.length === 0)) { + delete this.connections[origin] + } } + /** + * Causes the RPC engines associated with the connections to the given origin + * to emit a notification event with the given payload. + * Does nothing if the extension is locked or the origin is unknown. + * + * @param {string} origin - The connection's origin string. + * @param {any} payload - The event payload. + */ + notifyConnections (origin, payload) { + + const { isUnlocked } = this.getState() + const connections = this.connections[origin] + if (!isUnlocked || !connections) { + return + } + + Object.values(connections).forEach(conn => { + conn.engine && conn.engine.emit('notification', payload) + }) + } + + /** + * Causes the RPC engines associated with all connections to emit a + * notification event with the given payload. + * Does nothing if the extension is locked. + * + * @param {any} payload - The event payload. + */ + notifyAllConnections (payload) { + + const { isUnlocked } = this.getState() + if (!isUnlocked) { + return + } + + Object.values(this.connections).forEach(origin => { + Object.values(origin).forEach(conn => { + conn.engine && conn.engine.emit('notification', payload) + }) + }) + } + + // handlers + /** * Handle a KeyringController update * @param {object} state the KC state @@ -1623,6 +1672,8 @@ module.exports = class MetamaskController extends EventEmitter { } } + // misc + /** * A method for emitting the full MetaMask state to all registered listeners. * @private @@ -1631,6 +1682,10 @@ module.exports = class MetamaskController extends EventEmitter { this.emit('update', this.getState()) } + //============================================================================= + // MISCELLANEOUS + //============================================================================= + /** * A method for estimating a good gas price at recent prices. * Returns the lowest price that would have been included in diff --git a/app/scripts/migrations/029.js b/app/scripts/migrations/029.js index e17479ccc21a..59724c1545f7 100644 --- a/app/scripts/migrations/029.js +++ b/app/scripts/migrations/029.js @@ -24,4 +24,3 @@ module.exports = { return isApproved && now - createdTime > unacceptableDelay }), } - diff --git a/app/scripts/migrations/040.js b/app/scripts/migrations/040.js new file mode 100644 index 000000000000..042f499b2cd0 --- /dev/null +++ b/app/scripts/migrations/040.js @@ -0,0 +1,23 @@ +const version = 40 +const clone = require('clone') + +/** + * Site connections are now managed by the PermissionsController, and the + * ProviderApprovalController is removed. This migration deletes all + * ProviderApprovalController state. + */ +module.exports = { + version, + migrate: async function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + versionedData.data = transformState(state) + return versionedData + }, +} + +function transformState (state) { + delete state.ProviderApprovalController + return state +} diff --git a/docs/porting_to_new_environment.md b/docs/porting_to_new_environment.md index 9cb8e9c2dc64..92f06f37db63 100644 --- a/docs/porting_to_new_environment.md +++ b/docs/porting_to_new_environment.md @@ -18,8 +18,8 @@ const providerFromEngine = require('eth-json-rpc-middleware/providerFromEngine') /** * returns a provider restricted to the requesting domain **/ -function incomingConnection (domain, getSiteMetadata) { - const engine = metamaskController.setupProviderEngine(domain, getSiteMetadata) +function incomingConnection (domain) { + const engine = metamaskController.setupProviderEngine(domain) const provider = providerFromEngine(engine) return provider } @@ -32,15 +32,6 @@ const filterMiddleware = engine._middleware.filter(mid => mid.name === 'filterMi filterMiddleware.destroy() ``` -### getSiteMetadata() - -This method is used to enhance our confirmation screens with images and text representing the requesting domain. - -It should return a promise that resolves with an object with the following properties: - -- `name`: The requesting site's name. -- `icon`: A URI representing the site's logo. - ### Using the Streams Interface Only use this if you intend to construct the [metamask-inpage-provider](https://github.com/MetaMask/metamask-inpage-provider) over a stream! diff --git a/package.json b/package.json index f7df8b7e57eb..e9a96c0c2dc2 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "eth-block-tracker": "^4.4.2", "eth-contract-metadata": "^1.11.0", "eth-ens-namehash": "^2.0.8", - "eth-json-rpc-errors": "^1.1.0", + "eth-json-rpc-errors": "^2.0.0", "eth-json-rpc-filters": "^4.1.1", "eth-json-rpc-infura": "^4.0.1", "eth-json-rpc-middleware": "^4.2.0", @@ -116,6 +116,7 @@ "ethjs-query": "^0.3.4", "extension-port-stream": "^1.0.0", "extensionizer": "^1.0.1", + "fast-deep-equal": "^2.0.1", "fast-json-patch": "^2.0.4", "fuse.js": "^3.2.0", "gaba": "^1.9.0", @@ -128,10 +129,11 @@ "lodash.shuffle": "^4.2.0", "loglevel": "^1.4.1", "luxon": "^1.8.2", - "metamask-inpage-provider": "^3.0.0", + "metamask-inpage-provider": "MetaMask/metamask-inpage-provider#LoginPerSite", "metamask-logo": "^2.1.4", "mkdirp": "^0.5.1", "multihashes": "^0.4.12", + "nanoid": "^2.1.6", "nonce-tracker": "^1.0.0", "number-to-bn": "^1.7.0", "obj-multiplex": "^1.0.0", @@ -173,7 +175,9 @@ "redux-thunk": "^2.2.0", "request-promise": "^4.2.1", "reselect": "^3.0.1", + "rpc-cap": "^1.0.1", "safe-event-emitter": "^1.0.1", + "safe-json-stringify": "^1.2.0", "single-call-balance-checker-abi": "^1.0.0", "string.prototype.matchall": "^3.0.1", "swappable-obj-proxy": "^1.1.0", diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index f49e518c3f98..7aea39a36959 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -1,6 +1,7 @@ const assert = require('assert') const ObservableStore = require('obs-store') const PreferencesController = require('../../../../app/scripts/controllers/preferences') +const { addInternalMethodPrefix } = require('../../../../app/scripts/controllers/permissions') const sinon = require('sinon') describe('preferences controller', function () { @@ -375,7 +376,7 @@ describe('preferences controller', function () { await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) sandbox.assert.called(stubEnd) sandbox.assert.notCalled(stubNext) - req.method = 'wallet_watchAsset' + req.method = addInternalMethodPrefix('watchAsset') req.params.type = 'someasset' await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) sandbox.assert.calledTwice(stubEnd) diff --git a/test/unit/app/controllers/provider-approval-test.js b/test/unit/app/controllers/provider-approval-test.js deleted file mode 100644 index eeb9e813b26b..000000000000 --- a/test/unit/app/controllers/provider-approval-test.js +++ /dev/null @@ -1,330 +0,0 @@ -const assert = require('assert') -const sinon = require('sinon') -const ProviderApprovalController = require('../../../../app/scripts/controllers/provider-approval') - -const mockLockedKeyringController = { - memStore: { - getState: () => ({ - isUnlocked: false, - }), - }, -} - -const mockUnlockedKeyringController = { - memStore: { - getState: () => ({ - isUnlocked: true, - }), - }, -} - -describe('ProviderApprovalController', () => { - describe('#_handleProviderRequest', () => { - it('should add a pending provider request when unlocked', () => { - const controller = new ProviderApprovalController({ - keyringController: mockUnlockedKeyringController, - }) - - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - - controller._handleProviderRequest(metadata) - assert.deepEqual(controller._getMergedState(), { - approvedOrigins: {}, - providerRequests: [metadata], - }) - }) - - it('should add a pending provider request when locked', () => { - const controller = new ProviderApprovalController({ - keyringController: mockLockedKeyringController, - }) - - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - controller._handleProviderRequest(metadata) - assert.deepEqual(controller._getMergedState(), { - approvedOrigins: {}, - providerRequests: [metadata], - }) - }) - - it('should add a 2nd pending provider request when unlocked', () => { - const controller = new ProviderApprovalController({ - keyringController: mockUnlockedKeyringController, - }) - - const metadata = [{ - hostname: 'https://example1.com', - origin: 'example1.com', - siteTitle: 'Example 1', - siteImage: 'https://example1.com/logo.svg', - }, { - hostname: 'https://example2.com', - origin: 'example2.com', - siteTitle: 'Example 2', - siteImage: 'https://example2.com/logo.svg', - }] - - controller._handleProviderRequest(metadata[0]) - controller._handleProviderRequest(metadata[1]) - assert.deepEqual(controller._getMergedState(), { - approvedOrigins: {}, - providerRequests: metadata, - }) - }) - - it('should add a 2nd pending provider request when locked', () => { - const controller = new ProviderApprovalController({ - keyringController: mockLockedKeyringController, - }) - - const metadata = [{ - hostname: 'https://example1.com', - origin: 'example1.com', - siteTitle: 'Example 1', - siteImage: 'https://example1.com/logo.svg', - }, { - hostname: 'https://example2.com', - origin: 'example2.com', - siteTitle: 'Example 2', - siteImage: 'https://example2.com/logo.svg', - }] - - controller._handleProviderRequest(metadata[0]) - controller._handleProviderRequest(metadata[1]) - assert.deepEqual(controller._getMergedState(), { - approvedOrigins: {}, - providerRequests: metadata, - }) - }) - - it('should call openPopup when unlocked and when given', () => { - const openPopup = sinon.spy() - const controller = new ProviderApprovalController({ - openPopup, - keyringController: mockUnlockedKeyringController, - }) - - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - controller._handleProviderRequest(metadata) - assert.ok(openPopup.calledOnce) - }) - - it('should call openPopup when locked and when given', () => { - const openPopup = sinon.spy() - const controller = new ProviderApprovalController({ - openPopup, - keyringController: mockLockedKeyringController, - }) - - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - controller._handleProviderRequest(metadata) - assert.ok(openPopup.calledOnce) - }) - - it('should NOT call openPopup when unlocked and when the domain has already been approved', () => { - const openPopup = sinon.spy() - const controller = new ProviderApprovalController({ - openPopup, - keyringController: mockUnlockedKeyringController, - }) - - controller.store.updateState({ - approvedOrigins: { - 'example.com': { - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - }, - }, - }) - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - controller._handleProviderRequest(metadata) - assert.ok(openPopup.notCalled) - }) - }) - - describe('#approveProviderRequestByOrigin', () => { - it('should mark the origin as approved and remove the provider request', () => { - const controller = new ProviderApprovalController({ - keyringController: mockUnlockedKeyringController, - }) - - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - controller._handleProviderRequest(metadata) - controller.approveProviderRequestByOrigin('example.com') - assert.deepEqual(controller._getMergedState(), { - providerRequests: [], - approvedOrigins: { - 'example.com': { - hostname: 'https://example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - }, - }, - }) - }) - - it('should mark the origin as approved and multiple requests for the same domain', () => { - const controller = new ProviderApprovalController({ - keyringController: mockUnlockedKeyringController, - }) - - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - controller._handleProviderRequest(metadata) - controller._handleProviderRequest(metadata) - controller.approveProviderRequestByOrigin('example.com') - assert.deepEqual(controller._getMergedState(), { - providerRequests: [], - approvedOrigins: { - 'example.com': { - hostname: 'https://example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - }, - }, - }) - }) - - it('should mark the origin as approved without a provider request', () => { - const controller = new ProviderApprovalController({ - keyringController: mockUnlockedKeyringController, - }) - - controller.approveProviderRequestByOrigin('example.com') - assert.deepEqual(controller._getMergedState(), { - providerRequests: [], - approvedOrigins: { - 'example.com': { - hostname: null, - siteTitle: null, - siteImage: null, - }, - }, - }) - }) - }) - - describe('#rejectProviderRequestByOrigin', () => { - it('should remove the origin from approved', () => { - const controller = new ProviderApprovalController({ - keyringController: mockUnlockedKeyringController, - }) - - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - controller._handleProviderRequest(metadata) - controller.approveProviderRequestByOrigin('example.com') - controller.rejectProviderRequestByOrigin('example.com') - assert.deepEqual(controller._getMergedState(), { - providerRequests: [], - approvedOrigins: {}, - }) - }) - - it('should reject the origin even without a pending request', () => { - const controller = new ProviderApprovalController({ - keyringController: mockUnlockedKeyringController, - }) - - controller.rejectProviderRequestByOrigin('example.com') - assert.deepEqual(controller._getMergedState(), { - providerRequests: [], - approvedOrigins: {}, - }) - }) - }) - - describe('#clearApprovedOrigins', () => { - it('should clear the approved origins', () => { - const controller = new ProviderApprovalController({ - keyringController: mockUnlockedKeyringController, - }) - - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - controller._handleProviderRequest(metadata) - controller.approveProviderRequestByOrigin('example.com') - controller.clearApprovedOrigins() - assert.deepEqual(controller._getMergedState(), { - providerRequests: [], - approvedOrigins: {}, - }) - }) - }) - - describe('#shouldExposeAccounts', () => { - it('should return true for an approved origin', () => { - const controller = new ProviderApprovalController({ - keyringController: mockUnlockedKeyringController, - }) - - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - controller._handleProviderRequest(metadata) - controller.approveProviderRequestByOrigin('example.com') - assert.ok(controller.shouldExposeAccounts('example.com')) - }) - - it('should return false for an origin not yet approved', () => { - const controller = new ProviderApprovalController({ - keyringController: mockUnlockedKeyringController, - }) - - const metadata = { - hostname: 'https://example.com', - origin: 'example.com', - siteTitle: 'Example', - siteImage: 'https://example.com/logo.svg', - } - controller._handleProviderRequest(metadata) - controller.approveProviderRequestByOrigin('example.com') - assert.ok(!controller.shouldExposeAccounts('bad.website')) - }) - }) -}) diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index d0a989e71e16..d398c7e04f0c 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -48,6 +48,7 @@ describe('Transaction Controller', function () { ethTx.sign(fromAccount.key) resolve() }), + getPermittedAccounts: () => {}, }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) }) @@ -176,13 +177,15 @@ describe('Transaction Controller', function () { describe('#addUnapprovedTransaction', function () { const selectedAddress = '0x1678a085c290ebd122dc42cba69373b5953b831d' - let getSelectedAddress + let getSelectedAddress, getPermittedAccounts beforeEach(function () { getSelectedAddress = sinon.stub(txController, 'getSelectedAddress').returns(selectedAddress) + getPermittedAccounts = sinon.stub(txController, 'getPermittedAccounts').returns([selectedAddress]) }) afterEach(function () { getSelectedAddress.restore() + getPermittedAccounts.restore() }) it('should add an unapproved transaction and return a valid txMeta', function (done) { diff --git a/yarn.lock b/yarn.lock index edd2a97fb86d..c0f0461b7707 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10075,7 +10075,7 @@ eth-hd-keyring@^3.4.0: events "^1.1.1" xtend "^4.0.1" -eth-json-rpc-errors@^1.0.1, eth-json-rpc-errors@^1.1.0: +eth-json-rpc-errors@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/eth-json-rpc-errors/-/eth-json-rpc-errors-1.1.0.tgz#2a4291fb20c0483c99b53286a814ed14ca4efb2e" integrity sha512-AAA76BmwwSR5Mws+ivZUYxoDwMygDuMWxSTEmqDXhRPTExSWe5wuJLT/rSfvPSy9+owSudy67JmyRQ02RAOOYQ== @@ -12164,7 +12164,7 @@ fuse.js@^3.4.4: resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.5.tgz#8954fb43f9729bd5dbcb8c08f251db552595a7a6" integrity sha512-s9PGTaQIkT69HaeoTVjwGsLfb8V8ScJLx5XGFcKHg0MqLUH/UZ4EKOtqtXX9k7AFqCGxD1aJmYb8Q5VYDibVRQ== -gaba@^1.9.0: +gaba@^1.6.0, gaba@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/gaba/-/gaba-1.9.0.tgz#ccd9f99c56687b5acd39f9e3ceb435b2a59b6aa1" integrity sha512-HoVreAdZssL0jNHuzZ7WP+YKZ0riu44jVDWxhQ9hsgPuzxbVEsz9fO/HDxqAdNZS1Cswayq6+ciZ3HSCFWMKbQ== @@ -14247,6 +14247,11 @@ interpret@^1.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== +intersect-objects@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/intersect-objects/-/intersect-objects-1.0.0.tgz#b7630d28994b89b0f04d44728106136549ce816e" + integrity sha512-MS1xypHKJhWopnJgn4IbitVvt2vFy2KjINQJAPhAtDejZ+ZbMDfyPc6JsS/mWFRt9Eoku4A4usE4f2loEOoeKQ== + into-stream@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" @@ -17596,12 +17601,12 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@4.17.14, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.2, lodash@~4.17.4: +lodash@4.17.14, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.2, lodash@~4.17.4: version "4.17.14" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== -lodash@=3.10.1, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15: +lodash@=3.10.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -18213,12 +18218,13 @@ mersenne-twister@^1.0.1: resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a" integrity sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o= -metamask-inpage-provider@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/metamask-inpage-provider/-/metamask-inpage-provider-3.0.0.tgz#3b9d4bae6f67962b6a7b1a9ee1efaf424f67b6f4" - integrity sha512-44bBCbQwcFF/XGaXSweCWHJaslKhJEFgvcHdxZf9Fm1QfK7VN4U3iAI0BVOLAIkRg0xV3w7xYGLpx2cM1BU7Qw== +metamask-inpage-provider@MetaMask/metamask-inpage-provider#LoginPerSite: + version "4.0.0" + resolved "https://codeload.github.com/MetaMask/metamask-inpage-provider/tar.gz/e9206fd688f2fbd821b312bf0517e1fb2b423225" dependencies: - json-rpc-engine "^5.1.3" + eth-json-rpc-errors "^2.0.0" + fast-deep-equal "^2.0.1" + json-rpc-engine "^5.1.5" json-rpc-middleware-stream "^2.1.1" loglevel "^1.6.1" obj-multiplex "^1.0.0" @@ -18890,6 +18896,11 @@ nanoid@^2.0.0: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.0.3.tgz#dde999e173bc9d7bd2ee2746b89909ade98e075e" integrity sha512-NbaoqdhIYmY6FXDRB4eYtDVC9Z9eCbn8TyaiC16LNKtpPv/aqa0tOPD8y6gNE4yUNnaZ7LLhYtXOev/6+cBtfw== +nanoid@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.6.tgz#0665418f692e54cf44f34d4010761f3240a03314" + integrity sha512-2NDzpiuEy3+H0AVtdt8LoFi7PnqkOnIzYmJQp7xsEU6VexLluHQwKREuiz57XaQC5006seIadPrIZJhyS2n7aw== + nanomatch@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2" @@ -23669,6 +23680,21 @@ rn-host-detect@^1.1.5: resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.1.5.tgz#fbecb982b73932f34529e97932b9a63e58d8deb6" integrity sha512-ufk2dFT3QeP9HyZ/xTuMtW27KnFy815CYitJMqQm+pgG3ZAtHBsrU8nXizNKkqXGy3bQmhEoloVbrfbvMJMqkg== +rpc-cap@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rpc-cap/-/rpc-cap-1.0.1.tgz#c19f6651d9d003256c73831422e0bd60b4fa8b55" + integrity sha512-M75F5IfohYkwGvitWmstimP9OL+9h10m1ZRC2zCB1Nli4EPzL8n5re58xlrcOnwOO38FdSSPfcwcCzMuVT8K2g== + dependencies: + clone "^2.1.2" + eth-json-rpc-errors "^2.0.0" + fast-deep-equal "^2.0.1" + gaba "^1.6.0" + intersect-objects "^1.0.0" + is-subset "^0.1.1" + json-rpc-engine "^5.1.3" + obs-store "^4.0.3" + uuid "^3.3.2" + rsa-pem-to-jwk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/rsa-pem-to-jwk/-/rsa-pem-to-jwk-1.1.3.tgz#245e76bdb7e7234cfee7ca032d31b54c38fab98e" @@ -23804,6 +23830,11 @@ safe-json-parse@~1.0.1: resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-1.0.1.tgz#3e76723e38dfdda13c9b1d29a1e07ffee4b30b57" integrity sha1-PnZyPjjf3aE8mx0poeB//uSzC1c= +safe-json-stringify@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -27220,7 +27251,7 @@ uuid@3.2.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA== -uuid@3.3.2, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.2.2, uuid@^3.3.2: +uuid@3.3.2, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.2.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== @@ -27230,7 +27261,7 @@ uuid@^2.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= -uuid@^3.3.3: +uuid@^3.3.2, uuid@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== From 187eefc355afae8c117dc6fbe5358037e04cd9ef Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Wed, 27 Nov 2019 02:42:12 -0330 Subject: [PATCH 02/21] Login Per Site UI (#7368) * LoginPerSite original UI changes to keep * First commit * Get necessary connected tab info for redirect and icon display for permissioned sites * Fix up designs and add missing features * Some lint fixes * More lint fixes * Ensures the tx controller + tx-state-manager orders transactions in the order they are received * Code cleanup for LoginPerSite-ui * Update e2e tests to use new connection flow * Fix display of connect screen and app header after login when connect request present * Update metamask-responsive-ui.spec for new item in accounts dropdown * Fix approve container by replacing approvedOrigins with domainMetaData * Adds test/e2e/permissions.spec.js * Correctly handle cancellation of a permissions request * Redirect to home after disconnecting all sites / cancelling all permissions * Fix display of site icons in menu * Fix height of permissions page container * Remove unused locale messages * Set default values for openExternalTabs and tabIdOrigins in account-menu.container * More code cleanup for LoginPerSite-ui * Use extensions api to close tab in permissions-connect * Remove unnecessary change in domIsReady() in contentscript * Remove unnecessary private function markers and class methods (for background tab info) in metamask-controller. * Adds getOriginOfCurrentTab selector * Adds IconWithFallback component and substitutes for appropriate cases * Add and utilize font mixins * Remove unused method in disconnect-all.container.js * Simplify buttonSizeLarge code in page-container-footer.component.js * Add and utilize getAccountsWithLabels selector * Remove console.log in ui/app/store/actions.js * Change last connected time format to yyyy-M-d * Fix css associated with IconWithFallback change * Ensure tracked openNonMetamaskTabsIDs are correctly set to inactive on tab changes * Code cleanup for LoginPerSite-ui * Use reusable function for modifying openNonMetamaskTabsIDs in background.js * Enables automatic switching to connected account when connected domain is open * Prevent exploit of tabIdOriginMap in background.js * Remove unneeded code from contentscript.js * Simplify current tab origin and window opener logic using remotePort listener tabs.queryTabs * Design and styling fixes for LoginPerSite-ui * Fix permissionHistory and permission logging for eth_requestAccounts and eth_accounts * Front end changes to support display of lastConnected time in connected and permissions screens * Fix lint errors * Refactor structure of permissionsHistory * Fix default values and object modifications for domain and permissionsHistory related data * Fix connecting to new accounts from modal * Replace retweet.svg with connect-white.svg * Fix signature-request.spec * Update metamask-inpage-provider version * Fix permissions e2e tests * Remove unneeded delay from test/e2e/signature-request.spec.js * Add delay before attempting to retrieve network id in dapp in ethereum-on=.spec * Use requestAccountTabIds strategy for determining tab id that opened a given window * Improve default values for permissions requests * Add some message descriptions to app/_locales/en/messages.json * Code clean up in permission controller * Stopped deep cloning object in mapObjectValues * Bump metamask-inpage-provider version * Add missing description in app/_locales/en/messages.json * Return promises from queryTabs and switchToTab of extension.js * Remove unused getAllPermissions function * Use default props in icon-with-fallback.component.js * Stop passing to permissions controller * Delete no longer used clear-approved-origins modal code * Remove duplicate imports in ui/app/components/app/index.scss * Use URL instead of regex in getOriginFromUrl() * Add runtime error checking to platform, promise based extension.tab methods * Support permission requests from external extensions * Improve font size and colour of the domain origin on the permission confirmation screen * Add support for toggling permissions * Ensure getRenderablePermissionsDomains only returns domains with exposedAccount caveat permissions * Remove unused code from LoginPerSite-ui branch * Ensure modal closes on Enter press for new-account-modal.component.js * Lint fix --- app/_locales/am/messages.json | 15 - app/_locales/ar/messages.json | 15 - app/_locales/bg/messages.json | 15 - app/_locales/bn/messages.json | 15 - app/_locales/ca/messages.json | 15 - app/_locales/cs/messages.json | 6 - app/_locales/da/messages.json | 15 - app/_locales/de/messages.json | 15 - app/_locales/el/messages.json | 15 - app/_locales/en/messages.json | 98 ++++-- app/_locales/es/messages.json | 15 - app/_locales/es_419/messages.json | 15 - app/_locales/et/messages.json | 15 - app/_locales/fa/messages.json | 15 - app/_locales/fi/messages.json | 15 - app/_locales/fil/messages.json | 15 - app/_locales/fr/messages.json | 15 - app/_locales/gu/messages.json | 3 - app/_locales/he/messages.json | 15 - app/_locales/hi/messages.json | 15 - app/_locales/hn/messages.json | 6 - app/_locales/hr/messages.json | 15 - app/_locales/ht/messages.json | 6 - app/_locales/hu/messages.json | 15 - app/_locales/id/messages.json | 15 - app/_locales/it/messages.json | 33 -- app/_locales/ja/messages.json | 12 - app/_locales/kn/messages.json | 15 - app/_locales/ko/messages.json | 15 - app/_locales/lt/messages.json | 15 - app/_locales/lv/messages.json | 15 - app/_locales/ml/messages.json | 3 - app/_locales/mr/messages.json | 3 - app/_locales/ms/messages.json | 15 - app/_locales/nl/messages.json | 6 - app/_locales/no/messages.json | 15 - app/_locales/ph/messages.json | 6 - app/_locales/pl/messages.json | 15 - app/_locales/pt/messages.json | 6 - app/_locales/pt_BR/messages.json | 15 - app/_locales/pt_PT/messages.json | 3 - app/_locales/ro/messages.json | 15 - app/_locales/ru/messages.json | 15 - app/_locales/sk/messages.json | 15 - app/_locales/sl/messages.json | 15 - app/_locales/sr/messages.json | 15 - app/_locales/sv/messages.json | 15 - app/_locales/sw/messages.json | 15 - app/_locales/ta/messages.json | 9 - app/_locales/te/messages.json | 3 - app/_locales/th/messages.json | 12 - app/_locales/tr/messages.json | 6 - app/_locales/uk/messages.json | 15 - app/_locales/vi/messages.json | 6 - app/_locales/zh_CN/messages.json | 15 - app/_locales/zh_TW/messages.json | 15 - app/images/broken-line.svg | 3 + app/images/connect-white.svg | 3 + app/scripts/background.js | 15 + app/scripts/contentscript.js | 2 +- app/scripts/controllers/permissions/index.js | 26 +- .../permissions/loggerMiddleware.js | 87 +++--- app/scripts/controllers/transactions/index.js | 2 +- app/scripts/lib/local-store.js | 19 +- app/scripts/lib/util.js | 29 ++ app/scripts/metamask-controller.js | 7 +- app/scripts/platforms/extension.js | 54 +++- package.json | 2 +- test/e2e/contract-test/contract.js | 12 + test/e2e/contract-test/index.html | 5 + test/e2e/ethereum-on.spec.js | 19 +- test/e2e/metamask-responsive-ui.spec.js | 2 +- test/e2e/metamask-ui.spec.js | 19 +- test/e2e/permissions.spec.js | 201 +++++++++++++ test/e2e/run-all.sh | 8 + test/e2e/signature-request.spec.js | 24 +- .../account-details.component.js | 22 +- .../account-details.container.js | 4 +- .../components/app/account-details/index.scss | 6 + .../account-menu/account-menu.component.js | 13 + .../account-menu/account-menu.container.js | 9 +- ui/app/components/app/account-menu/index.scss | 4 + .../connected-sites-list.component.js | 121 ++++++++ .../connected-sites-list.container.js | 33 ++ .../app/connected-sites-list/index.js | 1 + .../app/connected-sites-list/index.scss | 115 +++++++ .../app/dropdowns/account-details-dropdown.js | 23 +- ui/app/components/app/index.scss | 12 +- .../components/app/modal/modal.component.js | 47 +-- .../clear-approved-origins.component.js | 39 --- .../modals/clear-approved-origins/index.js | 1 - .../disconnect-account.component.js | 51 ++++ .../disconnect-account.container.js | 41 +++ .../app/modals/disconnect-account/index.js | 1 + .../app/modals/disconnect-account/index.scss | 25 ++ .../disconnect-all.component.js | 54 ++++ .../disconnect-all.container.js} | 12 +- .../app/modals/disconnect-all/index.js | 1 + .../app/modals/disconnect-all/index.scss | 38 +++ ui/app/components/app/modals/index.scss | 6 + ui/app/components/app/modals/modal.js | 98 +++++- .../app/modals/new-account-modal/index.js | 1 + .../app/modals/new-account-modal/index.scss | 37 +++ .../new-account-modal.component.js | 78 +++++ .../new-account-modal.container.js | 44 +++ .../app/permission-page-container/index.js | 3 + .../app/permission-page-container/index.scss | 281 ++++++++++++++++++ .../index.js | 1 + ...ission-page-container-content.component.js | 155 ++++++++++ .../permission-page-container-header/index.js | 1 + ...ission-page-container-header.component.js} | 4 +- .../permission-page-container.component.js | 149 ++++++++++ .../permission-page-container.container.js | 30 ++ .../app/provider-page-container/index.js | 3 - .../app/provider-page-container/index.scss | 121 -------- .../provider-page-container-content/index.js | 1 - ...ovider-page-container-content.component.js | 87 ------ ...ovider-page-container-content.container.js | 11 - .../provider-page-container-header/index.js | 1 - .../provider-page-container.component.js | 107 ------- .../icon-with-fallback.component.js | 38 +++ .../components/ui/icon-with-fallback/index.js | 1 + .../ui/icon-with-fallback/index.scss | 30 ++ .../page-container-footer.component.js | 6 +- ui/app/css/itcss/components/pages/index.scss | 2 +- ...approval.scss => permission-approval.scss} | 4 +- ui/app/css/itcss/settings/variables.scss | 29 ++ ui/app/ducks/app/app.js | 18 ++ ui/app/helpers/constants/routes.js | 4 + ui/app/helpers/utils/util.js | 7 + .../confirm-approve.container.js | 4 +- .../connected-sites.component.js | 36 +++ ui/app/pages/connected-sites/index.js | 1 + ui/app/pages/connected-sites/index.scss | 37 +++ ui/app/pages/home/home.component.js | 39 +-- ui/app/pages/home/home.container.js | 12 +- ui/app/pages/index.scss | 4 + .../choose-account.component.js | 94 ++++++ .../choose-account/index.js | 1 + .../choose-account/index.scss | 87 ++++++ ui/app/pages/permissions-connect/index.js | 1 + ui/app/pages/permissions-connect/index.scss | 11 + .../permissions-connect-footer/index.js | 1 + .../permissions-connect-footer/index.scss | 27 ++ .../permissions-connect-footer.component.js | 26 ++ .../permissions-connect-header/index.js | 1 + .../permissions-connect-header/index.scss | 15 + .../permissions-connect-header.component.js | 25 ++ .../permissions-connect.component.js | 130 ++++++++ .../permissions-connect.container.js | 56 ++++ ui/app/pages/provider-approval/index.js | 1 - .../provider-approval.component.js | 36 --- .../provider-approval.container.js | 12 - ui/app/pages/routes/index.js | 49 ++- .../connected-site-row.component.js | 31 -- .../connected-site-row/index.js | 1 - .../connected-site-row/index.scss | 14 - .../connections-tab.component.js | 133 --------- .../connections-tab.container.js | 39 --- .../pages/settings/connections-tab/index.js | 1 - .../pages/settings/connections-tab/index.scss | 1 - ui/app/pages/settings/index.scss | 2 - ui/app/pages/settings/settings.component.js | 8 - ui/app/selectors/selectors.js | 185 ++++++++++++ ui/app/store/actions.js | 107 ++++++- yarn.lock | 7 +- 166 files changed, 2994 insertions(+), 1608 deletions(-) create mode 100644 app/images/broken-line.svg create mode 100644 app/images/connect-white.svg create mode 100644 test/e2e/permissions.spec.js create mode 100644 ui/app/components/app/connected-sites-list/connected-sites-list.component.js create mode 100644 ui/app/components/app/connected-sites-list/connected-sites-list.container.js create mode 100644 ui/app/components/app/connected-sites-list/index.js create mode 100644 ui/app/components/app/connected-sites-list/index.scss delete mode 100644 ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js delete mode 100644 ui/app/components/app/modals/clear-approved-origins/index.js create mode 100644 ui/app/components/app/modals/disconnect-account/disconnect-account.component.js create mode 100644 ui/app/components/app/modals/disconnect-account/disconnect-account.container.js create mode 100644 ui/app/components/app/modals/disconnect-account/index.js create mode 100644 ui/app/components/app/modals/disconnect-account/index.scss create mode 100644 ui/app/components/app/modals/disconnect-all/disconnect-all.component.js rename ui/app/components/app/modals/{clear-approved-origins/clear-approved-origins.container.js => disconnect-all/disconnect-all.container.js} (53%) create mode 100644 ui/app/components/app/modals/disconnect-all/index.js create mode 100644 ui/app/components/app/modals/disconnect-all/index.scss create mode 100644 ui/app/components/app/modals/new-account-modal/index.js create mode 100644 ui/app/components/app/modals/new-account-modal/index.scss create mode 100644 ui/app/components/app/modals/new-account-modal/new-account-modal.component.js create mode 100644 ui/app/components/app/modals/new-account-modal/new-account-modal.container.js create mode 100644 ui/app/components/app/permission-page-container/index.js create mode 100644 ui/app/components/app/permission-page-container/index.scss create mode 100644 ui/app/components/app/permission-page-container/permission-page-container-content/index.js create mode 100644 ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js create mode 100644 ui/app/components/app/permission-page-container/permission-page-container-header/index.js rename ui/app/components/app/{provider-page-container/provider-page-container-header/provider-page-container-header.component.js => permission-page-container/permission-page-container-header/permission-page-container-header.component.js} (58%) create mode 100644 ui/app/components/app/permission-page-container/permission-page-container.component.js create mode 100644 ui/app/components/app/permission-page-container/permission-page-container.container.js delete mode 100644 ui/app/components/app/provider-page-container/index.js delete mode 100644 ui/app/components/app/provider-page-container/index.scss delete mode 100644 ui/app/components/app/provider-page-container/provider-page-container-content/index.js delete mode 100644 ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js delete mode 100644 ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js delete mode 100644 ui/app/components/app/provider-page-container/provider-page-container-header/index.js delete mode 100644 ui/app/components/app/provider-page-container/provider-page-container.component.js create mode 100644 ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js create mode 100644 ui/app/components/ui/icon-with-fallback/index.js create mode 100644 ui/app/components/ui/icon-with-fallback/index.scss rename ui/app/css/itcss/components/pages/{provider-approval.scss => permission-approval.scss} (66%) create mode 100644 ui/app/pages/connected-sites/connected-sites.component.js create mode 100644 ui/app/pages/connected-sites/index.js create mode 100644 ui/app/pages/connected-sites/index.scss create mode 100644 ui/app/pages/permissions-connect/choose-account/choose-account.component.js create mode 100644 ui/app/pages/permissions-connect/choose-account/index.js create mode 100644 ui/app/pages/permissions-connect/choose-account/index.scss create mode 100644 ui/app/pages/permissions-connect/index.js create mode 100644 ui/app/pages/permissions-connect/index.scss create mode 100644 ui/app/pages/permissions-connect/permissions-connect-footer/index.js create mode 100644 ui/app/pages/permissions-connect/permissions-connect-footer/index.scss create mode 100644 ui/app/pages/permissions-connect/permissions-connect-footer/permissions-connect-footer.component.js create mode 100644 ui/app/pages/permissions-connect/permissions-connect-header/index.js create mode 100644 ui/app/pages/permissions-connect/permissions-connect-header/index.scss create mode 100644 ui/app/pages/permissions-connect/permissions-connect-header/permissions-connect-header.component.js create mode 100644 ui/app/pages/permissions-connect/permissions-connect.component.js create mode 100644 ui/app/pages/permissions-connect/permissions-connect.container.js delete mode 100644 ui/app/pages/provider-approval/index.js delete mode 100644 ui/app/pages/provider-approval/provider-approval.component.js delete mode 100644 ui/app/pages/provider-approval/provider-approval.container.js delete mode 100644 ui/app/pages/settings/connections-tab/connected-site-row/connected-site-row.component.js delete mode 100644 ui/app/pages/settings/connections-tab/connected-site-row/index.js delete mode 100644 ui/app/pages/settings/connections-tab/connected-site-row/index.scss delete mode 100644 ui/app/pages/settings/connections-tab/connections-tab.component.js delete mode 100644 ui/app/pages/settings/connections-tab/connections-tab.container.js delete mode 100644 ui/app/pages/settings/connections-tab/index.js delete mode 100644 ui/app/pages/settings/connections-tab/index.scss diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index d93c7cea0be9..f6729c375a3f 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "የግላዊነት ኩነት አሁን በንቡር ነቅቷል" - }, "chartOnlyAvailableEth": { "message": "ቻርት የሚገኘው በ Ethereum አውታረ መረቦች ላይ ብቻ ነው።" }, - "confirmClear": { - "message": "የተፈቀዱ ድረ ገጾችን ለማጥራት እንደሚፈልጉ እርግጠኛ ነዎት?" - }, "contractInteraction": { "message": "የግንኙነት ተግባቦት" }, "reject": { "message": "አይቀበሉ" }, - "providerRequest": { - "message": "$1ከመለያዎ ጋር ለመገናኘት ይፈልጋል" - }, - "providerRequestInfo": { - "message": "ይህ ድረ ገጽ የእርስዎን መለያ ወቅታዊ አድራሻ ለማየት እየጠየቀ ነው። ምንጊዜም ግንኙነት የሚያደርጉባቸውን ድረ ገጾች የሚያምኗቸው መሆኑን ያረጋግጡ።" - }, "about": { "message": "ስለ" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "ቀደም ሲል የተወሰነ Ether ካለዎት፣ በአዲሱ ቋትዎ Ether ለማግኘት ፈጣኑ መንገድ ቀጥተኛ ተቀማጭ ነው።" }, - "dismiss": { - "message": "አሰናብት" - }, "done": { "message": "ተጠናቅቋል" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 45a712b1a7cb..e2af1695c211 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "يتم تمكين وضع الخصوصية الآن بشكل افتراضي" - }, "chartOnlyAvailableEth": { "message": "الرسم البياني متاح فقط على شبكات إيثيريوم." }, - "confirmClear": { - "message": "هل أنت متأكد من أنك تريد مسح المواقع المعتمدة؟" - }, "contractInteraction": { "message": "التفاعل على العقد" }, "reject": { "message": "رفض" }, - "providerRequest": { - "message": "يرغب $1 في الاتصال بحسابك" - }, - "providerRequestInfo": { - "message": "يطلب هذا الموقع حق الوصول لعرض عنوان حسابك الحالي. تأكد دائماً من ثقتك في المواقع التي تتفاعل معها." - }, "about": { "message": "حول" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "إذا كان لديك بالفعل بعض الأثير، فإن أسرع طريقة للحصول على الأثير في محفظتك الجديدة عن طريق الإيداع المباشر." }, - "dismiss": { - "message": "رفض" - }, "done": { "message": "تم" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 6bb502105785..4cfd909bf844 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Режимът на поверителност вече е активиран по подразбиране" - }, "chartOnlyAvailableEth": { "message": "Диаграмата е достъпна само в мрежи на Ethereum." }, - "confirmClear": { - "message": "Сигурни ли сте, че искате да изчистите одобрените уебсайтове?" - }, "contractInteraction": { "message": "Взаимодействие с договор" }, "reject": { "message": "Отхвърляне" }, - "providerRequest": { - "message": "$1 би искал да се свърже с вашия акаунт" - }, - "providerRequestInfo": { - "message": "Този сайт иска достъп за преглед на адреса на текущия ви акаунт. Винаги се уверявайте, че се доверявате на сайтовете, с които взаимодействате." - }, "about": { "message": "Информация" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "Ако вече имате някакъв етер, най-бързият начин да получите етер в новия си портфейл е чрез директен депозит." }, - "dismiss": { - "message": "Отхвърляне" - }, "done": { "message": "Готово" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 8ba2889f33c1..114998d96c7d 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "গোপনীয়তার মোড এখন ডিফল্ট হিসাবে সক্রিয় করা আছে" - }, "chartOnlyAvailableEth": { "message": "শুধুমাত্র Ethereum নেটওয়ার্কগুলিতে চার্ট উপলভ্য। " }, - "confirmClear": { - "message": "আপনি কি অনুমোদিত ওয়েবসাইটগুলি মুছে পরিস্কার করার বিষয়ে নিশ্চিত?" - }, "contractInteraction": { "message": "কন্ট্র্যাক্ট বাক্যালাপ" }, "reject": { "message": "প্রত্যাখ্যান" }, - "providerRequest": { - "message": "$1 আপনার অ্যাকাউন্টের সাথে সংযোগ করতে চায়" - }, - "providerRequestInfo": { - "message": "এই সাইটটি আপনার বর্তমান অ্যাকাউন্টের ঠিকানা দেখার অ্যাক্সেসের জন্য অনুরোধ জানাচ্ছে। সবসময় নিশ্চিত হয়ে নেবেন যে আপনি যে সাইটের সাথে যোগাযোগ করছেন সেটি বিশ্বাসযোগ্য কিনা।" - }, "about": { "message": "সম্পর্কে" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "আপনার ইতিমধ্যে কিছু ইথার থেকে থাকলে আপনার নতুন ওয়ালেটে ইথার পাওয়ার দ্রুততম উপায় হল সরাসরি জমা করা।" }, - "dismiss": { - "message": "খারিজ" - }, "done": { "message": "সম্পন্ন " }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index 16368abd9e4c..33864912974d 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "El mode de privacitat ara està activat per defecte" - }, "chartOnlyAvailableEth": { "message": "Mostra només els disponibles a les xarxes Ethereum." }, - "confirmClear": { - "message": "Estàs segur que vols eliminar totes les pàgines web aprovades?" - }, "contractInteraction": { "message": "Contractar Interacció" }, "reject": { "message": "Rebutja" }, - "providerRequest": { - "message": "a $1 li agradaria connectar-se al teu compte" - }, - "providerRequestInfo": { - "message": "Aquesta pàgina està demanant accès a la teva adreça" - }, "about": { "message": "Informació" }, @@ -359,9 +347,6 @@ "directDepositEtherExplainer": { "message": "Si ja tens una mica d'Ether, la manera més ràpida de posar Ether al teu nou moneder és per dipòsit directe." }, - "dismiss": { - "message": "Omet" - }, "done": { "message": "Fet" }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 0a9a0758c50b..9d04ae048c55 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -1,13 +1,7 @@ { - "confirmClear": { - "message": "Naozaj chcete vymazať schválené webové stránky?" - }, "reject": { "message": "Odmítnout" }, - "providerRequestInfo": { - "message": "Níže uvedená doména se pokouší požádat o přístup k API Ethereum, aby mohla komunikovat s blokádou Ethereum. Před schválením přístupu Ethereum vždy zkontrolujte, zda jste na správném místě." - }, "account": { "message": "Účet" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 5e10540a86bf..5a4d366505f4 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Privatlivstilstand er nu som udgangspunkt aktiveret" - }, "chartOnlyAvailableEth": { "message": "Skema kun tilgængeligt på Ethereum-netværk." }, - "confirmClear": { - "message": "Er du sikker på, at du vil rydde godkendte hjemmesider?" - }, "contractInteraction": { "message": "Kontraktinteraktion" }, "reject": { "message": "Afvis" }, - "providerRequest": { - "message": "$1 ønsker at forbinde til din konto" - }, - "providerRequestInfo": { - "message": "Denne side anmoder om at se din nuværende kontoadresse. Sørg altid for, at du stoler på de sider du interagerer med." - }, "about": { "message": "Om" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "Hvis du allerede har Ether, er den hurtigste måde at få Ether i din nye tegnebog ved direkte indbetaling." }, - "dismiss": { - "message": "Luk" - }, "done": { "message": "Færdig" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index c5e5e8995d5b..f3d1e959eba8 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Der Datenschutzmodus ist jetzt standardmäßig aktiviert" - }, "chartOnlyAvailableEth": { "message": "Die Grafik ist nur in Ethereum-Netzwerken verfügbar." }, - "confirmClear": { - "message": "Möchten Sie die genehmigten Websites wirklich löschen?" - }, "contractInteraction": { "message": "Vertragsinteraktion" }, "reject": { "message": "Ablehnen" }, - "providerRequest": { - "message": "$1 möchte sich mit deinem Account verbinden" - }, - "providerRequestInfo": { - "message": "Diese Website fordert Zugriff auf Ihre aktuelle Kontoadresse. Stellen Sie immer sicher, dass Sie den Websites vertrauen, mit denen Sie interagieren." - }, "about": { "message": "Über" }, @@ -347,9 +335,6 @@ "directDepositEtherExplainer": { "message": "Wenn du bereits Ether besitzt, ist die sofortige Einzahlung die schnellste Methode Ether in deine neue Wallet zu bekommen." }, - "dismiss": { - "message": "Schließen" - }, "done": { "message": "Fertig" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index f22eb30a8106..b7e75e1f4d9f 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Η Λειτουργία Απορρήτου είναι πλέον ενεργοποιημένη από προεπιλογή" - }, "chartOnlyAvailableEth": { "message": "Το διάγραμμα είναι διαθέσιμο μόνο σε δίκτυα Ethereum." }, - "confirmClear": { - "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τους εγκεκριμένους ιστότοπους;" - }, "contractInteraction": { "message": "Αλληλεπίδραση Σύμβασης" }, "reject": { "message": "Απόρριψη" }, - "providerRequest": { - "message": "Αίτημα σύνδεσης στον λογαριασμό σας από $1" - }, - "providerRequestInfo": { - "message": "Ο ιστότοπος ζητά πρόσβαση για προβολή της τρέχουσας διεύθυνσης του λογαριασμού σας. Να σιγουρεύεστε πάντα ότι εμπιστεύεστε τους ιστότοπους με τους οποίους αλληλεπιδράτε." - }, "about": { "message": "Σχετικά με" }, @@ -359,9 +347,6 @@ "directDepositEtherExplainer": { "message": "Αν έχετε ήδη κάποια Ether, ο πιο γρήγορος τρόπος για να πάρετε τα Ether στο νέο σας πορτοφόλι με άμεση κατάθεση." }, - "dismiss": { - "message": "Παράβλεψη" - }, "done": { "message": "Τέλος" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index eaf5d8de5b0b..e4dfd083866c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -14,32 +14,20 @@ "showIncomingTransactionsDescription": { "message": "Select this to use Etherscan to show incoming transactions in the transactions list" }, + "cancelledConnectionWithMetaMask": { + "message": "Cancelled Connection With MetaMask" + }, "chartOnlyAvailableEth": { "message": "Chart only available on Ethereum networks." }, - "confirmClear": { - "message": "Are you sure you want to remove all dapp/website permissions?" - }, - "connections": { - "message": "Connections" - }, - "connectionsSettingsDescription": { - "message": "Sites allowed to read your accounts" - }, - "addSite": { - "message": "Add Site" - }, - "addSiteDescription": { - "message": "Manually add a site to allow it access to your accounts, useful for older dapps" - }, - "connected": { - "message": "Connected" + "connectedSites": { + "message": "Connected Sites" }, - "connectedDescription": { - "message": "The list of sites allowed access to your addresses" + "connectingWithMetaMask": { + "message": "Connecting With MetaMask..." }, - "privacyModeDefault": { - "message": "Privacy Mode is now enabled by default" + "chooseAnAcount": { + "message": "Choose an account" }, "contractInteraction": { "message": "Contract Interaction" @@ -47,11 +35,8 @@ "reject": { "message": "Reject" }, - "providerRequest": { - "message": "$1 would like to connect to your account" - }, - "providerRequestInfo": { - "message": "This site is requesting access to view your current account address. Always make sure you trust the sites you interact with." + "redirectingBackToDapp": { + "message": "Redirecting back to dapp..." }, "about": { "message": "About" @@ -425,14 +410,36 @@ "details": { "message": "Details" }, + "disconnectAccount": { + "message": "Disconnect account" + }, + "disconnectAll": { + "message": "Disconnect All" + }, + "disconnectAllModalDescription": { + "message": "Are you sure? You will no longer be able to interact with any of these sites." + }, + "disconnectAccountModalDescription": { + "message": "Are you sure? You will no longer be able to interact with this site." + }, + "disconnectAccountQuestion": { + "message": "Disconnect account?" + }, + "disconnectFromThisAccount": { + "message": "Disconnect from this account?" + }, + "disconnectAllAccountsQuestion": { + "message": "Disconnect all accounts?" + }, "directDepositEther": { "message": "Directly Deposit Ether" }, "directDepositEtherExplainer": { "message": "If you already have some Ether, the quickest way to get Ether in your new wallet by direct deposit." }, - "dismiss": { - "message": "Dismiss" + "domainLastConnect": { + "message": "Last Connected: $1", + "description": "$1 is the date at which the user was last connected to a given domain" }, "done": { "message": "Done" @@ -494,6 +501,13 @@ "endOfFlowMessage10": { "message": "All Done" }, + "extensionId": { + "message": "Extension ID: $1", + "description": "$1 is a string of random letters that are the id of another extension connecting to Metamask" + }, + "externalExtension": { + "message": "External Extension" + }, "onboardingReturnNotice": { "message": "\"$1\" will close this tab and direct back to $2", "description": "Return the user to the site that initiated onboarding" @@ -727,9 +741,15 @@ "max": { "message": "Max" }, + "lastConnected": { + "message": "Last Connected" + }, "learnMore": { "message": "Learn more" }, + "learnAboutRisks": { + "message": "Learn about the risks here." + }, "ledgerAccountRestriction": { "message": "You need to make use your last account before you can add a new one." }, @@ -739,6 +759,10 @@ "likeToAddTokens": { "message": "Would you like to add these tokens?" }, + "likeToConnect": { + "message": "$1 would like to connect to your Metamask account", + "description": "$1 is the name/url of a site/dapp asking to connect to MetaMask" + }, "links": { "message": "Links" }, @@ -870,6 +894,9 @@ "rpcUrl": { "message": "New RPC URL" }, + "onlyConnectTrust": { + "message": "Only connect with sites you trust." + }, "optionalChainId": { "message": "ChainID (optional)" }, @@ -1072,6 +1099,9 @@ "readyToConnect": { "message": "Ready to Connect?" }, + "revokeInPermissions": { + "message": "* You can view and revoke permissions in MetaMask settings." + }, "rinkeby": { "message": "Rinkeby Test Network" }, @@ -1325,6 +1355,14 @@ "testFaucet": { "message": "Test Faucet" }, + "thisWillAllow": { + "message": "This will allow $1 to:", + "description": "$1 is the name or domain of a site/dapp that is requesting permissions" + }, + "thisWillAllowExternalExtension": { + "message": "This will allow an external extension with id $1 to:", + "description": "$1 is a string of random letters that are the id of another extension connecting to Metamask" + }, "thisWillCreate": { "message": "This will create a new wallet and seed phrase" }, @@ -1337,6 +1375,10 @@ "toWithColon": { "message": "To:" }, + "toConnectWith": { + "message": "To connect with $1", + "description": "$1 is the name or domain of a site/dapp that asking to connect with MetaMask" + }, "toETHviaShapeShift": { "message": "$1 to ETH via ShapeShift", "description": "system will fill in deposit type in start of message" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 5e8f2e816e38..734eaf04dfad 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Modo Privado está activado ahora por defecto" - }, "chartOnlyAvailableEth": { "message": "Tabla solo disponible en redes Ethereum." }, - "confirmClear": { - "message": "¿Seguro que quieres borrar los sitios web aprobados?" - }, "contractInteraction": { "message": "Interacción con contrato" }, "reject": { "message": "Rechazar" }, - "providerRequest": { - "message": "$1 quisiera conectar con tu cuenta" - }, - "providerRequestInfo": { - "message": "El dominio que se muestra a continuación intenta solicitar acceso a la API Ethereum para que pueda interactuar con la blockchain de Ethereum. Siempre verifique que esté en el sitio correcto antes de aprobar el acceso Ethereum." - }, "about": { "message": "Acerca" }, @@ -319,9 +307,6 @@ "directDepositEtherExplainer": { "message": "Si posees Ether, la forma más rápida de transferirlo a tu nueva billetera es depositándolo directamente" }, - "dismiss": { - "message": "Descartar" - }, "done": { "message": "Completo" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index c4c9d5ba71e5..5571166cc59d 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "El modo de privacidad está habilitado de manera predeterminada" - }, "chartOnlyAvailableEth": { "message": "Chart está disponible únicamente en las redes de Ethereum." }, - "confirmClear": { - "message": "¿Estás seguro de que deseas borrar los sitios web aprobados?" - }, "contractInteraction": { "message": "Interacción contractual" }, "reject": { "message": "Rechazar" }, - "providerRequest": { - "message": "$1 desea conectarse a tu cuenta" - }, - "providerRequestInfo": { - "message": "Este sitio está solicitando acceso para ver la dirección de tu cuenta corriente. Asegúrate siempre de que confías en los sitios con los que interactúas." - }, "about": { "message": "Acerca de" }, @@ -359,9 +347,6 @@ "directDepositEtherExplainer": { "message": "Si ya tienes algunos Ethers, la forma más rápida de ingresarlos en tu nueva billetera es a través de un depósito directo." }, - "dismiss": { - "message": "Rechazar" - }, "done": { "message": "Listo" }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index 3f5d8027520c..38ec052ccbce 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Privaatsusrežiim on nüüd vaikimisi lubatud" - }, "chartOnlyAvailableEth": { "message": "Tabel on saadaval vaid Ethereumi võrkudes." }, - "confirmClear": { - "message": "Kas soovite kindlasti kinnitatud veebisaidid kustutada?" - }, "contractInteraction": { "message": "Lepingu suhtlus" }, "reject": { "message": "Lükka tagasi" }, - "providerRequest": { - "message": "$1 soovib teie kontoga ühenduse luua" - }, - "providerRequestInfo": { - "message": "See sait taotleb juurdepääsu teie praeguse konto aadressi vaatamiseks. Veenduge alati, et usaldate saite, millega suhtlete." - }, "about": { "message": "Teave" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "Kui teil on juba veidi eetrit, on kiirem viis eetri rahakotti saamiseks otsene sissemakse." }, - "dismiss": { - "message": "Loobu" - }, "done": { "message": "Valmis" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index bfd6fcffb64e..11c1fe76a9d0 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "وضعیت محرمیت حالا بصورت خودکار فعال است" - }, "chartOnlyAvailableEth": { "message": "تنها قابل دسترس را در شبکه های ایتریوم جدول بندی نمایید" }, - "confirmClear": { - "message": "آیا مطمئن هستید تا وب سایت های تصدیق شده را حذف کنید؟" - }, "contractInteraction": { "message": "تعامل قرارداد" }, "reject": { "message": "عدم پذیرش" }, - "providerRequest": { - "message": "1$1 میخواهید تا با حساب تان وصل شوید" - }, - "providerRequestInfo": { - "message": "این سایت در حال درخواست دسترسی است تا آدرس فعلی حساب تان را مشاهده نماید. همیشه متوجه باشید که بالای سایتی که با آن معامله میکنید، اعتماد دارید یا خیر." - }, "about": { "message": "درباره" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "در صورتیکه شما کدام ایتر داشته باشید، سریعترین روش برای گرفتن ایتر در کیف جدید تان توسط پرداخت مستقیم." }, - "dismiss": { - "message": "لغو کردن" - }, "done": { "message": "تمام" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index a5975c5fcb26..f0bfca3e7487 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Yksityisyystila on nyt oletusarvoisesti käytössä" - }, "chartOnlyAvailableEth": { "message": "Kaavio saatavilla vain Ethereum-verkoissa." }, - "confirmClear": { - "message": "Haluatko varmasti tyhjentää hyväksytyt verkkosivustot?" - }, "contractInteraction": { "message": "Sopimustoiminta" }, "reject": { "message": "Hylkää" }, - "providerRequest": { - "message": "$1 haluaisi yhdistää tiliisi" - }, - "providerRequestInfo": { - "message": "Tämä sivusto pyytää oikeuksia nähdä nykyisen tiliosoitteesi. Varmista aina, että luotat sivustoihin, joiden kanssa toimit." - }, "about": { "message": "Tietoja asetuksista" }, @@ -359,9 +347,6 @@ "directDepositEtherExplainer": { "message": "Jos sinulla on jo etheriä, nopein tapa hankkia etheriä uuteen lompakkoosi on suoratalletus." }, - "dismiss": { - "message": "Piilota" - }, "done": { "message": "Valmis" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index 3914f084fda0..a5da4bbf100c 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Naka-enable ang Privacy Mode bilang default" - }, "chartOnlyAvailableEth": { "message": "Available lang ang chart sa mga Ethereum network." }, - "confirmClear": { - "message": "Sigurado ka bang gusto mong i-clear ang mga inaprubahang website?" - }, "contractInteraction": { "message": "Paggamit sa Contract" }, "reject": { "message": "Tanggihan" }, - "providerRequest": { - "message": "Gusto ng $1 na kumonekta sa iyong account" - }, - "providerRequestInfo": { - "message": "Humihiling ng access ang site na ito na tingnan ang kasalukuyan mong account address. Palaging tiyaking pinagkakatiwalaan mo ang mga site na pinupuntahan mo." - }, "about": { "message": "Tungkol sa" }, @@ -335,9 +323,6 @@ "directDepositEtherExplainer": { "message": "Kung mayroon ka nang Ether, ang pinakamabilis na paraan para magkaroon ng Ether sa iyong bagong wallet ay sa pamamagitan ng direkang deposito." }, - "dismiss": { - "message": "Balewalain" - }, "done": { "message": "Tapos na" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 4fcc1a68791e..f4ae544546e2 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Le mode conversation privée est maintenant activé par défaut" - }, "chartOnlyAvailableEth": { "message": "Tableau disponible uniquement sur les réseaux Ethereum." }, - "confirmClear": { - "message": "Êtes-vous sûr de vouloir supprimer les sites Web approuvés?" - }, "contractInteraction": { "message": "Interaction avec un contrat" }, "reject": { "message": "Rejeter" }, - "providerRequest": { - "message": "$1 voudrait se connecter à votre compte" - }, - "providerRequestInfo": { - "message": "Le domaine répertorié ci-dessous tente de demander l'accès à l'API Ethereum pour pouvoir interagir avec la chaîne de blocs Ethereum. Vérifiez toujours que vous êtes sur le bon site avant d'autoriser l'accès à Ethereum." - }, "about": { "message": "À propos" }, @@ -350,9 +338,6 @@ "directDepositEtherExplainer": { "message": "Si vous avez déjà de l'Ether, le moyen le plus rapide d'obtenir des Ether dans votre nouveau portefeuille est par dépôt direct." }, - "dismiss": { - "message": "Ignorer" - }, "done": { "message": "Terminé" }, diff --git a/app/_locales/gu/messages.json b/app/_locales/gu/messages.json index 39f5ab26c809..d04514fd90c9 100644 --- a/app/_locales/gu/messages.json +++ b/app/_locales/gu/messages.json @@ -54,9 +54,6 @@ "details": { "message": "વિગતો" }, - "dismiss": { - "message": "કાઢી નાખો" - }, "done": { "message": "થઈ ગયું" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 88850dd264a2..0b5c0a075e32 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "מצב פרטיות זמין עכשיו כברירת מחדל" - }, "chartOnlyAvailableEth": { "message": "טבלה זמינה רק ברשתות אתריום." }, - "confirmClear": { - "message": "הנך בטוח/ה כי ברצונך למחוק אתרים שאושרו?" - }, "contractInteraction": { "message": "אינטראקציית חוזה" }, "reject": { "message": "דחה" }, - "providerRequest": { - "message": "$1 מבקש להתחבר לחשבון שלך" - }, - "providerRequestInfo": { - "message": "אתר זה מבקש גישה לצפייה בכתובת החשבון הנוכחית שלך. יש לוודא תמיד כי הנך בוטח/ת באתרים עמם הנך מתקשר/ת." - }, "about": { "message": "מידע כללי" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "אם כבר יש ברשותך את'ר (Ether) , הדרך המהירה ביותר להכניס את'ר לארנק החדש שלך היא באמצעות הפקדה ישירה." }, - "dismiss": { - "message": "סגור" - }, "done": { "message": "סיום" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 4f747474ace5..26b66ab1f665 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "गोपनीय मोड अब डिफ़ॉल्ट रूप से सक्षम है" - }, "chartOnlyAvailableEth": { "message": "केवल ईथरअम नेटवर्क पर उपलब्ध चार्ट।" }, - "confirmClear": { - "message": "क्या आप वाकई स्वीकृत वेबसाइटों को क्लियर करना चाहते हैं?" - }, "contractInteraction": { "message": "कॉन्ट्रैक्ट की बातचीत" }, "reject": { "message": "अस्‍वीकार करें" }, - "providerRequest": { - "message": "$1 आपके खाते से कनेक्ट होता चाहता हैं" - }, - "providerRequestInfo": { - "message": "यह साइट आपके वर्तमान खाते का पता देखने के लिए एक्सेस का अनुरोध कर रही है। हमेशा सुनिश्चित करें कि आप जिन साइटों पर जाते हैं वे विश्वसनीय हैं।" - }, "about": { "message": "इसके बारे में" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "यदि आपके पास पहले से ही कुछ Ether हैं, तो अपने नए वॉलेट में Ether पाने का सबसे तेज़ तरीका सीधे जमा करना है।" }, - "dismiss": { - "message": "खारिज करें" - }, "done": { "message": "पूर्ण" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index c959c6fc34b2..8009077982b8 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -1,16 +1,10 @@ { - "confirmClear": { - "message": "क्या आप वाकई अनुमोदित वेबसाइटों को साफ़ करना चाहते हैं?" - }, "approve": { "message": "मंजूर" }, "reject": { "message": "अस्वीकार" }, - "providerRequestInfo": { - "message": "नीचे सूचीबद्ध डोमेन वेब 3 एपीआई तक पहुंच का अनुरोध करने का प्रयास कर रहा है ताकि यह एथेरियम ब्लॉकचेन से बातचीत कर सके। वेब 3 एक्सेस को मंजूरी देने से पहले हमेशा सही जांच करें कि आप सही साइट पर हैं।" - }, "account": { "message": "खाता" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 6bfd8452753f..ad4dd30f8b90 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -1,13 +1,7 @@ { - "privacyModeDefault": { - "message": "Način se Privatnost sada zadano omogućava" - }, "chartOnlyAvailableEth": { "message": "Grafikon je dostupan samo na mrežama Ethereum." }, - "confirmClear": { - "message": "Sigurno želite očistiti odobrena mrežna mjesta?" - }, "contractInteraction": { "message": "Ugovorna interakcija" }, @@ -18,12 +12,6 @@ "reject": { "message": "Odbaci" }, - "providerRequest": { - "message": "Korisnik $1 želi se povezati na vaš račun" - }, - "providerRequestInfo": { - "message": "Na ovom se mjestu zahtijeva pristup za pregledavanje vaše trenutačne adrese računa. Uvijek pazite da vjerujete mrežnim mjestima s kojima rukujete." - }, "about": { "message": "O opcijama" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "Ako imate nešto Ethera, najbrži je način prebacivanja Ethera u vaš novi novčanik izravan polog." }, - "dismiss": { - "message": "Odbaci" - }, "done": { "message": "Gotovo" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 80505e5746be..dd63dce0c155 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "Èske ou sèten ou vle klè sitwèb apwouve?" - }, - "providerRequestInfo": { - "message": "Domèn ki nan lis anba a ap mande pou jwenn aksè a blòkchou Ethereum ak pou wè kont ou ye kounye a. Toujou double tcheke ke ou sou sit ki kòrèk la anvan apwouve aksè." - }, "accessingYourCamera": { "message": "Aksè a Kamera" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index 99b7ed1e7bdc..547141724fb7 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -1,13 +1,7 @@ { - "privacyModeDefault": { - "message": "Az adatvédelmi mód mostantól alapbeállításként engedélyezve van" - }, "chartOnlyAvailableEth": { "message": "A diagram csak Ethereum hálózatokon érhető el" }, - "confirmClear": { - "message": "Biztosan törölni szeretnéd a jóváhagyott weboldalakat?" - }, "contractInteraction": { "message": "Szerződéses interakció" }, @@ -18,12 +12,6 @@ "reject": { "message": "Elutasítás" }, - "providerRequest": { - "message": "$1 szeretne kapcsolódni az ön fiókjához" - }, - "providerRequestInfo": { - "message": "A webhely hozzáférést kér az ön jelenlegi fiókcímének megtekintéséhez. Mindig győződjön meg arról, hogy megbízható webhellyel létesít kapcsolatot." - }, "about": { "message": "Névjegy" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "Amennyiben már rendelkezik némi Ether-rel, a közvetlen letéttel gyorsan elhelyezheti azt új pénztárcájában." }, - "dismiss": { - "message": "Elvetés" - }, "done": { "message": "Kész" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 21d252d1845b..4e2216f2a1fa 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1,13 +1,7 @@ { - "privacyModeDefault": { - "message": "Modus Privasi kini aktif secara default" - }, "chartOnlyAvailableEth": { "message": "Grafik hanya tersedia pada jaringan Ethereum." }, - "confirmClear": { - "message": "Yakin ingin mengosongkan website yang disetujui?" - }, "contractInteraction": { "message": "Interaksi Kontrak" }, @@ -18,12 +12,6 @@ "reject": { "message": "Tolak" }, - "providerRequest": { - "message": "$1 ingin menghubungkan ke akun Anda" - }, - "providerRequestInfo": { - "message": "Situs ini meminta akses untuk melihat alamat akun Anda saat ini. Selalu pastikan bahwa Anda bisa mempercayai situs yang berinteraksi dengan Anda." - }, "about": { "message": "Tentang" }, @@ -356,9 +344,6 @@ "directDepositEtherExplainer": { "message": "Jika Anda sudah memiliki Ether, cara tercepat mendapatkan Ether di dompet baru lewat deposit langsung." }, - "dismiss": { - "message": "Tutup" - }, "done": { "message": "Selesai" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 40b70612b5d1..57a9207f0ba8 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "La modalità privacy è ora abilitata per impostazione predefinita" - }, "chartOnlyAvailableEth": { "message": "Grafico disponibile solo per le reti Ethereum." }, - "confirmClear": { - "message": "Sei sicuro di voler cancellare i siti Web approvati?" - }, "contractInteraction": { "message": "Interazione Contratto" }, "reject": { "message": "Annulla" }, - "providerRequest": { - "message": "$1 vorrebbe connettersi al tuo account" - }, - "providerRequestInfo": { - "message": "Il dominio elencato di seguito sta tentando di richiedere l'accesso all'API Ethereum in modo che possa interagire con la blockchain di Ethereum. Controlla sempre di essere sul sito corretto prima di approvare l'accesso a Ethereum." - }, "about": { "message": "Informazioni" }, @@ -341,9 +329,6 @@ "directDepositEtherExplainer": { "message": "Se possiedi già degli Ether, questa è la via più veloce per aggiungere Ether al tuo portafoglio con un deposito diretto." }, - "dismiss": { - "message": "Ignora" - }, "done": { "message": "Finito" }, @@ -1337,24 +1322,6 @@ "zeroGasPriceOnSpeedUpError": { "message": "Prezzo del gas maggiore di zero" }, - "connections": { - "message": "Connessioni" - }, - "connectionsSettingsDescription": { - "message": "Siti autorizzati ad accedere ai tuoi accounts" - }, - "addSite": { - "message": "Aggiungi Sito" - }, - "addSiteDescription": { - "message": "Aggiungi un sito autorizzato ad accedere ai tuoi accounts, utile per dapps obsolete" - }, - "connected": { - "message": "Connesso" - }, - "connectedDescription": { - "message": "La lista di siti web autorizzati ad accedere ai tuoi indirizzi" - }, "contacts": { "message": "Contatti" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 2618686c4969..0f2475bfbc83 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "プライバシーモードがデフォルトで有効になりました" - }, "chartOnlyAvailableEth": { "message": "チャートはEthereumネットワークでのみ利用可能です。" }, - "confirmClear": { - "message": "承認されたウェブサイトをクリアしてもよろしいですか?" - }, "contractInteraction": { "message": "コントラクトへのアクセス" }, "reject": { "message": "拒否" }, - "providerRequest": { - "message": "$1 はあなたのアカウントにアクセスしようとしています。" - }, - "providerRequestInfo": { - "message": "下記のドメインは、Ethereumブロックチェーンとやり取りできるようにEthereum APIへのアクセスをリクエストしようとしています。 Web3アクセスを承認する前に、正しいサイトにいることを常に確認してください。" - }, "aboutSettingsDescription": { "message": "バージョンやサポート、問合せ先など" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index ea3200f0fb31..59d73ea76f90 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -1,13 +1,7 @@ { - "privacyModeDefault": { - "message": "ಗೌಪ್ಯತೆ ಮೋಡ್ ಅನ್ನು ಡೀಫಾಲ್ಟ್‌ ಆಗಿ ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ" - }, "chartOnlyAvailableEth": { "message": "ಎಥೆರಿಯಮ್ ನೆಟ್‌ವರ್ಕ್‌ಗಳಲ್ಲಿ ಮಾತ್ರವೇ ಚಾರ್ಟ್‌ಗಳು ಲಭ್ಯವಿರುತ್ತವೆ." }, - "confirmClear": { - "message": "ನೀವು ಅನುಮೋದಿಸಿದ ವೆಬ್‌‌ಸೈಟ್‌ಗಳನ್ನು ತೆರವುಗೊಳಿಸಲು ಬಯಸುವಿರಾ?" - }, "contractInteraction": { "message": "ಒಪ್ಪಂದದ ಸಂವಹನ" }, @@ -18,12 +12,6 @@ "reject": { "message": "ತಿರಸ್ಕರಿಸಿ" }, - "providerRequest": { - "message": "$1 ನಿಮ್ಮ ಖಾತೆಗೆ ಸಂಪರ್ಕಿಸಲು ಬಯಸುತ್ತಿದೆ" - }, - "providerRequestInfo": { - "message": "ಈ ಸೈಟ್ ನಿಮ್ಮ ಪ್ರಸ್ತುತ ಖಾತೆ ವಿಳಾಸವನ್ನು ವೀಕ್ಷಿಸಲು ಪ್ರವೇಶವನ್ನು ವಿನಂತಿಸುತ್ತಿದೆ. ನೀವು ಸಂವಹನ ನಡೆಸುವ ಸೈಟ್‌ಗಳನ್ನು ನೀವು ನಂಬಿರುವಿರಿ ಎಂಬುದನ್ನು ಯಾವಾಗಲೂ ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ." - }, "about": { "message": "ಕುರಿತು" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "ನೀವು ಈಗಾಗಲೇ ಕೆಲವು ಎಥರ್ ಹೊಂದಿದ್ದರೆ, ನೇರ ಠೇವಣಿ ಮೂಲಕ ನಿಮ್ಮ ಹೊಸ ವ್ಯಾಲೆಟ್‌ನಲ್ಲಿ ಎಥರ್ ಅನ್ನು ಪಡೆಯುವ ತ್ವರಿತ ಮಾರ್ಗ." }, - "dismiss": { - "message": "ವಜಾಗೊಳಿಸಿ" - }, "done": { "message": "ಮುಗಿದಿದೆ" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index c8ec7e07f27e..8cd3f13e52b2 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "이제 프라이버시 모드가 기본 설정으로 활성화됐습니다" - }, "chartOnlyAvailableEth": { "message": "이더리움 네트워크에서만 사용 가능한 차트." }, - "confirmClear": { - "message": "승인 된 웹 사이트를 삭제 하시겠습니까?" - }, "contractInteraction": { "message": "계약 상호 작용" }, "reject": { "message": "거부" }, - "providerRequest": { - "message": "$1이 당신의 계정에 연결하길 원합니다." - }, - "providerRequestInfo": { - "message": "아래 나열된 도메인은 Web3 API에 대한 액세스를 요청하여 Ethereum 블록 체인과 상호 작용할 수 있습니다. Ethereum 액세스를 승인하기 전에 항상 올바른 사이트에 있는지 다시 확인하십시오." - }, "about": { "message": "정보" }, @@ -359,9 +347,6 @@ "directDepositEtherExplainer": { "message": "약간의 이더를 이미 보유하고 있다면, 새로 만든 지갑에 직접 입금하여 이더를 보유할 수 있습니다." }, - "dismiss": { - "message": "숨기기" - }, "done": { "message": "완료" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index e1e6defea938..f014d73725ee 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Dabar privatumo režimas suaktyvintas pagal numatytąją nuostatą" - }, "chartOnlyAvailableEth": { "message": "Diagramos yra tik „Ethereum“ tinkluose." }, - "confirmClear": { - "message": "Ar tikrai norite panaikinti patvirtintas svetaines?" - }, "contractInteraction": { "message": "Sutartinė sąveika" }, "reject": { "message": "Atmesti" }, - "providerRequest": { - "message": "$1 norėtų prisijungti prie jūsų paskyros" - }, - "providerRequestInfo": { - "message": "Ši svetainė prašo prieigos peržiūrėti jūsų dabartinės paskyros adresą. Visada patikrinkite, ar pasitikite svetainėmis, su kuriomis sąveikaujate." - }, "about": { "message": "Apie" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "Jeigu jau turite šiek tiek eterių, sparčiausias būdas gauti eterių į naują piniginę yra tiesioginis įnašas." }, - "dismiss": { - "message": "Atsisakyti" - }, "done": { "message": "Atlikta" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 5a5b5908e437..116af0cc4cd6 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -1,13 +1,7 @@ { - "privacyModeDefault": { - "message": "Privātais režīms tagad ieslēgts pēc noklusējuma" - }, "chartOnlyAvailableEth": { "message": "Grafiks pieejams vienīgi Ethereum tīklos." }, - "confirmClear": { - "message": "Vai tiešām vēlaties dzēst apstiprinātās vietnes?" - }, "contractInteraction": { "message": "Līguma mijiedarbības" }, @@ -18,12 +12,6 @@ "reject": { "message": "Noraidīt" }, - "providerRequest": { - "message": "$1 vēlas izveidot savienojumu ar jūsu kontu" - }, - "providerRequestInfo": { - "message": "Šī lapa pieprasa piekļuvi jūsu pašreizēja konta adreses informācijai. Vienmēr pārliecinieties, ka uzticaties lapām, kuras apmeklējat." - }, "about": { "message": "Par" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "Ja jums jau ir Ether, tad visātrāk Ether savā makā varat saņemt ar tiešo iemaksu." }, - "dismiss": { - "message": "Noraidīt" - }, "done": { "message": "Pabeigts" }, diff --git a/app/_locales/ml/messages.json b/app/_locales/ml/messages.json index cd9736e8c5f6..c898d1dc2c49 100644 --- a/app/_locales/ml/messages.json +++ b/app/_locales/ml/messages.json @@ -54,9 +54,6 @@ "details": { "message": "വിശദാംശങ്ങൾ‌" }, - "dismiss": { - "message": "ബഹിഷ്‌ക്കരിക്കുക" - }, "done": { "message": "പൂർത്തിയാക്കി" }, diff --git a/app/_locales/mr/messages.json b/app/_locales/mr/messages.json index fa8635a39832..ef341f40cea4 100644 --- a/app/_locales/mr/messages.json +++ b/app/_locales/mr/messages.json @@ -54,9 +54,6 @@ "details": { "message": "तपशील" }, - "dismiss": { - "message": "डिसमिस करा" - }, "done": { "message": "पूर्ण झाले" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 639f37cd2a76..403afc3af785 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Mod Privasi kini diaktifkan secara lalai" - }, "chartOnlyAvailableEth": { "message": "Carta hanya tersedia di rangkaian Ethereum." }, - "confirmClear": { - "message": "Adakah anda pasti mahu mengosongkan tapak web diluluskan?" - }, "contractInteraction": { "message": "Interaksi Kontrak" }, "reject": { "message": "Tolak" }, - "providerRequest": { - "message": "$1 ingin menyambung kepada akaun anda" - }, - "providerRequestInfo": { - "message": "Tapak ini meminta akses untuk melihat alamat akaun semasa anda. Sentiasa pastikan anda mempercayai tapak web yang anda berinteraksi." - }, "about": { "message": "Mengenai" }, @@ -353,9 +341,6 @@ "directDepositEtherExplainer": { "message": "Jika anda sudah mempunyai Ether, cara paling cepat untuk mendapatkan Ether di dompet baru anda ialah dengan deposit langsung." }, - "dismiss": { - "message": "Singkirkan" - }, "done": { "message": "Selesai" }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index c8ddfa3ab8ea..7ba285208d06 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -1,13 +1,7 @@ { - "confirmClear": { - "message": "Weet je zeker dat je goedgekeurde websites wilt wissen?" - }, "reject": { "message": "Afwijzen" }, - "providerRequestInfo": { - "message": "Het onderstaande domein probeert toegang tot de Ethereum API te vragen zodat deze kan communiceren met de Ethereum-blockchain. Controleer altijd eerst of u op de juiste site bent voordat u Ethereum-toegang goedkeurt." - }, "accountDetails": { "message": "Accountgegevens" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 62875b91f644..c57cd08c169a 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Personvernmodus er nå aktivert som standard" - }, "chartOnlyAvailableEth": { "message": "Diagram kun tilgjengelig på Ethereum-nettverk." }, - "confirmClear": { - "message": "Er du sikker på at du vil tømme godkjente nettsteder?" - }, "contractInteraction": { "message": "Kontraktssamhandling" }, "reject": { "message": "Avslå" }, - "providerRequest": { - "message": "$1 ønsker å forbindes med kontoen din " - }, - "providerRequestInfo": { - "message": "Dette nettstedet ber om tilgang til å vise din nåværende kontoadresse. Alltid kontroller at du stoler på nettstedene du samhandler med." - }, "about": { "message": "Info" }, @@ -359,9 +347,6 @@ "directDepositEtherExplainer": { "message": "Hvis du allerede har noe Ether, er den raskeste måten å få Ether i den nye lommeboken din på ved hjelp av direkte innskudd." }, - "dismiss": { - "message": "Lukk" - }, "done": { "message": "Ferdig" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 5c451cd688e7..1a3807ad5516 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -1,7 +1,4 @@ { - "confirmClear": { - "message": "Sigurado ka bang gusto mong i-clear ang mga naaprubahang website?" - }, "appName": { "message": "MetaMask", "description": "The name of the application" @@ -12,9 +9,6 @@ "reject": { "message": "Tanggihan" }, - "providerRequestInfo": { - "message": "Ang domain na nakalista sa ibaba ay sinusubukang humiling ng access sa Ethereum API upang maaari itong makipag-ugnayan sa Ethereum blockchain. Laging i-double check na ikaw ay nasa tamang site bago aprubahan ang Ethereum access." - }, "accountDetails": { "message": "Detalye ng Account" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 875d2385b098..1e77ba1f34f9 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Tryb prywatny jest domyślnie włączony" - }, "chartOnlyAvailableEth": { "message": "Wykres dostępny tylko w sieciach Ethereum" }, - "confirmClear": { - "message": "Czy na pewno chcesz usunąć zatwierdzone strony internetowe?" - }, "contractInteraction": { "message": "Interakcja z kontraktem" }, "reject": { "message": "Odrzuć" }, - "providerRequest": { - "message": "$1 chce połączyć się z Twoim kontem" - }, - "providerRequestInfo": { - "message": "Ta strona prosi o dostęp, aby zobaczyć adres Twojego aktualnego konta. Zawsze upewnij się, że ufasz stronom, z którymi wchodzisz w interakcję." - }, "about": { "message": "Informacje" }, @@ -359,9 +347,6 @@ "directDepositEtherExplainer": { "message": "Jeśli już masz Eter, najszybciej umieścisz go w swoim nowym portfelu przy pomocy bezpośredniego depozytu." }, - "dismiss": { - "message": "Zamknij" - }, "done": { "message": "Gotowe" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 6851c8489a4b..9d9ed04509d3 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1,13 +1,7 @@ { - "confirmClear": { - "message": "Tem certeza de que deseja limpar sites aprovados?" - }, "reject": { "message": "Rejeitar" }, - "providerRequestInfo": { - "message": "O domínio listado abaixo está tentando solicitar acesso à API Ethereum para que ele possa interagir com o blockchain Ethereum. Sempre verifique se você está no site correto antes de aprovar o acesso à Ethereum." - }, "account": { "message": "Conta" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 7916a05b6c69..ffebf02bcf81 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1,13 +1,7 @@ { - "privacyModeDefault": { - "message": "O Modo de Privacidade está ativado por padrão" - }, "chartOnlyAvailableEth": { "message": "Tabela disponível apenas em redes de Ethereum." }, - "confirmClear": { - "message": "Tem certeza de que deseja limpar os sites aprovados?" - }, "contractInteraction": { "message": "Interação do Contrato" }, @@ -18,12 +12,6 @@ "reject": { "message": "Rejeitar" }, - "providerRequest": { - "message": "$1 gostaria de se conectar à sua conta" - }, - "providerRequestInfo": { - "message": "Este site está solicitando acesso para visualizar o seu endereço de conta atual. Certifique-se sempre de confiar nos sites com os quais você interage." - }, "about": { "message": "Sobre" }, @@ -356,9 +344,6 @@ "directDepositEtherExplainer": { "message": "Se você já tem Ether, a forma mais rápida de colocá-lo em sua nova carteira é o depósito direto." }, - "dismiss": { - "message": "Dispensar" - }, "done": { "message": "Concluído" }, diff --git a/app/_locales/pt_PT/messages.json b/app/_locales/pt_PT/messages.json index b2f844fdd8c8..ae85eb26c119 100644 --- a/app/_locales/pt_PT/messages.json +++ b/app/_locales/pt_PT/messages.json @@ -63,9 +63,6 @@ "details": { "message": "Detalhes" }, - "dismiss": { - "message": "Ignorar" - }, "done": { "message": "Concluído" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index 0d20037290ac..2d1c8579be08 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Modul de confidențialitate este activat acum în mod implicit" - }, "chartOnlyAvailableEth": { "message": "Grafic disponibil numai pe rețelele Ethereum." }, - "confirmClear": { - "message": "Sunteți sigur că doriți să ștergeți site-urile aprobate?" - }, "contractInteraction": { "message": "Interacțiune contract" }, "reject": { "message": "Respingeți" }, - "providerRequest": { - "message": "$1 ar dori să se conecteze la contul dvs." - }, - "providerRequestInfo": { - "message": "Acest site solicită acces pentru a vedea adresa curentă a contului dvs. Asigurați-vă întotdeauna că aveți încredere în site-urile cu care interacționați." - }, "about": { "message": "Despre" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "Dacă deja aveți Ether, cel mai rapid mod de a avea Ether în portofelul nou prin depunere directă." }, - "dismiss": { - "message": "Închide" - }, "done": { "message": "Efectuat" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index a5d60718dfd8..a25fd87fb52b 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1,13 +1,7 @@ { - "confirmClear": { - "message": "Вы уверены, что хотите очистить утвержденные веб-сайты?Tem certeza de que deseja limpar sites aprovados?" - }, "reject": { "message": "Отклонить" }, - "providerRequestInfo": { - "message": "Домен, указанный ниже, пытается запросить доступ к API-интерфейсу Ethereum, чтобы он мог взаимодействовать с блокчейном Ethereum. Всегда проверяйте, что вы находитесь на правильном сайте, прежде чем одобрять доступ к веб-сайту." - }, "account": { "message": "Счет" }, @@ -542,18 +536,12 @@ "youSign": { "message": "Вы подписываете" }, - "privacyModeDefault": { - "message": "Режим конфиденциальности теперь включен по умолчанию" - }, "chartOnlyAvailableEth": { "message": "Диаграмма доступна только в сетях Ethereum." }, "contractInteraction": { "message": "Взаимодействие с контрактом" }, - "providerRequest": { - "message": "$1 запрашивает доступ к вашему аккаунту" - }, "about": { "message": "О нас" }, @@ -746,9 +734,6 @@ "deleteAccount": { "message": "Удалить аккаунт" }, - "dismiss": { - "message": "Отклюнить" - }, "downloadGoogleChrome": { "message": "Скачать Google Chrome" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index c97be718e73b..fa0280dce57a 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Režim súkromia je povolený. Je prednastavený automaticky" - }, "chartOnlyAvailableEth": { "message": "Graf je k dispozícii iba v sieťach Ethereum." }, - "confirmClear": { - "message": "Naozaj chcete vymazať schválené webové stránky?" - }, "contractInteraction": { "message": "Zmluvná interakcia" }, "reject": { "message": "Odmítnout" }, - "providerRequest": { - "message": "$1 sa chce pripojiť k vášmu účtu" - }, - "providerRequestInfo": { - "message": "Níže uvedená doména se pokouší požádat o přístup k API Ethereum, aby mohla komunikovat s blokádou Ethereum. Před schválením přístupu Ethereum vždy zkontrolujte, zda jste na správném místě." - }, "about": { "message": "Informácie" }, @@ -353,9 +341,6 @@ "directDepositEtherExplainer": { "message": "Pokud už vlastníte nějaký Ether, nejrychleji ho dostanete do peněženky přímým vkladem." }, - "dismiss": { - "message": "Zatvoriť" - }, "done": { "message": "Hotovo" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 0110f49a7899..292f24467ffd 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Zasebnostni način je zdaj privzeto omogočen" - }, "chartOnlyAvailableEth": { "message": "Grafikon na voljo le v glavnih omrežjih." }, - "confirmClear": { - "message": "Ste prepričani da želite počistiti odobrene spletne strani?" - }, "contractInteraction": { "message": "Interakcija s pogodbo" }, "reject": { "message": "Zavrni" }, - "providerRequest": { - "message": "$1 se želi povezati z vašim računom." - }, - "providerRequestInfo": { - "message": "Domena zahteva dostop do verige blokov in ogled vašega računa. Pred potrditvjo vedno preverite ali ste na želeni spletni strani." - }, "about": { "message": "O možnostih" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "Če že imate Ether, ga lahko najhitreje dobite v MetaMask z neposrednim vplačilom." }, - "dismiss": { - "message": "Opusti" - }, "done": { "message": "Končano" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index ffedda639878..2c5c3225b24c 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Režim privatnosti je podrazumevano omogućen" - }, "chartOnlyAvailableEth": { "message": "Grafikon dostupan jedino na mrežama Ethereum." }, - "confirmClear": { - "message": "Da li ste sigurni da želite da obrišete odobrene veb lokacije?" - }, "contractInteraction": { "message": "Ugovorna interakcija" }, "reject": { "message": "Одбиј" }, - "providerRequest": { - "message": "$1 bi hteo da se poveže sa vašim nalogom" - }, - "providerRequestInfo": { - "message": "Ovaj sajt traži pristup kako bi video vašu trenutnu adresu naloga. Uvek budite sigurni da verujete sajtovima s kojima komunicirate." - }, "about": { "message": "Основни подаци" }, @@ -359,9 +347,6 @@ "directDepositEtherExplainer": { "message": "Ako već imate neki Ether, najbrži način da preuzmete Ether u svoj novi novčanik jeste direktnim deponovanjem." }, - "dismiss": { - "message": "Одбаци" - }, "done": { "message": "Gotovo" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index 934cfe086194..bd02b0f477b9 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -1,13 +1,7 @@ { - "privacyModeDefault": { - "message": "Integritetsläge är nu aktiverat som standard" - }, "chartOnlyAvailableEth": { "message": "Tabellen är endast tillgänglig på Ethereum-nätverk." }, - "confirmClear": { - "message": "Är du säker på att du vill rensa godkända webbplatser?" - }, "contractInteraction": { "message": "Kontraktinteraktion" }, @@ -18,12 +12,6 @@ "reject": { "message": "Avvisa" }, - "providerRequest": { - "message": "$1 vill ansluta till ditt konto" - }, - "providerRequestInfo": { - "message": "Den här sidan begär åtkomst till din aktuella kontoadress. Se till att du kan lita på de sidor du använder dig av." - }, "about": { "message": "Om" }, @@ -356,9 +344,6 @@ "directDepositEtherExplainer": { "message": "Om du redan har Ether är det snabbaste sättet att få Ether i din nya plånbok att göra en direktinsättning." }, - "dismiss": { - "message": "Ta bort permanent" - }, "done": { "message": "Klart" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 6ebb31f66db7..6d9f96bedada 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -1,13 +1,7 @@ { - "privacyModeDefault": { - "message": "Hali ya Faragha sasa imewezeshwa kwa chaguomsingi" - }, "chartOnlyAvailableEth": { "message": "Zogoa inapatikana kwenye mitandao ya Ethereum pekee." }, - "confirmClear": { - "message": "Una uhakika unataka kufuta tovuti zilizodihinishwa?" - }, "contractInteraction": { "message": "Mwingiliono wa Mkataba" }, @@ -18,12 +12,6 @@ "reject": { "message": "Kataa" }, - "providerRequest": { - "message": "$1 ingependa kuunganishwa kwenye akaunti yako" - }, - "providerRequestInfo": { - "message": "Tovuti hii inaomba idhini ya kuangalia anwani yako ya akaunti ya sasa. Daima hakikisha unaziamami tovuti ambazo unaingiliana nazo." - }, "about": { "message": "Kuhusu" }, @@ -356,9 +344,6 @@ "directDepositEtherExplainer": { "message": "Ikiwa tayari una sarafu kadhaa za Ether, njia rahisi ya kupata Ether kwenye waleti yako mpya kupitia kuweka moja kwa moja." }, - "dismiss": { - "message": "Ondoa" - }, "done": { "message": "Imekamilika" }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index c671370399f6..dc48abb02dbf 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -1,16 +1,10 @@ { - "confirmClear": { - "message": "அங்கீகரிக்கப்பட்ட வலைத்தளங்களை நிச்சயமாக நீக்க விரும்புகிறீர்களா?" - }, "approve": { "message": "ஒப்புதல்" }, "reject": { "message": "நிராகரி" }, - "providerRequestInfo": { - "message": "கீழே பட்டியலிடப்பட்டுள்ள டொமைன் Web3 ஏபிஐ அணுகலைக் கோருவதற்கு முயற்சிக்கிறது, எனவே இது Ethereum blockchain உடன் தொடர்பு கொள்ள முடியும். Web3 அணுகுமுறையை அங்கீகரிப்பதற்கு முன் சரியான தளத்தில் இருப்பதை எப்போதும் இருமுறை சரிபார்க்கவும்." - }, "account": { "message": "கணக்கு" }, @@ -575,9 +569,6 @@ "delete": { "message": "நீக்கு" }, - "dismiss": { - "message": "நிராகரி" - }, "fast": { "message": "வேகமான" }, diff --git a/app/_locales/te/messages.json b/app/_locales/te/messages.json index 6de4b0464def..2df11217d5a7 100644 --- a/app/_locales/te/messages.json +++ b/app/_locales/te/messages.json @@ -54,9 +54,6 @@ "details": { "message": "వివరాలు" }, - "dismiss": { - "message": "తొలగించు" - }, "done": { "message": "పూర్తయింది" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index 14b8acc6484a..b511b5487b2b 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -1,16 +1,7 @@ { - "confirmClear": { - "message": "คุณแน่ใจหรือไม่ว่าต้องการล้างเว็บไซต์ที่ผ่านการอนุมัติ" - }, "reject": { "message": "ปฏิเสธ" }, - "providerRequest": { - "message": "$1 ต้องการเชื่อมต่อกับบัญชีของคุณ" - }, - "providerRequestInfo": { - "message": "โดเมนที่แสดงด้านล่างกำลังพยายามขอเข้าถึง API ของ Ethereum เพื่อให้สามารถโต้ตอบกับบล็อค Ethereum ได้ ตรวจสอบว่าคุณอยู่ในไซต์ที่ถูกต้องก่อนที่จะอนุมัติการเข้าถึง Ethereum เสมอ" - }, "about": { "message": "เกี่ยวกับ" }, @@ -181,9 +172,6 @@ "directDepositEtherExplainer": { "message": "ถ้าคุณมีอีเธอร์อยู่แล้ววิธีการที่เร็วที่สุดในการเอาเงินเข้ากระเป๋าใหม่ก็คือการโอนตรงๆ" }, - "dismiss": { - "message": "ปิด" - }, "done": { "message": "เสร็จสิ้น" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 3dc50dbbfb2c..fdb1ae927cd2 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1,13 +1,7 @@ { - "confirmClear": { - "message": "Onaylanmış web sitelerini silmek istediğinizden emin misiniz?" - }, "reject": { "message": "Reddetmek" }, - "providerRequestInfo": { - "message": "Aşağıda listelenen etki alanı, Ethereum API'sine erişim talep etmeye çalışmaktadır, böylece Ethereum blockchain ile etkileşime girebilir. Web3 erişimini onaylamadan önce her zaman doğru sitede olduğunuzu kontrol edin." - }, "account": { "message": "Hesap" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 0a444f24a15b..70511383c08a 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "Режим конфіденційності тепер увімкнено за замовчуванням" - }, "chartOnlyAvailableEth": { "message": "Таблиця доступна тільки в мережах Ethereum." }, - "confirmClear": { - "message": "Ви впевнені, що хочете очистити затверджені веб-сайти?" - }, "contractInteraction": { "message": "Контрактна взаємодія" }, "reject": { "message": "Відхилити" }, - "providerRequest": { - "message": "$1 бажає підключитися до вашого облікового запису" - }, - "providerRequestInfo": { - "message": "Цей сайт запитує доступ на перегляд вашої поточної адреси облікового запису. Завжди взаємодійте лише з веб-сайтами, яким довіряєте." - }, "about": { "message": "Про Google Chrome" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "Якщо ви вже маєте ефір, пряме переведення – найшвидший спосіб передати ефір у свій гаманець." }, - "dismiss": { - "message": "Відхилити" - }, "done": { "message": "Готово" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index e6e71dbdbdae..6136403baebc 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1,13 +1,7 @@ { - "confirmClear": { - "message": "Bạn có chắc chắn muốn xóa các trang web được phê duyệt không?" - }, "reject": { "message": "Từ chối" }, - "providerRequestInfo": { - "message": "Miền được liệt kê bên dưới đang cố gắng yêu cầu quyền truy cập vào API Ethereum để nó có thể tương tác với chuỗi khối Ethereum. Luôn kiểm tra kỹ xem bạn có đang ở đúng trang web trước khi phê duyệt quyền truy cập Ethereum hay không." - }, "account": { "message": "Tài khoản" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 0d4eeda8d930..a9825be5acf2 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "现已默认启用隐私模式" - }, "chartOnlyAvailableEth": { "message": "聊天功能仅对以太坊网络开放。" }, - "confirmClear": { - "message": "您确定要清除已批准的网站吗?" - }, "contractInteraction": { "message": "合约交互" }, "reject": { "message": "拒绝" }, - "providerRequest": { - "message": "$1 希望关联您的账户" - }, - "providerRequestInfo": { - "message": "下面列出的域正在尝试请求访问Ethereum API,以便它可以与以太坊区块链进行交互。在批准Ethereum访问之前,请务必仔细检查您是否在正确的站点上。" - }, "about": { "message": "关于" }, @@ -362,9 +350,6 @@ "directDepositEtherExplainer": { "message": "如果你已经有了一些 Ether,通过直接转入是你的新钱包获取 Ether 的最快捷方式。" }, - "dismiss": { - "message": "关闭" - }, "done": { "message": "完成" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index cc123c34527b..6cbfd0794179 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -1,25 +1,13 @@ { - "privacyModeDefault": { - "message": "隱私模式現已根據預設開啟" - }, "chartOnlyAvailableEth": { "message": "圖表僅適用於以太坊網路。" }, - "confirmClear": { - "message": "您確定要清除已批准的網站紀錄?" - }, "contractInteraction": { "message": "合約互動" }, "reject": { "message": "拒絕" }, - "providerRequest": { - "message": "$1 請求訪問帳戶權限" - }, - "providerRequestInfo": { - "message": "此網站希望能讀取您的帳戶資訊。請務必確認您信任這個網站、並了解後續可能的交易行為。" - }, "about": { "message": "關於" }, @@ -359,9 +347,6 @@ "directDepositEtherExplainer": { "message": "如果您已經擁有乙太幣,直接存入功能是讓新錢包最快取得乙太幣的方式。" }, - "dismiss": { - "message": "關閉" - }, "done": { "message": "完成" }, diff --git a/app/images/broken-line.svg b/app/images/broken-line.svg new file mode 100644 index 000000000000..ec4ed0d9c64b --- /dev/null +++ b/app/images/broken-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/images/connect-white.svg b/app/images/connect-white.svg new file mode 100644 index 000000000000..e9063ae4639f --- /dev/null +++ b/app/images/connect-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/scripts/background.js b/app/scripts/background.js index 3f152fbb7b49..eb75122de744 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -64,6 +64,7 @@ const isEdge = !isIE && !!window.StyleMedia let popupIsOpen = false let notificationIsOpen = false const openMetamaskTabsIDs = {} +const requestAccountTabIds = {} // state persistence const localStore = new LocalStore() @@ -247,6 +248,9 @@ function setupController (initState, initLangCode) { // platform specific api platform, encryptor: isEdge ? new EdgeEncryptor() : undefined, + getRequestAccountTabIds: () => { + return requestAccountTabIds + }, }) const provider = controller.provider @@ -386,6 +390,17 @@ function setupController (initState, initLangCode) { }) } } else { + if (remotePort.sender && remotePort.sender.tab && remotePort.sender.url) { + const tabId = remotePort.sender.tab.id + const url = new URL(remotePort.sender.url) + const origin = url.hostname + + remotePort.onMessage.addListener(msg => { + if (msg.data && msg.data.method === 'eth_requestAccounts') { + requestAccountTabIds[origin] = tabId + } + }) + } connectExternal(remotePort) } } diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 14614177eecc..d583399aeff5 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -269,5 +269,5 @@ async function domIsReady () { return } // wait for load - await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true })) + return new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true })) } diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index fb6929e89d64..c1b39d76aca2 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -25,7 +25,7 @@ class PermissionsController { constructor ( { - openPopup, closePopup, notifyDomain, notifyAllDomains, keyringController, + platform, notifyDomain, notifyAllDomains, keyringController, } = {}, restoredPermissions = {}, restoredState = {}) { @@ -36,9 +36,8 @@ class PermissionsController { }) this.notifyDomain = notifyDomain this.notifyAllDomains = notifyAllDomains - this._openPopup = openPopup - this._closePopup = closePopup this.keyringController = keyringController + this._platform = platform this._restrictedMethods = getRestrictedMethods(this) this._initializePermissions(restoredPermissions) } @@ -56,6 +55,15 @@ class PermissionsController { const engine = new JsonRpcEngine() + engine.push(createLoggerMiddleware({ + walletPrefix: WALLET_METHOD_PREFIX, + restrictedMethods: Object.keys(this._restrictedMethods), + ignoreMethods: [ 'wallet_sendDomainMetadata' ], + store: this.store, + logStoreKey: LOG_STORE_KEY, + historyStoreKey: HISTORY_STORE_KEY, + })) + engine.push(createMethodMiddleware({ store: this.store, storeKey: METADATA_STORE_KEY, @@ -65,14 +73,6 @@ class PermissionsController { ), })) - engine.push(createLoggerMiddleware({ - walletPrefix: WALLET_METHOD_PREFIX, - restrictedMethods: Object.keys(this._restrictedMethods), - store: this.store, - logStoreKey: LOG_STORE_KEY, - historyStoreKey: HISTORY_STORE_KEY, - })) - engine.push(this.permissions.providerMiddlewareFunction.bind( this.permissions, { origin } )) @@ -154,7 +154,6 @@ class PermissionsController { })) } - this._closePopup && this._closePopup() delete this.pendingApprovals[id] } @@ -166,7 +165,6 @@ class PermissionsController { async rejectPermissionsRequest (id) { const approval = this.pendingApprovals[id] approval.reject(ethErrors.provider.userRejectedRequest()) - this._closePopup && this._closePopup() delete this.pendingApprovals[id] } @@ -398,7 +396,7 @@ class PermissionsController { requestUserApproval: async (req) => { const { metadata: { id } } = req - this._openPopup && this._openPopup() + this._platform.openExtensionInBrowser('connect') return new Promise((resolve, reject) => { this.pendingApprovals[id] = { resolve, reject } diff --git a/app/scripts/controllers/permissions/loggerMiddleware.js b/app/scripts/controllers/permissions/loggerMiddleware.js index f2a7a4b22e48..0e3c7f39329e 100644 --- a/app/scripts/controllers/permissions/loggerMiddleware.js +++ b/app/scripts/controllers/permissions/loggerMiddleware.js @@ -9,19 +9,20 @@ const LOG_LIMIT = 100 * permissions-related methods. */ module.exports = function createLoggerMiddleware ({ - walletPrefix, restrictedMethods, store, logStoreKey, historyStoreKey, + walletPrefix, restrictedMethods, store, logStoreKey, historyStoreKey, ignoreMethods, }) { return (req, res, next, _end) => { - let activityEntry, requestedMethods const { origin, method } = req const isInternal = method.startsWith(walletPrefix) - - if (isInternal || restrictedMethods.includes(method)) { + if ((isInternal || restrictedMethods.includes(method)) && !ignoreMethods.includes(method)) { activityEntry = logActivity(req, isInternal) if (method === `${walletPrefix}requestPermissions`) { requestedMethods = getRequestedMethods(req) } + } else if (method === 'eth_requestAccounts') { + activityEntry = logActivity(req, isInternal) + requestedMethods = [ 'eth_accounts' ] } else { return next() } @@ -30,7 +31,7 @@ module.exports = function createLoggerMiddleware ({ const time = Date.now() addResponse(activityEntry, res, time) if (!res.error && requestedMethods) { - logHistory(requestedMethods, origin, res.result, time) + logHistory(requestedMethods, origin, res.result, time, method === 'eth_requestAccounts') } cb() }) @@ -81,51 +82,59 @@ module.exports = function createLoggerMiddleware ({ return Object.keys(request.params[0]) } - function logHistory (requestedMethods, origin, result, time) { - - let accounts - const entries = result - ? result.map(perm => { - if (perm.parentCapability === 'eth_accounts') { - accounts = getAccountsFromPermission(perm) - } - return perm.parentCapability - }) - .reduce((acc, m) => { - if (requestedMethods.includes(m)) { - acc[m] = { - lastApproved: time, + function logHistory (requestedMethods, origin, result, time, isEthRequestAccounts) { + let accounts, entries + if (isEthRequestAccounts) { + accounts = result + const accountToTimeMap = accounts.reduce((acc, account) => ({ ...acc, [account]: time }), {}) + entries = { 'eth_accounts': { accounts: accountToTimeMap, lastApproved: time } } + } else { + entries = result + ? result + .map(perm => { + if (perm.parentCapability === 'eth_accounts') { + accounts = getAccountsFromPermission(perm) } - if (m === 'eth_accounts') { - acc[m].accounts = accounts + return perm.parentCapability + }) + .reduce((acc, m) => { + if (requestedMethods.includes(m)) { + if (m === 'eth_accounts') { + const accountToTimeMap = accounts.reduce((acc, account) => ({ ...acc, [account]: time }), {}) + acc[m] = { lastApproved: time, accounts: accountToTimeMap } + } else { + acc[m] = { lastApproved: time } + } } - } - return acc - }, {}) - : {} + return acc + }, {}) + : {} + } if (Object.keys(entries).length > 0) { - commitHistory(origin, entries, accounts) + commitHistory(origin, entries) } } - function commitHistory (origin, entries, accounts) { - - const history = store.getState()[historyStoreKey] + function commitHistory (origin, entries) { + const history = store.getState()[historyStoreKey] || {} + const newOriginHistory = { + ...history[origin], + ...entries, + } - if (Array.isArray(accounts)) { - if (history[origin] && history[origin]['eth_accounts']) { - history[origin]['eth_accounts']['accounts'] - .filter(acc => !accounts.includes(acc)) - .forEach(acc => accounts.unshift(acc)) + if (history[origin] && history[origin]['eth_accounts'] && entries['eth_accounts']) { + newOriginHistory['eth_accounts'] = { + lastApproved: entries['eth_accounts'].lastApproved, + accounts: { + ...history[origin]['eth_accounts'].accounts, + ...entries['eth_accounts'].accounts, + }, } - accounts.sort() } - history[origin] = { - ...history[origin], - ...entries, - } + history[origin] = newOriginHistory + store.updateState({ [historyStoreKey]: history }) } } diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 0dd2c047b41f..75707116281b 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -199,7 +199,7 @@ class TransactionController extends EventEmitter { origin, fromAddress: normalizedTxParams.from, selectedAddress: this.getSelectedAddress(), - } + }, }) } } else { diff --git a/app/scripts/lib/local-store.js b/app/scripts/lib/local-store.js index 49294c84dd64..b7212c980546 100644 --- a/app/scripts/lib/local-store.js +++ b/app/scripts/lib/local-store.js @@ -1,5 +1,6 @@ const extension = require('extensionizer') const log = require('loglevel') +const { checkForError } = require('./util') /** * A wrapper around the extension's storage local API @@ -90,21 +91,3 @@ module.exports = class ExtensionStore { function isEmpty (obj) { return Object.keys(obj).length === 0 } - -/** - * Returns an Error if extension.runtime.lastError is present - * this is a workaround for the non-standard error object thats used - * @returns {Error} - */ -function checkForError () { - const lastError = extension.runtime.lastError - if (!lastError) { - return - } - // if it quacks like an Error, its an Error - if (lastError.stack && lastError.message) { - return lastError - } - // repair incomplete error object (eg chromium v77) - return new Error(lastError.message) -} diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 114203d7f8c8..36b836eb1107 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -1,3 +1,4 @@ +const extension = require('extensionizer') const ethUtil = require('ethereumjs-util') const assert = require('assert') const BN = require('bn.js') @@ -148,6 +149,32 @@ function getRandomArrayItem (array) { return array[Math.floor((Math.random() * array.length))] } +function mapObjectValues (object, cb) { + const mappedObject = {} + Object.keys(object).forEach(key => { + mappedObject[key] = cb(key, object[key]) + }) + return mappedObject +} + +/** + * Returns an Error if extension.runtime.lastError is present + * this is a workaround for the non-standard error object thats used + * @returns {Error} + */ +function checkForError () { + const lastError = extension.runtime.lastError + if (!lastError) { + return + } + // if it quacks like an Error, its an Error + if (lastError.stack && lastError.message) { + return lastError + } + // repair incomplete error object (eg chromium v77) + return new Error(lastError.message) +} + module.exports = { removeListeners, applyListeners, @@ -159,4 +186,6 @@ module.exports = { bnToHex, BnMultiplyByFraction, getRandomArrayItem, + mapObjectValues, + checkForError, } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 68680b1cb1ab..4fc8cf75b053 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -89,6 +89,8 @@ module.exports = class MetamaskController extends EventEmitter { // platform-specific api this.platform = opts.platform + this.getRequestAccountTabIds = opts.getRequestAccountTabIds + // observable state store this.store = new ComposableObservableStore(initState) @@ -200,8 +202,7 @@ module.exports = class MetamaskController extends EventEmitter { this.permissionsController = new PermissionsController({ keyringController: this.keyringController, - openPopup: opts.openPopup, - closePopup: opts.closePopup, + platform: opts.platform, notifyDomain: this.notifyConnections.bind(this), notifyAllDomains: this.notifyAllConnections.bind(this), }, initState.PermissionsController, initState.PermissionsMetadata) @@ -560,6 +561,8 @@ module.exports = class MetamaskController extends EventEmitter { getCaveat: permissionsController.getCaveat.bind(permissionsController), updateExposedAccounts: nodeify(permissionsController.updateExposedAccounts, permissionsController), legacyExposeAccounts: nodeify(permissionsController.legacyExposeAccounts, permissionsController), + + getRequestAccountTabIds: (cb) => cb(null, this.getRequestAccountTabIds()), } } diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index d54a8a7b323a..5ae05d23040b 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -1,7 +1,7 @@ const extension = require('extensionizer') const {createExplorerLink: explorerLink} = require('etherscan-link') -const {getEnvironmentType} = require('../lib/util') +const { getEnvironmentType, checkForError } = require('../lib/util') const {ENVIRONMENT_TYPE_BACKGROUND} = require('../lib/enums') class ExtensionPlatform { @@ -66,6 +66,58 @@ class ExtensionPlatform { } } + queryTabs () { + return new Promise((resolve, reject) => { + extension.tabs.query({}, tabs => { + const err = checkForError() + if (err) { + reject(err) + } else { + resolve(tabs) + } + }) + }) + } + + currentTab () { + return new Promise((resolve, reject) => { + extension.tabs.getCurrent(tab => { + const err = checkForError() + if (err) { + reject(err) + } else { + resolve(tab) + } + }) + }) + } + + switchToTab (tabId) { + return new Promise((resolve, reject) => { + extension.tabs.update(tabId, {highlighted: true}, (tab) => { + const err = checkForError() + if (err) { + reject(err) + } else { + resolve(tab) + } + }) + }) + } + + closeTab (tabId) { + return new Promise((resolve, reject) => { + extension.tabs.remove(tabId, () => { + const err = checkForError() + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } + _showConfirmedTransaction (txMeta) { this._subscribeToNotificationClicked() diff --git a/package.json b/package.json index e9a96c0c2dc2..48683a14e07b 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "lodash.shuffle": "^4.2.0", "loglevel": "^1.4.1", "luxon": "^1.8.2", - "metamask-inpage-provider": "MetaMask/metamask-inpage-provider#LoginPerSite", + "metamask-inpage-provider": "^4.0.2", "metamask-logo": "^2.1.4", "mkdirp": "^0.5.1", "multihashes": "^0.4.12", diff --git a/test/e2e/contract-test/contract.js b/test/e2e/contract-test/contract.js index a6b5f110bf01..6c3941b9deef 100644 --- a/test/e2e/contract-test/contract.js +++ b/test/e2e/contract-test/contract.js @@ -49,6 +49,8 @@ const initialize = () => { const approveTokensWithoutGas = document.getElementById('approveTokensWithoutGas') const signTypedData = document.getElementById('signTypedData') const signTypedDataResults = document.getElementById('signTypedDataResult') + const getAccountsButton = document.getElementById('getAccounts') + const getAccountsResults = document.getElementById('getAccountsResult') const contractStatus = document.getElementById('contractStatus') const tokenAddress = document.getElementById('tokenAddress') @@ -317,6 +319,16 @@ const initialize = () => { }) }) + getAccountsButton.addEventListener('click', async () => { + try { + const accounts = await ethereum.send({ method: 'eth_accounts' }) + getAccountsResults.innerHTML = accounts[0] || 'Not able to get accounts' + } catch (error) { + console.error(error) + getAccountsResults.innerHTML = `Error: ${error}` + } + }) + } updateButtons() diff --git a/test/e2e/contract-test/index.html b/test/e2e/contract-test/index.html index 9454a67dd28d..7655b87866f9 100644 --- a/test/e2e/contract-test/index.html +++ b/test/e2e/contract-test/index.html @@ -42,6 +42,11 @@

Send Tokens

+
+

Get Accounts

+ +
+

Status

diff --git a/test/e2e/ethereum-on.spec.js b/test/e2e/ethereum-on.spec.js index ca062ca26a8b..c50615521dc3 100644 --- a/test/e2e/ethereum-on.spec.js +++ b/test/e2e/ethereum-on.spec.js @@ -113,7 +113,8 @@ describe('MetaMask', function () { let extension let popup let dapp - it('switches to a dapp', async () => { + + it('connects to the dapp', async () => { await openNewPage(driver, 'http://127.0.0.1:8080/') await delay(regularDelayMs) @@ -126,19 +127,27 @@ describe('MetaMask', function () { const windowHandles = await driver.getAllWindowHandles() extension = windowHandles[0] - popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles) - dapp = windowHandles.find(handle => handle !== extension && handle !== popup) + dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles) + popup = windowHandles.find(handle => handle !== extension && handle !== dapp) + + await driver.switchTo().window(popup) await delay(regularDelayMs) - const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) - await approveButton.click() + const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account')) + await accountButton.click() + + const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`)) + await submitButton.click() + + await waitUntilXWindowHandles(driver, 2) await driver.switchTo().window(dapp) await delay(regularDelayMs) }) it('has the ganache network id within the dapp', async () => { const networkDiv = await findElement(driver, By.css('#network')) + await delay(regularDelayMs) assert.equal(await networkDiv.getText(), '5777') }) diff --git a/test/e2e/metamask-responsive-ui.spec.js b/test/e2e/metamask-responsive-ui.spec.js index 90cf35710805..143e759ee822 100644 --- a/test/e2e/metamask-responsive-ui.spec.js +++ b/test/e2e/metamask-responsive-ui.spec.js @@ -134,7 +134,7 @@ describe('MetaMask', function () { it('show account details dropdown menu', async () => { await driver.findElement(By.css('div.menu-bar__open-in-browser')).click() const options = await driver.findElements(By.css('div.menu.account-details-dropdown div.menu__item')) - assert.equal(options.length, 3) // HD Wallet type does not have to show the Remove Account option + assert.equal(options.length, 4) // HD Wallet type does not have to show the Remove Account option await delay(regularDelayMs) }) }) diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index b5a8220b0a29..a22c0c1ca121 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -432,7 +432,7 @@ describe('MetaMask', function () { await delay(largeDelayMs) }) - it('starts a send transaction inside the dapp', async () => { + it('connects the dapp', async () => { await openNewPage(driver, 'http://127.0.0.1:8080/') await delay(regularDelayMs) @@ -445,15 +445,22 @@ describe('MetaMask', function () { windowHandles = await driver.getAllWindowHandles() extension = windowHandles[0] - popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles) - dapp = windowHandles.find(handle => handle !== extension && handle !== popup) + dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles) + popup = windowHandles.find(handle => handle !== extension && handle !== dapp) + + await driver.switchTo().window(popup) await delay(regularDelayMs) - const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) - await approveButton.click() + const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account')) + await accountButton.click() + + const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`)) + await submitButton.click() + + await waitUntilXWindowHandles(driver, 2) await driver.switchTo().window(dapp) - await delay(2000) + await delay(regularDelayMs) }) it('initiates a send from the dapp', async () => { diff --git a/test/e2e/permissions.spec.js b/test/e2e/permissions.spec.js new file mode 100644 index 000000000000..7f3a4ffa4af0 --- /dev/null +++ b/test/e2e/permissions.spec.js @@ -0,0 +1,201 @@ +const assert = require('assert') +const webdriver = require('selenium-webdriver') +const { By, until } = webdriver +const { + delay, +} = require('./func') +const { + checkBrowserForConsoleErrors, + findElement, + findElements, + openNewPage, + verboseReportOnFailure, + waitUntilXWindowHandles, + switchToWindowWithTitle, + setupFetchMocking, + prepareExtensionForTesting, +} = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') + +describe('MetaMask', function () { + let driver + let publicAddress + + const tinyDelayMs = 200 + const regularDelayMs = tinyDelayMs * 2 + const largeDelayMs = regularDelayMs * 2 + + this.timeout(0) + this.bail(true) + + before(async function () { + const result = await prepareExtensionForTesting() + driver = result.driver + await setupFetchMocking(driver) + }) + + afterEach(async function () { + if (process.env.SELENIUM_BROWSER === 'chrome') { + const errors = await checkBrowserForConsoleErrors(driver) + if (errors.length) { + const errorReports = errors.map(err => err.message) + const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}` + console.error(new Error(errorMessage)) + } + } + if (this.currentTest.state === 'failed') { + await verboseReportOnFailure(driver, this.currentTest) + } + }) + + after(async function () { + await driver.quit() + }) + + describe('Going through the first time flow, but skipping the seed phrase challenge', () => { + it('clicks the continue button on the welcome screen', async () => { + await findElement(driver, By.css('.welcome-page__header')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) + welcomeScreenBtn.click() + await delay(largeDelayMs) + }) + + it('clicks the "Create New Wallet" option', async () => { + const customRpcButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Create a Wallet')]`)) + customRpcButton.click() + await delay(largeDelayMs) + }) + + it('clicks the "No thanks" option on the metametrics opt-in screen', async () => { + const optOutButton = await findElement(driver, By.css('.btn-default')) + optOutButton.click() + await delay(largeDelayMs) + }) + + it('accepts a secure password', async () => { + const passwordBox = await findElement(driver, By.css('.first-time-flow__form #create-password')) + const passwordBoxConfirm = await findElement(driver, By.css('.first-time-flow__form #confirm-password')) + const button = await findElement(driver, By.css('.first-time-flow__form button')) + + await passwordBox.sendKeys('correct horse battery staple') + await passwordBoxConfirm.sendKeys('correct horse battery staple') + + const tosCheckBox = await findElement(driver, By.css('.first-time-flow__checkbox')) + await tosCheckBox.click() + + await button.click() + await delay(largeDelayMs) + }) + + it('skips the seed phrase challenge', async () => { + const button = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`)) + await button.click() + await delay(regularDelayMs) + + const detailsButton = await findElement(driver, By.css('.account-details__details-button')) + await detailsButton.click() + await delay(regularDelayMs) + }) + + it('gets the current accounts address', async () => { + const addressInput = await findElement(driver, By.css('.qr-ellip-address')) + publicAddress = await addressInput.getAttribute('value') + const accountModal = await driver.findElement(By.css('span .modal')) + + await driver.executeScript("document.querySelector('.account-modal-close').click()") + + await driver.wait(until.stalenessOf(accountModal)) + await delay(regularDelayMs) + }) + }) + + describe('sets permissions', () => { + let extension + let popup + let dapp + + it('connects to the dapp', async () => { + await openNewPage(driver, 'http://127.0.0.1:8080/') + await delay(regularDelayMs) + + const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) + await connectButton.click() + + await waitUntilXWindowHandles(driver, 3) + const windowHandles = await driver.getAllWindowHandles() + + extension = windowHandles[0] + dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles) + popup = windowHandles.find(handle => handle !== extension && handle !== dapp) + + await driver.switchTo().window(popup) + + await delay(regularDelayMs) + + const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account')) + await accountButton.click() + + const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`)) + await submitButton.click() + + await waitUntilXWindowHandles(driver, 2) + await driver.switchTo().window(extension) + await delay(regularDelayMs) + }) + + it('shows connected sites', async () => { + const connectedSites = await findElement(driver, By.xpath(`//button[contains(text(), 'Connected Sites')]`)) + await connectedSites.click() + + await findElement(driver, By.css('.connected-sites__title')) + + const domains = await findElements(driver, By.css('.connected-sites-list__domain')) + assert.equal(domains.length, 1) + + const domainName = await findElement(driver, By.css('.connected-sites-list__domain-name')) + assert.equal(await domainName.getText(), 'E2E Test Dapp') + + await domains[0].click() + + const permissionDescription = await findElement(driver, By.css('.connected-sites-list__permission-description')) + assert.equal(await permissionDescription.getText(), 'View Ethereum accounts') + }) + + it('can get accounts within the dapp', async () => { + await driver.switchTo().window(dapp) + await delay(regularDelayMs) + + const getAccountsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'eth_accounts')]`)) + await getAccountsButton.click() + + const getAccountsResult = await findElement(driver, By.css('#getAccountsResult')) + assert.equal((await getAccountsResult.getText()).toLowerCase(), publicAddress.toLowerCase()) + }) + + it('can disconnect all accounts', async () => { + await driver.switchTo().window(extension) + + const disconnectAllButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Disconnect All')]`)) + await disconnectAllButton.click() + + const disconnectModal = await driver.findElement(By.css('span .modal')) + + const disconnectAllModalButton = await findElement(driver, By.css('.disconnect-all-modal .btn-danger')) + await disconnectAllModalButton.click() + + await driver.wait(until.stalenessOf(disconnectModal)) + await delay(regularDelayMs) + }) + + it('can no longer get accounts within the dapp', async () => { + await driver.switchTo().window(dapp) + await delay(regularDelayMs) + + const getAccountsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'eth_accounts')]`)) + await getAccountsButton.click() + + const getAccountsResult = await findElement(driver, By.css('#getAccountsResult')) + assert.equal(await getAccountsResult.getText(), 'Not able to get accounts') + }) + }) +}) diff --git a/test/e2e/run-all.sh b/test/e2e/run-all.sh index 33c2428dab08..55a06fc88846 100755 --- a/test/e2e/run-all.sh +++ b/test/e2e/run-all.sh @@ -60,6 +60,14 @@ concurrently --kill-others \ 'yarn dapp' \ 'sleep 5 && mocha test/e2e/ethereum-on.spec' +concurrently --kill-others \ + --names 'ganache,dapp,e2e' \ + --prefix '[{time}][{name}]' \ + --success first \ + 'yarn ganache:start' \ + 'yarn dapp' \ + 'sleep 5 && mocha test/e2e/permissions.spec' + export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x250F458997A364988956409A164BA4E16F0F99F916ACDD73ADCD3A1DE30CF8D1,0 --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000" concurrently --kill-others \ --names 'ganache,sendwithprivatedapp,e2e' \ diff --git a/test/e2e/signature-request.spec.js b/test/e2e/signature-request.spec.js index a5be61baf077..f36b6ac5131f 100644 --- a/test/e2e/signature-request.spec.js +++ b/test/e2e/signature-request.spec.js @@ -119,12 +119,13 @@ describe('MetaMask', function () { }) }) - describe('provider listening for events', () => { + describe('successfuly signs typed data', () => { let extension let popup let dapp let windowHandles - it('switches to a dapp', async () => { + + it('connects to the dapp', async () => { await openNewPage(driver, 'http://127.0.0.1:8080/') await delay(regularDelayMs) @@ -134,18 +135,24 @@ describe('MetaMask', function () { await delay(regularDelayMs) await waitUntilXWindowHandles(driver, 3) - windowHandles = await driver.getAllWindowHandles() + const windowHandles = await driver.getAllWindowHandles() extension = windowHandles[0] - popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles) - dapp = windowHandles.find(handle => handle !== extension && handle !== popup) + dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles) + popup = windowHandles.find(handle => handle !== extension && handle !== dapp) + + await driver.switchTo().window(popup) await delay(regularDelayMs) - const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) - await approveButton.click() + const accountButton = await findElement(driver, By.css('.permissions-connect-choose-account__account')) + await accountButton.click() + + const submitButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Submit')]`)) + await submitButton.click() + + await waitUntilXWindowHandles(driver, 2) await driver.switchTo().window(dapp) - await delay(regularDelayMs) }) it('creates a sign typed data signature request', async () => { @@ -153,6 +160,7 @@ describe('MetaMask', function () { await signTypedMessage.click() await delay(largeDelayMs) + await delay(regularDelayMs) windowHandles = await driver.getAllWindowHandles() await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles) await delay(regularDelayMs) diff --git a/ui/app/components/app/account-details/account-details.component.js b/ui/app/components/app/account-details/account-details.component.js index 55078cee0df0..2cbfb724decb 100644 --- a/ui/app/components/app/account-details/account-details.component.js +++ b/ui/app/components/app/account-details/account-details.component.js @@ -4,6 +4,7 @@ import classnames from 'classnames' import Identicon from '../../ui/identicon' import Tooltip from '../../ui/tooltip-v2' import copyToClipboard from 'copy-to-clipboard' +import { CONNECTED_ROUTE } from '../../../helpers/constants/routes' export default class AccountDetails extends Component { static contextTypes = { @@ -22,6 +23,7 @@ export default class AccountDetails extends Component { label: PropTypes.string.isRequired, checksummedAddress: PropTypes.string.isRequired, name: PropTypes.string.isRequired, + history: PropTypes.object.isRequired, } state = { @@ -42,6 +44,11 @@ export default class AccountDetails extends Component { setTimeout(() => this.setState({ hasCopied: false }), 3000) } + showConnectedSites = () => { + const { history } = this.props + history.push(CONNECTED_ROUTE) + } + render () { const { t } = this.context @@ -65,14 +72,19 @@ export default class AccountDetails extends Component {
{label}
-
- +
+ {name} - +
+ + +
list.concat(keyring.accounts), []) @@ -71,6 +76,8 @@ export default class AccountMenu extends PureComponent { const keyring = keyrings.find(kr => { return kr.accounts.includes(simpleAddress) || kr.accounts.includes(identity.address) }) + const addressDomains = addressConnectedDomainMap[identity.address] || {} + const iconAndNameForOpenDomain = addressDomains[originOfCurrentTab] return (
+ { iconAndNameForOpenDomain + ?
+ +
+ : null + } { this.renderKeyringType(keyring) } { this.renderRemoveAccount(keyring, identity) }
diff --git a/ui/app/components/app/account-menu/account-menu.container.js b/ui/app/components/app/account-menu/account-menu.container.js index ae2e28e7698e..00a0666ec3ce 100644 --- a/ui/app/components/app/account-menu/account-menu.container.js +++ b/ui/app/components/app/account-menu/account-menu.container.js @@ -11,7 +11,12 @@ import { showInfoPage, showModal, } from '../../../store/actions' -import { getMetaMaskAccounts } from '../../../selectors/selectors' +import { + getAddressConnectedDomainMap, + getMetaMaskAccounts, + getOriginOfCurrentTab, +} from '../../../selectors/selectors' + import AccountMenu from './account-menu.component' function mapStateToProps (state) { @@ -23,6 +28,8 @@ function mapStateToProps (state) { keyrings, identities, accounts: getMetaMaskAccounts(state), + addressConnectedDomainMap: getAddressConnectedDomainMap(state), + originOfCurrentTab: getOriginOfCurrentTab(state), } } diff --git a/ui/app/components/app/account-menu/index.scss b/ui/app/components/app/account-menu/index.scss index 614e191041e3..93b9766d35c1 100644 --- a/ui/app/components/app/account-menu/index.scss +++ b/ui/app/components/app/account-menu/index.scss @@ -175,4 +175,8 @@ opacity: 1; } } + + &__icon-list { + display: flex; + } } diff --git a/ui/app/components/app/connected-sites-list/connected-sites-list.component.js b/ui/app/components/app/connected-sites-list/connected-sites-list.component.js new file mode 100644 index 000000000000..bf8ba885301b --- /dev/null +++ b/ui/app/components/app/connected-sites-list/connected-sites-list.component.js @@ -0,0 +1,121 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import Button from '../../ui/button' +import IconWithFallBack from '../../ui/icon-with-fallback' + +export default class ConnectedSitesList extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + renderableDomains: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + icon: PropTypes.string, + key: PropTypes.string, + lastConnectedTime: PropTypes.string, + permissionDescriptions: PropTypes.array, + })).isRequired, + domains: PropTypes.object, + showDisconnectAccountModal: PropTypes.func.isRequired, + showDisconnectAllModal: PropTypes.func.isRequired, + } + + state = { + expandedDomain: '', + iconError: '', + domains: {}, + } + + handleDomainItemClick (domainKey) { + if (this.state.expandedDomain === domainKey) { + this.setState({ expandedDomain: '' }) + } else { + this.setState({ expandedDomain: domainKey }) + } + } + + render () { + const { renderableDomains, domains, showDisconnectAccountModal, showDisconnectAllModal } = this.props + const { expandedDomain } = this.state + const { t } = this.context + + return ( +
+ { + renderableDomains.map((domain, domainIndex) => { + const domainIsExpanded = expandedDomain === domain.key + return ( +
+
this.handleDomainItemClick(domain.key) }> +
+ + +
+
+
+ { domain.extensionId ? t('externalExtension') : domain.name } +
+
+ { domain.lastConnectedTime + ?
+ { t('domainLastConnect', [domain.lastConnectedTime]) } +
+ : null + } + {domainIsExpanded + ?
+ { domain.extensionId ? t('extensionId', [domain.extensionId]) : domain.key } +
+ : null + } +
+
+
+ { domainIsExpanded ? : } +
+
+ { domainIsExpanded + ?
+
+ { + domain.permissionDescriptions.map((description, pdIndex) => { + return ( +
+ +
+ { description } +
+
+ ) + }) + } +
+
showDisconnectAccountModal(domain.key, domains[domain.key]) } + > + { t('disconnectAccount') } +
+
+ : null + } +
+ ) + }) + } +
+ +
+
+ ) + } +} diff --git a/ui/app/components/app/connected-sites-list/connected-sites-list.container.js b/ui/app/components/app/connected-sites-list/connected-sites-list.container.js new file mode 100644 index 000000000000..a7b28215b364 --- /dev/null +++ b/ui/app/components/app/connected-sites-list/connected-sites-list.container.js @@ -0,0 +1,33 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' + +import ConnectedSitesList from './connected-sites-list.component' +import { + showModal, +} from '../../../store/actions' +import { + getRenderablePermissionsDomains, + getPermissionsDomains, +} from '../../../selectors/selectors' + +const mapStateToProps = state => { + return { + domains: getPermissionsDomains(state), + renderableDomains: getRenderablePermissionsDomains(state), + } +} + +const mapDispatchToProps = dispatch => { + return { + showDisconnectAccountModal: (domainKey, domain) => { + dispatch(showModal({ name: 'DISCONNECT_ACCOUNT', domainKey, domain })) + }, + showDisconnectAllModal: () => { + dispatch(showModal({ name: 'DISCONNECT_ALL' })) + }, + } +} + +export default compose( + connect(mapStateToProps, mapDispatchToProps) +)(ConnectedSitesList) diff --git a/ui/app/components/app/connected-sites-list/index.js b/ui/app/components/app/connected-sites-list/index.js new file mode 100644 index 000000000000..431a3a8eb6da --- /dev/null +++ b/ui/app/components/app/connected-sites-list/index.js @@ -0,0 +1 @@ +export { default } from './connected-sites-list.container' diff --git a/ui/app/components/app/connected-sites-list/index.scss b/ui/app/components/app/connected-sites-list/index.scss new file mode 100644 index 000000000000..ccf7f71e0847 --- /dev/null +++ b/ui/app/components/app/connected-sites-list/index.scss @@ -0,0 +1,115 @@ +.connected-sites-list { + font-family: Roboto; + font-style: normal; + font-weight: normal; + + &__domain, &__domain--expanded { + border-bottom: 1px solid #c4c4c4; + } + + &__domain { + cursor: pointer; + } + + &__domain--expanded { + background: #FAFBFC; + cursor: initial; + } + + &__domain-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + } + + &__domain-item-info-container { + display: flex; + } + + &__identicon-container { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 32px; + width: 32px; + } + + &__identicon-border { + height: 32px; + width: 32px; + border-radius: 50%; + border: 1px solid #F2F3F4; + position: absolute; + background: #FFFFFF; + } + + &__identicon { + width: 24px; + height: 24px; + z-index: 1; + + &--default { + z-index: 1; + color: black; + } + } + + &__domain-info { + display: flex; + flex-direction: column; + margin-left: 16px; + } + + &__domain-names { + display: flex; + align-items: center; + } + + &__domain-name { + font-size: 18px; + color: #24292E; + } + + &__domain-origin, &__domain-last-connected { + font-size: 12px; + color: #6A737D; + } + + &__domain-last-connected { + margin-top: 2px; + } + + &__expand-arrow { + align-self: flex-start; + margin-top: 6px; + } + + &__permissions { + padding-left: 16px; + padding-bottom: 24px; + } + + &__permission { + display: flex; + align-items: center; + color: #6A737D; + margin-left: 16px; + } + + &__permission-description { + margin-left: 18px; + } + + &__disconnect { + margin-top: 24px; + color: #D73A49; + cursor: pointer; + } + + &__disconnect-all { + padding: 10px; + width: 50%; + } +} \ No newline at end of file diff --git a/ui/app/components/app/dropdowns/account-details-dropdown.js b/ui/app/components/app/dropdowns/account-details-dropdown.js index 89e7f91ef60c..dc09b8c64336 100644 --- a/ui/app/components/app/dropdowns/account-details-dropdown.js +++ b/ui/app/components/app/dropdowns/account-details-dropdown.js @@ -1,9 +1,12 @@ import React, { Component } from 'react' const PropTypes = require('prop-types') +const { compose } = require('recompose') +const { withRouter } = require('react-router-dom') const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../../store/actions') const { getSelectedIdentity, getRpcPrefsForCurrentProvider } = require('../../../selectors/selectors') +const { CONNECTED_ROUTE } = require('../../../helpers/constants/routes') const genAccountLink = require('../../../../lib/account-link.js') const { Menu, Item, CloseArea } = require('./components/menu') @@ -12,7 +15,7 @@ AccountDetailsDropdown.contextTypes = { metricsEvent: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsDropdown) +module.exports = compose(withRouter, connect(mapStateToProps, mapDispatchToProps))(AccountDetailsDropdown) function mapStateToProps (state) { return { @@ -58,6 +61,7 @@ AccountDetailsDropdown.prototype.render = function AccountDetailsDropdown () { viewOnEtherscan, showRemoveAccountConfirmationModal, rpcPrefs, + history, } = this.props const address = selectedIdentity.address @@ -134,6 +138,23 @@ AccountDetailsDropdown.prototype.render = function AccountDetailsDropdown () { )} /> + { + e.stopPropagation() + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Account Options', + name: 'Opened Connected Sites', + }, + }) + history.push(CONNECTED_ROUTE) + }} + text={this.context.t('connectedSites')} + icon={( + + )} + /> { isRemovable ? ( diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss index 1afbebd00484..7578aa204a76 100644 --- a/ui/app/components/app/index.scss +++ b/ui/app/components/app/index.scss @@ -38,7 +38,7 @@ @import '../../pages/index'; -@import 'provider-page-container/index'; +@import 'permission-page-container/index'; @import 'selected-account/index'; @@ -64,18 +64,12 @@ @import 'transaction-status/index'; -@import 'app-header/index'; - @import 'sidebars/index'; @import '../ui/unit-input/index'; @import 'gas-customization/gas-modal-page-container/index'; -@import 'gas-customization/gas-modal-page-container/index'; - -@import 'gas-customization/gas-modal-page-container/index'; - @import 'gas-customization/index'; @import 'gas-customization/gas-price-button-group/index'; @@ -87,3 +81,7 @@ @import 'multiple-notifications/index'; @import 'signature-request/index'; + +@import 'connected-sites-list/index'; + +@import '../ui/icon-with-fallback/index'; diff --git a/ui/app/components/app/modal/modal.component.js b/ui/app/components/app/modal/modal.component.js index f0fdd3bd522a..753445460fa4 100644 --- a/ui/app/components/app/modal/modal.component.js +++ b/ui/app/components/app/modal/modal.component.js @@ -16,6 +16,7 @@ export default class Modal extends PureComponent { submitType: PropTypes.string, submitText: PropTypes.string, submitDisabled: PropTypes.bool, + hideFooter: PropTypes.bool, // Cancel button (left button) onCancel: PropTypes.func, cancelType: PropTypes.string, @@ -41,6 +42,7 @@ export default class Modal extends PureComponent { cancelText, contentClass, containerClass, + hideFooter, } = this.props return ( @@ -61,27 +63,30 @@ export default class Modal extends PureComponent {
{ children }
-
- { - onCancel && ( - - ) - } - -
+ { !hideFooter + ?
+ { + onCancel && ( + + ) + } + +
+ : null + } ) } diff --git a/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js deleted file mode 100644 index ceaa20a951b0..000000000000 --- a/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js +++ /dev/null @@ -1,39 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import Modal, { ModalContent } from '../../modal' - -export default class ClearApprovedOrigins extends PureComponent { - static propTypes = { - hideModal: PropTypes.func.isRequired, - clearApprovedOrigins: PropTypes.func.isRequired, - } - - static contextTypes = { - t: PropTypes.func, - } - - handleClear = () => { - const { clearApprovedOrigins, hideModal } = this.props - clearApprovedOrigins() - hideModal() - } - - render () { - const { t } = this.context - - return ( - this.props.hideModal()} - submitText={t('ok')} - cancelText={t('nevermind')} - submitType="secondary" - > - - - ) - } -} diff --git a/ui/app/components/app/modals/clear-approved-origins/index.js b/ui/app/components/app/modals/clear-approved-origins/index.js deleted file mode 100644 index b3e321995189..000000000000 --- a/ui/app/components/app/modals/clear-approved-origins/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './clear-approved-origins.container' diff --git a/ui/app/components/app/modals/disconnect-account/disconnect-account.component.js b/ui/app/components/app/modals/disconnect-account/disconnect-account.component.js new file mode 100644 index 000000000000..0d88a03a0598 --- /dev/null +++ b/ui/app/components/app/modals/disconnect-account/disconnect-account.component.js @@ -0,0 +1,51 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Modal from '../../modal' +import Button from '../../../ui/button' + + +export default class DisconnectAccount extends PureComponent { + static propTypes = { + hideModal: PropTypes.func.isRequired, + disconnectAccount: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + render () { + const { t } = this.context + const { hideModal, disconnectAccount } = this.props + + return ( + hideModal()} + hideFooter + > +
+
+ { t('disconnectAccountModalDescription') } +
+ + +
+
+ ) + } +} diff --git a/ui/app/components/app/modals/disconnect-account/disconnect-account.container.js b/ui/app/components/app/modals/disconnect-account/disconnect-account.container.js new file mode 100644 index 000000000000..628b3ca0f141 --- /dev/null +++ b/ui/app/components/app/modals/disconnect-account/disconnect-account.container.js @@ -0,0 +1,41 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' +import DisconnectAccount from './disconnect-account.component' +import { removePermissionsFor } from '../../../../store/actions' + +const mapStateToProps = state => { + return { + ...state.appState.modal.modalState.props || {}, + } +} + +const mapDispatchToProps = dispatch => { + return { + disconnectAccount: (domainKey, domain) => { + const permissionMethodNames = domain.permissions.map(perm => perm.parentCapability) + dispatch(removePermissionsFor({ [domainKey]: permissionMethodNames })) + }, + } +} + +const mergeProps = (stateProps, dispatchProps) => { + const { + domainKey, + domain, + } = stateProps + const { + disconnectAccount: dispatchDisconnectAccount, + } = dispatchProps + + return { + ...stateProps, + ...dispatchProps, + disconnectAccount: () => dispatchDisconnectAccount(domainKey, domain), + } +} + +export default compose( + withModalProps, + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(DisconnectAccount) diff --git a/ui/app/components/app/modals/disconnect-account/index.js b/ui/app/components/app/modals/disconnect-account/index.js new file mode 100644 index 000000000000..43bfac9fdcec --- /dev/null +++ b/ui/app/components/app/modals/disconnect-account/index.js @@ -0,0 +1 @@ +export { default } from './disconnect-account.container' diff --git a/ui/app/components/app/modals/disconnect-account/index.scss b/ui/app/components/app/modals/disconnect-account/index.scss new file mode 100644 index 000000000000..861b7cec224e --- /dev/null +++ b/ui/app/components/app/modals/disconnect-account/index.scss @@ -0,0 +1,25 @@ +.disconnect-account-modal { + &__description { + color: #24292E; + margin-bottom: 24px; + } + + &__cancel-button { + border: none; + margin-top: 12px; + } +} + +.disconnect-account-modal-container { + .modal-container__header-text { + @extend %header--18; + } + + .modal-container__content { + padding-bottom: 18px; + + @media screen and (max-width: 575px) { + padding-bottom: 18px; + } + } +} \ No newline at end of file diff --git a/ui/app/components/app/modals/disconnect-all/disconnect-all.component.js b/ui/app/components/app/modals/disconnect-all/disconnect-all.component.js new file mode 100644 index 000000000000..2d29fd9ea092 --- /dev/null +++ b/ui/app/components/app/modals/disconnect-all/disconnect-all.component.js @@ -0,0 +1,54 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Modal from '../../modal' +import Button from '../../../ui/button' +import { DEFAULT_ROUTE } from '../../../../helpers/constants/routes' + +export default class DisconnectAll extends PureComponent { + static propTypes = { + hideModal: PropTypes.func.isRequired, + disconnectAll: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + render () { + const { t } = this.context + const { hideModal, disconnectAll, history } = this.props + + return ( + hideModal()} + hideFooter + > +
+
+ { t('disconnectAllModalDescription') } +
+ + +
+
+ ) + } +} diff --git a/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js b/ui/app/components/app/modals/disconnect-all/disconnect-all.container.js similarity index 53% rename from ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js rename to ui/app/components/app/modals/disconnect-all/disconnect-all.container.js index 2276bc7e7822..2415c3fa9fad 100644 --- a/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js +++ b/ui/app/components/app/modals/disconnect-all/disconnect-all.container.js @@ -1,16 +1,20 @@ import { connect } from 'react-redux' import { compose } from 'recompose' +import { withRouter } from 'react-router-dom' import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' -import ClearApprovedOriginsComponent from './clear-approved-origins.component' -import { clearApprovedOrigins } from '../../../../store/actions' +import DisconnectAll from './disconnect-all.component' +import { clearPermissions } from '../../../../store/actions' const mapDispatchToProps = dispatch => { return { - clearApprovedOrigins: () => dispatch(clearApprovedOrigins()), + disconnectAll: () => { + dispatch(clearPermissions()) + }, } } export default compose( withModalProps, + withRouter, connect(null, mapDispatchToProps) -)(ClearApprovedOriginsComponent) +)(DisconnectAll) diff --git a/ui/app/components/app/modals/disconnect-all/index.js b/ui/app/components/app/modals/disconnect-all/index.js new file mode 100644 index 000000000000..7fdfac530bc2 --- /dev/null +++ b/ui/app/components/app/modals/disconnect-all/index.js @@ -0,0 +1 @@ +export { default } from './disconnect-all.container' diff --git a/ui/app/components/app/modals/disconnect-all/index.scss b/ui/app/components/app/modals/disconnect-all/index.scss new file mode 100644 index 000000000000..8f69baadebb0 --- /dev/null +++ b/ui/app/components/app/modals/disconnect-all/index.scss @@ -0,0 +1,38 @@ +.disconnect-all-modal { + height: 160px; + display: flex; + flex-direction: column; + justify-content: space-between; + + &__description { + color: #24292E; + margin-bottom: 24px; + } + + &__cancel-button { + border: none; + margin-top: 12px; + } + + .btn-secondary { + border: none; + } +} + +.disconnect-all-modal-container { + .modal-container__header-text { + font-family: Roboto; + font-style: normal; + font-weight: bold; + font-size: 18px; + color: #24292E; + } + + .modal-container__content { + padding-bottom: 18px; + + @media screen and (max-width: 575px) { + padding-bottom: 18px; + } + } +} diff --git a/ui/app/components/app/modals/index.scss b/ui/app/components/app/modals/index.scss index da7a27b84086..dbf47265f3c5 100644 --- a/ui/app/components/app/modals/index.scss +++ b/ui/app/components/app/modals/index.scss @@ -11,3 +11,9 @@ @import './add-to-addressbook-modal/index'; @import './edit-approval-permission/index'; + +@import './disconnect-account/index'; + +@import './disconnect-all/index'; + +@import './new-account-modal/index'; diff --git a/ui/app/components/app/modals/modal.js b/ui/app/components/app/modals/modal.js index 02690722b676..0409e901fb11 100644 --- a/ui/app/components/app/modals/modal.js +++ b/ui/app/components/app/modals/modal.js @@ -24,11 +24,13 @@ import CancelTransaction from './cancel-transaction' import MetaMetricsOptInModal from './metametrics-opt-in-modal' import RejectTransactions from './reject-transactions' -import ClearApprovedOrigins from './clear-approved-origins' import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container' import ConfirmDeleteNetwork from './confirm-delete-network' import AddToAddressBookModal from './add-to-addressbook-modal' import EditApprovalPermission from './edit-approval-permission' +import NewAccountModal from './new-account-modal' +import DisconnectAccount from './disconnect-account' +import DisconnectAll from './disconnect-all' const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -139,6 +141,87 @@ const MODALS = { }, }, + NEW_ACCOUNT: { + contents: , + mobileModalStyle: { + width: '95%', + top: '10%', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + borderRadius: '10px', + }, + laptopModalStyle: { + width: '375px', + top: '10%', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + borderRadius: '10px', + }, + contentStyle: { + borderRadius: '10px', + }, + }, + + DISCONNECT_ACCOUNT: { + contents: , + mobileModalStyle: { + width: '95%', + top: '10%', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + borderRadius: '10px', + }, + laptopModalStyle: { + width: '375px', + top: '10%', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + borderRadius: '10px', + }, + contentStyle: { + borderRadius: '10px', + }, + }, + + DISCONNECT_ALL: { + contents: , + mobileModalStyle: { + width: '95%', + top: '10%', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + borderRadius: '10px', + }, + laptopModalStyle: { + width: '375px', + top: '10%', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + borderRadius: '10px', + }, + contentStyle: { + borderRadius: '10px', + }, + }, + ACCOUNT_DETAILS: { contents: , ...accountModalStyle, @@ -161,19 +244,6 @@ const MODALS = { }, }, - CLEAR_APPROVED_ORIGINS: { - contents: , - mobileModalStyle: { - ...modalContainerMobileStyle, - }, - laptopModalStyle: { - ...modalContainerLaptopStyle, - }, - contentStyle: { - borderRadius: '8px', - }, - }, - METAMETRICS_OPT_IN_MODAL: { contents: , mobileModalStyle: { diff --git a/ui/app/components/app/modals/new-account-modal/index.js b/ui/app/components/app/modals/new-account-modal/index.js new file mode 100644 index 000000000000..2c8b788904a4 --- /dev/null +++ b/ui/app/components/app/modals/new-account-modal/index.js @@ -0,0 +1 @@ +export { default } from './new-account-modal.container' diff --git a/ui/app/components/app/modals/new-account-modal/index.scss b/ui/app/components/app/modals/new-account-modal/index.scss new file mode 100644 index 000000000000..d6c2d0ac1c97 --- /dev/null +++ b/ui/app/components/app/modals/new-account-modal/index.scss @@ -0,0 +1,37 @@ +.new-account-modal { + @extend %col-nowrap; + @extend %modal; + + &__content { + @extend %col-nowrap; + padding: 1.5rem; + border-bottom: 1px solid $Grey-100; + + &__header { + @extend %h3; + } + } + + &__input-label { + color: $Grey-600; + margin-top: 1.25rem; + } + + &__input { + @extend %input; + margin-top: 0.75rem; + + &::placeholder { + color: $Grey-300; + } + } + + &__footer { + @extend %row-nowrap; + padding: 1rem; + + button + button { + margin-left: 1rem; + } + } +} diff --git a/ui/app/components/app/modals/new-account-modal/new-account-modal.component.js b/ui/app/components/app/modals/new-account-modal/new-account-modal.component.js new file mode 100644 index 000000000000..26224ae63b13 --- /dev/null +++ b/ui/app/components/app/modals/new-account-modal/new-account-modal.component.js @@ -0,0 +1,78 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Button from '../../../ui/button/button.component' + +export default class NewAccountModal extends Component { + + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + hideModal: PropTypes.func.isRequired, + newAccountNumber: PropTypes.number.isRequired, + onSave: PropTypes.func.isRequired, + } + + state = { + alias: '', + } + + onChange = e => { + this.setState({ + alias: e.target.value, + }) + } + + onSubmit = () => { + this.props.onSave(this.state.alias) + .then(this.props.hideModal) + } + + onKeyPress = e => { + if (e.key === 'Enter' && this.state.alias) { + this.onSubmit() + } + } + + render () { + const { t } = this.context + + return ( +
+
+
+ {t('newAccount')} +
+
+ {t('accountName')} +
+ +
+
+ + +
+
+ ) + } +} diff --git a/ui/app/components/app/modals/new-account-modal/new-account-modal.container.js b/ui/app/components/app/modals/new-account-modal/new-account-modal.container.js new file mode 100644 index 000000000000..812e98dbd137 --- /dev/null +++ b/ui/app/components/app/modals/new-account-modal/new-account-modal.container.js @@ -0,0 +1,44 @@ +import { connect } from 'react-redux' +import NewAccountModal from './new-account-modal.component' +import actions from '../../../../store/actions' + +function mapStateToProps (state) { + return { + ...state.appState.modal.modalState.props || {}, + } +} + +function mapDispatchToProps (dispatch) { + return { + hideModal: () => dispatch(actions.hideModal()), + createAccount: newAccountName => { + return dispatch(actions.addNewAccount()) + .then(newAccountAddress => { + if (newAccountName) { + dispatch(actions.setAccountLabel(newAccountAddress, newAccountName)) + } + return newAccountAddress + }) + }, + } +} + +function mergeProps (stateProps, dispatchProps) { + const { + onCreateNewAccount, + } = stateProps + const { + createAccount, + } = dispatchProps + + return { + ...stateProps, + ...dispatchProps, + onSave: (newAccountName) => { + return createAccount(newAccountName) + .then(newAccountAddress => onCreateNewAccount(newAccountAddress)) + }, + } +} + +export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(NewAccountModal) diff --git a/ui/app/components/app/permission-page-container/index.js b/ui/app/components/app/permission-page-container/index.js new file mode 100644 index 000000000000..ea3b8daaa2a6 --- /dev/null +++ b/ui/app/components/app/permission-page-container/index.js @@ -0,0 +1,3 @@ +export {default} from './permission-page-container.container' +export {default as PermissionPageContainerContent} from './permission-page-container-content' +export {default as PermissionPageContainerHeader} from './permission-page-container-header' diff --git a/ui/app/components/app/permission-page-container/index.scss b/ui/app/components/app/permission-page-container/index.scss new file mode 100644 index 000000000000..7979867fa71d --- /dev/null +++ b/ui/app/components/app/permission-page-container/index.scss @@ -0,0 +1,281 @@ +.permission-approval-container { + display: flex; + border: none; + box-shadow: none; + margin-top: 45px; + width: 466px; + min-height: 468px; + + &__header { + display: flex; + flex-direction: column; + align-items: flex-end; + border-bottom: 1px solid $geyser; + padding: 9px; + } + + &__title { + @extend %header--18; + line-height: 25px; + text-align: center; + position: fixed; + left: 0; + width: 100%; + } + + &__content { + display: flex; + overflow-y: auto; + flex: 1; + flex-direction: column; + color: #7C808E; + + &--redirect { + margin-top: 60px; + } + + h1, h2 { + color: #4A4A4A; + display: flex; + justify-content: center; + text-align: center; + } + + h2 { + font-size: 16px; + line-height: 18px; + padding: 20px; + } + + h1 { + font-size: 22px; + line-height: 26px; + padding: 20px; + } + + p { + padding: 0 40px; + text-align: center; + font-size: 12px; + line-height: 18px; + } + + a, a:hover { + color: $dodger-blue; + } + + section { + h1 { + padding: 30px 0px 0px 0px; + } + + h2 { + padding: 0px 0px 20px 0px; + } + } + + &__requested { + text-align: left; + } + + &__revoke-note { + margin-top: 24px; + } + + &__checkbox { + margin-right: 10px; + } + + &__permission { + margin-top: 18px; + + i { + color: #6A737D; + } + label { + margin-left: 6px; + color: #24292E; + } + } + + .permission-approval-visual { + display: flex; + flex-direction: row; + justify-content: space-evenly; + position: relative; + margin: 0 32px; + margin-top: 40px; + + section { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + } + + h1 { + font-size: 14px; + line-height: 18px; + padding: 8px 0 0; + } + + h2 { + font-size: 12px; + line-height: 17px; + color: #6A737D; + padding: 0; + } + + &__check { + width: 40px; + height: 40px; + background: white url("/images/permissions-check.svg") no-repeat; + margin-top: 24px; + z-index: 1; + } + + &__reject { + background: white; + z-index: 1; + display: flex; + justify-content: center; + align-items: center; + + i { + color: #D73A49; + transform: scale(3); + } + } + + &__broken-line { + z-index: 0; + position: absolute; + top: 43px; + } + + &__identicon, .icon-with-fallback__identicon { + width: 32px; + height: 32px; + z-index: 1; + + &--default { + background-color: #777A87; + color: white; + width: 64px; + height: 64px; + border-radius: 32px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + z-index: 1; + } + } + + &__identicon-container, .icon-with-fallback__identicon-container { + padding: 1rem; + flex: 1; + position: relative; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + &__identicon-border, .icon-with-fallback__identicon-border { + height: 64px; + width: 64px; + border-radius: 50%; + border: 1px solid white; + position: absolute; + background: #FFFFFF; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); + } + + &:before { + border-top: 2px dashed #CDD1E4; + content: ""; + margin: 0 auto; + position: absolute; + top: 32px; + left: 0; + bottom: 0; + right: 0; + width: 65%; + z-index: -1; + } + + &__account-info { + display: flex; + flex-direction: column; + align-items: center; + + &__label { + @extend %content-text; + line-height: 20px; + color: #000000; + } + + &__address { + @extend %font; + font-size: 12px; + line-height: 17px; + color: #6A737D; + } + } + } + + .secure-badge { + display: flex; + justify-content: center; + padding: 25px; + } + } + + &__permissions-header { + @extend %content-text; + line-height: 20px; + color: #6A737D; + + &--redirect { + text-align: center; + } + } + + &__permissions-container { + display: flex; + flex-direction: column; + margin-top: 33px; + } + + .page-container__footer { + border-top: none; + align-items: center; + + header { + width: 300px; + } + } + + &__permissions-header-redirect { + text-align: center; + } + + @media screen and (max-width: 575px) { + width: 100%; + margin-top: 25px; + padding: 10px; + + &__title { + position: initial; + } + + &__content-approval-visual { + margin-top: 16px; + } + + .page-container__footer header { + padding: 0; + } + } +} diff --git a/ui/app/components/app/permission-page-container/permission-page-container-content/index.js b/ui/app/components/app/permission-page-container/permission-page-container-content/index.js new file mode 100644 index 000000000000..899d168f930f --- /dev/null +++ b/ui/app/components/app/permission-page-container/permission-page-container-content/index.js @@ -0,0 +1 @@ +export {default} from './permission-page-container-content.component' diff --git a/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js new file mode 100644 index 000000000000..d4bf5f43bcae --- /dev/null +++ b/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js @@ -0,0 +1,155 @@ +import PropTypes from 'prop-types' +import React, { PureComponent } from 'react' +import Identicon from '../../../ui/identicon' +import IconWithFallBack from '../../../ui/icon-with-fallback' +import classnames from 'classnames' + +export default class PermissionPageContainerContent extends PureComponent { + + static propTypes = { + requestMetadata: PropTypes.object.isRequired, + domainMetadata: PropTypes.object.isRequired, + selectedPermissions: PropTypes.object.isRequired, + permissionsDescriptions: PropTypes.object.isRequired, + onPermissionToggle: PropTypes.func.isRequired, + selectedAccount: PropTypes.object, + redirect: PropTypes.bool, + permissionRejected: PropTypes.bool, + } + + static defaultProps = { + redirect: null, + permissionRejected: null, + selectedAccount: {}, + } + + static contextTypes = { + t: PropTypes.func, + } + + state = { + iconError: false, + } + + renderAccountInfo = (account) => { + return ( +
+
+ { account.label } +
+
+ { account.truncatedAddress } +
+
+ ) + } + + renderPermissionApprovalVisual = () => { + const { + requestMetadata, domainMetadata, selectedAccount, redirect, permissionRejected, + } = this.props + + return ( +
+
+ + { redirect ? null :

{domainMetadata.name}

} + { redirect ? null :

{requestMetadata.origin}

} +
+ { permissionRejected + ? + : + } + +
+
+
+ +
+ { redirect ? null : this.renderAccountInfo(selectedAccount) } +
+
+ ) + } + + renderRequestedPermissions () { + const { + selectedPermissions, permissionsDescriptions, onPermissionToggle, + } = this.props + const { t } = this.context + + const items = Object.keys(selectedPermissions).map((methodName) => { + + // the request will almost certainly be reject by rpc-cap if this happens + if (!permissionsDescriptions[methodName]) { + console.warn(`Unknown permission requested: ${methodName}`) + } + const description = permissionsDescriptions[methodName] || methodName + + return ( +
onPermissionToggle(methodName)} + > + { selectedPermissions[methodName] + ? + : + } + +
+ ) + }) + + return ( +
+ {items} +
{ t('revokeInPermissions') }
+
+ ) + } + + render () { + const { domainMetadata, redirect, permissionRejected } = this.props + const { t } = this.context + + let titleArgs + if (redirect && permissionRejected) { + titleArgs = [ 'cancelledConnectionWithMetaMask' ] + } else if (redirect) { + titleArgs = [ 'connectingWithMetaMask' ] + } else if (domainMetadata.extensionId) { + titleArgs = [ 'externalExtension', [domainMetadata.extensionId] ] + } else { + titleArgs = [ 'likeToConnect', [domainMetadata.name] ] + } + + return ( +
+
+ { t(...titleArgs) } +
+ {this.renderPermissionApprovalVisual()} + { !redirect + ?
+
+ { domainMetadata.extensionId + ? t('thisWillAllowExternalExtension', [domainMetadata.extensionId]) + : t('thisWillAllow', [domainMetadata.name]) + } +
+ { this.renderRequestedPermissions() } +
+ :
+ { t('redirectingBackToDapp') } +
+ } +
+ ) + } +} diff --git a/ui/app/components/app/permission-page-container/permission-page-container-header/index.js b/ui/app/components/app/permission-page-container/permission-page-container-header/index.js new file mode 100644 index 000000000000..45ef9036b168 --- /dev/null +++ b/ui/app/components/app/permission-page-container/permission-page-container-header/index.js @@ -0,0 +1 @@ +export {default} from './permission-page-container-header.component' diff --git a/ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js b/ui/app/components/app/permission-page-container/permission-page-container-header/permission-page-container-header.component.js similarity index 58% rename from ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js rename to ui/app/components/app/permission-page-container/permission-page-container-header/permission-page-container-header.component.js index 41bf6c3dd244..8ba3444ba2f6 100644 --- a/ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js +++ b/ui/app/components/app/permission-page-container/permission-page-container-header/permission-page-container-header.component.js @@ -1,10 +1,10 @@ import React, {PureComponent} from 'react' import NetworkDisplay from '../../network-display' -export default class ProviderPageContainerHeader extends PureComponent { +export default class PermissionPageContainerHeader extends PureComponent { render () { return ( -
+
) diff --git a/ui/app/components/app/permission-page-container/permission-page-container.component.js b/ui/app/components/app/permission-page-container/permission-page-container.component.js new file mode 100644 index 000000000000..79ca0a2057bc --- /dev/null +++ b/ui/app/components/app/permission-page-container/permission-page-container.component.js @@ -0,0 +1,149 @@ +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import deepEqual from 'fast-deep-equal' +import { PermissionPageContainerContent } from '.' +import { PageContainerFooter } from '../../ui/page-container' + +export default class PermissionPageContainer extends Component { + + static propTypes = { + approvePermissionsRequest: PropTypes.func.isRequired, + rejectPermissionsRequest: PropTypes.func.isRequired, + selectedIdentity: PropTypes.object, + permissionsDescriptions: PropTypes.object.isRequired, + request: PropTypes.object, + redirect: PropTypes.bool, + permissionRejected: PropTypes.bool, + requestMetadata: PropTypes.object, + targetDomainMetadata: PropTypes.object.isRequired, + }; + + static defaultProps = { + redirect: null, + permissionRejected: null, + request: {}, + requestMetadata: {}, + selectedIdentity: {}, + }; + + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + }; + + state = { + selectedPermissions: this.getRequestedMethodState( + this.getRequestedMethodNames(this.props) + ), + } + + componentDidUpdate () { + const newMethodNames = this.getRequestedMethodNames(this.props) + + if (!deepEqual(Object.keys(this.state.selectedPermissions), newMethodNames)) { + // this should be a new request, so just overwrite + this.setState({ + selectedPermissions: this.getRequestedMethodState(newMethodNames), + }) + } + } + + getRequestedMethodState (methodNames) { + return methodNames.reduce( + (acc, methodName) => { + acc[methodName] = true + return acc + }, + {} + ) + } + + getRequestedMethodNames (props) { + return Object.keys(props.request.permissions || {}) + } + + onPermissionToggle = methodName => { + this.setState({ + selectedPermissions: { + ...this.state.selectedPermissions, + [methodName]: !this.state.selectedPermissions[methodName], + }, + }) + } + + componentDidMount () { + this.context.metricsEvent({ + eventOpts: { + category: 'Auth', + action: 'Connect', + name: 'Tab Opened', + }, + }) + } + + onCancel = () => { + const { request, rejectPermissionsRequest } = this.props + rejectPermissionsRequest(request.metadata.id) + } + + onSubmit = () => { + const { + request: _request, approvePermissionsRequest, rejectPermissionsRequest, selectedIdentity, + } = this.props + + const request = { + ..._request, + permissions: { ..._request.permissions }, + } + + Object.keys(this.state.selectedPermissions).forEach(key => { + if (!this.state.selectedPermissions[key]) { + delete request.permissions[key] + } + }) + + if (Object.keys(request.permissions).length > 0) { + approvePermissionsRequest(request, [selectedIdentity.address]) + } else { + rejectPermissionsRequest(request.metadata.id) + } + } + + render () { + const { + requestMetadata, + targetDomainMetadata, + permissionsDescriptions, + selectedIdentity, + redirect, + permissionRejected, + } = this.props + + return ( +
+ + { !redirect + ? this.onCancel()} + cancelText={this.context.t('cancel')} + onSubmit={() => this.onSubmit()} + submitText={this.context.t('submit')} + submitButtonType="confirm" + buttonSizeLarge={false} + /> + : null + } +
+ ) + } +} diff --git a/ui/app/components/app/permission-page-container/permission-page-container.container.js b/ui/app/components/app/permission-page-container/permission-page-container.container.js new file mode 100644 index 000000000000..227eaab79de6 --- /dev/null +++ b/ui/app/components/app/permission-page-container/permission-page-container.container.js @@ -0,0 +1,30 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import { withRouter } from 'react-router-dom' +import PermissionPageContainer from './permission-page-container.component' +import { + getPermissionsDescriptions, + getDomainMetadata, +} from '../../../selectors/selectors' + +const mapStateToProps = (state, ownProps) => { + const { request } = ownProps + const { metadata: requestMetadata = {} } = request || {} + + const domainMetadata = getDomainMetadata(state) + const targetDomainMetadata = ( + domainMetadata[requestMetadata.origin] || + { name: requestMetadata.origin, icon: null } + ) + + return { + permissionsDescriptions: getPermissionsDescriptions(state), + requestMetadata, + targetDomainMetadata, + } +} + +export default compose( + withRouter, + connect(mapStateToProps) +)(PermissionPageContainer) diff --git a/ui/app/components/app/provider-page-container/index.js b/ui/app/components/app/provider-page-container/index.js deleted file mode 100644 index 927c35940cb1..000000000000 --- a/ui/app/components/app/provider-page-container/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export {default} from './provider-page-container.component' -export {default as ProviderPageContainerContent} from './provider-page-container-content' -export {default as ProviderPageContainerHeader} from './provider-page-container-header' diff --git a/ui/app/components/app/provider-page-container/index.scss b/ui/app/components/app/provider-page-container/index.scss deleted file mode 100644 index 8d35ac179052..000000000000 --- a/ui/app/components/app/provider-page-container/index.scss +++ /dev/null @@ -1,121 +0,0 @@ -.provider-approval-container { - display: flex; - - &__header { - display: flex; - flex-direction: column; - align-items: flex-end; - border-bottom: 1px solid $geyser; - padding: 9px; - } - - &__content { - display: flex; - overflow-y: auto; - flex: 1; - flex-direction: column; - justify-content: space-between; - color: #7C808E; - - h1, h2 { - color: #4A4A4A; - display: flex; - justify-content: center; - text-align: center; - } - - h2 { - font-size: 16px; - line-height: 18px; - padding: 20px; - } - - h1 { - font-size: 22px; - line-height: 26px; - padding: 20px; - } - - p { - padding: 0 40px; - text-align: center; - font-size: 12px; - line-height: 18px; - } - - a, a:hover { - color: $dodger-blue; - } - - .provider-approval-visual { - display: flex; - flex-direction: row; - justify-content: space-evenly; - position: relative; - margin: 0 32px; - - section { - display: flex; - flex-direction: column; - align-items: center; - flex: 1; - } - - h1 { - font-size: 14px; - line-height: 18px; - padding: 8px 0 0; - } - - h2 { - font-size: 10px; - line-height: 14px; - padding: 0; - color: #A2A4AC; - } - - &__check { - width: 40px; - height: 40px; - background: white url("/images/provider-approval-check.svg") no-repeat; - margin-top: 14px; - } - - &__identicon { - width: 64px; - height: 64px; - - &--default { - background-color: #777A87; - color: white; - width: 64px; - height: 64px; - border-radius: 32px; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - } - } - - &:before { - border-top: 2px dashed #CDD1E4; - content: ""; - margin: 0 auto; - position: absolute; - top: 32px; - left: 0; - bottom: 0; - right: 0; - width: 65%; - z-index: -1; - } - } - - .secure-badge { - display: flex; - justify-content: center; - padding: 25px; - } - } -} diff --git a/ui/app/components/app/provider-page-container/provider-page-container-content/index.js b/ui/app/components/app/provider-page-container/provider-page-container-content/index.js deleted file mode 100644 index 73e491adc89f..000000000000 --- a/ui/app/components/app/provider-page-container/provider-page-container-content/index.js +++ /dev/null @@ -1 +0,0 @@ -export {default} from './provider-page-container-content.container' diff --git a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js deleted file mode 100644 index 4f94015b13bc..000000000000 --- a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js +++ /dev/null @@ -1,87 +0,0 @@ -import PropTypes from 'prop-types' -import React, {PureComponent} from 'react' -import Identicon from '../../../ui/identicon' - -export default class ProviderPageContainerContent extends PureComponent { - static propTypes = { - origin: PropTypes.string.isRequired, - selectedIdentity: PropTypes.object.isRequired, - siteImage: PropTypes.string, - siteTitle: PropTypes.string, - hostname: PropTypes.string, - extensionId: PropTypes.string, - } - - static contextTypes = { - t: PropTypes.func, - }; - - renderConnectVisual = (title, identifier) => { - const { selectedIdentity, siteImage } = this.props - - return ( -
-
- {siteImage ? ( - - ) : ( - - {title.charAt(0).toUpperCase()} - - )} -

{title}

-

{identifier}

-
- -
- -

{selectedIdentity.name}

-
-
- ) - } - - render () { - const { siteTitle, hostname, extensionId } = this.props - const { t } = this.context - - const title = extensionId ? - 'External Extension' : - siteTitle || hostname - - const identifier = extensionId ? - `Extension ID: '${extensionId}'` : - hostname - - return ( -
-
-

{t('connectRequest')}

- {this.renderConnectVisual(title, identifier)} -

{t('providerRequest', [title])}

-

- {t('providerRequestInfo')} -
- - {t('learnMore')}. - -

-
-
- -
-
- ) - } -} diff --git a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js deleted file mode 100644 index 4dbdddd161b0..000000000000 --- a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js +++ /dev/null @@ -1,11 +0,0 @@ -import { connect } from 'react-redux' -import ProviderPageContainerContent from './provider-page-container-content.component' -import { getSelectedIdentity } from '../../../../selectors/selectors' - -const mapStateToProps = (state) => { - return { - selectedIdentity: getSelectedIdentity(state), - } -} - -export default connect(mapStateToProps)(ProviderPageContainerContent) diff --git a/ui/app/components/app/provider-page-container/provider-page-container-header/index.js b/ui/app/components/app/provider-page-container/provider-page-container-header/index.js deleted file mode 100644 index 430627d3a3ae..000000000000 --- a/ui/app/components/app/provider-page-container/provider-page-container-header/index.js +++ /dev/null @@ -1 +0,0 @@ -export {default} from './provider-page-container-header.component' diff --git a/ui/app/components/app/provider-page-container/provider-page-container.component.js b/ui/app/components/app/provider-page-container/provider-page-container.component.js deleted file mode 100644 index 7d152e4cbb49..000000000000 --- a/ui/app/components/app/provider-page-container/provider-page-container.component.js +++ /dev/null @@ -1,107 +0,0 @@ -import PropTypes from 'prop-types' -import React, {PureComponent} from 'react' -import { ProviderPageContainerContent, ProviderPageContainerHeader } from '.' -import { PageContainerFooter } from '../../ui/page-container' -import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums' -import { getEnvironmentType } from '../../../../../app/scripts/lib/util' - -export default class ProviderPageContainer extends PureComponent { - static propTypes = { - approveProviderRequestByOrigin: PropTypes.func.isRequired, - rejectProviderRequestByOrigin: PropTypes.func.isRequired, - origin: PropTypes.string.isRequired, - siteImage: PropTypes.string, - siteTitle: PropTypes.string, - hostname: PropTypes.string, - extensionId: PropTypes.string, - }; - - static contextTypes = { - t: PropTypes.func, - metricsEvent: PropTypes.func, - }; - - componentDidMount () { - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { - window.addEventListener('beforeunload', this._beforeUnload) - } - this.context.metricsEvent({ - eventOpts: { - category: 'Auth', - action: 'Connect', - name: 'Popup Opened', - }, - }) - } - - _beforeUnload = () => { - const { origin, rejectProviderRequestByOrigin } = this.props - this.context.metricsEvent({ - eventOpts: { - category: 'Auth', - action: 'Connect', - name: 'Cancel Connect Request Via Notification Close', - }, - }) - this._removeBeforeUnload() - rejectProviderRequestByOrigin(origin) - } - - _removeBeforeUnload () { - window.removeEventListener('beforeunload', this._beforeUnload) - } - - componentWillUnmount () { - this._removeBeforeUnload() - } - - onCancel = () => { - const { origin, rejectProviderRequestByOrigin } = this.props - this.context.metricsEvent({ - eventOpts: { - category: 'Auth', - action: 'Connect', - name: 'Canceled', - }, - }) - this._removeBeforeUnload() - rejectProviderRequestByOrigin(origin) - } - - onSubmit = () => { - const { approveProviderRequestByOrigin, origin } = this.props - this.context.metricsEvent({ - eventOpts: { - category: 'Auth', - action: 'Connect', - name: 'Confirmed', - }, - }) - this._removeBeforeUnload() - approveProviderRequestByOrigin(origin) - } - - render () { - const {origin, siteImage, siteTitle, hostname, extensionId} = this.props - - return ( -
- - - this.onCancel()} - cancelText={this.context.t('cancel')} - onSubmit={() => this.onSubmit()} - submitText={this.context.t('connect')} - submitButtonType="confirm" - /> -
- ) - } -} diff --git a/ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js b/ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js new file mode 100644 index 000000000000..0ea32b5ab53b --- /dev/null +++ b/ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js @@ -0,0 +1,38 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' + +export default class IconWithFallback extends PureComponent { + static propTypes = { + icon: PropTypes.string, + name: PropTypes.string, + } + + static defaultProps = { + name: '', + icon: null, + } + + state = { + iconError: false, + } + + render () { + const { icon, name } = this.props + + return ( +
+
+ { !this.state.iconError && icon + ? this.setState({ iconError: true })} + /> + : + { name.length ? name.charAt(0).toUpperCase() : '' } + + } +
+ ) + } +} diff --git a/ui/app/components/ui/icon-with-fallback/index.js b/ui/app/components/ui/icon-with-fallback/index.js new file mode 100644 index 000000000000..8c1f9a154bfe --- /dev/null +++ b/ui/app/components/ui/icon-with-fallback/index.js @@ -0,0 +1 @@ +export { default } from './icon-with-fallback.component' diff --git a/ui/app/components/ui/icon-with-fallback/index.scss b/ui/app/components/ui/icon-with-fallback/index.scss new file mode 100644 index 000000000000..02ffe371dfde --- /dev/null +++ b/ui/app/components/ui/icon-with-fallback/index.scss @@ -0,0 +1,30 @@ +.icon-with-fallback { + &__identicon-container { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 32px; + width: 32px; + } + + &__identicon-border { + height: 32px; + width: 32px; + border-radius: 50%; + border: 1px solid #F2F3F4; + position: absolute; + background: #FFFFFF; + } + + &__identicon { + width: 24px; + height: 24px; + z-index: 1; + + &--default { + z-index: 1; + color: black; + } + } +} diff --git a/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js index da8da45d1a06..338df83d894a 100644 --- a/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js +++ b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js @@ -14,6 +14,7 @@ export default class PageContainerFooter extends Component { disabled: PropTypes.bool, submitButtonType: PropTypes.string, hideCancel: PropTypes.bool, + buttonSizeLarge: PropTypes.bool, } static contextTypes = { @@ -31,6 +32,7 @@ export default class PageContainerFooter extends Component { submitButtonType, hideCancel, cancelButtonType, + buttonSizeLarge = false, } = this.props return ( @@ -40,7 +42,7 @@ export default class PageContainerFooter extends Component { {!hideCancel && ( -
-
- - ) - } - - renderApprovedOriginsList () { - const { t } = this.context - const { approvedOrigins, rejectProviderRequestByOrigin, showClearApprovalModal } = this.props - const approvedEntries = Object.entries(approvedOrigins) - const approvalListEmpty = approvedEntries.length === 0 - - return ( -
-
- { t('connected') } - - { t('connectedDescription') } - -
-
- { - approvalListEmpty - ?
- : null - } - { - approvedEntries.map(([origin, { siteTitle, siteImage }]) => ( - { - rejectProviderRequestByOrigin(origin) - }} - /> - )) - } -
-
- -
-
- ) - } - - render () { - return ( -
- { this.renderNewOriginInput() } - { this.renderApprovedOriginsList() } -
- ) - } -} diff --git a/ui/app/pages/settings/connections-tab/connections-tab.container.js b/ui/app/pages/settings/connections-tab/connections-tab.container.js deleted file mode 100644 index cf3efc2b46cb..000000000000 --- a/ui/app/pages/settings/connections-tab/connections-tab.container.js +++ /dev/null @@ -1,39 +0,0 @@ -import ConnectionsTab from './connections-tab.component' -import { compose } from 'recompose' -import { connect } from 'react-redux' -import { withRouter } from 'react-router-dom' -import { - approveProviderRequestByOrigin, - rejectProviderRequestByOrigin, - showModal, -} from '../../../store/actions' - -export const mapStateToProps = state => { - const { - activeTab, - metamask, - } = state - const { - approvedOrigins, - } = metamask - - return { - activeTab, - approvedOrigins, - } -} - -export const mapDispatchToProps = dispatch => { - return { - approveProviderRequestByOrigin: (origin) => dispatch(approveProviderRequestByOrigin(origin)), - rejectProviderRequestByOrigin: (origin) => dispatch(rejectProviderRequestByOrigin(origin)), - showClearApprovalModal: () => dispatch(showModal({ - name: 'CLEAR_APPROVED_ORIGINS', - })), - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(ConnectionsTab) diff --git a/ui/app/pages/settings/connections-tab/index.js b/ui/app/pages/settings/connections-tab/index.js deleted file mode 100644 index b04f4e33a67a..000000000000 --- a/ui/app/pages/settings/connections-tab/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './connections-tab.container' diff --git a/ui/app/pages/settings/connections-tab/index.scss b/ui/app/pages/settings/connections-tab/index.scss deleted file mode 100644 index 249a7193f731..000000000000 --- a/ui/app/pages/settings/connections-tab/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './connected-site-row/index'; diff --git a/ui/app/pages/settings/index.scss b/ui/app/pages/settings/index.scss index 19545eb51363..780b930e742f 100644 --- a/ui/app/pages/settings/index.scss +++ b/ui/app/pages/settings/index.scss @@ -4,8 +4,6 @@ @import 'settings-tab/index'; -@import 'connections-tab/index'; - @import 'contact-list-tab/index'; .settings-page { diff --git a/ui/app/pages/settings/settings.component.js b/ui/app/pages/settings/settings.component.js index b0ee55dc7f1c..12d395f31501 100644 --- a/ui/app/pages/settings/settings.component.js +++ b/ui/app/pages/settings/settings.component.js @@ -4,7 +4,6 @@ import { Switch, Route, matchPath, withRouter } from 'react-router-dom' import TabBar from '../../components/app/tab-bar' import c from 'classnames' import SettingsTab from './settings-tab' -import ConnectionsTab from './connections-tab' import NetworksTab from './networks-tab' import AdvancedTab from './advanced-tab' import InfoTab from './info-tab' @@ -15,7 +14,6 @@ import { ADVANCED_ROUTE, SECURITY_ROUTE, GENERAL_ROUTE, - CONNECTIONS_ROUTE, ABOUT_US_ROUTE, SETTINGS_ROUTE, NETWORKS_ROUTE, @@ -160,7 +158,6 @@ class SettingsPage extends PureComponent { - { + const { address, name, balance } = account + return { + address, + truncatedAddress: `${address.slice(0, 6)}...${address.slice(-4)}`, + addressLabel: `${name} (...${address.slice(address.length - 4)})`, + label: name, + balance, + } + }) + return accountsWithLabels +} + function getCurrentAccountWithSendEtherInfo (state) { const currentAddress = getSelectedAddress(state) const accounts = accountsWithSendEtherInfoSelector(state) @@ -353,6 +385,22 @@ function getCustomNonceValue (state) { return String(state.metamask.customNonceValue) } +function getPermissionsDescriptions (state) { + return state.metamask.permissionsDescriptions +} + +function getPermissionsRequests (state) { + return state.metamask.permissionsRequests +} + +function getDomainMetadata (state) { + return state.metamask.domainMetadata +} + +function getActiveTab (state) { + return state.activeTab +} + function getMetaMetricState (state) { return { network: getCurrentNetworkId(state), @@ -386,3 +434,140 @@ function getKnownMethodData (state, data) { function getFeatureFlags (state) { return state.metamask.featureFlags } + +function getFirstPermissionRequest (state) { + const requests = getPermissionsRequests(state) + return requests && requests[0] ? requests[0] : null +} + +function hasPermissionRequests (state) { + return Boolean(getFirstPermissionRequest(state)) +} + +function getPermissionsDomains (state) { + return state.metamask.domains +} + +function getAddressConnectedDomainMap (state) { + const { + domains, + domainMetadata, + } = state.metamask + + const addressConnectedIconMap = {} + + if (domains) { + Object.keys(domains).forEach(domainKey => { + const { permissions } = domains[domainKey] + const { icon, name } = domainMetadata[domainKey] + permissions.forEach(perm => { + const caveats = perm.caveats || [] + const exposedAccountCaveat = caveats.find(caveat => caveat.name === 'exposedAccounts') + if (exposedAccountCaveat && exposedAccountCaveat.value && exposedAccountCaveat.value.length) { + exposedAccountCaveat.value.forEach(address => { + addressConnectedIconMap[address] = addressConnectedIconMap[address] + ? { ...addressConnectedIconMap[address], [domainKey]: { icon, name } } + : { [domainKey]: { icon, name } } + }) + } + }) + }) + } + + return addressConnectedIconMap +} + +function getDomainToConnectedAddressMap (state) { + const { domains = {} } = state.metamask + + const domainToConnectedAddressMap = mapObjectValues(domains, (_, { permissions }) => { + const ethAccountsPermissions = permissions.filter(permission => permission.parentCapability === 'eth_accounts') + const ethAccountsPermissionsExposedAccountAddresses = ethAccountsPermissions.map(permission => { + const caveats = permission.caveats + const exposedAccountsCaveats = caveats.filter(caveat => caveat.name === 'exposedAccounts') + const exposedAccountsAddresses = exposedAccountsCaveats.map(caveat => caveat.value[0]) + return exposedAccountsAddresses + }) + const allAddressesConnectedToDomain = ethAccountsPermissionsExposedAccountAddresses.reduce((acc, arrayOfAddresses) => { + return [ ...acc, ...arrayOfAddresses ] + }, []) + return allAddressesConnectedToDomain + }) + + return domainToConnectedAddressMap +} + +function getAddressConnectedToCurrentTab (state) { + const domainToConnectedAddressMap = getDomainToConnectedAddressMap(state) + const originOfCurrentTab = getOriginOfCurrentTab(state) + const addressesConnectedToCurrentTab = domainToConnectedAddressMap[originOfCurrentTab] + const addressConnectedToCurrentTab = addressesConnectedToCurrentTab && addressesConnectedToCurrentTab[0] + return addressConnectedToCurrentTab +} + +function getRenderablePermissionsDomains (state) { + const { + domains = {}, + domainMetadata, + permissionsHistory, + permissionsDescriptions, + selectedAddress, + } = state.metamask + + const renderableDomains = Object.keys(domains).reduce((acc, domainKey) => { + const { permissions } = domains[domainKey] + const permissionsWithCaveatsForSelectedAddress = permissions.filter(perm => { + const caveats = perm.caveats || [] + const exposedAccountCaveat = caveats.find(caveat => caveat.name === 'exposedAccounts') + const exposedAccountCaveatValue = exposedAccountCaveat && exposedAccountCaveat.value && exposedAccountCaveat.value.length + ? exposedAccountCaveat.value[0] + : {} + return exposedAccountCaveatValue === selectedAddress + }) + + if (permissionsWithCaveatsForSelectedAddress.length) { + const permissionKeys = permissions.map(permission => permission.parentCapability) + const { + name, + icon, + extensionId, + } = domainMetadata[domainKey] + const permissionsHistoryForDomain = permissionsHistory[domainKey] || {} + const ethAccountsPermissionsForDomain = permissionsHistoryForDomain['eth_accounts'] || {} + const accountsLastConnectedTime = ethAccountsPermissionsForDomain.accounts || {} + const selectedAddressLastConnectedTime = accountsLastConnectedTime[selectedAddress] + + const lastConnectedTime = formatDate(selectedAddressLastConnectedTime, 'yyyy-M-d') + + return [ ...acc, { + name, + icon, + key: domainKey, + lastConnectedTime, + permissionDescriptions: permissionKeys.map(permissionKey => permissionsDescriptions[permissionKey]), + extensionId, + }] + } else { + return acc + } + }, []) + + return renderableDomains +} + +function getOriginOfCurrentTab (state) { + const { appState: { currentActiveTab = {} } } = state + return currentActiveTab.url && getOriginFromUrl(currentActiveTab.url) +} + +function getLastConnectedInfo (state) { + const { permissionsHistory = {} } = state.metamask + const lastConnectedInfoData = Object.keys(permissionsHistory).reduce((acc, origin) => { + const ethAccountsHistory = JSON.parse(JSON.stringify(permissionsHistory[origin]['eth_accounts'])) + return { + ...acc, + [origin]: ethAccountsHistory.accounts, + } + }, {}) + return lastConnectedInfoData +} diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index d6c29fcd6759..743a77197e35 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -328,7 +328,6 @@ const actions = { setUseNativeCurrencyAsPrimaryCurrencyPreference, setShowFiatConversionOnTestnetsPreference, setAutoLogoutTimeLimit, - unsetMigratedPrivacyMode, // Onboarding setCompletedOnboarding, @@ -352,9 +351,11 @@ const actions = { createSpeedUpTransaction, createRetryTransaction, - approveProviderRequestByOrigin, - rejectProviderRequestByOrigin, - clearApprovedOrigins, + // Permissions + approvePermissionsRequest, + clearPermissions, + rejectPermissionsRequest, + removePermissionsFor, setFirstTimeFlowType, SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE', @@ -395,6 +396,14 @@ const actions = { turnThreeBoxSyncingOnAndInitialize, tryReverseResolveAddress, + + getRequestAccountTabIds, + getCurrentWindowTab, + SET_REQUEST_ACCOUNT_TABS: 'SET_REQUEST_ACCOUNT_TABS', + SET_CURRENT_WINDOW_TAB: 'SET_CURRENT_WINDOW_TAB', + setActiveTab, + SET_ACTIVE_TAB: 'SET_ACTIVE_TAB', + getActiveTab, } module.exports = actions @@ -2709,24 +2718,49 @@ function setPendingTokens (pendingTokens) { } } -function approveProviderRequestByOrigin (origin) { +// Permissions + +/** + * Approves the permission requests with the given IDs. + * @param {string} requestId - The id of the permissions request. + * @param {string[]} accounts - The accounts to expose, if any. + */ +function approvePermissionsRequest (requestId, accounts) { return () => { - background.approveProviderRequestByOrigin(origin) + background.approvePermissionsRequest(requestId, accounts) } } -function rejectProviderRequestByOrigin (origin) { +/** + * Rejects the permission requests with the given IDs. + * @param {Array} requestId + */ +function rejectPermissionsRequest (requestId) { return () => { - background.rejectProviderRequestByOrigin(origin) + background.rejectPermissionsRequest(requestId) } } -function clearApprovedOrigins () { +/** + * Clears the given permissions for the given origin. + */ +function removePermissionsFor (domains) { return () => { - background.clearApprovedOrigins() + background.removePermissionsFor(domains) } } +/** + * Clears all permissions for all domains. + */ +function clearPermissions () { + return () => { + background.clearPermissions() + } +} + +// //// + function setFirstTimeFlowType (type) { return (dispatch) => { log.debug(`background.setFirstTimeFlowType`) @@ -2847,12 +2881,6 @@ function getTokenParams (tokenAddress) { } } -function unsetMigratedPrivacyMode () { - return () => { - background.unsetMigratedPrivacyMode() - } -} - function setSeedPhraseBackedUp (seedPhraseBackupState) { return (dispatch) => { log.debug(`background.setSeedPhraseBackedUp`) @@ -2990,3 +3018,50 @@ function getNextNonce () { }) } } + +function setRequestAccountTabIds (requestAccountTabIds) { + return { + type: actions.SET_REQUEST_ACCOUNT_TABS, + value: requestAccountTabIds, + } +} + +function getRequestAccountTabIds () { + return async (dispatch) => { + const requestAccountTabIds = await pify(background.getRequestAccountTabIds).call(background) + dispatch(setRequestAccountTabIds(requestAccountTabIds)) + } +} + +function setCurrentWindowTab (currentWindowTab) { + return { + type: actions.SET_CURRENT_WINDOW_TAB, + value: currentWindowTab, + } +} + + +function getCurrentWindowTab () { + return async (dispatch) => { + const currentWindowTab = await global.platform.currentTab() + dispatch(setCurrentWindowTab(currentWindowTab)) + } +} + +function setActiveTab (activeTab) { + return { + type: actions.SET_ACTIVE_TAB, + value: activeTab, + } +} + +function getActiveTab () { + return (dispatch) => { + return global.platform.queryTabs() + .then(tabs => { + const activeTab = tabs.find(tab => tab.active) + dispatch(setActiveTab(activeTab)) + return activeTab + }) + } +} diff --git a/yarn.lock b/yarn.lock index c0f0461b7707..86748862727c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18218,9 +18218,10 @@ mersenne-twister@^1.0.1: resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a" integrity sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o= -metamask-inpage-provider@MetaMask/metamask-inpage-provider#LoginPerSite: - version "4.0.0" - resolved "https://codeload.github.com/MetaMask/metamask-inpage-provider/tar.gz/e9206fd688f2fbd821b312bf0517e1fb2b423225" +metamask-inpage-provider@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/metamask-inpage-provider/-/metamask-inpage-provider-4.0.2.tgz#50d9e46b5fdd6610ce185a165004e3c6b762dbb3" + integrity sha512-GXoMa7rP+fx9CriCA+RPHjvJJpfy9531eRdMvbDKv0q95/1pvtzYkj6BdzjxtbM91n4zYl6tmeKDILu+le9Qog== dependencies: eth-json-rpc-errors "^2.0.0" fast-deep-equal "^2.0.1" From 82548b52c3cb83b0a34c9deb293eb44264f2ee3d Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 26 Nov 2019 23:32:43 -0800 Subject: [PATCH 03/21] fixup! Login Per Site UI (#7368) --- app/_locales/en/messages.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index e4dfd083866c..b31d4f03057d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1307,6 +1307,9 @@ "storePhrase": { "message": "Store this phrase in a password manager like 1Password." }, + "submit": { + "message": "Submit" + }, "submitted": { "message": "Submitted" }, From bc15015683b6e416cb65459604a9887663094b46 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 28 Nov 2019 10:19:59 -0330 Subject: [PATCH 04/21] Some code cleanup for LoginPerSite --- app/scripts/controllers/permissions/index.js | 4 +-- app/scripts/controllers/transactions/index.js | 26 ++++++++++--------- app/scripts/metamask-controller.js | 2 +- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index c1b39d76aca2..ec4932bf4a84 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -186,8 +186,8 @@ class PermissionsController { let error try { - error = await new Promise((resolve) => { - this.permissions.grantNewPermissions(origin, permissions, {}, err => resolve(err)) + await new Promise((resolve, reject) => { + this.permissions.grantNewPermissions(origin, permissions, {}, err => err ? resolve() : reject(err)) }) } catch (err) { error = err diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 75707116281b..85733aa38247 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -190,6 +190,18 @@ class TransactionController extends EventEmitter { // validate const normalizedTxParams = txUtils.normalizeTxParams(txParams) + txUtils.validateTxParams(normalizedTxParams) + /** + `generateTxMeta` adds the default txMeta properties to the passed object. + These include the tx's `id`. As we use the id for determining order of + txes in the tx-state-manager, it is necessary to call the asynchronous + method `this._determineTransactionCategory` after `generateTxMeta`. + */ + let txMeta = this.txStateManager.generateTxMeta({ + txParams: normalizedTxParams, + type: TRANSACTION_TYPE_STANDARD, + }) + if (origin === 'metamask') { // Assert the from address is the selected address if (normalizedTxParams.from !== this.getSelectedAddress()) { @@ -211,18 +223,8 @@ class TransactionController extends EventEmitter { } } - txUtils.validateTxParams(normalizedTxParams) - /** - `generateTxMeta` adds the default txMeta properties to the passed object. - These include the tx's `id`. As we use the id for determining order of - txes in the tx-state-manager, it is necessary to call the asynchronous - method `this._determineTransactionCategory` after `generateTxMeta`. - */ - let txMeta = this.txStateManager.generateTxMeta({ - txParams: normalizedTxParams, - type: TRANSACTION_TYPE_STANDARD, - origin, - }) + txMeta['origin'] = origin + const { transactionCategory, getCodeResponse } = await this._determineTransactionCategory(txParams) txMeta.transactionCategory = transactionCategory this.addTx(txMeta) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 4fc8cf75b053..b9bd2aa1673b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1499,7 +1499,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {*} outStream - The stream to provide public config over. */ setupPublicConfig (outStream) { - const configStore = this.createPublicConfigStore({}) + const configStore = this.createPublicConfigStore() const configStream = asStream(configStore) pump( From ebab2529d41716277c8c1d1a9f9e7bca09f8dea5 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 28 Nov 2019 11:31:00 -0330 Subject: [PATCH 05/21] Adds UX for connecting to dapps via the connected sites screen (#7593) * Adds UX for connecting to dapps via the connected sites screen * Use openMetaMaskTabIds from background.js to determine if current active tab is MetaMask --- app/_locales/en/messages.json | 4 ++ app/scripts/background.js | 3 ++ app/scripts/metamask-controller.js | 2 + .../connected-sites-list.component.js | 41 ++++++++++++++++--- .../connected-sites-list.container.js | 24 +++++++++++ .../app/connected-sites-list/index.scss | 10 +++++ ui/app/ducks/app/app.js | 6 +++ ui/app/store/actions.js | 27 ++++++++++++ 8 files changed, 112 insertions(+), 5 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b31d4f03057d..0a5223d929b3 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -26,6 +26,10 @@ "connectingWithMetaMask": { "message": "Connecting With MetaMask..." }, + "connectTo": { + "message": "Connect to $1", + "description": "$1 is the name/origin of a site/dapp that the user can connect to metamask" + }, "chooseAnAcount": { "message": "Choose an account" }, diff --git a/app/scripts/background.js b/app/scripts/background.js index eb75122de744..e5c9ea66516b 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -251,6 +251,9 @@ function setupController (initState, initLangCode) { getRequestAccountTabIds: () => { return requestAccountTabIds }, + getOpenMetamaskTabsIds: () => { + return openMetamaskTabsIDs + }, }) const provider = controller.provider diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b9bd2aa1673b..08718a6e0df4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -90,6 +90,7 @@ module.exports = class MetamaskController extends EventEmitter { this.platform = opts.platform this.getRequestAccountTabIds = opts.getRequestAccountTabIds + this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds // observable state store this.store = new ComposableObservableStore(initState) @@ -563,6 +564,7 @@ module.exports = class MetamaskController extends EventEmitter { legacyExposeAccounts: nodeify(permissionsController.legacyExposeAccounts, permissionsController), getRequestAccountTabIds: (cb) => cb(null, this.getRequestAccountTabIds()), + getOpenMetamaskTabsIds: (cb) => cb(null, this.getOpenMetamaskTabsIds()), } } diff --git a/ui/app/components/app/connected-sites-list/connected-sites-list.component.js b/ui/app/components/app/connected-sites-list/connected-sites-list.component.js index bf8ba885301b..1bde4855f7ba 100644 --- a/ui/app/components/app/connected-sites-list/connected-sites-list.component.js +++ b/ui/app/components/app/connected-sites-list/connected-sites-list.component.js @@ -20,12 +20,22 @@ export default class ConnectedSitesList extends Component { domains: PropTypes.object, showDisconnectAccountModal: PropTypes.func.isRequired, showDisconnectAllModal: PropTypes.func.isRequired, + tabToConnect: PropTypes.object, + legacyExposeAccounts: PropTypes.func.isRequired, + selectedAddress: PropTypes.string.isRequired, + getOpenMetamaskTabsIds: PropTypes.func.isRequired, } state = { expandedDomain: '', iconError: '', domains: {}, + tabToConnect: null, + } + + componentWillMount () { + const { getOpenMetamaskTabsIds } = this.props + getOpenMetamaskTabsIds() } handleDomainItemClick (domainKey) { @@ -37,7 +47,15 @@ export default class ConnectedSitesList extends Component { } render () { - const { renderableDomains, domains, showDisconnectAccountModal, showDisconnectAllModal } = this.props + const { + renderableDomains, + domains, + showDisconnectAccountModal, + showDisconnectAllModal, + tabToConnect, + legacyExposeAccounts, + selectedAddress, + } = this.props const { expandedDomain } = this.state const { t } = this.context @@ -110,10 +128,23 @@ export default class ConnectedSitesList extends Component { ) }) } -
- +
+
+ +
+ { tabToConnect + ?
+ +
+ : null + }
) diff --git a/ui/app/components/app/connected-sites-list/connected-sites-list.container.js b/ui/app/components/app/connected-sites-list/connected-sites-list.container.js index a7b28215b364..253a1f2701e4 100644 --- a/ui/app/components/app/connected-sites-list/connected-sites-list.container.js +++ b/ui/app/components/app/connected-sites-list/connected-sites-list.container.js @@ -4,16 +4,36 @@ import { compose } from 'recompose' import ConnectedSitesList from './connected-sites-list.component' import { showModal, + legacyExposeAccounts, + getOpenMetamaskTabsIds, } from '../../../store/actions' import { getRenderablePermissionsDomains, getPermissionsDomains, + getAddressConnectedToCurrentTab, + getSelectedAddress, } from '../../../selectors/selectors' +import { getOriginFromUrl } from '../../../helpers/utils/util' const mapStateToProps = state => { + const addressConnectedToCurrentTab = getAddressConnectedToCurrentTab(state) + const { openMetaMaskTabs, currentActiveTab = {} } = state.appState + const { title, url, id } = currentActiveTab + + let tabToConnect + + if (!addressConnectedToCurrentTab && url && !openMetaMaskTabs[id]) { + tabToConnect = { + title, + origin: getOriginFromUrl(url), + } + } + return { domains: getPermissionsDomains(state), renderableDomains: getRenderablePermissionsDomains(state), + tabToConnect, + selectedAddress: getSelectedAddress(state), } } @@ -25,6 +45,10 @@ const mapDispatchToProps = dispatch => { showDisconnectAllModal: () => { dispatch(showModal({ name: 'DISCONNECT_ALL' })) }, + legacyExposeAccounts: (origin, account) => { + dispatch(legacyExposeAccounts(origin, [account])) + }, + getOpenMetamaskTabsIds: () => dispatch(getOpenMetamaskTabsIds()), } } diff --git a/ui/app/components/app/connected-sites-list/index.scss b/ui/app/components/app/connected-sites-list/index.scss index ccf7f71e0847..c2d98d2aa0c1 100644 --- a/ui/app/components/app/connected-sites-list/index.scss +++ b/ui/app/components/app/connected-sites-list/index.scss @@ -108,8 +108,18 @@ cursor: pointer; } + &__bottom-buttons { + display: flex; + align-items: center; + } + &__disconnect-all { padding: 10px; width: 50%; } + + &__connect-to { + padding: 10px; + width: 50%; + } } \ No newline at end of file diff --git a/ui/app/ducks/app/app.js b/ui/app/ducks/app/app.js index d176b2b2c7d1..34a2b93d5939 100644 --- a/ui/app/ducks/app/app.js +++ b/ui/app/ducks/app/app.js @@ -76,6 +76,7 @@ export default function reduceApp (state, action) { show3BoxModalAfterImport: false, threeBoxLastUpdated: null, requestAccountTabs: {}, + openMetaMaskTabs: {}, currentWindowTab: {}, currentActiveTab: {}, }, state.appState) @@ -771,6 +772,11 @@ export default function reduceApp (state, action) { requestAccountTabs: action.value, }) + case actions.SET_OPEN_METAMASK_TAB_IDS: + return extend(appState, { + openMetaMaskTabs: action.value, + }) + case actions.SET_CURRENT_WINDOW_TAB: return extend(appState, { currentWindowTab: action.value, diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 743a77197e35..a0e06ef9d426 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -356,6 +356,7 @@ const actions = { clearPermissions, rejectPermissionsRequest, removePermissionsFor, + legacyExposeAccounts, setFirstTimeFlowType, SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE', @@ -404,6 +405,8 @@ const actions = { setActiveTab, SET_ACTIVE_TAB: 'SET_ACTIVE_TAB', getActiveTab, + getOpenMetamaskTabsIds, + SET_OPEN_METAMASK_TAB_IDS: 'SET_OPEN_METAMASK_TAB_IDS', } module.exports = actions @@ -2741,6 +2744,16 @@ function rejectPermissionsRequest (requestId) { } } +/** + * Exposes the given account(s) to the given origin. + * Call ONLY as a result of direct user action. + */ +function legacyExposeAccounts (origin, accounts) { + return () => { + return background.legacyExposeAccounts(origin, accounts) + } +} + /** * Clears the given permissions for the given origin. */ @@ -3033,6 +3046,20 @@ function getRequestAccountTabIds () { } } +function setOpenMetamaskTabsIDs (openMetaMaskTabIDs) { + return { + type: actions.SET_OPEN_METAMASK_TAB_IDS, + value: openMetaMaskTabIDs, + } +} + +function getOpenMetamaskTabsIds () { + return async (dispatch) => { + const openMetaMaskTabIDs = await pify(background.getOpenMetamaskTabsIds).call(background) + dispatch(setOpenMetamaskTabsIDs(openMetaMaskTabIDs)) + } +} + function setCurrentWindowTab (currentWindowTab) { return { type: actions.SET_CURRENT_WINDOW_TAB, From 306630fd1c4a4bc910d29482ca1c8f7c0c0099bf Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 28 Nov 2019 11:34:49 -0330 Subject: [PATCH 06/21] Delete unused permissions controller methods --- app/scripts/controllers/permissions/index.js | 41 -------------------- app/scripts/metamask-controller.js | 4 -- 2 files changed, 45 deletions(-) diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index ec4932bf4a84..899044b13e37 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -309,29 +309,6 @@ class PermissionsController { }) } - /** - * Gets all caveats for the given origin and permission, or returns null - * if none exist. - * - * @param {string} permission - The name of the target permission. - * @param {string} origin - The origin that has the permission. - */ - getCaveatsFor (permission, origin) { - return this.permissions.getCaveats(origin, permission) || null - } - - /** - * Gets the caveat with the given name for the given permission of the - * given origin. - * - * @param {string} permission - The name of the target permission. - * @param {string} origin - The origin that has the permission. - * @param {string} caveatName - The name of the caveat to retrieve. - */ - getCaveat (permission, origin, caveatName) { - return this.permissions.getCaveat(origin, permission, caveatName) - } - /** * Removes all known domains and their related permissions. */ @@ -343,24 +320,6 @@ class PermissionsController { }) } - /** - * Clears the permissions log. - */ - clearLog () { - this.store.updateState({ - [LOG_STORE_KEY]: [], - }) - } - - /** - * Clears the permissions history. - */ - clearHistory () { - this.store.updateState({ - [HISTORY_STORE_KEY]: {}, - }) - } - /** * A convenience method for retrieving a login object * or creating a new one if needed. diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 08718a6e0df4..246ae511d36d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -553,13 +553,9 @@ module.exports = class MetamaskController extends EventEmitter { // permissions approvePermissionsRequest: nodeify(permissionsController.approvePermissionsRequest, permissionsController), clearPermissions: permissionsController.clearPermissions.bind(permissionsController), - clearPermissionsHistory: permissionsController.clearHistory.bind(permissionsController), - clearPermissionsLog: permissionsController.clearLog.bind(permissionsController), getApprovedAccounts: nodeify(permissionsController.getAccounts.bind(permissionsController)), rejectPermissionsRequest: nodeify(permissionsController.rejectPermissionsRequest, permissionsController), removePermissionsFor: permissionsController.removePermissionsFor.bind(permissionsController), - getCaveatsFor: permissionsController.getCaveatsFor.bind(permissionsController), - getCaveat: permissionsController.getCaveat.bind(permissionsController), updateExposedAccounts: nodeify(permissionsController.updateExposedAccounts, permissionsController), legacyExposeAccounts: nodeify(permissionsController.legacyExposeAccounts, permissionsController), From f39e6b07c227d8dcba9dbd46b4a6da0fd9371b53 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 28 Nov 2019 12:51:52 -0330 Subject: [PATCH 07/21] Fixes two small bugs in the LoginPerSite ui (#7595) --- .../modals/disconnect-account/disconnect-account.container.js | 3 ++- ui/app/selectors/selectors.js | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/app/components/app/modals/disconnect-account/disconnect-account.container.js b/ui/app/components/app/modals/disconnect-account/disconnect-account.container.js index 628b3ca0f141..2eb1bb92a11c 100644 --- a/ui/app/components/app/modals/disconnect-account/disconnect-account.container.js +++ b/ui/app/components/app/modals/disconnect-account/disconnect-account.container.js @@ -19,7 +19,7 @@ const mapDispatchToProps = dispatch => { } } -const mergeProps = (stateProps, dispatchProps) => { +const mergeProps = (stateProps, dispatchProps, ownProps) => { const { domainKey, domain, @@ -29,6 +29,7 @@ const mergeProps = (stateProps, dispatchProps) => { } = dispatchProps return { + ...ownProps, ...stateProps, ...dispatchProps, disconnectAccount: () => dispatchDisconnectAccount(domainKey, domain), diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 83a0fd32c862..206d27a62cb4 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -537,7 +537,9 @@ function getRenderablePermissionsDomains (state) { const accountsLastConnectedTime = ethAccountsPermissionsForDomain.accounts || {} const selectedAddressLastConnectedTime = accountsLastConnectedTime[selectedAddress] - const lastConnectedTime = formatDate(selectedAddressLastConnectedTime, 'yyyy-M-d') + const lastConnectedTime = selectedAddressLastConnectedTime + ? formatDate(selectedAddressLastConnectedTime, 'yyyy-M-d') + : '' return [ ...acc, { name, From dadc90c9537e0972726337d7febcfd63f0ff9e87 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 28 Nov 2019 16:01:48 -0400 Subject: [PATCH 08/21] Restore `providerRequest` message translations (#7600) This message was removed, but it was replaced with a very similar message called `likeToConnect`. The only difference is that the new message has "MetaMask" in it. Preserving these messages without "MetaMask" is probably better than deleting them, so these messages have all been restored and renamed to `likeToConnect`. --- app/_locales/am/messages.json | 3 +++ app/_locales/ar/messages.json | 3 +++ app/_locales/bg/messages.json | 3 +++ app/_locales/bn/messages.json | 3 +++ app/_locales/ca/messages.json | 3 +++ app/_locales/da/messages.json | 3 +++ app/_locales/de/messages.json | 3 +++ app/_locales/el/messages.json | 3 +++ app/_locales/es/messages.json | 3 +++ app/_locales/es_419/messages.json | 3 +++ app/_locales/et/messages.json | 3 +++ app/_locales/fa/messages.json | 3 +++ app/_locales/fi/messages.json | 3 +++ app/_locales/fil/messages.json | 3 +++ app/_locales/fr/messages.json | 3 +++ app/_locales/he/messages.json | 3 +++ app/_locales/hi/messages.json | 3 +++ app/_locales/hr/messages.json | 3 +++ app/_locales/hu/messages.json | 3 +++ app/_locales/id/messages.json | 3 +++ app/_locales/it/messages.json | 3 +++ app/_locales/ja/messages.json | 3 +++ app/_locales/kn/messages.json | 3 +++ app/_locales/ko/messages.json | 3 +++ app/_locales/lt/messages.json | 3 +++ app/_locales/lv/messages.json | 3 +++ app/_locales/ms/messages.json | 3 +++ app/_locales/no/messages.json | 3 +++ app/_locales/pl/messages.json | 3 +++ app/_locales/pt_BR/messages.json | 3 +++ app/_locales/ro/messages.json | 3 +++ app/_locales/ru/messages.json | 3 +++ app/_locales/sk/messages.json | 3 +++ app/_locales/sl/messages.json | 3 +++ app/_locales/sr/messages.json | 3 +++ app/_locales/sv/messages.json | 3 +++ app/_locales/sw/messages.json | 3 +++ app/_locales/th/messages.json | 3 +++ app/_locales/uk/messages.json | 3 +++ app/_locales/zh_CN/messages.json | 3 +++ app/_locales/zh_TW/messages.json | 3 +++ 41 files changed, 123 insertions(+) diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index f6729c375a3f..1b9026d4c858 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "አይቀበሉ" }, + "likeToConnect": { + "message": "$1ከመለያዎ ጋር ለመገናኘት ይፈልጋል" + }, "about": { "message": "ስለ" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index e2af1695c211..e686d498cf52 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "رفض" }, + "likeToConnect": { + "message": "يرغب $1 في الاتصال بحسابك" + }, "about": { "message": "حول" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 4cfd909bf844..249e1c77913a 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Отхвърляне" }, + "likeToConnect": { + "message": "$1 би искал да се свърже с вашия акаунт" + }, "about": { "message": "Информация" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 114998d96c7d..ae36440daca3 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "প্রত্যাখ্যান" }, + "likeToConnect": { + "message": "$1 আপনার অ্যাকাউন্টের সাথে সংযোগ করতে চায়" + }, "about": { "message": "সম্পর্কে" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index 33864912974d..2ad32cb66b6f 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Rebutja" }, + "likeToConnect": { + "message": "a $1 li agradaria connectar-se al teu compte" + }, "about": { "message": "Informació" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 5a4d366505f4..1c5fa4edb03c 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Afvis" }, + "likeToConnect": { + "message": "$1 ønsker at forbinde til din konto" + }, "about": { "message": "Om" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index f3d1e959eba8..f964d0c2d190 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Ablehnen" }, + "likeToConnect": { + "message": "$1 möchte sich mit deinem Account verbinden" + }, "about": { "message": "Über" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index b7e75e1f4d9f..6f35b1447495 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Απόρριψη" }, + "likeToConnect": { + "message": "Αίτημα σύνδεσης στον λογαριασμό σας από $1" + }, "about": { "message": "Σχετικά με" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 734eaf04dfad..f64105f26361 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Rechazar" }, + "likeToConnect": { + "message": "$1 quisiera conectar con tu cuenta" + }, "about": { "message": "Acerca" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 5571166cc59d..0645b0eafb6c 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Rechazar" }, + "likeToConnect": { + "message": "$1 desea conectarse a tu cuenta" + }, "about": { "message": "Acerca de" }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index 38ec052ccbce..e361e1e239f2 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Lükka tagasi" }, + "likeToConnect": { + "message": "$1 soovib teie kontoga ühenduse luua" + }, "about": { "message": "Teave" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index 11c1fe76a9d0..5be054e7cace 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "عدم پذیرش" }, + "likeToConnect": { + "message": "1$1 میخواهید تا با حساب تان وصل شوید" + }, "about": { "message": "درباره" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index f0bfca3e7487..56f48a6fda8a 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Hylkää" }, + "likeToConnect": { + "message": "$1 haluaisi yhdistää tiliisi" + }, "about": { "message": "Tietoja asetuksista" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index a5da4bbf100c..cd9b31234354 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Tanggihan" }, + "likeToConnect": { + "message": "Gusto ng $1 na kumonekta sa iyong account" + }, "about": { "message": "Tungkol sa" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index f4ae544546e2..1fc2a600d984 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Rejeter" }, + "likeToConnect": { + "message": "$1 voudrait se connecter à votre compte" + }, "about": { "message": "À propos" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 0b5c0a075e32..d0f92848c93f 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "דחה" }, + "likeToConnect": { + "message": "$1 מבקש להתחבר לחשבון שלך" + }, "about": { "message": "מידע כללי" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 26b66ab1f665..51aa21b84003 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "अस्‍वीकार करें" }, + "likeToConnect": { + "message": "$1 आपके खाते से कनेक्ट होता चाहता हैं" + }, "about": { "message": "इसके बारे में" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index ad4dd30f8b90..57b4b0d4c2e4 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -12,6 +12,9 @@ "reject": { "message": "Odbaci" }, + "likeToConnect": { + "message": "Korisnik $1 želi se povezati na vaš račun" + }, "about": { "message": "O opcijama" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index 547141724fb7..33b8dac325fd 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -12,6 +12,9 @@ "reject": { "message": "Elutasítás" }, + "likeToConnect": { + "message": "$1 szeretne kapcsolódni az ön fiókjához" + }, "about": { "message": "Névjegy" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 4e2216f2a1fa..874ca45761e0 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -12,6 +12,9 @@ "reject": { "message": "Tolak" }, + "likeToConnect": { + "message": "$1 ingin menghubungkan ke akun Anda" + }, "about": { "message": "Tentang" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 57a9207f0ba8..213407a3bd9f 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Annulla" }, + "likeToConnect": { + "message": "$1 vorrebbe connettersi al tuo account" + }, "about": { "message": "Informazioni" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 0f2475bfbc83..a7e1d96dfa63 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "拒否" }, + "likeToConnect": { + "message": "$1 はあなたのアカウントにアクセスしようとしています。" + }, "aboutSettingsDescription": { "message": "バージョンやサポート、問合せ先など" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index 59d73ea76f90..7f3dd0f17359 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -12,6 +12,9 @@ "reject": { "message": "ತಿರಸ್ಕರಿಸಿ" }, + "likeToConnect": { + "message": "$1 ನಿಮ್ಮ ಖಾತೆಗೆ ಸಂಪರ್ಕಿಸಲು ಬಯಸುತ್ತಿದೆ" + }, "about": { "message": "ಕುರಿತು" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 8cd3f13e52b2..bd412c75c023 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "거부" }, + "likeToConnect": { + "message": "$1이 당신의 계정에 연결하길 원합니다." + }, "about": { "message": "정보" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index f014d73725ee..c2a56ec54046 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Atmesti" }, + "likeToConnect": { + "message": "$1 norėtų prisijungti prie jūsų paskyros" + }, "about": { "message": "Apie" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 116af0cc4cd6..0b5af9c4e373 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -12,6 +12,9 @@ "reject": { "message": "Noraidīt" }, + "likeToConnect": { + "message": "$1 vēlas izveidot savienojumu ar jūsu kontu" + }, "about": { "message": "Par" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 403afc3af785..7c591b6065f9 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Tolak" }, + "likeToConnect": { + "message": "$1 ingin menyambung kepada akaun anda" + }, "about": { "message": "Mengenai" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index c57cd08c169a..6617284b795c 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Avslå" }, + "likeToConnect": { + "message": "$1 ønsker å forbindes med kontoen din " + }, "about": { "message": "Info" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 1e77ba1f34f9..2441408f69d3 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Odrzuć" }, + "likeToConnect": { + "message": "$1 chce połączyć się z Twoim kontem" + }, "about": { "message": "Informacje" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index ffebf02bcf81..e2de06a19f9b 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -12,6 +12,9 @@ "reject": { "message": "Rejeitar" }, + "likeToConnect": { + "message": "$1 gostaria de se conectar à sua conta" + }, "about": { "message": "Sobre" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index 2d1c8579be08..3ed11cb865eb 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Respingeți" }, + "likeToConnect": { + "message": "$1 ar dori să se conecteze la contul dvs." + }, "about": { "message": "Despre" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index a25fd87fb52b..43249d6bcdb8 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -542,6 +542,9 @@ "contractInteraction": { "message": "Взаимодействие с контрактом" }, + "likeToConnect": { + "message": "$1 запрашивает доступ к вашему аккаунту" + }, "about": { "message": "О нас" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index fa0280dce57a..d975048d84b3 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Odmítnout" }, + "likeToConnect": { + "message": "$1 sa chce pripojiť k vášmu účtu" + }, "about": { "message": "Informácie" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 292f24467ffd..312bcecfba23 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Zavrni" }, + "likeToConnect": { + "message": "$1 se želi povezati z vašim računom." + }, "about": { "message": "O možnostih" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index 2c5c3225b24c..f4084996f677 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Одбиј" }, + "likeToConnect": { + "message": "$1 bi hteo da se poveže sa vašim nalogom" + }, "about": { "message": "Основни подаци" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index bd02b0f477b9..7503570450fb 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -12,6 +12,9 @@ "reject": { "message": "Avvisa" }, + "likeToConnect": { + "message": "$1 vill ansluta till ditt konto" + }, "about": { "message": "Om" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 6d9f96bedada..38c89aca8643 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -12,6 +12,9 @@ "reject": { "message": "Kataa" }, + "likeToConnect": { + "message": "$1 ingependa kuunganishwa kwenye akaunti yako" + }, "about": { "message": "Kuhusu" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index b511b5487b2b..6b3686f45743 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -2,6 +2,9 @@ "reject": { "message": "ปฏิเสธ" }, + "likeToConnect": { + "message": "$1 ต้องการเชื่อมต่อกับบัญชีของคุณ" + }, "about": { "message": "เกี่ยวกับ" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 70511383c08a..cec516b6ccda 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "Відхилити" }, + "likeToConnect": { + "message": "$1 бажає підключитися до вашого облікового запису" + }, "about": { "message": "Про Google Chrome" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index a9825be5acf2..624ae8f54264 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "拒绝" }, + "likeToConnect": { + "message": "$1 希望关联您的账户" + }, "about": { "message": "关于" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 6cbfd0794179..2859e8fd407d 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -8,6 +8,9 @@ "reject": { "message": "拒絕" }, + "likeToConnect": { + "message": "$1 請求訪問帳戶權限" + }, "about": { "message": "關於" }, From 28302f00150aa1e42110ee8077d83d5f2d718f9c Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Mon, 2 Dec 2019 12:38:00 -0330 Subject: [PATCH 09/21] Login per site no sitemetadata fix (#7610) * Support connected sites for which we have no site metadata. * Change property containing subtitle info often populated by origin to a more accurate of purpose name * Lint fix --- .../connected-sites-list.component.js | 2 +- ui/app/selectors/selectors.js | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ui/app/components/app/connected-sites-list/connected-sites-list.component.js b/ui/app/components/app/connected-sites-list/connected-sites-list.component.js index 1bde4855f7ba..9890a4515359 100644 --- a/ui/app/components/app/connected-sites-list/connected-sites-list.component.js +++ b/ui/app/components/app/connected-sites-list/connected-sites-list.component.js @@ -89,7 +89,7 @@ export default class ConnectedSitesList extends Component { } {domainIsExpanded ?
- { domain.extensionId ? t('extensionId', [domain.extensionId]) : domain.key } + { domain.extensionId ? t('extensionId', [domain.extensionId]) : domain.secondaryName }
: null } diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 206d27a62cb4..c29dcb35f27d 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -459,15 +459,16 @@ function getAddressConnectedDomainMap (state) { if (domains) { Object.keys(domains).forEach(domainKey => { const { permissions } = domains[domainKey] - const { icon, name } = domainMetadata[domainKey] + const { icon, name } = domainMetadata[domainKey] || {} permissions.forEach(perm => { const caveats = perm.caveats || [] const exposedAccountCaveat = caveats.find(caveat => caveat.name === 'exposedAccounts') if (exposedAccountCaveat && exposedAccountCaveat.value && exposedAccountCaveat.value.length) { exposedAccountCaveat.value.forEach(address => { + const nameToRender = name || domainKey addressConnectedIconMap[address] = addressConnectedIconMap[address] - ? { ...addressConnectedIconMap[address], [domainKey]: { icon, name } } - : { [domainKey]: { icon, name } } + ? { ...addressConnectedIconMap[address], [domainKey]: { icon, name: nameToRender } } + : { [domainKey]: { icon, name: nameToRender } } }) } }) @@ -531,7 +532,7 @@ function getRenderablePermissionsDomains (state) { name, icon, extensionId, - } = domainMetadata[domainKey] + } = domainMetadata[domainKey] || {} const permissionsHistoryForDomain = permissionsHistory[domainKey] || {} const ethAccountsPermissionsForDomain = permissionsHistoryForDomain['eth_accounts'] || {} const accountsLastConnectedTime = ethAccountsPermissionsForDomain.accounts || {} @@ -542,7 +543,8 @@ function getRenderablePermissionsDomains (state) { : '' return [ ...acc, { - name, + name: name || domainKey, + secondaryName: name ? domainKey : '', icon, key: domainKey, lastConnectedTime, From 5508f2e60828ed57a1c80da90256c6d313d9bb41 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Mon, 2 Dec 2019 14:13:39 -0330 Subject: [PATCH 10/21] Improve disconnection modal messages (#7612) * Improve disconnectAccountModalDescription and disconnectAllModalDescription messages * Update disconnectAccountModalDescription app/_locales/en/messages.json Co-Authored-By: Mark Stacey * Improve disconnectAccount modal message clarity --- app/_locales/en/messages.json | 4 ++-- .../disconnect-account/disconnect-account.component.js | 5 +++-- .../disconnect-account/disconnect-account.container.js | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 0a5223d929b3..3dbc15c9a94b 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -421,10 +421,10 @@ "message": "Disconnect All" }, "disconnectAllModalDescription": { - "message": "Are you sure? You will no longer be able to interact with any of these sites." + "message": "Are you sure? You will be disconnected from all sites on all accounts." }, "disconnectAccountModalDescription": { - "message": "Are you sure? You will no longer be able to interact with this site." + "message": "Are you sure? Your account (\"$1\") will be disconnected from this site." }, "disconnectAccountQuestion": { "message": "Disconnect account?" diff --git a/ui/app/components/app/modals/disconnect-account/disconnect-account.component.js b/ui/app/components/app/modals/disconnect-account/disconnect-account.component.js index 0d88a03a0598..4fe5c7227eff 100644 --- a/ui/app/components/app/modals/disconnect-account/disconnect-account.component.js +++ b/ui/app/components/app/modals/disconnect-account/disconnect-account.component.js @@ -8,6 +8,7 @@ export default class DisconnectAccount extends PureComponent { static propTypes = { hideModal: PropTypes.func.isRequired, disconnectAccount: PropTypes.func.isRequired, + accountLabel: PropTypes.string.isRequired, } static contextTypes = { @@ -16,7 +17,7 @@ export default class DisconnectAccount extends PureComponent { render () { const { t } = this.context - const { hideModal, disconnectAccount } = this.props + const { hideModal, disconnectAccount, accountLabel } = this.props return (
- { t('disconnectAccountModalDescription') } + { t('disconnectAccountModalDescription', [ accountLabel ]) }
-
diff --git a/ui/app/components/app/account-details/account-details.container.js b/ui/app/components/app/account-details/account-details.container.js index d71326cd72bc..77692a361521 100644 --- a/ui/app/components/app/account-details/account-details.container.js +++ b/ui/app/components/app/account-details/account-details.container.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux' import { compose } from 'recompose' import { withRouter } from 'react-router-dom' +import PropTypes from 'prop-types' import { hideSidebar, showModal } from '../../../store/actions' import AccountDetails from './account-details.component' @@ -13,4 +14,16 @@ function mapDispatchToProps (dispatch) { } } -export default compose(withRouter, connect(null, mapDispatchToProps))(AccountDetails) +const AccountDetailsContainer = compose( + withRouter, + connect(null, mapDispatchToProps) +)(AccountDetails) + +AccountDetailsContainer.propTypes = { + label: PropTypes.string.isRequired, + checksummedAddress: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + showConnectedSites: PropTypes.func.isRequired, +} + +export default AccountDetailsContainer diff --git a/ui/app/components/app/wallet-view/wallet-view.component.js b/ui/app/components/app/wallet-view/wallet-view.component.js index ceccfea51bdc..dc3abf25933e 100644 --- a/ui/app/components/app/wallet-view/wallet-view.component.js +++ b/ui/app/components/app/wallet-view/wallet-view.component.js @@ -7,7 +7,7 @@ import AccountDetails from '../account-details' const { checksumAddress } = require('../../../helpers/utils/util') const TokenList = require('../token-list') -const { ADD_TOKEN_ROUTE } = require('../../../helpers/constants/routes') +const { ADD_TOKEN_ROUTE, CONNECTED_ROUTE } = require('../../../helpers/constants/routes') export default class WalletView extends Component { static contextTypes = { @@ -91,6 +91,18 @@ export default class WalletView extends Component { ) } + showConnectedSites = () => { + const { + sidebarOpen, + hideSidebar, + history, + } = this.props + history.push(CONNECTED_ROUTE) + if (sidebarOpen) { + hideSidebar() + } + } + render () { const { responsiveDisplayClassname, @@ -124,6 +136,7 @@ export default class WalletView extends Component { label={label} checksummedAddress={checksummedAddress} name={identities[selectedAddress].name} + showConnectedSites={this.showConnectedSites} /> {this.renderWalletBalance()} From 6ce80c6d5ab9e3af68675524a74b2972832ee6aa Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 2 Dec 2019 20:17:35 -0400 Subject: [PATCH 14/21] Reject permissions request upon tab close (#7618) Permissions requests are now rejected when the page is closed. This only applies to the full-screen view, as that is the view permission requests should be handled in. The case where the user deals with the request through a different view is handled in #7617 --- .../permissions-connect.component.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ui/app/pages/permissions-connect/permissions-connect.component.js b/ui/app/pages/permissions-connect/permissions-connect.component.js index 273d3b96b19d..2a68bc3817b0 100644 --- a/ui/app/pages/permissions-connect/permissions-connect.component.js +++ b/ui/app/pages/permissions-connect/permissions-connect.component.js @@ -3,6 +3,8 @@ import React, { Component } from 'react' import PermissionsConnectHeader from './permissions-connect-header' import PermissionsConnectFooter from './permissions-connect-footer' import ChooseAccount from './choose-account' +import { getEnvironmentType } from '../../../../app/scripts/lib/util' +import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../app/scripts/lib/enums' import PermissionPageContainer from '../../components/app/permission-page-container' export default class PermissionConnect extends Component { @@ -42,6 +44,21 @@ export default class PermissionConnect extends Component { originName: this.props.originName, } + beforeUnload = () => { + const { permissionsRequestId, rejectPermissionsRequest } = this.props + const { permissionAccepted } = this.state + + if (permissionAccepted === null && permissionsRequestId) { + rejectPermissionsRequest(permissionsRequestId) + } + } + + removeBeforeUnload = () => { + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_FULLSCREEN) { + window.removeEventListener('beforeunload', this.beforeUnload) + } + } + selectAccount = (address) => { this.setState({ page: 2, @@ -56,6 +73,7 @@ export default class PermissionConnect extends Component { page: null, permissionAccepted: accepted, }) + this.removeBeforeUnload() setTimeout(() => { global.platform.currentTab() @@ -75,6 +93,10 @@ export default class PermissionConnect extends Component { } = this.props getCurrentWindowTab() getRequestAccountTabIds() + + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_FULLSCREEN) { + window.addEventListener('beforeunload', this.beforeUnload) + } } render () { From b8c948d751eed599a6b93b16b5abc57e4bc91c4a Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 2 Dec 2019 20:18:01 -0400 Subject: [PATCH 15/21] Handle tab update failure (#7619) `extension.tabs.update` can sometimes fail if the user interacts with the tabs directly around the same time. The redirect flow has been updated to ensure that the permissions tab is still closed in that case. The user is on their own to find the dapp tab again in that case. --- .../permissions-connect.component.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ui/app/pages/permissions-connect/permissions-connect.component.js b/ui/app/pages/permissions-connect/permissions-connect.component.js index 2a68bc3817b0..d608b8af9552 100644 --- a/ui/app/pages/permissions-connect/permissions-connect.component.js +++ b/ui/app/pages/permissions-connect/permissions-connect.component.js @@ -75,14 +75,13 @@ export default class PermissionConnect extends Component { }) this.removeBeforeUnload() - setTimeout(() => { - global.platform.currentTab() - .then(({ id: currentTabId }) => { - global.platform.switchToTab(requestAccountTabs[originName]) - .then(() => { - global.platform.closeTab(currentTabId) - }) - }) + setTimeout(async () => { + const { id: currentTabId } = await global.platform.currentTab() + try { + await global.platform.switchToTab(requestAccountTabs[originName]) + } finally { + global.platform.closeTab(currentTabId) + } }, 2000) } From 320c1c712a0c455bf0c1f429e32b06b0540ec997 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Mon, 2 Dec 2019 22:28:49 -0330 Subject: [PATCH 16/21] Login per site tab popup fixes (#7617) * Handle redirect in response to state update in permissions-connect * Ensure origin is available to permissions-connect subcomponents during redirect * Hide app bar whenever on redirect route * Improvements to handling of redirects in permissions-connect * Ensure permission request id change handling only happens when page is not null * Lint fix --- .../permission-page-container.container.js | 8 +-- .../permissions-connect.component.js | 61 +++++++++++++++---- .../permissions-connect.container.js | 9 ++- ui/app/pages/routes/index.js | 6 +- 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/ui/app/components/app/permission-page-container/permission-page-container.container.js b/ui/app/components/app/permission-page-container/permission-page-container.container.js index 227eaab79de6..f83393c70e11 100644 --- a/ui/app/components/app/permission-page-container/permission-page-container.container.js +++ b/ui/app/components/app/permission-page-container/permission-page-container.container.js @@ -8,14 +8,12 @@ import { } from '../../../selectors/selectors' const mapStateToProps = (state, ownProps) => { - const { request } = ownProps + const { request, cachedOrigin } = ownProps const { metadata: requestMetadata = {} } = request || {} const domainMetadata = getDomainMetadata(state) - const targetDomainMetadata = ( - domainMetadata[requestMetadata.origin] || - { name: requestMetadata.origin, icon: null } - ) + const origin = requestMetadata.origin || cachedOrigin + const targetDomainMetadata = (domainMetadata[origin] || { name: origin, icon: null }) return { permissionsDescriptions: getPermissionsDescriptions(state), diff --git a/ui/app/pages/permissions-connect/permissions-connect.component.js b/ui/app/pages/permissions-connect/permissions-connect.component.js index d608b8af9552..21cf6f69b537 100644 --- a/ui/app/pages/permissions-connect/permissions-connect.component.js +++ b/ui/app/pages/permissions-connect/permissions-connect.component.js @@ -4,7 +4,12 @@ import PermissionsConnectHeader from './permissions-connect-header' import PermissionsConnectFooter from './permissions-connect-footer' import ChooseAccount from './choose-account' import { getEnvironmentType } from '../../../../app/scripts/lib/util' -import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../app/scripts/lib/enums' +import { + ENVIRONMENT_TYPE_FULLSCREEN, + ENVIRONMENT_TYPE_NOTIFICATION, + ENVIRONMENT_TYPE_POPUP, +} from '../../../../app/scripts/lib/enums' +import { DEFAULT_ROUTE, CONNECTED_ROUTE } from '../../helpers/constants/routes' import PermissionPageContainer from '../../components/app/permission-page-container' export default class PermissionConnect extends Component { @@ -22,15 +27,18 @@ export default class PermissionConnect extends Component { addressLastConnectedMap: PropTypes.object, requestAccountTabs: PropTypes.object, permissionsRequestId: PropTypes.string, + domains: PropTypes.object, + history: PropTypes.object.isRequired, } static defaultProps = { originName: '', nativeCurrency: '', - permissionsRequest: {}, + permissionsRequest: undefined, addressLastConnectedMap: {}, requestAccountTabs: {}, permissionsRequestId: '', + domains: {}, } static contextTypes = { @@ -59,6 +67,30 @@ export default class PermissionConnect extends Component { } } + componentDidUpdate (prevProps) { + const { domains, permissionsRequestId } = this.props + const { originName, page } = this.state + + if (!permissionsRequestId && prevProps.permissionsRequestId && page !== null) { + const permissionDataForDomain = domains && domains[originName] || {} + const permissionsForDomain = permissionDataForDomain.permissions || [] + const prevPermissionDataForDomain = prevProps.domains && prevProps.domains[originName] || {} + const prevPermissionsForDomain = prevPermissionDataForDomain.permissions || [] + const addedAPermission = permissionsForDomain.length > prevPermissionsForDomain.length + if (addedAPermission) { + this.redirectFlow(true) + } else { + this.redirectFlow(false) + } + } else if (permissionsRequestId && prevProps.permissionsRequestId && + permissionsRequestId !== prevProps.permissionsRequestId && page !== null) { + this.setState({ + originName: this.props.originName, + page: 1, + }) + } + } + selectAccount = (address) => { this.setState({ page: 2, @@ -67,22 +99,29 @@ export default class PermissionConnect extends Component { } redirectFlow (accepted) { - const { requestAccountTabs } = this.props + const { requestAccountTabs, history } = this.props const { originName } = this.state + this.setState({ page: null, permissionAccepted: accepted, }) this.removeBeforeUnload() - setTimeout(async () => { - const { id: currentTabId } = await global.platform.currentTab() - try { - await global.platform.switchToTab(requestAccountTabs[originName]) - } finally { - global.platform.closeTab(currentTabId) - } - }, 2000) + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_FULLSCREEN) { + setTimeout(async () => { + const { id: currentTabId } = await global.platform.currentTab() + try { + await global.platform.switchToTab(requestAccountTabs[originName]) + } finally { + global.platform.closeTab(currentTabId) + } + }, 2000) + } else if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + history.push(DEFAULT_ROUTE) + } else if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { + history.push(CONNECTED_ROUTE) + } } componentDidMount () { diff --git a/ui/app/pages/permissions-connect/permissions-connect.container.js b/ui/app/pages/permissions-connect/permissions-connect.container.js index 7824cf83618f..1209bd9ccac5 100644 --- a/ui/app/pages/permissions-connect/permissions-connect.container.js +++ b/ui/app/pages/permissions-connect/permissions-connect.container.js @@ -1,10 +1,13 @@ import { connect } from 'react-redux' +import { compose } from 'recompose' +import { withRouter } from 'react-router-dom' import PermissionApproval from './permissions-connect.component' import { getFirstPermissionRequest, getNativeCurrency, getAccountsWithLabels, getLastConnectedInfo, + getPermissionsDomains, } from '../../selectors/selectors' import { formatDate } from '../../helpers/utils/util' import { approvePermissionsRequest, rejectPermissionsRequest, showModal, getCurrentWindowTab, getRequestAccountTabIds } from '../../store/actions' @@ -37,6 +40,7 @@ const mapStateToProps = state => { nativeCurrency, requestAccountTabs, addressLastConnectedMap, + domains: getPermissionsDomains(state), } } @@ -56,4 +60,7 @@ const mapDispatchToProps = dispatch => { } } -export default connect(mapStateToProps, mapDispatchToProps)(PermissionApproval) +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(PermissionApproval) diff --git a/ui/app/pages/routes/index.js b/ui/app/pages/routes/index.js index 27a390a63702..b60ed1cfea59 100644 --- a/ui/app/pages/routes/index.js +++ b/ui/app/pages/routes/index.js @@ -180,7 +180,11 @@ class Routes extends Component { return this.onConfirmPage() || hasPermissionsRequests } - if (hasPermissionsRequests) { + const isHandlingPermissionsRequest = Boolean(matchPath(location.pathname, { + path: CONNECT_ROUTE, exact: false, + })) + + if (hasPermissionsRequests || isHandlingPermissionsRequest) { return true } } From d3c2a05f267557755a97c5af735e1747fbe63bf5 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Tue, 3 Dec 2019 02:17:29 -0330 Subject: [PATCH 17/21] Decouple confirm transaction screen from the selected address (#7622) --- .../confirm-transaction-base.container.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js index 9a238e780b1e..b95d166ee598 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -57,8 +57,6 @@ const mapStateToProps = (state, ownProps) => { identities, addressBook, currentCurrency, - selectedAddress, - selectedAddressTxList, assetImages, network, unapprovedTxs, @@ -72,7 +70,7 @@ const mapStateToProps = (state, ownProps) => { nonce, } = confirmTransaction const { txParams = {}, lastGasPrice, id: transactionId, transactionCategory } = txData - const transaction = R.find(({ id }) => id === (transactionId || Number(paramsTransactionId)))(selectedAddressTxList) || {} + const transaction = Object.values(unapprovedTxs).find(({ id }) => id === (transactionId || Number(paramsTransactionId))) || {} const { from: fromAddress, to: txParamsToAddress, @@ -84,8 +82,8 @@ const mapStateToProps = (state, ownProps) => { const accounts = getMetaMaskAccounts(state) const assetImage = assetImages[txParamsToAddress] - const { balance } = accounts[selectedAddress] - const { name: fromName } = identities[selectedAddress] + const { balance } = accounts[fromAddress] + const { name: fromName } = identities[fromAddress] const toAddress = propsToAddress || txParamsToAddress const toName = identities[toAddress] ? identities[toAddress].name From 6061ec7b10081e026996ddca1c9ee11a2b1bffe6 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 3 Dec 2019 02:57:58 -0400 Subject: [PATCH 18/21] Avoid race condtion that could prevent contextual account switching (#7623) There was a race condition in the logic responsible for switching the selected account based upon the active tab. It was asynchronously querying the active tab, then assuming it had been retrieved later. The active tab info itself was already in the redux store in another spot, one that is guaranteed to be set before the UI renders. The race condition was avoided by deleting the duplicate state, and using the other active tab state. --- .../connected-sites-list.container.js | 4 ++-- ui/app/ducks/app/app.js | 6 ------ ui/app/pages/routes/index.js | 17 +++++++++++---- ui/app/selectors/selectors.js | 4 ++-- ui/app/store/actions.js | 21 ------------------- 5 files changed, 17 insertions(+), 35 deletions(-) diff --git a/ui/app/components/app/connected-sites-list/connected-sites-list.container.js b/ui/app/components/app/connected-sites-list/connected-sites-list.container.js index 253a1f2701e4..de45831a9df7 100644 --- a/ui/app/components/app/connected-sites-list/connected-sites-list.container.js +++ b/ui/app/components/app/connected-sites-list/connected-sites-list.container.js @@ -17,8 +17,8 @@ import { getOriginFromUrl } from '../../../helpers/utils/util' const mapStateToProps = state => { const addressConnectedToCurrentTab = getAddressConnectedToCurrentTab(state) - const { openMetaMaskTabs, currentActiveTab = {} } = state.appState - const { title, url, id } = currentActiveTab + const { openMetaMaskTabs } = state.appState + const { title, url, id } = state.activeTab let tabToConnect diff --git a/ui/app/ducks/app/app.js b/ui/app/ducks/app/app.js index 34a2b93d5939..b877cefb4bfa 100644 --- a/ui/app/ducks/app/app.js +++ b/ui/app/ducks/app/app.js @@ -78,7 +78,6 @@ export default function reduceApp (state, action) { requestAccountTabs: {}, openMetaMaskTabs: {}, currentWindowTab: {}, - currentActiveTab: {}, }, state.appState) switch (action.type) { @@ -782,11 +781,6 @@ export default function reduceApp (state, action) { currentWindowTab: action.value, }) - case actions.SET_ACTIVE_TAB: - return extend(appState, { - currentActiveTab: action.value, - }) - default: return appState } diff --git a/ui/app/pages/routes/index.js b/ui/app/pages/routes/index.js index b60ed1cfea59..b2a2ced68967 100644 --- a/ui/app/pages/routes/index.js +++ b/ui/app/pages/routes/index.js @@ -86,7 +86,7 @@ import { class Routes extends Component { componentWillMount () { - const { currentCurrency, setCurrentCurrencyToUSD, getActiveTab } = this.props + const { currentCurrency, setCurrentCurrencyToUSD } = this.props if (!currentCurrency) { setCurrentCurrencyToUSD() @@ -105,8 +105,13 @@ class Routes extends Component { }) } }) + } - getActiveTab() + componentDidMount () { + const { addressConnectedToCurrentTab, showAccountDetail, selectedAddress } = this.props + if (addressConnectedToCurrentTab && addressConnectedToCurrentTab !== selectedAddress) { + showAccountDetail(addressConnectedToCurrentTab) + } } componentDidUpdate (prevProps) { @@ -357,6 +362,7 @@ Routes.propTypes = { textDirection: PropTypes.string, network: PropTypes.string, provider: PropTypes.object, + selectedAddress: PropTypes.string, frequentRpcListDetail: PropTypes.array, currentView: PropTypes.object, sidebar: PropTypes.object, @@ -373,11 +379,14 @@ Routes.propTypes = { providerId: PropTypes.string, hasPermissionsRequests: PropTypes.bool, autoLogoutTimeLimit: PropTypes.number, - getActiveTab: PropTypes.func, addressConnectedToCurrentTab: PropTypes.string, showAccountDetail: PropTypes.func, } +Routes.defaultProps = { + selectedAddress: undefined, +} + function mapStateToProps (state) { const { appState } = state const { @@ -403,6 +412,7 @@ function mapStateToProps (state) { submittedPendingTransactions: submittedPendingTransactionsSelector(state), network: state.metamask.network, provider: state.metamask.provider, + selectedAddress: state.metamask.selectedAddress, frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], currentCurrency: state.metamask.currentCurrency, isMouseUser: state.appState.isMouseUser, @@ -420,7 +430,6 @@ function mapDispatchToProps (dispatch) { setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)), setLastActiveTime: () => dispatch(actions.setLastActiveTime()), - getActiveTab: () => dispatch(actions.getActiveTab()), showAccountDetail: address => dispatch(actions.showAccountDetail(address)), } } diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index c29dcb35f27d..fe8c108b44e7 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -560,8 +560,8 @@ function getRenderablePermissionsDomains (state) { } function getOriginOfCurrentTab (state) { - const { appState: { currentActiveTab = {} } } = state - return currentActiveTab.url && getOriginFromUrl(currentActiveTab.url) + const { activeTab } = state + return activeTab && activeTab.url && getOriginFromUrl(activeTab.url) } function getLastConnectedInfo (state) { diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index a0e06ef9d426..a09548f0f104 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -402,9 +402,6 @@ const actions = { getCurrentWindowTab, SET_REQUEST_ACCOUNT_TABS: 'SET_REQUEST_ACCOUNT_TABS', SET_CURRENT_WINDOW_TAB: 'SET_CURRENT_WINDOW_TAB', - setActiveTab, - SET_ACTIVE_TAB: 'SET_ACTIVE_TAB', - getActiveTab, getOpenMetamaskTabsIds, SET_OPEN_METAMASK_TAB_IDS: 'SET_OPEN_METAMASK_TAB_IDS', } @@ -3074,21 +3071,3 @@ function getCurrentWindowTab () { dispatch(setCurrentWindowTab(currentWindowTab)) } } - -function setActiveTab (activeTab) { - return { - type: actions.SET_ACTIVE_TAB, - value: activeTab, - } -} - -function getActiveTab () { - return (dispatch) => { - return global.platform.queryTabs() - .then(tabs => { - const activeTab = tabs.find(tab => tab.active) - dispatch(setActiveTab(activeTab)) - return activeTab - }) - } -} From 946bc75a85bb519ffffd19b42c19966b25ae8e10 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 3 Dec 2019 03:03:04 -0400 Subject: [PATCH 19/21] Only redirect back to dapp if current tab is active (#7621) The "redirect back to dapp" behaviour can be disruptive when the permissions connect tab is not active. The purpose of the redirect was to maintain context between the dapp and the permissions request, but if the user has already moved to another tab, that no longer applies. --- .../permissions-connect/permissions-connect.component.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/app/pages/permissions-connect/permissions-connect.component.js b/ui/app/pages/permissions-connect/permissions-connect.component.js index 21cf6f69b537..e92ff16e9f6c 100644 --- a/ui/app/pages/permissions-connect/permissions-connect.component.js +++ b/ui/app/pages/permissions-connect/permissions-connect.component.js @@ -110,11 +110,13 @@ export default class PermissionConnect extends Component { if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_FULLSCREEN) { setTimeout(async () => { - const { id: currentTabId } = await global.platform.currentTab() + const currentTab = await global.platform.currentTab() try { - await global.platform.switchToTab(requestAccountTabs[originName]) + if (currentTab.active) { + await global.platform.switchToTab(requestAccountTabs[originName]) + } } finally { - global.platform.closeTab(currentTabId) + global.platform.closeTab(currentTab.id) } }, 2000) } else if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { From 6d9282797d859aa958ff299fe02239dfc9f6860d Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 3 Dec 2019 12:43:44 -0400 Subject: [PATCH 20/21] Fix JSX style lint errors --- .../account-menu/account-menu.component.js | 8 +- .../connected-sites-list.component.js | 76 ++++++++++--------- .../components/app/modal/modal.component.js | 44 ++++++----- ...ission-page-container-content.component.js | 32 ++++---- .../permission-page-container.component.js | 20 ++--- .../icon-with-fallback.component.js | 20 +++-- .../choose-account.component.js | 58 +++++++------- .../permissions-connect-footer.component.js | 3 +- .../permissions-connect.component.js | 76 ++++++++++--------- 9 files changed, 186 insertions(+), 151 deletions(-) diff --git a/ui/app/components/app/account-menu/account-menu.component.js b/ui/app/components/app/account-menu/account-menu.component.js index e6967cbbef1e..61edde4898d4 100644 --- a/ui/app/components/app/account-menu/account-menu.component.js +++ b/ui/app/components/app/account-menu/account-menu.component.js @@ -112,9 +112,11 @@ export default class AccountMenu extends PureComponent { /> { iconAndNameForOpenDomain - ?
- -
+ ? ( +
+ +
+ ) : null } { this.renderKeyringType(keyring) } diff --git a/ui/app/components/app/connected-sites-list/connected-sites-list.component.js b/ui/app/components/app/connected-sites-list/connected-sites-list.component.js index 9890a4515359..204782762b76 100644 --- a/ui/app/components/app/connected-sites-list/connected-sites-list.component.js +++ b/ui/app/components/app/connected-sites-list/connected-sites-list.component.js @@ -82,15 +82,19 @@ export default class ConnectedSitesList extends Component { { domain.lastConnectedTime - ?
- { t('domainLastConnect', [domain.lastConnectedTime]) } -
+ ? ( +
+ { t('domainLastConnect', [domain.lastConnectedTime]) } +
+ ) : null } {domainIsExpanded - ?
- { domain.extensionId ? t('extensionId', [domain.extensionId]) : domain.secondaryName } -
+ ? ( +
+ { domain.extensionId ? t('extensionId', [domain.extensionId]) : domain.secondaryName } +
+ ) : null } @@ -100,28 +104,30 @@ export default class ConnectedSitesList extends Component { { domainIsExpanded - ?
-
- { - domain.permissionDescriptions.map((description, pdIndex) => { - return ( -
- -
- { description } + ? ( +
+
+ { + domain.permissionDescriptions.map((description, pdIndex) => { + return ( +
+ +
+ { description } +
-
- ) - }) - } -
-
showDisconnectAccountModal(domain.key, domains[domain.key]) } - > - { t('disconnectAccount') } + ) + }) + } +
+
showDisconnectAccountModal(domain.key, domains[domain.key]) } + > + { t('disconnectAccount') } +
-
+ ) : null }
@@ -135,14 +141,16 @@ export default class ConnectedSitesList extends Component {
{ tabToConnect - ?
- -
+ ? ( +
+ +
+ ) : null } diff --git a/ui/app/components/app/modal/modal.component.js b/ui/app/components/app/modal/modal.component.js index 753445460fa4..6c45160fdc63 100644 --- a/ui/app/components/app/modal/modal.component.js +++ b/ui/app/components/app/modal/modal.component.js @@ -64,27 +64,29 @@ export default class Modal extends PureComponent { { children } { !hideFooter - ?
- { - onCancel && ( - - ) - } - -
+ ? ( +
+ { + onCancel && ( + + ) + } + +
+ ) : null } diff --git a/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js index 7e7659fb2cb8..ac38a715f3e9 100644 --- a/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js +++ b/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js @@ -94,7 +94,8 @@ export default class PermissionPageContainerContent extends PureComponent { return (
{ if (!isDisabled) { onPermissionToggle(methodName) @@ -136,24 +137,29 @@ export default class PermissionPageContainerContent extends PureComponent { return (
+ })} + >
{ t(...titleArgs) }
{this.renderPermissionApprovalVisual()} { !redirect - ?
-
- { domainMetadata.extensionId - ? t('thisWillAllowExternalExtension', [domainMetadata.extensionId]) - : t('thisWillAllow', [domainMetadata.name]) - } + ? ( +
+
+ { domainMetadata.extensionId + ? t('thisWillAllowExternalExtension', [domainMetadata.extensionId]) + : t('thisWillAllow', [domainMetadata.name]) + } +
+ { this.renderRequestedPermissions() } +
+ ) + : ( +
+ { t('redirectingBackToDapp') }
- { this.renderRequestedPermissions() } -
- :
- { t('redirectingBackToDapp') } -
+ ) }
) diff --git a/ui/app/components/app/permission-page-container/permission-page-container.component.js b/ui/app/components/app/permission-page-container/permission-page-container.component.js index 79ca0a2057bc..b7cbcd6ba652 100644 --- a/ui/app/components/app/permission-page-container/permission-page-container.component.js +++ b/ui/app/components/app/permission-page-container/permission-page-container.component.js @@ -132,15 +132,17 @@ export default class PermissionPageContainer extends Component { permissionRejected={permissionRejected} /> { !redirect - ? this.onCancel()} - cancelText={this.context.t('cancel')} - onSubmit={() => this.onSubmit()} - submitText={this.context.t('submit')} - submitButtonType="confirm" - buttonSizeLarge={false} - /> + ? ( + this.onCancel()} + cancelText={this.context.t('cancel')} + onSubmit={() => this.onSubmit()} + submitText={this.context.t('submit')} + submitButtonType="confirm" + buttonSizeLarge={false} + /> + ) : null }
diff --git a/ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js b/ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js index 0ea32b5ab53b..13b3e93d783a 100644 --- a/ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js +++ b/ui/app/components/ui/icon-with-fallback/icon-with-fallback.component.js @@ -23,14 +23,18 @@ export default class IconWithFallback extends PureComponent {
{ !this.state.iconError && icon - ? this.setState({ iconError: true })} - /> - : - { name.length ? name.charAt(0).toUpperCase() : '' } - + ? ( + this.setState({ iconError: true })} + /> + ) + : ( + + { name.length ? name.charAt(0).toUpperCase() : '' } + + ) }
) diff --git a/ui/app/pages/permissions-connect/choose-account/choose-account.component.js b/ui/app/pages/permissions-connect/choose-account/choose-account.component.js index e4669878f525..1a3a8fc4984d 100644 --- a/ui/app/pages/permissions-connect/choose-account/choose-account.component.js +++ b/ui/app/pages/permissions-connect/choose-account/choose-account.component.js @@ -36,36 +36,40 @@ export default class ChooseAccount extends Component { { accounts.map((account, index) => { const { address, addressLabel, balance } = account - return (
selectAccount(address) } - className="permissions-connect-choose-account__account" - > -
- -
-
{ addressLabel }
- selectAccount(address) } + className="permissions-connect-choose-account__account" + > +
+ +
+
{ addressLabel }
+ +
+ { addressLastConnectedMap[address] + ? ( +
+ { this.context.t('lastConnected') } + { addressLastConnectedMap[address] } +
+ ) + : null + }
- { addressLastConnectedMap[address] - ?
- { this.context.t('lastConnected') } - { addressLastConnectedMap[address] } -
- : null - } -
) + ) }) }
diff --git a/ui/app/pages/permissions-connect/permissions-connect-footer/permissions-connect-footer.component.js b/ui/app/pages/permissions-connect/permissions-connect-footer/permissions-connect-footer.component.js index 6d41771cf740..b8fad97a3126 100644 --- a/ui/app/pages/permissions-connect/permissions-connect-footer/permissions-connect-footer.component.js +++ b/ui/app/pages/permissions-connect/permissions-connect-footer/permissions-connect-footer.component.js @@ -18,7 +18,8 @@ export default class PermissionsConnectFooter extends Component { onClick={() => { global.platform.openWindow({ url: 'https://medium.com/metamask/privacy-mode-is-now-enabled-by-default-1c1c957f4d57' }) }} - >{ t('learnAboutRisks') }
+ >{ t('learnAboutRisks') } + ) diff --git a/ui/app/pages/permissions-connect/permissions-connect.component.js b/ui/app/pages/permissions-connect/permissions-connect.component.js index e92ff16e9f6c..69dcbd390ca4 100644 --- a/ui/app/pages/permissions-connect/permissions-connect.component.js +++ b/ui/app/pages/permissions-connect/permissions-connect.component.js @@ -160,41 +160,47 @@ export default class PermissionConnect extends Component { : null } { page === 1 - ? this.selectAccount(address)} - selectNewAccountViaModal={() => { - showNewAccountModal({ - onCreateNewAccount: this.selectAccount, - newAccountNumber, - }) - }} - addressLastConnectedMap={addressLastConnectedMap} - cancelPermissionsRequest={requestId => { - if (requestId) { - rejectPermissionsRequest(requestId) - this.redirectFlow(false) - } - }} - permissionsRequestId={permissionsRequestId} - /> - :
{ - approvePermissionsRequest(requestId, accounts) - this.redirectFlow(true) - }} - rejectPermissionsRequest={requestId => { - rejectPermissionsRequest(requestId) - this.redirectFlow(false) - }} - selectedIdentity={accounts.find(account => account.address === selectedAccountAddress)} - redirect={page === null} - permissionRejected={ permissionAccepted === false } - /> -
+ ? ( + this.selectAccount(address)} + selectNewAccountViaModal={() => { + showNewAccountModal({ + onCreateNewAccount: this.selectAccount, + newAccountNumber, + }) + }} + addressLastConnectedMap={addressLastConnectedMap} + cancelPermissionsRequest={requestId => { + if (requestId) { + rejectPermissionsRequest(requestId) + this.redirectFlow(false) + } + }} + permissionsRequestId={permissionsRequestId} + /> + ) + : ( +
+ { + approvePermissionsRequest(requestId, accounts) + this.redirectFlow(true) + }} + rejectPermissionsRequest={requestId => { + rejectPermissionsRequest(requestId) + this.redirectFlow(false) + }} + selectedIdentity={accounts.find(account => account.address === selectedAccountAddress)} + redirect={page === null} + permissionRejected={ permissionAccepted === false } + /> + +
+ ) } ) From 706dd35b7c199124c35817cfb4b7731277ff16a2 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 3 Dec 2019 12:45:39 -0400 Subject: [PATCH 21/21] Remove unused state --- .../connected-sites-list/connected-sites-list.component.js | 3 --- .../permission-page-container-content.component.js | 4 ---- 2 files changed, 7 deletions(-) diff --git a/ui/app/components/app/connected-sites-list/connected-sites-list.component.js b/ui/app/components/app/connected-sites-list/connected-sites-list.component.js index 204782762b76..a04f87486c04 100644 --- a/ui/app/components/app/connected-sites-list/connected-sites-list.component.js +++ b/ui/app/components/app/connected-sites-list/connected-sites-list.component.js @@ -28,9 +28,6 @@ export default class ConnectedSitesList extends Component { state = { expandedDomain: '', - iconError: '', - domains: {}, - tabToConnect: null, } componentWillMount () { diff --git a/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js index ac38a715f3e9..d6a62dbbfa69 100644 --- a/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js +++ b/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js @@ -27,10 +27,6 @@ export default class PermissionPageContainerContent extends PureComponent { t: PropTypes.func, } - state = { - iconError: false, - } - renderAccountInfo = (account) => { return (