diff options
author | kumavis <kumavis@users.noreply.github.com> | 2018-10-18 08:30:07 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-10-18 08:30:07 +0800 |
commit | c2f97717c0fbf9a64cf527891f7a1f35049fb023 (patch) | |
tree | f62dabf7a62798ad82641305422fb4e0bcc0a74a | |
parent | 85884b21afe6e5e85b58123b24e72776d1437cc6 (diff) | |
parent | 17372e150d7070e9f4870e65a7d6c1d88568f2f5 (diff) | |
download | tangerine-wallet-browser-c2f97717c0fbf9a64cf527891f7a1f35049fb023.tar tangerine-wallet-browser-c2f97717c0fbf9a64cf527891f7a1f35049fb023.tar.gz tangerine-wallet-browser-c2f97717c0fbf9a64cf527891f7a1f35049fb023.tar.bz2 tangerine-wallet-browser-c2f97717c0fbf9a64cf527891f7a1f35049fb023.tar.lz tangerine-wallet-browser-c2f97717c0fbf9a64cf527891f7a1f35049fb023.tar.xz tangerine-wallet-browser-c2f97717c0fbf9a64cf527891f7a1f35049fb023.tar.zst tangerine-wallet-browser-c2f97717c0fbf9a64cf527891f7a1f35049fb023.zip |
Merge pull request #5539 from MetaMask/v4.16.0
Version 4.16.0
115 files changed, 4688 insertions, 995 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb8197d4..5a9d7703c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,37 @@ ## Current Develop Branch +## 4.16.0 Wednesday October 17 2018 + +- Feature: Add toggle for primary currency (eth/fiat) +- Feature: add tooltip for view etherscan tx +- Feature: add Polish translations +- Feature: improve Korean translations +- Feature: improve Italian translations +- Bug Fix: Fix bug with "pending" block reference +- Bug Fix: Force AccountTracker to update balances on network change +- Bug Fix: Fix document extension check when injecting web3 +- Bug Fix: Fix some support links + +## 4.15.0 Thursday October 11 2018 + +- A rollback release, equivalent to `v4.11.1` to be deployed in the case that `v4.14.0` is found to have bugs. + ## 4.14.0 Thursday October 11 2018 - Update transaction statuses when switching networks. - [#5470](https://github.com/MetaMask/metamask-extension/pull/5470) 100% coverage in French locale, fixed the procedure to verify proposed locale. - Added rudimentary support for the subscription API to support web3 1.0 and Truffle's Drizzle. +- [#5502](https://github.com/MetaMask/metamask-extension/pull/5502) Update Italian translation. -## 4.12.0 Thursday September 27 2018 - -- Reintroduces changes from 4.10.0 ## 4.13.0 - A rollback release, equivalent to `v4.11.1` to be deployed in the case that `v4.12.0` is found to have bugs. +## 4.12.0 Thursday September 27 2018 + +- Reintroduces changes from 4.10.0 + ## 4.11.1 Tuesday September 25 2018 - Adds Ledger support. @@ -1,5 +1,5 @@ # MetaMask Browser Extension -[![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](https://waffle.io/MetaMask/metamask-extension) +[![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](https://waffle.io/MetaMask/metamask-extension) ## Support diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index d8467e9eb..bf5854a31 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -140,6 +140,9 @@ "clickCopy": { "message": "Click to Copy" }, + "clickToAdd": { + "message": "Click on $1 to add them to your account" + }, "close": { "message": "Close" }, @@ -361,6 +364,9 @@ "enterPasswordContinue": { "message": "Enter password to continue" }, + "eth": { + "message": "ETH" + }, "etherscanView": { "message": "View account on Etherscan" }, @@ -380,7 +386,7 @@ "message": "Failed" }, "fiat": { - "message": "FIAT", + "message": "Fiat", "description": "Exchange type" }, "fileImportFail": { @@ -424,6 +430,9 @@ "gasLimitTooLow": { "message": "Gas limit must be at least 21000" }, + "gasUsed": { + "message": "Gas Used" + }, "generatingSeed": { "message": "Generating Seed..." }, @@ -635,6 +644,9 @@ "min": { "message": "Minimum" }, + "missingYourTokens": { + "message": "Don't see your tokens?" + }, "myAccounts": { "message": "My Accounts" }, @@ -787,6 +799,12 @@ "prev": { "message": "Prev" }, + "primaryCurrencySetting": { + "message": "Primary Currency" + }, + "primaryCurrencySettingDescription": { + "message": "Select ETH to prioritize displaying values in ETH. Select Fiat to prioritize displaying values in your selected currency." + }, "privacyMsg": { "message": "Privacy Policy" }, @@ -795,7 +813,7 @@ "description": "select this type of file to use to import an account" }, "privateKeyWarning": { - "message": "Warning: Never disclose this key. Anyone with your private keys can take steal any assets held in your account." + "message": "Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account." }, "privateNetwork": { "message": "Private Network" @@ -1186,7 +1204,7 @@ "message": "These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret." }, "typePassword": { - "message": "Type Your Password" + "message": "Type your MetaMask password" }, "uiWelcome": { "message": "Welcome to the New UI (Beta)" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 3e43a7b43..be2a29ab5 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -156,7 +156,7 @@ "message": " Copiar " }, "copyPrivateKey": { - "message": "Ésta es tu llave privada (haz click para copiar)" + "message": "Ésta es tu clave privada (haz click para copiar)" }, "copyToClipboard": { "message": "Copiar al portapapeles" @@ -278,10 +278,10 @@ "message": "Tipo de cambio" }, "exportPrivateKey": { - "message": "Exportar llave privada" + "message": "Exportar clave privada" }, "exportPrivateKeyWarning": { - "message": "Exportar llaves privadas bajo TU PROPIO riesgo" + "message": "Exportar claves privadas bajo TU PROPIO riesgo" }, "failed": { "message": "Fallo" @@ -579,8 +579,8 @@ "description": "En el proceso de creación de contraseña, esta no es lo suficientemente larga para ser segura" }, "pastePrivateKey": { - "message": "Pega tu llave privada aqui", - "description": "Para importar una cuenta desde una llave privada" + "message": "Pega tu clave privada aqui", + "description": "Para importar una cuenta desde una clave privada" }, "pasteSeed": { "message": "¡Pega tu frase semilla aquí!" @@ -595,7 +595,7 @@ "message": "Política de privacidad" }, "privateKey": { - "message": "Llave privada", + "message": "Clave privada", "description": "Selecciona este tupo de archivo para importar una cuenta" }, "privateKeyWarning": { @@ -712,7 +712,7 @@ "message": "Comprar con ShapeShift" }, "showPrivateKeys": { - "message": "Mostrar llaves privadas" + "message": "Mostrar claves privadas" }, "showQRCode": { "message": "Mostrar codigo QR" diff --git a/app/_locales/index.json b/app/_locales/index.json index 0598aa9ec..46933dc3f 100644 --- a/app/_locales/index.json +++ b/app/_locales/index.json @@ -11,6 +11,7 @@ { "code": "ko", "name": "Korean" }, { "code": "nl", "name": "Dutch" }, { "code": "ph", "name": "Tagalog" }, + { "code": "pl", "name": "Polish" }, { "code": "pt", "name": "Portuguese" }, { "code": "ru", "name": "Russian" }, { "code": "sl", "name": "Slovenian" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 492bcc3de..06e3dc855 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -2,6 +2,9 @@ "accept": { "message": "Accetta" }, + "accessingYourCamera": { + "message": "Accesso alla fotocamera..." + }, "account": { "message": "Account" }, @@ -11,6 +14,15 @@ "accountName": { "message": "Nome Account" }, + "accountOptions": { + "message": "Account Options" + }, + "accountSelectionRequired": { + "message": "Devi selezionare un account!" + }, + "activityLog": { + "message": "log attività" + }, "address": { "message": "Indirizzo" }, @@ -23,6 +35,12 @@ "addTokens": { "message": "Aggiungi più token" }, + "addSuggestedTokens": { + "message": "Aggiungi Token Suggeriti" + }, + "addAcquiredTokens": { + "message": "Aggiungi i token che hai acquistato usando MetaMask" + }, "amount": { "message": "Importo" }, @@ -37,9 +55,21 @@ "message": "MetaMask", "description": "Il nome dell'applicazione" }, + "approve": { + "message": "Approva" + }, + "approved": { + "message": "Approvato" + }, "attemptingConnect": { "message": "Tentativo di connessione alla blockchain." }, + "attemptToCancel": { + "message": "Tentativo di Annullamento?" + }, + "attemptToCancelDescription": { + "message": "Tentare di annullare non garantisce che la transazione verrà annullata. Se annullata, è comunque richiesto pagare alla rete la commissione sulla transazione." + }, "attributions": { "message": "Attribuzioni" }, @@ -71,8 +101,11 @@ "borrowDharma": { "message": "Prendi in presisito con Dharma (Beta)" }, + "browserNotSupported": { + "message": "Il tuo Browser non è supportato..." + }, "builtInCalifornia": { - "message": "MetaMask è progettato e costruito in California." + "message": "MetaMask è progettato e realizzato in California." }, "buy": { "message": "Compra" @@ -83,8 +116,23 @@ "buyCoinbaseExplainer": { "message": "Coinbase è il servizio più popolare al mondo per comprare e vendere Bitcoin, Ethereum e Litecoin." }, + "bytes": { + "message": "Bytes" + }, + "ok": { + "message": "Ok" + }, "cancel": { - "message": "Cancella" + "message": "Annulla" + }, + "cancelAttempt": { + "message": "Tentativo di Annullamento" + }, + "cancellationGasFee": { + "message": "Commissione di Annullamento in Gas" + }, + "cancelN": { + "message": "Cancel all $1 transactions" }, "classicInterface": { "message": "Usa l'interfaccia classica" @@ -92,9 +140,18 @@ "clickCopy": { "message": "Clicca per Copiare" }, + "close": { + "message": "Chiudi" + }, + "chromeRequiredForHardwareWallets": { + "message": "Devi usare MetaMask con Google Chrome per connettere il tuo Portafoglio Hardware" + }, "confirm": { "message": "Conferma" }, + "confirmed": { + "message": "Confermata" + }, "confirmContract": { "message": "Conferma Contratto" }, @@ -104,6 +161,36 @@ "confirmTransaction": { "message": "Conferma Transazione" }, + "connectHardwareWallet": { + "message": "Connetti Portafoglio Hardware" + }, + "connect": { + "message": "Connetti" + }, + "connecting": { + "message": "Connessione..." + }, + "connectingToKovan": { + "message": "Connessione alla Rete di test Kovan" + }, + "connectingToMainnet": { + "message": "Connessione alla Rete Ethereum Principale" + }, + "connectingToRopsten": { + "message": "Connessione alla Rete di test Ropsten" + }, + "connectingToRinkeby": { + "message": "Connessione alla Rete di test Rinkeby" + }, + "connectingToUnknown": { + "message": "Connessione ad una Rete Sconosciuta" + }, + "connectToLedger": { + "message": "Connettersi al Ledger" + }, + "connectToTrezor": { + "message": "Connettersi al Trezor" + }, "continue": { "message": "Continua" }, @@ -131,6 +218,9 @@ "copy": { "message": "Copia" }, + "copyAddress": { + "message": "Copia l'indirizzo" + }, "copyToClipboard": { "message": "Copia negli appunti" }, @@ -156,12 +246,21 @@ "currentConversion": { "message": "Cambio Corrente" }, + "currentLanguage": { + "message": "Lingua Corrente" + }, "currentNetwork": { "message": "Rete Corrente" }, + "currentRpc": { + "message": "RPC Corrente" + }, "customGas": { "message": "Personalizza Gas" }, + "customToken": { + "message": "Token Personalizzato" + }, "customize": { "message": "Personalizza" }, @@ -223,33 +322,54 @@ "done": { "message": "Finito" }, + "downloadGoogleChrome": { + "message": "Scarica Google Chrome" + }, "downloadStateLogs": { "message": "Scarica i log di Stato" }, + "dontHaveAHardwareWallet": { + "message": "Non hai un portafoglio hardware?" + }, + "dropped": { + "message": "Abbandonata" + }, "edit": { "message": "Modifica" }, "editAccountName": { "message": "Modifica Nome Account" }, + "editingTransaction": { + "message": "Modifica la transazione" + }, "emailUs": { "message": "Mandaci una mail!" }, "encryptNewDen": { "message": "Cripta il tuo nuovo DEN" }, + "ensNameNotFound": { + "message": "Nome ENS non trovato" + }, "enterPassword": { "message": "Inserisci password" }, "enterPasswordConfirm": { "message": "Inserisci la tua password per confermare" }, + "enterPasswordContinue": { + "message": "Inserisci la tua password per continuare" + }, "etherscanView": { "message": "Vedi account su Etherscan" }, "exchangeRate": { "message": "Tasso di cambio" }, + "expandView": { + "message": "Expand View" + }, "exportPrivateKey": { "message": "Esporta Chiave Privata" }, @@ -257,7 +377,7 @@ "message": "Esporta chiave privata a tuo rischio." }, "failed": { - "message": "Fallito" + "message": "Fallita" }, "fiat": { "message": "FIAT", @@ -270,6 +390,9 @@ "followTwitter": { "message": "Seguici su Twitter" }, + "forgetDevice": { + "message": "Dimentica questo dispositivo" + }, "from": { "message": "Da" }, @@ -279,6 +402,9 @@ "fromShapeShift": { "message": "Da ShapeShift" }, + "functionType": { + "message": "Tipo della Funzione" + }, "gas": { "message": "Gas", "description": "Piccola indicazione del costo del gas" @@ -310,6 +436,9 @@ "gasPriceRequired": { "message": "Prezzo Gas Richiesto" }, + "generatingTransaction": { + "message": "Generando la transazione" + }, "getEther": { "message": "Ottieni Ether" }, @@ -317,10 +446,28 @@ "message": "Ottieni Get Ether da un faucet per $1", "description": "Visualizza il nome della rete per il faucet Ether" }, + "getHelp": { + "message": "Aiuto." + }, "greaterThanMin": { "message": "deve essere maggiore o uguale a $1.", "description": "aiuto per inserire un input esadecimale come decimale" }, + "hardware": { + "message": "hardware" + }, + "hardwareWalletConnected": { + "message": "Portafoglio hardware connesso" + }, + "hardwareWallets": { + "message": "Connetti portafoglio hardware" + }, + "hardwareWalletsMsg": { + "message": "Selezione un portafoglio hardware che vuoi utilizzare con MetaMask" + }, + "havingTroubleConnecting": { + "message": "Problemi di connessione?" + }, "here": { "message": "qui", "description": "per intendere -clicca qui- per maggiori informazioni (va con troubleTokenBalances)" @@ -328,6 +475,9 @@ "hereList": { "message": "Questa è una lista!!!!" }, + "hexData": { + "message": "Dati Hex" + }, "hide": { "message": "Nascondi" }, @@ -337,6 +487,9 @@ "hideTokenPrompt": { "message": "Nascondi Token?" }, + "history": { + "message": "Storico" + }, "howToDeposit": { "message": "Come vuoi depositare Ether?" }, @@ -363,9 +516,18 @@ "message": "Importato", "description": "stato che conferma che un account è stato totalmente caricato nel portachiavi" }, + "importUsingSeed": { + "message": "Importa account con frase seed" + }, + "info": { + "message": "Informazioni" + }, "infoHelp": { "message": "Informazioni & Aiuto" }, + "initialTransactionConfirmed": { + "message": "La transazione iniziale è stata confermata dalla rete. Clicca OK per tornare indietro." + }, "insufficientFunds": { "message": "Fondi non sufficienti." }, @@ -390,6 +552,9 @@ "invalidRPC": { "message": "URI RPC invalido" }, + "invalidSeedPhrase": { + "message": "Frase seed non valida" + }, "jsonFail": { "message": "Qualcosa è andato storto. Assicurati che il file JSON sia formattato correttamente." }, @@ -397,12 +562,24 @@ "message": "File JSON", "description": "formato per importare un account" }, + "keepTrackTokens": { + "message": "Tieni traccia dei tokens che hai acquistato con il tuo account MetaMask." + }, "kovan": { "message": "Rete di test Kovan" }, "knowledgeDataBase": { "message": "Visita la nostra Knowledge Base" }, + "max": { + "message": "Massimo" + }, + "learnMore": { + "message": "Scopri di più" + }, + "ledgerAccountRestriction": { + "message": "E' necessario utilizzare l'ultimo account prima di poterne aggiungere uno nuovo." + }, "lessThanMax": { "message": "deve essere minore o uguale a $1.", "description": "aiuto per inserire un input esadecimale come decimale" @@ -410,6 +587,9 @@ "likeToAddTokens": { "message": "Vorresti aggiungere questi token?" }, + "links": { + "message": "Collegamenti" + }, "limit": { "message": "Limite" }, @@ -437,17 +617,26 @@ "mainnet": { "message": "Rete Ethereum Principale" }, + "menu": { + "message": "Menu" + }, "message": { "message": "Messaggio" }, "metamaskDescription": { "message": "MetaMask è una cassaforte sicura per identità su Ethereum." }, + "metamaskSeedWords": { + "message": "Parole Seed di MetaMask" + }, + "metamaskVersion": { + "message": "versione di MetaMask" + }, "min": { "message": "Minimo" }, "myAccounts": { - "message": "Account Miei" + "message": "Miei Account" }, "mustSelectOne": { "message": "Devi selezionare almeno un token." @@ -469,6 +658,9 @@ "networks": { "message": "Reti" }, + "nevermind": { + "message": "Non importa" + }, "newAccount": { "message": "Nuovo Account" }, @@ -482,6 +674,9 @@ "newPassword": { "message": "Nuova Password (minimo 8 caratteri)" }, + "newPassword8Chars": { + "message": "Nuova Password (minimo 8 caratteri)" + }, "newRecipient": { "message": "Nuovo Destinatario" }, @@ -489,7 +684,7 @@ "message": "Nuovo URL RPC" }, "next": { - "message": "Prossimo" + "message": "Avanti" }, "noAddressForName": { "message": "Nessun indirizzo è stato impostato per questo nome." @@ -497,32 +692,75 @@ "noDeposits": { "message": "Nessun deposito ricevuto" }, + "noConversionRateAvailable": { + "message": "Tasso di Conversione non Disponibile" + }, "noTransactionHistory": { "message": "Nessuna cronologia delle transazioni." }, "noTransactions": { "message": "Nessuna Transazione" }, + "notFound": { + "message": "Non Trovata" + }, "notStarted": { "message": "Non Iniziato" }, + "noWebcamFoundTitle": { + "message": "Webcam non trovata" + }, + "noWebcamFound": { + "message": "La webcam del tuo computer non è stata trovata. Per favore riprovaci." + }, "oldUI": { "message": "Vecchia interfaccia" }, "oldUIMessage": { "message": "Sei ritornato alla vecchia interfaccia. Puoi ritornare alla nuova interfaccia tramite l'opzione nel menu a discesa in alto a destra." }, + "onlySendToEtherAddress": { + "message": "Invia ETH solamente ad un account Ethereum." + }, + "onlySendTokensToAccountAddress": { + "message": "Invia solamente $1 ad un account Ethereum.", + "description": "mostra il simbolo del token" + }, + "openInTab": { + "message": "Apri in una scheda" + }, "or": { "message": "o", "description": "scelta tra creare o importare un nuovo account" }, + "orderOneHere": { + "message": "Compra un Trezor o un Ledger e tieni i tuoi soldi al sicuro" + }, + "origin": { + "message": "Origine" + }, + "outgoing": { + "message": "In Uscita" + }, + "parameters": { + "message": "Parametri" + }, + "password": { + "message": "Password" + }, "passwordCorrect": { "message": "Assicurati che la password sia corretta." }, + "passwordsDontMatch": { + "message": "Le Password Non Corrispondonos" + }, "passwordMismatch": { "message": "le password non corrispondono", "description": "nella creazione della password, le due password all'interno dei campi non corrispondono" }, + "passwordNotLongEnough": { + "message": "Password non abbastanza lunga" + }, "passwordShort": { "message": "password non sufficientemente lunga", "description": "nella creazione della password, la password non è lunga abbastanza" @@ -534,12 +772,21 @@ "pasteSeed": { "message": "Incolla la tua frase seed qui!" }, + "pending": { + "message": "in corso" + }, "personalAddressDetected": { "message": "Rilevato indirizzo personale. Inserisci l'indirizzo del contratto del token." }, "pleaseReviewTransaction": { "message": "Ricontrolla la tua transazione." }, + "popularTokens": { + "message": "Tokens Popolari" + }, + "prev": { + "message": "Precedente" + }, "privacyMsg": { "message": "Politica sulla Privacy" }, @@ -556,6 +803,9 @@ "qrCode": { "message": "Mostra Codice QR" }, + "queue": { + "message": "Coda" + }, "readdToken": { "message": "Puoi aggiungere nuovamente questo token in futuro andando in “Aggiungi token” nel menu delle opzioni del tuo account." }, @@ -574,36 +824,87 @@ "refundAddress": { "message": "Indirizzo di Rimborso" }, + "reject": { + "message": "Reject" + }, + "rejectAll": { + "message": "Reject All" + }, + "rejectTxsN": { + "message": "Reject $1 transactions" + }, + "rejectTxsDescription": { + "message": "You are about to batch reject $1 transactions." + }, "rejected": { "message": "Respinta" }, + "reset": { + "message": "Reset" + }, "resetAccount": { "message": "Resetta Account" }, + "resetAccountDescription": { + "message": "Resettare il tuo account cancellerà lo storico delle transazioni." + }, "restoreFromSeed": { "message": "Ripristina da una frase seed" }, + "restoreVault": { + "message": "Ripristina Cassaforte" + }, + "restoreAccountWithSeed": { + "message": "Ripristina Account con la Frase Seed" + }, "required": { "message": "Richiesto" }, "retryWithMoreGas": { "message": "Riprova con un prezzo del Gas maggiore qui" }, + "restore": { + "message": "Ripristina" + }, "revealSeedWords": { "message": "Rivela Frase Seed" }, + "revealSeedWordsTitle": { + "message": "Frase Seed" + }, + "revealSeedWordsDescription": { + "message": "Se cambierai browser o computer, ti servirà questa frase seed per accedere ai tuoi account. Salvala in un posto sicuro e segreto." + }, + "revealSeedWordsWarningTitle": { + "message": "NON CONDIVIDERE questa frase con nessuno!" + }, "revealSeedWordsWarning": { "message": "Non ripristinare la tua frase seed in pubblico!. Queste parole possono essere usate per rubare il tuo account." }, "revert": { "message": "Annulla" }, + "remove": { + "message": "rimuovi" + }, + "removeAccount": { + "message": "Rimuovi account" + }, + "removeAccountDescription": { + "message": "Questo account sarà rimosso dal tuo portafoglio. Per favore assicurati che hai la frase seed originale o la chiave privata per questo account importato prima di continuare. Puoi nuovamente importare o creare un account dal menù a tendina. " + }, + "readyToConnect": { + "message": "Pronto a Connetterti?" + }, "rinkeby": { "message": "Rete di test Rinkeby" }, "ropsten": { "message": "Rete di test Ropsten" }, + "rpc": { + "message": "RPC Personalizzata" + }, "sampleAccountName": { "message": "Es: Il mio nuovo account", "description": "Aiuta l'utente a capire il concetto di aggiungere un nome leggibile al loro account" @@ -611,6 +912,9 @@ "save": { "message": "Salva" }, + "saveAsCsvFile": { + "message": "Salva Come File CSV" + }, "saveAsFile": { "message": "Salva come File", "description": "Processo per esportare un account" @@ -618,9 +922,18 @@ "saveSeedAsFile": { "message": "Salva la Frase Seed come File" }, + "scanInstructions": { + "message": "Posizione il codice QR davanti alla fotocamera" + }, + "scanQrCode": { + "message": "Scansiona Codice QR" + }, "search": { "message": "Cerca" }, + "searchResults": { + "message": "Risultati Ricerca" + }, "secretPhrase": { "message": "Inserisci la tua frase segreta di dodici parole per ripristinare la cassaforte." }, @@ -633,6 +946,9 @@ "selectCurrency": { "message": "Seleziona Moneta" }, + "selectLocale": { + "message": "Selezione Lingua" + }, "selectService": { "message": "Seleziona Servizio" }, @@ -648,6 +964,33 @@ "sendTokens": { "message": "Invia Tokens" }, + "sentEther": { + "message": "ether inviati" + }, + "sentTokens": { + "message": "tokens inviati" + }, + "separateEachWord": { + "message": "Separa ogni parola con un solo spazio" + }, + "searchTokens": { + "message": "Cerca Tokens" + }, + "selectAnAddress": { + "message": "Seleziona un Indirizzo" + }, + "selectAnAccount": { + "message": "Seleziona un Account" + }, + "selectAnAccountHelp": { + "message": "Selezione l'account da visualizzare in MetaMask" + }, + "selectHdPath": { + "message": "Seleziona Percorso HD" + }, + "selectPathHelp": { + "message": "Se non vedi il tuo account Ledger esistente di seguito, prova a cambiare il percorso in \"Legacy (MEW / MyCrypto)\"" + }, "sendTokensAnywhere": { "message": "Invia Tokens a chiunque abbia un account Ethereum" }, @@ -663,9 +1006,21 @@ "showQRCode": { "message": "Mostra Codie QR" }, + "showHexData": { + "message": "Mostra Dati Hex" + }, + "showHexDataDescription": { + "message": "Seleziona per mostrare il campo dei dati hex nella schermata di invio" + }, "sign": { "message": "Firma" }, + "signatureRequest": { + "message": "Firma Richiesta" + }, + "signed": { + "message": "Firmata" + }, "signMessage": { "message": "Firma Messaggio" }, @@ -681,6 +1036,15 @@ "spaceBetween": { "message": "ci può essere solo uno spazio tra le parole" }, + "speedUp": { + "message": "velocizza" + }, + "speedUpTitle": { + "message": "Velocizza Transazione" + }, + "speedUpSubtitle": { + "message": "Aumenta il prezzo del gas per tentare di sovrascrivere e velocizzare la transazione" + }, "status": { "message": "Stato" }, @@ -690,9 +1054,33 @@ "stateLogsDescription": { "message": "I log di stato contengono i tuoi indirizzi pubblici e le transazioni effettuate." }, + "stateLogError": { + "message": "Errore nel recupero dei log di stato." + }, + "step1HardwareWallet": { + "message": "1. Connetti Portafoglio Hardware" + }, + "step1HardwareWalletMsg": { + "message": "Connetti il tuo portafoglio hardware al tuo computer." + }, + "step2HardwareWallet": { + "message": "2. Seleziona un Account" + }, + "step2HardwareWalletMsg": { + "message": "Selezione l'account che vuoi vedere. Puoi selezionarne solo uno alla volta." + }, + "step3HardwareWallet": { + "message": "3. Inizia a usare dApps e molto altro ancora!" + }, + "step3HardwareWalletMsg": { + "message": "Usa il tuo account hardware come utilizzeresti qualsiasi account Ethereum. Accedi alle dApps, invia Eth, compra e conserva token ERC20 e token non fungibili come CryptoKitties" + }, "submit": { "message": "Invia" }, + "submitted": { + "message": "Inviata" + }, "supportCenter": { "message": "Visita il nostro Centro di Supporto" }, @@ -715,6 +1103,9 @@ "message": "$1 a ETH via ShapeShift", "description": "il sistema riempirà il tipo di deposito all'inizio del messaggio" }, + "token": { + "message": "Token" + }, "tokenAddress": { "message": "Indirizzo Token" }, @@ -736,22 +1127,61 @@ "total": { "message": "Totale" }, + "transaction": { + "message": "transazione" + }, + "transactionConfirmed": { + "message": "Transazione confermata il $2." + }, + "transactionCreated": { + "message": "Transazione di valore $1 creata il $2." + }, + "transactionWithNonce": { + "message": "Transazione $1" + }, + "transactionDropped": { + "message": "Transazione abbandonata il $2." + }, + "transactionSubmitted": { + "message": "Transazione inviata il $2." + }, + "transactionUpdated": { + "message": "Transazione aggiornata il $2." + }, + "transactionUpdatedGas": { + "message": "Transazione aggiornata con un prezzo del gas di $1 il $2." + }, "transactions": { "message": "transazioni" }, + "transactionError": { + "message": "Errore Transazione. Eccceziona generata nel codice del contratto." + }, "transactionMemo": { "message": "Promemoria Transazione (opzionale)" }, "transactionNumber": { "message": "Numero Transazione" }, + "transfer": { + "message": "Trasferisci" + }, + "transferFrom": { + "message": "Transfer From" + }, "transfers": { "message": "Trasferimenti" }, + "trezorHardwareWallet": { + "message": "TREZOR Portafoglio Hardware" + }, "troubleTokenBalances": { "message": "Abbiamo avuto un problema a caricare il bilancio dei tuoi token. Puoi vederlo ", "description": "Seguito da un link (qui) per vedere il bilancio dei token" }, + "tryAgain": { + "message": "Prova di nuovo" + }, "twelveWords": { "message": "Queste 12 parole sono l'unico modo per ripristinare i tuoi account MetaMask. \nSalvale in un posto sicuro e segreto." }, @@ -764,18 +1194,45 @@ "uiWelcomeMessage": { "message": "Stai utilizzanto la nuova interfaccia di MetaMask. Guarda in giro, prova nuove funzionalità come inviare token, e facci sapere se hai dei problemi." }, + "unapproved": { + "message": "Non approvata" + }, "unavailable": { "message": "Non Disponibile" }, + "units": { + "message": "unità" + }, "unknown": { "message": "Sconosciuto" }, + "unknownFunction": { + "message": "Funzione Sconosciuta" + }, "unknownNetwork": { "message": "Rete Privata Sconosciuta" }, "unknownNetworkId": { "message": "ID rete sconosciuto" }, + "unknownQrCode": { + "message": "Errore: Non siamo riusciti a identificare il codice QR" + }, + "unknownCameraErrorTitle": { + "message": "Ooops! Qualcosa è andato storto...." + }, + "unknownCameraError": { + "message": "C'è stato un errore nel tentativo di accedere alla fotocamera. Per favore prova di nuovo..." + }, + "unlock": { + "message": "Sblocca" + }, + "unlockMessage": { + "message": "Il web decentralizzato ti attende" + }, + "updatedWithDate": { + "message": "Aggiornata $1" + }, "uriErrorMsg": { "message": "Gli URI richiedono un prefisso HTTP/HTTPS." }, @@ -798,22 +1255,40 @@ "viewAccount": { "message": "Vedi Account" }, + "viewOnEtherscan": { + "message": "Vedi su Etherscan" + }, "visitWebSite": { "message": "Visita il nostro sito web" }, + "walletSeed": { + "message": "Seed del Portafoglio" + }, "warning": { "message": "Attenzione" }, + "welcomeBack": { + "message": "Bentornato!" + }, "welcomeBeta": { "message": "Benvenuto nella Beta di MetaMask" }, "whatsThis": { "message": "Cos'è questo?" }, + "yesLetsTry": { + "message": "Si, proviamo" + }, + "youNeedToAllowCameraAccess": { + "message": "Devi consentire l'accesso alla fotocamera per usare questa funzionalità." + }, "yourSigRequested": { "message": "E' richiesta la tua firma" }, "youSign": { "message": "Ti stai connettendo" + }, + "yourPrivateSeedPhrase": { + "message": "La tua frase seed privata" } } diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 55549bb87..c8d470188 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -14,9 +14,15 @@ "accountName": { "message": "계정 이름" }, + "accountOptions": { + "message": "계정 옵션" + }, "accountSelectionRequired": { "message": "계정을 선택하셔야 합니다!" }, + "activityLog": { + "message": "활동 로그" + }, "address": { "message": "주소" }, @@ -29,6 +35,9 @@ "addTokens": { "message": "토큰 추가" }, + "addSuggestedTokens": { + "message": "제안된 토큰 추가" + }, "addAcquiredTokens": { "message": "메타마스크를 통해 획득한 토큰 추가" }, @@ -55,6 +64,9 @@ "attemptingConnect": { "message": "블록체인에 접속을 시도하는 중입니다." }, + "attemptToCancel": { + "message": "취소 하시겠습니까?" + }, "attributions": { "message": "속성" }, @@ -110,6 +122,15 @@ "cancel": { "message": "취소" }, + "cancelAttempt": { + "message": "취소 시도" + }, + "cancellationGasFee": { + "message": "취소 가스 수수료" + }, + "cancelN": { + "message": "모든 $1 트랜잭션 취소" + }, "classicInterface": { "message": "예전 인터페이스" }, @@ -220,7 +241,10 @@ "description": "거래 유형 (암호화폐)" }, "currentConversion": { - "message": "선택된 단위" + "message": "현재 통화" + }, + "currentLanguage": { + "message": "현재 언어" }, "currentNetwork": { "message": "현재 네트워크" @@ -340,6 +364,9 @@ "exchangeRate": { "message": "환율" }, + "expandView": { + "message": "큰 화면으로 보기" + }, "exportPrivateKey": { "message": "개인키 내보내기" }, @@ -457,6 +484,9 @@ "hideTokenPrompt": { "message": "토큰 숨기기?" }, + "history": { + "message": "히스토리" + }, "howToDeposit": { "message": "어떤 방법으로 이더를 입금하시겠습니까?" }, @@ -486,6 +516,9 @@ "importUsingSeed": { "message": "계정 시드 구문으로 가져오기" }, + "info": { + "message": "정보" + }, "infoHelp": { "message": "정보 및 도움말" }, @@ -539,7 +572,7 @@ "message": "최대" }, "learnMore": { - "message": "더 배우기." + "message": "더 알아보기." }, "ledgerAccountRestriction": { "message": "새 계정을 추가하려면 최소 마지막 계정을 사용해야 합니다." @@ -590,6 +623,9 @@ "metamaskDescription": { "message": "메타마스크는 이더리움을 위한 안전한 신분 저장소입니다." }, + "metamaskVersion": { + "message": "메타마스크 버전" + }, "metamaskSeedWords": { "message": "메타마스크 시드 단어" }, @@ -610,7 +646,7 @@ "description": "사용자는 계정을 가져오기 위해서 파일을 추가후 계속 진행해야 합니다" }, "needImportPassword": { - "message": "선택 된 파일에 대한 비밀번호를 입력해주세요.", + "message": "선택된 파일에 대한 비밀번호를 입력해주세요.", "description": "계정을 가져오기 위해서 비밀번호와 파일이 필요합니다." }, "negativeETH": { @@ -727,6 +763,9 @@ "pasteSeed": { "message": "시드 구문을 이곳에 붙여넣어 주세요!" }, + "pending": { + "message": "펜딩 중" + }, "personalAddressDetected": { "message": "개인 주소가 탐지됨. 토큰 컨트랙트 주소를 입력하세요." }, @@ -755,6 +794,9 @@ "qrCode": { "message": "QR 코드 보기" }, + "queue": { + "message": "큐" + }, "readdToken": { "message": "옵션 메뉴에서 “토큰 추가”를 눌러서 추후에 다시 이 토큰을 추가하실 수 있습니다." }, @@ -773,6 +815,18 @@ "refundAddress": { "message": "환불받을 주소" }, + "reject": { + "message": "거부" + }, + "rejectAll": { + "message": "모두 거부" + }, + "rejectTxsN": { + "message": "$1 트랜잭션 거부" + }, + "rejectTxsDescription": { + "message": "$1 트랜잭션을 거부합니다." + }, "rejected": { "message": "거부됨" }, @@ -892,6 +946,9 @@ "selectCurrency": { "message": "통화 선택" }, + "selectLocale": { + "message": "언어 선택" + }, "selectService": { "message": "서비스 선택" }, @@ -907,6 +964,12 @@ "sendTokens": { "message": "토큰 전송" }, + "sentEther": { + "message": "전송된 이더" + }, + "sentTokens": { + "message": "전송된 토큰" + }, "separateEachWord": { "message": "각 단어는 공백 한칸으로 분리합니다" }, @@ -934,9 +997,6 @@ "settings": { "message": "설정" }, - "info": { - "message": "정보" - }, "shapeshiftBuy": { "message": "Shapeshift를 통해서 구매하기" }, @@ -946,12 +1006,21 @@ "showQRCode": { "message": "QR 코드 보기" }, + "showHexData": { + "message": "Hex 데이터 보기" + }, + "showHexDataDescription": { + "message": "선택하면 전송화면의 hex 데이터 필드 값을 보여줍니다." + }, "sign": { "message": "서명" }, "signed": { "message": "서명됨" }, + "signatureRequest": { + "message": "서명 요청" + }, "signMessage": { "message": "메시지 서명" }, @@ -1049,6 +1118,9 @@ "total": { "message": "합계" }, + "transaction": { + "message": "트랜잭션" + }, "transactions": { "message": "트랜잭션" }, @@ -1095,6 +1167,9 @@ "unavailable": { "message": "이용할 수 없음" }, + "units": { + "message": "단위" + }, "unknown": { "message": "알 수 없음" }, @@ -1165,11 +1240,14 @@ "whatsThis": { "message": "이것은 무엇인가요?" }, + "yesLetsTry": { + "message": "네, 시도해보겠습니다." + }, "yourSigRequested": { "message": "서명을 요청 중입니다." }, "youSign": { - "message": "서명 중입니다" + "message": "서명합니다" }, "yourPrivateSeedPhrase": { "message": "개인 시드 구문" diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json new file mode 100644 index 000000000..c6d797c34 --- /dev/null +++ b/app/_locales/pl/messages.json @@ -0,0 +1,1213 @@ +{ + "accept": { + "message": "Akceptacja" + }, + "accessingYourCamera": { + "message": "Uruchamianie kamery..." + }, + "account": { + "message": "Konto" + }, + "accountDetails": { + "message": "Szczegóły konta" + }, + "accountName": { + "message": "Nazwa konta" + }, + "accountSelectionRequired": { + "message": "Należy wybrać konto!" + }, + "address": { + "message": "Adres" + }, + "addCustomToken": { + "message": "Dodaj token" + }, + "addToken": { + "message": "Dodaj token" + }, + "addTokens": { + "message": "Dodaj tokeny" + }, + "addSuggestedTokens": { + "message": "Dodaj sugerowane tokeny." + }, + "addAcquiredTokens": { + "message": "Dodaj tokeny pozyskane przy pomocy MetaMask" + }, + "amount": { + "message": "Ilość" + }, + "amountPlusGas": { + "message": "Ilość + gaz" + }, + "appDescription": { + "message": "Wtyczka przeglądarki do Ethereum", + "description": "Opis aplikacji" + }, + "appName": { + "message": "MetaMask", + "description": "Nazwa aplikacji" + }, + "approve": { + "message": "Zatwierdź" + }, + "approved": { + "message": "Zatwierdzone" + }, + "attemptingConnect": { + "message": "Próba połączenia z blockchainem." + }, + "attributions": { + "message": "Atrybuty" + }, + "available": { + "message": "Dostępne" + }, + "back": { + "message": "Wstecz" + }, + "balance": { + "message": "Ilość środków" + }, + "balances": { + "message": "Ilość tokenów" + }, + "balanceIsInsufficientGas": { + "message": "Niewystarczająca ilość środków na opłatę za gaz" + }, + "beta": { + "message": "BETA" + }, + "betweenMinAndMax": { + "message": "musi być większe lub równe $1 i mniejsze lub równe $2,", + "description": "pomoc przy wpisywaniu hex jako dane dziesiętne" + }, + "blockiesIdenticon": { + "message": "Użyj Blockies Identicon" + }, + "borrowDharma": { + "message": "Pożycz z Dharma (Beta)" + }, + "browserNotSupported": { + "message": "Twoja przeglądarka nie jest obsługiwana..." + }, + "builtInCalifornia": { + "message": "MetaMask został zaprojektowany i stworzony w Kaliforni." + }, + "buy": { + "message": "Kup" + }, + "buyCoinbase": { + "message": "Kup na Coinbase" + }, + "buyCoinbaseExplainer": { + "message": "Coinbase to najpopularniejszy sposób na kupno i sprzedaż Bitcoin, Ethereum i Litecoin." + }, + "bytes": { + "message": "Bajty" + }, + "ok": { + "message": "Ok" + }, + "cancel": { + "message": "Anuluj" + }, + "classicInterface": { + "message": "Użyj klasycznego interfejsu" + }, + "clickCopy": { + "message": "Kliknij żeby skopiować" + }, + "close": { + "message": "Zamknij" + }, + "chromeRequiredForHardwareWallets": { + "message": "Żeby połączyć się z portfelem sprzętowym, należy uruchomić MetaMask z przeglądarką Google Chrome." + }, + "confirm": { + "message": "Potwierdź" + }, + "confirmed": { + "message": "Potwierdzone" + }, + "confirmContract": { + "message": "Zatwierdź kontrakt" + }, + "confirmPassword": { + "message": "Potwierdź hasło" + }, + "confirmTransaction": { + "message": "Potwierdź transakcję" + }, + "connectHardwareWallet": { + "message": "Podłącz portfel sprzętowy" + }, + "connect": { + "message": "Połącz" + }, + "connecting": { + "message": "Łączenie..." + }, + "connectToLedger": { + "message": "Połącz z Ledger" + }, + "connectToTrezor": { + "message": "Połącz z Trezor" + }, + "continue": { + "message": "Kontynuuj" + }, + "continueToCoinbase": { + "message": "Przejdź do Coinbase" + }, + "contractDeployment": { + "message": "Uruchomienie kontraktu" + }, + "conversionProgress": { + "message": "Przeliczanie w toku" + }, + "copiedButton": { + "message": "Skopiowane" + }, + "copiedClipboard": { + "message": "Skopiowane do schowka" + }, + "copiedExclamation": { + "message": "Skopiowane!" + }, + "copiedSafe": { + "message": "Skopiowałem to w bezpieczne miejsce" + }, + "copy": { + "message": "Skopiuj" + }, + "copyContractAddress": { + "message": "Skopiuj adres kontaktowy" + }, + "copyAddress": { + "message": "Skopiuj adres do schowka" + }, + "copyToClipboard": { + "message": "Skopiuj do schowka" + }, + "copyButton": { + "message": " Skopiuj " + }, + "copyPrivateKey": { + "message": "To jest Twój prywatny klucz (kliknij żeby skopiować)" + }, + "create": { + "message": "Utwórz" + }, + "createAccount": { + "message": "Utwórz konto" + }, + "createDen": { + "message": "Utwórz" + }, + "crypto": { + "message": "Krypto", + "description": "Tym platformy wymiany (kryptowaluty)" + }, + "currentConversion": { + "message": "Obecny kurs" + }, + "currentNetwork": { + "message": "Bieżąca sieć" + }, + "customGas": { + "message": "Ustaw gaz" + }, + "customToken": { + "message": "Własny token" + }, + "customize": { + "message": "Ustaw" + }, + "customRPC": { + "message": "Własne RPC" + }, + "decimalsMustZerotoTen": { + "message": "Liczb po przecinku musi być co najmniej 0 i nie więcej niż 36." + }, + "decimal": { + "message": "Dokładność liczb po przecinku" + }, + "defaultNetwork": { + "message": "Domyślna sieć dla Eteru to Main Net." + }, + "denExplainer": { + "message": "Twój DEN to chroniony hasłem schowek w MetaMasku." + }, + "deposit": { + "message": "Zdeponuj" + }, + "depositBTC": { + "message": "Zdeponuj swoje BTC na poniższy adres:" + }, + "depositCoin": { + "message": "Zdeponuj $1 na poniższy adres", + "description": "Pokazuje użytkownikowi jakie waluty wybrał do zdeponowania w ShapeShift" + }, + "depositEth": { + "message": "Zdeponuj Eth" + }, + "depositEther": { + "message": "Zdeponuj Eter" + }, + "depositFiat": { + "message": "Zdeponuj w Fiat" + }, + "depositFromAccount": { + "message": "Zdeponuj z innego konta" + }, + "depositShapeShift": { + "message": "Zdeponuj przez ShapeShift" + }, + "depositShapeShiftExplainer": { + "message": "Jeśli posiadasz inne kryptowaluty, możesz nimi handlować i deponować Eter bezpośrednio do swojego portfela MetaMask. Nie trzeba żadnego konta." + }, + "details": { + "message": "Szczegóły" + }, + "directDeposit": { + "message": "Bezpośredni depozyt" + }, + "directDepositEther": { + "message": "Zdeponuj Eter bezpośrednio" + }, + "directDepositEtherExplainer": { + "message": "Jeśli już masz Eter, najszybciej umieścisz go w swoim nowym portfelu przy pomocy bezpośredniego depozytu." + }, + "done": { + "message": "Gotowe" + }, + "downloadGoogleChrome": { + "message": "Ściągnij Google Chrome" + }, + "downloadStateLogs": { + "message": "Załaduj logi stanów" + }, + "dontHaveAHardwareWallet": { + "message": "Nie masz portfela sprzętowego?" + }, + "dropped": { + "message": "Odrzucone" + }, + "edit": { + "message": "Edytuj" + }, + "editAccountName": { + "message": "Edytuj nazwę konta" + }, + "editingTransaction": { + "message": "Dokonaj zmian w swojej transakcji" + }, + "emailUs": { + "message": "Napisz do nas!" + }, + "encryptNewDen": { + "message": "Zaszyfruj swój nowy DEN" + }, + "ensNameNotFound": { + "message": "Nie znaleziono nazwy ENS" + }, + "enterPassword": { + "message": "Wpisz hasło" + }, + "enterPasswordConfirm": { + "message": "Wpisz hasło żeby potwierdzić" + }, + "enterPasswordContinue": { + "message": "Podaj hasło żeby kontynuować" + }, + "parameters": { + "message": "Parametry" + }, + "passwordNotLongEnough": { + "message": "Hasło jest za krótkie" + }, + "passwordsDontMatch": { + "message": "Hasła są niezgodne" + }, + "etherscanView": { + "message": "Zobacz konto na Etherscan" + }, + "exchangeRate": { + "message": "Kurs wymiany" + }, + "exportPrivateKey": { + "message": "Eksportuj klucz prywatny" + }, + "exportPrivateKeyWarning": { + "message": "Eksportujesz prywatne klucze na własne ryzyko." + }, + "failed": { + "message": "Nie udało się" + }, + "fiat": { + "message": "FIAT", + "description": "Rodzaj wymiany" + }, + "fileImportFail": { + "message": "Importowanie pliku nie działa? Kliknij tutaj!", + "description": "Wspomaga użytkowników przy importowaniu ich konta z pliku JSON" + }, + "followTwitter": { + "message": "Śledź nas na Twitterze" + }, + "forgetDevice": { + "message": "Usuń to urządzenie." + }, + "from": { + "message": "Z" + }, + "fromToSame": { + "message": "Adresy Z i Do nie mogą być identyczne" + }, + "fromShapeShift": { + "message": "Z ShapeShift" + }, + "functionType": { + "message": "Typ funkcji" + }, + "gas": { + "message": "Gaz", + "description": "Krótkie oznaczenie kosztu gazu" + }, + "gasFee": { + "message": "Opłata za gaz" + }, + "gasLimit": { + "message": "Limit gazu" + }, + "gasLimitCalculation": { + "message": "Obliczamy sugerowany limit gazu na podstawie danych z transakcji w sieci." + }, + "gasLimitRequired": { + "message": "Limit gazu jest wymagany" + }, + "gasLimitTooLow": { + "message": "Limit gazu musi wynosić co najmniej 21000" + }, + "generatingSeed": { + "message": "Generowanie seed..." + }, + "gasPrice": { + "message": "Cena gazu (GWEI)" + }, + "gasPriceCalculation": { + "message": "Obliczamy ceny gazu na podstawie danych z transakcji w sieci." + }, + "gasPriceRequired": { + "message": "Wymagana cena gazu" + }, + "generatingTransaction": { + "message": "Generowanie transakcji" + }, + "getEther": { + "message": "Zdobądź Eter" + }, + "getEtherFromFaucet": { + "message": "Zdobądź Eter ze źródła za $1", + "description": "Wyświetla nazwę sieci dla źródła Eteru" + }, + "getHelp": { + "message": "Po pomoc." + }, + "greaterThanMin": { + "message": "musi być większe lub równe $1.", + "description": "pomoc przy wpisywaniu hex jako dane dziesiętne" + }, + "hardware": { + "message": "sprzęt" + }, + "hardwareWalletConnected": { + "message": "Podłączono sprzętowy portfel" + }, + "hardwareWallets": { + "message": "Podłącz sprzętowy portfel" + }, + "hardwareWalletsMsg": { + "message": "Wybierz portfel sprzętowy, którego chcesz użyć z MetaMaskiem" + }, + "havingTroubleConnecting": { + "message": "Problem z połączeniem?" + }, + "here": { + "message": "tutaj", + "description": "jak w -kliknij tutaj- po więcej informacji (połączone z troubleTokenBalances)" + }, + "hereList": { + "message": "Oto lista!!!" + }, + "hexData": { + "message": "Dane Hex" + }, + "hide": { + "message": "Schowaj" + }, + "hideToken": { + "message": "Schowaj token" + }, + "hideTokenPrompt": { + "message": "Schować token?" + }, + "history": { + "message": "Historia" + }, + "howToDeposit": { + "message": "Jak chcesz zdeponować Eter?" + }, + "holdEther": { + "message": "Umożliwia przechowywanie eteru i tokenów oraz służy jako łącznik do zdecentralizowanych aplikacji." + }, + "import": { + "message": "Importuj", + "description": "Przycisk do importowania konta z wybranego pliku." + }, + "importAccount": { + "message": "Importuj konto" + }, + "importAccountMsg": { + "message": " Importowane konta nie będą powiązane z Twoją pierwotną frazą seed MetaMask. Dowiedz się więcej o importowaniu kont " + }, + "importAnAccount": { + "message": "Importuj konto" + }, + "importDen": { + "message": "Importuj istniejące DEN" + }, + "imported": { + "message": "Zaimportowane", + "description": "status pokazujący, że konto zostało w pełni załadowane na keyring" + }, + "importUsingSeed": { + "message": "Importuj przy pomocy frazy seed konta" + }, + "infoHelp": { + "message": "Info & pomoc" + }, + "initialTransactionConfirmed": { + "message": "Twoja transakcja została potwierdzona w sieci. Kliknij OK żeby wrócić." + }, + "insufficientFunds": { + "message": "Niewystarczające środki." + }, + "insufficientTokens": { + "message": "Niewystarczająca liczba tokenów." + }, + "invalidAddress": { + "message": "Nieprawidłowy adres" + }, + "invalidAddressRecipient": { + "message": "Nieprawidłowy adres odbiorcy" + }, + "invalidGasParams": { + "message": "Nieprawidłowe parametry gazu" + }, + "invalidInput": { + "message": "Nieprawidłowe dane." + }, + "invalidRequest": { + "message": "Nieprawidłowe zapytanie" + }, + "invalidRPC": { + "message": "Nieprawidłowe RPC URI" + }, + "invalidSeedPhrase": { + "message": "Nieprawidłowa fraza seed" + }, + "jsonFail": { + "message": "Coś poszło nie tak. Upewnij się, że plik JSON jest prawidłowo sformatowany." + }, + "jsonFile": { + "message": "Plik JSON", + "description": "formatuj do importowania konta" + }, + "keepTrackTokens": { + "message": "Monitoruj stan tokenów kupionych przy pomocy konta MetaMask." + }, + "kovan": { + "message": "Sieć testowa Kovan" + }, + "knowledgeDataBase": { + "message": "Sprawdź naszą Bazę wiedzy." + }, + "max": { + "message": "Maks." + }, + "learnMore": { + "message": "Dowiedz się więcej" + }, + "ledgerAccountRestriction": { + "message": "Musisz użyć swojego poprzedniego konta zanim dodasz kolejne." + }, + "lessThanMax": { + "message": "musi być mniejsze lub równe $1.", + "description": "pomoc przy wpisywaniu hex jako dane dziesiętne" + }, + "likeToAddTokens": { + "message": "Czy chcesz dodać te tokeny?" + }, + "links": { + "message": "Łącza" + }, + "limit": { + "message": "Limit" + }, + "loading": { + "message": "Ładowanie..." + }, + "loadingTokens": { + "message": "Ładowanie tokenów..." + }, + "localhost": { + "message": "Serwer lokalny 8545" + }, + "login": { + "message": "Zaloguj się" + }, + "logout": { + "message": "Wyloguj się" + }, + "loose": { + "message": "Porzuć" + }, + "loweCaseWords": { + "message": "słowa seed mogą być pisane wyłącznie małymi literami" + }, + "mainnet": { + "message": "Główna sieć Ethereum" + }, + "menu": { + "message": "Menu" + }, + "message": { + "message": "Wiadomość" + }, + "metamaskDescription": { + "message": "MetaMask to bezpieczny portfel dla Ethereum." + }, + "metamaskSeedWords": { + "message": "Słowa Seed MetaMask" + }, + "min": { + "message": "Minimum" + }, + "myAccounts": { + "message": "Moje konta" + }, + "mustSelectOne": { + "message": "Należy wybrać co najmniej 1 token." + }, + "needEtherInWallet": { + "message": "Żeby skorzystać ze zdecentraliowanych aplikacji (dApps) przy pomocy MetaMask, potrzebujesz Eteru w swoim portfelu." + }, + "needImportFile": { + "message": "Musisz wybrać plik do zaimportowania.", + "description": "Użytkownik importuje konto i musi dodać plik, żeby kontynuować" + }, + "needImportPassword": { + "message": "Musisz podać hasło dla wybranego pliku.", + "description": "Hasło i plik niezbędne do zaimportowania konta" + }, + "negativeETH": { + "message": "Nie można wysłać ujemnych ilości ETH." + }, + "networks": { + "message": "Sieci" + }, + "nevermind": { + "message": "Nie ważne" + }, + "newAccount": { + "message": "Nowe konto" + }, + "newAccountNumberName": { + "message": "Konto $1", + "description": "Automatyczna nazwa kolejnego konta utworzonego w widoku Utwórz konto" + }, + "newContract": { + "message": "Nowy kontrakt" + }, + "newPassword": { + "message": "Nowe hasło (min. 8 znaków)" + }, + "newRecipient": { + "message": "Nowy odbiorca" + }, + "newRPC": { + "message": "Nowy RPC URL" + }, + "next": { + "message": "Dalej" + }, + "noAddressForName": { + "message": "Nie wybrano żadnego adresu dla tej nazwy." + }, + "noDeposits": { + "message": "Brak otrzymanych depozytów" + }, + "noConversionRateAvailable": { + "message": "Brak kursu waluty" + }, + "noTransactionHistory": { + "message": "Brak historii transakcji." + }, + "noTransactions": { + "message": "Nie ma transakcji" + }, + "notFound": { + "message": "Nie znaleziono" + }, + "notStarted": { + "message": "Nie rozpoczęto" + }, + "noWebcamFoundTitle": { + "message": "Nie znaleziono kamery" + }, + "noWebcamFound": { + "message": "Twoja kamera nie została znaleziona. Spróbuj ponownie." + }, + "oldUI": { + "message": "Stary interfejs" + }, + "oldUIMessage": { + "message": "Wróciłeś do starego interfejsu. Możesz włączyć nowy interfejs przez opcje w rozwijanym menu w prawym górnym rogu." + }, + "openInTab": { + "message": "Otwórz w zakładce" + }, + "or": { + "message": "lub", + "description": "wybór między tworzeniem i importowaniem nowego konta" + }, + "origin": { + "message": "Pochodzenie" + }, + "password": { + "message": "Hasło" + }, + "passwordCorrect": { + "message": "Upewnij się, że Twoje hasło jest poprawne." + }, + "passwordMismatch": { + "message": "hasła nie są takie same", + "description": "podczas tworzenia hasła, tekst w dwóch polach haseł nie był taki sam" + }, + "passwordShort": { + "message": "hasło za krótkie", + "description": "podczas tworzenia hasła, hasło nie jest bezpieczne, ponieważ nie jest wystarczająco długie" + }, + "pastePrivateKey": { + "message": "Tutaj wklej swój prywatny klucz:", + "description": "Do importowania konta z prywatnego klucza" + }, + "pasteSeed": { + "message": "Tutaj wklej swoją frazę seed!" + }, + "pending": { + "message": "oczekiwanie" + }, + "personalAddressDetected": { + "message": "Wykryto osobisty adres. Wprowadź adres kontraktu tokenów." + }, + "pleaseReviewTransaction": { + "message": "Proszę, sprawdź transakcję." + }, + "popularTokens": { + "message": "Popularne tokeny" + }, + "prev": { + "message": "Poprzednie" + }, + "privacyMsg": { + "message": "Polityka prywatności" + }, + "privateKey": { + "message": "Klucz prywatny", + "description": "wybierz ten typ pliku żeby importować konto" + }, + "privateKeyWarning": { + "message": "Uwaga: Nie ujawniaj nikomu tego klucza. Ktokolwiek posiadający Twoje prywatne klucze może użyć środków znajdujących się na Twoim koncie." + }, + "privateNetwork": { + "message": "Sieć prywatna" + }, + "qrCode": { + "message": "Pokaż kod QR" + }, + "queue": { + "message": "Kolejka" + }, + "readdToken": { + "message": "Możesz później ponownie dodać ten token poprzez \"Dodaj token\" w opcjach menu swojego konta." + }, + "readMore": { + "message": "Dowiedz się więcej tutaj." + }, + "readMore2": { + "message": "Dowiedz się więcej." + }, + "receive": { + "message": "Otrzymaj" + }, + "recipientAddress": { + "message": "Adres odbiorcy" + }, + "refundAddress": { + "message": "Twój adres na zwroty" + }, + "rejected": { + "message": "Odrzucone" + }, + "reset": { + "message": "Reset" + }, + "resetAccount": { + "message": "Resetuj konto" + }, + "resetAccountDescription": { + "message": "Zresetowanie konta wyczyści Twoją historię transakcji." + }, + "restoreFromSeed": { + "message": "Przywrócić konto?" + }, + "restoreVault": { + "message": "Przywróć schowek" + }, + "restoreAccountWithSeed": { + "message": "Przywróć konto frazą seed" + }, + "required": { + "message": "Wymagane" + }, + "retryWithMoreGas": { + "message": "Spróbuj ponownie z większą ceną gazu" + }, + "walletSeed": { + "message": "Seed portfela" + }, + "restore": { + "message": "Przywróć" + }, + "revealSeedWords": { + "message": "Pokaż słowa seed" + }, + "revealSeedWordsTitle": { + "message": "Fraza seed" + }, + "revealSeedWordsDescription": { + "message": "Jeśli kiedyś zmienisz przeglądarkę lub komputer, będziesz potrzebować tej frazy seed, żeby dostać się do swoich kont. Zapisz ją w bezpiecznym miejscu." + }, + "revealSeedWordsWarningTitle": { + "message": "NIE pokazuj tej frazy nikomu!" + }, + "revealSeedWordsWarning": { + "message": "Te słowa mogą być użyte żeby ukraść Twoje konta." + }, + "revert": { + "message": "Wycofaj" + }, + "remove": { + "message": "usuń" + }, + "removeAccount": { + "message": "Usuń konto" + }, + "removeAccountDescription": { + "message": "To konto będzie usunięte z Twojego portfela. Zanim przejdziesz dalej, upewnij się, że masz frazę seed i klucz prywatny do tego importowanego konta. Możesz później importować lub utworzyć nowe konta z rozwijanego menu kont. " + }, + "readyToConnect": { + "message": "Gotowy na połączenie?" + }, + "rinkeby": { + "message": "Sieć testowa Rinkeby" + }, + "ropsten": { + "message": "Sieć testowa Ropsten" + }, + "rpc": { + "message": "Indywidualne RPC" + }, + "currentRpc": { + "message": "Obecne RPC" + }, + "connectingToMainnet": { + "message": "Łączenie z główną siecią Ethereum" + }, + "connectingToRopsten": { + "message": "Łączenie z siecią testową Ropsten" + }, + "connectingToKovan": { + "message": "Łączenie z siecią testową Kovan" + }, + "connectingToRinkeby": { + "message": "Łączenie z siecią testową Rinkeby" + }, + "connectingToUnknown": { + "message": "Łączenie z nieznaną siecią" + }, + "sampleAccountName": { + "message": "Np. Moje nowe konto", + "description": "Umożliwia użytkownikom zrozumieć ideę dodawania własnej nazwy to ich konta" + }, + "save": { + "message": "Zapisz" + }, + "speedUpTitle": { + "message": "Przyspiesz transakcję" + }, + "speedUpSubtitle": { + "message": "Zwiększ cenę gazu żeby nadpisać i przyspieszyć transakcję" + }, + "saveAsCsvFile": { + "message": "Zapisz jako plik CSV" + }, + "saveAsFile": { + "message": "Zapisz jako", + "description": "Proces eksportu konta" + }, + "saveSeedAsFile": { + "message": "Zapisz słowa seed jako plik" + }, + "search": { + "message": "Szukaj" + }, + "searchResults": { + "message": "Wyniki wyszukiwania" + }, + "secretPhrase": { + "message": "Żeby otworzyć schowek, wpisz tutaj swoją frazę dwunastu słów." + }, + "showHexData": { + "message": "Pokaż dane hex" + }, + "showHexDataDescription": { + "message": "Wybierz to żeby pokazać pole danych hex na ekranie wysyłania" + }, + "newPassword8Chars": { + "message": "Nowe hasło (min. 8 znaków)" + }, + "seedPhraseReq": { + "message": "Frazy seed mają 12 słów" + }, + "select": { + "message": "Wybierz" + }, + "selectCurrency": { + "message": "Wybierz walutę" + }, + "selectService": { + "message": "Wybierz usługę" + }, + "selectType": { + "message": "Wybierz rodzaj" + }, + "send": { + "message": "Wyślij" + }, + "sendETH": { + "message": "Wyślij ETH" + }, + "sendTokens": { + "message": "Wyślij tokeny" + }, + "sentEther": { + "message": "wyślij eter" + }, + "sentTokens": { + "message": "wysłane tokeny" + }, + "separateEachWord": { + "message": "Oddziel słowa pojedynczą spacją" + }, + "onlySendToEtherAddress": { + "message": "Na adres Ethereum wysyłaj tylko ETH." + }, + "onlySendTokensToAccountAddress": { + "message": "Wyślij tylko $1 na adres konta Ethereum.", + "description": "wyświetla symbol tokena" + }, + "orderOneHere": { + "message": "Zamów Trezor lub Ledger i trzymaj swoje środki w portfelu sprzętowym." + }, + "outgoing": { + "message": "Wychodzące" + }, + "searchTokens": { + "message": "Szukaj tokenów" + }, + "selectAnAddress": { + "message": "Wybierz adres" + }, + "selectAnAccount": { + "message": "Wybierz konto" + }, + "selectAnAccountHelp": { + "message": "Wybierz konto do przeglądania w MetaMask" + }, + "selectHdPath": { + "message": "Wybierz ścieżkę HD" + }, + "selectPathHelp": { + "message": "Jeśli nie widzisz poniżej swoich kont Ledger, spróbuj przełączyć się na \"Legacy (MEW / MyCrypto)\"" + }, + "sendTokensAnywhere": { + "message": "Wyślij tokeny do kogoś z adresem Ethereum" + }, + "settings": { + "message": "Ustawienia" + }, + "step1HardwareWallet": { + "message": "1. Podłącz portfel sprzętowy" + }, + "step1HardwareWalletMsg": { + "message": "Połącz swój portfel sprzętowy z komputerem." + }, + "step2HardwareWallet": { + "message": "2. Wybierz konto" + }, + "step2HardwareWalletMsg": { + "message": "Wybierz konto, które chcesz przeglądać. Możesz wybrać tylko jedno konto w danym momencie." + }, + "step3HardwareWallet": { + "message": "3. Zacznij używać dystrybuowanych aplikacji (dApps) i wiele więcej!" + }, + "step3HardwareWalletMsg": { + "message": "Używaj swojego konta sprzętowego tak, jak używasz jakiegokolwiek konta z Ethereum. Loguj się do dystrybuowanych aplikacji (dApps), wysyłaj Eth, kupuj i przechowaj tokeny ERC20 i niewymienne tokeny, jak np. CryptoKitties." + }, + "info": { + "message": "Info" + }, + "scanInstructions": { + "message": "Umieść kod QR na wprost kamery" + }, + "scanQrCode": { + "message": "Skanuj kod QR" + }, + "shapeshiftBuy": { + "message": "Kup w ShapeShift" + }, + "showPrivateKeys": { + "message": "Pokaż prywatne klucze" + }, + "showQRCode": { + "message": "Pokaż kod QR" + }, + "sign": { + "message": "Podpisz" + }, + "signatureRequest": { + "message": "Prośba o podpis" + }, + "signed": { + "message": "Podpisane" + }, + "signMessage": { + "message": "Podpisz wiadomość" + }, + "signNotice": { + "message": "Podpisanie tej wiadomości może mieć \nniebezpieczne skutki uboczne. Podpisuj wiadomości \ntylko ze stron, którym chcesz udostępnić swoje konto.\nTa niebezpieczna metoda będzie usunięta w przyszłych wersjach. " + }, + "sigRequest": { + "message": "Prośba o podpis" + }, + "sigRequested": { + "message": "Podpis wymagany" + }, + "spaceBetween": { + "message": "między słowami może być tylko pojedyncza spacja" + }, + "status": { + "message": "Status" + }, + "stateLogs": { + "message": "Logi stanów" + }, + "stateLogsDescription": { + "message": "Logi stanów zawierają Twoje publiczne adresy kont i wykonanych transakcji." + }, + "stateLogError": { + "message": "Błąd podczas pobierania logów stanów." + }, + "submit": { + "message": "Wyślij" + }, + "submitted": { + "message": "Wysłane" + }, + "supportCenter": { + "message": "Odwiedź nasze Centrum Pomocy" + }, + "symbolBetweenZeroTen": { + "message": "Symbol musi mieć od 0 do 10 znaków." + }, + "takesTooLong": { + "message": "Trwa zbyt długo?" + }, + "terms": { + "message": "Regulamin" + }, + "testFaucet": { + "message": "Źródło testowego ETH" + }, + "to": { + "message": "Do" + }, + "toETHviaShapeShift": { + "message": "$1 na ETH przez ShapeShift", + "description": "system uzupełni typ depozytu na początku wiadomości" + }, + "token": { + "message": "Token" + }, + "tokenAddress": { + "message": "Adres tokena" + }, + "tokenAlreadyAdded": { + "message": "Token jest już dodany." + }, + "tokenBalance": { + "message": "Liczba Twoich tokenów:" + }, + "tokenSelection": { + "message": "Szukaj tokenów lub wybierz z naszej listy popularnych tokenów." + }, + "tokenSymbol": { + "message": "Symbol tokena" + }, + "tokenWarning1": { + "message": "Monitoruj stan tokenów kupionych przy pomocy konta MetaMask. Jeśli masz tokeny kupione przy pomocy innych kont, nie pojawią się tutaj." + }, + "total": { + "message": "Suma" + }, + "transactions": { + "message": "transakcje" + }, + "transactionError": { + "message": "Błąd transakcji. Wyjątek w kodzie kontraktu." + }, + "transactionMemo": { + "message": "Memo transakcji (opcjonalnie)" + }, + "transactionNumber": { + "message": "Numer transakcji" + }, + "transfer": { + "message": "Przelew" + }, + "transfers": { + "message": "Przelewy" + }, + "trezorHardwareWallet": { + "message": "Sprzętowy portfel TREZOR" + }, + "troubleTokenBalances": { + "message": "Wystąpił problem z załadowaniem informacji o Twoich tokenach. Można je zobaczyć ", + "description": "Z linkiem (tutaj) do informacji o stanie tokenów" + }, + "tryAgain": { + "message": "Spróbuj ponownie" + }, + "twelveWords": { + "message": "Tych 12 słów to jedyny sposób, żeby odzyskać konta w MetaMasku. Zapisz je w bezpiecznym miejscu." + }, + "typePassword": { + "message": "Wpisz hasło" + }, + "uiWelcome": { + "message": "Witamy w nowym interfejsie (Beta)" + }, + "uiWelcomeMessage": { + "message": "Używasz teraz nowego interfejsu MetaMask." + }, + "unapproved": { + "message": "Niezatwierdzone" + }, + "unavailable": { + "message": "Niedostępne" + }, + "unknown": { + "message": "Nieznane" + }, + "unknownFunction": { + "message": "Nieznana funkcja" + }, + "unknownNetwork": { + "message": "Nieznana sieć prywatna" + }, + "unknownNetworkId": { + "message": "Nieznane sieciowe ID" + }, + "unknownQrCode": { + "message": "Błąd: nie mogliśmy odczytać tego kodu QR" + }, + "unknownCameraErrorTitle": { + "message": "Ups! Coś poszło nie tak..." + }, + "unknownCameraError": { + "message": "Podczas łączenia się z kamerą wystąpił błąd. Spróbuj ponownie..." + }, + "unlock": { + "message": "Odblokuj" + }, + "unlockMessage": { + "message": "Zdecentralizowana sieć oczekuje" + }, + "uriErrorMsg": { + "message": "URI wymaga prawidłowego prefiksu HTTP/HTTPS." + }, + "usaOnly": { + "message": "Tylko USA", + "description": "Ta platforma wymiany jest dostępna tylko dla osób mieszkających w USA" + }, + "usedByClients": { + "message": "Używany przez różnych klientów" + }, + "useOldUI": { + "message": "Przełącz na stary interfejs" + }, + "validFileImport": { + "message": "Należy wybrać prawidłowy plik do zaimportowania." + }, + "vaultCreated": { + "message": "Schowek utworzony" + }, + "viewAccount": { + "message": "Zobacz konto" + }, + "viewOnEtherscan": { + "message": "Zobacz na Etherscan" + }, + "visitWebSite": { + "message": "Odwiedź naszą stronę" + }, + "warning": { + "message": "Uwaga" + }, + "welcomeBack": { + "message": "Witaj z powrotem!" + }, + "welcomeBeta": { + "message": "Witaj w MetaMask Beta" + }, + "whatsThis": { + "message": "Co to jest?" + }, + "youNeedToAllowCameraAccess": { + "message": "Żeby użyć tej opcji należy podłączyć kamerę" + }, + "yourSigRequested": { + "message": "Twój podpis jest wymagany" + }, + "youSign": { + "message": "Podpisujesz" + }, + "yourPrivateSeedPhrase": { + "message": "Twoja prywatna fraza seed" + } +}
\ No newline at end of file diff --git a/app/images/eth.svg b/app/images/eth.svg new file mode 100644 index 000000000..6375b790f --- /dev/null +++ b/app/images/eth.svg @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#38393A;} +</style> +<title>deposit-eth</title> +<desc>Created with Sketch.</desc> +<g id="deposit-eth" transform="translate(0.000000, 14.000000)"> + <path id="Shape" class="st0" d="M19.9,16L7.5,8.7L19.9,26L32.3,8.7L19.9,16L19.9,16z M20.1-14L7.7,6.4l12.4,7.3l12.4-7.2L20.1-14z" + /> +</g> +</svg> diff --git a/app/manifest.json b/app/manifest.json index cd34a586d..aabacd49a 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "4.14.0", + "version": "4.16.0", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", @@ -77,4 +77,4 @@ "*" ] } -} +}
\ No newline at end of file diff --git a/app/scripts/background.js b/app/scripts/background.js index 0343e134c..509a0001d 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -2,6 +2,9 @@ * @file The entry point for the web extension singleton process. */ +// this needs to run before anything else +require('./lib/setupFetchDebugging')() + const urlUtil = require('url') const endOfStream = require('end-of-stream') const pump = require('pump') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index d870741d6..33523eb46 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -135,17 +135,22 @@ function doctypeCheck () { } /** - * Checks the current document extension + * Returns whether or not the extension (suffix) of the current document is prohibited * - * @returns {boolean} {@code true} if the current extension is not prohibited + * This checks {@code window.location.pathname} against a set of file extensions + * that should not have web3 injected into them. This check is indifferent of query parameters + * in the location. + * + * @returns {boolean} whether or not the extension of the current document is prohibited */ function suffixCheck () { - var prohibitedTypes = ['xml', 'pdf'] - var currentUrl = window.location.href - var currentRegex + const prohibitedTypes = [ + /\.xml$/, + /\.pdf$/, + ] + const currentUrl = window.location.pathname for (let i = 0; i < prohibitedTypes.length; i++) { - currentRegex = new RegExp(`\\.${prohibitedTypes[i]}$`) - if (currentRegex.test(currentUrl)) { + if (prohibitedTypes[i].test(currentUrl)) { return false } } diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index fd6a4866d..8eb2bce0c 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -38,6 +38,9 @@ class PreferencesController { lostIdentities: {}, seedWords: null, forgottenPassword: false, + preferences: { + useETHAsPrimaryCurrency: true, + }, }, opts.initState) this.diagnostics = opts.diagnostics @@ -463,6 +466,33 @@ class PreferencesController { getFeatureFlags () { return this.store.getState().featureFlags } + + /** + * Updates the `preferences` property, which is an object. These are user-controlled features + * found in the settings page. + * @param {string} preference The preference to enable or disable. + * @param {boolean} value Indicates whether or not the preference should be enabled or disabled. + * @returns {Promise<object>} Promises a new object; the updated preferences object. + */ + setPreference (preference, value) { + const currentPreferences = this.getPreferences() + const updatedPreferences = { + ...currentPreferences, + [preference]: value, + } + + this.store.updateState({ preferences: updatedPreferences }) + return Promise.resolve(updatedPreferences) + } + + /** + * A getter for the `preferences` property + * @returns {object} A key-boolean map of user-selected preferences. + */ + getPreferences () { + return this.store.getState().preferences + } + // // PRIVATE METHODS // diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index a57c85f50..9f2290924 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -366,7 +366,40 @@ class TransactionController extends EventEmitter { this.txStateManager.setTxStatusSubmitted(txId) } - confirmTransaction (txId) { + /** + * Sets the status of the transaction to confirmed and sets the status of nonce duplicates as + * dropped if the txParams have data it will fetch the txReceipt + * @param {number} txId - The tx's ID + * @returns {Promise<void>} + */ + async confirmTransaction (txId) { + // get the txReceipt before marking the transaction confirmed + // to ensure the receipt is gotten before the ui revives the tx + const txMeta = this.txStateManager.getTx(txId) + + if (!txMeta) { + return + } + + try { + const txReceipt = await this.query.getTransactionReceipt(txMeta.hash) + + // It seems that sometimes the numerical values being returned from + // this.query.getTransactionReceipt are BN instances and not strings. + const gasUsed = typeof txReceipt.gasUsed !== 'string' + ? txReceipt.gasUsed.toString(16) + : txReceipt.gasUsed + + txMeta.txReceipt = { + ...txReceipt, + gasUsed, + } + + this.txStateManager.updateTx(txMeta, 'transactions#confirmTransaction - add txReceipt') + } catch (err) { + log.error(err) + } + this.txStateManager.setTxStatusConfirmed(txId) this._markNonceDuplicatesDropped(txId) } diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index daa6cc388..58c48e34e 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -400,6 +400,11 @@ class TransactionStateManager extends EventEmitter { */ _setTxStatus (txId, status) { const txMeta = this.getTx(txId) + + if (!txMeta) { + return + } + txMeta.status = status setTimeout(() => { try { diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 431702d63..b885a7e05 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -27,6 +27,8 @@ var metamaskStream = new LocalMessageDuplexStream({ // compose the inpage provider var inpageProvider = new MetamaskInpageProvider(metamaskStream) +// set a high max listener count to avoid unnecesary warnings +inpageProvider.setMaxListeners(100) // Augment the provider with its enable method inpageProvider.enable = function (options = {}) { diff --git a/app/scripts/lib/setupFetchDebugging.js b/app/scripts/lib/setupFetchDebugging.js new file mode 100644 index 000000000..dd87b65a6 --- /dev/null +++ b/app/scripts/lib/setupFetchDebugging.js @@ -0,0 +1,34 @@ +module.exports = setupFetchDebugging + +// +// This is a utility to help resolve cases where `window.fetch` throws a +// `TypeError: Failed to Fetch` without any stack or context for the request +// https://github.com/getsentry/sentry-javascript/pull/1293 +// + +function setupFetchDebugging() { + if (!global.fetch) return + const originalFetch = global.fetch + + global.fetch = wrappedFetch + + async function wrappedFetch(...args) { + const initialStack = getCurrentStack() + try { + return await originalFetch.call(window, ...args) + } catch (err) { + console.warn('FetchDebugger - fetch encountered an Error', err) + console.warn('FetchDebugger - overriding stack to point of original call') + err.stack = initialStack + throw err + } + } +} + +function getCurrentStack() { + try { + throw new Error('Fake error for generating stack trace') + } catch (err) { + return err.stack + } +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 493877345..32ceb6790 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -129,6 +129,7 @@ module.exports = class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.blockTracker, }) + // start and stop polling for balances based on activeControllerConnections this.on('controllerConnectionChanged', (activeControllerConnections) => { if (activeControllerConnections > 0) { @@ -137,7 +138,12 @@ module.exports = class MetamaskController extends EventEmitter { this.accountTracker.stop() } }) - + + // ensure accountTracker updates balances after network change + this.networkController.on('networkDidChange', () => { + this.accountTracker._updateAccounts() + }) + // key mgmt const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] this.keyringController = new KeyringController({ @@ -387,6 +393,7 @@ module.exports = class MetamaskController extends EventEmitter { setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController), setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController), + setPreference: nodeify(preferencesController.setPreference, preferencesController), // BlacklistController whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this), diff --git a/development/states/add-token.json b/development/states/add-token.json index d04b3a3ca..6a525f2b3 100644 --- a/development/states/add-token.json +++ b/development/states/add-token.json @@ -107,7 +107,10 @@ "maxModeOn": false, "editingTransactionId": null }, - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json index 5017a4d57..c7103cd13 100644 --- a/development/states/confirm-sig-requests.json +++ b/development/states/confirm-sig-requests.json @@ -150,7 +150,10 @@ "maxModeOn": false, "editingTransactionId": null }, - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json index 847ea11a3..7dea42ade 100644 --- a/development/states/currency-localization.json +++ b/development/states/currency-localization.json @@ -108,7 +108,10 @@ "maxModeOn": false, "editingTransactionId": null }, - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/development/states/first-time.json b/development/states/first-time.json index a31b985a3..3206b67a3 100644 --- a/development/states/first-time.json +++ b/development/states/first-time.json @@ -37,7 +37,10 @@ "shapeShiftTxList": [], "lostAccounts": [], "tokens": [], - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index bb4847155..d9924dd74 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -109,7 +109,10 @@ "maxModeOn": false, "editingTransactionId": null }, - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/development/states/tx-list-items.json b/development/states/tx-list-items.json index 0d2273cb0..cbffa98e5 100644 --- a/development/states/tx-list-items.json +++ b/development/states/tx-list-items.json @@ -102,7 +102,10 @@ "shapeShiftTxList": [{"depositAddress":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkke","depositType":"BTC","key":"shapeshift","response":{"status":"no_deposits","address":"34vJ3AfmNcLiziA4VFgEVcQTwxVLD1qkke"},"time":1522377459106}], "lostAccounts": [], "send": {}, - "currentLocale": "en" + "currentLocale": "en", + "preferences": { + "useETHAsPrimaryCurrency": true + } }, "appState": { "menuOpen": false, diff --git a/package-lock.json b/package-lock.json index 9bc5c068a..09b66d261 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9691,12 +9691,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -9771,12 +9772,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -9897,9 +9899,9 @@ } }, "eth-json-rpc-middleware": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/eth-json-rpc-middleware/-/eth-json-rpc-middleware-3.1.1.tgz", - "integrity": "sha512-5mRpjmszVQbKaUk3kiKkP9+hyyD3ZIg3mJ+jiydZ46cfNbrFVzfTDvZKnYLPrQPEi4+CaYqF+7XnIHIGztd9JQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/eth-json-rpc-middleware/-/eth-json-rpc-middleware-3.1.3.tgz", + "integrity": "sha512-glp/mCefhsqrgVOTTuYlHYiTL+9mMPfaZsuQv4vnRg3kqNigblS1nqARaMeVW9WOM8ssh9TqIFpuUr7JDgNmKQ==", "dev": true, "requires": { "async": "^2.5.0", @@ -10002,12 +10004,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -10068,12 +10071,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -10333,12 +10337,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -10371,21 +10376,21 @@ } }, "eth-token-tracker": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/eth-token-tracker/-/eth-token-tracker-1.1.4.tgz", - "integrity": "sha1-Kf8kV9Zr+juO5JDoP/QP0M8s7EE=", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/eth-token-tracker/-/eth-token-tracker-1.1.5.tgz", + "integrity": "sha512-dyNzEt62i5vpbAAHHj6kEVxSHg/WqCr7TBq1Sbs4y0PvsxcvfWLJpEYtJilndg36H7nJHGadgmHqGW5mYbcNfw==", "requires": { "deep-equal": "^1.0.1", "eth-block-tracker": "^1.0.7", - "ethjs": "^0.2.7", - "ethjs-contract": "^0.1.9", - "ethjs-query": "^0.2.6", + "ethjs": "^0.3.6", + "ethjs-contract": "^0.2.1", + "ethjs-query": "^0.3.7", "human-standard-token-abi": "^1.0.2" }, "dependencies": { "babelify": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", "requires": { "babel-core": "^6.0.14", @@ -10408,81 +10413,58 @@ "ethjs-util": "^0.1.3", "pify": "^2.3.0", "tape": "^4.6.3" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } } }, "ethjs": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/ethjs/-/ethjs-0.2.9.tgz", - "integrity": "sha1-yagNR7ydVg9Z53gEnSIlXlgfMSs=", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/ethjs/-/ethjs-0.3.9.tgz", + "integrity": "sha512-gOQzA3tDUjoLpNONSOALJ/rUFtHi5tXl2mholHasF1cvXhoddqi06yU4OJFJu9AGd6n9v9ywzHlYeIKg1t1hdw==", "requires": { "bn.js": "4.11.6", - "ethjs-abi": "0.2.0", - "ethjs-contract": "0.1.9", - "ethjs-filter": "0.1.5", + "ethjs-abi": "0.2.1", + "ethjs-contract": "0.2.2", + "ethjs-filter": "0.1.8", "ethjs-provider-http": "0.1.6", - "ethjs-query": "0.3.0", + "ethjs-query": "0.3.7", "ethjs-unit": "0.1.6", "ethjs-util": "0.1.3", "js-sha3": "0.5.5", "number-to-bn": "1.7.0" }, "dependencies": { - "ethjs-format": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/ethjs-format/-/ethjs-format-0.2.3.tgz", - "integrity": "sha1-m9hnyu6CstvtmEYAuzAiDPPLWDA=", + "ethjs-contract": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.2.2.tgz", + "integrity": "sha512-xxPqEjsULQ/QNWuvX6Ako0PGs5RxALA8N/H3+boLvnaXDFZVGpD7H63H1gBCRTZyYqCldPpVlVHuw/rD45vazw==", "requires": { - "bn.js": "4.11.6", - "ethjs-schema": "^0.1.6", + "ethjs-abi": "0.2.0", + "ethjs-filter": "0.1.8", "ethjs-util": "0.1.3", - "is-hex-prefixed": "1.0.0", - "number-to-bn": "1.7.0", - "strip-hex-prefix": "1.0.0" + "js-sha3": "0.5.5" + }, + "dependencies": { + "ethjs-abi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.0.tgz", + "integrity": "sha1-0+LCIQEVIPxJm3FoIDbBT8wvWyU=", + "requires": { + "bn.js": "4.11.6", + "js-sha3": "0.5.5", + "number-to-bn": "1.7.0" + } + } } }, "ethjs-query": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.3.0.tgz", - "integrity": "sha1-CAmNYQ+BvV+VSnpXq0mJ9+mBX8Q=", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.3.7.tgz", + "integrity": "sha512-TZnKUwfkWjy0SowFdPLtmsytCorHi0i4vvkQn7Jg8rZt33cRzKhuzOwKr/G3vdigCc+ePXOhUGMcJSAPlOG44A==", "requires": { - "ethjs-format": "0.2.3", - "ethjs-rpc": "0.1.5" + "ethjs-format": "0.2.7", + "ethjs-rpc": "0.2.0", + "promise-to-callback": "^1.0.0" } }, - "ethjs-schema": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/ethjs-schema/-/ethjs-schema-0.1.9.tgz", - "integrity": "sha1-hYwqXacGrgSBK0zosetLSSHjMJI=" - }, - "ethjs-util": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", - "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=", - "requires": { - "is-hex-prefixed": "1.0.0", - "strip-hex-prefix": "1.0.0" - } - } - } - }, - "ethjs-contract": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.1.9.tgz", - "integrity": "sha1-HCdmiWpW1H7B1tZhgpxJzDilUgo=", - "requires": { - "ethjs-abi": "0.2.0", - "ethjs-filter": "0.1.5", - "ethjs-util": "0.1.3", - "js-sha3": "0.5.5" - }, - "dependencies": { "ethjs-util": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", @@ -10494,43 +10476,39 @@ } } }, - "ethjs-format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ethjs-format/-/ethjs-format-0.2.2.tgz", - "integrity": "sha1-1zs6YFwuElcHn3B3/VRI6ZjOD80=", + "ethjs-abi": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.1.tgz", + "integrity": "sha1-4KepOn6BFjqUR3utVu3lJKtt5TM=", "requires": { "bn.js": "4.11.6", - "ethjs-schema": "0.1.5", - "ethjs-util": "0.1.3", - "is-hex-prefixed": "1.0.0", - "number-to-bn": "1.7.0", - "strip-hex-prefix": "1.0.0" - }, - "dependencies": { - "ethjs-util": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", - "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=", - "requires": { - "is-hex-prefixed": "1.0.0", - "strip-hex-prefix": "1.0.0" - } - } + "js-sha3": "0.5.5", + "number-to-bn": "1.7.0" } }, + "ethjs-filter": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/ethjs-filter/-/ethjs-filter-0.1.8.tgz", + "integrity": "sha512-qTDPskDL2UadHwjvM8A+WG9HwM4/FoSY3p3rMJORkHltYcAuiQZd2otzOYKcL5w2Q3sbAkW/E3yt/FPFL/AVXA==" + }, "ethjs-query": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.2.9.tgz", - "integrity": "sha1-om5rTzhpnpLzSyGE51x4lDKcQvE=", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.3.8.tgz", + "integrity": "sha512-/J5JydqrOzU8O7VBOwZKUWXxHDGr46VqNjBCJgBVNNda+tv7Xc8Y2uJc6aMHHVbeN3YOQ7YRElgIc0q1CI02lQ==", "requires": { - "ethjs-format": "0.2.2", - "ethjs-rpc": "0.1.5" + "babel-runtime": "^6.26.0", + "ethjs-format": "0.2.7", + "ethjs-rpc": "0.2.0", + "promise-to-callback": "^1.0.0" } }, - "ethjs-schema": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/ethjs-schema/-/ethjs-schema-0.1.5.tgz", - "integrity": "sha1-WXQOOzl3vNu5sRvDBoIB6Kzquw0=" + "ethjs-rpc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ethjs-rpc/-/ethjs-rpc-0.2.0.tgz", + "integrity": "sha512-RINulkNZTKnj4R/cjYYtYMnFFaBcVALzbtEJEONrrka8IeoarNB9Jbzn+2rT00Cv8y/CxAI+GgY1d0/i2iQeOg==", + "requires": { + "promise-to-callback": "^1.0.0" + } }, "human-standard-token-abi": { "version": "1.0.2", @@ -10541,6 +10519,11 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.5.tgz", "integrity": "sha1-uvDA6MVK1ZA0R9+Wreekobynmko=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" } } }, @@ -10561,12 +10544,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -10799,12 +10783,13 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -13752,12 +13737,14 @@ "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "dev": true, "requires": { + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "dev": true, "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -32800,6 +32787,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, "requires": { "is-typedarray": "^1.0.0" } @@ -33825,6 +33813,7 @@ "resolved": "https://registry.npmjs.org/web3/-/web3-0.20.3.tgz", "integrity": "sha1-yqRDc9yIFayHZ73ba6cwc5ZMqos=", "requires": { + "bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", "crypto-js": "^3.1.4", "utf8": "^2.1.1", "xhr2": "*", @@ -33833,7 +33822,7 @@ "dependencies": { "bignumber.js": { "version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", - "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934" + "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git" } } }, @@ -34231,7 +34220,8 @@ "dev": true, "requires": { "underscore": "1.8.3", - "web3-core-helpers": "1.0.0-beta.34" + "web3-core-helpers": "1.0.0-beta.34", + "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2" }, "dependencies": { "underscore": { @@ -34241,8 +34231,9 @@ "dev": true }, "websocket": { - "version": "git://github.com/frozeman/WebSocket-Node.git#7004c39c42ac98875ab61126e5b4a925430f592c", - "from": "git://github.com/frozeman/WebSocket-Node.git#7004c39c42ac98875ab61126e5b4a925430f592c", + "version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", + "from": "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible", + "dev": true, "requires": { "debug": "^2.2.0", "nan": "^2.3.3", @@ -34856,7 +34847,8 @@ "yaeti": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" + "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=", + "dev": true }, "yallist": { "version": "2.1.2", diff --git a/package.json b/package.json index 3a906a271..c994acc2f 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^2.0.2", - "eth-token-tracker": "^1.1.4", + "eth-token-tracker": "^1.1.5", "eth-trezor-keyring": "^0.1.0", "ethereumjs-abi": "^0.6.4", "ethereumjs-tx": "^1.3.0", @@ -261,7 +261,7 @@ "eslint-plugin-json": "^1.2.0", "eslint-plugin-mocha": "^5.0.0", "eslint-plugin-react": "^7.4.0", - "eth-json-rpc-middleware": "^3.1.1", + "eth-json-rpc-middleware": "^3.1.3", "eth-keyring-controller": "^3.3.1", "fetch-mock": "^6.5.2", "file-loader": "^1.1.11", diff --git a/test/e2e/beta/from-import-beta-ui.spec.js b/test/e2e/beta/from-import-beta-ui.spec.js index 32aaa29a6..b782a1c40 100644 --- a/test/e2e/beta/from-import-beta-ui.spec.js +++ b/test/e2e/beta/from-import-beta-ui.spec.js @@ -286,7 +286,7 @@ describe('Using MetaMask with an existing account', function () { await delay(regularDelayMs) const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]')) - const inputAmount = await findElement(driver, By.css('.currency-display__input')) + const inputAmount = await findElement(driver, By.css('.unit-input__input')) await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAmount.sendKeys('1') diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index 8d1ecac0d..f29f242c1 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -383,7 +383,7 @@ describe('MetaMask', function () { await delay(regularDelayMs) const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]')) - const inputAmount = await findElement(driver, By.css('.currency-display__input')) + const inputAmount = await findElement(driver, By.css('.unit-input__input')) await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAmount.sendKeys('1') @@ -662,7 +662,7 @@ describe('MetaMask', function () { }) it('clicks on the Add Token button', async () => { - const addToken = await driver.findElement(By.css('.wallet-view__add-token-button')) + const addToken = await driver.findElement(By.xpath(`//div[contains(text(), 'Add Token')]`)) await addToken.click() await delay(regularDelayMs) }) @@ -702,7 +702,7 @@ describe('MetaMask', function () { await delay(regularDelayMs) const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]')) - const inputAmount = await findElement(driver, By.css('.currency-display__input')) + const inputAmount = await findElement(driver, By.css('.unit-input__input')) await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAmount.sendKeys('50') @@ -834,8 +834,8 @@ describe('MetaMask', function () { await save.click() await driver.wait(until.stalenessOf(gasModal)) - const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth')) - assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006') + const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary')) + assert.equal(await gasFeeInputs[0].getText(), '0.0006') }) it('submits the transaction', async function () { @@ -957,8 +957,8 @@ describe('MetaMask', function () { await save.click() await driver.wait(until.stalenessOf(gasModal)) - const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth')) - assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006') + const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary')) + assert.equal(await gasFeeInputs[0].getText(), '0.0006') }) it('submits the transaction', async function () { @@ -1002,7 +1002,7 @@ describe('MetaMask', function () { describe('Add existing token using search', () => { it('clicks on the Add Token button', async () => { - const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`)) + const addToken = await findElement(driver, By.xpath(`//div[contains(text(), 'Add Token')]`)) await addToken.click() await delay(regularDelayMs) }) diff --git a/test/integration/lib/add-token.js b/test/integration/lib/add-token.js deleted file mode 100644 index bb9d0d10f..000000000 --- a/test/integration/lib/add-token.js +++ /dev/null @@ -1,140 +0,0 @@ -const reactTriggerChange = require('react-trigger-change') -const { - timeout, - queryAsync, - findAsync, -} = require('../../lib/util') - -QUnit.module('Add token flow') - -QUnit.test('successful add token flow', (assert) => { - const done = assert.async() - runAddTokenFlowTest(assert) - .then(done) - .catch(err => { - assert.notOk(err, `Error was thrown: ${err.stack}`) - done() - }) -}) - -async function runAddTokenFlowTest (assert, done) { - const selectState = await queryAsync($, 'select') - selectState.val('add token') - reactTriggerChange(selectState[0]) - - // Used to set values on TextField input component - const nativeInputValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, 'value' - ).set - - // Check that no tokens have been added - assert.ok($('.token-list-item').length === 0, 'no tokens added') - - // Go to Add Token screen - let addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button') - assert.ok(addTokenButton[0], 'add token button present') - addTokenButton[0].click() - - // Verify Add Token screen - let addTokenWrapper = await queryAsync($, '.page-container') - assert.ok(addTokenWrapper[0], 'add token wrapper renders') - - let addTokenTitle = await queryAsync($, '.page-container__title') - assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct') - - // Cancel Add Token - const cancelAddTokenButton = await queryAsync($, 'button.btn-default.btn--large.page-container__footer-button') - assert.ok(cancelAddTokenButton[0], 'cancel add token button present') - cancelAddTokenButton.click() - - assert.ok($('.wallet-view')[0], 'cancelled and returned to account detail wallet view') - - // Return to Add Token Screen - addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button') - assert.ok(addTokenButton[0], 'add token button present') - addTokenButton[0].click() - - // Verify Add Token Screen - addTokenWrapper = await queryAsync($, '.page-container') - addTokenTitle = await queryAsync($, '.page-container__title') - assert.ok(addTokenWrapper[0], 'add token wrapper renders') - assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct') - - // Search for token - const searchInput = (await findAsync(addTokenWrapper, '#search-tokens'))[0] - searchInput.focus() - await timeout(1000) - nativeInputValueSetter.call(searchInput, 'a') - searchInput.dispatchEvent(new Event('input', { bubbles: true})) - - // Click token to add - const tokenWrapper = await queryAsync($, 'div.token-list__token') - assert.ok(tokenWrapper[0], 'token found') - const tokenImageProp = tokenWrapper.find('.token-list__token-icon').css('background-image') - const tokenImageUrl = tokenImageProp.slice(5, -2) - tokenWrapper[0].click() - - // Click Next button - const nextButton = await queryAsync($, 'button.btn-primary.btn--large') - assert.equal(nextButton[0].textContent, 'Next', 'next button rendered') - nextButton[0].click() - - // Confirm Add token - const confirmAddToken = await queryAsync($, '.confirm-add-token') - assert.ok(confirmAddToken[0], 'confirm add token rendered') - assert.ok($('button.btn-primary.btn--large')[0], 'confirm add token button found') - $('button.btn-primary.btn--large')[0].click() - - // Verify added token image - let heroBalance = await queryAsync($, '.transaction-view-balance__balance-container') - assert.ok(heroBalance, 'rendered hero balance') - assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added') - - // Return to Add Token Screen - addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button') - assert.ok(addTokenButton[0], 'add token button present') - addTokenButton[0].click() - - addTokenWrapper = await queryAsync($, '.page-container') - const addTokenTabs = await queryAsync($, '.page-container__tab') - assert.equal(addTokenTabs.length, 2, 'expected number of tabs') - assert.equal(addTokenTabs[1].textContent, 'Custom Token', 'Custom Token tab present') - assert.ok(addTokenTabs[1], 'add custom token tab present') - addTokenTabs[1].click() - await timeout(1000) - - // Input token contract address - const customInput = (await findAsync(addTokenWrapper, '#custom-address'))[0] - customInput.focus() - await timeout(1000) - nativeInputValueSetter.call(customInput, '0x177af043D3A1Aed7cc5f2397C70248Fc6cDC056c') - customInput.dispatchEvent(new Event('input', { bubbles: true})) - - - // Click Next button - // nextButton = await queryAsync($, 'button.btn-primary--lg') - // assert.equal(nextButton[0].textContent, 'Next', 'next button rendered') - // nextButton[0].click() - - // // Verify symbol length error since contract address won't return symbol - const errorMessage = await queryAsync($, '#custom-symbol-helper-text') - assert.ok(errorMessage[0], 'error rendered') - - $('button.btn-default.btn--large')[0].click() - - // await timeout(100000) - - // Confirm Add token - // assert.equal( - // $('.page-container__subtitle')[0].textContent, - // 'Would you like to add these tokens?', - // 'confirm add token rendered' - // ) - // assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found') - // $('button.btn-primary--lg')[0].click() - - // Verify added token image - heroBalance = await queryAsync($, '.transaction-view-balance__balance-container') - assert.ok(heroBalance, 'rendered hero balance') - assert.ok(heroBalance.find('.identicon')[0], 'token added') -} diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index ac1cc2e14..e13016e68 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -40,7 +40,7 @@ async function customizeGas (assert, price, limit, ethFee, usdFee) { const sendGasField = await queryAsync($, '.send-v2__gas-fee-display') assert.equal( - (await findAsync(sendGasField, '.currency-display__input-wrapper > input')).val(), + (await findAsync(sendGasField, '.currency-display-component'))[0].textContent, ethFee, 'send gas field should show customized gas total' ) @@ -97,9 +97,9 @@ async function runSendFlowTest (assert, done) { assert.equal(sendToAccountAddress, '0x2f8D4a878cFA04A6E60D46362f5644DeAb66572D', 'send to dropdown selects the correct address') const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)') - sendAmountField.find('.currency-display')[0].click() + sendAmountField.find('.unit-input')[0].click() - const sendAmountFieldInput = await findAsync(sendAmountField, '.currency-display__input') + const sendAmountFieldInput = await findAsync(sendAmountField, '.unit-input__input') sendAmountFieldInput.val('5.1') reactTriggerChange(sendAmountField.find('input')[0]) @@ -112,9 +112,9 @@ async function runSendFlowTest (assert, done) { errorMessage = $('.send-v2__error') assert.equal(errorMessage.length, 0, 'send should stop rendering amount error message after amount is corrected') - await customizeGas(assert, 0, 21000, '0', '$0.00 USD') - await customizeGas(assert, 1, 21000, '0.000021', '$0.03 USD') - await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD') + await customizeGas(assert, 0, 21000, '0 ETH', '$0.00 USD') + await customizeGas(assert, 1, 21000, '0.000021 ETH', '$0.03 USD') + await customizeGas(assert, 500, 60000, '0.03 ETH', '$36.03 USD') const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button') assert.equal(sendButton[0].textContent, 'Next', 'next button rendered') @@ -130,11 +130,11 @@ async function runSendFlowTest (assert, done) { const confirmToName = (await queryAsync($, '.sender-to-recipient__name')).last() assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name') - const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat') + const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__secondary') const confirmScreenGas = confirmScreenRowFiats[0] assert.equal(confirmScreenGas.textContent, '$3.60', 'confirm screen should show correct gas') const confirmScreenTotal = confirmScreenRowFiats[1] - assert.equal(confirmScreenTotal.textContent, '$2,405.36', 'confirm screen should show correct total') + assert.equal(confirmScreenTotal.textContent, '$2,405.37', 'confirm screen should show correct total') const confirmScreenBackButton = await queryAsync($, '.confirm-page-container-header__back-button') confirmScreenBackButton[0].click() @@ -150,9 +150,9 @@ async function runSendFlowTest (assert, done) { sendToFieldInputInEdit.val('0xd85a4b6a394794842887b8284293d69163007bbb') const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)') - sendAmountFieldInEdit.find('.currency-display')[0].click() + sendAmountFieldInEdit.find('.unit-input')[0].click() - const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.currency-display__input') + const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.unit-input__input') sendAmountFieldInputInEdit.val('1.0') reactTriggerChange(sendAmountFieldInputInEdit[0]) diff --git a/test/unit/components/balance-component-test.js b/test/unit/components/balance-component-test.js deleted file mode 100644 index aa9763b72..000000000 --- a/test/unit/components/balance-component-test.js +++ /dev/null @@ -1,44 +0,0 @@ -const assert = require('assert') -const h = require('react-hyperscript') -const { createMockStore } = require('redux-test-utils') -const { shallowWithStore } = require('../../lib/render-helpers') -const BalanceComponent = require('../../../ui/app/components/balance-component') -const mockState = { - metamask: { - accounts: { abc: {} }, - network: 1, - selectedAddress: 'abc', - }, -} - -describe('BalanceComponent', function () { - let balanceComponent - let store - let component - beforeEach(function () { - store = createMockStore(mockState) - component = shallowWithStore(h(BalanceComponent), store) - balanceComponent = component.dive() - }) - - it('shows token balance and convert to fiat value based on conversion rate', function () { - const formattedBalance = '1.23 ETH' - - const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false) - const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 2) - - assert.equal('1.23 ETH', tokenBalance) - assert.equal(2.46, fiatDisplayNumber) - }) - - it('shows only the token balance when conversion rate is not available', function () { - const formattedBalance = '1.23 ETH' - - const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false) - const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 0) - - assert.equal('1.23 ETH', tokenBalance) - assert.equal('N/A', fiatDisplayNumber) - }) - -}) diff --git a/ui/app/actions.js b/ui/app/actions.js index eea581d33..f8a375e2f 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -305,6 +305,12 @@ var actions = { updateFeatureFlags, UPDATE_FEATURE_FLAGS: 'UPDATE_FEATURE_FLAGS', + // Preferences + setPreference, + updatePreferences, + UPDATE_PREFERENCES: 'UPDATE_PREFERENCES', + setUseETHAsPrimaryCurrencyPreference, + setMouseUserState, SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE', @@ -2298,6 +2304,36 @@ function updateFeatureFlags (updatedFeatureFlags) { } } +function setPreference (preference, value) { + return dispatch => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.setPreference(preference, value, (err, updatedPreferences) => { + dispatch(actions.hideLoadingIndication()) + + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.updatePreferences(updatedPreferences)) + resolve(updatedPreferences) + }) + }) + } +} + +function updatePreferences (value) { + return { + type: actions.UPDATE_PREFERENCES, + value, + } +} + +function setUseETHAsPrimaryCurrencyPreference (value) { + return setPreference('useETHAsPrimaryCurrency', value) +} + function setNetworkNonce (networkNonce) { return { type: actions.SET_NETWORK_NONCE, diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index bcada41e3..c9c5b60e1 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -8,11 +8,11 @@ const h = require('react-hyperscript') const actions = require('../../actions') const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') const Identicon = require('../identicon') -const { formatBalance } = require('../../util') const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') const { getEnvironmentType } = require('../../../../app/scripts/lib/util') const Tooltip = require('../tooltip') - +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' +import { PRIMARY } from '../../constants/common' const { SETTINGS_ROUTE, @@ -163,7 +163,6 @@ AccountMenu.prototype.renderAccounts = function () { const isSelected = identity.address === selectedAddress const balanceValue = accounts[address] ? accounts[address].balance : '' - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() const keyring = keyrings.find((kr) => { @@ -189,7 +188,11 @@ AccountMenu.prototype.renderAccounts = function () { h('div.account-menu__account-info', [ h('div.account-menu__name', identity.name || ''), - h('div.account-menu__balance', formattedBalance), + h(UserPreferencedCurrencyDisplay, { + className: 'account-menu__balance', + value: balanceValue, + type: PRIMARY, + }), ]), this.renderKeyringType(keyring), diff --git a/ui/app/components/add-token-button/add-token-button.component.js b/ui/app/components/add-token-button/add-token-button.component.js new file mode 100644 index 000000000..10887aed8 --- /dev/null +++ b/ui/app/components/add-token-button/add-token-button.component.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types' +import React, {PureComponent} from 'react' + +export default class AddTokenButton extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + } + + static defaultProps = { + onClick: () => {}, + } + + static propTypes = { + onClick: PropTypes.func, + } + + render () { + const { t } = this.context + const { onClick } = this.props + + return ( + <div className="add-token-button"> + <h1 className="add-token-button__help-header">{t('missingYourTokens')}</h1> + <p className="add-token-button__help-desc">{t('clickToAdd', [t('addToken')])}</p> + <div + className="add-token-button__button" + onClick={onClick} + > + {t('addToken')} + </div> + </div> + ) + } +} diff --git a/ui/app/components/add-token-button/index.js b/ui/app/components/add-token-button/index.js new file mode 100644 index 000000000..15c4fe6ca --- /dev/null +++ b/ui/app/components/add-token-button/index.js @@ -0,0 +1 @@ +export { default } from './add-token-button.component' diff --git a/ui/app/components/add-token-button/index.scss b/ui/app/components/add-token-button/index.scss new file mode 100644 index 000000000..39f404716 --- /dev/null +++ b/ui/app/components/add-token-button/index.scss @@ -0,0 +1,26 @@ +.add-token-button { + display: flex; + flex-direction: column; + color: lighten($scorpion, 25%); + width: 185px; + margin: 36px auto; + text-align: center; + + &__help-header { + font-weight: bold; + font-size: 1rem; + } + + &__help-desc { + font-size: 0.75rem; + margin-top: 1rem; + } + + &__button { + font-size: 0.75rem; + margin: 1rem; + text-transform: uppercase; + color: $curious-blue; + cursor: pointer; + } +} diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index d63d78c9f..e1fcf08e0 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -4,10 +4,11 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const TokenBalance = require('./token-balance') const Identicon = require('./identicon') -import CurrencyDisplay from './currency-display' +import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../constants/common' const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors') -const { formatBalance, generateBalanceObject } = require('../util') +const { formatBalance } = require('../util') module.exports = connect(mapStateToProps)(BalanceComponent) @@ -65,7 +66,7 @@ BalanceComponent.prototype.renderTokenBalance = function () { BalanceComponent.prototype.renderBalance = function () { const props = this.props - const { shorten, account } = props + const { account } = props const balanceValue = account && account.balance const needsParse = 'needsParse' in props ? props.needsParse : true const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...' @@ -80,25 +81,20 @@ BalanceComponent.prototype.renderBalance = function () { } return h('div.flex-column.balance-display', {}, [ - h('div.token-amount', { - style: {}, - }, this.getTokenBalance(formattedBalance, shorten)), + h('div.token-amount', {}, h(UserPreferencedCurrencyDisplay, { + value: balanceValue, + type: PRIMARY, + ethNumberOfDecimals: 3, + })), - showFiat && h(CurrencyDisplay, { + showFiat && h(UserPreferencedCurrencyDisplay, { value: balanceValue, + type: SECONDARY, + ethNumberOfDecimals: 3, }), ]) } -BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) { - const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3) - - const balanceValue = shorten ? balanceObj.shortBalance : balanceObj.balance - const label = balanceObj.label - - return `${balanceValue} ${label}` -} - BalanceComponent.prototype.getFiatDisplayNumber = function (formattedBalance, conversionRate) { if (formattedBalance === 'None') return formattedBalance if (conversionRate === 0) return 'N/A' diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js index f0703dde2..c7262d2a9 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js +++ b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js @@ -1,16 +1,19 @@ import React from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../constants/common' const ConfirmDetailRow = props => { const { label, - fiatText, - ethText, + primaryText, + secondaryText, onHeaderClick, - fiatTextColor, + primaryValueTextColor, headerText, headerTextClassName, + value, } = props return ( @@ -25,28 +28,57 @@ const ConfirmDetailRow = props => { > { headerText } </div> - <div - className="confirm-detail-row__fiat" - style={{ color: fiatTextColor }} - > - { fiatText } - </div> - <div className="confirm-detail-row__eth"> - { ethText } - </div> + { + primaryText + ? ( + <div + className="confirm-detail-row__primary" + style={{ color: primaryValueTextColor }} + > + { primaryText } + </div> + ) : ( + <UserPreferencedCurrencyDisplay + className="confirm-detail-row__primary" + type={PRIMARY} + value={value} + showEthLogo + ethLogoHeight="18" + style={{ color: primaryValueTextColor }} + hideLabel + /> + ) + } + { + secondaryText + ? ( + <div className="confirm-detail-row__secondary"> + { secondaryText } + </div> + ) : ( + <UserPreferencedCurrencyDisplay + className="confirm-detail-row__secondary" + type={SECONDARY} + value={value} + showEthLogo + hideLabel + /> + ) + } </div> </div> ) } ConfirmDetailRow.propTypes = { - label: PropTypes.string, - fiatText: PropTypes.string, - ethText: PropTypes.string, - fiatTextColor: PropTypes.string, - onHeaderClick: PropTypes.func, headerText: PropTypes.string, headerTextClassName: PropTypes.string, + label: PropTypes.string, + onHeaderClick: PropTypes.func, + primaryValueTextColor: PropTypes.string, + primaryText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + secondaryText: PropTypes.string, + value: PropTypes.string, } export default ConfirmDetailRow diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss index dd6f87c17..580a41fde 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss +++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss @@ -18,18 +18,14 @@ min-width: 0; } - &__fiat { + &__primary { font-size: 1.5rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + justify-content: flex-end; } - &__eth { + &__secondary { color: $oslo-gray; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + justify-content: flex-end; } &__header-text { diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js index 6f2489071..c8507985d 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js +++ b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js @@ -12,17 +12,19 @@ describe('Confirm Detail Row Component', function () { let wrapper beforeEach(() => { - wrapper = shallow(<ConfirmDetailRow - errorType={'mockErrorType'} - label={'mockLabel'} - showError={false} - fiatText = {'mockFiatText'} - ethText = {'mockEthText'} - fiatTextColor= {'mockColor'} - onHeaderClick= {propsMethodSpies.onHeaderClick} - headerText = {'mockHeaderText'} - headerTextClassName = {'mockHeaderClass'} - />) + wrapper = shallow( + <ConfirmDetailRow + errorType={'mockErrorType'} + label={'mockLabel'} + showError={false} + primaryText = {'mockFiatText'} + secondaryText = {'mockEthText'} + primaryValueTextColor= {'mockColor'} + onHeaderClick= {propsMethodSpies.onHeaderClick} + headerText = {'mockHeaderText'} + headerTextClassName = {'mockHeaderClass'} + /> + ) }) describe('render', () => { @@ -38,16 +40,16 @@ describe('Confirm Detail Row Component', function () { assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__header-text').childAt(0).text(), 'mockHeaderText') }) - it('should render the fiatText as a child of the confirm-detail-row__fiat', () => { - assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__fiat').childAt(0).text(), 'mockFiatText') + it('should render the primaryText as a child of the confirm-detail-row__primary', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__primary').childAt(0).text(), 'mockFiatText') }) - it('should render the ethText as a child of the confirm-detail-row__eth', () => { - assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__eth').childAt(0).text(), 'mockEthText') + it('should render the ethText as a child of the confirm-detail-row__secondary', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__secondary').childAt(0).text(), 'mockEthText') }) - it('should set the fiatTextColor on confirm-detail-row__fiat', () => { - assert.equal(wrapper.find('.confirm-detail-row__fiat').props().style.color, 'mockColor') + it('should set the fiatTextColor on confirm-detail-row__primary', () => { + assert.equal(wrapper.find('.confirm-detail-row__primary').props().style.color, 'mockColor') }) it('should assure the confirm-detail-row__header-text classname is correct', () => { @@ -58,7 +60,5 @@ describe('Confirm Detail Row Component', function () { wrapper.find('.confirm-detail-row__header-text').props().onClick() assert.equal(assert.equal(propsMethodSpies.onHeaderClick.callCount, 1)) }) - - }) }) diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index 74e95ece6..1dca81560 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -17,9 +17,10 @@ export default class ConfirmPageContainerContent extends Component { nonce: PropTypes.string, assetImage: PropTypes.string, subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + subtitleComponent: PropTypes.node, summaryComponent: PropTypes.node, title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - titleComponent: PropTypes.func, + titleComponent: PropTypes.node, warning: PropTypes.string, } @@ -54,7 +55,9 @@ export default class ConfirmPageContainerContent extends Component { errorKey, errorMessage, title, + titleComponent, subtitle, + subtitleComponent, hideSubtitle, identiconAddress, nonce, @@ -80,7 +83,9 @@ export default class ConfirmPageContainerContent extends Component { })} action={action} title={title} + titleComponent={titleComponent} subtitle={subtitle} + subtitleComponent={subtitleComponent} hideSubtitle={hideSubtitle} identiconAddress={identiconAddress} nonce={nonce} diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index 38b158fd3..89ceb015f 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -4,7 +4,18 @@ import classnames from 'classnames' import Identicon from '../../../identicon' const ConfirmPageContainerSummary = props => { - const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce, assetImage } = props + const { + action, + title, + titleComponent, + subtitle, + subtitleComponent, + hideSubtitle, + className, + identiconAddress, + nonce, + assetImage, + } = props return ( <div className={classnames('confirm-page-container-summary', className)}> @@ -32,12 +43,12 @@ const ConfirmPageContainerSummary = props => { ) } <div className="confirm-page-container-summary__title-text"> - { title } + { titleComponent || title } </div> </div> { hideSubtitle || <div className="confirm-page-container-summary__subtitle"> - { subtitle } + { subtitleComponent || subtitle } </div> } </div> @@ -47,7 +58,9 @@ const ConfirmPageContainerSummary = props => { ConfirmPageContainerSummary.propTypes = { action: PropTypes.string, title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + titleComponent: PropTypes.node, subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + subtitleComponent: PropTypes.node, hideSubtitle: PropTypes.bool, className: PropTypes.string, identiconAddress: PropTypes.string, diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/confirm-page-container/confirm-page-container.component.js index 36d5a1f58..8b2e47cbb 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/confirm-page-container/confirm-page-container.component.js @@ -16,8 +16,9 @@ export default class ConfirmPageContainer extends Component { onEdit: PropTypes.func, showEdit: PropTypes.bool, subtitle: PropTypes.string, + subtitleComponent: PropTypes.node, title: PropTypes.string, - titleComponent: PropTypes.func, + titleComponent: PropTypes.node, // Sender to Recipient fromAddress: PropTypes.string, fromName: PropTypes.string, @@ -65,6 +66,7 @@ export default class ConfirmPageContainer extends Component { title, titleComponent, subtitle, + subtitleComponent, hideSubtitle, summaryComponent, detailsComponent, @@ -101,6 +103,7 @@ export default class ConfirmPageContainer extends Component { title={title} titleComponent={titleComponent} subtitle={subtitle} + subtitleComponent={subtitleComponent} hideSubtitle={hideSubtitle} summaryComponent={summaryComponent} detailsComponent={detailsComponent} diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/currency-display/currency-display.component.js index e4eb58a2a..5f5717be3 100644 --- a/ui/app/components/currency-display/currency-display.component.js +++ b/ui/app/components/currency-display/currency-display.component.js @@ -1,5 +1,6 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' +import classnames from 'classnames' import { ETH, GWEI } from '../../constants/common' export default class CurrencyDisplay extends PureComponent { @@ -7,6 +8,8 @@ export default class CurrencyDisplay extends PureComponent { className: PropTypes.string, displayValue: PropTypes.string, prefix: PropTypes.string, + prefixComponent: PropTypes.node, + style: PropTypes.object, // Used in container currency: PropTypes.oneOf([ETH]), denomination: PropTypes.oneOf([GWEI]), @@ -16,15 +19,17 @@ export default class CurrencyDisplay extends PureComponent { } render () { - const { className, displayValue, prefix } = this.props + const { className, displayValue, prefix, prefixComponent, style } = this.props const text = `${prefix || ''}${displayValue}` return ( <div - className={className} + className={classnames('currency-display-component', className)} + style={style} title={text} > - { text } + { prefixComponent} + <span className="currency-display-component__text">{ text }</span> </div> ) } diff --git a/ui/app/components/currency-display/currency-display.container.js b/ui/app/components/currency-display/currency-display.container.js index 6644a1099..b387229b5 100644 --- a/ui/app/components/currency-display/currency-display.container.js +++ b/ui/app/components/currency-display/currency-display.container.js @@ -2,10 +2,26 @@ import { connect } from 'react-redux' import CurrencyDisplay from './currency-display.component' import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util' -const mapStateToProps = (state, ownProps) => { - const { value, numberOfDecimals = 2, currency, denomination, hideLabel } = ownProps +const mapStateToProps = state => { const { metamask: { currentCurrency, conversionRate } } = state + return { + currentCurrency, + conversionRate, + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { currentCurrency, conversionRate, ...restStateProps } = stateProps + const { + value, + numberOfDecimals = 2, + currency, + denomination, + hideLabel, + ...restOwnProps + } = ownProps + const toCurrency = currency || currentCurrency const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination, @@ -14,8 +30,11 @@ const mapStateToProps = (state, ownProps) => { const displayValue = hideLabel ? formattedValue : `${formattedValue} ${toCurrency.toUpperCase()}` return { + ...restStateProps, + ...dispatchProps, + ...restOwnProps, displayValue, } } -export default connect(mapStateToProps)(CurrencyDisplay) +export default connect(mapStateToProps, null, mergeProps)(CurrencyDisplay) diff --git a/ui/app/components/currency-display/index.scss b/ui/app/components/currency-display/index.scss new file mode 100644 index 000000000..8c0196102 --- /dev/null +++ b/ui/app/components/currency-display/index.scss @@ -0,0 +1,10 @@ +.currency-display-component { + display: flex; + align-items: center; + + &__text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/ui/app/components/currency-display/tests/currency-display.container.test.js b/ui/app/components/currency-display/tests/currency-display.container.test.js index 5265bbb04..b9f98c543 100644 --- a/ui/app/components/currency-display/tests/currency-display.container.test.js +++ b/ui/app/components/currency-display/tests/currency-display.container.test.js @@ -1,12 +1,13 @@ import assert from 'assert' import proxyquire from 'proxyquire' -let mapStateToProps +let mapStateToProps, mergeProps proxyquire('../currency-display.container.js', { 'react-redux': { - connect: ms => { + connect: (ms, md, mp) => { mapStateToProps = ms + mergeProps = mp return () => ({}) }, }, @@ -22,6 +23,20 @@ describe('CurrencyDisplay container', () => { }, } + assert.deepEqual(mapStateToProps(mockState), { + conversionRate: 280.45, + currentCurrency: 'usd', + }) + }) + }) + + describe('mergeProps()', () => { + it('should return the correct props', () => { + const mockStateProps = { + conversionRate: 280.45, + currentCurrency: 'usd', + } + const tests = [ { props: { @@ -98,7 +113,7 @@ describe('CurrencyDisplay container', () => { ] tests.forEach(({ props, result }) => { - assert.deepEqual(mapStateToProps(mockState, props), result) + assert.deepEqual(mergeProps(mockStateProps, {}, { ...props }), result) }) }) }) diff --git a/ui/app/components/currency-input/currency-input.component.js b/ui/app/components/currency-input/currency-input.component.js new file mode 100644 index 000000000..54cd0e1ac --- /dev/null +++ b/ui/app/components/currency-input/currency-input.component.js @@ -0,0 +1,120 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import UnitInput from '../unit-input' +import CurrencyDisplay from '../currency-display' +import { getValueFromWeiHex, getWeiHexFromDecimalValue } from '../../helpers/conversions.util' +import { ETH } from '../../constants/common' + +/** + * Component that allows user to enter currency values as a number, and props receive a converted + * hex value in WEI. props.value, used as a default or forced value, should be a hex value, which + * gets converted into a decimal value depending on the currency (ETH or Fiat). + */ +export default class CurrencyInput extends PureComponent { + static propTypes = { + conversionRate: PropTypes.number, + currentCurrency: PropTypes.string, + onChange: PropTypes.func, + onBlur: PropTypes.func, + suffix: PropTypes.string, + useFiat: PropTypes.bool, + value: PropTypes.string, + } + + constructor (props) { + super(props) + + const { value: hexValue } = props + const decimalValue = hexValue ? this.getDecimalValue(props) : 0 + + this.state = { + decimalValue, + hexValue, + } + } + + componentDidUpdate (prevProps) { + const { value: prevPropsHexValue } = prevProps + const { value: propsHexValue } = this.props + const { hexValue: stateHexValue } = this.state + + if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) { + const decimalValue = this.getDecimalValue(this.props) + this.setState({ hexValue: propsHexValue, decimalValue }) + } + } + + getDecimalValue (props) { + const { value: hexValue, useFiat, currentCurrency, conversionRate } = props + const decimalValueString = useFiat + ? getValueFromWeiHex({ + value: hexValue, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2, + }) + : getValueFromWeiHex({ + value: hexValue, toCurrency: ETH, numberOfDecimals: 6, + }) + + return Number(decimalValueString) || 0 + } + + handleChange = decimalValue => { + const { useFiat, currentCurrency: fromCurrency, conversionRate, onChange } = this.props + + const hexValue = useFiat + ? getWeiHexFromDecimalValue({ + value: decimalValue, fromCurrency, conversionRate, invertConversionRate: true, + }) + : getWeiHexFromDecimalValue({ + value: decimalValue, fromCurrency: ETH, fromDenomination: ETH, conversionRate, + }) + + this.setState({ hexValue, decimalValue }) + onChange(hexValue) + } + + handleBlur = () => { + this.props.onBlur(this.state.hexValue) + } + + renderConversionComponent () { + const { useFiat, currentCurrency } = this.props + const { hexValue } = this.state + let currency, numberOfDecimals + + if (useFiat) { + // Display ETH + currency = ETH + numberOfDecimals = 6 + } else { + // Display Fiat + currency = currentCurrency + numberOfDecimals = 2 + } + + return ( + <CurrencyDisplay + className="currency-input__conversion-component" + currency={currency} + value={hexValue} + numberOfDecimals={numberOfDecimals} + /> + ) + } + + render () { + const { suffix, ...restProps } = this.props + const { decimalValue } = this.state + + return ( + <UnitInput + {...restProps} + suffix={suffix} + onChange={this.handleChange} + onBlur={this.handleBlur} + value={decimalValue} + > + { this.renderConversionComponent() } + </UnitInput> + ) + } +} diff --git a/ui/app/components/currency-input/currency-input.container.js b/ui/app/components/currency-input/currency-input.container.js new file mode 100644 index 000000000..18e5533de --- /dev/null +++ b/ui/app/components/currency-input/currency-input.container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux' +import CurrencyInput from './currency-input.component' +import { ETH } from '../../constants/common' + +const mapStateToProps = state => { + const { metamask: { currentCurrency, conversionRate } } = state + + return { + currentCurrency, + conversionRate, + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { currentCurrency } = stateProps + const { useFiat } = ownProps + const suffix = useFiat ? currentCurrency.toUpperCase() : ETH + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + suffix, + } +} + +export default connect(mapStateToProps, null, mergeProps)(CurrencyInput) diff --git a/ui/app/components/currency-input/index.js b/ui/app/components/currency-input/index.js new file mode 100644 index 000000000..d8069fb67 --- /dev/null +++ b/ui/app/components/currency-input/index.js @@ -0,0 +1 @@ +export { default } from './currency-input.container' diff --git a/ui/app/components/currency-input/index.scss b/ui/app/components/currency-input/index.scss new file mode 100644 index 000000000..fcb2db461 --- /dev/null +++ b/ui/app/components/currency-input/index.scss @@ -0,0 +1,7 @@ +.currency-input { + &__conversion-component { + font-size: 12px; + line-height: 12px; + padding-left: 1px; + } +} diff --git a/ui/app/components/currency-input/tests/currency-input.component.test.js b/ui/app/components/currency-input/tests/currency-input.component.test.js new file mode 100644 index 000000000..8de0ef863 --- /dev/null +++ b/ui/app/components/currency-input/tests/currency-input.component.test.js @@ -0,0 +1,239 @@ +import React from 'react' +import assert from 'assert' +import { shallow, mount } from 'enzyme' +import sinon from 'sinon' +import { Provider } from 'react-redux' +import configureMockStore from 'redux-mock-store' +import CurrencyInput from '../currency-input.component' +import UnitInput from '../../unit-input' +import CurrencyDisplay from '../../currency-display' + +describe('CurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly without a suffix', () => { + const wrapper = shallow( + <CurrencyInput /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(UnitInput).length, 1) + }) + + it('should render properly with a suffix', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <CurrencyInput + suffix="ETH" + /> + </Provider> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should render properly with an ETH value', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <CurrencyInput + value="de0b6b3a7640000" + suffix="ETH" + currentCurrency="usd" + conversionRate={231.06} + /> + </Provider> + ) + + assert.ok(wrapper) + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') + assert.equal(wrapper.find('.unit-input__input').props().value, '1') + assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD') + }) + + it('should render properly with a fiat value', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <CurrencyInput + value="f602f2234d0ea" + suffix="USD" + useFiat + currentCurrency="usd" + conversionRate={231.06} + /> + </Provider> + ) + + assert.ok(wrapper) + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'USD') + assert.equal(wrapper.find('.unit-input__input').props().value, '1') + assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH') + }) + }) + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy() + const handleBlurSpy = sinon.spy() + + afterEach(() => { + handleChangeSpy.resetHistory() + handleBlurSpy.resetHistory() + }) + + it('should call onChange and onBlur on input changes with the hex value for ETH', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + <Provider store={store}> + <CurrencyInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + suffix="ETH" + currentCurrency="usd" + conversionRate={231.06} + /> + </Provider> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 0) + assert.equal(currencyInputInstance.state.hexValue, undefined) + assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD') + const input = wrapper.find('input') + assert.equal(input.props().value, 0) + + input.simulate('change', { target: { value: 1 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000')) + assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD') + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('de0b6b3a7640000')) + }) + + it('should call onChange and onBlur on input changes with the hex value for fiat', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + <Provider store={store}> + <CurrencyInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + suffix="USD" + currentCurrency="usd" + conversionRate={231.06} + useFiat + /> + </Provider> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 0) + assert.equal(currencyInputInstance.state.hexValue, undefined) + assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH') + const input = wrapper.find('input') + assert.equal(input.props().value, 0) + + input.simulate('change', { target: { value: 1 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith('f602f2234d0ea')) + assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH') + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('f602f2234d0ea')) + }) + + it('should change the state and pass in a new decimalValue when props.value changes', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = shallow( + <Provider store={store}> + <CurrencyInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + suffix="USD" + currentCurrency="usd" + conversionRate={231.06} + useFiat + /> + </Provider> + ) + + assert.ok(wrapper) + const currencyInputInstance = wrapper.find(CurrencyInput).dive() + assert.equal(currencyInputInstance.state('decimalValue'), 0) + assert.equal(currencyInputInstance.state('hexValue'), undefined) + assert.equal(currencyInputInstance.find(UnitInput).props().value, 0) + + currencyInputInstance.setProps({ value: '1ec05e43e72400' }) + currencyInputInstance.update() + assert.equal(currencyInputInstance.state('decimalValue'), 2) + assert.equal(currencyInputInstance.state('hexValue'), '1ec05e43e72400') + assert.equal(currencyInputInstance.find(UnitInput).props().value, 2) + }) + }) +}) diff --git a/ui/app/components/currency-input/tests/currency-input.container.test.js b/ui/app/components/currency-input/tests/currency-input.container.test.js new file mode 100644 index 000000000..e77945e4d --- /dev/null +++ b/ui/app/components/currency-input/tests/currency-input.container.test.js @@ -0,0 +1,55 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps, mergeProps + +proxyquire('../currency-input.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mergeProps = mp + return () => ({}) + }, + }, +}) + +describe('CurrencyInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + conversionRate: 280.45, + currentCurrency: 'usd', + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + conversionRate: 280.45, + currentCurrency: 'usd', + }) + }) + }) + + describe('mergeProps()', () => { + it('should return the correct props', () => { + const mockStateProps = { + conversionRate: 280.45, + currentCurrency: 'usd', + } + const mockDispatchProps = {} + + assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, { useFiat: true }), { + conversionRate: 280.45, + currentCurrency: 'usd', + useFiat: true, + suffix: 'USD', + }) + + assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, {}), { + conversionRate: 280.45, + currentCurrency: 'usd', + suffix: 'ETH', + }) + }) + }) +}) diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index 21b65bf55..beffdb221 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -1,11 +1,17 @@ @import './app-header/index'; +@import './add-token-button/index'; + @import './button-group/index'; @import './card/index'; @import './confirm-page-container/index'; +@import './currency-input/index'; + +@import './currency-display/index'; + @import './error-message/index'; @import './export-text-container/index'; @@ -49,3 +55,5 @@ @import './app-header/index'; @import './sidebars/index'; + +@import './unit-input/index'; diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js index b082db1d0..b973f221c 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js +++ b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import CurrencyDisplay from '../../../currency-display' -import { ETH } from '../../../../constants/common' +import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../../constants/common' export default class CancelTransaction extends PureComponent { static propTypes = { @@ -13,15 +13,15 @@ export default class CancelTransaction extends PureComponent { return ( <div className="cancel-transaction-gas-fee"> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="cancel-transaction-gas-fee__eth" - currency={ETH} value={value} - numberOfDecimals={6} + type={PRIMARY} /> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="cancel-transaction-gas-fee__fiat" value={value} + type={SECONDARY} /> </div> ) diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js index 994c2a577..014815503 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js +++ b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import CancelTransactionGasFee from '../cancel-transaction-gas-fee.component' -import CurrencyDisplay from '../../../../currency-display' +import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display' describe('CancelTransactionGasFee Component', () => { it('should render', () => { @@ -13,12 +13,11 @@ describe('CancelTransactionGasFee Component', () => { ) assert.ok(wrapper) - assert.equal(wrapper.find(CurrencyDisplay).length, 2) - const ethDisplay = wrapper.find(CurrencyDisplay).at(0) - const fiatDisplay = wrapper.find(CurrencyDisplay).at(1) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) + const ethDisplay = wrapper.find(UserPreferencedCurrencyDisplay).at(0) + const fiatDisplay = wrapper.find(UserPreferencedCurrencyDisplay).at(1) assert.equal(ethDisplay.props().value, '0x3b9aca00') - assert.equal(ethDisplay.props().currency, 'ETH') assert.equal(ethDisplay.props().className, 'cancel-transaction-gas-fee__eth') assert.equal(fiatDisplay.props().value, '0x3b9aca00') diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js index acaed383a..7f1fb4e49 100644 --- a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js +++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js @@ -1,12 +1,15 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import ConfirmTransactionBase from '../confirm-transaction-base' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' import { formatCurrency, convertTokenToFiat, addFiat, roundExponential, } from '../../../helpers/confirm-transaction/util' +import { getWeiHexFromDecimalValue } from '../../../helpers/conversions.util' +import { ETH, PRIMARY } from '../../../constants/common' export default class ConfirmTokenTransactionBase extends Component { static contextTypes = { @@ -36,19 +39,48 @@ export default class ConfirmTokenTransactionBase extends Component { }) } - getSubtitle () { - const { currentCurrency, contractExchangeRate } = this.props + renderSubtitleComponent () { + const { contractExchangeRate, tokenAmount } = this.props - if (typeof contractExchangeRate === 'undefined') { - return this.context.t('noConversionRateAvailable') - } else { - const fiatTransactionAmount = this.getFiatTransactionAmount() - const roundedFiatTransactionAmount = roundExponential(fiatTransactionAmount) - return formatCurrency(roundedFiatTransactionAmount, currentCurrency) - } + const decimalEthValue = (tokenAmount * contractExchangeRate) || 0 + const hexWeiValue = getWeiHexFromDecimalValue({ + value: decimalEthValue, + fromCurrency: ETH, + fromDenomination: ETH, + }) + + return typeof contractExchangeRate === 'undefined' + ? ( + <span> + { this.context.t('noConversionRateAvailable') } + </span> + ) : ( + <UserPreferencedCurrencyDisplay + value={hexWeiValue} + type={PRIMARY} + showEthLogo + hideLabel + /> + ) + } + + renderPrimaryTotalTextOverride () { + const { tokenAmount, tokenSymbol, ethTransactionTotal } = this.props + const tokensText = `${tokenAmount} ${tokenSymbol}` + + return ( + <div> + <span>{ `${tokensText} + ` }</span> + <img + src="/images/eth.svg" + height="18" + /> + <span>{ ethTransactionTotal }</span> + </div> + ) } - getFiatTotalTextOverride () { + getSecondaryTotalTextOverride () { const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props if (typeof contractExchangeRate === 'undefined') { @@ -67,7 +99,6 @@ export default class ConfirmTokenTransactionBase extends Component { tokenAddress, tokenSymbol, tokenAmount, - ethTransactionTotal, ...restProps } = this.props @@ -78,9 +109,9 @@ export default class ConfirmTokenTransactionBase extends Component { toAddress={toAddress} identiconAddress={tokenAddress} title={tokensText} - subtitle={this.getSubtitle()} - ethTotalTextOverride={`${tokensText} + \u2666 ${ethTransactionTotal}`} - fiatTotalTextOverride={this.getFiatTotalTextOverride()} + subtitleComponent={this.renderSubtitleComponent()} + primaryTotalTextOverride={this.renderPrimaryTotalTextOverride()} + secondaryTotalTextOverride={this.getSecondaryTotalTextOverride()} {...restProps} /> ) diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js index 707dad62d..c92867afe 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -1,7 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container' -import { formatCurrency } from '../../../helpers/confirm-transaction/util' import { isBalanceSufficient } from '../../send/send.utils' import { DEFAULT_ROUTE } from '../../../routes' import { @@ -9,6 +8,8 @@ import { TRANSACTION_ERROR_KEY, } from '../../../constants/error-keys' import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../../constants/transactions' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../constants/common' export default class ConfirmTransactionBase extends Component { static contextTypes = { @@ -36,7 +37,9 @@ export default class ConfirmTransactionBase extends Component { fiatTransactionTotal: PropTypes.string, fromAddress: PropTypes.string, fromName: PropTypes.string, - hexGasTotal: PropTypes.string, + hexTransactionAmount: PropTypes.string, + hexTransactionFee: PropTypes.string, + hexTransactionTotal: PropTypes.string, isTxReprice: PropTypes.bool, methodData: PropTypes.object, nonce: PropTypes.string, @@ -59,8 +62,8 @@ export default class ConfirmTransactionBase extends Component { detailsComponent: PropTypes.node, errorKey: PropTypes.string, errorMessage: PropTypes.string, - ethTotalTextOverride: PropTypes.string, - fiatTotalTextOverride: PropTypes.string, + primaryTotalTextOverride: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + secondaryTotalTextOverride: PropTypes.string, hideData: PropTypes.bool, hideDetails: PropTypes.bool, hideSubtitle: PropTypes.bool, @@ -70,8 +73,10 @@ export default class ConfirmTransactionBase extends Component { onEditGas: PropTypes.func, onSubmit: PropTypes.func, subtitle: PropTypes.string, + subtitleComponent: PropTypes.node, summaryComponent: PropTypes.node, title: PropTypes.string, + titleComponent: PropTypes.node, valid: PropTypes.bool, warning: PropTypes.string, } @@ -105,7 +110,7 @@ export default class ConfirmTransactionBase extends Component { const { balance, conversionRate, - hexGasTotal, + hexTransactionFee, txData: { simulationFails, txParams: { @@ -116,7 +121,7 @@ export default class ConfirmTransactionBase extends Component { const insufficientBalance = balance && !isBalanceSufficient({ amount, - gasTotal: hexGasTotal || '0x0', + gasTotal: hexTransactionFee || '0x0', balance, conversionRate, }) @@ -153,13 +158,10 @@ export default class ConfirmTransactionBase extends Component { renderDetails () { const { detailsComponent, - fiatTransactionFee, - ethTransactionFee, - currentCurrency, - fiatTransactionTotal, - ethTransactionTotal, - fiatTotalTextOverride, - ethTotalTextOverride, + primaryTotalTextOverride, + secondaryTotalTextOverride, + hexTransactionFee, + hexTransactionTotal, hideDetails, } = this.props @@ -167,16 +169,13 @@ export default class ConfirmTransactionBase extends Component { return null } - const formattedCurrency = formatCurrency(fiatTransactionTotal, currentCurrency) - return ( detailsComponent || ( <div className="confirm-page-container-content__details"> <div className="confirm-page-container-content__gas-fee"> <ConfirmDetailRow label="Gas Fee" - fiatText={formatCurrency(fiatTransactionFee, currentCurrency)} - ethText={`\u2666 ${ethTransactionFee}`} + value={hexTransactionFee} headerText="Edit" headerTextClassName="confirm-detail-row__header-text--edit" onHeaderClick={() => this.handleEditGas()} @@ -185,11 +184,12 @@ export default class ConfirmTransactionBase extends Component { <div> <ConfirmDetailRow label="Total" - fiatText={fiatTotalTextOverride || formattedCurrency} - ethText={ethTotalTextOverride || `\u2666 ${ethTransactionTotal}`} + value={hexTransactionTotal} + primaryText={primaryTotalTextOverride} + secondaryText={secondaryTotalTextOverride} headerText="Amount + Gas Fee" headerTextClassName="confirm-detail-row__header-text--total" - fiatTextColor="#2f9ae0" + primaryValueTextColor="#2f9ae0" /> </div> </div> @@ -311,6 +311,43 @@ export default class ConfirmTransactionBase extends Component { } } + renderTitleComponent () { + const { title, titleComponent, hexTransactionAmount } = this.props + + // Title string passed in by props takes priority + if (title) { + return null + } + + return titleComponent || ( + <UserPreferencedCurrencyDisplay + value={hexTransactionAmount} + type={PRIMARY} + showEthLogo + ethLogoHeight="26" + hideLabel + /> + ) + } + + renderSubtitleComponent () { + const { subtitle, subtitleComponent, hexTransactionAmount } = this.props + + // Subtitle string passed in by props takes priority + if (subtitle) { + return null + } + + return subtitleComponent || ( + <UserPreferencedCurrencyDisplay + value={hexTransactionAmount} + type={SECONDARY} + showEthLogo + hideLabel + /> + ) + } + render () { const { isTxReprice, @@ -319,12 +356,9 @@ export default class ConfirmTransactionBase extends Component { toName, toAddress, methodData, - ethTransactionAmount, - fiatTransactionAmount, valid: propsValid = true, errorMessage, errorKey: propsErrorKey, - currentCurrency, action, title, subtitle, @@ -341,7 +375,6 @@ export default class ConfirmTransactionBase extends Component { const { submitting, submitError } = this.state const { name } = methodData - const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency) const { valid, errorKey } = this.getErrorKey() return ( @@ -352,8 +385,10 @@ export default class ConfirmTransactionBase extends Component { toAddress={toAddress} showEdit={onEdit && !isTxReprice} action={action || name || this.context.t('unknownFunction')} - title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`} - subtitle={subtitle || `\u2666 ${ethTransactionAmount}`} + title={title} + titleComponent={this.renderTitleComponent()} + subtitle={subtitle} + subtitleComponent={this.renderSubtitleComponent()} hideSubtitle={hideSubtitle} summaryComponent={summaryComponent} detailsComponent={this.renderDetails()} diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js index b34067686..c366d5137 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -36,7 +36,9 @@ const mapStateToProps = (state, props) => { fiatTransactionAmount, fiatTransactionFee, fiatTransactionTotal, - hexGasTotal, + hexTransactionAmount, + hexTransactionFee, + hexTransactionTotal, tokenData, methodData, txData, @@ -87,7 +89,9 @@ const mapStateToProps = (state, props) => { fiatTransactionAmount, fiatTransactionFee, fiatTransactionTotal, - hexGasTotal, + hexTransactionAmount, + hexTransactionFee, + hexTransactionTotal, txData, tokenData, methodData, diff --git a/ui/app/components/pages/settings/settings-tab/index.scss b/ui/app/components/pages/settings/settings-tab/index.scss index 76a0cec6f..3bf840c86 100644 --- a/ui/app/components/pages/settings/settings-tab/index.scss +++ b/ui/app/components/pages/settings/settings-tab/index.scss @@ -48,4 +48,22 @@ border-color: $ecstasy; } } + + &__radio-buttons { + display: flex; + align-items: center; + } + + &__radio-button { + display: flex; + align-items: center; + + &:not(:last-child) { + margin-right: 16px; + } + } + + &__radio-label { + padding-left: 4px; + } } diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js index 9da624f56..a9e2a723e 100644 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js @@ -55,6 +55,8 @@ export default class SettingsTab extends PureComponent { sendHexData: PropTypes.bool, currentCurrency: PropTypes.string, conversionDate: PropTypes.number, + useETHAsPrimaryCurrency: PropTypes.bool, + setUseETHAsPrimaryCurrencyPreference: PropTypes.func, } state = { @@ -339,6 +341,56 @@ export default class SettingsTab extends PureComponent { ) } + renderUseEthAsPrimaryCurrency () { + const { t } = this.context + const { useETHAsPrimaryCurrency, setUseETHAsPrimaryCurrencyPreference } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('primaryCurrencySetting') }</span> + <div className="settings-page__content-description"> + { t('primaryCurrencySettingDescription') } + </div> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <div className="settings-tab__radio-buttons"> + <div className="settings-tab__radio-button"> + <input + type="radio" + id="eth-primary-currency" + onChange={() => setUseETHAsPrimaryCurrencyPreference(true)} + checked={Boolean(useETHAsPrimaryCurrency)} + /> + <label + htmlFor="eth-primary-currency" + className="settings-tab__radio-label" + > + { t('eth') } + </label> + </div> + <div className="settings-tab__radio-button"> + <input + type="radio" + id="fiat-primary-currency" + onChange={() => setUseETHAsPrimaryCurrencyPreference(false)} + checked={!useETHAsPrimaryCurrency} + /> + <label + htmlFor="fiat-primary-currency" + className="settings-tab__radio-label" + > + { t('fiat') } + </label> + </div> + </div> + </div> + </div> + </div> + ) + } + render () { const { warning, isMascara } = this.props @@ -346,6 +398,7 @@ export default class SettingsTab extends PureComponent { <div className="settings-page__content"> { warning && <div className="settings-tab__error">{ warning }</div> } { this.renderCurrentConversion() } + { this.renderUseEthAsPrimaryCurrency() } { this.renderCurrentLocale() } { this.renderNewRpcUrl() } { this.renderStateLogs() } diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js index 665b56f5c..de30f309c 100644 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js +++ b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js @@ -11,7 +11,9 @@ import { updateCurrentLocale, setFeatureFlag, showModal, + setUseETHAsPrimaryCurrencyPreference, } from '../../../../actions' +import { preferencesSelector } from '../../../../selectors' const mapStateToProps = state => { const { appState: { warning }, metamask } = state @@ -24,6 +26,7 @@ const mapStateToProps = state => { isMascara, currentLocale, } = metamask + const { useETHAsPrimaryCurrency } = preferencesSelector(state) return { warning, @@ -34,6 +37,7 @@ const mapStateToProps = state => { useBlockie, sendHexData, provider, + useETHAsPrimaryCurrency, } } @@ -50,6 +54,9 @@ const mapDispatchToProps = dispatch => { }, setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)), showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })), + setUseETHAsPrimaryCurrencyPreference: value => { + return dispatch(setUseETHAsPrimaryCurrencyPreference(value)) + }, } } diff --git a/ui/app/components/send/account-list-item/account-list-item.component.js b/ui/app/components/send/account-list-item/account-list-item.component.js index 9f4a96e61..14bb7471f 100644 --- a/ui/app/components/send/account-list-item/account-list-item.component.js +++ b/ui/app/components/send/account-list-item/account-list-item.component.js @@ -2,7 +2,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { checksumAddress } from '../../../util' import Identicon from '../../identicon' -import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../constants/common' export default class AccountListItem extends Component { @@ -25,8 +26,6 @@ export default class AccountListItem extends Component { const { account, className, - conversionRate, - currentCurrency, displayAddress = false, displayBalance = true, handleClick, @@ -57,16 +56,20 @@ export default class AccountListItem extends Component { { checksumAddress(address) } </div>} - {displayBalance && <CurrencyDisplay - className="account-list-item__account-balances" - conversionRate={conversionRate} - convertedBalanceClassName="account-list-item__account-secondary-balance" - convertedCurrency={currentCurrency} - primaryBalanceClassName="account-list-item__account-primary-balance" - primaryCurrency="ETH" - readOnly={true} - value={balance} - />} + { + displayBalance && ( + <div className="account-list-item__account-balances"> + <UserPreferencedCurrencyDisplay + type={PRIMARY} + value={balance} + /> + <UserPreferencedCurrencyDisplay + type={SECONDARY} + value={balance} + /> + </div> + ) + } </div>) } diff --git a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js index ef152d2e7..f88c0dbd0 100644 --- a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js +++ b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js @@ -4,7 +4,7 @@ import { shallow } from 'enzyme' import sinon from 'sinon' import proxyquire from 'proxyquire' import Identicon from '../../../identicon' -import CurrencyDisplay from '../../currency-display' +import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display' const utilsMethodStubs = { checksumAddress: sinon.stub().returns('mockCheckSumAddress'), @@ -114,17 +114,11 @@ describe('AccountListItem Component', function () { it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => { wrapper.setProps({ displayBalance: true }) - assert.equal(wrapper.find(CurrencyDisplay).length, 1) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) assert.deepEqual( - wrapper.find(CurrencyDisplay).props(), + wrapper.find(UserPreferencedCurrencyDisplay).at(0).props(), { - className: 'account-list-item__account-balances', - conversionRate: 4, - convertedBalanceClassName: 'account-list-item__account-secondary-balance', - convertedCurrency: 'mockCurrentyCurrency', - primaryBalanceClassName: 'account-list-item__account-primary-balance', - primaryCurrency: 'ETH', - readOnly: true, + type: 'PRIMARY', value: 'mockBalance', } ) @@ -132,7 +126,7 @@ describe('AccountListItem Component', function () { it('should not render a CurrencyDisplay if displayBalance is false', () => { wrapper.setProps({ displayBalance: false }) - assert.equal(wrapper.find(CurrencyDisplay).length, 0) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 0) }) }) }) diff --git a/ui/app/components/send/currency-display/currency-display.js b/ui/app/components/send/currency-display/currency-display.js deleted file mode 100644 index 2b8eaa41f..000000000 --- a/ui/app/components/send/currency-display/currency-display.js +++ /dev/null @@ -1,186 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const { conversionUtil, multiplyCurrencies } = require('../../../conversion-util') -const { removeLeadingZeroes } = require('../send.utils') -const currencyFormatter = require('currency-formatter') -const currencies = require('currency-formatter/currencies') -const ethUtil = require('ethereumjs-util') -const PropTypes = require('prop-types') - -CurrencyDisplay.contextTypes = { - t: PropTypes.func, -} - -module.exports = CurrencyDisplay - -inherits(CurrencyDisplay, Component) -function CurrencyDisplay () { - Component.call(this) -} - -function toHexWei (value) { - return conversionUtil(value, { - fromNumericBase: 'dec', - toNumericBase: 'hex', - toDenomination: 'WEI', - }) -} - -CurrencyDisplay.prototype.componentWillMount = function () { - this.setState({ - valueToRender: this.getValueToRender(this.props), - }) -} - -CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) { - const currentValueToRender = this.getValueToRender(this.props) - const newValueToRender = this.getValueToRender(nextProps) - if (currentValueToRender !== newValueToRender) { - this.setState({ - valueToRender: newValueToRender, - }) - } -} - -CurrencyDisplay.prototype.getAmount = function (value) { - const { selectedToken } = this.props - const { decimals } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - - const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'}) - - return selectedToken - ? sendAmount - : toHexWei(value) -} - -CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) { - if (value === '0x0') return readOnly ? '0' : '' - const { decimals, symbol } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - - return selectedToken - ? conversionUtil(ethUtil.addHexPrefix(value), { - fromNumericBase: 'hex', - toNumericBase: 'dec', - toCurrency: symbol, - conversionRate: multiplier, - invertConversionRate: true, - }) - : conversionUtil(ethUtil.addHexPrefix(value), { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromDenomination: 'WEI', - numberOfDecimals: 9, - conversionRate, - }) -} - -CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { - const { primaryCurrency, convertedCurrency, conversionRate } = this.props - - if (conversionRate === 0 || conversionRate === null || conversionRate === undefined) { - if (nonFormattedValue !== 0) { - return null - } - } - - let convertedValue = conversionUtil(nonFormattedValue, { - fromNumericBase: 'dec', - fromCurrency: primaryCurrency, - toCurrency: convertedCurrency, - numberOfDecimals: 2, - conversionRate, - }) - - convertedValue = Number(convertedValue).toFixed(2) - const upperCaseCurrencyCode = convertedCurrency.toUpperCase() - return currencies.find(currency => currency.code === upperCaseCurrencyCode) - ? currencyFormatter.format(Number(convertedValue), { - code: upperCaseCurrencyCode, - }) - : convertedValue - } - -CurrencyDisplay.prototype.handleChange = function (newVal) { - this.setState({ valueToRender: removeLeadingZeroes(newVal) }) - this.props.onChange(this.getAmount(newVal)) -} - -CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) { - const valueString = String(valueToRender) - const valueLength = valueString.length || 1 - const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 - return (valueLength + decimalPointDeficit + 0.75) + 'ch' -} - -CurrencyDisplay.prototype.onlyRenderConversions = function (convertedValueToRender) { - const { - convertedBalanceClassName = 'currency-display__converted-value', - convertedCurrency, - } = this.props - return h('div', { - className: convertedBalanceClassName, - }, convertedValueToRender == null - ? this.context.t('noConversionRateAvailable') - : `${convertedValueToRender} ${convertedCurrency.toUpperCase()}` -) - } - -CurrencyDisplay.prototype.render = function () { - const { - className = 'currency-display', - primaryBalanceClassName = 'currency-display__input', - primaryCurrency, - readOnly = false, - inError = false, - onBlur, - step, - } = this.props - const { valueToRender } = this.state - - const convertedValueToRender = this.getConvertedValueToRender(valueToRender) - - return h('div', { - className, - style: { - borderColor: inError ? 'red' : null, - }, - onClick: () => { - this.currencyInput && this.currencyInput.focus() - }, - }, [ - - h('div.currency-display__primary-row', [ - - h('div.currency-display__input-wrapper', [ - - h('input', { - className: primaryBalanceClassName, - value: `${valueToRender}`, - placeholder: '0', - type: 'number', - readOnly, - ...(!readOnly ? { - onChange: e => this.handleChange(e.target.value), - onBlur: () => onBlur(this.getAmount(valueToRender)), - } : {}), - ref: input => { this.currencyInput = input }, - style: { - width: this.getInputWidth(valueToRender, readOnly), - }, - min: 0, - step, - }), - - h('span.currency-display__currency-symbol', primaryCurrency), - - ]), - - ]), this.onlyRenderConversions(convertedValueToRender), - - ]) - -} - diff --git a/ui/app/components/send/currency-display/index.js b/ui/app/components/send/currency-display/index.js deleted file mode 100644 index 5dc269c5a..000000000 --- a/ui/app/components/send/currency-display/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './currency-display.js' diff --git a/ui/app/components/send/currency-display/tests/currency-display.test.js b/ui/app/components/send/currency-display/tests/currency-display.test.js deleted file mode 100644 index c9560b81c..000000000 --- a/ui/app/components/send/currency-display/tests/currency-display.test.js +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react' -import assert from 'assert' -import sinon from 'sinon' -import { shallow, mount } from 'enzyme' -import CurrencyDisplay from '../currency-display' - -describe('', () => { - - const token = { - address: '0xTest', - symbol: 'TST', - decimals: '13', - } - - it('retuns ETH value for wei value', () => { - const wrapper = mount(<CurrencyDisplay />, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getValueToRender({ - // 1000000000000000000 - value: 'DE0B6B3A7640000', - }) - - assert.equal(value, 1) - }) - - it('returns value of token based on token decimals', () => { - const wrapper = mount(<CurrencyDisplay />, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getValueToRender({ - selectedToken: token, - // 1000000000000000000 - value: 'DE0B6B3A7640000', - }) - - assert.equal(value, 100000) - }) - - it('returns hex value with decimal adjustment', () => { - - const wrapper = mount( - <CurrencyDisplay - selectedToken={token} - />, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getAmount(1) - // 10000000000000 - assert.equal(value, '9184e72a000') - }) - - it('#getConvertedValueToRender converts input value based on conversionRate', () => { - - const wrapper = mount( - <CurrencyDisplay - primaryCurrency={'usd'} - convertedCurrency={'ja'} - conversionRate={2} - />, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getConvertedValueToRender(32) - - assert.equal(value, 64) - }) - - it('#onlyRenderConversions renders single element for converted currency and value', () => { - const wrapper = mount( - <CurrencyDisplay - convertedCurrency={'test'} - />, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().onlyRenderConversions(10) - assert.equal(value.props.className, 'currency-display__converted-value') - assert.equal(value.props.children, '10 TEST') - }) - - it('simulates change value in input', () => { - const handleChangeSpy = sinon.spy() - - const wrapper = shallow( - <CurrencyDisplay - onChange={handleChangeSpy} - />, {context: {t: str => str + '_t'}}) - - const input = wrapper.find('input') - input.simulate('focus') - input.simulate('change', { target: { value: '100' } }) - - assert.equal(wrapper.state().valueToRender, '100') - assert.equal(wrapper.find('input').prop('value'), '100') - }) - -}) diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js index 4d0d36ab4..ceb620941 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js @@ -34,21 +34,27 @@ export default class AmountMaxButton extends Component { }) } + onMaxClick = (event) => { + const { setMaxModeTo } = this.props + + event.preventDefault() + setMaxModeTo(true) + this.setMaxAmount() + } + render () { - const { setMaxModeTo, maxModeOn } = this.props - - return ( - <div - className="send-v2__amount-max" - onClick={(event) => { - event.preventDefault() - setMaxModeTo(true) - this.setMaxAmount() - }} - > - {!maxModeOn ? this.context.t('max') : ''} - </div> - ) + return this.props.maxModeOn + ? null + : ( + <div> + <span + className="send-v2__amount-max" + onClick={this.onMaxClick} + > + {this.context.t('max')} + </span> + </div> + ) } } diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js index 86a05ff21..b04d3897f 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js @@ -56,9 +56,8 @@ describe('AmountMaxButton Component', function () { }) describe('render', () => { - it('should render a div with a send-v2__amount-max class', () => { - assert.equal(wrapper.find('.send-v2__amount-max').length, 1) - assert(wrapper.find('.send-v2__amount-max').is('div')) + it('should render an element with a send-v2__amount-max class', () => { + assert(wrapper.exists('.send-v2__amount-max')) }) it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => { @@ -77,9 +76,9 @@ describe('AmountMaxButton Component', function () { ) }) - it('should not render text when maxModeOn is true', () => { + it('should not render anything when maxModeOn is true', () => { wrapper.setProps({ maxModeOn: true }) - assert.equal(wrapper.find('.send-v2__amount-max').text(), '') + assert.ok(!wrapper.exists('.send-v2__amount-max')) }) it('should render the expected text when maxModeOn is false', () => { diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js index c548a5695..0268376bf 100644 --- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js +++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js @@ -2,7 +2,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import SendRowWrapper from '../send-row-wrapper/' import AmountMaxButton from './amount-max-button/' -import CurrencyDisplay from '../../currency-display' +import UserPreferencedCurrencyInput from '../../../user-preferenced-currency-input' +import UserPreferencedTokenInput from '../../../user-preferenced-token-input' export default class SendAmountRow extends Component { @@ -84,16 +85,25 @@ export default class SendAmountRow extends Component { } } + renderInput () { + const { amount, inError, selectedToken } = this.props + const Component = selectedToken ? UserPreferencedTokenInput : UserPreferencedCurrencyInput + + return ( + <Component + onChange={newAmount => this.validateAmount(newAmount)} + onBlur={newAmount => { + this.updateGas(newAmount) + this.updateAmount(newAmount) + }} + error={inError} + value={amount} + /> + ) + } + render () { - const { - amount, - amountConversionRate, - convertedCurrency, - gasTotal, - inError, - primaryCurrency, - selectedToken, - } = this.props + const { gasTotal, inError } = this.props return ( <SendRowWrapper @@ -102,20 +112,7 @@ export default class SendAmountRow extends Component { errorType={'amount'} > {!inError && gasTotal && <AmountMaxButton />} - <CurrencyDisplay - conversionRate={amountConversionRate} - convertedCurrency={convertedCurrency} - onBlur={newAmount => { - this.updateGas(newAmount) - this.updateAmount(newAmount) - }} - onChange={newAmount => this.validateAmount(newAmount)} - inError={inError} - primaryCurrency={primaryCurrency || 'ETH'} - selectedToken={selectedToken} - value={amount} - step="any" - /> + { this.renderInput() } </SendRowWrapper> ) } diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js index 8425e076e..e63db4a2d 100644 --- a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js +++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js @@ -6,7 +6,7 @@ import SendAmountRow from '../send-amount-row.component.js' import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' import AmountMaxButton from '../amount-max-button/amount-max-button.container' -import CurrencyDisplay from '../../../currency-display' +import UserPreferencedTokenInput from '../../../../user-preferenced-token-input' const propsMethodSpies = { setMaxModeTo: sinon.spy(), @@ -150,26 +150,19 @@ describe('SendAmountRow Component', function () { assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton)) }) - it('should render a CurrencyDisplay as the second child of the SendRowWrapper', () => { - assert(wrapper.find(SendRowWrapper).childAt(1).is(CurrencyDisplay)) + it('should render a UserPreferencedTokenInput as the second child of the SendRowWrapper', () => { + console.log('HI', wrapper.find(SendRowWrapper).childAt(1)) + assert(wrapper.find(SendRowWrapper).childAt(1).is(UserPreferencedTokenInput)) }) - it('should render the CurrencyDisplay with the correct props', () => { + it('should render the UserPreferencedTokenInput with the correct props', () => { const { - conversionRate, - convertedCurrency, onBlur, onChange, - inError, - primaryCurrency, - selectedToken, + error, value, } = wrapper.find(SendRowWrapper).childAt(1).props() - assert.equal(conversionRate, 'mockAmountConversionRate') - assert.equal(convertedCurrency, 'mockConvertedCurrency') - assert.equal(inError, false) - assert.equal(primaryCurrency, 'mockPrimaryCurrency') - assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) + assert.equal(error, false) assert.equal(value, 'mockAmount') assert.equal(SendAmountRow.prototype.updateGas.callCount, 0) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0) @@ -192,11 +185,5 @@ describe('SendAmountRow Component', function () { ['mockNewAmount'] ) }) - - it('should pass the default primaryCurrency to the CurrencyDisplay if primaryCurrency is falsy', () => { - wrapper.setProps({ primaryCurrency: null }) - const { primaryCurrency } = wrapper.find(SendRowWrapper).childAt(1).props() - assert.equal(primaryCurrency, 'ETH') - }) }) }) diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js index bb9a94428..9bbb67506 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js @@ -1,7 +1,7 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' -import CurrencyDisplay from '../../../../send/currency-display' - +import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../../../constants/common' export default class GasFeeDisplay extends Component { @@ -19,27 +19,24 @@ export default class GasFeeDisplay extends Component { }; render () { - const { - conversionRate, - gasTotal, - onClick, - primaryCurrency = 'ETH', - convertedCurrency, - gasLoadingError, - } = this.props + const { gasTotal, onClick, gasLoadingError } = this.props return ( <div className="send-v2__gas-fee-display"> {gasTotal - ? <CurrencyDisplay - primaryCurrency={primaryCurrency} - convertedCurrency={convertedCurrency} - value={gasTotal} - conversionRate={conversionRate} - gasLoadingError={gasLoadingError} - convertedPrefix={'$'} - readOnly - /> + ? ( + <div className="currency-display"> + <UserPreferencedCurrencyDisplay + value={gasTotal} + type={PRIMARY} + /> + <UserPreferencedCurrencyDisplay + className="currency-display__converted-value" + value={gasTotal} + type={SECONDARY} + /> + </div> + ) : gasLoadingError ? <div className="currency-display.currency-display--message"> {this.context.t('setGasPrice')} diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js index 7cbe8d0df..9ff01493a 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import {shallow} from 'enzyme' import GasFeeDisplay from '../gas-fee-display.component' -import CurrencyDisplay from '../../../../../send/currency-display' +import UserPreferencedCurrencyDisplay from '../../../../../user-preferenced-currency-display' import sinon from 'sinon' @@ -29,17 +29,15 @@ describe('SendGasRow Component', function () { describe('render', () => { it('should render a CurrencyDisplay component', () => { - assert.equal(wrapper.find(CurrencyDisplay).length, 1) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) }) it('should render the CurrencyDisplay with the correct props', () => { const { - conversionRate, - convertedCurrency, + type, value, - } = wrapper.find(CurrencyDisplay).props() - assert.equal(conversionRate, 20) - assert.equal(convertedCurrency, 'mockConvertedCurrency') + } = wrapper.find(UserPreferencedCurrencyDisplay).at(0).props() + assert.equal(type, 'PRIMARY') assert.equal(value, 'mockGasTotal') }) diff --git a/ui/app/components/token-input/index.js b/ui/app/components/token-input/index.js new file mode 100644 index 000000000..22c06111e --- /dev/null +++ b/ui/app/components/token-input/index.js @@ -0,0 +1 @@ +export { default } from './token-input.container' diff --git a/ui/app/components/token-input/tests/token-input.component.test.js b/ui/app/components/token-input/tests/token-input.component.test.js new file mode 100644 index 000000000..2131e7705 --- /dev/null +++ b/ui/app/components/token-input/tests/token-input.component.test.js @@ -0,0 +1,308 @@ +import React from 'react' +import PropTypes from 'prop-types' +import assert from 'assert' +import { shallow, mount } from 'enzyme' +import sinon from 'sinon' +import { Provider } from 'react-redux' +import configureMockStore from 'redux-mock-store' +import TokenInput from '../token-input.component' +import UnitInput from '../../unit-input' +import CurrencyDisplay from '../../currency-display' + +describe('TokenInput Component', () => { + const t = key => `translate ${key}` + + describe('rendering', () => { + it('should render properly without a token', () => { + const wrapper = shallow( + <TokenInput />, + { context: { t } } + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(UnitInput).length, 1) + }) + + it('should render properly with a token', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <TokenInput + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + /> + </Provider>, + { context: { t }, + childContextTypes: { + t: PropTypes.func, + }, + }, + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find('.currency-input__conversion-component').length, 1) + assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'translate noConversionRateAvailable') + }) + + it('should render properly with a token and selectedTokenExchangeRate', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <TokenInput + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + /> + </Provider>, + { context: { t }, + childContextTypes: { + t: PropTypes.func, + }, + }, + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should render properly with a token value for ETH', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <TokenInput + value="2710" + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + /> + </Provider> + ) + + assert.ok(wrapper) + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find('.unit-input__input').props().value, '1') + assert.equal(wrapper.find('.currency-display-component').text(), '2 ETH') + }) + + it('should render properly with a token value for fiat', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <TokenInput + value="2710" + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + showFiat + /> + </Provider> + ) + + assert.ok(wrapper) + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find('.unit-input__input').props().value, '1') + assert.equal(wrapper.find('.currency-display-component').text(), '$462.12 USD') + }) + }) + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy() + const handleBlurSpy = sinon.spy() + + afterEach(() => { + handleChangeSpy.resetHistory() + handleBlurSpy.resetHistory() + }) + + it('should call onChange and onBlur on input changes with the hex value for ETH', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + <Provider store={store}> + <TokenInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + /> + </Provider> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 0) + assert.equal(tokenInputInstance.state.hexValue, undefined) + assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH') + const input = wrapper.find('input') + assert.equal(input.props().value, 0) + + input.simulate('change', { target: { value: 1 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith('2710')) + assert.equal(wrapper.find('.currency-display-component').text(), '2 ETH') + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('2710')) + }) + + it('should call onChange and onBlur on input changes with the hex value for fiat', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + <Provider store={store}> + <TokenInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + showFiat + /> + </Provider> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 0) + assert.equal(tokenInputInstance.state.hexValue, undefined) + assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD') + const input = wrapper.find('input') + assert.equal(input.props().value, 0) + + input.simulate('change', { target: { value: 1 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith('2710')) + assert.equal(wrapper.find('.currency-display-component').text(), '$462.12 USD') + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('2710')) + }) + + it('should change the state and pass in a new decimalValue when props.value changes', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = shallow( + <Provider store={store}> + <TokenInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + showFiat + /> + </Provider> + ) + + assert.ok(wrapper) + const tokenInputInstance = wrapper.find(TokenInput).dive() + assert.equal(tokenInputInstance.state('decimalValue'), 0) + assert.equal(tokenInputInstance.state('hexValue'), undefined) + assert.equal(tokenInputInstance.find(UnitInput).props().value, 0) + + tokenInputInstance.setProps({ value: '2710' }) + tokenInputInstance.update() + assert.equal(tokenInputInstance.state('decimalValue'), 1) + assert.equal(tokenInputInstance.state('hexValue'), '2710') + assert.equal(tokenInputInstance.find(UnitInput).props().value, 1) + }) + }) +}) diff --git a/ui/app/components/token-input/tests/token-input.container.test.js b/ui/app/components/token-input/tests/token-input.container.test.js new file mode 100644 index 000000000..d73bc9a94 --- /dev/null +++ b/ui/app/components/token-input/tests/token-input.container.test.js @@ -0,0 +1,129 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps, mergeProps + +proxyquire('../token-input.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mergeProps = mp + return () => ({}) + }, + }, +}) + +describe('TokenInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props when send is empty', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x1', + contractExchangeRates: {}, + send: {}, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 0, + }) + }) + + it('should return the correct props when selectedTokenAddress is not found and send is populated', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x2', + contractExchangeRates: {}, + send: { + token: { address: 'test' }, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: 'test', + }, + selectedTokenExchangeRate: 0, + }) + }) + + it('should return the correct props when contractExchangeRates is populated', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x1', + contractExchangeRates: { + '0x1': 5, + }, + send: {}, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 5, + }) + }) + }) + + describe('mergeProps()', () => { + it('should return the correct props', () => { + const mockStateProps = { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 5, + } + + assert.deepEqual(mergeProps(mockStateProps, {}, {}), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 5, + suffix: 'ABC', + }) + }) + }) +}) diff --git a/ui/app/components/token-input/token-input.component.js b/ui/app/components/token-input/token-input.component.js new file mode 100644 index 000000000..d1388945b --- /dev/null +++ b/ui/app/components/token-input/token-input.component.js @@ -0,0 +1,136 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import UnitInput from '../unit-input' +import CurrencyDisplay from '../currency-display' +import { getWeiHexFromDecimalValue } from '../../helpers/conversions.util' +import ethUtil from 'ethereumjs-util' +import { conversionUtil, multiplyCurrencies } from '../../conversion-util' +import { ETH } from '../../constants/common' + +/** + * Component that allows user to enter token values as a number, and props receive a converted + * hex value. props.value, used as a default or forced value, should be a hex value, which + * gets converted into a decimal value. + */ +export default class TokenInput extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + currentCurrency: PropTypes.string, + onChange: PropTypes.func, + onBlur: PropTypes.func, + value: PropTypes.string, + suffix: PropTypes.string, + showFiat: PropTypes.bool, + selectedToken: PropTypes.object, + selectedTokenExchangeRate: PropTypes.number, + } + + constructor (props) { + super(props) + + const { value: hexValue } = props + const decimalValue = hexValue ? this.getDecimalValue(props) : 0 + + this.state = { + decimalValue, + hexValue, + } + } + + componentDidUpdate (prevProps) { + const { value: prevPropsHexValue } = prevProps + const { value: propsHexValue } = this.props + const { hexValue: stateHexValue } = this.state + + if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) { + const decimalValue = this.getDecimalValue(this.props) + this.setState({ hexValue: propsHexValue, decimalValue }) + } + } + + getDecimalValue (props) { + const { value: hexValue, selectedToken: { decimals, symbol } = {} } = props + + const multiplier = Math.pow(10, Number(decimals || 0)) + const decimalValueString = conversionUtil(ethUtil.addHexPrefix(hexValue), { + fromNumericBase: 'hex', + toNumericBase: 'dec', + toCurrency: symbol, + conversionRate: multiplier, + invertConversionRate: true, + }) + + return Number(decimalValueString) || 0 + } + + handleChange = decimalValue => { + const { selectedToken: { decimals } = {}, onChange } = this.props + + const multiplier = Math.pow(10, Number(decimals || 0)) + const hexValue = multiplyCurrencies(decimalValue || 0, multiplier, { toNumericBase: 'hex' }) + + this.setState({ hexValue, decimalValue }) + onChange(hexValue) + } + + handleBlur = () => { + this.props.onBlur(this.state.hexValue) + } + + renderConversionComponent () { + const { selectedTokenExchangeRate, showFiat, currentCurrency } = this.props + const { decimalValue } = this.state + let currency, numberOfDecimals + + if (showFiat) { + // Display Fiat + currency = currentCurrency + numberOfDecimals = 2 + } else { + // Display ETH + currency = ETH + numberOfDecimals = 6 + } + + const decimalEthValue = (decimalValue * selectedTokenExchangeRate) || 0 + const hexWeiValue = getWeiHexFromDecimalValue({ + value: decimalEthValue, + fromCurrency: ETH, + fromDenomination: ETH, + }) + + return selectedTokenExchangeRate + ? ( + <CurrencyDisplay + className="currency-input__conversion-component" + currency={currency} + value={hexWeiValue} + numberOfDecimals={numberOfDecimals} + /> + ) : ( + <div className="currency-input__conversion-component"> + { this.context.t('noConversionRateAvailable') } + </div> + ) + } + + render () { + const { suffix, ...restProps } = this.props + const { decimalValue } = this.state + + return ( + <UnitInput + {...restProps} + suffix={suffix} + onChange={this.handleChange} + onBlur={this.handleBlur} + value={decimalValue} + > + { this.renderConversionComponent() } + </UnitInput> + ) + } +} diff --git a/ui/app/components/token-input/token-input.container.js b/ui/app/components/token-input/token-input.container.js new file mode 100644 index 000000000..ec233b1b8 --- /dev/null +++ b/ui/app/components/token-input/token-input.container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux' +import TokenInput from './token-input.component' +import { getSelectedToken, getSelectedTokenExchangeRate } from '../../selectors' + +const mapStateToProps = state => { + const { metamask: { currentCurrency } } = state + + return { + currentCurrency, + selectedToken: getSelectedToken(state), + selectedTokenExchangeRate: getSelectedTokenExchangeRate(state), + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { selectedToken } = stateProps + const suffix = selectedToken && selectedToken.symbol + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + suffix, + } +} + +export default connect(mapStateToProps, null, mergeProps)(TokenInput) diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js index 32834ff47..97aa9a8f1 100644 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js @@ -46,11 +46,15 @@ export function getActivities (transaction) { if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) { const { time, txParams: { value } = {} } = base return acc.concat(eventCreator(TRANSACTION_CREATED_EVENT, time, value)) + // An entry in the history may be an array of more sub-entries. } else if (Array.isArray(base)) { const events = [] base.forEach(entry => { - const { op, path, value, timestamp } = entry + const { op, path, value, timestamp: entryTimestamp } = entry + // Not all sub-entries in a history entry have a timestamp. If the sub-entry does not have a + // timestamp, the first sub-entry in a history entry should. + const timestamp = entryTimestamp || base[0] && base[0].timestamp if (path in eventPathsHash && op === REPLACE_OP) { switch (path) { diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js index bb6075e9f..77bedcad7 100644 --- a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js +++ b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js @@ -4,8 +4,9 @@ import classnames from 'classnames' import TransactionBreakdownRow from './transaction-breakdown-row' import Card from '../card' import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import HexToDecimal from '../hex-to-decimal' -import { ETH, GWEI } from '../../constants/common' +import { ETH, GWEI, PRIMARY, SECONDARY } from '../../constants/common' import { getHexGasTotal } from '../../helpers/confirm-transaction/util' import { sumHexes } from '../../helpers/transactions.util' @@ -26,8 +27,11 @@ export default class TransactionBreakdown extends PureComponent { render () { const { t } = this.context const { transaction, className } = this.props - const { txParams: { gas, gasPrice, value } = {} } = transaction - const hexGasTotal = getHexGasTotal({ gasLimit: gas, gasPrice }) + const { txParams: { gas, gasPrice, value } = {}, txReceipt: { gasUsed } = {} } = transaction + + const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas + + const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice }) const totalInHex = sumHexes(hexGasTotal, value) return ( @@ -37,9 +41,9 @@ export default class TransactionBreakdown extends PureComponent { className="transaction-breakdown__card" > <TransactionBreakdownRow title={t('amount')}> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-breakdown__value" - currency={ETH} + type={PRIMARY} value={value} /> </TransactionBreakdownRow> @@ -52,6 +56,19 @@ export default class TransactionBreakdown extends PureComponent { value={gas} /> </TransactionBreakdownRow> + { + typeof gasUsed === 'string' && ( + <TransactionBreakdownRow + title={`${t('gasUsed')} (${t('units')})`} + className="transaction-breakdown__row-title" + > + <HexToDecimal + className="transaction-breakdown__value" + value={gasUsed} + /> + </TransactionBreakdownRow> + ) + } <TransactionBreakdownRow title={t('gasPrice')}> <CurrencyDisplay className="transaction-breakdown__value" @@ -63,14 +80,14 @@ export default class TransactionBreakdown extends PureComponent { </TransactionBreakdownRow> <TransactionBreakdownRow title={t('total')}> <div> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-breakdown__value transaction-breakdown__value--eth-total" - currency={ETH} + type={PRIMARY} value={totalInHex} - numberOfDecimals={6} /> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-breakdown__value" + type={SECONDARY} value={totalInHex} /> </div> diff --git a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js index 13cb51349..a4f28fd63 100644 --- a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js @@ -5,6 +5,7 @@ import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.consta import TransactionActivityLog from '../transaction-activity-log' import TransactionBreakdown from '../transaction-breakdown' import Button from '../button' +import Tooltip from '../tooltip' import prefixForNetwork from '../../../lib/etherscan-prefix-for-network' export default class TransactionListItemDetails extends PureComponent { @@ -75,13 +76,15 @@ export default class TransactionListItemDetails extends PureComponent { </Button> ) } - <Button - type="raised" - onClick={this.handleEtherscanClick} - className="transaction-list-item-details__header-button" - > - <img src="/images/arrow-popout.svg" /> - </Button> + <Tooltip title={t('viewOnEtherscan')}> + <Button + type="raised" + onClick={this.handleEtherscanClick} + className="transaction-list-item-details__header-button" + > + <img src="/images/arrow-popout.svg" /> + </Button> + </Tooltip> </div> </div> <div className="transaction-list-item-details__sender-to-recipient-container"> diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/transaction-list-item/transaction-list-item.component.js index c1c69f59b..88573d2d5 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/transaction-list-item/transaction-list-item.component.js @@ -4,13 +4,14 @@ import classnames from 'classnames' import Identicon from '../identicon' import TransactionStatus from '../transaction-status' import TransactionAction from '../transaction-action' -import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import TokenCurrencyDisplay from '../token-currency-display' import TransactionListItemDetails from '../transaction-list-item-details' import { CONFIRM_TRANSACTION_ROUTE } from '../../routes' import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions' -import { ETH } from '../../constants/common' +import { PRIMARY, SECONDARY } from '../../constants/common' import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../app/scripts/lib/enums' +import { getStatusKey } from '../../helpers/transactions.util' export default class TransactionListItem extends PureComponent { static propTypes = { @@ -102,12 +103,11 @@ export default class TransactionListItem extends PureComponent { prefix="-" /> ) : ( - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-list-item__amount transaction-list-item__amount--primary" value={value} + type={PRIMARY} prefix="-" - numberOfDecimals={2} - currency={ETH} /> ) } @@ -118,10 +118,11 @@ export default class TransactionListItem extends PureComponent { return token ? null : ( - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-list-item__amount transaction-list-item__amount--secondary" - prefix="-" value={value} + prefix="-" + type={SECONDARY} /> ) } @@ -167,7 +168,7 @@ export default class TransactionListItem extends PureComponent { </div> <TransactionStatus className="transaction-list-item__status" - statusKey={transaction.status} + statusKey={getStatusKey(transaction)} title={( (transaction.err && transaction.err.rpc) ? transaction.err.rpc.message diff --git a/ui/app/components/transaction-status/index.scss b/ui/app/components/transaction-status/index.scss index 35be550f7..26a1f5d38 100644 --- a/ui/app/components/transaction-status/index.scss +++ b/ui/app/components/transaction-status/index.scss @@ -25,4 +25,9 @@ background-color: #FFF2DB; color: #CA810A; } -}
\ No newline at end of file + + &--failed { + background: lighten($monzo, 56%); + color: $monzo; + } +} diff --git a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js index bb95cb27e..513a8aac9 100644 --- a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js +++ b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js @@ -3,7 +3,7 @@ import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' import TokenBalance from '../../token-balance' -import CurrencyDisplay from '../../currency-display' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' import { SEND_ROUTE } from '../../../routes' import TransactionViewBalance from '../transaction-view-balance.component' @@ -35,7 +35,7 @@ describe('TransactionViewBalance Component', () => { assert.equal(wrapper.find('.transaction-view-balance').length, 1) assert.equal(wrapper.find('.transaction-view-balance__button').length, 2) - assert.equal(wrapper.find(CurrencyDisplay).length, 2) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) const buttons = wrapper.find('.transaction-view-balance__buttons') assert.equal(propsMethodSpies.showDepositModal.callCount, 0) diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js index 1b7a29c87..273845c47 100644 --- a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js +++ b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js @@ -3,9 +3,9 @@ import PropTypes from 'prop-types' import Button from '../button' import Identicon from '../identicon' import TokenBalance from '../token-balance' -import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import { SEND_ROUTE } from '../../routes' -import { ETH } from '../../constants/common' +import { PRIMARY, SECONDARY } from '../../constants/common' export default class TransactionViewBalance extends PureComponent { static contextTypes = { @@ -33,15 +33,17 @@ export default class TransactionViewBalance extends PureComponent { /> ) : ( <div className="transaction-view-balance__balance"> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-view-balance__primary-balance" value={balance} - currency={ETH} - numberOfDecimals={3} + type={PRIMARY} + ethNumberOfDecimals={3} /> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-view-balance__secondary-balance" value={balance} + type={SECONDARY} + ethNumberOfDecimals={3} /> </div> ) diff --git a/ui/app/components/unit-input/index.js b/ui/app/components/unit-input/index.js new file mode 100644 index 000000000..7c33c9e5c --- /dev/null +++ b/ui/app/components/unit-input/index.js @@ -0,0 +1 @@ +export { default } from './unit-input.component' diff --git a/ui/app/components/unit-input/index.scss b/ui/app/components/unit-input/index.scss new file mode 100644 index 000000000..28c5bf6f0 --- /dev/null +++ b/ui/app/components/unit-input/index.scss @@ -0,0 +1,44 @@ +.unit-input { + min-height: 54px; + border: 1px solid #dedede; + border-radius: 4px; + background-color: #fff; + color: #4d4d4d; + font-size: 1rem; + padding: 8px 10px; + position: relative; + + input[type="number"] { + -moz-appearance: textfield; + } + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + &__input { + color: #4d4d4d; + font-size: 1rem; + font-family: Roboto; + border: none; + outline: 0 !important; + max-width: 22ch; + } + + &__input-container { + display: flex; + align-items: center; + } + + &--error { + border-color: $red; + } +} diff --git a/ui/app/components/unit-input/tests/unit-input.component.test.js b/ui/app/components/unit-input/tests/unit-input.component.test.js new file mode 100644 index 000000000..97d987bc7 --- /dev/null +++ b/ui/app/components/unit-input/tests/unit-input.component.test.js @@ -0,0 +1,146 @@ +import React from 'react' +import assert from 'assert' +import { shallow, mount } from 'enzyme' +import sinon from 'sinon' +import UnitInput from '../unit-input.component' + +describe('UnitInput Component', () => { + describe('rendering', () => { + it('should render properly without a suffix', () => { + const wrapper = shallow( + <UnitInput /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 0) + }) + + it('should render properly with a suffix', () => { + const wrapper = shallow( + <UnitInput + suffix="ETH" + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') + }) + + it('should render properly with a child omponent', () => { + const wrapper = shallow( + <UnitInput> + <div className="testing"> + TESTCOMPONENT + </div> + </UnitInput> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.testing').length, 1) + assert.equal(wrapper.find('.testing').text(), 'TESTCOMPONENT') + }) + + it('should render with an error class when props.error === true', () => { + const wrapper = shallow( + <UnitInput + error + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input--error').length, 1) + }) + }) + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy() + const handleBlurSpy = sinon.spy() + + afterEach(() => { + handleChangeSpy.resetHistory() + handleBlurSpy.resetHistory() + }) + + it('should focus the input on component click', () => { + const wrapper = mount( + <UnitInput /> + ) + + assert.ok(wrapper) + const handleFocusSpy = sinon.spy(wrapper.instance(), 'handleFocus') + wrapper.instance().forceUpdate() + wrapper.update() + assert.equal(handleFocusSpy.callCount, 0) + wrapper.find('.unit-input').simulate('click') + assert.equal(handleFocusSpy.callCount, 1) + }) + + it('should call onChange on input changes with the value', () => { + const wrapper = mount( + <UnitInput + onChange={handleChangeSpy} + /> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + assert.equal(wrapper.state('value'), 123) + }) + + it('should call onBlur on blur with the value', () => { + const wrapper = mount( + <UnitInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + /> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + assert.equal(wrapper.state('value'), 123) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith(123)) + }) + + it('should set the component state value with props.value', () => { + const wrapper = mount( + <UnitInput + value={123} + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.state('value'), 123) + }) + + it('should update the component state value with props.value', () => { + const wrapper = mount( + <UnitInput + onChange={handleChangeSpy} + /> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(wrapper.state('value'), 123) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + wrapper.setProps({ value: 456 }) + assert.equal(wrapper.state('value'), 456) + assert.equal(handleChangeSpy.callCount, 1) + }) + }) +}) diff --git a/ui/app/components/unit-input/unit-input.component.js b/ui/app/components/unit-input/unit-input.component.js new file mode 100644 index 000000000..f1ebf4d77 --- /dev/null +++ b/ui/app/components/unit-input/unit-input.component.js @@ -0,0 +1,104 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { removeLeadingZeroes } from '../send/send.utils' + +/** + * Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also + * allows rendering a child component underneath the input to, for example, display conversions of + * the shown suffix. + */ +export default class UnitInput extends PureComponent { + static propTypes = { + children: PropTypes.node, + error: PropTypes.bool, + onBlur: PropTypes.func, + onChange: PropTypes.func, + placeholder: PropTypes.string, + suffix: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + } + + static defaultProps = { + placeholder: '0', + } + + constructor (props) { + super(props) + + this.state = { + value: props.value || '', + } + } + + componentDidUpdate (prevProps) { + const { value: prevPropsValue } = prevProps + const { value: propsValue } = this.props + const { value: stateValue } = this.state + + if (prevPropsValue !== propsValue && propsValue !== stateValue) { + this.setState({ value: propsValue }) + } + } + + handleFocus = () => { + this.unitInput.focus() + } + + handleChange = event => { + const { value: userInput } = event.target + let value = userInput + + if (userInput.length && userInput.length > 1) { + value = removeLeadingZeroes(userInput) + } + + this.setState({ value }) + this.props.onChange(value) + } + + handleBlur = event => { + const { onBlur } = this.props + typeof onBlur === 'function' && onBlur(this.state.value) + } + + getInputWidth (value) { + const valueString = String(value) + const valueLength = valueString.length || 1 + const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 + return (valueLength + decimalPointDeficit + 0.75) + 'ch' + } + + render () { + const { error, placeholder, suffix, children } = this.props + const { value } = this.state + + return ( + <div + className={classnames('unit-input', { 'unit-input--error': error })} + onClick={this.handleFocus} + > + <div className="unit-input__input-container"> + <input + type="number" + className="unit-input__input" + value={value} + placeholder={placeholder} + onChange={this.handleChange} + onBlur={this.handleBlur} + style={{ width: this.getInputWidth(value) }} + ref={ref => { this.unitInput = ref }} + /> + { + suffix && ( + <div className="unit-input__suffix"> + { suffix } + </div> + ) + } + </div> + { children } + </div> + ) + } +} diff --git a/ui/app/components/user-preferenced-currency-display/index.js b/ui/app/components/user-preferenced-currency-display/index.js new file mode 100644 index 000000000..0deddaecf --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/index.js @@ -0,0 +1 @@ +export { default } from './user-preferenced-currency-display.container' diff --git a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js new file mode 100644 index 000000000..ead584c26 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js @@ -0,0 +1,34 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display.component' +import CurrencyDisplay from '../../currency-display' + +describe('UserPreferencedCurrencyDisplay Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow( + <UserPreferencedCurrencyDisplay /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should pass all props to the CurrencyDisplay child component', () => { + const wrapper = shallow( + <UserPreferencedCurrencyDisplay + prop1={true} + prop2="test" + prop3={1} + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + assert.equal(wrapper.find(CurrencyDisplay).props().prop1, true) + assert.equal(wrapper.find(CurrencyDisplay).props().prop2, 'test') + assert.equal(wrapper.find(CurrencyDisplay).props().prop3, 1) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js new file mode 100644 index 000000000..41ad3b73e --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js @@ -0,0 +1,105 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps, mergeProps + +proxyquire('../user-preferenced-currency-display.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mergeProps = mp + return () => ({}) + }, + }, +}) + +describe('UserPreferencedCurrencyDisplay container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + preferences: { + useETHAsPrimaryCurrency: true, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + useETHAsPrimaryCurrency: true, + }) + }) + }) + + describe('mergeProps()', () => { + it('should return the correct props', () => { + const mockDispatchProps = {} + + const tests = [ + { + stateProps: { + useETHAsPrimaryCurrency: true, + }, + ownProps: { + type: 'PRIMARY', + }, + result: { + currency: 'ETH', + numberOfDecimals: 6, + prefix: undefined, + }, + }, + { + stateProps: { + useETHAsPrimaryCurrency: false, + }, + ownProps: { + type: 'PRIMARY', + }, + result: { + currency: undefined, + numberOfDecimals: 2, + prefix: undefined, + }, + }, + { + stateProps: { + useETHAsPrimaryCurrency: true, + }, + ownProps: { + type: 'SECONDARY', + fiatNumberOfDecimals: 4, + fiatPrefix: '-', + }, + result: { + currency: undefined, + numberOfDecimals: 4, + prefix: '-', + }, + }, + { + stateProps: { + useETHAsPrimaryCurrency: false, + }, + ownProps: { + type: 'SECONDARY', + fiatNumberOfDecimals: 4, + numberOfDecimals: 3, + fiatPrefix: 'a', + prefix: 'b', + }, + result: { + currency: 'ETH', + numberOfDecimals: 3, + prefix: 'b', + }, + }, + ] + + tests.forEach(({ stateProps, ownProps, result }) => { + assert.deepEqual(mergeProps({ ...stateProps }, mockDispatchProps, { ...ownProps }), { + ...result, + }) + }) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js new file mode 100644 index 000000000..4d948ca6a --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -0,0 +1,45 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { PRIMARY, SECONDARY, ETH } from '../../constants/common' +import CurrencyDisplay from '../currency-display' + +export default class UserPreferencedCurrencyDisplay extends PureComponent { + static propTypes = { + className: PropTypes.string, + prefix: PropTypes.string, + value: PropTypes.string, + numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + hideLabel: PropTypes.bool, + style: PropTypes.object, + showEthLogo: PropTypes.bool, + ethLogoHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + // Used in container + type: PropTypes.oneOf([PRIMARY, SECONDARY]), + ethNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + fiatNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + ethPrefix: PropTypes.string, + fiatPrefix: PropTypes.string, + // From container + currency: PropTypes.string, + } + + renderEthLogo () { + const { currency, showEthLogo, ethLogoHeight = 12 } = this.props + + return currency === ETH && showEthLogo && ( + <img + src="/images/eth.svg" + height={ethLogoHeight} + /> + ) + } + + render () { + return ( + <CurrencyDisplay + {...this.props} + prefixComponent={this.renderEthLogo()} + /> + ) + } +} diff --git a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js new file mode 100644 index 000000000..23240c649 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js @@ -0,0 +1,52 @@ +import { connect } from 'react-redux' +import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component' +import { preferencesSelector } from '../../selectors' +import { ETH, PRIMARY, SECONDARY } from '../../constants/common' + +const mapStateToProps = (state, ownProps) => { + const { useETHAsPrimaryCurrency } = preferencesSelector(state) + + return { + useETHAsPrimaryCurrency, + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { useETHAsPrimaryCurrency, ...restStateProps } = stateProps + const { + type, + numberOfDecimals: propsNumberOfDecimals, + ethNumberOfDecimals, + fiatNumberOfDecimals, + ethPrefix, + fiatPrefix, + prefix: propsPrefix, + ...restOwnProps + } = ownProps + + let currency, numberOfDecimals, prefix + + if (type === PRIMARY && useETHAsPrimaryCurrency || + type === SECONDARY && !useETHAsPrimaryCurrency) { + // Display ETH + currency = ETH + numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6 + prefix = propsPrefix || ethPrefix + } else if (type === SECONDARY && useETHAsPrimaryCurrency || + type === PRIMARY && !useETHAsPrimaryCurrency) { + // Display Fiat + numberOfDecimals = propsNumberOfDecimals || fiatNumberOfDecimals || 2 + prefix = propsPrefix || fiatPrefix + } + + return { + ...restStateProps, + ...dispatchProps, + ...restOwnProps, + currency, + numberOfDecimals, + prefix, + } +} + +export default connect(mapStateToProps, null, mergeProps)(UserPreferencedCurrencyDisplay) diff --git a/ui/app/components/user-preferenced-currency-input/index.js b/ui/app/components/user-preferenced-currency-input/index.js new file mode 100644 index 000000000..4dc70db3d --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/index.js @@ -0,0 +1 @@ +export { default } from './user-preferenced-currency-input.container' diff --git a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js new file mode 100644 index 000000000..0af80a03d --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js @@ -0,0 +1,32 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import UserPreferencedCurrencyInput from '../user-preferenced-currency-input.component' +import CurrencyInput from '../../currency-input' + +describe('UserPreferencedCurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow( + <UserPreferencedCurrencyInput /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyInput).length, 1) + }) + + it('should render useFiat for CurrencyInput based on preferences.useETHAsPrimaryCurrency', () => { + const wrapper = shallow( + <UserPreferencedCurrencyInput + useETHAsPrimaryCurrency + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyInput).length, 1) + assert.equal(wrapper.find(CurrencyInput).props().useFiat, false) + wrapper.setProps({ useETHAsPrimaryCurrency: false }) + assert.equal(wrapper.find(CurrencyInput).props().useFiat, true) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js new file mode 100644 index 000000000..d860c38da --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js @@ -0,0 +1,31 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../user-preferenced-currency-input.container.js', { + 'react-redux': { + connect: ms => { + mapStateToProps = ms + return () => ({}) + }, + }, +}) + +describe('UserPreferencedCurrencyInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + preferences: { + useETHAsPrimaryCurrency: true, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + useETHAsPrimaryCurrency: true, + }) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js new file mode 100644 index 000000000..6e0e00a1d --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js @@ -0,0 +1,20 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import CurrencyInput from '../currency-input' + +export default class UserPreferencedCurrencyInput extends PureComponent { + static propTypes = { + useETHAsPrimaryCurrency: PropTypes.bool, + } + + render () { + const { useETHAsPrimaryCurrency, ...restProps } = this.props + + return ( + <CurrencyInput + {...restProps} + useFiat={!useETHAsPrimaryCurrency} + /> + ) + } +} diff --git a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js new file mode 100644 index 000000000..397cdc7cc --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component' +import { preferencesSelector } from '../../selectors' + +const mapStateToProps = state => { + const { useETHAsPrimaryCurrency } = preferencesSelector(state) + + return { + useETHAsPrimaryCurrency, + } +} + +export default connect(mapStateToProps)(UserPreferencedCurrencyInput) diff --git a/ui/app/components/user-preferenced-token-input/index.js b/ui/app/components/user-preferenced-token-input/index.js new file mode 100644 index 000000000..54167e633 --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/index.js @@ -0,0 +1 @@ +export { default } from './user-preferenced-token-input.container' diff --git a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js new file mode 100644 index 000000000..910c7089f --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js @@ -0,0 +1,32 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import UserPreferencedTokenInput from '../user-preferenced-token-input.component' +import TokenInput from '../../token-input' + +describe('UserPreferencedCurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow( + <UserPreferencedTokenInput /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(TokenInput).length, 1) + }) + + it('should render showFiat for TokenInput based on preferences.useETHAsPrimaryCurrency', () => { + const wrapper = shallow( + <UserPreferencedTokenInput + useETHAsPrimaryCurrency + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(TokenInput).length, 1) + assert.equal(wrapper.find(TokenInput).props().showFiat, false) + wrapper.setProps({ useETHAsPrimaryCurrency: false }) + assert.equal(wrapper.find(TokenInput).props().showFiat, true) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js new file mode 100644 index 000000000..e3509149a --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js @@ -0,0 +1,31 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../user-preferenced-token-input.container.js', { + 'react-redux': { + connect: ms => { + mapStateToProps = ms + return () => ({}) + }, + }, +}) + +describe('UserPreferencedTokenInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + preferences: { + useETHAsPrimaryCurrency: true, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + useETHAsPrimaryCurrency: true, + }) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js new file mode 100644 index 000000000..f2b537f11 --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js @@ -0,0 +1,20 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import TokenInput from '../token-input' + +export default class UserPreferencedTokenInput extends PureComponent { + static propTypes = { + useETHAsPrimaryCurrency: PropTypes.bool, + } + + render () { + const { useETHAsPrimaryCurrency, ...restProps } = this.props + + return ( + <TokenInput + {...restProps} + showFiat={!useETHAsPrimaryCurrency} + /> + ) + } +} diff --git a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js new file mode 100644 index 000000000..416d069dd --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import UserPreferencedTokenInput from './user-preferenced-token-input.component' +import { preferencesSelector } from '../../selectors' + +const mapStateToProps = state => { + const { useETHAsPrimaryCurrency } = preferencesSelector(state) + + return { + useETHAsPrimaryCurrency, + } +} + +export default connect(mapStateToProps)(UserPreferencedTokenInput) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 064a6ab55..8a7cb0f8d 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -17,7 +17,7 @@ const TokenList = require('./token-list') const selectors = require('../selectors') const { ADD_TOKEN_ROUTE } = require('../routes') -import Button from './button' +import AddTokenButton from './add-token-button' module.exports = compose( withRouter, @@ -100,15 +100,30 @@ WalletView.prototype.renderWalletBalance = function () { ]) } +WalletView.prototype.renderAddToken = function () { + const { + sidebarOpen, + hideSidebar, + history, + } = this.props + + return h(AddTokenButton, { + onClick () { + history.push(ADD_TOKEN_ROUTE) + if (sidebarOpen) { + hideSidebar() + } + }, + }) +} + WalletView.prototype.render = function () { const { responsiveDisplayClassname, selectedAddress, keyrings, showAccountDetailModal, - sidebarOpen, hideSidebar, - history, identities, } = this.props // temporary logs + fake extra wallets @@ -201,14 +216,7 @@ WalletView.prototype.render = function () { h(TokenList), - h(Button, { - type: 'primary', - className: 'wallet-view__add-token-button', - onClick: () => { - history.push(ADD_TOKEN_ROUTE) - sidebarOpen && hideSidebar() - }, - }, this.context.t('addToken')), + this.renderAddToken(), ]) } diff --git a/ui/app/constants/common.js b/ui/app/constants/common.js index a20f6cc02..4ff4dc837 100644 --- a/ui/app/constants/common.js +++ b/ui/app/constants/common.js @@ -1,3 +1,6 @@ export const ETH = 'ETH' export const GWEI = 'GWEI' export const WEI = 'WEI' + +export const PRIMARY = 'PRIMARY' +export const SECONDARY = 'SECONDARY' diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index 8e963d495..233e781ef 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -120,18 +120,6 @@ $wallet-view-bg: $alabaster; } } } - - &__add-token-button { - flex: 0 0 auto; - margin: 36px auto; - background: none; - transition: border-color .3s ease; - width: 150px; - - &:hover { - border-color: $curious-blue; - } - } } @media screen and (min-width: 576px) { diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js index 30c32f2bf..2ceafbe08 100644 --- a/ui/app/ducks/confirm-transaction.duck.js +++ b/ui/app/ducks/confirm-transaction.duck.js @@ -14,7 +14,13 @@ import { hexGreaterThan, } from '../helpers/confirm-transaction/util' -import { getTokenData, getMethodData, isSmartContractAddress } from '../helpers/transactions.util' +import { + getTokenData, + getMethodData, + isSmartContractAddress, + sumHexes, +} from '../helpers/transactions.util' + import { getSymbolAndDecimals } from '../token-util' import { conversionUtil } from '../conversion-util' @@ -31,7 +37,6 @@ const CLEAR_CONFIRM_TRANSACTION = createActionType('CLEAR_CONFIRM_TRANSACTION') const UPDATE_TRANSACTION_AMOUNTS = createActionType('UPDATE_TRANSACTION_AMOUNTS') const UPDATE_TRANSACTION_FEES = createActionType('UPDATE_TRANSACTION_FEES') const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS') -const UPDATE_HEX_GAS_TOTAL = createActionType('UPDATE_HEX_GAS_TOTAL') const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS') const UPDATE_NONCE = createActionType('UPDATE_NONCE') const UPDATE_TO_SMART_CONTRACT = createActionType('UPDATE_TO_SMART_CONTRACT') @@ -53,7 +58,9 @@ const initState = { ethTransactionAmount: '', ethTransactionFee: '', ethTransactionTotal: '', - hexGasTotal: '', + hexTransactionAmount: '', + hexTransactionFee: '', + hexTransactionTotal: '', nonce: '', toSmartContract: false, fetchingData: false, @@ -99,30 +106,28 @@ export default function reducer ({ confirmTransaction: confirmState = initState methodData: {}, } case UPDATE_TRANSACTION_AMOUNTS: - const { fiatTransactionAmount, ethTransactionAmount } = action.payload + const { fiatTransactionAmount, ethTransactionAmount, hexTransactionAmount } = action.payload return { ...confirmState, fiatTransactionAmount: fiatTransactionAmount || confirmState.fiatTransactionAmount, ethTransactionAmount: ethTransactionAmount || confirmState.ethTransactionAmount, + hexTransactionAmount: hexTransactionAmount || confirmState.hexTransactionAmount, } case UPDATE_TRANSACTION_FEES: - const { fiatTransactionFee, ethTransactionFee } = action.payload + const { fiatTransactionFee, ethTransactionFee, hexTransactionFee } = action.payload return { ...confirmState, fiatTransactionFee: fiatTransactionFee || confirmState.fiatTransactionFee, ethTransactionFee: ethTransactionFee || confirmState.ethTransactionFee, + hexTransactionFee: hexTransactionFee || confirmState.hexTransactionFee, } case UPDATE_TRANSACTION_TOTALS: - const { fiatTransactionTotal, ethTransactionTotal } = action.payload + const { fiatTransactionTotal, ethTransactionTotal, hexTransactionTotal } = action.payload return { ...confirmState, fiatTransactionTotal: fiatTransactionTotal || confirmState.fiatTransactionTotal, ethTransactionTotal: ethTransactionTotal || confirmState.ethTransactionTotal, - } - case UPDATE_HEX_GAS_TOTAL: - return { - ...confirmState, - hexGasTotal: action.payload, + hexTransactionTotal: hexTransactionTotal || confirmState.hexTransactionTotal, } case UPDATE_TOKEN_PROPS: const { tokenSymbol = '', tokenDecimals = '' } = action.payload @@ -222,13 +227,6 @@ export function updateTransactionTotals (totals) { } } -export function updateHexGasTotal (hexGasTotal) { - return { - type: UPDATE_HEX_GAS_TOTAL, - payload: hexGasTotal, - } -} - export function updateTokenProps (tokenProps) { return { type: UPDATE_TOKEN_PROPS, @@ -297,7 +295,7 @@ export function updateTxDataAndCalculate (txData) { dispatch(updateTxData(txData)) - const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData + const { txParams: { value = '0x0', gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData const fiatTransactionAmount = getValueFromWeiHex({ value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2, @@ -306,31 +304,39 @@ export function updateTxDataAndCalculate (txData) { value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6, }) - dispatch(updateTransactionAmounts({ fiatTransactionAmount, ethTransactionAmount })) + dispatch(updateTransactionAmounts({ + fiatTransactionAmount, + ethTransactionAmount, + hexTransactionAmount: value, + })) - const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice }) - - dispatch(updateHexGasTotal(hexGasTotal)) + const hexTransactionFee = getHexGasTotal({ gasLimit, gasPrice }) const fiatTransactionFee = getTransactionFee({ - value: hexGasTotal, + value: hexTransactionFee, toCurrency: currentCurrency, numberOfDecimals: 2, conversionRate, }) const ethTransactionFee = getTransactionFee({ - value: hexGasTotal, + value: hexTransactionFee, toCurrency: 'ETH', numberOfDecimals: 6, conversionRate, }) - dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee })) + dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee, hexTransactionFee })) const fiatTransactionTotal = addFiat(fiatTransactionFee, fiatTransactionAmount) const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount) - - dispatch(updateTransactionTotals({ fiatTransactionTotal, ethTransactionTotal })) + console.log('HIHIH', value, hexTransactionFee) + const hexTransactionTotal = sumHexes(value, hexTransactionFee) + + dispatch(updateTransactionTotals({ + fiatTransactionTotal, + ethTransactionTotal, + hexTransactionTotal, + })) } } diff --git a/ui/app/ducks/tests/confirm-transaction.duck.test.js b/ui/app/ducks/tests/confirm-transaction.duck.test.js index 1bab0add0..eceacd0bd 100644 --- a/ui/app/ducks/tests/confirm-transaction.duck.test.js +++ b/ui/app/ducks/tests/confirm-transaction.duck.test.js @@ -19,7 +19,9 @@ const initialState = { ethTransactionAmount: '', ethTransactionFee: '', ethTransactionTotal: '', - hexGasTotal: '', + hexTransactionAmount: '', + hexTransactionFee: '', + hexTransactionTotal: '', nonce: '', toSmartContract: false, fetchingData: false, @@ -34,7 +36,6 @@ const CLEAR_METHOD_DATA = 'metamask/confirm-transaction/CLEAR_METHOD_DATA' const UPDATE_TRANSACTION_AMOUNTS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS' const UPDATE_TRANSACTION_FEES = 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES' const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS' -const UPDATE_HEX_GAS_TOTAL = 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL' const UPDATE_TOKEN_PROPS = 'metamask/confirm-transaction/UPDATE_TOKEN_PROPS' const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE' const UPDATE_TO_SMART_CONTRACT = 'metamask/confirm-transaction/UPDATE_TO_SMART_CONTRACT' @@ -65,7 +66,9 @@ describe('Confirm Transaction Duck', () => { ethTransactionAmount: '1', ethTransactionFee: '0.000021', ethTransactionTotal: '469.27', - hexGasTotal: '0x1319718a5000', + hexTransactionAmount: '', + hexTransactionFee: '0x1319718a5000', + hexTransactionTotal: '', nonce: '0x0', toSmartContract: false, fetchingData: false, @@ -186,12 +189,14 @@ describe('Confirm Transaction Duck', () => { payload: { fiatTransactionAmount: '123.45', ethTransactionAmount: '.5', + hexTransactionAmount: '0x1', }, }), { ...mockState.confirmTransaction, fiatTransactionAmount: '123.45', ethTransactionAmount: '.5', + hexTransactionAmount: '0x1', } ) }) @@ -203,12 +208,14 @@ describe('Confirm Transaction Duck', () => { payload: { fiatTransactionFee: '123.45', ethTransactionFee: '.5', + hexTransactionFee: '0x1', }, }), { ...mockState.confirmTransaction, fiatTransactionFee: '123.45', ethTransactionFee: '.5', + hexTransactionFee: '0x1', } ) }) @@ -220,25 +227,14 @@ describe('Confirm Transaction Duck', () => { payload: { fiatTransactionTotal: '123.45', ethTransactionTotal: '.5', + hexTransactionTotal: '0x1', }, }), { ...mockState.confirmTransaction, fiatTransactionTotal: '123.45', ethTransactionTotal: '.5', - } - ) - }) - - it('should update hexGasTotal when receiving an UPDATE_HEX_GAS_TOTAL action', () => { - assert.deepEqual( - ConfirmTransactionReducer(mockState, { - type: UPDATE_HEX_GAS_TOTAL, - payload: '0x0', - }), - { - ...mockState.confirmTransaction, - hexGasTotal: '0x0', + hexTransactionTotal: '0x1', } ) }) @@ -435,19 +431,6 @@ describe('Confirm Transaction Duck', () => { ) }) - it('should create an action to update hexGasTotal', () => { - const hexGasTotal = '0x0' - const expectedAction = { - type: UPDATE_HEX_GAS_TOTAL, - payload: hexGasTotal, - } - - assert.deepEqual( - actions.updateHexGasTotal(hexGasTotal), - expectedAction - ) - }) - it('should create an action to update tokenProps', () => { const tokenProps = { tokenDecimals: '1', @@ -568,7 +551,6 @@ describe('Confirm Transaction Duck', () => { const expectedActions = [ 'metamask/confirm-transaction/UPDATE_TX_DATA', 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS', - 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL', 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES', 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS', ] @@ -637,7 +619,6 @@ describe('Confirm Transaction Duck', () => { const expectedActions = [ 'metamask/confirm-transaction/UPDATE_TX_DATA', 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS', - 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL', 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES', 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS', ] @@ -687,7 +668,6 @@ describe('Confirm Transaction Duck', () => { const expectedActions = [ 'metamask/confirm-transaction/UPDATE_TX_DATA', 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS', - 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL', 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES', 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS', ] diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js index 20ef9e35b..777537e1e 100644 --- a/ui/app/helpers/conversions.util.js +++ b/ui/app/helpers/conversions.util.js @@ -61,3 +61,22 @@ export function getValueFromWeiHex ({ conversionRate, }) } + +export function getWeiHexFromDecimalValue ({ + value, + fromCurrency, + conversionRate, + fromDenomination, + invertConversionRate, +}) { + return conversionUtil(value, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + toCurrency: ETH, + fromCurrency, + conversionRate, + invertConversionRate, + fromDenomination, + toDenomination: WEI, + }) +} diff --git a/ui/app/helpers/tests/transactions.util.test.js b/ui/app/helpers/tests/transactions.util.test.js index 103a84a8c..838522e35 100644 --- a/ui/app/helpers/tests/transactions.util.test.js +++ b/ui/app/helpers/tests/transactions.util.test.js @@ -19,4 +19,39 @@ describe('Transactions utils', () => { assert.doesNotThrow(() => utils.getTokenData()) }) }) + + describe('getStatusKey', () => { + it('should return the correct status', () => { + const tests = [ + { + transaction: { + status: 'confirmed', + txReceipt: { + status: '0x0', + }, + }, + expected: 'failed', + }, + { + transaction: { + status: 'confirmed', + txReceipt: { + status: '0x1', + }, + }, + expected: 'confirmed', + }, + { + transaction: { + status: 'pending', + }, + expected: 'pending', + }, + ] + + tests.forEach(({ transaction, expected }) => { + assert.equal(utils.getStatusKey(transaction), expected) + }) + }) + }) }) diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js index f7d249e63..e50196196 100644 --- a/ui/app/helpers/transactions.util.js +++ b/ui/app/helpers/transactions.util.js @@ -126,3 +126,21 @@ export function sumHexes (...args) { return ethUtil.addHexPrefix(total) } + +/** + * Returns a status key for a transaction. Requires parsing the txMeta.txReceipt on top of + * txMeta.status because txMeta.status does not reflect on-chain errors. + * @param {Object} transaction - The txMeta object of a transaction. + * @param {Object} transaction.txReceipt - The transaction receipt. + * @returns {string} + */ +export function getStatusKey (transaction) { + const { txReceipt: { status } = {} } = transaction + + // There was an on-chain failure + if (status === '0x0') { + return 'failed' + } + + return transaction.status +} diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 3f1d3394f..37d8a9187 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -51,6 +51,9 @@ function reduceMetamask (state, action) { isRevealingSeedWords: false, welcomeScreenSeen: false, currentLocale: '', + preferences: { + useETHAsPrimaryCurrency: true, + }, }, state.metamask) switch (action.type) { @@ -365,6 +368,12 @@ function reduceMetamask (state, action) { }) } + case actions.UPDATE_PREFERENCES: { + return extend(metamaskState, { + preferences: { ...action.payload }, + }) + } + default: return metamaskState diff --git a/ui/app/selectors.js b/ui/app/selectors.js index fb4517628..9f11551be 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -33,6 +33,7 @@ const selectors = { getSendMaxModeState, getCurrentViewContext, getTotalUnapprovedCount, + preferencesSelector, } module.exports = selectors @@ -195,3 +196,7 @@ function getTotalUnapprovedCount ({ metamask }) { return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount + unapprovedTypedMessagesCount } + +function preferencesSelector ({ metamask }) { + return metamask.preferences +} diff --git a/ui/i18n-helper.js b/ui/i18n-helper.js index c6a7d0bf1..db07049e1 100644 --- a/ui/i18n-helper.js +++ b/ui/i18n-helper.js @@ -13,7 +13,7 @@ const getMessage = (locale, key, substitutions) => { return null } if (!locale[key]) { - log.error(`Translator - Unable to find value for key "${key}"`) + log.warn(`Translator - Unable to find value for key "${key}"`) return null } const entry = locale[key] |