diff --git a/.eslintrc.js b/.eslintrc.js index 677ca3d98..3cc83ee1f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,6 +23,7 @@ module.exports = { '@metamask/eslint-config/config/nodejs', '@metamask/eslint-config/config/mocha', 'plugin:react/recommended', + 'plugin:react-hooks/recommended', ], plugins: [ diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index 7d30ec452..c279f5f17 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -648,9 +648,6 @@ "metamaskVersion": { "message": "የ MetaMask ስሪት" }, - "missingYourTokens": { - "message": "ተለዋጭ ስሞችዎን አላዩም?" - }, "mobileSyncText": { "message": "እርስዎ መሆንዎትን ለማረጋገጥ እባከዎ የይለፍ ቃልዎን ያስገቡ!" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index a4ff5b403..46f240e2c 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -644,9 +644,6 @@ "metamaskVersion": { "message": "إصدار MetaMask " }, - "missingYourTokens": { - "message": "لا تجد رموزك؟" - }, "mobileSyncText": { "message": "يرجى إدخال كلمة مرورك لتأكيد هويتك!" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 44feb5321..668960321 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -647,9 +647,6 @@ "metamaskVersion": { "message": "Версия на MetaMask" }, - "missingYourTokens": { - "message": "Не виждате жетоните си?" - }, "mobileSyncText": { "message": "Моля, въведете вашата парола, за да потвърдите, че сте вие!" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 1b98f11c2..0b24f07a4 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -651,9 +651,6 @@ "metamaskVersion": { "message": "MetaMask সংস্করণ" }, - "missingYourTokens": { - "message": "আপনার টোকেনগুলি দেখতে পাচ্ছেন না?" - }, "mobileSyncText": { "message": "এটি যে আপনি তা নিশ্চিত করতে অনুগ্রহ করে আপনার পাসওয়ার্ড লিখুন!" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index 11c658aa8..1d5eed571 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -635,9 +635,6 @@ "metamaskVersion": { "message": "Versió MetaMask" }, - "missingYourTokens": { - "message": "No veus els teus tokens?" - }, "mobileSyncText": { "message": "Si us plau, introdueix la teva contrasenya per confirmar que ets tu!" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 92867354d..136dd6a55 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -635,9 +635,6 @@ "metamaskDescription": { "message": "Som forbinder dig til Ethereum og de decentraliserede internet." }, - "missingYourTokens": { - "message": "Kan du ikke se dine tokens?" - }, "mobileSyncText": { "message": "Indtast din adgangskode for at bekræfte, at det er dig!" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 8778d28fc..14192b687 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -627,9 +627,6 @@ "metamaskVersion": { "message": "MetaMask-Version" }, - "missingYourTokens": { - "message": "Sie können Ihre Token nicht sehen?" - }, "mobileSyncText": { "message": "Bitte geben Sie Ihr Passwort ein, um Ihre Identität zu verifizieren!" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index f0ca2ba3b..7d021f7ab 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -648,9 +648,6 @@ "metamaskVersion": { "message": "Έκδοση MetaMask " }, - "missingYourTokens": { - "message": "Δεν βλέπετε τα διακριτικά σας;" - }, "mobileSyncText": { "message": "Παρακαλούμε δώστε τον κωδικό πρόσβασής σας για να επιβεβαιώσετε ότι είστε εσείς!" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f86498776..60c283a29 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -26,13 +26,10 @@ "description": "$1 is the number of accounts" }, "connectedAccountsEmptyDescription": { - "message": "MetaMask is not connected this site. To connect to a decentralized app (dapp), find the connect button on their site." + "message": "MetaMask is not connected this site. To connect to a web3 site, find the connect button on their site." }, - "primary": { - "message": "Primary" - }, - "lastActive": { - "message": "Last active" + "currentAccountNotConnected": { + "message": "Your current account is not connected" }, "switchToThisAccount": { "message": "Switch to this account" @@ -73,12 +70,6 @@ "showIncomingTransactionsDescription": { "message": "Select this to use Etherscan to show incoming transactions in the transactions list" }, - "cancelling": { - "message": "Cancelling..." - }, - "cancelledConnectionWithMetaMask": { - "message": "Cancelled Connection With MetaMask" - }, "chartOnlyAvailableEth": { "message": "Chart only available on Ethereum networks." }, @@ -88,12 +79,9 @@ "connectWithMetaMask": { "message": "Connect With MetaMask" }, - "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" + "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" }, "connectToAll": { "message": "Connect to all your $1", @@ -109,7 +97,7 @@ }, "connectToMultipleNumberOfAccounts": { "message": "$1 accounts", - "description": "$1 is the number of accounts to which the site/dapp is asking to connect; this will substitute $1 in connectToMultiple" + "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" }, "contractInteraction": { "message": "Contract Interaction" @@ -148,6 +136,12 @@ "accountSelectionRequired": { "message": "You need to select an account!" }, + "active": { + "message": "Active" + }, + "activity": { + "message": "Activity" + }, "activityLog": { "message": "activity log" }, @@ -193,17 +187,11 @@ "alertsSettingsDescription": { "message": "Enable or disable each alert" }, - "alertSettingsSwitchToConnected": { - "message": "Opening popup with an unconnected account selected" - }, - "alertSettingsSwitchToConnectedDescription": { - "message": "This alert is shown when you open the popup with an unconnected account selected." - }, - "alertSettingsUnconnectedAccount": { - "message": "Switching to an unconnected account" - }, - "alertSettingsUnconnectedAccountDescription": { - "message": "This alert is shown in the popup when you switch from a connected account to an unconnected account." + "alertSettingsUnconnectedAccount": { + "message": "Browsing a website with an unconnected account selected" + }, + "alertSettingsUnconnectedAccountDescription": { + "message": "This alert is shown in the popup when you are browsing a connected Web3 site, but the currently selected account is not connected." }, "allowOriginSpendToken": { "message": "Allow $1 to spend your $2?", @@ -242,6 +230,9 @@ "asset": { "message": "Asset" }, + "assets": { + "message": "Assets" + }, "attemptingConnect": { "message": "Attempting to connect to blockchain." }, @@ -370,7 +361,7 @@ "message": "Confirmed" }, "confirmPassword": { - "message": "Confirm Password" + "message": "Confirm password" }, "confirmSecretBackupPhrase": { "message": "Confirm your Secret Backup Phrase" @@ -784,10 +775,10 @@ "message": " Imported accounts will not be associated with your originally created MetaMask account seedphrase. Learn more about imported accounts " }, "importAccountSeedPhrase": { - "message": "Import an Account with Seed Phrase" + "message": "Import an account with seed phrase" }, "importWallet": { - "message": "Import Wallet" + "message": "Import wallet" }, "importYourExisting": { "message": "Import your existing wallet using a 12 word seed phrase" @@ -929,9 +920,6 @@ "metamaskVersion": { "message": "MetaMask Version" }, - "missingYourTokens": { - "message": "Don't see your tokens?" - }, "mobileSyncText": { "message": "Please enter your password to confirm it's you!" }, @@ -986,7 +974,7 @@ "message": "New Contract" }, "newPassword": { - "message": "New Password (min 8 chars)" + "message": "New password (min 8 chars)" }, "newNetwork": { "message": "New Network" @@ -997,9 +985,6 @@ "noAlreadyHaveSeed": { "message": "No, I already have a seed phrase" }, - "notConnected": { - "message": "Not connected" - }, "protectYourKeys": { "message": "Protect Your Keys!" }, @@ -1015,6 +1000,9 @@ "onlyConnectTrust": { "message": "Only connect with sites you trust." }, + "onlyAddTrustedNetworks": { + "message": "A malicious Ethereum network provider can lie about the state of the blockchain and record your network activity. Only add custom networks you trust." + }, "optionalChainId": { "message": "ChainID (optional)" }, @@ -1090,7 +1078,7 @@ "description": "For importing an account from a private key" }, "pending": { - "message": "pending" + "message": "Pending" }, "permissionCheckedIconDescription": { "message": "You have approved this permission" @@ -1281,6 +1269,12 @@ "seedPhrasePlaceholder": { "message": "Separate each word with a single space" }, + "seedPhrasePlaceholderPaste": { + "message": "Paste seed phrase from clipboard" + }, + "showSeedPhrase": { + "message": "Show seed phrase" + }, "seedPhraseReq": { "message": "Seed phrases contain 12, 15, 18, 21, or 24 words" }, @@ -1305,6 +1299,9 @@ "selectType": { "message": "Select Type" }, + "buy": { + "message": "Buy" + }, "send": { "message": "Send" }, @@ -1455,10 +1452,10 @@ "message": "Select the account you want to view. You can only choose one at a time." }, "step3HardwareWallet": { - "message": "3. Start using dApps and more!" + "message": "3. Start using web3 sites and more!" }, "step3HardwareWalletMsg": { - "message": "Use your hardware account like you would with any Ethereum account. Connect to dApps, send ETH, buy and store ERC20 tokens and non-fungible tokens like CryptoKitties." + "message": "Use your hardware account like you would with any Ethereum account. Connect to web3 sites, send ETH, buy and store ERC20 tokens and non-fungible tokens like CryptoKitties." }, "storePhrase": { "message": "Store this phrase in a password manager like 1Password." @@ -1472,16 +1469,6 @@ "supportCenter": { "message": "Visit our Support Center" }, - "switchAccounts": { - "message": "Switch accounts" - }, - "switchToConnectedAlertMultipleAccountsDescription": { - "message": "This account is not connected. Switch to a connected account?" - }, - "switchToConnectedAlertSingleAccountDescription": { - "message": "This account is not connected. Switch to a connected account ($1)?", - "description": "$1 will be replaced by the name of the connected account" - }, "symbol": { "message": "Symbol" }, @@ -1600,6 +1587,9 @@ "transactionTime": { "message": "Transaction Time" }, + "showTransactionTimeDescription": { + "message": "Select this to display pending transaction time estimates in the activity tab while on the Main Ethereum Network. Note: estimates are approximations based on network conditions." + }, "transfer": { "message": "Transfer" }, @@ -1626,10 +1616,7 @@ "unapproved": { "message": "Unapproved" }, - "unconnectedAccountAlertDescription": { - "message": "$1 is not connected to $2." - }, - "unconnectedAccountAlertDisableTooltip": { + "alertDisableTooltip": { "message": "This can be changed in \"Settings > Alerts\"" }, "units": { @@ -1702,7 +1689,7 @@ "message": "Visit our web site" }, "walletSeed": { - "message": "Wallet Seed" + "message": "Seed phrase" }, "welcomeBack": { "message": "Welcome Back!" @@ -1746,7 +1733,7 @@ }, "decryptMessageNotice": { "message": "$1 would like to read this message to complete your action", - "description": "$1 is website or dapp name" + "description": "$1 is the web3 site name" }, "decryptMetamask": { "message": "Decrypt message" @@ -1766,12 +1753,6 @@ }, "encryptionPublicKeyNotice": { "message": "$1 would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you.", - "description": "$1 is website or dapp name" - }, - "thisSite": { - "message": "this site" - }, - "thisAccount": { - "message": "This account" + "description": "$1 is the web3 site name" } } diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 8c52642d6..d513a2f19 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -527,9 +527,6 @@ "metamaskVersion": { "message": "Versión de MetaMask" }, - "missingYourTokens": { - "message": "¿No ves tus tokens?" - }, "myAccounts": { "message": "Mis cuentas" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 88f08edd6..8a83b65e8 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -636,9 +636,6 @@ "metamaskVersion": { "message": "Versión de MetaMask" }, - "missingYourTokens": { - "message": "¿No puedes ver tus tokens?" - }, "mobileSyncText": { "message": "Ingresa tu contraseña para confirmar que eres tú." }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index cd2fd0611..5f1ca3132 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -641,9 +641,6 @@ "metamaskVersion": { "message": "MetaMaski versioon" }, - "missingYourTokens": { - "message": "Te ei näe oma lube?" - }, "mobileSyncText": { "message": "Sisestage parool, et saaksime teid tuvastada!" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index 81570c579..d59bb3f32 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -651,9 +651,6 @@ "metamaskVersion": { "message": "نسخه MetaMask" }, - "missingYourTokens": { - "message": "آیا رمزیاب های تان را نمیبیند؟" - }, "mobileSyncText": { "message": "لطفًا رمز عبور را وارد نمایید تا تأیید شود که خود شما هستید!" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index dad9c42b5..a67ef40ea 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -648,9 +648,6 @@ "metamaskVersion": { "message": "MetaMask-versio" }, - "missingYourTokens": { - "message": "Etkö näe tietueitasi?" - }, "mobileSyncText": { "message": "Vahvista henkilöllisyytesi antamalla salasanasi!" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index 9f8cd1f2a..d925e9f63 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -589,9 +589,6 @@ "metamaskVersion": { "message": "Bersyon ng MetaMask" }, - "missingYourTokens": { - "message": "Hindi mo ba nakikita ang iyong mga token?" - }, "mobileSyncText": { "message": "Pakilagay ang iyong password para kumpirmahing ikaw ito!" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 76273f0d0..7d28b3a0a 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -633,9 +633,6 @@ "metamaskVersion": { "message": "Version de MetaMask" }, - "missingYourTokens": { - "message": "Vous ne voyez pas vos jetons ?" - }, "mobileSyncText": { "message": "Veuillez entrer votre mot de passe pour confirmer que c'est bien vous !" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 74f6fc9eb..7fdd31a89 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -648,9 +648,6 @@ "metamaskVersion": { "message": "גרסת MetaMask" }, - "missingYourTokens": { - "message": "אין באפשרותך לראות את הטוקנים שלך?" - }, "mobileSyncText": { "message": "נא להזין את הססמה שלך כדי לאשר שזה/זו אכן את/ה!" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 6df3a9cbd..1686b79df 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -648,9 +648,6 @@ "metamaskVersion": { "message": "MetaMask का संस्करण" }, - "missingYourTokens": { - "message": "अपने टोकन को देख नहीं पा रहें हैं?" - }, "mobileSyncText": { "message": "यह आप ही हैं इसकी पुष्टि करने के लिए कृपया अपना पासवर्ड दर्ज करें!" }, diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index df0e4af15..bc4d39ff4 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -644,9 +644,6 @@ "metamaskVersion": { "message": "Inačica usluge MetaMask" }, - "missingYourTokens": { - "message": "Ne vidite svoje tokene?" - }, "mobileSyncText": { "message": "Upišite svoju lozinku kako biste potvrdili da ste to vi!" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index a2526ad88..c60e60c5a 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -383,9 +383,6 @@ "metamaskVersion": { "message": "MetaMask Vèsyon" }, - "missingYourTokens": { - "message": "Ou pa wè token ou a?" - }, "myAccounts": { "message": "Kont mwen" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index bbeb0710a..84be04277 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -644,9 +644,6 @@ "metamaskVersion": { "message": "MetaMask verzió" }, - "missingYourTokens": { - "message": "Nem látja az érméit?" - }, "mobileSyncText": { "message": "Kérünk írd be jelszavad, hogy igazold kiléted!" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 44c7416f4..7d942d231 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -632,9 +632,6 @@ "metamaskVersion": { "message": "Versi MetaMask" }, - "missingYourTokens": { - "message": "Tidak melihat token Anda?" - }, "mobileSyncText": { "message": "Harap masukkan sandi untuk mengonfirmasi ini memang Anda!" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 0e2b45233..a12481f1a 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -5,18 +5,12 @@ "showIncomingTransactionsDescription": { "message": "Usa Etherscan per visualizzare le transazioni in ingresso nella lista delle transazioni" }, - "cancelledConnectionWithMetaMask": { - "message": "Transazioni Annullate con MetaMask" - }, "chartOnlyAvailableEth": { "message": "Grafico disponibile solo per le reti Ethereum." }, "connectedSites": { "message": "Siti connessi" }, - "connectingWithMetaMask": { - "message": "Connettendo con MetaMask..." - }, "connectTo": { "message": "Collegati a $1", "description": "$1 is the name/origin of a site/dapp that the user can connect to metamask" @@ -789,9 +783,6 @@ "metamaskVersion": { "message": "versione di MetaMask" }, - "missingYourTokens": { - "message": "Non vedi i tuoi token?" - }, "mobileSyncText": { "message": "Per favore inserisci la password per confermare che sei te!" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index fd1b6110d..7b8f6253f 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -651,9 +651,6 @@ "metamaskVersion": { "message": "MetaMask ಆವೃತ್ತಿ" }, - "missingYourTokens": { - "message": "ನಿಮ್ಮ ಟೋಕನ್‌ಗಳು ಕಾಣಿಸುತ್ತಿಲ್ಲವೇ?" - }, "mobileSyncText": { "message": "ಇದು ನೀವೇ ಎಂಬುದನ್ನು ಖಚಿತಪಡಿಸಲು ದಯವಿಟ್ಟು ನಿಮ್ಮ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ನಮೂದಿಸಿ!" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 6fbe305be..18f302c7d 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -645,9 +645,6 @@ "metamaskVersion": { "message": "메타마스크 버전" }, - "missingYourTokens": { - "message": "당신의 토큰이 보이지 않나요?" - }, "mobileSyncText": { "message": "본인 여부를 확인하기 위해 비밀번호를 입력해 주세요!" }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index fcc5b33f6..30fa9dd23 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -651,9 +651,6 @@ "metamaskVersion": { "message": "„MetaMask“ versija" }, - "missingYourTokens": { - "message": "Nematote savo žetonų?" - }, "mobileSyncText": { "message": "Įveskite savo slaptažodį, kad patvirtintumėte, jog tai tikrai jūs!" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 38895f49e..f06839e6c 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -647,9 +647,6 @@ "metamaskVersion": { "message": "MetaMask versija" }, - "missingYourTokens": { - "message": "Neredzat savus marķierus?" - }, "mobileSyncText": { "message": "Lūdzu, ievadiet paroli, lai apstiprinātu, ka tas esat jūs." }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 1071db2bd..1cc5cd6bb 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -625,9 +625,6 @@ "metamaskVersion": { "message": "Versi MetaMask" }, - "missingYourTokens": { - "message": "Tidak nampak token anda?" - }, "mobileSyncText": { "message": "Sila masukkan kata laluan anda untuk mengesahkan ini memang anda!" }, diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index b53fde4f6..f4352505d 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -641,9 +641,6 @@ "metamaskVersion": { "message": "MetaMask-versjon " }, - "missingYourTokens": { - "message": "Ser du ikke sjetongene dine?" - }, "mobileSyncText": { "message": "Vennligst skriv inn passordet ditt for å bekrefte at det er deg!" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 069aca5b9..a3d06bba8 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -645,9 +645,6 @@ "metamaskVersion": { "message": "Wersja MetaMask" }, - "missingYourTokens": { - "message": "Nie widzisz swoich token?" - }, "mobileSyncText": { "message": "Wpisz hasło, aby potwierdzić, że to Ty!" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 70dbed539..4a5b2efbe 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -639,9 +639,6 @@ "metamaskVersion": { "message": "Versão do MetaMask" }, - "missingYourTokens": { - "message": "Não está vendo seus tokens?" - }, "mobileSyncText": { "message": "Insira sua senha para confirmar que é você!" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index 6e73bd0ab..40cc1f2d5 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -638,9 +638,6 @@ "metamaskVersion": { "message": "Versiune MetaMask" }, - "missingYourTokens": { - "message": "Nu vă vedeți token-urile?" - }, "mobileSyncText": { "message": "Vă rugăm introduceți parola pentru a vă confirma identitatea!" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 4739b0484..33d2e217e 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -868,9 +868,6 @@ "metamaskVersion": { "message": "Версия MetaMask" }, - "missingYourTokens": { - "message": "Не видите свои токены?" - }, "mobileSyncText": { "message": "Пожалуйста, введите ваш пароль, чтобы подтвердить, что это вы!" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index dce4d6899..940c816ec 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -620,9 +620,6 @@ "metamaskVersion": { "message": "Verzia MetaMask" }, - "missingYourTokens": { - "message": "Nevidíte svoje tokeny?" - }, "mobileSyncText": { "message": "Zadajte svoje heslo a potvrďte, že ste to vy!" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 2605948e0..84fda7bb1 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -636,9 +636,6 @@ "metamaskVersion": { "message": "Različica" }, - "missingYourTokens": { - "message": "Ne vidite vaših žetonov?" - }, "mobileSyncText": { "message": "Vnesite geslo in potrdite, da ste to vi." }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index 24e855f88..fcbcb4cf9 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -642,9 +642,6 @@ "metamaskVersion": { "message": "MetaMask verzija" }, - "missingYourTokens": { - "message": "Ne vidite svoje tokene?" - }, "mobileSyncText": { "message": "Molimo vas unesite šifru kako biste potvrdili da ste to vi!" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index 3efe883a1..944b710f1 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -635,9 +635,6 @@ "metamaskVersion": { "message": "MetaMask-version" }, - "missingYourTokens": { - "message": "Ser du inte dina tokens?" - }, "mobileSyncText": { "message": "Var vänlig ange ditt lösenord för att bekräfta att det är du!" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 3c757b81b..77e7268b5 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -629,9 +629,6 @@ "metamaskVersion": { "message": "Toleo la MetaMask" }, - "missingYourTokens": { - "message": "Je, huoni vianzio vyako?" - }, "mobileSyncText": { "message": "Tafadhali ingiza nenosiri lako ili kuthibitisha kuwa ni wewe!" }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 6be7f6156..c9d389c78 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -651,9 +651,6 @@ "metamaskVersion": { "message": "Версія MetaMask" }, - "missingYourTokens": { - "message": "Не бачите ваших токенів?" - }, "mobileSyncText": { "message": "Введіть пароль, щоб підтвердити свою особу!" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 66fa5136c..05566ca8e 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -639,9 +639,6 @@ "metamaskVersion": { "message": "MetaMask 版本" }, - "missingYourTokens": { - "message": "无法看到您的代币?" - }, "mobileSyncText": { "message": "请输入密码确认个人身份!" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index d05c038b4..532799252 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -645,9 +645,6 @@ "metamaskVersion": { "message": "MetaMask 版本" }, - "missingYourTokens": { - "message": "看不到您的代幣?" - }, "mobileSyncText": { "message": "請輸入密碼確認身份!" }, diff --git a/app/images/icons/connected-sites-black.svg b/app/images/icons/connected-sites.svg similarity index 85% rename from app/images/icons/connected-sites-black.svg rename to app/images/icons/connected-sites.svg index a2411b42b..4d90e233f 100644 --- a/app/images/icons/connected-sites-black.svg +++ b/app/images/icons/connected-sites.svg @@ -1,3 +1,3 @@ - + diff --git a/app/images/icons/disconnect.svg b/app/images/icons/disconnect.svg index c51387353..146d6c727 100644 --- a/app/images/icons/disconnect.svg +++ b/app/images/icons/disconnect.svg @@ -1,3 +1,3 @@ - + diff --git a/app/scripts/background.js b/app/scripts/background.js index cf273d31a..beaeb78c3 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -30,7 +30,6 @@ import NotificationManager from './lib/notification-manager.js' import MetamaskController from './metamask-controller' import rawFirstTimeState from './first-time-state' import setupSentry from './lib/setupSentry' -import reportFailedTxToSentry from './lib/reportFailedTxToSentry' import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code' import getObjStructure from './lib/getObjStructure' import setupEnsIpfsResolver from './lib/ens-ipfs/setup' @@ -105,10 +104,8 @@ initialize().catch(log.error) * @property {Object} contractExchangeRates - Info about current token prices. * @property {Array} tokens - Tokens held by the current user, including their balances. * @property {Object} send - TODO: Document - * @property {Object} coinOptions - TODO: Document * @property {boolean} useBlockie - Indicates preferred user identicon format. True for blockie, false for Jazzicon. * @property {Object} featureFlags - An object for optional feature flags. - * @property {string} networkEndpointType - TODO: Document * @property {boolean} welcomeScreen - True if welcome screen should be shown. * @property {string} currentLocale - A locale string matching the user's preferred display language. * @property {Object} provider - The current selected network provider. @@ -255,19 +252,6 @@ function setupController (initState, initLangCode) { provider: controller.provider, }) - // report failed transactions to Sentry - controller.txController.on(`tx:status-update`, (txId, status) => { - if (status !== 'failed') { - return - } - const txMeta = controller.txController.txStateManager.getTx(txId) - try { - reportFailedTxToSentry({ sentry, txMeta }) - } catch (e) { - console.error(e) - } - }) - // setup state persistence pump( asStream(controller.store), diff --git a/app/scripts/controllers/alert.js b/app/scripts/controllers/alert.js index e2b477905..ff0d7fb89 100644 --- a/app/scripts/controllers/alert.js +++ b/app/scripts/controllers/alert.js @@ -12,7 +12,6 @@ import ObservableStore from 'obs-store' */ export const ALERT_TYPES = { - switchToConnected: 'switchToConnected', unconnectedAccount: 'unconnectedAccount', } @@ -25,7 +24,7 @@ const defaultState = { }, {} ), - switchToConnectedAlertShown: {}, + unconnectedAccountAlertShownOrigins: {}, } /** @@ -44,7 +43,7 @@ export default class AlertController { defaultState, initState, { - switchToConnectedAlertShown: {}, + unconnectedAccountAlertShownOrigins: {}, } ) this.store = new ObservableStore(state) @@ -54,9 +53,9 @@ export default class AlertController { preferencesStore.subscribe(({ selectedAddress }) => { const currentState = this.store.getState() - if (currentState.switchToConnectedAlertShown && this.selectedAddress !== selectedAddress) { + if (currentState.unconnectedAccountAlertShownOrigins && this.selectedAddress !== selectedAddress) { this.selectedAddress = selectedAddress - this.store.updateState({ switchToConnectedAlertShown: {} }) + this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }) } }) } @@ -72,10 +71,10 @@ export default class AlertController { * Sets the "switch to connected" alert as shown for the given origin * @param {string} origin - The origin the alert has been shown for */ - setSwitchToConnectedAlertShown (origin) { - let { switchToConnectedAlertShown } = this.store.getState() - switchToConnectedAlertShown = { ...switchToConnectedAlertShown } - switchToConnectedAlertShown[origin] = true - this.store.updateState({ switchToConnectedAlertShown }) + setUnconnectedAccountAlertShown (origin) { + let { unconnectedAccountAlertShownOrigins } = this.store.getState() + unconnectedAccountAlertShownOrigins = { ...unconnectedAccountAlertShownOrigins } + unconnectedAccountAlertShownOrigins[origin] = true + this.store.updateState({ unconnectedAccountAlertShownOrigins }) } } diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index 876b2cd2f..4c55e7b8a 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -341,6 +341,24 @@ export class PermissionsController { this.notifyAccountsChanged(origin, newPermittedAccounts) } + /** + * Remove all permissions associated with a particular account. Any eth_accounts + * permissions left with no permitted accounts will be removed as well. + * + * Throws error if the account is invalid, or if the update fails. + * + * @param {string} account - The account to remove. + */ + async removeAllAccountPermissions (account) { + this.validatePermittedAccounts([account]) + + const domains = this.permissions.getDomains() + const connectedOrigins = Object.keys(domains) + .filter((origin) => this._getPermittedAccounts(origin).includes(account)) + + await Promise.all(connectedOrigins.map((origin) => this.removePermittedAccount(origin, account))) + } + /** * Finalizes a permissions request. Throws if request validation fails. * Clones the passed-in parameters to prevent inadvertent modification. diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index 6724ede3d..4ef8b8ff8 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -27,9 +27,6 @@ export default class TokenRatesController { * Updates exchange rates for all tokens */ async updateExchangeRates () { - if (!this.isActive) { - return - } const contractExchangeRates = {} const nativeCurrency = this.currency ? this.currency.state.nativeCurrency.toLowerCase() : 'eth' const pairs = this._tokens.map((token) => token.address).join(',') diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 87f9e9134..cdb922c7d 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -37,7 +37,7 @@ export default class TxGasUtil { let estimatedGasHex = bnToHex(saferGasLimitBN) let simulationFails try { - estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) + estimatedGasHex = await this.estimateTxGas(txMeta) } catch (error) { log.warn(error) simulationFails = { diff --git a/app/scripts/lib/reportFailedTxToSentry.js b/app/scripts/lib/reportFailedTxToSentry.js deleted file mode 100644 index ecf832ce7..000000000 --- a/app/scripts/lib/reportFailedTxToSentry.js +++ /dev/null @@ -1,14 +0,0 @@ -import extractEthjsErrorMessage from './extractEthjsErrorMessage' - -// -// utility for formatting failed transaction messages -// for sending to sentry -// - -export default function reportFailedTxToSentry ({ sentry, txMeta }) { - const errorMessage = 'Transaction Failed: ' + extractEthjsErrorMessage(txMeta.err.message) - sentry.captureMessage(errorMessage, { - // "extra" key is required by Sentry - extra: { txMeta }, - }) -} diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index dcc31207c..c41a4a5dc 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -8,8 +8,7 @@ const METAMASK_ENVIRONMENT = process.env.METAMASK_ENVIRONMENT const SENTRY_DSN_PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505' const SENTRY_DSN_DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496' -export default function setupSentry (opts) { - const { release, getState } = opts +export default function setupSentry ({ release }) { let sentryTarget // detect brave const isBrave = Boolean(window.chrome.ipcRenderer) @@ -44,11 +43,6 @@ export default function setupSentry (opts) { simplifyErrorMessages(report) // modify report urls rewriteReportUrls(report) - // append app state - if (getState) { - const appState = getState() - report.extra.appState = appState - } } catch (err) { console.warn(err) } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5c36d8d23..6b2da2946 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -545,7 +545,7 @@ export default class MetamaskController extends EventEmitter { // alert controller setAlertEnabledness: nodeify(alertController.setAlertEnabledness, alertController), - setSwitchToConnectedAlertShown: nodeify(this.alertController.setSwitchToConnectedAlertShown, this.alertController), + setUnconnectedAccountAlertShown: nodeify(this.alertController.setUnconnectedAccountAlertShown, this.alertController), // 3Box setThreeBoxSyncingPermission: nodeify(threeBoxController.setThreeBoxSyncingPermission, threeBoxController), @@ -620,6 +620,10 @@ export default class MetamaskController extends EventEmitter { // clear known identities this.preferencesController.setAddresses([]) + + // clear permissions + this.permissionsController.clearPermissions() + // create new vault const vault = await keyringController.createNewVaultAndRestore(password, seed) @@ -993,6 +997,8 @@ export default class MetamaskController extends EventEmitter { * */ async removeAccount (address) { + // Remove all associated permissions + await this.permissionsController.removeAllAccountPermissions(address) // Remove account from the preferences controller this.preferencesController.removeAddress(address) // Remove account from the account tracker controller diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 5a403828d..5e8cc8e66 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -4,6 +4,7 @@ import './lib/freezeGlobals' // polyfills import 'abortcontroller-polyfill/dist/polyfill-patch-fetch' +import '@formatjs/intl-relativetimeformat/polyfill' import PortStream from 'extension-port-stream' import { getEnvironmentType } from './lib/util' @@ -35,18 +36,7 @@ async function start () { // setup sentry error reporting const release = global.platform.getVersion() - setupSentry({ release, getState }) - // provide app state to append to error logs - function getState () { - // get app state - const state = window.getCleanAppState - ? window.getCleanAppState() - : {} - // remove unnecessary data - delete state.localeMessages - // return state to be added to request - return state - } + setupSentry({ release }) // identify window type (popup, notification) const windowType = getEnvironmentType() diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json index a8eb67cdd..18a31a93f 100644 --- a/development/states/confirm-sig-requests.json +++ b/development/states/confirm-sig-requests.json @@ -522,9 +522,6 @@ "priceAndTimeEstimatesLastRetrieved": 1541527901281, "errors": {} }, - "switchToConnected": { - "state": "CLOSED" - }, "unconnectedAccount": { "state": "CLOSED" }, diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json index a61ab10ed..ca8ee7c42 100644 --- a/development/states/currency-localization.json +++ b/development/states/currency-localization.json @@ -473,9 +473,6 @@ "priceAndTimeEstimatesLastRetrieved": 1541527901281, "errors": {} }, - "switchToConnected": { - "state": "CLOSED" - }, "unconnectedAccount": { "state": "CLOSED" }, diff --git a/development/states/tx-list-items.json b/development/states/tx-list-items.json index 969535180..17a2435d5 100644 --- a/development/states/tx-list-items.json +++ b/development/states/tx-list-items.json @@ -1295,9 +1295,6 @@ "errors": {} }, "confirmTransaction": {}, - "switchToConnected": { - "state": "CLOSED" - }, "unconnectedAccount": { "state": "CLOSED" } diff --git a/package.json b/package.json index 1d27a3913..7901bbbaf 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "3box": "^1.10.2", "@babel/runtime": "^7.5.5", "@download/blockies": "^1.0.3", + "@formatjs/intl-relativetimeformat": "^5.2.6", "@fortawesome/fontawesome-free": "^5.13.0", "@material-ui/core": "1.0.0", "@metamask/controllers": "^2.0.0", @@ -223,6 +224,7 @@ "eslint-plugin-json": "^1.2.0", "eslint-plugin-mocha": "^6.2.2", "eslint-plugin-react": "^7.18.3", + "eslint-plugin-react-hooks": "^4.0.4", "fancy-log": "^1.3.3", "fast-glob": "^3.2.2", "file-loader": "^1.1.11", diff --git a/test/e2e/address-book.spec.js b/test/e2e/address-book.spec.js index 6d21eed6f..04ea4a14c 100644 --- a/test/e2e/address-book.spec.js +++ b/test/e2e/address-book.spec.js @@ -195,7 +195,7 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - await driver.clickElement(By.css('[data-testid="home__history-tab"]')) + await driver.clickElement(By.css('[data-testid="home__activity-tab"]')) await driver.wait(async () => { const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item')) return confirmedTxes.length === 1 diff --git a/test/e2e/from-import-ui.spec.js b/test/e2e/from-import-ui.spec.js index b65786629..cb8da4a45 100644 --- a/test/e2e/from-import-ui.spec.js +++ b/test/e2e/from-import-ui.spec.js @@ -63,7 +63,7 @@ describe('Using MetaMask with an existing account', function () { }) it('clicks the "Import Wallet" option', async function () { - await driver.clickElement(By.xpath(`//button[contains(text(), 'Import Wallet')]`)) + await driver.clickElement(By.xpath(`//button[contains(text(), 'Import wallet')]`)) await driver.delay(largeDelayMs) }) @@ -73,7 +73,7 @@ describe('Using MetaMask with an existing account', function () { }) it('imports a seed phrase', async function () { - const [seedTextArea] = await driver.findElements(By.css('textarea.first-time-flow__textarea')) + const [seedTextArea] = await driver.findElements(By.css('input[placeholder="Paste seed phrase from clipboard"]')) await seedTextArea.sendKeys(testSeedPhrase) await driver.delay(regularDelayMs) @@ -82,7 +82,7 @@ describe('Using MetaMask with an existing account', function () { const [confirmPassword] = await driver.findElements(By.id('confirm-password')) confirmPassword.sendKeys('correct horse battery staple') - await driver.clickElement(By.css('.first-time-flow__checkbox')) + await driver.clickElement(By.css('.first-time-flow__terms')) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`)) await driver.delay(regularDelayMs) @@ -215,7 +215,7 @@ describe('Using MetaMask with an existing account', function () { }) it('finds the transaction in the transactions list', async function () { - await driver.clickElement(By.css('[data-testid="home__history-tab"]')) + await driver.clickElement(By.css('[data-testid="home__activity-tab"]')) await driver.wait(async () => { const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item')) return confirmedTxes.length === 1 diff --git a/test/e2e/metamask-responsive-ui.spec.js b/test/e2e/metamask-responsive-ui.spec.js index edd10758e..0969af53f 100644 --- a/test/e2e/metamask-responsive-ui.spec.js +++ b/test/e2e/metamask-responsive-ui.spec.js @@ -211,7 +211,7 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - await driver.clickElement(By.css('[data-testid="home__history-tab"]')) + await driver.clickElement(By.css('[data-testid="home__activity-tab"]')) await driver.wait(async () => { const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item')) return confirmedTxes.length === 1 diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 3a2677405..7f1517362 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -266,7 +266,7 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - await driver.clickElement(By.css('[data-testid="home__history-tab"]')) + await driver.clickElement(By.css('[data-testid="home__activity-tab"]')) await driver.wait(async () => { const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item')) return confirmedTxes.length === 1 diff --git a/test/e2e/send-edit.spec.js b/test/e2e/send-edit.spec.js index d5b9f8e55..48ee97cb0 100644 --- a/test/e2e/send-edit.spec.js +++ b/test/e2e/send-edit.spec.js @@ -61,7 +61,7 @@ describe('Using MetaMask with an existing account', function () { }) it('clicks the "Import Wallet" option', async function () { - await driver.clickElement(By.xpath(`//button[contains(text(), 'Import Wallet')]`)) + await driver.clickElement(By.xpath(`//button[contains(text(), 'Import wallet')]`)) await driver.delay(largeDelayMs) }) @@ -71,7 +71,7 @@ describe('Using MetaMask with an existing account', function () { }) it('imports a seed phrase', async function () { - const [seedTextArea] = await driver.findElements(By.css('textarea.first-time-flow__textarea')) + const [seedTextArea] = await driver.findElements(By.css('input[placeholder="Paste seed phrase from clipboard"]')) await seedTextArea.sendKeys(testSeedPhrase) await driver.delay(regularDelayMs) @@ -80,7 +80,7 @@ describe('Using MetaMask with an existing account', function () { const [confirmPassword] = await driver.findElements(By.id('confirm-password')) confirmPassword.sendKeys('correct horse battery staple') - await driver.clickElement(By.css('.first-time-flow__checkbox')) + await driver.clickElement(By.css('.first-time-flow__terms')) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`)) await driver.delay(regularDelayMs) @@ -197,7 +197,7 @@ describe('Using MetaMask with an existing account', function () { }) it('finds the transaction in the transactions list', async function () { - await driver.clickElement(By.css('[data-testid="home__history-tab"]')) + await driver.clickElement(By.css('[data-testid="home__activity-tab"]')) await driver.wait(async () => { const confirmedTxes = await driver.findElements(By.css('.transaction-list__completed-transactions .transaction-list-item')) return confirmedTxes.length === 1 diff --git a/test/e2e/tests/simple-send.spec.js b/test/e2e/tests/simple-send.spec.js index d40ee8e8d..0b48916a7 100644 --- a/test/e2e/tests/simple-send.spec.js +++ b/test/e2e/tests/simple-send.spec.js @@ -22,7 +22,7 @@ describe('MetaMask Browser Extension', function () { await amountField.sendKeys('1') await driver.clickElement(By.css('[data-testid="page-container-footer-next"]')) await driver.clickElement(By.css('[data-testid="page-container-footer-next"]')) - await driver.clickElement(By.css('[data-testid="home__history-tab"]')) + await driver.clickElement(By.css('[data-testid="home__activity-tab"]')) await driver.findElement(By.css('.transaction-list-item')) }) }) diff --git a/test/e2e/threebox.spec.js b/test/e2e/threebox.spec.js index 33b5b1ff7..46669f1df 100644 --- a/test/e2e/threebox.spec.js +++ b/test/e2e/threebox.spec.js @@ -64,7 +64,7 @@ describe('MetaMask', function () { }) it('clicks the "Import Wallet" option', async function () { - await driver.clickElement(By.xpath(`//button[contains(text(), 'Import Wallet')]`)) + await driver.clickElement(By.xpath(`//button[contains(text(), 'Import wallet')]`)) await driver.delay(largeDelayMs) }) @@ -74,7 +74,7 @@ describe('MetaMask', function () { }) it('imports a seed phrase', async function () { - const [seedTextArea] = await driver.findElements(By.css('textarea.first-time-flow__textarea')) + const [seedTextArea] = await driver.findElements(By.css('input[placeholder="Paste seed phrase from clipboard"]')) await seedTextArea.sendKeys(testSeedPhrase) await driver.delay(regularDelayMs) @@ -83,7 +83,7 @@ describe('MetaMask', function () { const [confirmPassword] = await driver.findElements(By.id('confirm-password')) confirmPassword.sendKeys('correct horse battery staple') - await driver.clickElement(By.css('.first-time-flow__checkbox')) + await driver.clickElement(By.css('.first-time-flow__terms')) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`)) await driver.delay(regularDelayMs) @@ -170,7 +170,7 @@ describe('MetaMask', function () { }) it('clicks the "Import Wallet" option', async function () { - await driver2.clickElement(By.xpath(`//button[contains(text(), 'Import Wallet')]`)) + await driver2.clickElement(By.xpath(`//button[contains(text(), 'Import wallet')]`)) await driver2.delay(largeDelayMs) }) @@ -180,7 +180,7 @@ describe('MetaMask', function () { }) it('imports a seed phrase', async function () { - const [seedTextArea] = await driver2.findElements(By.css('textarea.first-time-flow__textarea')) + const [seedTextArea] = await driver2.findElements(By.css('input[placeholder="Paste seed phrase from clipboard"]')) await seedTextArea.sendKeys(testSeedPhrase) await driver2.delay(regularDelayMs) @@ -189,7 +189,7 @@ describe('MetaMask', function () { const [confirmPassword] = await driver2.findElements(By.id('confirm-password')) confirmPassword.sendKeys('correct horse battery staple') - await driver2.clickElement(By.css('.first-time-flow__checkbox')) + await driver2.clickElement(By.css('.first-time-flow__terms')) await driver2.clickElement(By.xpath(`//button[contains(text(), 'Import')]`)) await driver2.delay(regularDelayMs) diff --git a/test/e2e/web3.spec.js b/test/e2e/web3.spec.js index 2aae92bc1..7c2c4c5e5 100644 --- a/test/e2e/web3.spec.js +++ b/test/e2e/web3.spec.js @@ -60,7 +60,7 @@ describe('Using MetaMask with an existing account', function () { }) it('clicks the "Import Wallet" option', async function () { - await driver.clickElement(By.xpath(`//button[contains(text(), 'Import Wallet')]`)) + await driver.clickElement(By.xpath(`//button[contains(text(), 'Import wallet')]`)) await driver.delay(largeDelayMs) }) @@ -70,7 +70,7 @@ describe('Using MetaMask with an existing account', function () { }) it('imports a seed phrase', async function () { - const [seedTextArea] = await driver.findElements(By.css('textarea.first-time-flow__textarea')) + const [seedTextArea] = await driver.findElements(By.css('input[placeholder="Paste seed phrase from clipboard"]')) await seedTextArea.sendKeys(testSeedPhrase) await driver.delay(regularDelayMs) @@ -79,7 +79,7 @@ describe('Using MetaMask with an existing account', function () { const [confirmPassword] = await driver.findElements(By.id('confirm-password')) confirmPassword.sendKeys('correct horse battery staple') - await driver.clickElement(By.css('.first-time-flow__checkbox')) + await driver.clickElement(By.css('.first-time-flow__terms')) await driver.clickElement(By.xpath(`//button[contains(text(), 'Import')]`)) await driver.delay(regularDelayMs) diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 3201e695c..262202419 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -599,6 +599,7 @@ describe('MetaMaskController', function () { sinon.stub(metamaskController.preferencesController, 'removeAddress') sinon.stub(metamaskController.accountTracker, 'removeAccount') sinon.stub(metamaskController.keyringController, 'removeAccount') + sinon.stub(metamaskController.permissionsController, 'removeAllAccountPermissions') ret = await metamaskController.removeAccount(addressToRemove) @@ -608,6 +609,7 @@ describe('MetaMaskController', function () { metamaskController.keyringController.removeAccount.restore() metamaskController.accountTracker.removeAccount.restore() metamaskController.preferencesController.removeAddress.restore() + metamaskController.permissionsController.removeAllAccountPermissions.restore() }) it('should call preferencesController.removeAddress', async function () { @@ -619,6 +621,9 @@ describe('MetaMaskController', function () { it('should call keyringController.removeAccount', async function () { assert(metamaskController.keyringController.removeAccount.calledWith(addressToRemove)) }) + it('should call permissionsController.removeAllAccountPermissions', async function () { + assert(metamaskController.permissionsController.removeAllAccountPermissions.calledWith(addressToRemove)) + }) it('should return address', async function () { assert.equal(ret, '0x1') }) diff --git a/test/unit/app/controllers/permissions/permissions-controller-test.js b/test/unit/app/controllers/permissions/permissions-controller-test.js index b5958baa5..f2272e311 100644 --- a/test/unit/app/controllers/permissions/permissions-controller-test.js +++ b/test/unit/app/controllers/permissions/permissions-controller-test.js @@ -660,6 +660,112 @@ describe('permissions controller', function () { }) }) + + describe('removeAllAccountPermissions', function () { + let permController, notifications + + beforeEach(function () { + notifications = initNotifications() + permController = initPermController(notifications) + grantPermissions( + permController, DOMAINS.a.origin, + PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted) + ) + grantPermissions( + permController, DOMAINS.b.origin, + PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted) + ) + grantPermissions( + permController, DOMAINS.c.origin, + PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted) + ) + }) + + it('should throw if account is not a string', async function () { + await assert.rejects( + () => permController.removeAllAccountPermissions({}), + ERRORS.validatePermittedAccounts.nonKeyringAccount({}), + 'should throw on non-string account param' + ) + }) + + it('should throw if given account is not in keyring', async function () { + await assert.rejects( + () => permController.removeAllAccountPermissions(DUMMY_ACCOUNT), + ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT), + 'should throw on non-keyring account' + ) + }) + + it('should remove permitted account from single origin', async function () { + await permController.removeAllAccountPermissions(ACCOUNTS.a.permitted[1]) + + const accounts = await permController._getPermittedAccounts(DOMAINS.a.origin) + + assert.deepEqual( + accounts, ACCOUNTS.a.permitted.filter((acc) => acc !== ACCOUNTS.a.permitted[1]), + 'origin should have correct accounts' + ) + + assert.deepEqual( + notifications[DOMAINS.a.origin][0], + NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary]), + 'origin should have correct notification' + ) + }) + + it('should permitted account from multiple origins', async function () { + await permController.removeAllAccountPermissions(ACCOUNTS.b.permitted[0]) + + const bAccounts = await permController.getAccounts(DOMAINS.b.origin) + assert.deepEqual( + bAccounts, [], + 'first origin should no accounts' + ) + + const cAccounts = await permController.getAccounts(DOMAINS.c.origin) + assert.deepEqual( + cAccounts, [], + 'second origin no accounts' + ) + + assert.deepEqual( + notifications[DOMAINS.b.origin][0], + NOTIFICATIONS.removedAccounts(), + 'first origin should have correct notification' + ) + + assert.deepEqual( + notifications[DOMAINS.c.origin][0], + NOTIFICATIONS.removedAccounts(), + 'second origin should have correct notification' + ) + }) + + it('should remove eth_accounts permission if removing only permitted account', async function () { + await permController.removeAllAccountPermissions(ACCOUNTS.b.permitted[0]) + + const accounts = await permController.getAccounts(DOMAINS.b.origin) + + assert.deepEqual( + accounts, [], + 'origin should have no accounts' + ) + + const permission = await permController.permissions.getPermission( + DOMAINS.b.origin, PERM_NAMES.eth_accounts + ) + + assert.equal(permission, undefined, 'origin should not have eth_accounts permission') + + assert.deepEqual( + notifications[DOMAINS.b.origin][0], + NOTIFICATIONS.removedAccounts(), + 'origin should have correct notification' + ) + }) + }) + describe('finalizePermissionsRequest', function () { let permController diff --git a/test/unit/app/controllers/permissions/permissions-log-controller-test.js b/test/unit/app/controllers/permissions/permissions-log-controller-test.js index ea99fcd2d..fa2e32889 100644 --- a/test/unit/app/controllers/permissions/permissions-log-controller-test.js +++ b/test/unit/app/controllers/permissions/permissions-log-controller-test.js @@ -58,6 +58,8 @@ const initMiddleware = (permLog) => { } const initClock = () => { + // useFakeTimers, is in fact, not a react-hook + // eslint-disable-next-line clock = useFakeTimers(1) } diff --git a/test/unit/app/controllers/token-rates-controller.js b/test/unit/app/controllers/token-rates-controller.js index 8802c5c6e..2bc341d22 100644 --- a/test/unit/app/controllers/token-rates-controller.js +++ b/test/unit/app/controllers/token-rates-controller.js @@ -6,18 +6,20 @@ import ObservableStore from 'obs-store' describe('TokenRatesController', function () { it('should listen for preferences store updates', function () { const preferences = new ObservableStore({ tokens: [] }) - const controller = new TokenRatesController({ preferences }) preferences.putState({ tokens: ['foo'] }) + const controller = new TokenRatesController({ preferences }) assert.deepEqual(controller._tokens, ['foo']) }) it('should poll on correct interval', async function () { const stub = sinon.stub(global, 'setInterval') - const rateController = new TokenRatesController() // eslint-disable-line no-new - rateController.start(1337) + const preferences = new ObservableStore({ tokens: [] }) + preferences.putState({ tokens: ['foo'] }) + const controller = new TokenRatesController({ preferences }) + controller.start(1337) assert.strictEqual(stub.getCall(0).args[1], 1337) stub.restore() - rateController.stop() + controller.stop() }) }) diff --git a/test/unit/ui/app/actions.spec.js b/test/unit/ui/app/actions.spec.js index b9cdd5716..0affebe46 100644 --- a/test/unit/ui/app/actions.spec.js +++ b/test/unit/ui/app/actions.spec.js @@ -221,10 +221,10 @@ describe('Actions', function () { }) describe('#removeAccount', function () { - let removeAccountSpy + let removeAccountStub afterEach(function () { - removeAccountSpy.restore() + removeAccountStub.restore() }) it('calls removeAccount in background and expect actions to show account', async function () { @@ -238,10 +238,11 @@ describe('Actions', function () { 'SHOW_ACCOUNTS_PAGE', ] - removeAccountSpy = sinon.spy(background, 'removeAccount') + removeAccountStub = sinon.stub(background, 'removeAccount') + removeAccountStub.callsFake((_, callback) => callback()) await store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc')) - assert(removeAccountSpy.calledOnce) + assert(removeAccountStub.calledOnce) const actionTypes = store .getActions() .map((action) => action.type) @@ -257,8 +258,8 @@ describe('Actions', function () { 'HIDE_LOADING_INDICATION', ] - removeAccountSpy = sinon.stub(background, 'removeAccount') - removeAccountSpy.callsFake((_, callback) => { + removeAccountStub = sinon.stub(background, 'removeAccount') + removeAccountStub.callsFake((_, callback) => { callback(new Error('error')) }) 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 f6245f304..6409a9cd6 100644 --- a/ui/app/components/app/account-menu/account-menu.component.js +++ b/ui/app/components/app/account-menu/account-menu.component.js @@ -8,7 +8,7 @@ import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu' import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' import { getEnvironmentType } from '../../../../../app/scripts/lib/util' import Identicon from '../../ui/identicon' -import IconWithFallBack from '../../ui/icon-with-fallback' +import SiteIcon from '../../ui/site-icon' import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import { PRIMARY } from '../../../helpers/constants/common' import { @@ -178,7 +178,7 @@ export default class AccountMenu extends Component { { iconAndNameForOpenDomain ? (
- +
) : null diff --git a/ui/app/components/app/add-token-button/add-token-button.component.js b/ui/app/components/app/add-token-button/add-token-button.component.js index 373c6b116..e1a40d563 100644 --- a/ui/app/components/app/add-token-button/add-token-button.component.js +++ b/ui/app/components/app/add-token-button/add-token-button.component.js @@ -1,33 +1,35 @@ -import PropTypes from 'prop-types' -import React, { PureComponent } from 'react' +import React from 'react' +import { useMetricEvent } from '../../../hooks/useMetricEvent' +import { useI18nContext } from '../../../hooks/useI18nContext' +import { useHistory } from 'react-router-dom' +import { ADD_TOKEN_ROUTE } from '../../../helpers/constants/routes' +import Button from '../../ui/button' -export default class AddTokenButton extends PureComponent { - static contextTypes = { - t: PropTypes.func.isRequired, - } - static defaultProps = { - onClick: () => {}, - } +export default function AddTokenButton () { + const addTokenEvent = useMetricEvent({ + eventOpts: { + category: 'Navigation', + action: 'Token Menu', + name: 'Clicked "Add Token"', + }, + }) + const t = useI18nContext() + const history = useHistory() - static propTypes = { - onClick: PropTypes.func, - } - - render () { - const { t } = this.context - const { onClick } = this.props - - return ( -
- {t('missingYourTokens')} - -
- ) - } + return ( +
+ +
+ ) } diff --git a/ui/app/components/app/add-token-button/index.scss b/ui/app/components/app/add-token-button/index.scss index 9f13d9f9e..e8b2c1a43 100644 --- a/ui/app/components/app/add-token-button/index.scss +++ b/ui/app/components/app/add-token-button/index.scss @@ -1,22 +1,6 @@ .add-token-button { - width: 100%; - height: 90px; - padding: 20px; - text-align: center; - font-size: 1rem; - &__button { - background-color: transparent; - color: $Blue-500; - display: inline-block; - font-size: 1rem; - - &:hover { - color: $Blue-400; - } - - &:active { - color: $Blue-600; - } + max-width: 200px; + margin: 16px auto; } } diff --git a/ui/app/components/app/alerts/alerts.js b/ui/app/components/app/alerts/alerts.js index 9f804d7d9..5d376494c 100644 --- a/ui/app/components/app/alerts/alerts.js +++ b/ui/app/components/app/alerts/alerts.js @@ -2,22 +2,15 @@ import React from 'react' import { useSelector } from 'react-redux' import UnconnectedAccountAlert from './unconnected-account-alert' -import SwitchToConnectedAlert from './switch-to-connected-alert' import { alertIsOpen as unconnectedAccountAlertIsOpen } from '../../../ducks/alerts/unconnected-account' -import { alertIsOpen as switchToConnectedAlertIsOpen } from '../../../ducks/alerts/switch-to-connected' const Alerts = () => { const _unconnectedAccountAlertIsOpen = useSelector(unconnectedAccountAlertIsOpen) - const _switchToConnectedAlertIsOpen = useSelector(switchToConnectedAlertIsOpen) if (_unconnectedAccountAlertIsOpen) { return ( ) - } else if (_switchToConnectedAlertIsOpen) { - return ( - - ) } return null diff --git a/ui/app/components/app/alerts/alerts.scss b/ui/app/components/app/alerts/alerts.scss index 82ec8ead0..280171f5e 100644 --- a/ui/app/components/app/alerts/alerts.scss +++ b/ui/app/components/app/alerts/alerts.scss @@ -1,3 +1 @@ @import './unconnected-account-alert/unconnected-account-alert'; - -@import './switch-to-connected-alert/switch-to-connected-alert'; diff --git a/ui/app/components/app/alerts/switch-to-connected-alert/index.js b/ui/app/components/app/alerts/switch-to-connected-alert/index.js deleted file mode 100644 index 9811c0df2..000000000 --- a/ui/app/components/app/alerts/switch-to-connected-alert/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './switch-to-connected-alert' diff --git a/ui/app/components/app/alerts/switch-to-connected-alert/switch-to-connected-alert.js b/ui/app/components/app/alerts/switch-to-connected-alert/switch-to-connected-alert.js deleted file mode 100644 index d440c250c..000000000 --- a/ui/app/components/app/alerts/switch-to-connected-alert/switch-to-connected-alert.js +++ /dev/null @@ -1,121 +0,0 @@ -import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' - -import { - ALERT_STATE, - switchToAccount, - dismissAlert, - dismissAndDisableAlert, - getAlertState, -} from '../../../../ducks/alerts/switch-to-connected' -import { getPermittedIdentitiesForCurrentTab } from '../../../../selectors' -import Popover from '../../../ui/popover' -import Button from '../../../ui/button' -import Dropdown from '../../../ui/dropdown' -import Checkbox from '../../../ui/check-box' -import Tooltip from '../../../ui/tooltip-v2' -import { useI18nContext } from '../../../../hooks/useI18nContext' - -const { - ERROR, - LOADING, -} = ALERT_STATE - -const SwitchToUnconnectedAccountAlert = () => { - const t = useI18nContext() - const dispatch = useDispatch() - const alertState = useSelector(getAlertState) - const connectedAccounts = useSelector(getPermittedIdentitiesForCurrentTab) - const [accountToSwitchTo, setAccountToSwitchTo] = useState(connectedAccounts[0].address) - const [dontShowThisAgain, setDontShowThisAgain] = useState(false) - - const onClose = async () => { - return dontShowThisAgain - ? await dispatch(dismissAndDisableAlert()) - : dispatch(dismissAlert()) - } - - const options = connectedAccounts.map((account) => { - return { name: account.name, value: account.address } - }) - - return ( - - { - alertState === ERROR - ? ( -
- { t('failureMessage') } -
- ) - : null - } -
- - -
- - )} - footerClassName="switch-to-connected-alert__footer" - onClose={onClose} - subtitle={ - connectedAccounts.length > 1 - ? t('switchToConnectedAlertMultipleAccountsDescription') - : t('switchToConnectedAlertSingleAccountDescription', [connectedAccounts[0].name]) - } - title={t('notConnected')} - > - { - connectedAccounts.length > 1 - ? ( - setAccountToSwitchTo(address)} - options={options} - selectedOption={accountToSwitchTo} - /> - ) - : null - } -
- setDontShowThisAgain((checked) => !checked)} - /> - -
-
- ) -} - -export default SwitchToUnconnectedAccountAlert diff --git a/ui/app/components/app/alerts/switch-to-connected-alert/switch-to-connected-alert.scss b/ui/app/components/app/alerts/switch-to-connected-alert/switch-to-connected-alert.scss deleted file mode 100644 index e4e7d3f80..000000000 --- a/ui/app/components/app/alerts/switch-to-connected-alert/switch-to-connected-alert.scss +++ /dev/null @@ -1,66 +0,0 @@ -.switch-to-connected-alert { - &__footer { - flex-direction: column; - - :only-child { - margin: 0; - } - } - - &__footer-buttons { - display: flex; - flex-direction: row; - - button:first-child { - margin-right: 24px; - } - - button { - font-size: 14px; - line-height: 20px; - padding: 8px; - } - } - - &__error { - margin-bottom: 16px; - padding: 16px; - font-size: 14px; - border: 1px solid #D73A49; - background: #F8EAE8; - border-radius: 3px; - } - - &__content { - align-items: center; - padding: 0 24px 24px 24px; - } - - &__dropdown { - background-color: white; - width: 100%; - margin-bottom: 24px; - } - - &__checkbox-wrapper { - display: flex; - flex-direction: row; - width: 100%; - } - - &__checkbox { - margin-right: 8px; - } - - &__checkbox-label { - font-size: 14px; - margin-top: auto; - margin-bottom: auto; - color: $Grey-500; - display: flex; - } - - &__checkbox-label-tooltip { - margin-left: 8px; - } -} diff --git a/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js index ca5ed7db4..38a8fda6e 100644 --- a/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js +++ b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js @@ -7,12 +7,20 @@ import { dismissAlert, dismissAndDisableAlert, getAlertState, + switchToAccount, } from '../../../../ducks/alerts/unconnected-account' +import { + getOriginOfCurrentTab, + getPermittedIdentitiesForCurrentTab, + getSelectedAddress, + getSelectedIdentity, +} from '../../../../selectors' +import { isExtensionUrl } from '../../../../helpers/utils/util' import Popover from '../../../ui/popover' import Button from '../../../ui/button' import Checkbox from '../../../ui/check-box' import Tooltip from '../../../ui/tooltip-v2' -import { getSelectedIdentity, getOriginOfCurrentTab } from '../../../../selectors' +import ConnectedAccountsList from '../../connected-accounts-list' import { useI18nContext } from '../../../../hooks/useI18nContext' const { @@ -20,12 +28,14 @@ const { LOADING, } = ALERT_STATE -const SwitchToUnconnectedAccountAlert = () => { +const UnconnectedAccountAlert = () => { const t = useI18nContext() const dispatch = useDispatch() const alertState = useSelector(getAlertState) + const connectedAccounts = useSelector(getPermittedIdentitiesForCurrentTab) const origin = useSelector(getOriginOfCurrentTab) const selectedIdentity = useSelector(getSelectedIdentity) + const selectedAddress = useSelector(getSelectedAddress) const [dontShowThisAgain, setDontShowThisAgain] = useState(false) const onClose = async () => { @@ -34,67 +44,70 @@ const SwitchToUnconnectedAccountAlert = () => { : dispatch(dismissAlert()) } - const accountName = selectedIdentity?.name || t('thisAccount') - const siteName = origin || t('thisSite') + const footer = ( + <> + { + alertState === ERROR + ? ( +
+ { t('failureMessage') } +
+ ) + : null + } +
+
+ setDontShowThisAgain((checked) => !checked)} + /> + +
+ +
+ + ) return ( - { - alertState === ERROR - ? ( -
- { t('failureMessage') } -
- ) - : null - } -
- - -
- - )} + contentClassName="unconnected-account-alert__content" footerClassName="unconnected-account-alert__footer" + footer={footer} > - setDontShowThisAgain((checked) => !checked)} + dispatch(connectAccount(selectedAddress))} + connectedAccounts={connectedAccounts} + selectedAddress={selectedAddress} + setSelectedAddress={(address) => dispatch(switchToAccount(address))} + shouldRenderListOptions={false} /> -
) } -export default SwitchToUnconnectedAccountAlert +export default UnconnectedAccountAlert diff --git a/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.scss b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.scss index 61ecf9a18..54f37beac 100644 --- a/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.scss +++ b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.scss @@ -1,25 +1,28 @@ .unconnected-account-alert { + &__content { + border-radius: 0; + } + &__footer { flex-direction: column; - :only-child { + > :only-child { margin: 0; } } - &__footer-buttons { + &__footer-row { display: flex; flex-direction: row; + } - button:first-child { - margin-right: 24px; - } - - button { - font-size: 14px; - line-height: 20px; - padding: 8px; - } + &__dismiss-button { + background: #037DD6; + color: white; + height: 40px; + width: 100px; + border: 0; + border-radius: 100px; } &__error { @@ -31,21 +34,24 @@ border-radius: 3px; } - &__content { + &__checkbox-wrapper { + width: 100%; + display: flex; flex-direction: row; - padding: 0 24px 24px 24px; + align-items: center; } &__checkbox { margin-right: 8px; + padding-top: 1px; // better alignment with rest of content } &__checkbox-label { - font-size: 14px; + display: flex; + font-size: 12px; margin-top: auto; margin-bottom: auto; color: $Grey-500; - display: flex; } &__checkbox-label-tooltip { diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js index 11b30660d..cfff63c1c 100644 --- a/ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js @@ -12,13 +12,15 @@ export default class ConnectedAccountsListItem extends PureComponent { address: PropTypes.string.isRequired, className: PropTypes.string, name: PropTypes.node.isRequired, - status: PropTypes.node.isRequired, + status: PropTypes.string, + action: PropTypes.node, options: PropTypes.node, } static defaultProps = { className: null, options: null, + action: null, } render () { @@ -27,6 +29,7 @@ export default class ConnectedAccountsListItem extends PureComponent { className, name, status, + action, options, } = this.props @@ -39,18 +42,20 @@ export default class ConnectedAccountsListItem extends PureComponent { diameter={32} />
-

- {name} +

+ {name}

{ status ? (

+    {status}

) : null } + {action}
{options} diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/index.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/index.js deleted file mode 100644 index 7733018f8..000000000 --- a/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './connected-accounts-list-permissions.component' diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list.component.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list.component.js index 78627c160..d4e10db7f 100644 --- a/ui/app/components/app/connected-accounts-list/connected-accounts-list.component.js +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list.component.js @@ -1,7 +1,5 @@ -import { DateTime } from 'luxon' import PropTypes from 'prop-types' import React, { PureComponent } from 'react' -import ConnectedAccountsListPermissions from './connected-accounts-list-permissions' import ConnectedAccountsListItem from './connected-accounts-list-item' import ConnectedAccountsListOptions from './connected-accounts-list-options' import { MenuItem } from '../../ui/menu' @@ -13,7 +11,6 @@ export default class ConnectedAccountsList extends PureComponent { static defaultProps = { accountToConnect: null, - permissions: undefined, } static propTypes = { @@ -26,31 +23,35 @@ export default class ConnectedAccountsList extends PureComponent { name: PropTypes.string.isRequired, lastActive: PropTypes.number, })).isRequired, - permissions: PropTypes.arrayOf(PropTypes.shape({ - key: PropTypes.string.isRequired, - })), + connectAccount: PropTypes.func.isRequired, selectedAddress: PropTypes.string.isRequired, - addPermittedAccount: PropTypes.func.isRequired, - removePermittedAccount: PropTypes.func.isRequired, + removePermittedAccount: PropTypes.func, setSelectedAddress: PropTypes.func.isRequired, + shouldRenderListOptions: (props, propName, componentName) => { + if (typeof props[propName] !== 'boolean') { + return new Error( + `Warning: Failed prop type: '${propName}' of component '${componentName}' must be a boolean. Received: ${typeof props[propName]}` + ) + } else if (props[propName] && !props['removePermittedAccount']) { + return new Error( + `Warning: Failed prop type: '${propName}' of component '${componentName}' requires prop 'removePermittedAccount'.` + ) + } + }, } state = { accountWithOptionsShown: null, } - connectAccount = () => { - this.props.addPermittedAccount(this.props.accountToConnect?.address) - } - disconnectAccount = () => { this.hideAccountOptions() this.props.removePermittedAccount(this.state.accountWithOptionsShown) } - switchAccount = () => { + switchAccount = (address) => { this.hideAccountOptions() - this.props.setSelectedAddress(this.state.accountWithOptionsShown) + this.props.setSelectedAddress(address) } hideAccountOptions = () => { @@ -62,7 +63,7 @@ export default class ConnectedAccountsList extends PureComponent { } renderUnconnectedAccount () { - const { accountToConnect } = this.props + const { accountToConnect, connectAccount } = this.props const { t } = this.context if (!accountToConnect) { @@ -75,73 +76,87 @@ export default class ConnectedAccountsList extends PureComponent { className="connected-accounts-list__row--highlight" address={address} name={`${name} (…${address.substr(-4, 4)})`} - status={( - <> - {t('statusNotConnected')} -  ·  - - {t('connect')} - - + status={t('statusNotConnected')} + action={( + connectAccount(accountToConnect.address)} + > + {t('connect')} + )} /> ) } - render () { - const { connectedAccounts, permissions, selectedAddress } = this.props + renderListItemOptions (address) { const { accountWithOptionsShown } = this.state const { t } = this.context + return ( + + + {t('disconnectThisAccount')} + + + ) + } + + renderListItemAction (address) { + const { t } = this.context + + return ( + this.switchAccount(address)} + > + {t('switchToThisAccount')} + + ) + } + + render () { + const { + connectedAccounts, + selectedAddress, + shouldRenderListOptions, + } = this.props + const { t } = this.context + return ( <>
{this.renderUnconnectedAccount()} { - connectedAccounts.map(({ address, name, lastActive }, index) => { - let status - if (index === 0) { - status = t('primary') - } else if (lastActive) { - status = `${t('lastActive')}: ${DateTime.fromMillis(lastActive).toISODate()}` - } - + connectedAccounts.map(({ address, name }, index) => { return ( - { - address === selectedAddress ? null : ( - - {t('switchToThisAccount')} - - ) - } - - {t('disconnectThisAccount')} - - - )} + status={index === 0 ? t('active') : null} + options={ + shouldRenderListOptions + ? this.renderListItemOptions(address) + : null + } + action={ + address !== selectedAddress + ? this.renderListItemAction(address) + : null + } /> ) }) }
- ) } diff --git a/ui/app/components/app/connected-accounts-list/index.scss b/ui/app/components/app/connected-accounts-list/index.scss index edb77009d..fb462775e 100644 --- a/ui/app/components/app/connected-accounts-list/index.scss +++ b/ui/app/components/app/connected-accounts-list/index.scss @@ -8,28 +8,34 @@ } &__account-name { + display: inline; font-weight: bold; font-size: 14px; line-height: 20px; } + %account-status-typography { + font-size: 12px; + line-height: 17px; + padding-top: 4px; + } + &__account-status { + @extend %account-status-typography; + display: inline; color: $Grey-500; } &__account-status-link { + @extend %account-status-typography; + display: block; + &, &:hover { color: $curious-blue; cursor: pointer; } } - &__account-status { - font-size: 12px; - line-height: 17px; - padding-top: 4px; - } - &__row { display: flex; flex-direction: row; @@ -40,10 +46,6 @@ border-top: 1px solid $geyser; - &:last-of-type { - border-bottom: 1px solid $geyser; - } - &--highlight { background-color: $warning-light-yellow; border: 1px solid $warning-yellow; @@ -70,72 +72,6 @@ } } -.connected-accounts-permissions { - display: flex; - flex-direction: column; - padding: 24px; - - font-size: 12px; - line-height: 17px; - color: $Grey-500; - - strong { - font-weight: bold; - } - - p + p { - padding-top: 8px; - } - - &__header { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - cursor: pointer; - - font-size: 14px; - line-height: 20px; - color: #24292E; - - button { - font-size: 16px; - line-height: 24px; - - background: none; - padding: 0; - margin-left: 8px; - } - } - - &__list { - padding-top: 8px; - } - - &__list-item { - display: flex; - - i { - display: block; - padding-right: 8px; - font-size: 18px; - color: $Grey-800; - } - } - - &__list-container { - max-height: 0px; - overflow: hidden; - height: auto; - transition: max-height 0.8s cubic-bezier(0.4, 0.0, 0.2, 1); - - &--expanded { - // arbitrarily set hard coded value for effect to work - max-height: 100px; - } - } -} - .tippy-tooltip.none-theme { background: none; padding: 0; diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/connected-accounts-list-permissions.component.js b/ui/app/components/app/connected-accounts-permissions/connected-accounts-permissions.component.js similarity index 95% rename from ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/connected-accounts-list-permissions.component.js rename to ui/app/components/app/connected-accounts-permissions/connected-accounts-permissions.component.js index 7ee497d53..45f886912 100644 --- a/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/connected-accounts-list-permissions.component.js +++ b/ui/app/components/app/connected-accounts-permissions/connected-accounts-permissions.component.js @@ -2,7 +2,7 @@ import classnames from 'classnames' import PropTypes from 'prop-types' import React, { PureComponent } from 'react' -export default class ConnectedAccountsListPermissions extends PureComponent { +export default class ConnectedAccountsPermissions extends PureComponent { static contextTypes = { t: PropTypes.func.isRequired, } diff --git a/ui/app/components/app/connected-accounts-permissions/index.js b/ui/app/components/app/connected-accounts-permissions/index.js new file mode 100644 index 000000000..6c6113c82 --- /dev/null +++ b/ui/app/components/app/connected-accounts-permissions/index.js @@ -0,0 +1 @@ +export { default } from './connected-accounts-permissions.component' diff --git a/ui/app/components/app/connected-accounts-permissions/index.scss b/ui/app/components/app/connected-accounts-permissions/index.scss new file mode 100644 index 000000000..838e727dc --- /dev/null +++ b/ui/app/components/app/connected-accounts-permissions/index.scss @@ -0,0 +1,64 @@ +.connected-accounts-permissions { + display: flex; + flex-direction: column; + + font-size: 12px; + line-height: 17px; + color: $Grey-500; + + strong { + font-weight: bold; + } + + p + p { + padding-top: 8px; + } + + &__header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + cursor: pointer; + + font-size: 14px; + line-height: 20px; + color: #24292E; + + button { + font-size: 16px; + line-height: 24px; + + background: none; + padding: 0; + margin-left: 8px; + } + } + + &__list { + padding-top: 8px; + } + + &__list-item { + display: flex; + + i { + display: block; + padding-right: 8px; + font-size: 18px; + color: $Grey-800; + } + } + + &__list-container { + max-height: 0px; + overflow: hidden; + height: auto; + transition: max-height 0.8s cubic-bezier(0.4, 0.0, 0.2, 1); + + &--expanded { + // arbitrarily set hard coded value for effect to work + max-height: 100px; + } + } +} 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 436b65338..6f6ff4f9a 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 @@ -1,6 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import IconWithFallBack from '../../ui/icon-with-fallback' +import SiteIcon from '../../ui/site-icon' import { stripHttpSchemes } from '../../../helpers/utils/util' export default class ConnectedSitesList extends Component { @@ -28,7 +28,7 @@ export default class ConnectedSitesList extends Component { { connectedDomains.map((domain) => (
- + {this.getDomainDisplayName(domain)} diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss index 6fbe4389b..59088d232 100644 --- a/ui/app/components/app/index.scss +++ b/ui/app/components/app/index.scss @@ -88,10 +88,14 @@ @import 'connected-accounts-list/index'; -@import '../ui/icon-with-fallback/index'; +@import 'connected-accounts-permissions/index'; @import '../ui/icon/index'; +@import '../ui/icon-with-fallback/icon-with-fallback'; + +@import '../ui/icon-with-label/index'; + @import '../ui/circle-icon/index'; @import '../ui/alert-circle-icon/index'; @@ -109,3 +113,5 @@ @import 'wallet-overview/index'; @import '../ui/account-mismatch-warning/index'; + +@import '../ui/icon-border/icon-border'; diff --git a/ui/app/components/app/menu-bar/index.scss b/ui/app/components/app/menu-bar/index.scss index 368e5a8ec..4b9e428fa 100644 --- a/ui/app/components/app/menu-bar/index.scss +++ b/ui/app/components/app/menu-bar/index.scss @@ -21,7 +21,7 @@ .account-options-menu { &__connected-sites:before { content: ""; - background-image: url(/images/icons/connected-sites-black.svg); + background-image: url(/images/icons/connected-sites.svg); background-size: contain; background-repeat: no-repeat; background-position: center; diff --git a/ui/app/components/app/permission-page-container/index.scss b/ui/app/components/app/permission-page-container/index.scss index a996aed5b..9a17655ec 100644 --- a/ui/app/components/app/permission-page-container/index.scss +++ b/ui/app/components/app/permission-page-container/index.scss @@ -46,15 +46,6 @@ padding-left: 24px; padding-right: 24px; - &--redirect { - margin-top: 140px; - width: 100%; - display: flex; - align-items: center; - padding-top: 8px; - height: 144px; - } - a, a:hover { color: $dodger-blue; } @@ -94,10 +85,6 @@ @extend %content-text; line-height: 20px; color: #6A737D; - - &--redirect { - text-align: center; - } } &__permissions-container { @@ -147,105 +134,3 @@ font-weight: bold; } } - -.permission-result { - @extend %header--24; - - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - text-align: center; - color: $Black-100; - - &__icons { - display: flex; - } - - &__center-icon { - display: flex; - position: relative; - justify-content: center; - align-items: center; - font-size: 12px; - } - - 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; - position: absolute; - } - - &__reject { - position: absolute; - background: white; - display: flex; - justify-content: center; - align-items: center; - - i { - color: #D73A49; - transform: scale(3); - } - } - - &__identicon, .icon-with-fallback__identicon { - width: 32px; - height: 32px; - - &--default { - background-color: #777A87; - color: white; - width: 64px; - height: 64px; - border-radius: 32px; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - } - } - - &__identicon-container, .icon-with-fallback__identicon-container { - height: auto; - position: relative; - display: flex; - justify-content: center; - align-items: center; - height: 64px; - width: 64px; - } - - &__identicon-border, .icon-with-fallback__identicon-border { - height: 64px; - width: 64px; - border-radius: 50%; - border: 1px solid white; - background: #FFFFFF; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); - } - - &__identicon-border { - display: flex; - justify-content: center; - align-items: center; - } - - .icon-with-fallback__identicon-border { - position: absolute; - } -} 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 db7a225b3..c15e93a87 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 @@ -1,9 +1,7 @@ import PropTypes from 'prop-types' import React, { PureComponent } from 'react' -import IconWithFallBack from '../../../ui/icon-with-fallback' import PermissionsConnectHeader from '../../permissions-connect-header' import Tooltip from '../../../ui/tooltip-v2' -import classnames from 'classnames' export default class PermissionPageContainerContent extends PureComponent { @@ -13,13 +11,9 @@ export default class PermissionPageContainerContent extends PureComponent { onPermissionToggle: PropTypes.func.isRequired, selectedIdentities: PropTypes.array, allIdentitiesSelected: PropTypes.bool, - redirect: PropTypes.bool, - permissionRejected: PropTypes.bool, } static defaultProps = { - redirect: null, - permissionRejected: null, selectedIdentities: [], allIdentitiesSelected: false, } @@ -28,39 +22,6 @@ export default class PermissionPageContainerContent extends PureComponent { t: PropTypes.func, } - renderBrokenLine () { - return ( - - - - ) - } - - renderRedirect () { - const { t } = this.context - const { permissionRejected, domainMetadata } = this.props - return ( -
- { permissionRejected ? t('cancelling') : t('connecting') } -
- -
- { permissionRejected - ? - : - } - { this.renderBrokenLine() } -
-
-
- -
-
-
-
- ) - } - renderRequestedPermissions () { const { selectedPermissions, onPermissionToggle, @@ -134,14 +95,10 @@ export default class PermissionPageContainerContent extends PureComponent { } getTitle () { - const { domainMetadata, redirect, permissionRejected, selectedIdentities, allIdentitiesSelected } = this.props + const { domainMetadata, selectedIdentities, allIdentitiesSelected } = this.props const { t } = this.context - if (redirect && permissionRejected) { - return t('cancelledConnectionWithMetaMask') - } else if (redirect) { - return t('connectingWithMetaMask') - } else if (domainMetadata.extensionId) { + if (domainMetadata.extensionId) { return t('externalExtension', [domainMetadata.extensionId]) } else if (allIdentitiesSelected) { return t( @@ -166,36 +123,27 @@ export default class PermissionPageContainerContent extends PureComponent { } render () { - const { domainMetadata, redirect } = this.props + const { domainMetadata } = this.props const { t } = this.context const title = this.getTitle() return ( -
- { !redirect - ? ( -
- -
- { this.renderRequestedPermissions() } -
-
- ) - : this.renderRedirect() - } +
+
+ +
+ { this.renderRequestedPermissions() } +
+
) } 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 320c28be4..3020b82b5 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 @@ -13,15 +13,11 @@ export default class PermissionPageContainer extends Component { selectedIdentities: PropTypes.array, allIdentitiesSelected: PropTypes.bool, request: PropTypes.object, - redirect: PropTypes.bool, - permissionRejected: PropTypes.bool, requestMetadata: PropTypes.object, targetDomainMetadata: PropTypes.object.isRequired, } static defaultProps = { - redirect: null, - permissionRejected: null, request: {}, requestMetadata: {}, selectedIdentities: [], @@ -116,8 +112,6 @@ export default class PermissionPageContainer extends Component { requestMetadata, targetDomainMetadata, selectedIdentities, - redirect, - permissionRejected, allIdentitiesSelected, } = this.props @@ -129,27 +123,20 @@ export default class PermissionPageContainer extends Component { selectedPermissions={this.state.selectedPermissions} onPermissionToggle={this.onPermissionToggle} selectedIdentities={selectedIdentities} - redirect={redirect} - permissionRejected={permissionRejected} allIdentitiesSelected={allIdentitiesSelected} /> - { !redirect - ? ( -
- - this.onCancel()} - cancelText={this.context.t('cancel')} - onSubmit={() => this.onSubmit()} - submitText={this.context.t('connect')} - submitButtonType="confirm" - buttonSizeLarge={false} - /> -
- ) - : null - } +
+ + this.onCancel()} + cancelText={this.context.t('cancel')} + onSubmit={() => this.onSubmit()} + submitText={this.context.t('connect')} + submitButtonType="confirm" + buttonSizeLarge={false} + /> +
) } diff --git a/ui/app/components/app/permissions-connect-header/permissions-connect-header.component.js b/ui/app/components/app/permissions-connect-header/permissions-connect-header.component.js index 456295536..e32f8a0b1 100644 --- a/ui/app/components/app/permissions-connect-header/permissions-connect-header.component.js +++ b/ui/app/components/app/permissions-connect-header/permissions-connect-header.component.js @@ -1,17 +1,18 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' -import IconWithFallBack from '../../ui/icon-with-fallback' +import SiteIcon from '../../ui/site-icon' export default class PermissionsConnectHeader extends Component { static propTypes = { icon: PropTypes.string, - iconName: PropTypes.string.isRequired, + iconName: PropTypes.string, headerTitle: PropTypes.node, headerText: PropTypes.string, } static defaultProps = { icon: null, + iconName: '', headerTitle: '', headerText: '', } @@ -21,7 +22,7 @@ export default class PermissionsConnectHeader extends Component { return (
- +
{iconName}
) diff --git a/ui/app/components/app/token-cell/token-cell.js b/ui/app/components/app/token-cell/token-cell.js index 87b3210a8..cf23b51ac 100644 --- a/ui/app/components/app/token-cell/token-cell.js +++ b/ui/app/components/app/token-cell/token-cell.js @@ -1,47 +1,18 @@ import classnames from 'classnames' import PropTypes from 'prop-types' import React from 'react' -import { conversionUtil, multiplyCurrencies } from '../../../helpers/utils/conversion-util' import AssetListItem from '../asset-list-item' import { useSelector } from 'react-redux' -import { getTokenExchangeRates, getConversionRate, getCurrentCurrency, getSelectedAddress } from '../../../selectors' +import { getSelectedAddress } from '../../../selectors' import { useI18nContext } from '../../../hooks/useI18nContext' -import { formatCurrency } from '../../../helpers/utils/confirm-tx.util' +import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount' + export default function TokenCell ({ address, outdatedBalance, symbol, string, image, onClick }) { - const contractExchangeRates = useSelector(getTokenExchangeRates) - const conversionRate = useSelector(getConversionRate) - const currentCurrency = useSelector(getCurrentCurrency) const userAddress = useSelector(getSelectedAddress) const t = useI18nContext() - let currentTokenToFiatRate - let currentTokenInFiat - let formattedFiat = '' - - - // if the conversionRate is 0 eg: currently unknown - // or the contract exchange rate is currently unknown - // the effective currentTokenToFiatRate is 0 and erroneous. - // Skipping this entire block will result in fiat not being - // shown to the user, instead of a fiat value of 0 for a non-zero - // token amount. - if (conversionRate > 0 && contractExchangeRates[address]) { - currentTokenToFiatRate = multiplyCurrencies( - contractExchangeRates[address], - conversionRate - ) - currentTokenInFiat = conversionUtil(string, { - fromNumericBase: 'dec', - fromCurrency: symbol, - toCurrency: currentCurrency.toUpperCase(), - numberOfDecimals: 2, - conversionRate: currentTokenToFiatRate, - }) - formattedFiat = `${formatCurrency(currentTokenInFiat, currentCurrency)} ${currentCurrency.toUpperCase()}` - } - - const showFiat = Boolean(currentTokenInFiat) && currentCurrency.toUpperCase() !== symbol + const formattedFiat = useTokenFiatAmount(address, string, symbol) const warning = outdatedBalance ? ( @@ -68,7 +39,7 @@ export default function TokenCell ({ address, outdatedBalance, symbol, string, i tokenImage={image} warning={warning} primary={`${string || 0} ${symbol}`} - secondary={showFiat ? formattedFiat : undefined} + secondary={formattedFiat} /> ) diff --git a/ui/app/components/app/token-cell/token-cell.test.js b/ui/app/components/app/token-cell/token-cell.test.js index fba52b602..2b239dab1 100644 --- a/ui/app/components/app/token-cell/token-cell.test.js +++ b/ui/app/components/app/token-cell/token-cell.test.js @@ -20,6 +20,12 @@ describe('Token Cell', function () { '0xAnotherToken': 0.015, }, conversionRate: 7.00, + preferences: {}, + provider: { + chainId: '1', + ticker: 'ETH', + type: 'mainnet', + }, }, appState: { sidebar: { diff --git a/ui/app/components/app/token-list/token-list.js b/ui/app/components/app/token-list/token-list.js index cc0a4acaf..591dbe33a 100644 --- a/ui/app/components/app/token-list/token-list.js +++ b/ui/app/components/app/token-list/token-list.js @@ -1,6 +1,5 @@ import React from 'react' import PropTypes from 'prop-types' -import contracts from 'eth-contract-metadata' import { isEqual } from 'lodash' import TokenCell from '../token-cell' @@ -10,15 +9,6 @@ import { useSelector } from 'react-redux' import { getAssetImages } from '../../../selectors' import { getTokens } from '../../../ducks/metamask/metamask' -const defaultTokens = [] -for (const address in contracts) { - const contract = contracts[address] - if (contract.erc20) { - contract.address = address - defaultTokens.push(contract) - } -} - export default function TokenList ({ onTokenClick }) { const t = useI18nContext() const assetImages = useSelector(getAssetImages) diff --git a/ui/app/components/app/transaction-icon/index.js b/ui/app/components/app/transaction-icon/index.js new file mode 100644 index 000000000..d41970a54 --- /dev/null +++ b/ui/app/components/app/transaction-icon/index.js @@ -0,0 +1 @@ +export { default } from './transaction-icon' diff --git a/ui/app/components/app/transaction-icon/transaction-icon.js b/ui/app/components/app/transaction-icon/transaction-icon.js new file mode 100644 index 000000000..24f4f4715 --- /dev/null +++ b/ui/app/components/app/transaction-icon/transaction-icon.js @@ -0,0 +1,58 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Approve from '../../ui/icon/approve-icon.component' +import Interaction from '../../ui/icon/interaction-icon.component' +import Receive from '../../ui/icon/receive-icon.component' +import Send from '../../ui/icon/send-icon.component' +import Sign from '../../ui/icon/sign-icon.component' +import { + TRANSACTION_CATEGORY_APPROVAL, + TRANSACTION_CATEGORY_SIGNATURE_REQUEST, + TRANSACTION_CATEGORY_INTERACTION, + TRANSACTION_CATEGORY_SEND, + TRANSACTION_CATEGORY_RECEIVE, + UNAPPROVED_STATUS, + FAILED_STATUS, + REJECTED_STATUS, + CANCELLED_STATUS, + DROPPED_STATUS, + SUBMITTED_STATUS, + APPROVED_STATUS, +} from '../../../helpers/constants/transactions' + + +const ICON_MAP = { + [TRANSACTION_CATEGORY_APPROVAL]: Approve, + [TRANSACTION_CATEGORY_INTERACTION]: Interaction, + [TRANSACTION_CATEGORY_SEND]: Send, + [TRANSACTION_CATEGORY_SIGNATURE_REQUEST]: Sign, + [TRANSACTION_CATEGORY_RECEIVE]: Receive, +} + +const FAIL_COLOR = '#D73A49' +const PENDING_COLOR = '#6A737D' +const OK_COLOR = '#2F80ED' + +const COLOR_MAP = { + [SUBMITTED_STATUS]: PENDING_COLOR, + [UNAPPROVED_STATUS]: PENDING_COLOR, + [APPROVED_STATUS]: PENDING_COLOR, + [FAILED_STATUS]: FAIL_COLOR, + [REJECTED_STATUS]: FAIL_COLOR, + [CANCELLED_STATUS]: FAIL_COLOR, + [DROPPED_STATUS]: FAIL_COLOR, +} + +export default function TransactionIcon ({ status, category }) { + + const color = COLOR_MAP[status] || OK_COLOR + + const Icon = ICON_MAP[category] + + return +} + +TransactionIcon.propTypes = { + status: PropTypes.string.isRequired, + category: PropTypes.string.isRequired, +} diff --git a/ui/app/components/app/transaction-list-item/index.scss b/ui/app/components/app/transaction-list-item/index.scss index 3f7af9586..71282e718 100644 --- a/ui/app/components/app/transaction-list-item/index.scss +++ b/ui/app/components/app/transaction-list-item/index.scss @@ -1,10 +1,4 @@ .transaction-list-item { - cursor: pointer; - - &:hover { - background-color: $Grey-000; - } - &__primary-currency { color: $Black-100; } @@ -15,29 +9,14 @@ color: $Grey-500; } - &--pending { + &--unconfirmed { color: $Grey-500; } - &--pending &__primary-currency { + &--unconfirmed &__primary-currency { color: $Grey-500; } - &__status { - &--unapproved { - color: $flamingo; - } - &--failed { - color: $valencia; - } - &--cancelled { - color: $valencia; - } - &--queued { - color: $Grey-500; - } - } - &__pending-actions { padding-top: 12px; display: flex; diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index e22345916..a92880c52 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -3,11 +3,7 @@ import PropTypes from 'prop-types' import classnames from 'classnames' import ListItem from '../../ui/list-item' import { useTransactionDisplayData } from '../../../hooks/useTransactionDisplayData' -import Approve from '../../ui/icon/approve-icon.component' -import Interaction from '../../ui/icon/interaction-icon.component' -import Receive from '../../ui/icon/receive-icon.component' import Preloader from '../../ui/icon/preloader' -import Send from '../../ui/icon/send-icon.component' import { useI18nContext } from '../../../hooks/useI18nContext' import { useCancelTransaction } from '../../../hooks/useCancelTransaction' import { useRetryTransaction } from '../../../hooks/useRetryTransaction' @@ -17,17 +13,17 @@ import TransactionListItemDetails from '../transaction-list-item-details' import { useHistory } from 'react-router-dom' import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes' import { - TRANSACTION_CATEGORY_APPROVAL, TRANSACTION_CATEGORY_SIGNATURE_REQUEST, - TRANSACTION_CATEGORY_INTERACTION, - TRANSACTION_CATEGORY_SEND, - TRANSACTION_CATEGORY_RECEIVE, UNAPPROVED_STATUS, FAILED_STATUS, - CANCELLED_STATUS, + DROPPED_STATUS, + REJECTED_STATUS, } from '../../../helpers/constants/transactions' import { useShouldShowSpeedUp } from '../../../hooks/useShouldShowSpeedUp' -import Sign from '../../ui/icon/sign-icon.component' +import TransactionStatus from '../transaction-status/transaction-status.component' +import TransactionIcon from '../transaction-icon' +import { useTransactionTimeRemaining } from '../../../hooks/useTransactionTimeRemaining' +import IconWithLabel from '../../ui/icon-with-label' export default function TransactionListItem ({ transactionGroup, isEarliestNonce = false }) { @@ -36,8 +32,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce const { hasCancelled } = transactionGroup const [showDetails, setShowDetails] = useState(false) - const { initialTransaction: { id } } = transactionGroup - + const { initialTransaction: { id }, primaryTransaction: { err, submittedTime, gasPrice } } = transactionGroup const [cancelEnabled, cancelTransaction] = useCancelTransaction(transactionGroup) const retryTransaction = useRetryTransaction(transactionGroup) const shouldShowSpeedUp = useShouldShowSpeedUp(transactionGroup, isEarliestNonce) @@ -55,50 +50,15 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce senderAddress, } = useTransactionDisplayData(transactionGroup) - const isApprove = category === TRANSACTION_CATEGORY_APPROVAL - const isSignatureReq = category === TRANSACTION_CATEGORY_SIGNATURE_REQUEST - const isInteraction = category === TRANSACTION_CATEGORY_INTERACTION - const isSend = category === TRANSACTION_CATEGORY_SEND - const isReceive = category === TRANSACTION_CATEGORY_RECEIVE - const isUnapproved = status === UNAPPROVED_STATUS - const isFailed = status === FAILED_STATUS - const isCancelled = status === CANCELLED_STATUS - - const color = isFailed ? '#D73A49' : '#2F80ED' + const timeRemaining = useTransactionTimeRemaining(isPending, isEarliestNonce, submittedTime, gasPrice) - let Icon - if (isApprove) { - Icon = Approve - } else if (isSend) { - Icon = Send - } else if (isReceive) { - Icon = Receive - } else if (isInteraction) { - Icon = Interaction - } else if (isSignatureReq) { - Icon = Sign - } - let subtitleStatus = {date} · - if (isUnapproved) { - subtitleStatus = ( - {t('unapproved')} · - ) - } else if (isFailed) { - subtitleStatus = ( - {t('failed')} · - ) - } else if (isCancelled) { - subtitleStatus = ( - {t('cancelled')} · - ) - } else if (isPending && !isEarliestNonce) { - subtitleStatus = ( - {t('queued')} · - ) - } + const isSignatureReq = category === TRANSACTION_CATEGORY_SIGNATURE_REQUEST + const isUnapproved = status === UNAPPROVED_STATUS - const className = classnames('transaction-list-item', { 'transaction-list-item--pending': isPending }) + const className = classnames('transaction-list-item', { + 'transaction-list-item--unconfirmed': isPending || [FAILED_STATUS, DROPPED_STATUS, REJECTED_STATUS].includes(status), + }) const toggleShowDetails = useCallback(() => { if (isUnapproved) { @@ -106,7 +66,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce return } setShowDetails((prev) => !prev) - }, [isUnapproved, id]) + }, [isUnapproved, history, id]) const cancelButton = useMemo(() => { const cancelButton = ( @@ -131,7 +91,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce ) : cancelButton - }, [cancelEnabled, cancelTransaction, hasCancelled]) + }, [isPending, t, isUnapproved, cancelEnabled, cancelTransaction, hasCancelled]) const speedUpButton = useMemo(() => { if (!shouldShowSpeedUp || !isPending || isUnapproved) { @@ -147,7 +107,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce { t('speedUp') } ) - }, [shouldShowSpeedUp, isPending, retryTransaction]) + }, [shouldShowSpeedUp, isUnapproved, t, isPending, retryTransaction]) return ( <> @@ -156,14 +116,22 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce className={className} title={title} titleIcon={!isUnapproved && isPending && isEarliestNonce && ( - } + label={timeRemaining} /> )} - icon={} + icon={} subtitle={subtitle} - subtitleStatus={subtitleStatus} + subtitleStatus={( + + )} rightContent={!isSignatureReq && ( <>

{primaryCurrency}

@@ -184,7 +152,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce senderAddress={senderAddress} recipientAddress={recipientAddress} onRetry={retryTransaction} - showRetry={isFailed} + showRetry={status === FAILED_STATUS} showSpeedUp={shouldShowSpeedUp} isEarliestNonce={isEarliestNonce} onCancel={cancelTransaction} diff --git a/ui/app/components/app/transaction-list/transaction-list.component.js b/ui/app/components/app/transaction-list/transaction-list.component.js index 46b8b45de..dac902add 100644 --- a/ui/app/components/app/transaction-list/transaction-list.component.js +++ b/ui/app/components/app/transaction-list/transaction-list.component.js @@ -12,6 +12,7 @@ import * as actions from '../../../ducks/gas/gas.duck' import { useI18nContext } from '../../../hooks/useI18nContext' import TransactionListItem from '../transaction-list-item' import Button from '../../ui/button' +import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions' const PAGE_INCREMENT = 10 @@ -19,7 +20,23 @@ const getTransactionGroupRecipientAddressFilter = (recipientAddress) => { return ({ initialTransaction: { txParams } }) => txParams && txParams.to === recipientAddress } -export default function TransactionList ({ tokenAddress }) { +const tokenTransactionFilter = ({ + initialTransaction: { + transactionCategory, + }, +}) => !TOKEN_CATEGORY_HASH[transactionCategory] + + +const getFilteredTransactionGroups = (transactionGroups, hideTokenTransactions, tokenAddress) => { + if (hideTokenTransactions) { + return transactionGroups.filter(tokenTransactionFilter) + } else if (tokenAddress) { + return transactionGroups.filter(getTransactionGroupRecipientAddressFilter(tokenAddress)) + } + return transactionGroups +} + +export default function TransactionList ({ hideTokenTransactions, tokenAddress }) { const [limit, setLimit] = useState(PAGE_INCREMENT) const t = useI18nContext() @@ -29,22 +46,12 @@ export default function TransactionList ({ tokenAddress }) { const { transactionTime: transactionTimeFeatureActive } = useSelector(getFeatureFlags) const pendingTransactions = useMemo( - () => ( - tokenAddress && tokenAddress.startsWith('0x') - ? unfilteredPendingTransactions - .filter(getTransactionGroupRecipientAddressFilter(tokenAddress)) - : unfilteredPendingTransactions - ), - [unfilteredPendingTransactions, tokenAddress] + () => getFilteredTransactionGroups(unfilteredPendingTransactions, hideTokenTransactions, tokenAddress), + [hideTokenTransactions, tokenAddress, unfilteredPendingTransactions] ) const completedTransactions = useMemo( - () => ( - tokenAddress && tokenAddress.startsWith('0x') - ? unfilteredCompletedTransactions - .filter(getTransactionGroupRecipientAddressFilter(tokenAddress)) - : unfilteredCompletedTransactions - ), - [unfilteredCompletedTransactions, tokenAddress] + () => getFilteredTransactionGroups(unfilteredCompletedTransactions, hideTokenTransactions, tokenAddress), + [hideTokenTransactions, tokenAddress, unfilteredCompletedTransactions] ) const { fetchGasEstimates, fetchBasicGasAndTimeEstimates } = useMemo(() => ({ @@ -122,9 +129,11 @@ export default function TransactionList ({ tokenAddress }) { } TransactionList.propTypes = { + hideTokenTransactions: PropTypes.bool, tokenAddress: PropTypes.string, } TransactionList.defaultProps = { + hideTokenTransactions: false, tokenAddress: undefined, } diff --git a/ui/app/components/app/transaction-status/index.scss b/ui/app/components/app/transaction-status/index.scss index 99884d28c..13dcd6c30 100644 --- a/ui/app/components/app/transaction-status/index.scss +++ b/ui/app/components/app/transaction-status/index.scss @@ -1,52 +1,24 @@ .transaction-status { - height: 26px; - width: 84px; - border-radius: 4px; - background-color: #f0f0f0; - color: #5e6064; - font-size: .625rem; - text-transform: uppercase; - display: flex; - justify-content: center; - align-items: center; - - @media screen and (max-width: $break-small) { - height: 16px; - min-width: 72px; - font-size: 10px; - padding: 0 12px; + display: inline; + &--unapproved { + color: $Orange-500; } - - &--confirmed { - background-color: #eafad7; - color: #609a1c; - - .transaction-status__transaction-count { - border: 1px solid #609a1c; - } + &--failed { + color: $Red-500; } - - &--approved, &--submitted { - background-color: #FFF2DB; - color: #CA810A; - - .transaction-status__transaction-count { - border: 1px solid #CA810A; - } + &--cancelled { + color: $Red-500; } - - &--failed { - background: lighten($monzo, 56%); - color: $monzo; - - .transaction-status__transaction-count { - border: 1px solid $monzo; - } + &--dropped { + color: $Red-500; + } + &--rejected { + color: $Red-500; + } + &--pending { + color: $Orange-500; } - - &__pending-spinner { - height: 16px; - width: 16px; - margin-right: 6px; + &--queued { + color: $Grey-500; } } diff --git a/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js b/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js index 510950248..939090875 100644 --- a/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js +++ b/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js @@ -1,33 +1,80 @@ import React from 'react' import assert from 'assert' import { mount } from 'enzyme' +import sinon from 'sinon' +import * as i18nHook from '../../../../hooks/useI18nContext' import TransactionStatus from '../transaction-status.component' import Tooltip from '../../../ui/tooltip-v2' describe('TransactionStatus Component', function () { - it('should render APPROVED properly', function () { + before(function () { + sinon.stub(i18nHook, 'useI18nContext').returns((str) => str.toUpperCase()) + }) + + it('should render CONFIRMED properly', function () { const wrapper = mount( , - { context: { t: (str) => str.toUpperCase() } } + status="confirmed" + date="June 1" + /> ) assert.ok(wrapper) - assert.equal(wrapper.text(), 'APPROVED') + assert.equal(wrapper.text(), 'June 1 · ') + }) + + it('should render PENDING properly when status is APPROVED', function () { + const wrapper = mount( + + ) + + assert.ok(wrapper) + assert.equal(wrapper.text(), 'PENDING · ') assert.equal(wrapper.find(Tooltip).props().title, 'test-title') }) - it('should render SUBMITTED properly', function () { + it('should render PENDING properly', function () { const wrapper = mount( , - { context: { t: (str) => str.toUpperCase() } } + date="June 1" + status="submitted" + isEarliestNonce + /> ) assert.ok(wrapper) - assert.equal(wrapper.text(), 'PENDING') + assert.equal(wrapper.text(), 'PENDING · ') + }) + + it('should render QUEUED properly', function () { + const wrapper = mount( + + ) + + assert.ok(wrapper) + assert.ok(wrapper.find('.transaction-status--queued').length, 'queued className not found') + assert.equal(wrapper.text(), 'QUEUED · ') + }) + + it('should render UNAPPROVED properly', function () { + const wrapper = mount( + + ) + + assert.ok(wrapper) + assert.ok(wrapper.find('.transaction-status--unapproved').length, 'unapproved className not found') + assert.equal(wrapper.text(), 'UNAPPROVED · ') + }) + + after(function () { + sinon.restore() }) }) diff --git a/ui/app/components/app/transaction-status/transaction-status.component.js b/ui/app/components/app/transaction-status/transaction-status.component.js index a97b79bde..c525f7484 100644 --- a/ui/app/components/app/transaction-status/transaction-status.component.js +++ b/ui/app/components/app/transaction-status/transaction-status.component.js @@ -1,66 +1,78 @@ -import React, { PureComponent } from 'react' +import React from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import Tooltip from '../../ui/tooltip-v2' -import Spinner from '../../ui/spinner' import { UNAPPROVED_STATUS, REJECTED_STATUS, - APPROVED_STATUS, - SIGNED_STATUS, SUBMITTED_STATUS, CONFIRMED_STATUS, FAILED_STATUS, DROPPED_STATUS, CANCELLED_STATUS, + APPROVED_STATUS, + SIGNED_STATUS, } from '../../../helpers/constants/transactions' +import { useI18nContext } from '../../../hooks/useI18nContext' + +const QUEUED_PSEUDO_STATUS = 'queued' +const PENDING_PSEUDO_STATUS = 'pending' + +/** + * A note about status logic for this component: + * Approved, Signed and Submitted statuses are all treated, effectively + * as pending. Transactions are only approved or signed for less than a + * second, usually, and ultimately should be rendered in the UI no + * differently than a pending transaction. + * + * Confirmed transactions are not especially highlighted except that their + * status label will be the date the transaction was finalized. + */ +const pendingStatusHash = { + [SUBMITTED_STATUS]: PENDING_PSEUDO_STATUS, + [APPROVED_STATUS]: PENDING_PSEUDO_STATUS, + [SIGNED_STATUS]: PENDING_PSEUDO_STATUS, +} const statusToClassNameHash = { [UNAPPROVED_STATUS]: 'transaction-status--unapproved', [REJECTED_STATUS]: 'transaction-status--rejected', - [APPROVED_STATUS]: 'transaction-status--approved', - [SIGNED_STATUS]: 'transaction-status--signed', - [SUBMITTED_STATUS]: 'transaction-status--submitted', - [CONFIRMED_STATUS]: 'transaction-status--confirmed', [FAILED_STATUS]: 'transaction-status--failed', [DROPPED_STATUS]: 'transaction-status--dropped', - [CANCELLED_STATUS]: 'transaction-status--failed', + [CANCELLED_STATUS]: 'transaction-status--cancelled', + [QUEUED_PSEUDO_STATUS]: 'transaction-status--queued', + [PENDING_PSEUDO_STATUS]: 'transaction-status--pending', } -const statusToTextHash = { - [SUBMITTED_STATUS]: 'pending', -} - -export default class TransactionStatus extends PureComponent { - static defaultProps = { - title: null, +export default function TransactionStatus ({ status, date, error, isEarliestNonce, className }) { + const t = useI18nContext() + const tooltipText = error?.rpc?.message || error?.message + let statusKey = status + if (pendingStatusHash[status]) { + statusKey = isEarliestNonce ? PENDING_PSEUDO_STATUS : QUEUED_PSEUDO_STATUS } - static contextTypes = { - t: PropTypes.func, - } + const statusText = statusKey === CONFIRMED_STATUS ? date : t(statusKey) - static propTypes = { - statusKey: PropTypes.string, - className: PropTypes.string, - title: PropTypes.string, - } - - render () { - const { className, statusKey, title } = this.props - const statusText = this.context.t(statusToTextHash[statusKey] || statusKey) + return ( + + + { statusText } + + {' · '} + + ) +} - return ( -
- { statusToTextHash[statusKey] === 'pending' ? : null } - - { statusText } - -
- ) - } +TransactionStatus.propTypes = { + status: PropTypes.string, + className: PropTypes.string, + date: PropTypes.string, + error: PropTypes.object, + isEarliestNonce: PropTypes.bool, } diff --git a/ui/app/components/app/transaction-time-remaining/index.js b/ui/app/components/app/transaction-time-remaining/index.js deleted file mode 100644 index 87c6821d8..000000000 --- a/ui/app/components/app/transaction-time-remaining/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './transaction-time-remaining.container' diff --git a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js deleted file mode 100644 index c9598d69b..000000000 --- a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js +++ /dev/null @@ -1,52 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import { calcTransactionTimeRemaining } from './transaction-time-remaining.util' - -export default class TransactionTimeRemaining extends PureComponent { - static propTypes = { - className: PropTypes.string, - initialTimeEstimate: PropTypes.number, - submittedTime: PropTypes.number, - } - - constructor (props) { - super(props) - const { initialTimeEstimate, submittedTime } = props - this.state = { - timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime), - } - this.interval = setInterval( - () => this.setState({ timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) }), - 1000 - ) - } - - componentDidUpdate (prevProps) { - const { initialTimeEstimate, submittedTime } = this.props - if (initialTimeEstimate !== prevProps.initialTimeEstimate) { - clearInterval(this.interval) - const calcedTimeRemaining = calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) - this.setState({ timeRemaining: calcedTimeRemaining }) - this.interval = setInterval( - () => this.setState({ timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) }), - 1000 - ) - } - } - - componentWillUnmount () { - clearInterval(this.interval) - } - - render () { - const { className } = this.props - const { timeRemaining } = this.state - - return ( -
- { timeRemaining } -
- - ) - } -} diff --git a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js deleted file mode 100644 index 754d84991..000000000 --- a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js +++ /dev/null @@ -1,33 +0,0 @@ -import { connect } from 'react-redux' -import TransactionTimeRemaining from './transaction-time-remaining.component' -import { - getEstimatedGasPrices, - getEstimatedGasTimes, -} from '../../../selectors' -import { getRawTimeEstimateData } from '../../../helpers/utils/gas-time-estimates.util' -import { hexWEIToDecGWEI } from '../../../helpers/utils/conversions.util' - -const mapStateToProps = (state, ownProps) => { - const { transaction } = ownProps - const { gasPrice: currentGasPrice } = transaction.txParams - const customGasPrice = calcCustomGasPrice(currentGasPrice) - const gasPrices = getEstimatedGasPrices(state) - const estimatedTimes = getEstimatedGasTimes(state) - - const { - newTimeEstimate: initialTimeEstimate, - } = getRawTimeEstimateData(customGasPrice, gasPrices, estimatedTimes) - - const submittedTime = transaction.submittedTime - - return { - initialTimeEstimate, - submittedTime, - } -} - -export default connect(mapStateToProps)(TransactionTimeRemaining) - -function calcCustomGasPrice (customGasPriceInHex) { - return Number(hexWEIToDecGWEI(customGasPriceInHex)) -} diff --git a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js deleted file mode 100644 index 0ba81edfc..000000000 --- a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js +++ /dev/null @@ -1,13 +0,0 @@ -import { formatTimeEstimate } from '../../../helpers/utils/gas-time-estimates.util' - -export function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) { - const currentTime = (new Date()).getTime() - const timeElapsedSinceSubmission = (currentTime - submittedTime) / 1000 - const timeRemainingOnEstimate = initialTimeEstimate - timeElapsedSinceSubmission - - const renderingTimeRemainingEstimate = timeRemainingOnEstimate < 30 - ? '< 30 s' - : formatTimeEstimate(timeRemainingOnEstimate) - - return renderingTimeRemainingEstimate -} diff --git a/ui/app/components/app/wallet-overview/eth-overview.js b/ui/app/components/app/wallet-overview/eth-overview.js index c9b142af2..d232b0af8 100644 --- a/ui/app/components/app/wallet-overview/eth-overview.js +++ b/ui/app/components/app/wallet-overview/eth-overview.js @@ -15,6 +15,7 @@ import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' import { showModal } from '../../../store/actions' import { isBalanceCached, getSelectedAccount, getShouldShowFiat } from '../../../selectors/selectors' +import PaperAirplane from '../../ui/icon/paper-airplane-icon' const EthOverview = ({ className }) => { const dispatch = useDispatch() @@ -78,18 +79,21 @@ const EthOverview = ({ className }) => { buttons={( <>
)} buttons={( ) @@ -48,6 +49,7 @@ Button.propTypes = { rounded: PropTypes.bool, className: PropTypes.string, children: PropTypes.node, + icon: PropTypes.node, } Button.defaultProps = { diff --git a/ui/app/components/ui/button/buttons.scss b/ui/app/components/ui/button/buttons.scss index be2295a9f..a28dfa4d2 100644 --- a/ui/app/components/ui/button/buttons.scss +++ b/ui/app/components/ui/button/buttons.scss @@ -66,6 +66,12 @@ $warning-light-orange: #F8B588; .button { @extend %button; + + &__icon { + display: flex; + align-items: center; + margin-right: 4px; + } } .btn-secondary { diff --git a/ui/app/components/ui/editable-label.js b/ui/app/components/ui/editable-label.js index 553965b1c..d1788472e 100644 --- a/ui/app/components/ui/editable-label.js +++ b/ui/app/components/ui/editable-label.js @@ -57,7 +57,7 @@ class EditableLabel extends Component {
{this.state.value}
), (
- this.setState({ isEditing: true })} /> + this.setState({ isEditing: true })} />
)] } diff --git a/ui/app/components/ui/icon-border/icon-border.js b/ui/app/components/ui/icon-border/icon-border.js new file mode 100644 index 000000000..5911cf918 --- /dev/null +++ b/ui/app/components/ui/icon-border/icon-border.js @@ -0,0 +1,16 @@ +import React from 'react' +import PropTypes from 'prop-types' + +export default function IconBorder ({ children, size }) { + const borderStyle = { height: `${size}px`, width: `${size}px` } + return ( +
+ { children } +
+ ) +} + +IconBorder.propTypes = { + children: PropTypes.node.isRequired, + size: PropTypes.number.isRequired, +} diff --git a/ui/app/components/ui/icon-border/icon-border.scss b/ui/app/components/ui/icon-border/icon-border.scss new file mode 100644 index 000000000..6a718c83a --- /dev/null +++ b/ui/app/components/ui/icon-border/icon-border.scss @@ -0,0 +1,10 @@ + +.icon-border { + border-radius: 50%; + border: 1px solid #F2F3F4; + background: #FFFFFF; + + display: flex; + justify-content: center; + align-items: center; +} diff --git a/ui/app/components/ui/icon-border/index.js b/ui/app/components/ui/icon-border/index.js new file mode 100644 index 000000000..dea3f116e --- /dev/null +++ b/ui/app/components/ui/icon-border/index.js @@ -0,0 +1 @@ +export { default } from './icon-border' 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 13b3e93d7..898861468 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 @@ -5,6 +5,7 @@ export default class IconWithFallback extends PureComponent { static propTypes = { icon: PropTypes.string, name: PropTypes.string, + size: PropTypes.number.isRequired, } static defaultProps = { @@ -17,26 +18,21 @@ export default class IconWithFallback extends PureComponent { } render () { - const { icon, name } = this.props + const { icon, name, size } = this.props + const style = { height: `${size}px`, width: `${size}px` } - return ( -
-
- { !this.state.iconError && icon - ? ( - this.setState({ iconError: true })} - /> - ) - : ( - - { name.length ? name.charAt(0).toUpperCase() : '' } - - ) - } -
- ) + return !this.state.iconError && icon + ? ( + this.setState({ iconError: true })} + src={icon} + style={style} + /> + ) + : ( + + { name.length ? name.charAt(0).toUpperCase() : '' } + + ) } } diff --git a/ui/app/components/ui/icon-with-fallback/icon-with-fallback.scss b/ui/app/components/ui/icon-with-fallback/icon-with-fallback.scss new file mode 100644 index 000000000..a0cb7cd42 --- /dev/null +++ b/ui/app/components/ui/icon-with-fallback/icon-with-fallback.scss @@ -0,0 +1,5 @@ +.icon-with-fallback { + &__fallback { + color: black; + } +} diff --git a/ui/app/components/ui/icon-with-fallback/index.scss b/ui/app/components/ui/icon-with-fallback/index.scss deleted file mode 100644 index 215ea6138..000000000 --- a/ui/app/components/ui/icon-with-fallback/index.scss +++ /dev/null @@ -1,30 +0,0 @@ -.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 { - position: relative; - width: 24px; - height: 24px; - - &--default { - position: relative; - color: black; - } - } -} diff --git a/ui/app/components/ui/icon-with-label/icon-with-label.js b/ui/app/components/ui/icon-with-label/icon-with-label.js new file mode 100644 index 000000000..9e282611b --- /dev/null +++ b/ui/app/components/ui/icon-with-label/icon-with-label.js @@ -0,0 +1,18 @@ +import React from 'react' +import classnames from 'classnames' +import PropTypes from 'prop-types' + +export default function IconWithLabel ({ icon, label, className }) { + return ( +
+ {icon} + {label && {label}} +
+ ) +} + +IconWithLabel.propTypes = { + icon: PropTypes.node.isRequired, + className: PropTypes.string, + label: PropTypes.string, +} diff --git a/ui/app/components/ui/icon-with-label/index.js b/ui/app/components/ui/icon-with-label/index.js new file mode 100644 index 000000000..10432ac8f --- /dev/null +++ b/ui/app/components/ui/icon-with-label/index.js @@ -0,0 +1 @@ +export { default } from './icon-with-label' diff --git a/ui/app/components/ui/icon-with-label/index.scss b/ui/app/components/ui/icon-with-label/index.scss new file mode 100644 index 000000000..628a54272 --- /dev/null +++ b/ui/app/components/ui/icon-with-label/index.scss @@ -0,0 +1,10 @@ +.icon-with-label { + display: flex; + align-items: center; + + &__label { + font-size: 10px; + margin-left: 4px; + color: $Grey-500; + } +} diff --git a/ui/app/components/ui/icon/icon.stories.js b/ui/app/components/ui/icon/icon.stories.js index 73d25e6fd..ee52a2f90 100644 --- a/ui/app/components/ui/icon/icon.stories.js +++ b/ui/app/components/ui/icon/icon.stories.js @@ -51,3 +51,7 @@ export const preloader = () => ( size={number('size', 40)} /> ) + +export const PaperAirplane = () => ( + +) diff --git a/ui/app/components/ui/icon/paper-airplane-icon.js b/ui/app/components/ui/icon/paper-airplane-icon.js new file mode 100644 index 000000000..552f02d7c --- /dev/null +++ b/ui/app/components/ui/icon/paper-airplane-icon.js @@ -0,0 +1,33 @@ +import React from 'react' +import PropTypes from 'prop-types' + +export default function PaperAirplane ({ size, className, color }) { + return ( + + + + ) +} + +PaperAirplane.defaultProps = { + color: '#FFFFFF', +} + +PaperAirplane.propTypes = { + className: PropTypes.string, + size: PropTypes.number.isRequired, + color: PropTypes.string, +} + diff --git a/ui/app/components/ui/identicon/blockieIdenticon/blockieIdenticon.component.js b/ui/app/components/ui/identicon/blockieIdenticon/blockieIdenticon.component.js index 7ed0d00e1..ae75e5474 100644 --- a/ui/app/components/ui/identicon/blockieIdenticon/blockieIdenticon.component.js +++ b/ui/app/components/ui/identicon/blockieIdenticon/blockieIdenticon.component.js @@ -14,7 +14,7 @@ const BlockieIdenticon = ({ address, diameter }) => { if (updatedDataUrl !== dataUrl) { setDataUrl(updatedDataUrl) } - }) + }, [dataUrl, address]) return ( <> diff --git a/ui/app/components/ui/list-item/index.scss b/ui/app/components/ui/list-item/index.scss index f424bc4c6..b6991ea22 100644 --- a/ui/app/components/ui/list-item/index.scss +++ b/ui/app/components/ui/list-item/index.scss @@ -15,6 +15,11 @@ 'icon sub sub sub sub sub sub sub right right right right' '. actions actions actions actions actions actions actions right right right right'; align-items: start; + cursor: pointer; + + &:hover { + background-color: $Grey-000; + } &__icon { grid-area: icon; @@ -33,12 +38,11 @@ font-size: 16px; line-height: 160%; position: relative; + display: flex; + align-items: center; &-wrap { display: inline-block; - position: absolute; - width: 16px; - height: 16px; margin-left: 8px; } } diff --git a/ui/app/components/ui/list-item/list-item.component.js b/ui/app/components/ui/list-item/list-item.component.js index 6f04c5590..60d395ab3 100644 --- a/ui/app/components/ui/list-item/list-item.component.js +++ b/ui/app/components/ui/list-item/list-item.component.js @@ -26,9 +26,9 @@ export default function ListItem ({ )}

{ title } {titleIcon && ( - +
{titleIcon} - +
)}

diff --git a/ui/app/components/ui/menu/menu.scss b/ui/app/components/ui/menu/menu.scss index 5674aee69..76102c49b 100644 --- a/ui/app/components/ui/menu/menu.scss +++ b/ui/app/components/ui/menu/menu.scss @@ -44,6 +44,7 @@ &__icon { margin-right: 8px; grid-row: 1 / span 2; + color: $Grey-500; } .disconnect-icon { diff --git a/ui/app/components/ui/menu/menu.stories.js b/ui/app/components/ui/menu/menu.stories.js index ec8822d62..c3071c798 100644 --- a/ui/app/components/ui/menu/menu.stories.js +++ b/ui/app/components/ui/menu/menu.stories.js @@ -6,7 +6,7 @@ export default { title: 'Menu', } -export const basic = () => { +export const Basic = () => { return ( { ) } -export const anchored = () => { +export const Anchored = () => { const [anchorElement, setAnchorElement] = useState(null) return ( <> diff --git a/ui/app/components/ui/popover/index.scss b/ui/app/components/ui/popover/index.scss index 1cf453914..1742c7754 100644 --- a/ui/app/components/ui/popover/index.scss +++ b/ui/app/components/ui/popover/index.scss @@ -107,7 +107,7 @@ border-top: 1px solid #D2D8DD; padding: 16px 24px 24px; - & :only-child { + > :only-child { margin: 0 auto; } } diff --git a/ui/app/components/ui/site-icon/index.js b/ui/app/components/ui/site-icon/index.js new file mode 100644 index 000000000..a68d9779d --- /dev/null +++ b/ui/app/components/ui/site-icon/index.js @@ -0,0 +1 @@ +export { default } from './site-icon' diff --git a/ui/app/components/ui/site-icon/site-icon.js b/ui/app/components/ui/site-icon/site-icon.js new file mode 100644 index 000000000..2ddc9f344 --- /dev/null +++ b/ui/app/components/ui/site-icon/site-icon.js @@ -0,0 +1,24 @@ +import React from 'react' +import PropTypes from 'prop-types' +import IconBorder from '../icon-border' +import IconWithFallback from '../icon-with-fallback' + +export default function SiteIcon ({ icon, name, size }) { + const iconSize = Math.floor(size * 0.75) + return ( + + + + ) +} + +SiteIcon.propTypes = { + icon: PropTypes.string, + name: PropTypes.string, + size: PropTypes.number.isRequired, +} + +SiteIcon.defaultProps = { + icon: undefined, + name: undefined, +} diff --git a/ui/app/ducks/alerts/index.js b/ui/app/ducks/alerts/index.js index d1cdd0374..a28d23ecb 100644 --- a/ui/app/ducks/alerts/index.js +++ b/ui/app/ducks/alerts/index.js @@ -1,2 +1 @@ -export { default as switchToConnected } from './switch-to-connected' export { default as unconnectedAccount } from './unconnected-account' diff --git a/ui/app/ducks/alerts/switch-to-connected.js b/ui/app/ducks/alerts/switch-to-connected.js deleted file mode 100644 index fb6d478e2..000000000 --- a/ui/app/ducks/alerts/switch-to-connected.js +++ /dev/null @@ -1,111 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' -import { captureException } from '@sentry/browser' - -import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' -import * as actionConstants from '../../store/actionConstants' -import { setAlertEnabledness, setSelectedAddress } from '../../store/actions' - -// Constants - -export const ALERT_STATE = { - CLOSED: 'CLOSED', - ERROR: 'ERROR', - LOADING: 'LOADING', - OPEN: 'OPEN', -} - -const name = ALERT_TYPES.switchToConnected - -const initialState = { - state: ALERT_STATE.CLOSED, -} - -// Slice (reducer plus auto-generated actions and action creators) - -const slice = createSlice({ - name, - initialState, - reducers: { - disableAlertFailed: (state) => { - state.state = ALERT_STATE.ERROR - }, - disableAlertRequested: (state) => { - state.state = ALERT_STATE.LOADING - }, - disableAlertSucceeded: (state) => { - state.state = ALERT_STATE.CLOSED - }, - dismissAlert: (state) => { - state.state = ALERT_STATE.CLOSED - }, - switchAccountFailed: (state) => { - state.state = ALERT_STATE.ERROR - }, - switchAccountRequested: (state) => { - state.state = ALERT_STATE.LOADING - }, - switchAccountSucceeded: (state) => { - state.state = ALERT_STATE.CLOSED - }, - }, - extraReducers: { - [actionConstants.SELECTED_ADDRESS_CHANGED]: (state) => { - // close the alert if the account is switched while it's open - if (state.state === ALERT_STATE.OPEN) { - state.state = ALERT_STATE.CLOSED - } - }, - }, -}) - -const { actions, reducer } = slice - -export default reducer - -// Selectors - -export const getAlertState = (state) => state[name].state - -export const alertIsOpen = (state) => state[name].state !== ALERT_STATE.CLOSED - -// Actions / action-creators - -const { - disableAlertFailed, - disableAlertRequested, - disableAlertSucceeded, - dismissAlert, - switchAccountFailed, - switchAccountRequested, - switchAccountSucceeded, -} = actions - -export { dismissAlert } - -export const dismissAndDisableAlert = () => { - return async (dispatch) => { - try { - await dispatch(disableAlertRequested()) - await dispatch(setAlertEnabledness(name, false)) - await dispatch(disableAlertSucceeded()) - } catch (error) { - console.error(error) - captureException(error) - await dispatch(disableAlertFailed()) - } - } -} - -export const switchToAccount = (address) => { - return async (dispatch) => { - try { - await dispatch(switchAccountRequested()) - await dispatch(setSelectedAddress(address)) - await dispatch(switchAccountSucceeded()) - } catch (error) { - console.error(error) - captureException(error) - await dispatch(switchAccountFailed()) - } - } -} diff --git a/ui/app/ducks/alerts/unconnected-account.js b/ui/app/ducks/alerts/unconnected-account.js index 9610d5080..397533463 100644 --- a/ui/app/ducks/alerts/unconnected-account.js +++ b/ui/app/ducks/alerts/unconnected-account.js @@ -3,7 +3,11 @@ import { captureException } from '@sentry/browser' import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' import * as actionConstants from '../../store/actionConstants' -import { addPermittedAccount, setAlertEnabledness } from '../../store/actions' +import { + addPermittedAccount, + setAlertEnabledness, + setSelectedAddress, +} from '../../store/actions' import { getOriginOfCurrentTab, getSelectedAddress, @@ -39,9 +43,6 @@ const slice = createSlice({ connectAccountSucceeded: (state) => { state.state = ALERT_STATE.CLOSED }, - dismissAlert: (state) => { - state.state = ALERT_STATE.CLOSED - }, disableAlertFailed: (state) => { state.state = ALERT_STATE.ERROR }, @@ -51,6 +52,18 @@ const slice = createSlice({ disableAlertSucceeded: (state) => { state.state = ALERT_STATE.CLOSED }, + dismissAlert: (state) => { + state.state = ALERT_STATE.CLOSED + }, + switchAccountFailed: (state) => { + state.state = ALERT_STATE.ERROR + }, + switchAccountRequested: (state) => { + state.state = ALERT_STATE.LOADING + }, + switchAccountSucceeded: (state) => { + state.state = ALERT_STATE.CLOSED + }, switchedToUnconnectedAccount: (state) => { state.state = ALERT_STATE.OPEN }, @@ -81,10 +94,13 @@ const { connectAccountFailed, connectAccountRequested, connectAccountSucceeded, - dismissAlert, disableAlertFailed, disableAlertRequested, disableAlertSucceeded, + dismissAlert, + switchAccountFailed, + switchAccountRequested, + switchAccountSucceeded, switchedToUnconnectedAccount, } = actions @@ -104,6 +120,20 @@ export const dismissAndDisableAlert = () => { } } +export const switchToAccount = (address) => { + return async (dispatch) => { + try { + await dispatch(switchAccountRequested()) + await dispatch(setSelectedAddress(address)) + await dispatch(switchAccountSucceeded()) + } catch (error) { + console.error(error) + captureException(error) + await dispatch(switchAccountFailed()) + } + } +} + export const connectAccount = () => { return async (dispatch, getState) => { const state = getState() diff --git a/ui/app/ducks/index.js b/ui/app/ducks/index.js index 5e7cd7889..09af8914e 100644 --- a/ui/app/ducks/index.js +++ b/ui/app/ducks/index.js @@ -5,12 +5,11 @@ import sendReducer from './send/send.duck' import appStateReducer from './app/app' import confirmTransactionReducer from './confirm-transaction/confirm-transaction.duck' import gasReducer from './gas/gas.duck' -import { switchToConnected, unconnectedAccount } from './alerts' +import { unconnectedAccount } from './alerts' import historyReducer from './history/history' import { ALERT_TYPES } from '../../../app/scripts/controllers/alert' export default combineReducers({ - [ALERT_TYPES.switchToConnected]: switchToConnected, [ALERT_TYPES.unconnectedAccount]: unconnectedAccount, activeTab: (s) => (s === undefined ? null : s), metamask: metamaskReducer, diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js index e85f717ab..e3f35bf38 100644 --- a/ui/app/ducks/metamask/metamask.js +++ b/ui/app/ducks/metamask/metamask.js @@ -31,10 +31,8 @@ export default function reduceMetamask (state = {}, action) { ensResolution: null, ensResolutionError: '', }, - coinOptions: {}, useBlockie: false, featureFlags: {}, - networkEndpointType: undefined, welcomeScreenSeen: false, currentLocale: '', preferences: { @@ -373,10 +371,8 @@ export const getCurrentLocale = (state) => state.metamask.currentLocale export const getAlertEnabledness = (state) => state.metamask.alertEnabledness -export const getSwitchToConnectedAlertEnabledness = (state) => getAlertEnabledness(state)[ALERT_TYPES.switchToConnected] - export const getUnconnectedAccountAlertEnabledness = (state) => getAlertEnabledness(state)[ALERT_TYPES.unconnectedAccount] -export const getSwitchToConnectedAlertShown = (state) => state.metamask.switchToConnectedAlertShown +export const getUnconnectedAccountAlertShown = (state) => state.metamask.unconnectedAccountAlertShownOrigins export const getTokens = (state) => state.metamask.tokens diff --git a/ui/app/helpers/utils/token-util.js b/ui/app/helpers/utils/token-util.js index ac34b0882..e6245547f 100644 --- a/ui/app/helpers/utils/token-util.js +++ b/ui/app/helpers/utils/token-util.js @@ -2,6 +2,8 @@ import log from 'loglevel' import * as util from './util' import BigNumber from 'bignumber.js' import contractMap from 'eth-contract-metadata' +import { conversionUtil, multiplyCurrencies } from './conversion-util' +import { formatCurrency } from './confirm-tx.util' const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { return { @@ -142,3 +144,41 @@ export function getTokenToAddress (tokenParams = []) { const toAddressData = tokenParams.find((param) => param.name === '_to') return toAddressData ? toAddressData.value : tokenParams[0].value } + +/** + * Get the token balance converted to fiat and formatted for display + * + * @param {number} [contractExchangeRate] - The exchange rate between the current token and the native currency + * @param {number} conversionRate - The exchange rate between the current fiat currency and the native currency + * @param {string} currentCurrency - The currency code for the user's chosen fiat currency + * @param {string} [tokenAmount] - The current token balance + * @param {string} [tokenSymbol] - The token symbol + * @returns {string|undefined} The formatted token amount in the user's chosen fiat currency + */ +export function getFormattedTokenFiatAmount ( + contractExchangeRate, + conversionRate, + currentCurrency, + tokenAmount, + tokenSymbol +) { + // If the conversionRate is 0 (i.e. unknown) or the contract exchange rate + // is currently unknown, the fiat amount cannot be calculated so it is not + // shown to the user + if (conversionRate <= 0 || !contractExchangeRate || tokenAmount === undefined) { + return undefined + } + + const currentTokenToFiatRate = multiplyCurrencies( + contractExchangeRate, + conversionRate + ) + const currentTokenInFiat = conversionUtil(tokenAmount, { + fromNumericBase: 'dec', + fromCurrency: tokenSymbol, + toCurrency: currentCurrency.toUpperCase(), + numberOfDecimals: 2, + conversionRate: currentTokenToFiatRate, + }) + return `${formatCurrency(currentTokenInFiat, currentCurrency)} ${currentCurrency.toUpperCase()}` +} diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js index 33cad8860..cc1aba75c 100644 --- a/ui/app/helpers/utils/util.js +++ b/ui/app/helpers/utils/util.js @@ -309,3 +309,27 @@ export function getAccountByAddress (accounts = [], targetAddress) { export function stripHttpSchemes (urlString) { return urlString.replace(/^https?:\/\//u, '') } + +/** + * Checks whether a URL-like value (object or string) is an extension URL. + * + * @param {string | URL | object} urlLike - The URL-like value to test. + * @returns {boolean} Whether the URL-like value is an extension URL. + */ +export function isExtensionUrl (urlLike) { + + const EXT_PROTOCOLS = ['chrome-extension:', 'moz-extension:'] + + if (typeof urlLike === 'string') { + for (const protocol of EXT_PROTOCOLS) { + if (urlLike.startsWith(protocol)) { + return true + } + } + } + + if (urlLike?.protocol) { + return EXT_PROTOCOLS.includes(urlLike.protocol) + } + return false +} diff --git a/ui/app/hooks/tests/useTransactionDisplayData.test.js b/ui/app/hooks/tests/useTransactionDisplayData.test.js index c394cfbb2..e8281cc72 100644 --- a/ui/app/hooks/tests/useTransactionDisplayData.test.js +++ b/ui/app/hooks/tests/useTransactionDisplayData.test.js @@ -4,6 +4,7 @@ import { renderHook } from '@testing-library/react-hooks' import sinon from 'sinon' import transactions from '../../../../test/data/transaction-data.json' import { useTransactionDisplayData } from '../useTransactionDisplayData' +import * as useTokenFiatAmountHooks from '../useTokenFiatAmount' import { getPreferences, getShouldShowFiat, getNativeCurrency, getCurrentCurrency } from '../../selectors' import { getTokens } from '../../ducks/metamask/metamask' import * as i18nhooks from '../useI18nContext' @@ -74,11 +75,15 @@ const expectedResults = [ status: 'confirmed' }, ] -let useSelector, useI18nContext +let useSelector, useI18nContext, useTokenFiatAmount describe('useTransactionDisplayData', function () { before(function () { useSelector = sinon.stub(reactRedux, 'useSelector') + useTokenFiatAmount = sinon.stub(useTokenFiatAmountHooks, 'useTokenFiatAmount') + useTokenFiatAmount.returns((tokenAddress) => { + return tokenAddress ? '1 TST' : undefined + }) useI18nContext = sinon.stub(i18nhooks, 'useI18nContext') useI18nContext.returns((key, variables) => getMessage('en', messages, key, variables)) useSelector.callsFake((selector) => { diff --git a/ui/app/hooks/useMetricEvent.js b/ui/app/hooks/useMetricEvent.js index 33ca751ec..a5a883b5f 100644 --- a/ui/app/hooks/useMetricEvent.js +++ b/ui/app/hooks/useMetricEvent.js @@ -4,6 +4,6 @@ import { MetaMetricsContext } from '../contexts/metametrics' export function useMetricEvent (config = {}, overrides = {}) { const metricsEvent = useContext(MetaMetricsContext) - const trackEvent = useCallback(() => metricsEvent(config, overrides), [config, overrides]) + const trackEvent = useCallback(() => metricsEvent(config, overrides), [config, metricsEvent, overrides]) return trackEvent } diff --git a/ui/app/hooks/useShouldShowSpeedUp.js b/ui/app/hooks/useShouldShowSpeedUp.js index d728f042c..437c6cba2 100644 --- a/ui/app/hooks/useShouldShowSpeedUp.js +++ b/ui/app/hooks/useShouldShowSpeedUp.js @@ -40,7 +40,7 @@ export function useShouldShowSpeedUp (transactionGroup, isEarliestNonce) { clearTimeout(timeoutId) } } - }, [submittedTime, hasRetried, isEarliestNonce]) + }, [submittedTime, speedUpEnabled, hasRetried, isEarliestNonce]) return speedUpEnabled } diff --git a/ui/app/hooks/useTokenFiatAmount.js b/ui/app/hooks/useTokenFiatAmount.js new file mode 100644 index 000000000..164220ce5 --- /dev/null +++ b/ui/app/hooks/useTokenFiatAmount.js @@ -0,0 +1,38 @@ +import { useMemo } from 'react' +import { useSelector } from 'react-redux' +import { getTokenExchangeRates, getConversionRate, getCurrentCurrency, getShouldShowFiat } from '../selectors' +import { getFormattedTokenFiatAmount } from '../helpers/utils/token-util' + +/** + * Get the token balance converted to fiat and formatted for display + * + * @param {string} [tokenAddress] - The token address + * @param {string} [tokenAmount] - The token balance + * @param {string} [tokenSymbol] - The token symbol + * @return {string} - The formatted token amount in the user's chosen fiat currency + */ +export function useTokenFiatAmount (tokenAddress, tokenAmount, tokenSymbol) { + const contractExchangeRates = useSelector(getTokenExchangeRates) + const conversionRate = useSelector(getConversionRate) + const currentCurrency = useSelector(getCurrentCurrency) + const showFiat = useSelector(getShouldShowFiat) + + const tokenExchangeRate = contractExchangeRates[tokenAddress] + + const formattedFiat = useMemo( + () => getFormattedTokenFiatAmount( + tokenExchangeRate, + conversionRate, + currentCurrency, + tokenAmount, + tokenSymbol + ), + [tokenExchangeRate, conversionRate, currentCurrency, tokenAmount, tokenSymbol] + ) + + if (!showFiat || currentCurrency.toUpperCase() === tokenSymbol) { + return undefined + } + + return formattedFiat +} diff --git a/ui/app/hooks/useTokenTracker.js b/ui/app/hooks/useTokenTracker.js index e06ec8a55..406eade3f 100644 --- a/ui/app/hooks/useTokenTracker.js +++ b/ui/app/hooks/useTokenTracker.js @@ -82,7 +82,7 @@ export function useTokenTracker (tokens) { } buildTracker(userAddress, tokens) - }, [userAddress, network, tokens, updateBalances, buildTracker]) + }, [userAddress, teardownTracker, network, tokens, updateBalances, buildTracker]) return { loading, tokensWithBalances, error } } diff --git a/ui/app/hooks/useTransactionDisplayData.js b/ui/app/hooks/useTransactionDisplayData.js index 2894191f6..a0509966f 100644 --- a/ui/app/hooks/useTransactionDisplayData.js +++ b/ui/app/hooks/useTransactionDisplayData.js @@ -3,6 +3,7 @@ import { getKnownMethodData } from '../selectors/selectors' import { getTransactionActionKey, getStatusKey } from '../helpers/utils/transactions.util' import { camelCaseToCapitalize } from '../helpers/utils/common.util' import { useI18nContext } from './useI18nContext' +import { useTokenFiatAmount } from './useTokenFiatAmount' import { PRIMARY, SECONDARY } from '../helpers/constants/common' import { getTokenToAddress } from '../helpers/utils/token-util' import { useUserPreferencedCurrency } from './useUserPreferencedCurrency' @@ -84,6 +85,7 @@ export function useTransactionDisplayData (transactionGroup) { const token = isTokenCategory && knownTokens.find((token) => token.address === recipientAddress) const tokenData = useTokenData(initialTransaction?.txParams?.data, isTokenCategory) const tokenDisplayValue = useTokenDisplayValue(initialTransaction?.txParams?.data, token, isTokenCategory) + const tokenFiatAmount = useTokenFiatAmount(token?.address, tokenDisplayValue, token?.symbol) let category let title @@ -127,14 +129,15 @@ export function useTransactionDisplayData (transactionGroup) { const [primaryCurrency] = useCurrencyDisplay(primaryValue, { prefix, - displayValue: isTokenCategory && tokenDisplayValue, - suffix: isTokenCategory && token?.symbol, + displayValue: isTokenCategory ? tokenDisplayValue : undefined, + suffix: isTokenCategory ? token?.symbol : undefined, ...primaryCurrencyPreferences, }) const [secondaryCurrency] = useCurrencyDisplay(primaryValue, { prefix, - displayValue: isTokenCategory && tokenDisplayValue, + displayValue: isTokenCategory ? tokenFiatAmount : undefined, + hideLabel: isTokenCategory ? true : undefined, ...secondaryCurrencyPreferences, }) @@ -146,7 +149,7 @@ export function useTransactionDisplayData (transactionGroup) { primaryCurrency, senderAddress, recipientAddress, - secondaryCurrency: isTokenCategory ? undefined : secondaryCurrency, + secondaryCurrency: isTokenCategory && !tokenFiatAmount ? undefined : secondaryCurrency, status, isPending: status in PENDING_STATUS_HASH, } diff --git a/ui/app/hooks/useTransactionTimeRemaining.js b/ui/app/hooks/useTransactionTimeRemaining.js new file mode 100644 index 000000000..c1d5ddd25 --- /dev/null +++ b/ui/app/hooks/useTransactionTimeRemaining.js @@ -0,0 +1,98 @@ +import { getEstimatedGasPrices, getEstimatedGasTimes, getFeatureFlags, getIsMainnet } from '../selectors' +import { hexWEIToDecGWEI } from '../helpers/utils/conversions.util' +import { useSelector } from 'react-redux' +import { useRef, useEffect, useState, useMemo } from 'react' +import { isEqual } from 'lodash' +import { getRawTimeEstimateData } from '../helpers/utils/gas-time-estimates.util' +import { getCurrentLocale } from '../ducks/metamask/metamask' + + +/** + * Calculate the number of minutes remaining until the transaction completes. + * @param {number} initialTimeEstimate - timestamp for the projected completion time + * @param {number} submittedTime - timestamp of when the tx was submitted + * @return {number} minutes remaining + */ +function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) { + const currentTime = (new Date()).getTime() + const timeElapsedSinceSubmission = (currentTime - submittedTime) / 1000 + const timeRemainingOnEstimate = initialTimeEstimate - timeElapsedSinceSubmission + + const renderingTimeRemainingEstimate = Math.round(timeRemainingOnEstimate / 60) + return renderingTimeRemainingEstimate +} + +/** + * returns a string representing the number of minutes predicted for the transaction to be + * completed. Only returns this prediction if the transaction is the earliest pending + * transaction, and the feature flag for showing timing is enabled. + * @param {bool} isPending - is the transaction currently pending + * @param {bool} isEarliestNonce - is this transaction the earliest nonce in list + * @param {number} submittedTime - the timestamp for when the transaction was submitted + * @param {number} currentGasPrice - gas price to use for calculation of time + * @returns {string | undefined} i18n formatted string if applicable + */ +export function useTransactionTimeRemaining ( + isPending, + isEarliestNonce, + submittedTime, + currentGasPrice +) { + // the following two selectors return the result of mapping over an array, as such they + // will always be new objects and trigger effects. To avoid this, we use isEqual as the + // equalityFn to only update when the data is new. + const gasPrices = useSelector(getEstimatedGasPrices, isEqual) + const estimatedTimes = useSelector(getEstimatedGasTimes, isEqual) + const locale = useSelector(getCurrentLocale) + const isMainNet = useSelector(getIsMainnet) + const interval = useRef() + const [timeRemaining, setTimeRemaining] = useState(null) + const featureFlags = useSelector(getFeatureFlags) + const transactionTimeFeatureActive = featureFlags?.transactionTime + + const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'narrow' }) + + // Memoize this value so it can be used as a dependency in the effect below + const initialTimeEstimate = useMemo(() => { + const customGasPrice = Number(hexWEIToDecGWEI(currentGasPrice)) + const { + newTimeEstimate, + } = getRawTimeEstimateData(customGasPrice, gasPrices, estimatedTimes) + return newTimeEstimate + }, [ currentGasPrice, gasPrices, estimatedTimes ]) + + useEffect(() => { + if ( + isMainNet && + transactionTimeFeatureActive && + isPending && + isEarliestNonce && + !isNaN(initialTimeEstimate) + ) { + clearInterval(interval.current) + setTimeRemaining( + calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) + ) + interval.current = setInterval(() => { + setTimeRemaining( + calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) + ) + }, 10000) + return () => clearInterval(interval.current) + } + }, [ + isMainNet, + transactionTimeFeatureActive, + isEarliestNonce, + isPending, + submittedTime, + initialTimeEstimate, + ]) + + // there are numerous checks to determine if time should be displayed. + // if any of the following are true, the timeRemaining will be null + // User is currently not on the mainnet + // User does not have the transactionTime feature flag enabled + // The transaction is not pending, or isn't the earliest nonce + return timeRemaining ? rtf.format(timeRemaining, 'minute') : undefined +} diff --git a/ui/app/pages/add-token/token-search/token-search.component.js b/ui/app/pages/add-token/token-search/token-search.component.js index 9cb3425b3..82aaae3fc 100644 --- a/ui/app/pages/add-token/token-search/token-search.component.js +++ b/ui/app/pages/add-token/token-search/token-search.component.js @@ -6,7 +6,7 @@ import InputAdornment from '@material-ui/core/InputAdornment' import TextField from '../../../components/ui/text-field' const contractList = Object.entries(contractMap) - .map(([ _, tokenData]) => tokenData) + .map(([address, tokenData]) => Object.assign({}, tokenData, { address })) .filter((tokenData) => Boolean(tokenData.erc20)) const fuse = new Fuse(contractList, { diff --git a/ui/app/pages/asset/asset.js b/ui/app/pages/asset/asset.js index 7393d8f1b..7cdc08ec7 100644 --- a/ui/app/pages/asset/asset.js +++ b/ui/app/pages/asset/asset.js @@ -1,63 +1,30 @@ import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { Redirect, useHistory, useParams } from 'react-router-dom' -import { createAccountLink } from '@metamask/etherscan-link' - -import TransactionList from '../../components/app/transaction-list' -import { EthOverview, TokenOverview } from '../../components/app/wallet-overview' -import { getCurrentNetworkId, getSelectedIdentity } from '../../selectors/selectors' +import { useSelector } from 'react-redux' +import { Redirect, useParams } from 'react-router-dom' import { getTokens } from '../../ducks/metamask/metamask' import { DEFAULT_ROUTE } from '../../helpers/constants/routes' -import { showModal } from '../../store/actions' -import AssetNavigation from './components/asset-navigation' -import TokenOptions from './components/token-options' +import NativeAsset from './components/native-asset' +import TokenAsset from './components/token-asset' const Asset = () => { - const dispatch = useDispatch() - const network = useSelector(getCurrentNetworkId) - const selectedAccountName = useSelector((state) => getSelectedIdentity(state).name) const nativeCurrency = useSelector((state) => state.metamask.nativeCurrency) const tokens = useSelector(getTokens) - const history = useHistory() const { asset } = useParams() const token = tokens.find((token) => token.address === asset) - let assetName - let optionsButton - + let content if (token) { - assetName = token.symbol - optionsButton = ( - dispatch(showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token }))} - onViewEtherscan={() => { - const url = createAccountLink(token.address, network) - global.platform.openTab({ url }) - }} - tokenSymbol={token.symbol} - /> - ) + content = } else if (asset === nativeCurrency) { - assetName = nativeCurrency + content = } else { - return + content = } - - const overview = token - ? - : return (
- history.push(DEFAULT_ROUTE)} - optionsButton={optionsButton} - /> - { overview } - + { content }
) } diff --git a/ui/app/pages/asset/components/native-asset.js b/ui/app/pages/asset/components/native-asset.js new file mode 100644 index 000000000..3bda491b8 --- /dev/null +++ b/ui/app/pages/asset/components/native-asset.js @@ -0,0 +1,32 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import TransactionList from '../../../components/app/transaction-list' +import { EthOverview } from '../../../components/app/wallet-overview' +import { getSelectedIdentity } from '../../../selectors/selectors' +import { DEFAULT_ROUTE } from '../../../helpers/constants/routes' + +import AssetNavigation from './asset-navigation' + +export default function NativeAsset ({ nativeCurrency }) { + const selectedAccountName = useSelector((state) => getSelectedIdentity(state).name) + const history = useHistory() + + return ( + <> + history.push(DEFAULT_ROUTE)} + /> + + + + ) +} + +NativeAsset.propTypes = { + nativeCurrency: PropTypes.string.isRequired, +} diff --git a/ui/app/pages/asset/components/token-asset.js b/ui/app/pages/asset/components/token-asset.js new file mode 100644 index 000000000..84c7eee37 --- /dev/null +++ b/ui/app/pages/asset/components/token-asset.js @@ -0,0 +1,51 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' +import { createAccountLink } from '@metamask/etherscan-link' + +import TransactionList from '../../../components/app/transaction-list' +import { TokenOverview } from '../../../components/app/wallet-overview' +import { getCurrentNetworkId, getSelectedIdentity } from '../../../selectors/selectors' +import { DEFAULT_ROUTE } from '../../../helpers/constants/routes' +import { showModal } from '../../../store/actions' + +import AssetNavigation from './asset-navigation' +import TokenOptions from './token-options' + +export default function TokenAsset ({ token }) { + const dispatch = useDispatch() + const network = useSelector(getCurrentNetworkId) + const selectedAccountName = useSelector((state) => getSelectedIdentity(state).name) + const history = useHistory() + + return ( + <> + history.push(DEFAULT_ROUTE)} + optionsButton={( + dispatch(showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token }))} + onViewEtherscan={() => { + const url = createAccountLink(token.address, network) + global.platform.openTab({ url }) + }} + tokenSymbol={token.symbol} + /> + )} + /> + + + + ) +} + +TokenAsset.propTypes = { + token: PropTypes.shape({ + address: PropTypes.string.isRequired, + decimals: PropTypes.number, + symbol: PropTypes.string, + }).isRequired, +} diff --git a/ui/app/pages/connected-accounts/connected-accounts.component.js b/ui/app/pages/connected-accounts/connected-accounts.component.js index cd0900f93..3fea60ed3 100644 --- a/ui/app/pages/connected-accounts/connected-accounts.component.js +++ b/ui/app/pages/connected-accounts/connected-accounts.component.js @@ -1,8 +1,8 @@ import PropTypes from 'prop-types' import React, { PureComponent } from 'react' -import { CONNECTED_ROUTE } from '../../helpers/constants/routes' import Popover from '../../components/ui/popover' import ConnectedAccountsList from '../../components/app/connected-accounts-list' +import ConnectedAccountsPermissions from '../../components/app/connected-accounts-permissions' export default class ConnectedAccounts extends PureComponent { static contextTypes = { @@ -17,7 +17,7 @@ export default class ConnectedAccounts extends PureComponent { static propTypes = { accountToConnect: PropTypes.object, activeTabOrigin: PropTypes.string.isRequired, - addPermittedAccount: PropTypes.func.isRequired, + connectAccount: PropTypes.func.isRequired, connectedAccounts: PropTypes.array.isRequired, mostRecentOverviewPage: PropTypes.string.isRequired, permissions: PropTypes.array, @@ -28,16 +28,12 @@ export default class ConnectedAccounts extends PureComponent { history: PropTypes.object.isRequired, } - viewConnectedSites = () => { - this.props.history.push(CONNECTED_ROUTE) - } - render () { const { accountToConnect, activeTabOrigin, isActiveTabExtension, - addPermittedAccount, + connectAccount, connectedAccounts, history, mostRecentOverviewPage, @@ -58,15 +54,16 @@ export default class ConnectedAccounts extends PureComponent { subtitle={connectedAccounts.length ? connectedAccountsDescription : t('connectedAccountsEmptyDescription')} onClose={() => history.push(mostRecentOverviewPage)} footerClassName="connected-accounts__footer" + footer={} > ) diff --git a/ui/app/pages/connected-accounts/connected-accounts.container.js b/ui/app/pages/connected-accounts/connected-accounts.container.js index 1aab4ebfb..1bb5ba84a 100644 --- a/ui/app/pages/connected-accounts/connected-accounts.container.js +++ b/ui/app/pages/connected-accounts/connected-accounts.container.js @@ -6,11 +6,10 @@ import { getPermissionsForActiveTab, getSelectedAddress, } from '../../selectors' +import { isExtensionUrl } from '../../helpers/utils/util' import { addPermittedAccount, removePermittedAccount, setSelectedAddress } from '../../store/actions' import { getMostRecentOverviewPage } from '../../ducks/history/history' -const EXT_PROTOCOLS = ['chrome-extension:', 'moz-extension:'] - const mapStateToProps = (state) => { const { activeTab } = state const accountToConnect = getAccountToConnectToActiveTab(state) @@ -18,7 +17,7 @@ const mapStateToProps = (state) => { const permissions = getPermissionsForActiveTab(state) const selectedAddress = getSelectedAddress(state) - const isActiveTabExtension = EXT_PROTOCOLS.includes(activeTab.protocol) + const isActiveTabExtension = isExtensionUrl(activeTab) return { accountToConnect, isActiveTabExtension, @@ -39,14 +38,14 @@ const mapDispatchToProps = (dispatch) => { } const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { activeTabOrigin: origin } = stateProps + const { activeTabOrigin } = stateProps return { ...ownProps, ...stateProps, ...dispatchProps, - addPermittedAccount: (address) => dispatchProps.addPermittedAccount(origin, address), - removePermittedAccount: (address) => dispatchProps.removePermittedAccount(origin, address), + connectAccount: (address) => dispatchProps.addPermittedAccount(activeTabOrigin, address), + removePermittedAccount: (address) => dispatchProps.removePermittedAccount(activeTabOrigin, address), } } diff --git a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js index f62af2d1d..b13088915 100644 --- a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js +++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js @@ -23,6 +23,7 @@ export default class ImportWithSeedPhrase extends PureComponent { state = { seedPhrase: '', + showSeedPhrase: false, password: '', confirmPassword: '', seedPhraseError: '', @@ -179,9 +180,15 @@ export default class ImportWithSeedPhrase extends PureComponent { })) } + toggleShowSeedPhrase = () => { + this.setState(({ showSeedPhrase }) => ({ + showSeedPhrase: !showSeedPhrase, + })) + } + render () { const { t } = this.context - const { seedPhraseError, passwordError, confirmPasswordError, termsChecked } = this.state + const { seedPhraseError, showSeedPhrase, passwordError, confirmPasswordError, termsChecked } = this.state return (
-