diff --git a/.babelrc b/.babelrc index fcabd2d1a..9b1d5409b 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,4 @@ { - "presets": [["env"], "react", "stage-0"], + "presets": [["env", { "targets": { "browsers": [">0.25%", "not ie 11", "not op_mini all"] } } ], "react", "stage-0"], "plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index ddaa496dd..4b7ea17a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,13 @@ ## Current Develop Branch +- [#4606](https://github.com/MetaMask/metamask-extension/pull/4606): Add new metamask_watchAsset method. + ## 4.9.3 Wed Aug 15 2018 -- (#4897)[https://github.com/MetaMask/metamask-extension/pull/4897]: QR code scan for recipient addresses. -- (#4961)[https://github.com/MetaMask/metamask-extension/pull/4961]: Add a download seed phrase link. -- (#5060)[https://github.com/MetaMask/metamask-extension/pull/5060]: Fix bug where gas was not updating properly. +- [#4897](https://github.com/MetaMask/metamask-extension/pull/4897): QR code scan for recipient addresses. +- [#4961](https://github.com/MetaMask/metamask-extension/pull/4961): Add a download seed phrase link. +- [#5060](https://github.com/MetaMask/metamask-extension/pull/5060): Fix bug where gas was not updating properly. ## 4.9.2 Mon Aug 09 2018 diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 6a4ebc8a5..55344f3e1 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -796,7 +796,7 @@ "message": "Testovací faucet" }, "to": { - "message": "Komu: " + "message": "Komu" }, "toETHviaShapeShift": { "message": "$1 na ETH přes ShapeShift", diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index c06a99250..352d5ad7d 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -384,7 +384,7 @@ "infoHelp": { "message": "Info & Hilfe" }, - "insufficientFunds": { + "insufficientFunds": { "message": "Nicht genügend Guthaben." }, "insufficientTokens": { @@ -572,7 +572,7 @@ "description": "Wähle diesen Dateityp um damit einen Account zu importieren" }, "privateKeyWarning": { - "message": "Warnung: Niemals jemanden deinen Private Key mitteilen. Jeder der im Besitz deines Private Keys ist, kann jegliches Guthaben deines Accounts stehlen." + "message": "Warnung: Niemals jemanden deinen Private Key mitteilen. Jeder der im Besitz deines Private Keys ist, kann jegliches Guthaben deines Accounts stehlen." }, "privateNetwork": { "message": "Privates Netzwerk" @@ -775,7 +775,7 @@ "message": "Testfaucet" }, "to": { - "message": "An:" + "message": "An" }, "toETHviaShapeShift": { "message": "$1 an ETH via ShapeShift", diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index a25a2bd59..14e867b33 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -29,6 +29,9 @@ "addTokens": { "message": "Add Tokens" }, + "addSuggestedTokens": { + "message": "Add Suggested Tokens" + }, "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, @@ -451,6 +454,9 @@ "hideTokenPrompt": { "message": "Hide Token?" }, + "history": { + "message": "History" + }, "howToDeposit": { "message": "How would you like to deposit Ether?" }, @@ -651,7 +657,7 @@ "message": "No transaction history." }, "noTransactions": { - "message": "No Transactions" + "message": "You have no transactions" }, "notFound": { "message": "Not Found" @@ -702,6 +708,9 @@ "pasteSeed": { "message": "Paste your seed phrase here!" }, + "pending": { + "message": "pending" + }, "personalAddressDetected": { "message": "Personal address detected. Input the token contract address." }, @@ -730,6 +739,9 @@ "qrCode": { "message": "Show QR Code" }, + "queue": { + "message": "Queue" + }, "readdToken": { "message": "You can add this token back in the future by going go to “Add token” in your accounts options menu." }, @@ -870,6 +882,12 @@ "secretPhrase": { "message": "Enter your secret twelve word phrase here to restore your vault." }, + "showHexData": { + "message": "Show Hex Data" + }, + "showHexDataDescription": { + "message": "Select this to show the hex data field on the send screen" + }, "newPassword8Chars": { "message": "New Password (min 8 chars)" }, @@ -897,6 +915,12 @@ "sendTokens": { "message": "Send Tokens" }, + "sentEther": { + "message": "sent ether" + }, + "sentTokens": { + "message": "sent tokens" + }, "separateEachWord": { "message": "Separate each word with a single space" }, @@ -910,6 +934,9 @@ "orderOneHere": { "message": "Order a Trezor or Ledger and keep your funds in cold storage" }, + "outgoing": { + "message": "Outgoing" + }, "searchTokens": { "message": "Search Tokens" }, @@ -973,6 +1000,9 @@ "sign": { "message": "Sign" }, + "signatureRequest": { + "message": "Signature Request" + }, "signed": { "message": "Signed" }, @@ -1025,7 +1055,7 @@ "message": "Test Faucet" }, "to": { - "message": "To: " + "message": "To" }, "toETHviaShapeShift": { "message": "$1 to ETH via ShapeShift", diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index ed7f8f681..3e43a7b43 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -772,7 +772,7 @@ "message": "Probar Faucet" }, "to": { - "message": "Para:" + "message": "Para" }, "toETHviaShapeShift": { "message": "$1 a ETH via ShapeShift", diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 1463e2b5f..6f850d89b 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -490,7 +490,7 @@ "message": "Sélectionner un service" }, "send": { - "message": "Envoyé" + "message": "Envoyer" }, "sendTokens": { "message": "Envoyer des jetons" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index d3801c4f5..30d032357 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -3,86 +3,125 @@ "message": "수락" }, "account": { - "message": "계좌" + "message": "계정" }, "accountDetails": { - "message": "계좌 상세보기" + "message": "계정 상세보기" }, "accountName": { - "message": "계좌 이름" + "message": "계정 이름" }, "address": { "message": "주소" }, + "addCustomToken": { + "message": "사용자 정의 토큰 추가" + }, "addToken": { "message": "토큰 추가" }, + "addTokens": { + "message": "토큰 추가" + }, + "addAcquiredTokens": { + "message": "메타마스크를 통해 획득한 토큰 추가" + }, "amount": { - "message": "금액" + "message": "수량" }, "amountPlusGas": { - "message": "금액 + 가스" + "message": "수량 + 가스" }, "appDescription": { "message": "이더리움 브라우저 확장 프로그램", - "description": "어플리케이션 내용" + "description": "어플리케이션 설명" }, "appName": { "message": "메타마스크", "description": "어플리케이션 이름" }, + "approved": { + "message": "수락" + }, "attemptingConnect": { - "message": "블록체인에 접속 시도 중입니다." + "message": "블록체인에 접속을 시도하는 중입니다." + }, + "attributions": { + "message": "속성" }, "available": { - "message": "사용 가능한" + "message": "사용 가능" }, "back": { - "message": "뒤로" + "message": "돌아가기" }, "balance": { - "message": "잔액:" + "message": "잔액" + }, + "balances": { + "message": "토큰 잔액" }, "balanceIsInsufficientGas": { - "message": "가스가 충분하지 않습니다." + "message": "현재 가스 총합에 대해 잔액이 부족합니다" }, "beta": { - "message": "베타" + "message": "BETA" }, "betweenMinAndMax": { "message": "$1 이상 $2 이하여야 합니다.", - "description": "helper for inputting hex as decimal input" + "description": "10진수 입력으로 hex값 입력을 도와줍니다" + }, + "blockiesIdenticon": { + "message": "Blockies 아이덴티콘 사용" }, "borrowDharma": { - "message": "Dharma에서 빌리기(베타)" + "message": "Dharma에서 대여하기(Beta)" + }, + "builtInCalifornia": { + "message": "메타마스크는 캘리포니아에서 디자인되고 만들어졌습니다." }, "buy": { "message": "구매" }, "buyCoinbase": { - "message": "코인베이스에서 구매" + "message": "코인베이스에서 구매하기" }, "buyCoinbaseExplainer": { - "message": "코인베이스에서 비트코인, 이더리움, 라이트코인을 구매하실 수 있습니다." + "message": "코인베이스는 비트코인, 이더리움, 라이트코인을 거래할 수 있는 유명한 거래소입니다." + }, + "ok": { + "message": "확인" }, "cancel": { "message": "취소" }, + "classicInterface": { + "message": "예전 인터페이스" + }, "clickCopy": { "message": "클릭하여 복사" }, + "close": { + "message": "닫기" + }, "confirm": { "message": "승인" }, + "confirmed": { + "message": "승인됨" + }, "confirmContract": { "message": "컨트랙트 승인" }, "confirmPassword": { - "message": "패스워드 승인" + "message": "비밀번호 확인" }, "confirmTransaction": { "message": "트랜잭션 승인" }, + "continue": { + "message": "계속" + }, "continueToCoinbase": { "message": "코인베이스로 계속하기" }, @@ -90,72 +129,93 @@ "message": "컨트랙트 배포" }, "conversionProgress": { - "message": "변환중.." + "message": "변환 진행중" }, "copiedButton": { - "message": "복사되었습니다." + "message": "복사됨" }, "copiedClipboard": { - "message": "클립보드에 복사되었습니다." + "message": "클립보드에 복사되었습니다" }, "copiedExclamation": { - "message": "복사되었습니다." + "message": "복사됨!" + }, + "copiedSafe": { + "message": "안전한 곳에 복사하였습니다" }, "copy": { - "message": "복사하기" + "message": "복사" + }, + "copyContractAddress": { + "message": "컨트랙트 주소 복사" }, "copyToClipboard": { - "message": "클립보드에 복사" + "message": "클립보드로 복사" }, "copyButton": { "message": " 복사 " }, "copyPrivateKey": { - "message": "비밀 키 (클릭하여 복사)" + "message": "비밀 키입니다 (클릭하여 복사)" }, "create": { "message": "생성" }, "createAccount": { - "message": "계좌 생성" + "message": "계정 생성" }, "createDen": { "message": "생성" }, "crypto": { "message": "암호화폐", - "description": "Exchange type (cryptocurrencies)" + "description": "거래 유형 (암호화폐)" + }, + "currentConversion": { + "message": "선택된 단위" + }, + "currentNetwork": { + "message": "현재 네트워크" }, "customGas": { "message": "가스 설정" }, + "customToken": { + "message": "사용자 정의 토큰" + }, "customize": { - "message": "커스터마이즈" + "message": "맞춤화 하기" }, "customRPC": { - "message": "커스텀 RPC" + "message": "사용자 정의 RPC" + }, + "decimalsMustZerotoTen": { + "message": "소수점은 0 이상이고 36 이하여야 합니다." + }, + "decimal": { + "message": "소수점 정확도" }, "defaultNetwork": { "message": "이더리움 트랜잭션의 기본 네트워크는 메인넷입니다." }, "denExplainer": { - "message": "DEN은 비밀번호가 암호화 된 MetaMask의 스토리지입니다." + "message": "DEN은 비밀번호로 암호화 된 메타마스크의 저장소입니다." }, "deposit": { "message": "입금" }, "depositBTC": { - "message": "아래 주소로 BTC를 입급해주세요." + "message": "다음 주소로 BTC를 입급해주세요." }, "depositCoin": { - "message": "아래 주소로 $1를 입금해주세요.", - "description": "Tells the user what coin they have selected to deposit with shapeshift" + "message": "다음 주소로 $1 만큼 입금해주세요.", + "description": "사용자에게 shapeshift에서 어떤 코인을 선택해 입금했는지 알려줍니다" }, "depositEth": { - "message": "이더 입금" + "message": "이더 입금하기" }, "depositEther": { - "message": "이더 입금" + "message": "이더리움 입금하기" }, "depositFiat": { "message": "현금으로 입금하기" @@ -167,10 +227,10 @@ "message": "ShapeShift를 통해 입금하기" }, "depositShapeShiftExplainer": { - "message": "다른 암호화폐를 가지고 있으면, 계좌 생성 필요없이, 거래를 하거나 메타마스크 지갑을 통해 이더를 입금할 수 있습니다." + "message": "다른 암호화폐를 가지고 있으면, 계정을 생성할 필요없이 메타마스크 지갑에 이더리움을 바로 거래하거나 입금할 수 있습니다." }, "details": { - "message": "상세" + "message": "세부사항" }, "directDeposit": { "message": "즉시 입금" @@ -179,70 +239,103 @@ "message": "이더 즉시 입금" }, "directDepositEtherExplainer": { - "message": "이더를 이미 보유하고 있다면, 직접 입금을 통해 이더를 즉시 입금하실 수 있습니다." + "message": "약간의 이더를 이미 보유하고 있다면, 새로 만든 지갑에 직접 입금하여 이더를 보유할 수 있습니다." }, "done": { "message": "완료" }, + "downloadStateLogs": { + "message": "상태 로그 다운로드" + }, + "dropped": { + "message": "중단됨" + }, "edit": { "message": "수정" }, "editAccountName": { - "message": "계좌명 수정" + "message": "계정 이름 수정" + }, + "editingTransaction": { + "message": "트랜젝션을 변경합니다" + }, + "emailUs": { + "message": "저자에게 메일 보내기!" }, "encryptNewDen": { - "message": "새 DEN 암호화" + "message": "새로운 DEN을 암호화" }, "enterPassword": { - "message": "패스워드를 입력해주세요." + "message": "비밀번호를 입력해주세요" + }, + "enterPasswordConfirm": { + "message": "비밀번호를 다시 입력해 주세요" + }, + "enterPasswordContinue": { + "message": "계속하기 위해 비밀번호 입력" + }, + "passwordNotLongEnough": { + "message": "비밀번호가 충분히 길지 않습니다" + }, + "passwordsDontMatch": { + "message": "비밀번호가 맞지 않습니다" }, "etherscanView": { - "message": "이더스캔에서 계좌보기" + "message": "이더스캔에서 계정보기" }, "exchangeRate": { "message": "환율" }, "exportPrivateKey": { - "message": "비밀키 내보내기" + "message": "개인키 내보내기" }, "exportPrivateKeyWarning": { - "message": "Export private keys at your own risk." + "message": "개인키 내보내기는 위험을 감수해야 합니다." }, "failed": { "message": "실패" }, "fiat": { "message": "FIAT", - "description": "Exchange type" + "description": "거래 형식" }, "fileImportFail": { - "message": "파일을 가져올 수 없나요? 여기를 클릭해주세요!", - "description": "Helps user import their account from a JSON file" + "message": "파일을 가져올 수 없나요? 이곳을 클릭해주세요!", + "description": "JSON 파일로부터 계정 가져오기를 도와줍니다" + }, + "followTwitter": { + "message": "트위터에서 팔로우하세요" }, "from": { - "message": "보내는 사람" + "message": "보내는 이" + }, + "fromToSame": { + "message": "보내고 받는 주소는 동일할 수 없습니다" }, "fromShapeShift": { "message": "ShapeShift로 부터" }, "gas": { "message": "가스", - "description": "Short indication of gas cost" + "description": "가스 가격의 줄임" }, "gasFee": { "message": "가스 수수료" }, "gasLimit": { - "message": "가스 리밋" + "message": "가스 한도" }, "gasLimitCalculation": { - "message": "네트워크 성공률을 기반으로 적합한 가스 리밋을 계산합니다." + "message": "네트워크 성공률을 기반으로 적합한 가스 한도를 계산합니다." }, "gasLimitRequired": { - "message": "가스 리밋이 필요합니다." + "message": "가스 한도가 필요합니다." }, "gasLimitTooLow": { - "message": "가스 리밋은 21000 이상이여야 합니다." + "message": "가스 한도는 최소 21000 이상이여야 합니다." + }, + "generatingSeed": { + "message": "시드 생성중..." }, "gasPrice": { "message": "가스 가격 (GWEI)" @@ -253,21 +346,27 @@ "gasPriceRequired": { "message": "가스 가격이 필요합니다." }, + "generatingTransaction": { + "message": "트랜잭션 생성중" + }, "getEther": { "message": "이더 얻기" }, "getEtherFromFaucet": { - "message": "faucet에서 $1에 달하는 이더를 얻으세요.", - "description": "Displays network name for Ether faucet" + "message": "파우셋에서 $1에 달하는 이더를 얻으세요.", + "description": "이더 파우셋에 대한 네트워크 이름을 표시합니다" }, "greaterThanMin": { "message": "$1 이상이어야 합니다.", - "description": "helper for inputting hex as decimal input" + "description": "10진수 입력으로 hex값 입력을 도와줍니다" }, "here": { "message": "여기", "description": "as in -click here- for more information (goes with troubleTokenBalances)" }, + "hereList": { + "message": "리스트가 있습니다!!!!" + }, "hide": { "message": "숨기기" }, @@ -280,51 +379,96 @@ "howToDeposit": { "message": "어떤 방법으로 이더를 입금하시겠습니까?" }, + "holdEther": { + "message": "It allows you to hold ether & tokens, and serves as your bridge to decentralized applications." + }, "import": { - "message": "파일에서 가져오기", - "description": "Button to import an account from a selected file" + "message": "가져오기", + "description": "선택된 파일로부터 계정 가져오기 버튼" }, "importAccount": { - "message": "계좌 가져오기" + "message": "계정 가져오기" + }, + "importAccountMsg": { + "message": " 가져온 계정은 메타마스크에서 원래 생성된 계정의 시드구문과 연관성이 없습니다. 가져온 계정에 대해 더 배우기 " }, "importAnAccount": { - "message": "계좌 가져오기" + "message": "계정 가져오기" }, "importDen": { - "message": "기존 DEN 가져오기" + "message": "기존의 DEN 가져오기" }, "imported": { - "message": "가져오기 완료", - "description": "status showing that an account has been fully loaded into the keyring" + "message": "가져온 계정", + "description": "이 상태는 해당 계정이 keyring으로 완전히 적재된 상태임을 표시합니다" + }, + "importUsingSeed": { + "message": "계정 시드 구문으로 가져오기" }, "infoHelp": { "message": "정보 및 도움말" }, + "initialTransactionConfirmed": { + "message": "초기 트랜잭션이 네트워크를 통해 확정되었습니다. 확인을 누르고 이전으로 돌아갑니다." + }, + "insufficientFunds": { + "message": "충분하지 않은 자금." + }, + "insufficientTokens": { + "message": "충분하지 않은 토큰." + }, "invalidAddress": { - "message": "유효하지 않은 주소" + "message": "올바르지 않은 주소" + }, + "invalidAddressRecipient": { + "message": "수신 주소가 올바르지 않습니다" }, "invalidGasParams": { - "message": "유효하지 않은 가스 입력값" + "message": "올바르지 않은 가스 입력값" }, "invalidInput": { - "message": "유효하지 않은 입력값" + "message": "올바르지 않은 입력값" }, "invalidRequest": { "message": "유효하지 않은 요청" }, + "invalidRPC": { + "message": "올바르지 않은 RPC URI" + }, + "jsonFail": { + "message": "이상이 있습니다. JSON 파일이 올바른 파일인지 확인해주세요." + }, "jsonFile": { "message": "JSON 파일", - "description": "format for importing an account" + "description": "계정을 가져오기 위한 형식" + }, + "keepTrackTokens": { + "message": "메타마스크 계정을 통해 구입한 토큰 기록을 추적 및 보관합니다." }, "kovan": { "message": "Kovan 테스트넷" }, + "knowledgeDataBase": { + "message": "지식 베이스 방문" + }, + "max": { + "message": "최대" + }, + "learnMore": { + "message": "더 배우기." + }, "lessThanMax": { "message": "$1 이하여야합니다.", - "description": "helper for inputting hex as decimal input" + "description": "10진수 입력으로 hex값 입력을 도와줍니다" + }, + "likeToAddTokens": { + "message": "토큰을 추가하시겠습니까?" + }, + "links": { + "message": "링크" }, "limit": { - "message": "리밋" + "message": "한도" }, "loading": { "message": "로딩중..." @@ -335,11 +479,17 @@ "localhost": { "message": "로컬호스트 8545" }, + "login": { + "message": "로그인" + }, "logout": { "message": "로그아웃" }, "loose": { - "message": "외부 비밀키" + "message": "느슨함" + }, + "loweCaseWords": { + "message": "시드 단어는 소문자만 가능합니다" }, "mainnet": { "message": "이더리움 메인넷" @@ -347,94 +497,127 @@ "message": { "message": "메시지" }, + "metamaskDescription": { + "message": "메타마스크는 이더리움을 위한 안전한 신분 저장소입니다." + }, + "metamaskSeedWords": { + "message": "메타마스크 시드 단어" + }, "min": { "message": "최소" }, "myAccounts": { - "message": "내 계좌" + "message": "내 계정" + }, + "mustSelectOne": { + "message": "적어도 하나의 토큰을 선택하세요." }, "needEtherInWallet": { - "message": "dApp을 이용하기 위해서는 지갑에 이더가 있어야 합니다." + "message": "메타마스크를 통한 dApp을 이용하기 위해서는 지갑에 이더가 있어야 합니다." }, "needImportFile": { "message": "가져올 파일을 선택해주세요.", - "description": "User is important an account and needs to add a file to continue" + "description": "사용자는 계정을 가져오기 위해서 파일을 추가후 계속 진행해야 합니다" }, "needImportPassword": { - "message": "선택 된 파일에 패스워드를 입력해주세요.", - "description": "Password and file needed to import an account" + "message": "선택 된 파일에 대한 비밀번호를 입력해주세요.", + "description": "계정을 가져오기 위해서 비밀번호와 파일이 필요합니다." + }, + "negativeETH": { + "message": "음수값의 이더를 보낼 수 없습니다." }, "networks": { "message": "네트워크" }, + "nevermind": { + "message": "상관안함" + }, "newAccount": { - "message": "새 계좌" + "message": "새 계정" }, "newAccountNumberName": { - "message": "새 계좌 $1", - "description": "Default name of next account to be created on create account screen" + "message": "새 계정 $1", + "description": "계정 생성시 볼 수 있는 새 계정의 기본 이름" }, "newContract": { "message": "새 컨트랙트" }, "newPassword": { - "message": "새 패스워드 (최소 8자 이상)" + "message": "새 비밀번호 (최소 8자 이상)" }, "newRecipient": { "message": "받는 사람" }, + "newRPC": { + "message": "새로운 RPC URL" + }, "next": { "message": "다음" }, "noAddressForName": { - "message": "이 이름에는 주소가 설정되어 있지 않습니다." + "message": "이 이름에 대해 주소가 설정되어 있지 않습니다." }, "noDeposits": { - "message": "입금이 없습니다." + "message": "입금 내역이 없습니다." }, "noTransactionHistory": { "message": "트랜잭션 기록이 없습니다." }, "noTransactions": { - "message": "트랜잭션이 없습니다." + "message": "트랜잭션이 없습니다" }, "notStarted": { - "message": "시작되지 않음." + "message": "시작 안됨" }, "oldUI": { - "message": "구버전의 UI" + "message": "구버전 UI" }, "oldUIMessage": { - "message": "구버전 UI로 변경하셨습니다. 우 상단 드랍다운 메뉴에서 새 UI로 변경하실 수 있습니다." + "message": "구버전 UI로 변경하셨습니다. 우 상단 드롭다운 메뉴에서 새 UI로 변경하실 수 있습니다." }, "or": { "message": "또는", - "description": "choice between creating or importing a new account" + "description": "새 계정을 만들거나 가져오기중에 선택하기" + }, + "password": { + "message": "비밀번호" + }, + "passwordCorrect": { + "message": "비밀번호가 맞는지 확인해주세요." }, "passwordMismatch": { - "message": "패스워드가 일치하지 않습니다.", - "description": "in password creation process, the two new password fields did not match" + "message": "비밀번호가 일치하지 않습니다.", + "description": "비밀번호를 생성하는 과정에서 두개의 새 비밀번호 입력란이 일치하지 않습니다" }, "passwordShort": { - "message": "패스워드가 너무 짧습니다.", - "description": "in password creation process, the password is not long enough to be secure" + "message": "비밀번호가 짧습니다.", + "description": "비밀번호를 생성하는 과정에서 비밀번호가 짧으면 안전하지 않습니다" }, "pastePrivateKey": { - "message": "비밀키를 입력해주세요.", - "description": "For importing an account from a private key" + "message": "개인키를 입력해주세요:", + "description": "개인키로부터 계정을 가져오는 것입니다" }, "pasteSeed": { - "message": "시드 문장들을 붙여넣어주세요." + "message": "시드 구문을 이곳에 붙여넣어주세요!" + }, + "personalAddressDetected": { + "message": "개인 주소가 탐지됨. 토큰 컨트랙트 주소를 입력하세요." }, "pleaseReviewTransaction": { "message": "트랜잭션을 검토해주세요." }, + "popularTokens": { + "message": "인기있는 토큰" + }, + "privacyMsg": { + "message": "개인정보 보호 정책" + }, "privateKey": { - "message": "비밀키", - "description": "select this type of file to use to import an account" + "message": "개인키", + "description": "이 형식의 파일을 선택하여 계정을 가져올 수 있습니다" }, "privateKeyWarning": { - "message": " 절대 이 키를 노출하지 마십시오. 비밀키가 노출되면 누구나 당신의 계좌에서 자산을 빼갈 수 있습니다." + "message": " 절대 이 키를 노출하지 마십시오. 개인키가 노출되면 누구나 당신의 계정에서 자산을 빼갈 수 있습니다." }, "privateNetwork": { "message": "프라이빗 네트워크" @@ -446,28 +629,64 @@ "message": "옵션 메뉴에서 “토큰 추가”를 눌러서 추후에 다시 이 토큰을 추가하실 수 있습니다." }, "readMore": { - "message": "더 읽기." + "message": "여기서 더 보기." + }, + "readMore2": { + "message": "더 보기." }, "receive": { "message": "받기" }, "recipientAddress": { - "message": "받는 사람 주소" + "message": "받는 주소" }, "refundAddress": { "message": "환불받을 주소" }, "rejected": { - "message": "거부되었음." + "message": "거부됨" + }, + "reset": { + "message": "초기화" + }, + "resetAccount": { + "message": "계정 초기화" + }, + "resetAccountDescription": { + "message": "계정을 초기화 하는 경우에 트랜잭션 기록이 삭제됩니다." + }, + "restoreFromSeed": { + "message": "계정을 복구하시겠습니까?" + }, + "restoreVault": { + "message": "저장소 복구" }, "required": { - "message": "필요함." + "message": "필요함" }, "retryWithMoreGas": { - "message": "더 높은 가스 가격으로 다시 시도해주세요." + "message": "더 높은 가스 가격으로 다시 시도해주세요" + }, + "walletSeed": { + "message": "지갑 시드값" + }, + "revealSeedWords": { + "message": "시드 단어 보이기" + }, + "revealSeedWordsTitle": { + "message": "시드 단어" + }, + "revealSeedWordsDescription": { + "message": "브라우저를 바꾸거나 컴퓨터를 옮기는 경우 이 시드 구문이 필요하며 이를 통해 계정에 접근할 수 있습니다. 시드 구문을 안전한 곳에 보관하세요." + }, + "revealSeedWordsWarningTitle": { + "message": "이 구문을 다른사람과 절대로 공유하지 마세요!" + }, + "revealSeedWordsWarning": { + "message": "이 단어모음은 당신의 모든 계정을 훔치는데 사용할 수 있습니다." }, "revert": { - "message": "취소" + "message": "되돌림" }, "rinkeby": { "message": "Rinkeby 테스트넷" @@ -475,46 +694,122 @@ "ropsten": { "message": "Ropsten 테스트넷" }, + "rpc": { + "message": "사용자 정의 RPC" + }, + "currentRpc": { + "message": "현재 RPC" + }, + "connectingToMainnet": { + "message": "이더리움 메인넷 접속중" + }, + "connectingToRopsten": { + "message": "Ropsten 테스트넷 접속중" + }, + "connectingToKovan": { + "message": "Kovan 테스트넷 접속중" + }, + "connectingToRinkeby": { + "message": "Rinkeby 테스트넷 접속중" + }, + "connectingToUnknown": { + "message": "알려지지 않은 네트워크 접속중" + }, "sampleAccountName": { - "message": "예) 나의 새 계좌", - "description": "Help user understand concept of adding a human-readable name to their account" + "message": "예) 나의 새 계정", + "description": "각 계정에 대해서 구별하기 쉬운 이름을 지정하여 사용자가 쉽게 이해할 수 있게 합니다" }, "save": { "message": "저장" }, + "speedUpTitle": { + "message": "트랜잭션 속도 향상하기" + }, + "speedUpSubtitle": { + "message": "트랜잭션 가스 가격을 늘려서 해당 트랙잭션에 덮어쓰기 하여 속도를 빠르게 합니다" + }, + "saveAsCsvFile": { + "message": "CSV 파일로 저장" + }, "saveAsFile": { "message": "파일로 저장", - "description": "Account export process" + "description": "계정 내보내기 절차" + }, + "saveSeedAsFile": { + "message": "시드 단어를 파일로 저장하기" + }, + "search": { + "message": "검색" + }, + "searchResults": { + "message": "검색 결과" + }, + "secretPhrase": { + "message": "12개 단어로 구성된 비밀 구문을 입력하여 저장소를 복구하세요." + }, + "newPassword8Chars": { + "message": "새 비밀번호 (최소 8문자)" + }, + "seedPhraseReq": { + "message": "시드 구문은 12개의 단어입니다" + }, + "select": { + "message": "선택" + }, + "selectCurrency": { + "message": "통화 선택" }, "selectService": { "message": "서비스 선택" }, + "selectType": { + "message": "형식 선택" + }, "send": { "message": "전송" }, + "sendETH": { + "message": "ETH 보내기" + }, "sendTokens": { "message": "토큰 전송" }, + "onlySendToEtherAddress": { + "message": "이더리움 주소로 ETH만 송금하세요." + }, + "onlySendTokensToAccountAddress": { + "message": "이더리움계정 주소로 $1 만 보내기.", + "description": "토큰 심볼 보이기" + }, + "searchTokens": { + "message": "토큰 검색" + }, "sendTokensAnywhere": { - "message": "이더 계좌로 토큰 전송" + "message": "이더 계정로 토큰 전송" }, "settings": { "message": "설정" }, + "info": { + "message": "정보" + }, "shapeshiftBuy": { "message": "Shapeshift를 통해서 구매하기" }, "showPrivateKeys": { - "message": "비밀키 보기" + "message": "개인키 보기" }, "showQRCode": { - "message": "QR코드 보기" + "message": "QR 코드 보기" }, "sign": { "message": "서명" }, + "signed": { + "message": "서명됨" + }, "signMessage": { - "message": "서명 메시지" + "message": "메시지 서명" }, "signNotice": { "message": "이 메시지에 대한 서명은 위험할 수 있습니다.\n 완전히 신뢰할 수 있는 사이트에서만 서명해주세요.\n 안전을 위해 추후의 버전에서는 삭제될 기능입니다. " @@ -523,33 +818,81 @@ "message": "서명 요청" }, "sigRequested": { - "message": "서명이 요청되었습니다." + "message": "서명이 요청됨" + }, + "spaceBetween": { + "message": "단어 사이에는 공백만 올 수 있습니다" }, "status": { "message": "상태" }, + "stateLogs": { + "message": "상태 로그" + }, + "stateLogsDescription": { + "message": "상태 로그에는 공개 계정 주소와 송신 트랜잭션 정보가 들어있습니다." + }, + "stateLogError": { + "message": "상태 로그 받기 실패." + }, "submit": { "message": "제출" }, + "submitted": { + "message": "제출됨" + }, + "supportCenter": { + "message": "지원 센터에 방문하기" + }, + "symbolBetweenZeroTen": { + "message": "심볼은 0에서 10개 사이의 문자여야 합니다." + }, "takesTooLong": { "message": "너무 오래걸리나요?" }, + "terms": { + "message": "사용 지침" + }, "testFaucet": { - "message": "Faucet 테스트" + "message": "파우셋 테스트" }, "to": { - "message": "대상" + "message": "받는이: " }, "toETHviaShapeShift": { "message": "ShapeShift를 통해 $1를 ETH로 바꾸기", - "description": "system will fill in deposit type in start of message" + "description": "시스템이 시작할 때에 입금 유형을 입력해줍니다" + }, + "token": { + "message": "토큰" + }, + "tokenAddress": { + "message": "토큰 주소" + }, + "tokenAlreadyAdded": { + "message": "토큰이 이미 추가되어있습니다." }, "tokenBalance": { - "message": "현재 토큰 잔액: " + "message": "현재 토큰 잔액:" + }, + "tokenSelection": { + "message": "토큰을 검색하거나 유명한 토큰 리스트에서 선택하시기 바랍니다." + }, + "tokenSymbol": { + "message": "토큰 기호" + }, + "tokenWarning1": { + "message": "메타마스크 계좌를 통해 구입한 토큰을 추적합니다. 다른 계정으로 토큰을 구입한 경우 이곳에 나타나지 않습니다." }, "total": { "message": "합계" }, + "transactions": { + "message": "트랜잭션" + }, + "transactionError": { + "message": "트랜잭션 오류. 컨트랙트 코드에서 예외 발생(Exception thrown)." + }, "transactionMemo": { "message": "트랜잭션 메모 (선택사항)" }, @@ -560,23 +903,29 @@ "message": "전송" }, "troubleTokenBalances": { - "message": "토큰 잔액을 가져오는데에 문제가 생겼습니다. (여기)서 상세내용을 볼 수 있습니다.", - "description": "Followed by a link (here) to view token balances" + "message": "토큰 잔액을 가져오는데에 문제가 생겼습니다. 링크에서 상세내용을 볼 수 있습니다.", + "description": "토큰 잔액을 보려면 (here) 링크를 따라가세요" + }, + "twelveWords": { + "message": "12개의 단어는 메타마스크 계정을 복구하기 위한 유일한 방법입니다.\n안전한 장소에 보관하시기 바랍니다." }, "typePassword": { - "message": "패스워드를 입력하세요." + "message": "비밀번호를 입력하세요" }, "uiWelcome": { - "message": "새 UI에 오신 것을 환영합니다. (베타)" + "message": "새로운 UI에 오신 것을 환영합니다. (Beta)" }, "uiWelcomeMessage": { - "message": "새 메타마스크 UI를 사용하고 계십니다. 토큰 전송과 같은 새 기능들을 사용해보시면서 문제가 있다면 알려주세요." + "message": "새로운 메타마스크 UI를 사용하고 계십니다. 토큰 전송과 같은 새 기능들을 사용해보시면서 문제가 있다면 알려주세요." + }, + "unapproved": { + "message": "허가안됨" }, "unavailable": { - "message": "유효하지 않은" + "message": "유효하지 않음" }, "unknown": { - "message": "알려지지 않은" + "message": "알려지지 않음" }, "unknownNetwork": { "message": "알려지지 않은 프라이빗 네트워크" @@ -584,19 +933,46 @@ "unknownNetworkId": { "message": "알려지지 않은 네트워크 ID" }, + "unlockMessage": { + "message": "우리가 기다리던 분권형 웹입니다" + }, + "uriErrorMsg": { + "message": "URI는 HTTP/HTTPS로 시작해야 합니다." + }, "usaOnly": { "message": "USA 거주자 한정", - "description": "Using this exchange is limited to people inside the USA" + "description": "해당 거래소는 USA거주자에 한해서만 사용가능합니다" }, "usedByClients": { - "message": "다양한 클라이언트에서 사용되고 있습니다." + "message": "다양한 클라이언트에서 사용되고 있습니다" + }, + "useOldUI": { + "message": "예전 UI 사용" + }, + "validFileImport": { + "message": "가져오기 위해 유효한 파일을 선택해야 합니다." + }, + "vaultCreated": { + "message": "저장소가 생성됨" }, "viewAccount": { - "message": "계좌 보기" + "message": "계정 보기" + }, + "viewOnEtherscan": { + "message": "이터스캔에서 보기" + }, + "visitWebSite": { + "message": "웹사이트 방문" }, "warning": { "message": "경고" }, + "welcomeBack": { + "message": "환영합니다!" + }, + "welcomeBeta": { + "message": "메타마스크 Beta에 오신 것을 환영합니다" + }, "whatsThis": { "message": "이것은 무엇인가요?" }, @@ -604,6 +980,204 @@ "message": "서명이 요청되고 있습니다." }, "youSign": { - "message": "서명 중입니다." + "message": "서명 중입니다" + }, + "yourPrivateSeedPhrase": { + "message": "개인 시드 구문" + }, + "accessingYourCamera": { + "message": "카메라 접근중..." + }, + "accountSelectionRequired": { + "message": "계정을 선택하셔야 합니다!" + }, + "approve": { + "message": "수락" + }, + "browserNotSupported": { + "message": "브라우저가 지원하지 않습니다..." + }, + "bytes": { + "message": "바이트" + }, + "chromeRequiredForHardwareWallets": { + "message": "하드웨어 지갑을 연결하기 위해서는 구글 크롬에서 메타마스크를 사용하셔야 합니다." + }, + "connectHardwareWallet": { + "message": "하드웨어 지갑 연결" + }, + "connect": { + "message": "연결" + }, + "connecting": { + "message": "연결중..." + }, + "connectToLedger": { + "message": "Ledger 연결" + }, + "connectToTrezor": { + "message": "Trezor 연결" + }, + "copyAddress": { + "message": "클립보드로 주소 복사" + }, + "downloadGoogleChrome": { + "message": "구글 크롬 다운로드" + }, + "dontHaveAHardwareWallet": { + "message": "하드웨어 지갑이 없나요?" + }, + "ensNameNotFound": { + "message": "ENS 이름을 찾을 수 없습니다" + }, + "parameters": { + "message": "매개변수" + }, + "forgetDevice": { + "message": "장치 연결 해제" + }, + "functionType": { + "message": "함수 유형" + }, + "getHelp": { + "message": "도움말" + }, + "hardware": { + "message": "하드웨어" + }, + "hardwareWalletConnected": { + "message": "하드웨어 지갑이 연결됨" + }, + "hardwareWallets": { + "message": "하드웨어 지갑 연결" + }, + "hardwareWalletsMsg": { + "message": "메타마스크에서 사용할 하드웨어 지갑을 선택해주세요" + }, + "havingTroubleConnecting": { + "message": "연결에 문제가 있나요?" + }, + "hexData": { + "message": "Hex 데이터" + }, + "invalidSeedPhrase": { + "message": "잘못된 시드 구문" + }, + "ledgerAccountRestriction": { + "message": "새 계정을 추가하려면 최소 마지막 계정을 사용해야 합니다." + }, + "menu": { + "message": "메뉴" + }, + "noConversionRateAvailable": { + "message": "변환 비율을 찾을 수 없습니다" + }, + "notFound": { + "message": "찾을 수 없음" + }, + "noWebcamFoundTitle": { + "message": "웹캠이 없습니다" + }, + "noWebcamFound": { + "message": "컴퓨터의 웹캠을 찾을 수 없습니다. 다시 시도해보세요." + }, + "openInTab": { + "message": "탭으로 열기" + }, + "origin": { + "message": "Origin" + }, + "prev": { + "message": "이전" + }, + "restoreAccountWithSeed": { + "message": "시드 구문으로 계정 복구하기" + }, + "restore": { + "message": "복구" + }, + "remove": { + "message": "제거" + }, + "removeAccount": { + "message": "계정 제거" + }, + "removeAccountDescription": { + "message": "이 계정한 지갑에서 삭제될 것입니다. 지우기 전에 이 계정에 대한 개인 키 혹은 시드 구문을 가지고 있는지 확인하세요. 계정 드랍다운 메뉴를 통해서 계정을 가져오거나 생성할 수 있습니다." + }, + "readyToConnect": { + "message": "접속 준비되었나요?" + }, + "separateEachWord": { + "message": "각 단어는 공백 한칸으로 분리합니다" + }, + "orderOneHere": { + "message": "Trezor 혹은 Ledger를 구입하고 자금을 콜드 스토리지에 저장합니다" + }, + "selectAnAddress": { + "message": "주소 선택" + }, + "selectAnAccount": { + "message": "계정 선택" + }, + "selectAnAccountHelp": { + "message": "메타마스크에서 보기위한 계정 선택" + }, + "selectHdPath": { + "message": "HD 경로 지정" + }, + "selectPathHelp": { + "message": "하단에서 Ledger지갑 계정을 찾지 못하겠으면 \"Legacy (MEW / MyCrypto)\" 경로로 바꿔보세요" + }, + "step1HardwareWallet": { + "message": "1. 하드웨어 지갑 연결" + }, + "step1HardwareWalletMsg": { + "message": "하드웨어 지갑을 컴퓨터에 연결해주세요." + }, + "step2HardwareWallet": { + "message": "2. 계정 선택" + }, + "step2HardwareWalletMsg": { + "message": "보고 싶은 계정을 선택합니다. 한번에 하나의 계정만 선택할 수 있습니다." + }, + "step3HardwareWallet": { + "message": "3. dApps을 사용하거나 다른 것을 합니다!" + }, + "step3HardwareWalletMsg": { + "message": "다른 이더리움 계정을 사용하듯 하드웨어 계정을 사용합니다. dApps을 로그인하거나, 이더를 보내거나, ERC20토큰 혹은 대체가능하지 않은 토큰 (예를 들어 CryptoKitties)을 사거나 저장하거나 합니다." + }, + "scanInstructions": { + "message": "QR 코드를 카메라 앞에 가져다 놓아주세요" + }, + "scanQrCode": { + "message": "QR 코드 스캔" + }, + "transfer": { + "message": "전송" + }, + "trezorHardwareWallet": { + "message": "TREZOR 하드웨어 지갑" + }, + "tryAgain": { + "message": "다시 시도하세요" + }, + "unknownFunction": { + "message": "알 수 없는 함수" + }, + "unknownQrCode": { + "message": "오류: QR 코드를 확인할 수 없습니다" + }, + "unknownCameraErrorTitle": { + "message": "이런! 뭔가 잘못되었습니다...." + }, + "unknownCameraError": { + "message": "카메라를 접근하는 중 오류가 발생했습니다. 다시 시도해 주세요..." + }, + "unlock": { + "message": "잠금 해제" + }, + "youNeedToAllowCameraAccess": { + "message": "이 기능을 사용하려면 카메라 접근을 허용해야 합니다." } } diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index bb722735d..6344e1beb 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -784,7 +784,7 @@ "message": "Тестовый кран" }, "to": { - "message": "Получатель: " + "message": "Получатель" }, "toETHviaShapeShift": { "message": "$1 в ETH через ShapeShift", diff --git a/app/_locales/tml/messages.json b/app/_locales/tml/messages.json index fcc418bac..4f733458e 100644 --- a/app/_locales/tml/messages.json +++ b/app/_locales/tml/messages.json @@ -796,7 +796,7 @@ "message": "சோதனை குழாய்" }, "to": { - "message": "பெறுநர்: " + "message": "பெறுநர்" }, "toETHviaShapeShift": { "message": "$ 1 முதல் ETH வரை வடிவம்", diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 08ba6cde8..8be695108 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -796,7 +796,7 @@ "message": "Test Musluğu" }, "to": { - "message": "Kime: " + "message": "Kime" }, "toETHviaShapeShift": { "message": "ShapeShift üstünden $1'dan ETH'e", diff --git a/app/manifest.json b/app/manifest.json index 1e12f35b4..7793c6e38 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -71,7 +71,6 @@ "activeTab", "webRequest", "*://*.eth/", - "*://*.test/", "notifications" ], "web_accessible_resources": [ diff --git a/app/scripts/background.js b/app/scripts/background.js index 1599b8f99..e13d2842a 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -256,6 +256,7 @@ function setupController (initState, initLangCode) { showUnconfirmedMessage: triggerUi, unlockAccountMessage: triggerUi, showUnapprovedTx: triggerUi, + showWatchAssetUi: showWatchAssetUi, // initial state initState, // initial locale code @@ -451,9 +452,28 @@ function triggerUi () { }) } +/** + * Opens the browser popup for user confirmation of watchAsset + * then it waits until user interact with the UI + */ +function showWatchAssetUi () { + triggerUi() + return new Promise( + (resolve) => { + var interval = setInterval(() => { + if (!notificationIsOpen) { + clearInterval(interval) + resolve() + } + }, 1000) + } + ) +} + // On first install, open a window to MetaMask website to how-it-works. extension.runtime.onInstalled.addListener(function (details) { if ((details.reason === 'install') && (!METAMASK_DEBUG)) { extension.tabs.create({url: 'https://metamask.io/#how-it-works'}) } }) + diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 707fd7de9..464a37017 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,5 +1,6 @@ const ObservableStore = require('obs-store') const normalizeAddress = require('eth-sig-util').normalize +const { isValidAddress } = require('ethereumjs-util') const extend = require('xtend') @@ -14,6 +15,7 @@ class PreferencesController { * @property {string} store.currentAccountTab Indicates the selected tab in the ui * @property {array} store.tokens The tokens the user wants display in their token lists * @property {object} store.accountTokens The tokens stored per account and then per network type + * @property {object} store.assetImages Contains assets objects related to assets added * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the * user wishes to see that feature @@ -26,7 +28,9 @@ class PreferencesController { frequentRpcList: [], currentAccountTab: 'history', accountTokens: {}, + assetImages: {}, tokens: [], + suggestedTokens: {}, useBlockie: false, featureFlags: {}, currentLocale: opts.initLangCode, @@ -37,6 +41,7 @@ class PreferencesController { this.diagnostics = opts.diagnostics this.network = opts.network this.store = new ObservableStore(initState) + this.showWatchAssetUi = opts.showWatchAssetUi this._subscribeProviderType() } // PUBLIC METHODS @@ -51,6 +56,53 @@ class PreferencesController { this.store.updateState({ useBlockie: val }) } + getSuggestedTokens () { + return this.store.getState().suggestedTokens + } + + getAssetImages () { + return this.store.getState().assetImages + } + + addSuggestedERC20Asset (tokenOpts) { + this._validateERC20AssetParams(tokenOpts) + const suggested = this.getSuggestedTokens() + const { rawAddress, symbol, decimals, image } = tokenOpts + const address = normalizeAddress(rawAddress) + const newEntry = { address, symbol, decimals, image } + suggested[address] = newEntry + this.store.updateState({ suggestedTokens: suggested }) + } + + /** + * RPC engine middleware for requesting new asset added + * + * @param req + * @param res + * @param {Function} - next + * @param {Function} - end + */ + async requestWatchAsset (req, res, next, end) { + if (req.method === 'metamask_watchAsset') { + const { type, options } = req.params + switch (type) { + case 'ERC20': + const result = await this._handleWatchAssetERC20(options) + if (result instanceof Error) { + end(result) + } else { + res.result = result + end() + } + break + default: + end(new Error(`Asset of type ${type} not supported`)) + } + } else { + next() + } + } + /** * Getter for the `useBlockie` property * @@ -186,6 +238,13 @@ class PreferencesController { return selected } + removeSuggestedTokens () { + return new Promise((resolve, reject) => { + this.store.updateState({ suggestedTokens: {} }) + resolve({}) + }) + } + /** * Setter for the `selectedAddress` property * @@ -232,11 +291,11 @@ class PreferencesController { * @returns {Promise} Promises the new array of AddedToken objects. * */ - async addToken (rawAddress, symbol, decimals) { + async addToken (rawAddress, symbol, decimals, image) { const address = normalizeAddress(rawAddress) const newEntry = { address, symbol, decimals } - const tokens = this.store.getState().tokens + const assetImages = this.getAssetImages() const previousEntry = tokens.find((token, index) => { return token.address === address }) @@ -247,7 +306,8 @@ class PreferencesController { } else { tokens.push(newEntry) } - this._updateAccountTokens(tokens) + assetImages[address] = image + this._updateAccountTokens(tokens, assetImages) return Promise.resolve(tokens) } @@ -260,8 +320,10 @@ class PreferencesController { */ removeToken (rawAddress) { const tokens = this.store.getState().tokens + const assetImages = this.getAssetImages() const updatedTokens = tokens.filter(token => token.address !== rawAddress) - this._updateAccountTokens(updatedTokens) + delete assetImages[rawAddress] + this._updateAccountTokens(updatedTokens, assetImages) return Promise.resolve(updatedTokens) } @@ -322,7 +384,7 @@ class PreferencesController { /** * Returns an updated rpcList based on the passed url and the current list. - * The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the + * The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the * end of the list. The current list is modified and returned as a promise. * * @param {string} _url The rpc url to add to the frequentRpcList. @@ -338,7 +400,7 @@ class PreferencesController { if (_url !== 'http://localhost:8545') { rpcList.push(_url) } - if (rpcList.length > 2) { + if (rpcList.length > 3) { rpcList.shift() } return Promise.resolve(rpcList) @@ -387,6 +449,7 @@ class PreferencesController { // // PRIVATE METHODS // + /** * Subscription to network provider type. * @@ -405,10 +468,10 @@ class PreferencesController { * @param {array} tokens Array of tokens to be updated. * */ - _updateAccountTokens (tokens) { + _updateAccountTokens (tokens, assetImages) { const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates() accountTokens[selectedAddress][providerType] = tokens - this.store.updateState({ accountTokens, tokens }) + this.store.updateState({ accountTokens, tokens, assetImages }) } /** @@ -438,6 +501,47 @@ class PreferencesController { const tokens = accountTokens[selectedAddress][providerType] return { tokens, accountTokens, providerType, selectedAddress } } + + /** + * Handle the suggestion of an ERC20 asset through `watchAsset` + * * + * @param {Promise} promise Promise according to addition of ERC20 token + * + */ + async _handleWatchAssetERC20 (options) { + const { address, symbol, decimals, image } = options + const rawAddress = address + try { + this._validateERC20AssetParams({ rawAddress, symbol, decimals }) + } catch (err) { + return err + } + const tokenOpts = { rawAddress, decimals, symbol, image } + this.addSuggestedERC20Asset(tokenOpts) + return this.showWatchAssetUi().then(() => { + const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress)) + return tokenAddresses.length > 0 + }) + } + + /** + * Validates that the passed options for suggested token have all required properties. + * + * @param {Object} opts The options object to validate + * @throws {string} Throw a custom error indicating that address, symbol and/or decimals + * doesn't fulfill requirements + * + */ + _validateERC20AssetParams (opts) { + const { rawAddress, symbol, decimals } = opts + if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`) + if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`) + const numDecimals = parseInt(decimals, 10) + if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { + throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`) + } + if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`) + } } module.exports = PreferencesController diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index b7e2c7cbe..3a52d5e8d 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -43,10 +43,24 @@ class AccountTracker { this._provider = opts.provider this._query = pify(new EthQuery(this._provider)) this._blockTracker = opts.blockTracker - // subscribe to latest block - this._blockTracker.on('latest', this._updateForBlock.bind(this)) // blockTracker.currentBlock may be null this._currentBlockNumber = this._blockTracker.getCurrentBlock() + // bind function for easier listener syntax + this._updateForBlock = this._updateForBlock.bind(this) + } + + start () { + // remove first to avoid double add + this._blockTracker.removeListener('latest', this._updateForBlock) + // add listener + this._blockTracker.addListener('latest', this._updateForBlock) + // fetch account balances + this._updateAccounts() + } + + stop () { + // remove listener + this._blockTracker.removeListener('latest', this._updateForBlock) } /** diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js index 5db63f47d..38682b916 100644 --- a/app/scripts/lib/ipfsContent.js +++ b/app/scripts/lib/ipfsContent.js @@ -34,7 +34,7 @@ module.exports = function (provider) { return { cancel: true } } - extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']}) + extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/']}) return { remove () { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 29838ad2d..98cb62bfa 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -67,6 +67,10 @@ module.exports = class MetamaskController extends EventEmitter { const initState = opts.initState || {} this.recordFirstTimeInfo(initState) + // this keeps track of how many "controllerStream" connections are open + // the only thing that uses controller connections are open metamask UI instances + this.activeControllerConnections = 0 + // platform-specific api this.platform = opts.platform @@ -88,6 +92,7 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, + showWatchAssetUi: opts.showWatchAssetUi, network: this.networkController, }) @@ -127,6 +132,14 @@ 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) { + this.accountTracker.start() + } else { + this.accountTracker.stop() + } + }) // key mgmt const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] @@ -137,19 +150,7 @@ module.exports = class MetamaskController extends EventEmitter { encryptor: opts.encryptor || undefined, }) - // If only one account exists, make sure it is selected. - this.keyringController.memStore.subscribe((state) => { - const addresses = state.keyrings.reduce((res, keyring) => { - return res.concat(keyring.accounts) - }, []) - if (addresses.length === 1) { - const address = addresses[0] - this.preferencesController.setSelectedAddress(address) - } - // ensure preferences + identities controller know about all addresses - this.preferencesController.addAddresses(addresses) - this.accountTracker.syncWithAddresses(addresses) - }) + this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s)) // detect tokens controller this.detectTokensController = new DetectTokensController({ @@ -386,6 +387,7 @@ module.exports = class MetamaskController extends EventEmitter { setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), addToken: nodeify(preferencesController.addToken, preferencesController), removeToken: nodeify(preferencesController.removeToken, preferencesController), + removeSuggestedTokens: nodeify(preferencesController.removeSuggestedTokens, preferencesController), setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController), setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController), @@ -1209,11 +1211,19 @@ module.exports = class MetamaskController extends EventEmitter { setupControllerConnection (outStream) { const api = this.getApi() const dnode = Dnode(api) + // report new active controller connection + this.activeControllerConnections++ + this.emit('controllerConnectionChanged', this.activeControllerConnections) + // connect dnode api to remote connection pump( outStream, dnode, outStream, (err) => { + // report new active controller connection + this.activeControllerConnections-- + this.emit('controllerConnectionChanged', this.activeControllerConnections) + // report any error if (err) log.error(err) } ) @@ -1242,6 +1252,7 @@ module.exports = class MetamaskController extends EventEmitter { engine.push(createOriginMiddleware({ origin })) engine.push(createLoggerMiddleware({ origin })) engine.push(filterMiddleware) + engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) engine.push(createProviderMiddleware({ provider: this.provider })) // setup connection @@ -1278,6 +1289,34 @@ module.exports = class MetamaskController extends EventEmitter { ) } + /** + * Handle a KeyringController update + * @param {object} state the KC state + * @return {Promise} + * @private + */ + async _onKeyringControllerUpdate (state) { + const {isUnlocked, keyrings} = state + const addresses = keyrings.reduce((acc, {accounts}) => acc.concat(accounts), []) + + if (!addresses.length) { + return + } + + // Ensure preferences + identities controller know about all addresses + this.preferencesController.addAddresses(addresses) + this.accountTracker.syncWithAddresses(addresses) + + const wasLocked = !isUnlocked + if (wasLocked) { + const oldSelectedAddress = this.preferencesController.getSelectedAddress() + if (!addresses.includes(oldSelectedAddress)) { + const address = addresses[0] + await this.preferencesController.setSelectedAddress(address) + } + } + } + /** * A method for emitting the full MetaMask state to all registered listeners. * @private @@ -1424,6 +1463,7 @@ module.exports = class MetamaskController extends EventEmitter { } } + // TODO: Replace isClientOpen methods with `controllerConnectionChanged` events. /** * A method for recording whether the MetaMask user interface is open or not. * @private diff --git a/development/states/add-token.json b/development/states/add-token.json index 84ad5dd4c..d04b3a3ca 100644 --- a/development/states/add-token.json +++ b/development/states/add-token.json @@ -123,6 +123,7 @@ "modalState": {}, "previousModalState": {} }, + "sidebar": {}, "transForward": true, "isLoading": false, "warning": null, diff --git a/development/states/confirm-new-ui.json b/development/states/confirm-new-ui.json index 2c2e17704..fffee9893 100644 --- a/development/states/confirm-new-ui.json +++ b/development/states/confirm-new-ui.json @@ -141,6 +141,7 @@ "accountDetail": { "subview": "transactions" }, + "sidebar": {}, "modal": { "modalState": {}, "previousModalState": {} diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json index 829f513a8..5017a4d57 100644 --- a/development/states/confirm-sig-requests.json +++ b/development/states/confirm-sig-requests.json @@ -162,6 +162,7 @@ "accountDetail": { "subview": "transactions" }, + "sidebar": {}, "modal": { "modalState": {}, "previousModalState": {} diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json index 6848c0840..847ea11a3 100644 --- a/development/states/currency-localization.json +++ b/development/states/currency-localization.json @@ -120,6 +120,7 @@ "accountDetail": { "subview": "transactions" }, + "sidebar": {}, "modal": { "modalState": {}, "previousModalState": {} diff --git a/development/states/first-time.json b/development/states/first-time.json index f44148973..a31b985a3 100644 --- a/development/states/first-time.json +++ b/development/states/first-time.json @@ -48,6 +48,7 @@ "accountDetail": { "subview": "transactions" }, + "sidebar": {}, "transForward": true, "isLoading": false, "warning": null, diff --git a/development/states/send-edit.json b/development/states/send-edit.json index 8e5c25a82..6330b777d 100644 --- a/development/states/send-edit.json +++ b/development/states/send-edit.json @@ -22,6 +22,7 @@ "name": "Send Account 4" } }, + "assetImages": {}, "unapprovedTxs": {}, "currentCurrency": "USD", "conversionRate": 1200.88200327, @@ -141,6 +142,7 @@ "accountDetail": { "subview": "transactions" }, + "sidebar": {}, "modal": { "modalState": {}, "previousModalState": {} diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index ad2ff3d6e..bb4847155 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -61,6 +61,7 @@ "name": "Address Book Account 1" } ], + "assetImages": {}, "tokens": [], "transactions": {}, "selectedAddressTxList": [], @@ -120,6 +121,7 @@ "accountDetail": { "subview": "transactions" }, + "sidebar": {}, "modal": { "modalState": {}, "previousModalState": {} diff --git a/development/states/send.json b/development/states/send.json index 73ac62f65..4c67f8ac6 100644 --- a/development/states/send.json +++ b/development/states/send.json @@ -21,6 +21,7 @@ "name": "Account 4" } }, + "assetImages": {}, "unapprovedTxs": {}, "currentCurrency": "USD", "conversionRate": 16.88200327, @@ -99,6 +100,7 @@ "accountExport": "none", "privateKey": "" }, + "sidebar": {}, "transForward": true, "isLoading": false, "warning": null, diff --git a/development/states/tx-list-items.json b/development/states/tx-list-items.json index f22fd0a56..0d2273cb0 100644 --- a/development/states/tx-list-items.json +++ b/development/states/tx-list-items.json @@ -118,6 +118,7 @@ "modalState": {}, "previousModalState": {} }, + "sidebar": {}, "transForward": true, "isLoading": false, "warning": null, diff --git a/old-ui/app/account-detail.js b/old-ui/app/account-detail.js index c67f0cf71..d240fc38e 100644 --- a/old-ui/app/account-detail.js +++ b/old-ui/app/account-detail.js @@ -32,6 +32,7 @@ function mapStateToProps (state) { currentCurrency: state.metamask.currentCurrency, currentAccountTab: state.metamask.currentAccountTab, tokens: state.metamask.tokens, + suggestedTokens: state.metamask.suggestedTokens, computedBalances: state.metamask.computedBalances, } } @@ -49,6 +50,10 @@ AccountDetailScreen.prototype.render = function () { var account = props.accounts[selected] const { network, conversionRate, currentCurrency } = props + if (Object.keys(props.suggestedTokens).length > 0) { + this.props.dispatch(actions.showAddSuggestedTokenPage()) + } + return ( h('.account-detail-section.full-flex-height', [ diff --git a/old-ui/app/add-suggested-token.js b/old-ui/app/add-suggested-token.js new file mode 100644 index 000000000..ea534b7da --- /dev/null +++ b/old-ui/app/add-suggested-token.js @@ -0,0 +1,202 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../ui/app/actions') +const Tooltip = require('./components/tooltip.js') +const ethUtil = require('ethereumjs-util') +const Copyable = require('./components/copyable') +const addressSummary = require('./util').addressSummary + + +module.exports = connect(mapStateToProps)(AddSuggestedTokenScreen) + +function mapStateToProps (state) { + return { + identities: state.metamask.identities, + suggestedTokens: state.metamask.suggestedTokens, + } +} + +inherits(AddSuggestedTokenScreen, Component) +function AddSuggestedTokenScreen () { + this.state = { + warning: null, + } + Component.call(this) +} + +AddSuggestedTokenScreen.prototype.render = function () { + const state = this.state + const props = this.props + const { warning } = state + const key = Object.keys(props.suggestedTokens)[0] + const { address, symbol, decimals } = props.suggestedTokens[key] + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('h2.page-subtitle', 'Add Suggested Token'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + h('div', [ + h(Tooltip, { + position: 'top', + title: 'The contract of the actual token contract. Click for more info.', + }, [ + h('a', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + href: 'https://support.metamask.io/kb/article/24-what-is-a-token-contract-address', + target: '_blank', + }, [ + h('span', 'Token Contract Address '), + h('i.fa.fa-question-circle'), + ]), + ]), + ]), + + h('div', { + style: { display: 'flex' }, + }, [ + h(Copyable, { + value: ethUtil.toChecksumAddress(address), + }, [ + h('span#token-address', { + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + display: 'flex', + }, + }, addressSummary(address, 24, 4, false)), + ]), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Symbol'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('p#token_symbol', { + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }, symbol), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('p#token_decimals', { + type: 'number', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }, decimals), + ]), + + h('button', { + style: { + alignSelf: 'center', + margin: '8px', + }, + onClick: (event) => { + this.props.dispatch(actions.removeSuggestedTokens()) + }, + }, 'Cancel'), + + h('button', { + style: { + alignSelf: 'center', + margin: '8px', + }, + onClick: (event) => { + const valid = this.validateInputs({ address, symbol, decimals }) + if (!valid) return + + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + .then(() => { + this.props.dispatch(actions.removeSuggestedTokens()) + }) + }, + }, 'Add'), + ]), + ]), + ]) + ) +} + +AddSuggestedTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return +} + +AddSuggestedTokenScreen.prototype.validateInputs = function (opts) { + let msg = '' + const identitiesList = Object.keys(this.props.identities) + const { address, symbol, decimals } = opts + const standardAddress = ethUtil.addHexPrefix(address).toLowerCase() + + const validAddress = ethUtil.isValidAddress(address) + if (!validAddress) { + msg += 'Address is invalid.' + } + + const validDecimals = decimals >= 0 && decimals <= 36 + if (!validDecimals) { + msg += 'Decimals must be at least 0, and not over 36. ' + } + + const symbolLen = symbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + msg += 'Symbol must be between 0 and 10 characters.' + } + + const ownAddress = identitiesList.includes(standardAddress) + if (ownAddress) { + msg = 'Personal address detected. Input the token contract address.' + } + + const isValid = validAddress && validDecimals && !ownAddress + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid +} diff --git a/old-ui/app/add-token.js b/old-ui/app/add-token.js index e869ac39a..6cf211636 100644 --- a/old-ui/app/add-token.js +++ b/old-ui/app/add-token.js @@ -196,7 +196,7 @@ AddTokenScreen.prototype.validateInputs = function () { msg += 'Address is invalid.' } - const validDecimals = decimals >= 0 && decimals < 36 + const validDecimals = decimals >= 0 && decimals <= 36 if (!validDecimals) { msg += 'Decimals must be at least 0, and not over 36. ' } diff --git a/old-ui/app/app.js b/old-ui/app/app.js index d3e9e823b..9be21ebad 100644 --- a/old-ui/app/app.js +++ b/old-ui/app/app.js @@ -23,6 +23,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice') // other views const ConfigScreen = require('./config') const AddTokenScreen = require('./add-token') +const AddSuggestedTokenScreen = require('./add-suggested-token') const Import = require('./accounts/import') const InfoScreen = require('./info') const NewUiAnnouncement = require('./new-ui-annoucement') @@ -74,6 +75,7 @@ function mapStateToProps (state) { lostAccounts: state.metamask.lostAccounts, frequentRpcList: state.metamask.frequentRpcList || [], featureFlags, + suggestedTokens: state.metamask.suggestedTokens, // state needed to get account dropdown temporarily rendering from app bar identities, @@ -236,6 +238,10 @@ App.prototype.renderPrimary = function () { log.debug('rendering add-token screen from unlock screen.') return h(AddTokenScreen, {key: 'add-token'}) + case 'add-suggested-token': + log.debug('rendering add-suggested-token screen from unlock screen.') + return h(AddSuggestedTokenScreen, {key: 'add-suggested-token'}) + case 'config': log.debug('rendering config screen') return h(ConfigScreen, {key: 'config'}) diff --git a/old-ui/app/components/app-bar.js b/old-ui/app/components/app-bar.js index 8ab647efd..234c06a01 100644 --- a/old-ui/app/components/app-bar.js +++ b/old-ui/app/components/app-bar.js @@ -350,11 +350,14 @@ module.exports = class AppBar extends Component { } } - renderCommonRpc (rpcList, {rpcTarget}) { + renderCommonRpc (rpcList, provider) { const {dispatch} = this.props + const reversedRpcList = rpcList.slice().reverse() - return rpcList.map((rpc) => { - if ((rpc === LOCALHOST_RPC_URL) || (rpc === rpcTarget)) { + return reversedRpcList.map((rpc) => { + const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget + + if ((rpc === LOCALHOST_RPC_URL) || currentRpcTarget) { return null } else { return h(DropdownMenuItem, { @@ -364,7 +367,7 @@ module.exports = class AppBar extends Component { }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), rpc, - rpcTarget === rpc + currentRpcTarget ? h('.check', '✓') : null, ]) diff --git a/package-lock.json b/package-lock.json index a40bb112c..93c3f13a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16169,6 +16169,14 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=", + "requires": { + "string-convert": "^0.2.0" + } + }, "json3": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", @@ -25028,6 +25036,16 @@ "xtend": "^4.0.1" } }, + "react-media": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/react-media/-/react-media-1.8.0.tgz", + "integrity": "sha512-XcfqkDQj5/hmJod/kXUAZljJyMVkWrBWOkzwynAR8BXOGlbFLGBwezM0jQHtp2BrSymhf14/XrQrb3gGBnGK4g==", + "requires": { + "invariant": "^2.2.2", + "json2mq": "^0.2.0", + "prop-types": "^15.5.10" + } + }, "react-modal": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.4.4.tgz", @@ -27885,6 +27903,11 @@ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" }, + "string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c=" + }, "string-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", diff --git a/package.json b/package.json index 01e51fd9d..beb122a3c 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,11 @@ [ "env", { - "debug": true + "browsers": [ + ">0.25%", + "not ie 11", + "not op_mini all" + ] } ], "stage-0" @@ -143,7 +147,7 @@ "gulp-eslint": "^4.0.0", "gulp-sass": "^4.0.0", "hat": "0.0.3", - "human-standard-token-abi": "^1.0.2", + "human-standard-token-abi": "^2.0.0", "idb-global": "^2.1.0", "identicon.js": "^2.3.1", "iframe": "^1.0.0", @@ -185,6 +189,7 @@ "react-dom": "^15.6.2", "react-hyperscript": "^3.0.0", "react-markdown": "^3.0.0", + "react-media": "^1.8.0", "react-redux": "^5.0.5", "react-router-dom": "^4.2.2", "react-select": "^1.0.0", @@ -246,8 +251,8 @@ "del": "^3.0.0", "dot-only-hunter": "^1.0.3", "envify": "^4.0.0", - "enzyme": "^3.3.0", - "enzyme-adapter-react-15": "^1.0.5", + "enzyme": "^3.4.4", + "enzyme-adapter-react-15": "^1.0.6", "eslint-plugin-chai": "0.0.1", "eslint-plugin-json": "^1.2.0", "eslint-plugin-mocha": "^5.0.0", diff --git a/test/e2e/beta/from-import-beta-ui.spec.js b/test/e2e/beta/from-import-beta-ui.spec.js index abc684b4f..5ee8f0268 100644 --- a/test/e2e/beta/from-import-beta-ui.spec.js +++ b/test/e2e/beta/from-import-beta-ui.spec.js @@ -314,12 +314,12 @@ describe('Using MetaMask with an existing account', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.tx-list-item')) + const transactions = await findElements(driver, By.css('.transaction-list-item')) assert.equal(transactions.length, 1) - const txValues = await findElements(driver, By.css('.tx-list-value')) + const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary')) assert.equal(txValues.length, 1) - assert.equal(await txValues[0].getText(), '1 ETH') + assert.equal(await txValues[0].getText(), '-1 ETH') }) }) diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index dd327accb..c9f759780 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -225,19 +225,9 @@ describe('MetaMask', function () { await delay(regularDelayMs) } - await clickWordAndWait(words[0]) - await clickWordAndWait(words[1]) - await clickWordAndWait(words[2]) - await clickWordAndWait(words[3]) - await clickWordAndWait(words[4]) - await clickWordAndWait(words[5]) - await clickWordAndWait(words[6]) - await clickWordAndWait(words[7]) - await clickWordAndWait(words[8]) - await clickWordAndWait(words[9]) - await clickWordAndWait(words[10]) - await clickWordAndWait(words[11]) - + for (let i = 0; i < 12; i++) { + await clickWordAndWait(words[i]) + } } catch (e) { if (count > 2) { throw e @@ -414,12 +404,12 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.tx-list-item')) + const transactions = await findElements(driver, By.css('.transaction-list-item')) assert.equal(transactions.length, 1) if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000) + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary')) + await driver.wait(until.elementTextMatches(txValues, /-1\sETH/), 10000) } }) }) @@ -457,14 +447,11 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.tx-list-item')) + const transactions = await findElements(driver, By.css('.transaction-list-item')) assert.equal(transactions.length, 2) - const txStatuses = await findElements(driver, By.css('.tx-list-status')) - await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) - - const txValues = await findElement(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000) + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary')) + await driver.wait(until.elementTextMatches(txValues, /-3\sETH/), 10000) }) }) @@ -487,9 +474,9 @@ describe('MetaMask', function () { await driver.switchTo().window(extension) await delay(regularDelayMs) - const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`)) + const txListItem = await findElement(driver, By.xpath(`//div[contains(text(), 'Contract Deployment')]`)) await txListItem.click() - await delay(regularDelayMs) + await delay(largeDelayMs) }) it('displays the contract creation data', async () => { @@ -511,13 +498,15 @@ describe('MetaMask', function () { it('confirms a deploy contract transaction', async () => { const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirmButton.click() - await delay(regularDelayMs) + await delay(largeDelayMs) - const txStatuses = await findElements(driver, By.css('.tx-list-status')) - await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) + driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 3 + }, 10000) - const txAccounts = await findElements(driver, By.css('.tx-list-account')) - assert.equal(await txAccounts[0].getText(), 'Contract Deployment') + const txAction = await findElements(driver, By.css('.transaction-list-item__action')) + await driver.wait(until.elementTextMatches(txAction[0], /Contract\sDeployment/), 10000) await delay(regularDelayMs) }) @@ -538,9 +527,9 @@ describe('MetaMask', function () { await driver.switchTo().window(extension) await delay(largeDelayMs) - await findElements(driver, By.css('.tx-list-pending-item-container')) - const [txListValue] = await findElements(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000) + await findElements(driver, By.css('.transaction-list-item')) + const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--secondary')) + await driver.wait(until.elementTextMatches(txListValue, /-4\sETH/), 10000) await txListValue.click() await delay(regularDelayMs) @@ -568,15 +557,17 @@ describe('MetaMask', function () { await confirmButton.click() await delay(regularDelayMs) - const txStatuses = await findElements(driver, By.css('.tx-list-status')) - await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) + driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 4 + }, 10000) - const txValues = await findElement(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000) + const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary')) + await driver.wait(until.elementTextMatches(txValues[0], /-4\sETH/), 10000) - const txAccounts = await findElements(driver, By.css('.tx-list-account')) - const firstTxAddress = await txAccounts[0].getText() - assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/)) + // const txAccounts = await findElements(driver, By.css('.tx-list-account')) + // const firstTxAddress = await txAccounts[0].getText() + // assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/)) }) it('calls and confirms a contract method where ETH is received', async () => { @@ -590,7 +581,7 @@ describe('MetaMask', function () { await driver.switchTo().window(extension) await delay(regularDelayMs) - const txListItem = await findElement(driver, By.css('.tx-list-item')) + const txListItem = await findElement(driver, By.css('.transaction-list-item')) await txListItem.click() await delay(regularDelayMs) @@ -598,18 +589,20 @@ describe('MetaMask', function () { await confirmButton.click() await delay(regularDelayMs) - const txStatuses = await findElements(driver, By.css('.tx-list-status')) - await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) + driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 5 + }, 10000) - const txValues = await findElement(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000) + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary')) + await driver.wait(until.elementTextMatches(txValues, /-0\sETH/), 10000) await closeAllWindowHandlesExcept(driver, [extension, dapp]) await driver.switchTo().window(extension) }) it('renders the correct ETH balance', async () => { - const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) + const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance')) await delay(regularDelayMs) if (process.env.SELENIUM_BROWSER !== 'firefox') { await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000) @@ -654,18 +647,17 @@ describe('MetaMask', function () { await closeAllWindowHandlesExcept(driver, [extension, dapp]) await delay(regularDelayMs) await driver.switchTo().window(extension) - await delay(regularDelayMs) - + await delay(largeDelayMs) }) it('clicks on the Add Token button', async () => { - const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`)) + const addToken = await driver.findElement(By.css('.wallet-view__add-token-button')) await addToken.click() await delay(regularDelayMs) }) it('picks the newly created Test token', async () => { - const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]")) + const addCustomToken = await findElement(driver, By.xpath("//li[contains(text(), 'Custom Token')]")) await addCustomToken.click() await delay(regularDelayMs) @@ -683,7 +675,7 @@ describe('MetaMask', function () { }) it('renders the balance for the new token', async () => { - const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) + const balance = await findElement(driver, By.css('.transaction-view-balance .transaction-view-balance__token-balance')) await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/)) const tokenAmount = await balance.getText() assert.ok(/^100\s*TST\s*$/.test(tokenAmount)) @@ -752,21 +744,25 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.tx-list-item')) + const transactions = await findElements(driver, By.css('.transaction-list-item')) assert.equal(transactions.length, 1) - const txValues = await findElements(driver, By.css('.tx-list-value')) + const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) assert.equal(txValues.length, 1) // test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved, // or possibly until we use latest version of firefox in the tests if (process.env.SELENIUM_BROWSER !== 'firefox') { - await driver.wait(until.elementTextMatches(txValues[0], /50\sTST/), 10000) + await driver.wait(until.elementTextMatches(txValues[0], /-50\sTST/), 10000) } - const txStatuses = await findElements(driver, By.css('.tx-list-status')) - const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed|Failed/), 10000) - assert.equal(await tx.getText(), 'Confirmed') + driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) + const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) + const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken|Failed/), 10000) + assert.equal(await tx.getText(), 'Sent Tokens') }) }) @@ -789,9 +785,9 @@ describe('MetaMask', function () { await driver.switchTo().window(extension) await delay(largeDelayMs) - await findElements(driver, By.css('.tx-list-pending-item-container')) - const [txListValue] = await findElements(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000) + await findElements(driver, By.css('.transaction-list__pending-transactions')) + const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/), 10000) await txListValue.click() await delay(regularDelayMs) @@ -838,25 +834,28 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.tx-list-item')) - assert.equal(transactions.length, 2) + driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 2 + }, 10000) - const txValues = await findElements(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/)) - const txStatuses = await findElements(driver, By.css('.tx-list-status')) - await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) + const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/)) + const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) + await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/)) const walletBalance = await findElement(driver, By.css('.wallet-balance')) await walletBalance.click() const tokenListItems = await findElements(driver, By.css('.token-list-item')) await tokenListItems[0].click() + await delay(regularDelayMs) // test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved, // or possibly until we use latest version of firefox in the tests if (process.env.SELENIUM_BROWSER !== 'firefox') { - const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount')) - assert.equal(await tokenBalanceAmount.getText(), '43') + const tokenBalanceAmount = await findElement(driver, By.css('.transaction-view-balance__token-balance')) + assert.equal(await tokenBalanceAmount.getText(), '43 TST') } }) }) @@ -880,9 +879,14 @@ describe('MetaMask', function () { await driver.switchTo().window(extension) await delay(regularDelayMs) - const [txListItem] = await findElements(driver, By.css('.tx-list-item')) - const [txListValue] = await findElements(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txListValue, /0\sETH/)) + driver.wait(async () => { + const pendingTxes = await findElements(driver, By.css('.transaction-list__pending-transactions .transaction-list-item')) + return pendingTxes.length === 1 + }, 10000) + + const [txListItem] = await findElements(driver, By.css('.transaction-list-item')) + const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/)) await txListItem.click() await delay(regularDelayMs) }) @@ -953,10 +957,15 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const txValues = await findElements(driver, By.css('.tx-list-value')) - await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/)) - const txStatuses = await findElements(driver, By.css('.tx-list-status')) - await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/)) + driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 3 + }, 10000) + + const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/)) + const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) + await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/)) }) }) @@ -1006,9 +1015,69 @@ describe('MetaMask', function () { }) it('renders the balance for the chosen token', async () => { - const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) + const balance = await findElement(driver, By.css('.transaction-view-balance__token-balance')) await driver.wait(until.elementTextMatches(balance, /0\sBAT/)) await delay(regularDelayMs) }) }) -}) \ No newline at end of file + + describe('Stores custom RPC history', () => { + const customRpcUrls = [ + 'https://mainnet.infura.io/1', + 'https://mainnet.infura.io/2', + 'https://mainnet.infura.io/3', + 'https://mainnet.infura.io/4', + ] + + customRpcUrls.forEach(customRpcUrl => { + it('creates custom RPC: ' + customRpcUrl, async () => { + const networkDropdown = await findElement(driver, By.css('.network-name')) + await networkDropdown.click() + await delay(regularDelayMs) + + const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Custom RPC')]`)) + await customRpcButton.click() + await delay(regularDelayMs) + + const customRpcInput = await findElement(driver, By.css('input[placeholder="New RPC URL"]')) + await customRpcInput.clear() + await customRpcInput.sendKeys(customRpcUrl) + + const customRpcSave = await findElement(driver, By.css('.settings__rpc-save-button')) + await customRpcSave.click() + await delay(largeDelayMs * 2) + }) + }) + + it('selects another provider', async () => { + const networkDropdown = await findElement(driver, By.css('.network-name')) + await networkDropdown.click() + await delay(regularDelayMs) + + const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Main Ethereum Network')]`)) + await customRpcButton.click() + await delay(largeDelayMs * 2) + }) + + it('finds 3 recent RPCs in history', async () => { + const networkDropdown = await findElement(driver, By.css('.network-name')) + await networkDropdown.click() + await delay(regularDelayMs) + + // oldest selected RPC is not found + await assertElementNotPresent(webdriver, driver, By.xpath(`//span[contains(text(), '${customRpcUrls[0]}')]`)) + + // only recent 3 are found and in correct order (most recent at the top) + const customRpcs = await findElements(driver, By.xpath(`//span[contains(text(), 'https://mainnet.infura.io/')]`)) + + assert.equal(customRpcs.length, 3) + + for (let i = 0; i < customRpcs.length; i++) { + const linkText = await customRpcs[i].getText() + const rpcUrl = customRpcUrls[customRpcUrls.length - i - 1] + + assert.notEqual(linkText.indexOf(rpcUrl), -1) + } + }) + }) +}) diff --git a/test/integration/lib/add-token.js b/test/integration/lib/add-token.js index 6de7574c4..bb9d0d10f 100644 --- a/test/integration/lib/add-token.js +++ b/test/integration/lib/add-token.js @@ -86,7 +86,7 @@ async function runAddTokenFlowTest (assert, done) { $('button.btn-primary.btn--large')[0].click() // Verify added token image - let heroBalance = await queryAsync($, '.hero-balance') + 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') @@ -134,7 +134,7 @@ async function runAddTokenFlowTest (assert, done) { // $('button.btn-primary--lg')[0].click() // Verify added token image - heroBalance = await queryAsync($, '.hero-balance') + 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/confirm-sig-requests.js b/test/integration/lib/confirm-sig-requests.js index dcc25c493..3539e97be 100644 --- a/test/integration/lib/confirm-sig-requests.js +++ b/test/integration/lib/confirm-sig-requests.js @@ -19,7 +19,7 @@ async function runConfirmSigRequestsTest (assert, done) { selectState.val('confirm sig requests') reactTriggerChange(selectState[0]) - const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable') + const pendingRequestItem = $.find('.transaction-list-item') if (pendingRequestItem[0]) { pendingRequestItem[0].click() diff --git a/test/integration/lib/currency-localization.js b/test/integration/lib/currency-localization.js index d42b7495d..8d5acf5d0 100644 --- a/test/integration/lib/currency-localization.js +++ b/test/integration/lib/currency-localization.js @@ -22,8 +22,8 @@ async function runCurrencyLocalizationTest (assert, done) { await timeout(1000) reactTriggerChange(selectState[0]) await timeout(1000) - const txView = await queryAsync($, '.tx-view') - const heroBalance = await findAsync($(txView), '.hero-balance') - const fiatAmount = await findAsync($(heroBalance), '.fiat-amount') - assert.equal(fiatAmount[0].textContent, '₱102,707.97') + const txView = await queryAsync($, '.transaction-view') + const heroBalance = await findAsync($(txView), '.transaction-view-balance__balance') + const fiatAmount = await findAsync($(heroBalance), '.transaction-view-balance__secondary-balance') + assert.equal(fiatAmount[0].textContent, '₱102,707.97 PHP') } diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index 406863ca6..7f3c114e4 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -58,7 +58,7 @@ async function runSendFlowTest (assert, done) { selectState.val('send new ui') reactTriggerChange(selectState[0]) - const sendScreenButton = await queryAsync($, 'button.btn-primary.hero-balance-button') + const sendScreenButton = await queryAsync($, 'button.btn-primary.transaction-view-balance__button') assert.ok(sendScreenButton[1], 'send screen button present') sendScreenButton[1].click() @@ -124,10 +124,10 @@ async function runSendFlowTest (assert, done) { selectState.val('send edit') reactTriggerChange(selectState[0]) - const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first() + const confirmFromName = (await queryAsync($, '.sender-to-recipient__name')).first() assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name') - const confirmToName = (await queryAsync($, '.sender-to-recipient__recipient-name')).last() + 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') diff --git a/test/integration/lib/tx-list-items.js b/test/integration/lib/tx-list-items.js index 9075efe03..7572d1629 100644 --- a/test/integration/lib/tx-list-items.js +++ b/test/integration/lib/tx-list-items.js @@ -29,26 +29,23 @@ async function runTxListItemsTest (assert, done) { assert.ok(metamaskLogo[0], 'metamask logo present') metamaskLogo[0].click() - const txListItems = await queryAsync($, '.tx-list-item') + const txListItems = await queryAsync($, '.transaction-list-item') assert.equal(txListItems.length, 8, 'all tx list items are rendered') - const unapprovedTx = txListItems[0] - assert.equal($(unapprovedTx).hasClass('tx-list-pending-item-container'), true, 'unapprovedTx has the correct class') - const retryTx = txListItems[1] - const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-container span') + const retryTxLink = await findAsync($(retryTx), '.transaction-list-item__retry') assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link') const approvedTx = txListItems[2] - const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status') - assert.equal(approvedTxRenderedStatus[0].textContent, 'Approved', 'approvedTx has correct label') + const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status') + assert.equal(approvedTxRenderedStatus[0].textContent, 'pending', 'approvedTx has correct label') const unapprovedMsg = txListItems[3] - const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.tx-list-account') + const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__action') assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description') const failedTx = txListItems[4] - const failedTxRenderedStatus = await findAsync($(failedTx), '.tx-list-status') + const failedTxRenderedStatus = await findAsync($(failedTx), '.transaction-list-item__status') assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label') const shapeShiftTx = txListItems[5] @@ -56,10 +53,10 @@ async function runTxListItemsTest (assert, done) { assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status') const confirmedTokenTx = txListItems[6] - const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.tx-list-account') - assert.equal(confirmedTokenTxAddress[0].textContent, '0xE7884118...81a9', 'confirmedTokenTx has correct address') + const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.transaction-list-item__status') + assert.equal(confirmedTokenTxAddress[0].textContent, 'Confirmed', 'confirmedTokenTx has correct address') const rejectedTx = txListItems[7] - const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.tx-list-status') + const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status') assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label') } diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index a798d41e2..85c78fe1e 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -814,6 +814,77 @@ describe('MetaMaskController', function () { }) }) + describe('#_onKeyringControllerUpdate', function () { + it('should do nothing if there are no keyrings in state', async function () { + const addAddresses = sinon.fake() + const syncWithAddresses = sinon.fake() + sandbox.replace(metamaskController, 'preferencesController', { + addAddresses, + }) + sandbox.replace(metamaskController, 'accountTracker', { + syncWithAddresses, + }) + + const oldState = metamaskController.getState() + await metamaskController._onKeyringControllerUpdate({keyrings: []}) + + assert.ok(addAddresses.notCalled) + assert.ok(syncWithAddresses.notCalled) + assert.deepEqual(metamaskController.getState(), oldState) + }) + + it('should update selected address if keyrings was locked', async function () { + const addAddresses = sinon.fake() + const getSelectedAddress = sinon.fake.returns('0x42') + const setSelectedAddress = sinon.fake() + const syncWithAddresses = sinon.fake() + sandbox.replace(metamaskController, 'preferencesController', { + addAddresses, + getSelectedAddress, + setSelectedAddress, + }) + sandbox.replace(metamaskController, 'accountTracker', { + syncWithAddresses, + }) + + const oldState = metamaskController.getState() + await metamaskController._onKeyringControllerUpdate({ + isUnlocked: false, + keyrings: [{ + accounts: ['0x1', '0x2'], + }], + }) + + assert.deepEqual(addAddresses.args, [[['0x1', '0x2']]]) + assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]]) + assert.deepEqual(setSelectedAddress.args, [['0x1']]) + assert.deepEqual(metamaskController.getState(), oldState) + }) + + it('should NOT update selected address if already unlocked', async function () { + const addAddresses = sinon.fake() + const syncWithAddresses = sinon.fake() + sandbox.replace(metamaskController, 'preferencesController', { + addAddresses, + }) + sandbox.replace(metamaskController, 'accountTracker', { + syncWithAddresses, + }) + + const oldState = metamaskController.getState() + await metamaskController._onKeyringControllerUpdate({ + isUnlocked: true, + keyrings: [{ + accounts: ['0x1', '0x2'], + }], + }) + + assert.deepEqual(addAddresses.args, [[['0x1', '0x2']]]) + assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]]) + assert.deepEqual(metamaskController.getState(), oldState) + }) + }) + }) function deferredPromise () { diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index 9b2c846bd..2c261be90 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -1,6 +1,7 @@ const assert = require('assert') const ObservableStore = require('obs-store') const PreferencesController = require('../../../../app/scripts/controllers/preferences') +const sinon = require('sinon') describe('preferences controller', function () { let preferencesController @@ -339,5 +340,114 @@ describe('preferences controller', function () { assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network') }) }) + + describe('on watchAsset', function () { + var stubNext, stubEnd, stubHandleWatchAssetERC20, asy, req, res + const sandbox = sinon.createSandbox() + + beforeEach(() => { + req = {params: {}} + res = {} + asy = {next: () => {}, end: () => {}} + stubNext = sandbox.stub(asy, 'next') + stubEnd = sandbox.stub(asy, 'end').returns(0) + stubHandleWatchAssetERC20 = sandbox.stub(preferencesController, '_handleWatchAssetERC20') + }) + after(() => { + sandbox.restore() + }) + + it('shouldn not do anything if method not corresponds', async function () { + const asy = {next: () => {}, end: () => {}} + var stubNext = sandbox.stub(asy, 'next') + var stubEnd = sandbox.stub(asy, 'end').returns(0) + req.method = 'metamask' + await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) + sandbox.assert.notCalled(stubEnd) + sandbox.assert.called(stubNext) + }) + it('should do something if method is supported', async function () { + const asy = {next: () => {}, end: () => {}} + var stubNext = sandbox.stub(asy, 'next') + var stubEnd = sandbox.stub(asy, 'end').returns(0) + req.method = 'metamask_watchAsset' + req.params.type = 'someasset' + await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) + sandbox.assert.called(stubEnd) + sandbox.assert.notCalled(stubNext) + }) + it('should through error if method is supported but asset type is not', async function () { + req.method = 'metamask_watchAsset' + req.params.type = 'someasset' + await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) + sandbox.assert.called(stubEnd) + sandbox.assert.notCalled(stubHandleWatchAssetERC20) + sandbox.assert.notCalled(stubNext) + assert.deepEqual(res, {}) + }) + it('should trigger handle add asset if type supported', async function () { + const asy = {next: () => {}, end: () => {}} + req.method = 'metamask_watchAsset' + req.params.type = 'ERC20' + await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) + sandbox.assert.called(stubHandleWatchAssetERC20) + }) + }) + + describe('on watchAsset of type ERC20', function () { + var req + + const sandbox = sinon.createSandbox() + beforeEach(() => { + req = {params: {type: 'ERC20'}} + }) + after(() => { + sandbox.restore() + }) + + it('should add suggested token', async function () { + const address = '0xabcdef1234567' + const symbol = 'ABBR' + const decimals = 5 + const image = 'someimage' + req.params.options = { address, symbol, decimals, image } + + sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true) + preferencesController.showWatchAssetUi = async () => {} + + await preferencesController._handleWatchAssetERC20(req.params.options) + const suggested = preferencesController.getSuggestedTokens() + assert.equal(Object.keys(suggested).length, 1, `one token added ${Object.keys(suggested)}`) + + assert.equal(suggested[address].address, address, 'set address correctly') + assert.equal(suggested[address].symbol, symbol, 'set symbol correctly') + assert.equal(suggested[address].decimals, decimals, 'set decimals correctly') + assert.equal(suggested[address].image, image, 'set image correctly') + }) + + it('should add token correctly if user confirms', async function () { + const address = '0xabcdef1234567' + const symbol = 'ABBR' + const decimals = 5 + const image = 'someimage' + req.params.options = { address, symbol, decimals, image } + + sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true) + preferencesController.showWatchAssetUi = async () => { + await preferencesController.addToken(address, symbol, decimals, image) + } + + await preferencesController._handleWatchAssetERC20(req.params.options) + const tokens = preferencesController.getTokens() + assert.equal(tokens.length, 1, `one token added`) + const added = tokens[0] + assert.equal(added.address, address, 'set address correctly') + assert.equal(added.symbol, symbol, 'set symbol correctly') + assert.equal(added.decimals, decimals, 'set decimals correctly') + + const assetImages = preferencesController.getAssetImages() + assert.ok(assetImages[address], `set image correctly`) + }) + }) }) diff --git a/ui/app/account-and-transaction-details.js b/ui/app/account-and-transaction-details.js deleted file mode 100644 index 03101d37a..000000000 --- a/ui/app/account-and-transaction-details.js +++ /dev/null @@ -1,33 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -// Main Views -const TxView = require('./components/tx-view') -const WalletView = require('./components/wallet-view') - -module.exports = AccountAndTransactionDetails - -inherits(AccountAndTransactionDetails, Component) -function AccountAndTransactionDetails () { - Component.call(this) -} - -AccountAndTransactionDetails.prototype.render = function () { - return h('div.account-and-transaction-details', [ - // wallet - h(WalletView, { - style: { - }, - responsiveDisplayClassname: '.lap-visible', - }, [ - ]), - - // transaction - h(TxView, { - style: { - }, - }, [ - ]), - ]) -} - diff --git a/ui/app/actions.js b/ui/app/actions.js index 6bcc64e17..6d5b1ef3f 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -227,11 +227,14 @@ var actions = { SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', showConfigPage, SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + SHOW_ADD_SUGGESTED_TOKEN_PAGE: 'SHOW_ADD_SUGGESTED_TOKEN_PAGE', showAddTokenPage, + showAddSuggestedTokenPage, addToken, addTokens, removeToken, updateTokens, + removeSuggestedTokens, UPDATE_TOKENS: 'UPDATE_TOKENS', setRpcTarget: setRpcTarget, setProviderType: setProviderType, @@ -1147,6 +1150,10 @@ function updateAndApproveTx (txData) { return txData }) + .catch((err) => { + dispatch(actions.hideLoadingIndication()) + return Promise.reject(err) + }) } } @@ -1589,11 +1596,18 @@ function showAddTokenPage (transitionForward = true) { } } -function addToken (address, symbol, decimals) { +function showAddSuggestedTokenPage (transitionForward = true) { + return { + type: actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE, + value: transitionForward, + } +} + +function addToken (address, symbol, decimals, image) { return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.addToken(address, symbol, decimals, (err, tokens) => { + background.addToken(address, symbol, decimals, image, (err, tokens) => { dispatch(actions.hideLoadingIndication()) if (err) { dispatch(actions.displayWarning(err.message)) @@ -1643,6 +1657,27 @@ function addTokens (tokens) { } } +function removeSuggestedTokens () { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.removeSuggestedTokens((err, suggestedTokens) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.clearPendingTokens()) + if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) { + return global.platform.closeCurrentWindow() + } + resolve(suggestedTokens) + }) + }) + .then(() => updateMetamaskStateFromBackground()) + .then(suggestedTokens => dispatch(actions.updateMetamaskState({...suggestedTokens}))) + } +} + function updateTokens (newTokens) { return { type: actions.UPDATE_TOKENS, @@ -1650,6 +1685,12 @@ function updateTokens (newTokens) { } } +function clearPendingTokens () { + return { + type: actions.CLEAR_PENDING_TOKENS, + } +} + function goBackToInitView () { return { type: actions.BACK_TO_INIT_MENU, @@ -1812,9 +1853,13 @@ function hideModal (payload) { } } -function showSidebar () { +function showSidebar ({ transitionName, type }) { return { type: actions.SIDEBAR_OPEN, + value: { + transitionName, + type, + }, } } @@ -2310,9 +2355,3 @@ function setPendingTokens (pendingTokens) { payload: tokens, } } - -function clearPendingTokens () { - return { - type: actions.CLEAR_PENDING_TOKENS, - } -} diff --git a/ui/app/app.js b/ui/app/app.js index 4fcf092ca..aa051280b 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -15,10 +15,10 @@ const SendTransactionScreen = require('./components/send/send.container') const ConfirmTransaction = require('./components/pages/confirm-transaction') // slideout menu -const WalletView = require('./components/wallet-view') +const Sidebar = require('./components/sidebars').default // other views -const Home = require('./components/pages/home') +import Home from './components/pages/home' const Authenticated = require('./components/pages/authenticated') const Initialized = require('./components/pages/initialized') const Settings = require('./components/pages/settings') @@ -26,11 +26,11 @@ const RestoreVaultPage = require('./components/pages/keychains/restore-vault').d const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed') const AddTokenPage = require('./components/pages/add-token') const ConfirmAddTokenPage = require('./components/pages/confirm-add-token') +const ConfirmAddSuggestedTokenPage = require('./components/pages/confirm-add-suggested-token') const CreateAccountPage = require('./components/pages/create-account') const NoticeScreen = require('./components/pages/notice') const Loading = require('./components/loading-screen') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const NetworkDropdown = require('./components/dropdowns/network-dropdown') const AccountMenu = require('./components/account-menu') @@ -51,6 +51,7 @@ const { RESTORE_VAULT_ROUTE, ADD_TOKEN_ROUTE, CONFIRM_ADD_TOKEN_ROUTE, + CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, NEW_ACCOUNT_ROUTE, SEND_ROUTE, CONFIRM_TRANSACTION_ROUTE, @@ -85,6 +86,7 @@ class App extends Component { h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }), h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }), + h(Authenticated, { path: CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, exact, component: ConfirmAddSuggestedTokenPage }), h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }), h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }), ]) @@ -102,6 +104,7 @@ class App extends Component { frequentRpcList, currentView, setMouseUserState, + sidebar, } = this.props const isLoadingNetwork = network === 'loading' && currentView.name !== 'config' const loadMessage = loadingMessage || isLoadingNetwork ? @@ -134,7 +137,12 @@ class App extends Component { h(AppHeader), // sidebar - this.renderSidebar(), + h(Sidebar, { + sidebarOpen: sidebar.isOpen, + hideSidebar: this.props.hideSidebar, + transitionName: sidebar.transitionName, + type: sidebar.type, + }), // network dropdown h(NetworkDropdown, { @@ -154,51 +162,6 @@ class App extends Component { ) } - renderSidebar () { - return h('div', [ - h('style', ` - .sidebar-enter { - transition: transform 300ms ease-in-out; - transform: translateX(-100%); - } - .sidebar-enter.sidebar-enter-active { - transition: transform 300ms ease-in-out; - transform: translateX(0%); - } - .sidebar-leave { - transition: transform 200ms ease-out; - transform: translateX(0%); - } - .sidebar-leave.sidebar-leave-active { - transition: transform 200ms ease-out; - transform: translateX(-100%); - } - `), - - h(ReactCSSTransitionGroup, { - transitionName: 'sidebar', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 200, - }, [ - // A second instance of Walletview is used for non-mobile viewports - this.props.sidebarOpen ? h(WalletView, { - responsiveDisplayClassname: '.sidebar', - style: {}, - }) : undefined, - - ]), - - // overlay - // TODO: add onClick for overlay to close sidebar - this.props.sidebarOpen ? h('div.sidebar-overlay', { - style: {}, - onClick: () => { - this.props.hideSidebar() - }, - }, []) : undefined, - ]) - } - toggleMetamaskActive () { if (!this.props.isUnlocked) { // currently inactive: redirect to password box @@ -267,7 +230,7 @@ App.propTypes = { provider: PropTypes.object, frequentRpcList: PropTypes.array, currentView: PropTypes.object, - sidebarOpen: PropTypes.bool, + sidebar: PropTypes.object, alertOpen: PropTypes.bool, hideSidebar: PropTypes.func, isMascara: PropTypes.bool, @@ -303,7 +266,7 @@ function mapStateToProps (state) { const { appState, metamask } = state const { networkDropdownOpen, - sidebarOpen, + sidebar, alertOpen, alertMessage, isLoading, @@ -330,7 +293,7 @@ function mapStateToProps (state) { return { // state from plugin networkDropdownOpen, - sidebarOpen, + sidebar, alertOpen, alertMessage, isLoading, diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index e31552f2d..d63d78c9f 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -4,8 +4,8 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const TokenBalance = require('./token-balance') const Identicon = require('./identicon') -const currencyFormatter = require('currency-formatter') -const currencies = require('currency-formatter/currencies') +import CurrencyDisplay from './currency-display' +const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors') const { formatBalance, generateBalanceObject } = require('../util') @@ -20,8 +20,9 @@ function mapStateToProps (state) { return { account, network, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, + conversionRate: conversionRateSelector(state), + currentCurrency: getCurrentCurrency(state), + assetImages: getAssetImages(state), } } @@ -32,7 +33,9 @@ function BalanceComponent () { BalanceComponent.prototype.render = function () { const props = this.props - const { token, network } = props + const { token, network, assetImages } = props + const address = token && token.address + const image = assetImages && address ? assetImages[token.address] : undefined return h('div.balance-container', {}, [ @@ -43,8 +46,9 @@ BalanceComponent.prototype.render = function () { // }), h(Identicon, { diameter: 50, - address: token && token.address, + address, network, + image, }), token ? this.renderTokenBalance() : this.renderBalance(), @@ -80,38 +84,12 @@ BalanceComponent.prototype.renderBalance = function () { style: {}, }, this.getTokenBalance(formattedBalance, shorten)), - showFiat ? this.renderFiatValue(formattedBalance) : null, + showFiat && h(CurrencyDisplay, { + value: balanceValue, + }), ]) } -BalanceComponent.prototype.renderFiatValue = function (formattedBalance) { - - const { conversionRate, currentCurrency } = this.props - - const fiatDisplayNumber = this.getFiatDisplayNumber(formattedBalance, conversionRate) - - const fiatPrefix = currentCurrency === 'USD' ? '$' : '' - - return this.renderFiatAmount(fiatDisplayNumber, currentCurrency, fiatPrefix) -} - -BalanceComponent.prototype.renderFiatAmount = function (fiatDisplayNumber, fiatSuffix, fiatPrefix) { - const shouldNotRenderFiat = fiatDisplayNumber === 'N/A' || Number(fiatDisplayNumber) === 0 - if (shouldNotRenderFiat) return null - - const upperCaseFiatSuffix = fiatSuffix.toUpperCase() - - const display = currencies.find(currency => currency.code === upperCaseFiatSuffix) - ? currencyFormatter.format(Number(fiatDisplayNumber), { - code: upperCaseFiatSuffix, - }) - : `${fiatPrefix}${fiatDisplayNumber} ${upperCaseFiatSuffix}` - - return h('div.fiat-amount', { - style: {}, - }, display) -} - BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) { const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3) diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js deleted file mode 100644 index c6957d2aa..000000000 --- a/ui/app/components/buy-button-subview.js +++ /dev/null @@ -1,267 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') -const CoinbaseForm = require('./coinbase-form') -const ShapeshiftForm = require('./shapeshift-form') -const Loading = require('./loading-screen') -const AccountPanel = require('./account-panel') -const RadioList = require('./custom-radio-list') -const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util') - -BuyButtonSubview.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps)(BuyButtonSubview) - - -function mapStateToProps (state) { - return { - identity: state.appState.identity, - account: state.metamask.accounts[state.appState.buyView.buyAddress], - warning: state.appState.warning, - buyView: state.appState.buyView, - network: state.metamask.network, - provider: state.metamask.provider, - context: state.appState.currentView.context, - isSubLoading: state.appState.isSubLoading, - } -} - -inherits(BuyButtonSubview, Component) -function BuyButtonSubview () { - Component.call(this) -} - -BuyButtonSubview.prototype.render = function () { - return ( - h('div', { - style: { - width: '100%', - }, - }, [ - this.headerSubview(), - this.primarySubview(), - ]) - ) -} - -BuyButtonSubview.prototype.headerSubview = function () { - const props = this.props - const isLoading = props.isSubLoading - return ( - - h('.flex-column', { - style: { - alignItems: 'center', - }, - }, [ - - // header bar (back button, label) - h('.flex-row', { - style: { - alignItems: 'center', - justifyContent: 'center', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.backButtonContext.bind(this), - style: { - position: 'absolute', - left: '10px', - }, - }), - h('h2.text-transform-uppercase.flex-center', { - style: { - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, this.context.t('depositEth')), - ]), - - // loading indication - h('div', { - style: { - position: 'absolute', - top: '57vh', - left: '49vw', - }, - }, [ - isLoading && h(Loading), - ]), - - // account panel - h('div', { - style: { - width: '80%', - }, - }, [ - h(AccountPanel, { - showFullAddress: true, - identity: props.identity, - account: props.account, - }), - ]), - - h('.flex-row', { - style: { - alignItems: 'center', - justifyContent: 'center', - }, - }, [ - h('h3.text-transform-uppercase.flex-center', { - style: { - paddingLeft: '15px', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, this.context.t('selectService')), - ]), - - ]) - - ) -} - - -BuyButtonSubview.prototype.primarySubview = function () { - const props = this.props - const network = props.network - - switch (network) { - case 'loading': - return - - case '1': - return this.mainnetSubview() - - // Ropsten, Rinkeby, Kovan - case '3': - case '4': - case '42': - const networkName = getNetworkDisplayName(network) - const label = `${networkName} ${this.context.t('testFaucet')}` - return ( - h('div.flex-column', { - style: { - alignItems: 'center', - margin: '20px 50px', - }, - }, [ - h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, label), - // Kovan only: Dharma loans beta - network === '42' ? ( - h('button.text-transform-uppercase', { - onClick: () => this.navigateTo('https://borrow.dharma.io/'), - style: { - marginTop: '15px', - }, - }, this.context.t('borrowDharma')) - ) : null, - ]) - ) - - default: - return ( - h('h2.error', this.context.t('unknownNetworkId')) - ) - - } -} - -BuyButtonSubview.prototype.mainnetSubview = function () { - const props = this.props - - return ( - - h('.flex-column', { - style: { - alignItems: 'center', - }, - }, [ - - h('.flex-row.selected-exchange', { - style: { - position: 'relative', - right: '35px', - marginTop: '20px', - marginBottom: '20px', - }, - }, [ - h(RadioList, { - defaultFocus: props.buyView.subview, - labels: [ - 'Coinbase', - 'ShapeShift', - ], - subtext: { - 'Coinbase': `${this.context.t('crypto')}/${this.context.t('fiat')} (${this.context.t('usaOnly')})`, - 'ShapeShift': this.context.t('crypto'), - }, - onClick: this.radioHandler.bind(this), - }), - ]), - - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, props.buyView.subview), - - this.formVersionSubview(), - ]) - - ) -} - -BuyButtonSubview.prototype.formVersionSubview = function () { - const network = this.props.network - if (network === '1') { - if (this.props.buyView.formView.coinbase) { - return h(CoinbaseForm, this.props) - } else if (this.props.buyView.formView.shapeshift) { - return h(ShapeshiftForm, this.props) - } - } -} - -BuyButtonSubview.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - -BuyButtonSubview.prototype.backButtonContext = function () { - if (this.props.context === 'confTx') { - this.props.dispatch(actions.showConfTxPage({transForward: false})) - } else { - this.props.dispatch(actions.goHome()) - } -} - -BuyButtonSubview.prototype.radioHandler = function (event) { - switch (event.target.title) { - case 'Coinbase': - return this.props.dispatch(actions.coinBaseSubview()) - case 'ShapeShift': - return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) - } -} 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 08923af88..de9aa6eb7 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 @@ -18,6 +18,7 @@ export default class ConfirmPageContainerContent extends Component { hideSubtitle: PropTypes.bool, identiconAddress: PropTypes.string, nonce: PropTypes.string, + assetImage: PropTypes.string, subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), summaryComponent: PropTypes.node, title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), @@ -60,6 +61,7 @@ export default class ConfirmPageContainerContent extends Component { hideSubtitle, identiconAddress, nonce, + assetImage, summaryComponent, detailsComponent, dataComponent, @@ -85,6 +87,7 @@ export default class ConfirmPageContainerContent extends Component { hideSubtitle={hideSubtitle} identiconAddress={identiconAddress} nonce={nonce} + assetImage={assetImage} /> ) } diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js index 70ebdeb20..4965d7b4e 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js @@ -11,7 +11,9 @@ const ConfirmPageContainerError = (props, context) => { src="/images/alert-red.svg" className="confirm-page-container-error__icon" /> - { `ALERT: ${error}` } +
+ { `ALERT: ${error}` } +
) } diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss index e99b0f631..89ff25578 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss @@ -1,5 +1,5 @@ .confirm-page-container-error { - height: 32px; + min-height: 32px; border: 1px solid $monzo; color: $monzo; background: lighten($monzo, 56%); @@ -8,10 +8,14 @@ display: flex; justify-content: flex-start; align-items: center; - padding-left: 16px; + padding: 8px 16px; &__icon { margin-right: 8px; flex: 0 0 auto; } + + &__text { + overflow: auto; + } } 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 3b1ee62c5..38b158fd3 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,7 @@ import classnames from 'classnames' import Identicon from '../../../identicon' const ConfirmPageContainerSummary = props => { - const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props + const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce, assetImage } = props return (
@@ -27,6 +27,7 @@ const ConfirmPageContainerSummary = props => { className="confirm-page-container-summary__identicon" diameter={36} address={identiconAddress} + image={assetImage} /> ) } @@ -51,6 +52,7 @@ ConfirmPageContainerSummary.propTypes = { className: PropTypes.string, identiconAddress: PropTypes.string, nonce: PropTypes.string, + assetImage: PropTypes.string, } export default ConfirmPageContainerSummary 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 24ff05353..b1582051e 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 @@ -38,6 +38,7 @@ export default class ConfirmPageContainer extends Component { detailsComponent: PropTypes.node, identiconAddress: PropTypes.string, nonce: PropTypes.string, + assetImage: PropTypes.string, summaryComponent: PropTypes.node, warning: PropTypes.string, // Footer @@ -70,8 +71,10 @@ export default class ConfirmPageContainer extends Component { onSubmit, identiconAddress, nonce, + assetImage, warning, } = this.props + const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress) return (
@@ -84,6 +87,7 @@ export default class ConfirmPageContainer extends Component { senderAddress={fromAddress} recipientName={toName} recipientAddress={toAddress} + assetImage={renderAssetImage ? assetImage : undefined} /> { @@ -101,6 +105,7 @@ export default class ConfirmPageContainer extends Component { errorKey={errorKey} identiconAddress={identiconAddress} nonce={nonce} + assetImage={assetImage} warning={warning} /> ) diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/currency-display/currency-display.component.js new file mode 100644 index 000000000..389791b42 --- /dev/null +++ b/ui/app/components/currency-display/currency-display.component.js @@ -0,0 +1,26 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { ETH } from '../../constants/common' + +export default class CurrencyDisplay extends PureComponent { + static propTypes = { + className: PropTypes.string, + displayValue: PropTypes.string, + prefix: PropTypes.string, + currency: PropTypes.oneOf([ETH]), + } + + render () { + const { className, displayValue, prefix } = this.props + const text = `${prefix || ''}${displayValue}` + + return ( +
+ { text } +
+ ) + } +} diff --git a/ui/app/components/currency-display/currency-display.container.js b/ui/app/components/currency-display/currency-display.container.js new file mode 100644 index 000000000..b8a738c65 --- /dev/null +++ b/ui/app/components/currency-display/currency-display.container.js @@ -0,0 +1,19 @@ +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 } = ownProps + const { metamask: { currentCurrency, conversionRate } } = state + + const toCurrency = currency || currentCurrency + const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals }) + const formattedValue = formatCurrency(convertedValue, toCurrency) + const displayValue = `${formattedValue} ${toCurrency.toUpperCase()}` + + return { + displayValue, + } +} + +export default connect(mapStateToProps)(CurrencyDisplay) diff --git a/ui/app/components/currency-display/index.js b/ui/app/components/currency-display/index.js new file mode 100644 index 000000000..38f08765f --- /dev/null +++ b/ui/app/components/currency-display/index.js @@ -0,0 +1 @@ +export { default } from './currency-display.container' diff --git a/ui/app/components/currency-display/tests/currency-display.component.test.js b/ui/app/components/currency-display/tests/currency-display.component.test.js new file mode 100644 index 000000000..d9ef052f1 --- /dev/null +++ b/ui/app/components/currency-display/tests/currency-display.component.test.js @@ -0,0 +1,27 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import CurrencyDisplay from '../currency-display.component' + +describe('CurrencyDisplay Component', () => { + it('should render text with a className', () => { + const wrapper = shallow() + + assert.ok(wrapper.hasClass('currency-display')) + assert.equal(wrapper.text(), '$123.45') + }) + + it('should render text with a prefix', () => { + const wrapper = shallow() + + assert.ok(wrapper.hasClass('currency-display')) + assert.equal(wrapper.text(), '-$123.45') + }) +}) 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 new file mode 100644 index 000000000..474ce5378 --- /dev/null +++ b/ui/app/components/currency-display/tests/currency-display.container.test.js @@ -0,0 +1,61 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../currency-display.container.js', { + 'react-redux': { + connect: ms => { + mapStateToProps = ms + return () => ({}) + }, + }, +}) + +describe('CurrencyDisplay container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + conversionRate: 280.45, + currentCurrency: 'usd', + }, + } + + const tests = [ + { + props: { + value: '0x2386f26fc10000', + numberOfDecimals: 2, + currency: 'usd', + }, + result: { + displayValue: '$2.80 USD', + }, + }, + { + props: { + value: '0x2386f26fc10000', + }, + result: { + displayValue: '$2.80 USD', + }, + }, + { + props: { + value: '0x1193461d01595930', + currency: 'ETH', + numberOfDecimals: 3, + }, + result: { + displayValue: '1.266 ETH', + }, + }, + ] + + tests.forEach(({ props, result }) => { + assert.deepEqual(mapStateToProps(mockState, props), result) + }) + }) + }) +}) diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js deleted file mode 100644 index a4c525396..000000000 --- a/ui/app/components/custom-radio-list.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RadioList - -inherits(RadioList, Component) -function RadioList () { - Component.call(this) -} - -RadioList.prototype.render = function () { - const props = this.props - const activeClass = '.custom-radio-selected' - const inactiveClass = '.custom-radio-inactive' - const { - labels, - defaultFocus, - } = props - - - return ( - h('.flex-row', { - style: { - fontSize: '12px', - }, - }, [ - h('.flex-column.custom-radios', { - style: { - marginRight: '5px', - }, - }, - labels.map((lable, i) => { - let isSelcted = (this.state !== null) - isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) - return h(isSelcted ? activeClass : inactiveClass, { - title: lable, - onClick: (event) => { - this.setState({selected: event.target.title}) - props.onClick(event) - }, - }) - }) - ), - h('.text', {}, - labels.map((lable) => { - if (props.subtext) { - return h('.flex-row', {}, [ - h('.radio-titles', lable), - h('.radio-titles-subtext', `- ${props.subtext[lable]}`), - ]) - } else { - return h('.radio-titles', lable) - } - }) - ), - ]) - ) -} - diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index 179b6617f..b497f5c09 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -459,7 +459,7 @@ const mapDispatchToProps = (dispatch) => { function mapStateToProps (state) { return { keyrings: state.metamask.keyrings, - sidebarOpen: state.appState.sidebarOpen, + sidebarOpen: state.appState.sidebar.isOpen, } } diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js index e5363ff56..63a30dd82 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -272,10 +272,12 @@ NetworkDropdown.prototype.getNetworkName = function () { NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { const props = this.props - const rpcTarget = provider.rpcTarget + const reversedRpcList = rpcList.slice().reverse() - return rpcList.map((rpc) => { - if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { + return reversedRpcList.map((rpc) => { + const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget + + if ((rpc === 'http://localhost:8545') || currentRpcTarget) { return null } else { return h( @@ -291,11 +293,11 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { }, }, [ - rpcTarget === rpc ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + currentRpcTarget ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), h('i.fa.fa-question-circle.fa-med.menu-icon-circle'), h('span.network-name-item', { style: { - color: rpcTarget === rpc ? '#ffffff' : '#9b9b9b', + color: currentRpcTarget ? '#ffffff' : '#9b9b9b', }, }, rpc), ] diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index 424048745..076e65b81 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -26,36 +26,42 @@ function mapStateToProps (state) { IdenticonComponent.prototype.render = function () { var props = this.props - const { className = '', address } = props + const { className = '', address, image } = props var diameter = props.diameter || this.defaultDiameter - - return address - ? ( - h('div', { - className: `${className} identicon`, - key: 'identicon-' + address, - style: { - display: 'flex', - flexShrink: 0, - alignItems: 'center', - justifyContent: 'center', - height: diameter, - width: diameter, - borderRadius: diameter / 2, - overflow: 'hidden', - }, - }) - ) - : ( - h('img.balance-icon', { - src: './images/eth_logo.svg', - style: { - height: diameter, - width: diameter, - borderRadius: diameter / 2, - }, - }) - ) + const style = { + height: diameter, + width: diameter, + borderRadius: diameter / 2, + } + if (image) { + return h('img', { + className: `${className} identicon`, + src: image, + style: { + ...style, + }, + }) + } else if (address) { + return h('div', { + className: `${className} identicon`, + key: 'identicon-' + address, + style: { + display: 'flex', + flexShrink: 0, + alignItems: 'center', + justifyContent: 'center', + ...style, + overflow: 'hidden', + }, + }) + } else { + return h('img.balance-icon', { + src: './images/eth_logo.svg', + style: { + ...style, + }, + }) + } } IdenticonComponent.prototype.componentDidMount = function () { diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index 35d38e2a3..cb4065fd9 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -1,23 +1,39 @@ +@import './app-header/index'; + @import './button-group/index'; -@import './export-text-container/index'; +@import './confirm-page-container/index'; -@import './selected-account/index'; +@import './export-text-container/index'; @import './info-box/index'; -@import './network-display/index'; +@import './menu-bar/index'; -@import './confirm-page-container/index'; +@import './modals/index'; + +@import './network-display/index'; @import './page-container/index'; @import './pages/index'; -@import './modals/index'; +@import './selected-account/index'; @import './sender-to-recipient/index'; @import './tabs/index'; +@import './transaction-view/index'; + +@import './transaction-view-balance/index'; + +@import './transaction-list/index'; + +@import './transaction-list-item/index'; + +@import './transaction-status/index'; + @import './app-header/index'; + +@import './sidebars/index'; diff --git a/ui/app/components/menu-bar/index.js b/ui/app/components/menu-bar/index.js new file mode 100644 index 000000000..c5760847f --- /dev/null +++ b/ui/app/components/menu-bar/index.js @@ -0,0 +1 @@ +export { default } from './menu-bar.container' diff --git a/ui/app/components/menu-bar/index.scss b/ui/app/components/menu-bar/index.scss new file mode 100644 index 000000000..f699f4090 --- /dev/null +++ b/ui/app/components/menu-bar/index.scss @@ -0,0 +1,23 @@ +.menu-bar { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + flex: 0 0 auto; + margin-bottom: 16px; + padding: 5px; + border-bottom: 1px solid #e5e5e5; + + &__sidebar-button { + font-size: 1.25rem; + cursor: pointer; + padding: 10px; + } + + &__open-in-browser { + cursor: pointer; + display: flex; + justify-content: center; + padding: 10px; + } +} diff --git a/ui/app/components/menu-bar/menu-bar.component.js b/ui/app/components/menu-bar/menu-bar.component.js new file mode 100644 index 000000000..eee9feebb --- /dev/null +++ b/ui/app/components/menu-bar/menu-bar.component.js @@ -0,0 +1,52 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Tooltip from '../tooltip' +import SelectedAccount from '../selected-account' + +export default class MenuBar extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + hideSidebar: PropTypes.func, + isMascara: PropTypes.bool, + sidebarOpen: PropTypes.bool, + showSidebar: PropTypes.func, + } + + render () { + const { t } = this.context + const { isMascara, sidebarOpen, hideSidebar, showSidebar } = this.props + + return ( +
+ +
sidebarOpen ? hideSidebar() : showSidebar()} + /> + + + { + !isMascara && ( + +
global.platform.openExtensionInBrowser()} + > + +
+
+ ) + } +
+ ) + } +} diff --git a/ui/app/components/menu-bar/menu-bar.container.js b/ui/app/components/menu-bar/menu-bar.container.js new file mode 100644 index 000000000..ae32882ae --- /dev/null +++ b/ui/app/components/menu-bar/menu-bar.container.js @@ -0,0 +1,26 @@ +import { connect } from 'react-redux' +import MenuBar from './menu-bar.component' +import { showSidebar, hideSidebar } from '../../actions' + +const mapStateToProps = state => { + const { appState: { sidebar: { isOpen }, isMascara } } = state + + return { + sidebarOpen: isOpen, + isMascara, + } +} + +const mapDispatchToProps = dispatch => { + return { + showSidebar: () => { + dispatch(showSidebar({ + transitionName: 'sidebar-right', + type: 'wallet-view', + })) + }, + hideSidebar: () => dispatch(hideSidebar()), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(MenuBar) diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js index 8e252787a..0bbabf38d 100644 --- a/ui/app/components/modals/account-details-modal.js +++ b/ui/app/components/modals/account-details-modal.js @@ -61,7 +61,7 @@ AccountDetailsModal.prototype.render = function () { let exportPrivateKeyFeatureEnabled = true // This feature is disabled for hardware wallets - if (keyring.type.search('Hardware') !== -1) { + if (keyring && keyring.type.search('Hardware') !== -1) { exportPrivateKeyFeatureEnabled = false } diff --git a/ui/app/components/modals/account-modal-container.js b/ui/app/components/modals/account-modal-container.js index a9856b20f..aa0593df8 100644 --- a/ui/app/components/modals/account-modal-container.js +++ b/ui/app/components/modals/account-modal-container.js @@ -7,9 +7,9 @@ const actions = require('../../actions') const { getSelectedIdentity } = require('../../selectors') const Identicon = require('../identicon') -function mapStateToProps (state) { +function mapStateToProps (state, ownProps) { return { - selectedIdentity: getSelectedIdentity(state), + selectedIdentity: ownProps.selectedIdentity || getSelectedIdentity(state), } } diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js index 80ece425f..60a416304 100644 --- a/ui/app/components/modals/export-private-key-modal.js +++ b/ui/app/components/modals/export-private-key-modal.js @@ -1,3 +1,4 @@ +const log = require('loglevel') const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') @@ -11,19 +12,33 @@ const ReadOnlyInput = require('../readonly-input') const copyToClipboard = require('copy-to-clipboard') const { checksumAddress } = require('../../util') -function mapStateToProps (state) { - return { - warning: state.appState.warning, - privateKey: state.appState.accountDetail.privateKey, - network: state.metamask.network, - selectedIdentity: getSelectedIdentity(state), - previousModalState: state.appState.modal.previousModalState.name, +function mapStateToPropsFactory () { + let selectedIdentity = null + return function mapStateToProps (state) { + // We should **not** change the identity displayed here even if it changes from underneath us. + // If we do, we will be showing the user one private key and a **different** address and name. + // Note that the selected identity **will** change from underneath us when we unlock the keyring + // which is the expected behavior that we are side-stepping. + selectedIdentity = selectedIdentity || getSelectedIdentity(state) + return { + warning: state.appState.warning, + privateKey: state.appState.accountDetail.privateKey, + network: state.metamask.network, + selectedIdentity, + previousModalState: state.appState.modal.previousModalState.name, + } } } function mapDispatchToProps (dispatch) { return { - exportAccount: (password, address) => dispatch(actions.exportAccount(password, address)), + exportAccount: (password, address) => { + return dispatch(actions.exportAccount(password, address)) + .then((res) => { + dispatch(actions.hideWarning()) + return res + }) + }, showAccountDetailModal: () => dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })), hideModal: () => dispatch(actions.hideModal()), } @@ -36,6 +51,7 @@ function ExportPrivateKeyModal () { this.state = { password: '', privateKey: null, + showWarning: true, } } @@ -43,14 +59,18 @@ ExportPrivateKeyModal.contextTypes = { t: PropTypes.func, } -module.exports = connect(mapStateToProps, mapDispatchToProps)(ExportPrivateKeyModal) +module.exports = connect(mapStateToPropsFactory, mapDispatchToProps)(ExportPrivateKeyModal) ExportPrivateKeyModal.prototype.exportAccountAndGetPrivateKey = function (password, address) { const { exportAccount } = this.props exportAccount(password, address) - .then(privateKey => this.setState({ privateKey })) + .then(privateKey => this.setState({ + privateKey, + showWarning: false, + })) + .catch((e) => log.error(e)) } ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) { @@ -110,9 +130,13 @@ ExportPrivateKeyModal.prototype.render = function () { } = this.props const { name, address } = selectedIdentity - const { privateKey } = this.state + const { + privateKey, + showWarning, + } = this.state return h(AccountModalContainer, { + selectedIdentity, showBackButton: previousModalState === 'ACCOUNT_DETAILS', backButtonAction: () => showAccountDetailModal(), }, [ @@ -134,7 +158,7 @@ ExportPrivateKeyModal.prototype.render = function () { this.renderPasswordInput(privateKey), - !warning ? null : h('span.private-key-password-error', warning), + showWarning && warning ? h('span.private-key-password-error', warning) : null, ]), h('div.private-key-password-warning', this.context.t('privateKeyWarning')), diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/modals/hide-token-confirmation-modal.js index 1518fa9a0..fb38516d3 100644 --- a/ui/app/components/modals/hide-token-confirmation-modal.js +++ b/ui/app/components/modals/hide-token-confirmation-modal.js @@ -10,6 +10,7 @@ function mapStateToProps (state) { return { network: state.metamask.network, token: state.appState.modal.modalState.props.token, + assetImages: state.metamask.assetImages, } } @@ -40,8 +41,9 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(HideTokenConfirmat HideTokenConfirmationModal.prototype.render = function () { - const { token, network, hideToken, hideModal } = this.props + const { token, network, hideToken, hideModal, assetImages } = this.props const { symbol, address } = token + const image = assetImages[address] return h('div.hide-token-confirmation', {}, [ h('div.hide-token-confirmation__container', { @@ -55,6 +57,7 @@ HideTokenConfirmationModal.prototype.render = function () { diameter: 45, address, network, + image, }), h('div.hide-token-confirmation__symbol', {}, symbol), diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss index 06c3ef709..14cdbacd3 100644 --- a/ui/app/components/page-container/index.scss +++ b/ui/app/components/page-container/index.scss @@ -109,7 +109,7 @@ &--selected { color: $curious-blue; - border-bottom: 3px solid $curious-blue; + border-bottom: 2px solid $curious-blue; } } diff --git a/ui/app/components/page-container/page-container-header/page-container-header.component.js b/ui/app/components/page-container/page-container-header/page-container-header.component.js index 5a5de1e5a..a8458604e 100644 --- a/ui/app/components/page-container/page-container-header/page-container-header.component.js +++ b/ui/app/components/page-container/page-container-header/page-container-header.component.js @@ -1,8 +1,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' +import classnames from 'classnames' export default class PageContainerHeader extends Component { - static propTypes = { title: PropTypes.string, subtitle: PropTypes.string, @@ -11,8 +11,18 @@ export default class PageContainerHeader extends Component { onBackButtonClick: PropTypes.func, backButtonStyles: PropTypes.object, backButtonString: PropTypes.string, - children: PropTypes.node, - }; + tabs: PropTypes.node, + } + + renderTabs () { + const { tabs } = this.props + + return tabs && ( +
    + { tabs } +
+ ) + } renderHeaderRow () { const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props @@ -31,15 +41,18 @@ export default class PageContainerHeader extends Component { } render () { - const { title, subtitle, onClose, children } = this.props + const { title, subtitle, onClose, tabs } = this.props return ( -
+
{ this.renderHeaderRow() } - { children } - { title &&
{ title } @@ -59,6 +72,7 @@ export default class PageContainerHeader extends Component { /> } + { this.renderTabs() }
) } diff --git a/ui/app/components/page-container/page-container.component.js b/ui/app/components/page-container/page-container.component.js index 9bfb99ade..3a2274a29 100644 --- a/ui/app/components/page-container/page-container.component.js +++ b/ui/app/components/page-container/page-container.component.js @@ -1,30 +1,82 @@ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import PageContainerHeader from './page-container-header' import PageContainerFooter from './page-container-footer' -export default class PageContainer extends Component { - +export default class PageContainer extends PureComponent { static propTypes = { // PageContainerHeader props - title: PropTypes.string.isRequired, - subtitle: PropTypes.string, + backButtonString: PropTypes.string, + backButtonStyles: PropTypes.object, + onBackButtonClick: PropTypes.func, onClose: PropTypes.func, showBackButton: PropTypes.bool, - onBackButtonClick: PropTypes.func, - backButtonStyles: PropTypes.object, - backButtonString: PropTypes.string, + subtitle: PropTypes.string, + title: PropTypes.string.isRequired, + // Tabs-related props + defaultActiveTabIndex: PropTypes.number, + tabsComponent: PropTypes.node, // Content props - ContentComponent: PropTypes.func, - contentComponentProps: PropTypes.object, + contentComponent: PropTypes.node, // PageContainerFooter props - onCancel: PropTypes.func, cancelText: PropTypes.string, + disabled: PropTypes.bool, + onCancel: PropTypes.func, onSubmit: PropTypes.func, submitText: PropTypes.string, - disabled: PropTypes.bool, - }; + } + + state = { + activeTabIndex: this.props.defaultActiveTabIndex || 0, + } + + handleTabClick (activeTabIndex) { + this.setState({ activeTabIndex }) + } + + renderTabs () { + const { tabsComponent } = this.props + + if (!tabsComponent) { + return + } + + const numberOfTabs = React.Children.count(tabsComponent.props.children) + + return React.Children.map(tabsComponent.props.children, (child, tabIndex) => { + return child && React.cloneElement(child, { + onClick: index => this.handleTabClick(index), + tabIndex, + isActive: numberOfTabs > 1 && tabIndex === this.state.activeTabIndex, + key: tabIndex, + className: 'page-container__tab', + activeClassName: 'page-container__tab--selected', + }) + }) + } + + renderActiveTabContent () { + const { tabsComponent } = this.props + const { children } = tabsComponent.props + const { activeTabIndex } = this.state + + return children[activeTabIndex] + ? children[activeTabIndex].props.children + : children.props.children + } + + renderContent () { + const { contentComponent, tabsComponent } = this.props + + if (contentComponent) { + return contentComponent + } else if (tabsComponent) { + return this.renderActiveTabContent() + } else { + return null + } + } render () { const { @@ -35,8 +87,6 @@ export default class PageContainer extends Component { onBackButtonClick, backButtonStyles, backButtonString, - ContentComponent, - contentComponentProps, onCancel, cancelText, onSubmit, @@ -54,9 +104,10 @@ export default class PageContainer extends Component { onBackButtonClick={onBackButtonClick} backButtonStyles={backButtonStyles} backButtonString={backButtonString} + tabs={this.renderTabs()} />
- + { this.renderContent() }
) } - } diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js index bcb93d401..3612e676c 100644 --- a/ui/app/components/pages/add-token/add-token.component.js +++ b/ui/app/components/pages/add-token/add-token.component.js @@ -1,14 +1,14 @@ import React, { Component } from 'react' -import classnames from 'classnames' import PropTypes from 'prop-types' import ethUtil from 'ethereumjs-util' import { checkExistingAddresses } from './util' import { tokenInfoGetter } from '../../../token-util' import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes' -import Button from '../../button' import TextField from '../../text-field' import TokenList from './token-list' import TokenSearch from './token-search' +import PageContainer from '../../page-container' +import { Tabs, Tab } from '../../tabs' const emptyAddr = '0x0000000000000000000000000000000000000000' const SEARCH_TAB = 'SEARCH' @@ -206,7 +206,7 @@ class AddToken extends Component { const validDecimals = customDecimals !== null && customDecimals !== '' && customDecimals >= 0 && - customDecimals < 36 + customDecimals <= 36 let customDecimalsError = null if (!validDecimals) { @@ -285,65 +285,33 @@ class AddToken extends Component { ) } + renderTabs () { + return ( + + + { this.renderSearchToken() } + + + { this.renderCustomTokenForm() } + + + ) + } + render () { - const { displayedTab } = this.state const { history, clearPendingTokens } = this.props return ( -
-
-
- { this.context.t('addTokens') } -
-
-
this.setState({ displayedTab: SEARCH_TAB })} - > - { this.context.t('search') } -
-
this.setState({ displayedTab: CUSTOM_TOKEN_TAB })} - > - { this.context.t('customToken') } -
-
-
-
- { - displayedTab === CUSTOM_TOKEN_TAB - ? this.renderCustomTokenForm() - : this.renderSearchToken() - } -
-
- - -
-
+ this.handleNext()} + disabled={this.hasError() || !this.hasSelected()} + onCancel={() => { + clearPendingTokens() + history.push(DEFAULT_ROUTE) + }} + /> ) } } diff --git a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js new file mode 100644 index 000000000..c24e1e0ea --- /dev/null +++ b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js @@ -0,0 +1,126 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { DEFAULT_ROUTE } from '../../../routes' +import Button from '../../button' +import Identicon from '../../../components/identicon' +import TokenBalance from '../../token-balance' + +export default class ConfirmAddSuggestedToken extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + history: PropTypes.object, + clearPendingTokens: PropTypes.func, + addToken: PropTypes.func, + pendingTokens: PropTypes.object, + removeSuggestedTokens: PropTypes.func, + } + + componentDidMount () { + const { pendingTokens = {}, history } = this.props + + if (Object.keys(pendingTokens).length === 0) { + history.push(DEFAULT_ROUTE) + } + } + + getTokenName (name, symbol) { + return typeof name === 'undefined' + ? symbol + : `${name} (${symbol})` + } + + render () { + const { addToken, pendingTokens, removeSuggestedTokens, history } = this.props + const pendingTokenKey = Object.keys(pendingTokens)[0] + const pendingToken = pendingTokens[pendingTokenKey] + + return ( +
+
+
+ { this.context.t('addSuggestedTokens') } +
+
+ { this.context.t('likeToAddTokens') } +
+
+
+
+
+
+ { this.context.t('token') } +
+
+ { this.context.t('balance') } +
+
+
+ { + Object.entries(pendingTokens) + .map(([ address, token ]) => { + const { name, symbol, image } = token + + return ( +
+
+ +
+ { this.getTokenName(name, symbol) } +
+
+
+ +
+
+ ) + }) + } +
+
+
+
+ + +
+
+ ) + } +} diff --git a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js new file mode 100644 index 000000000..1f2737e52 --- /dev/null +++ b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js @@ -0,0 +1,29 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component' +import { withRouter } from 'react-router-dom' + +const extend = require('xtend') + +const { addToken, removeSuggestedTokens } = require('../../../actions') + +const mapStateToProps = ({ metamask }) => { + const { pendingTokens, suggestedTokens } = metamask + const params = extend(pendingTokens, suggestedTokens) + + return { + pendingTokens: params, + } +} + +const mapDispatchToProps = dispatch => { + return { + addToken: ({address, symbol, decimals, image}) => dispatch(addToken(address, symbol, decimals, image)), + removeSuggestedTokens: () => dispatch(removeSuggestedTokens()), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(ConfirmAddSuggestedToken) diff --git a/ui/app/components/pages/confirm-add-suggested-token/index.js b/ui/app/components/pages/confirm-add-suggested-token/index.js new file mode 100644 index 000000000..2ca56b43c --- /dev/null +++ b/ui/app/components/pages/confirm-add-suggested-token/index.js @@ -0,0 +1,2 @@ +import ConfirmAddSuggestedToken from './confirm-add-suggested-token.container' +module.exports = ConfirmAddSuggestedToken diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js index 65d654b92..3dcc8cda9 100644 --- a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js +++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js @@ -2,8 +2,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes' import Button from '../../button' -import Identicon from '../../../components/identicon' -import TokenBalance from './token-balance' +import Identicon from '../../identicon' +import TokenBalance from '../../token-balance' export default class ConfirmAddToken extends Component { static contextTypes = { diff --git a/ui/app/components/pages/confirm-add-token/token-balance/index.js b/ui/app/components/pages/confirm-add-token/token-balance/index.js deleted file mode 100644 index 6fb5c8223..000000000 --- a/ui/app/components/pages/confirm-add-token/token-balance/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import TokenBalance from './token-balance.container' -module.exports = TokenBalance diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js deleted file mode 100644 index 976788d4c..000000000 --- a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js +++ /dev/null @@ -1,16 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' - -export default class TokenBalance extends Component { - static propTypes = { - string: PropTypes.string, - symbol: PropTypes.string, - error: PropTypes.string, - } - - render () { - return ( -
{ this.props.string }
- ) - } -} 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 961aa304e..56cfbccc8 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 @@ -38,6 +38,7 @@ export default class ConfirmTransactionBase extends Component { isTxReprice: PropTypes.bool, methodData: PropTypes.object, nonce: PropTypes.string, + assetImage: PropTypes.string, sendTransaction: PropTypes.func, showCustomizeGasModal: PropTypes.func, showTransactionConfirmedModal: PropTypes.func, @@ -73,6 +74,7 @@ export default class ConfirmTransactionBase extends Component { state = { submitting: false, + submitError: null, } componentDidUpdate () { @@ -268,7 +270,7 @@ export default class ConfirmTransactionBase extends Component { return } - this.setState({ submitting: true }) + this.setState({ submitting: true, submitError: null }) if (onSubmit) { Promise.resolve(onSubmit(txData)) @@ -280,7 +282,9 @@ export default class ConfirmTransactionBase extends Component { this.setState({ submitting: false }) history.push(DEFAULT_ROUTE) }) - .catch(() => this.setState({ submitting: false })) + .catch(error => { + this.setState({ submitting: false, submitError: error.message }) + }) } } @@ -307,9 +311,10 @@ export default class ConfirmTransactionBase extends Component { contentComponent, onEdit, nonce, + assetImage, warning, } = this.props - const { submitting } = this.state + const { submitting, submitError } = this.state const { name } = methodData const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency) @@ -331,8 +336,9 @@ export default class ConfirmTransactionBase extends Component { dataComponent={this.renderData()} contentComponent={contentComponent} nonce={nonce} + assetImage={assetImage} identiconAddress={identiconAddress} - errorMessage={errorMessage} + errorMessage={errorMessage || submitError} errorKey={propsErrorKey || errorKey} warning={warning} disabled={!propsValid || !valid || submitting} 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 0c0deff18..8f54c8040 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 @@ -52,8 +52,9 @@ const mapStateToProps = (state, props) => { accounts, selectedAddress, selectedAddressTxList, + assetImages, } = metamask - + const assetImage = assetImages[txParamsToAddress] const { balance } = accounts[selectedAddress] const { name: fromName } = identities[selectedAddress] const toAddress = propsToAddress || txParamsToAddress @@ -88,6 +89,7 @@ const mapStateToProps = (state, props) => { conversionRate, transactionStatus, nonce, + assetImage, } } diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js index 0280f73c6..2c44b6094 100644 --- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js +++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js @@ -12,25 +12,27 @@ import { CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, } from '../../../routes' -import { isConfirmDeployContract } from './confirm-transaction-switch.util' +import { isConfirmDeployContract } from '../../../helpers/transactions.util' import { TOKEN_METHOD_TRANSFER, TOKEN_METHOD_APPROVE, TOKEN_METHOD_TRANSFER_FROM, -} from './confirm-transaction-switch.constants' +} from '../../../constants/transactions' export default class ConfirmTransactionSwitch extends Component { static propTypes = { txData: PropTypes.object, methodData: PropTypes.object, - fetchingMethodData: PropTypes.bool, + fetchingData: PropTypes.bool, + isEtherTransaction: PropTypes.bool, } redirectToTransaction () { const { txData, methodData: { name }, - fetchingMethodData, + fetchingData, + isEtherTransaction, } = this.props const { id, txParams: { data } = {} } = txData @@ -39,10 +41,15 @@ export default class ConfirmTransactionSwitch extends Component { return } - if (fetchingMethodData) { + if (fetchingData) { return } + if (isEtherTransaction) { + const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}` + return + } + if (data) { const methodName = name && name.toLowerCase() diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js deleted file mode 100644 index 9db4a2f96..000000000 --- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const TOKEN_METHOD_TRANSFER = 'transfer' -export const TOKEN_METHOD_APPROVE = 'approve' -export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom' diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js index 3d7fc78cc..7f2c36af2 100644 --- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js +++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js @@ -6,14 +6,16 @@ const mapStateToProps = state => { confirmTransaction: { txData, methodData, - fetchingMethodData, + fetchingData, + toSmartContract, }, } = state return { txData, methodData, - fetchingMethodData, + fetchingData, + isEtherTransaction: !toSmartContract, } } diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js deleted file mode 100644 index 5e3fdc9af..000000000 --- a/ui/app/components/pages/home.js +++ /dev/null @@ -1,239 +0,0 @@ -const { Component } = require('react') -const { connect } = require('react-redux') -const PropTypes = require('prop-types') -const { Redirect, withRouter } = require('react-router-dom') -const { compose } = require('recompose') -const h = require('react-hyperscript') -const actions = require('../../actions') -const log = require('loglevel') - -// init -const NewKeyChainScreen = require('../../new-keychain') -// mascara -const MascaraBuyEtherScreen = require('../../../../mascara/src/app/first-time/buy-ether-screen').default - -// accounts -const MainContainer = require('../../main-container') - -// other views -const BuyView = require('../../components/buy-button-subview') -const QrView = require('../../components/qr-code') - -// Routes -const { - INITIALIZE_BACKUP_PHRASE_ROUTE, - RESTORE_VAULT_ROUTE, - CONFIRM_TRANSACTION_ROUTE, - NOTICE_ROUTE, -} = require('../../routes') - -const { unconfirmedTransactionsCountSelector } = require('../../selectors/confirm-transaction') - -class Home extends Component { - componentDidMount () { - const { - history, - unconfirmedTransactionsCount = 0, - } = this.props - - // unapprovedTxs and unapproved messages - if (unconfirmedTransactionsCount > 0) { - history.push(CONFIRM_TRANSACTION_ROUTE) - } - } - - render () { - log.debug('rendering primary') - const { - noActiveNotices, - lostAccounts, - forgottenPassword, - currentView, - activeAddress, - seedWords, - } = this.props - - // notices - if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) { - return h(Redirect, { - to: { - pathname: NOTICE_ROUTE, - }, - }) - } - - // seed words - if (seedWords) { - log.debug('rendering seed words') - return h(Redirect, { - to: { - pathname: INITIALIZE_BACKUP_PHRASE_ROUTE, - }, - }) - } - - if (forgottenPassword) { - log.debug('rendering restore vault screen') - return h(Redirect, { - to: { - pathname: RESTORE_VAULT_ROUTE, - }, - }) - } - - // show current view - switch (currentView.name) { - - case 'accountDetail': - log.debug('rendering main container') - return h(MainContainer, {key: 'account-detail'}) - - case 'newKeychain': - log.debug('rendering new keychain screen') - return h(NewKeyChainScreen, {key: 'new-keychain'}) - - case 'buyEth': - log.debug('rendering buy ether screen') - return h(BuyView, {key: 'buyEthView'}) - - case 'onboardingBuyEth': - log.debug('rendering onboarding buy ether screen') - return h(MascaraBuyEtherScreen, {key: 'buyEthView'}) - - case 'qr': - log.debug('rendering show qr screen') - return h('div', { - style: { - position: 'absolute', - height: '100%', - top: '0px', - left: '0px', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(activeAddress)), - style: { - marginLeft: '10px', - marginTop: '50px', - }, - }), - h('div', { - style: { - position: 'absolute', - left: '44px', - width: '285px', - }, - }, [ - h(QrView, {key: 'qr'}), - ]), - ]) - - default: - log.debug('rendering default, account detail screen') - return h(MainContainer, {key: 'account-detail'}) - } - } -} - -Home.propTypes = { - currentCurrency: PropTypes.string, - isLoading: PropTypes.bool, - loadingMessage: PropTypes.string, - network: PropTypes.string, - provider: PropTypes.object, - frequentRpcList: PropTypes.array, - currentView: PropTypes.object, - sidebarOpen: PropTypes.bool, - isMascara: PropTypes.bool, - isOnboarding: PropTypes.bool, - isUnlocked: PropTypes.bool, - networkDropdownOpen: PropTypes.bool, - history: PropTypes.object, - dispatch: PropTypes.func, - selectedAddress: PropTypes.string, - noActiveNotices: PropTypes.bool, - lostAccounts: PropTypes.array, - isInitialized: PropTypes.bool, - forgottenPassword: PropTypes.bool, - activeAddress: PropTypes.string, - unapprovedTxs: PropTypes.object, - seedWords: PropTypes.string, - unapprovedMsgCount: PropTypes.number, - unapprovedPersonalMsgCount: PropTypes.number, - unapprovedTypedMessagesCount: PropTypes.number, - welcomeScreenSeen: PropTypes.bool, - isPopup: PropTypes.bool, - isMouseUser: PropTypes.bool, - t: PropTypes.func, - unconfirmedTransactionsCount: PropTypes.number, -} - -function mapStateToProps (state) { - const { appState, metamask } = state - const { - networkDropdownOpen, - sidebarOpen, - isLoading, - loadingMessage, - } = appState - - const { - accounts, - address, - isInitialized, - noActiveNotices, - seedWords, - unapprovedTxs, - nextUnreadNotice, - lostAccounts, - unapprovedMsgCount, - unapprovedPersonalMsgCount, - unapprovedTypedMessagesCount, - } = metamask - const selected = address || Object.keys(accounts)[0] - - return { - // state from plugin - networkDropdownOpen, - sidebarOpen, - isLoading, - loadingMessage, - noActiveNotices, - isInitialized, - isUnlocked: state.metamask.isUnlocked, - selectedAddress: state.metamask.selectedAddress, - currentView: state.appState.currentView, - activeAddress: state.appState.activeAddress, - transForward: state.appState.transForward, - isMascara: state.metamask.isMascara, - isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized), - isPopup: state.metamask.isPopup, - seedWords: state.metamask.seedWords, - unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - unapprovedMsgCount, - unapprovedPersonalMsgCount, - unapprovedTypedMessagesCount, - menuOpen: state.appState.menuOpen, - network: state.metamask.network, - provider: state.metamask.provider, - forgottenPassword: state.appState.forgottenPassword, - nextUnreadNotice, - lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], - currentCurrency: state.metamask.currentCurrency, - isMouseUser: state.appState.isMouseUser, - isRevealingSeedWords: state.metamask.isRevealingSeedWords, - Qr: state.appState.Qr, - welcomeScreenSeen: state.metamask.welcomeScreenSeen, - - // state needed to get account dropdown temporarily rendering from app bar - selected, - unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), - } -} - -module.exports = compose( - withRouter, - connect(mapStateToProps) -)(Home) diff --git a/ui/app/components/pages/home/home.component.js b/ui/app/components/pages/home/home.component.js new file mode 100644 index 000000000..d3c71c4f6 --- /dev/null +++ b/ui/app/components/pages/home/home.component.js @@ -0,0 +1,77 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Media from 'react-media' +import { Redirect } from 'react-router-dom' +import WalletView from '../../wallet-view' +import TransactionView from '../../transaction-view' +import { + INITIALIZE_BACKUP_PHRASE_ROUTE, + RESTORE_VAULT_ROUTE, + CONFIRM_TRANSACTION_ROUTE, + NOTICE_ROUTE, + CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, +} from '../../../routes' + +export default class Home extends PureComponent { + static propTypes = { + history: PropTypes.object, + noActiveNotices: PropTypes.bool, + lostAccounts: PropTypes.array, + forgottenPassword: PropTypes.bool, + seedWords: PropTypes.string, + suggestedTokens: PropTypes.object, + unconfirmedTransactionsCount: PropTypes.number, + } + + componentDidMount () { + const { + history, + suggestedTokens = {}, + unconfirmedTransactionsCount = 0, + } = this.props + + // suggested new tokens + if (Object.keys(suggestedTokens).length > 0) { + history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE) + } + + if (unconfirmedTransactionsCount > 0) { + history.push(CONFIRM_TRANSACTION_ROUTE) + } + } + + render () { + const { + noActiveNotices, + lostAccounts, + forgottenPassword, + seedWords, + } = this.props + + // notices + if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) { + return + } + + // seed words + if (seedWords) { + return + } + + if (forgottenPassword) { + return + } + + return ( +
+
+ } + /> + +
+
+ ) + } +} diff --git a/ui/app/components/pages/home/home.container.js b/ui/app/components/pages/home/home.container.js new file mode 100644 index 000000000..58001df6b --- /dev/null +++ b/ui/app/components/pages/home/home.container.js @@ -0,0 +1,30 @@ +import Home from './home.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { unconfirmedTransactionsCountSelector } from '../../../selectors/confirm-transaction' + +const mapStateToProps = state => { + const { metamask, appState } = state + const { + noActiveNotices, + lostAccounts, + seedWords, + suggestedTokens, + } = metamask + const { forgottenPassword } = appState + + return { + noActiveNotices, + lostAccounts, + forgottenPassword, + seedWords, + suggestedTokens, + unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), + } +} + +export default compose( + withRouter, + connect(mapStateToProps) +)(Home) diff --git a/ui/app/components/pages/home/index.js b/ui/app/components/pages/home/index.js new file mode 100644 index 000000000..4474ba5b8 --- /dev/null +++ b/ui/app/components/pages/home/index.js @@ -0,0 +1 @@ +export { default } from './home.container' diff --git a/ui/app/components/pages/settings/settings.js b/ui/app/components/pages/settings/settings.js index ff42a13de..a5ea1b89c 100644 --- a/ui/app/components/pages/settings/settings.js +++ b/ui/app/components/pages/settings/settings.js @@ -66,6 +66,30 @@ class Settings extends Component { ]) } + renderHexDataOptIn () { + const { metamask: { featureFlags: { sendHexData } }, setHexDataFeatureFlag } = this.props + + return h('div.settings__content-row', [ + h('div.settings__content-item', [ + h('span', this.context.t('showHexData')), + h( + 'div.settings__content-description', + this.context.t('showHexDataDescription') + ), + ]), + h('div.settings__content-item', [ + h('div.settings__content-item-col', [ + h(ToggleButton, { + value: sendHexData, + onToggle: (value) => setHexDataFeatureFlag(!value), + activeLabel: '', + inactiveLabel: '', + }), + ]), + ]), + ]) + } + renderCurrentConversion () { const { metamask: { currentCurrency, conversionDate }, setCurrentCurrency } = this.props @@ -307,6 +331,7 @@ class Settings extends Component { !isMascara && this.renderOldUI(), this.renderResetAccount(), this.renderBlockieOptIn(), + this.renderHexDataOptIn(), ]) ) } @@ -315,6 +340,7 @@ class Settings extends Component { Settings.propTypes = { metamask: PropTypes.object, setUseBlockie: PropTypes.func, + setHexDataFeatureFlag: PropTypes.func, setCurrentCurrency: PropTypes.func, setRpcTarget: PropTypes.func, displayWarning: PropTypes.func, @@ -349,6 +375,9 @@ const mapDispatchToProps = dispatch => { setFeatureFlagToBeta: () => { return dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')) }, + setHexDataFeatureFlag: (featureFlagShowState) => { + return dispatch(actions.setFeatureFlag('sendHexData', featureFlagShowState)) + }, showResetAccountConfirmationModal: () => { return dispatch(actions.showModal({ name: 'CONFIRM_RESET_ACCOUNT' })) }, diff --git a/ui/app/components/pending-msg-details.js b/ui/app/components/pending-msg-details.js deleted file mode 100644 index f16fcb1c7..000000000 --- a/ui/app/components/pending-msg-details.js +++ /dev/null @@ -1,56 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect - -const AccountPanel = require('./account-panel') - -PendingMsgDetails.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect()(PendingMsgDetails) - - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-column.flex-space-between', [ - h('label.font-small.allcaps', this.context.t('message')), - h('span.font-small', msgParams.data), - ]), - ]), - - ]) - ) -} diff --git a/ui/app/components/pending-msg.js b/ui/app/components/pending-msg.js deleted file mode 100644 index 21a7864e4..000000000 --- a/ui/app/components/pending-msg.js +++ /dev/null @@ -1,73 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-msg-details') -const connect = require('react-redux').connect - -PendingMsg.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect()(PendingMsg) - - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - style: { - maxWidth: '350px', - }, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, this.context.t('signMessage')), - - h('.error', { - style: { - margin: '10px', - }, - }, [ - this.context.t('signNotice'), - h('a', { - href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527', - style: { color: 'rgb(247, 134, 28)' }, - onClick: (event) => { - event.preventDefault() - const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527' - global.platform.openWindow({ url }) - }, - }, this.context.t('readMore')), - ]), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelMessage, - }, this.context.t('cancel')), - h('button', { - onClick: state.signMessage, - }, this.context.t('sign')), - ]), - ]) - - ) -} diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js index df7bcb7cc..9e0ce9c23 100644 --- a/ui/app/components/send/send-content/send-content.component.js +++ b/ui/app/components/send/send-content/send-content.component.js @@ -12,6 +12,7 @@ export default class SendContent extends Component { static propTypes = { updateGas: PropTypes.func, scanQrCode: PropTypes.func, + showHexData: PropTypes.bool, }; render () { @@ -25,7 +26,7 @@ export default class SendContent extends Component { /> this.props.updateGas(updateData)} /> - + { this.props.showHexData ? : null }
) diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js index 1163dcffc..434db81e5 100644 --- a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js +++ b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js @@ -48,7 +48,7 @@ export default class SendToRow extends Component { return ( { - wrapper = shallow() + wrapper = shallow() }) describe('render', () => { @@ -33,6 +34,17 @@ describe('SendContent Component', function () { assert(PageContainerContentChild.childAt(1).is(SendToRow)) assert(PageContainerContentChild.childAt(2).is(SendAmountRow)) assert(PageContainerContentChild.childAt(3).is(SendGasRow)) + assert(PageContainerContentChild.childAt(4).is(SendHexDataRow)) + }) + + it('should not render the SendHexDataRow if props.showHexData is false', () => { + wrapper.setProps({ showHexData: false }) + const PageContainerContentChild = wrapper.find(PageContainerContent).children() + assert(PageContainerContentChild.childAt(0).is(SendFromRow)) + assert(PageContainerContentChild.childAt(1).is(SendToRow)) + assert(PageContainerContentChild.childAt(2).is(SendAmountRow)) + assert(PageContainerContentChild.childAt(3).is(SendGasRow)) + assert.equal(PageContainerContentChild.childAt(4).exists(), false) }) }) }) diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js index 0d8ffd179..0dc973632 100644 --- a/ui/app/components/send/send.component.js +++ b/ui/app/components/send/send.component.js @@ -193,7 +193,7 @@ export default class SendTransactionScreen extends PersistentForm { } render () { - const { history } = this.props + const { history, showHexData } = this.props return (
@@ -201,6 +201,7 @@ export default class SendTransactionScreen extends PersistentForm { this.updateGas(updateData)} scanQrCode={_ => this.props.scanQrCode()} + showHexData={showHexData} />
diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js index 41735de64..6ee8de9aa 100644 --- a/ui/app/components/send/send.container.js +++ b/ui/app/components/send/send.container.js @@ -18,6 +18,7 @@ import { getSelectedTokenToFiatRate, getSendAmount, getSendEditingTransactionId, + getSendHexDataFeatureFlagState, getSendFromObject, getSendTo, getTokenBalance, @@ -64,6 +65,7 @@ function mapStateToProps (state) { recentBlocks: getRecentBlocks(state), selectedAddress: getSelectedAddress(state), selectedToken: getSelectedToken(state), + showHexData: getSendHexDataFeatureFlagState(state), to: getSendTo(state), tokenBalance: getTokenBalance(state), tokenContract: getSelectedTokenContract(state), diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js index ab3f6d34b..22e379693 100644 --- a/ui/app/components/send/send.selectors.js +++ b/ui/app/components/send/send.selectors.js @@ -34,6 +34,7 @@ const selectors = { getSelectedTokenToFiatRate, getSendAmount, getSendHexData, + getSendHexDataFeatureFlagState, getSendEditingTransactionId, getSendErrors, getSendFrom, @@ -216,6 +217,10 @@ function getSendHexData (state) { return state.metamask.send.data } +function getSendHexDataFeatureFlagState (state) { + return state.metamask.featureFlags.sendHexData +} + function getSendEditingTransactionId (state) { return state.metamask.send.editingTransactionId } diff --git a/ui/app/components/send/tests/send-component.test.js b/ui/app/components/send/tests/send-component.test.js index 6194ec508..d2c2ee926 100644 --- a/ui/app/components/send/tests/send-component.test.js +++ b/ui/app/components/send/tests/send-component.test.js @@ -47,6 +47,7 @@ describe('Send Component', function () { recentBlocks={['mockBlock']} selectedAddress={'mockSelectedAddress'} selectedToken={'mockSelectedToken'} + showHexData={true} tokenBalance={'mockTokenBalance'} tokenContract={'mockTokenContract'} updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal} @@ -328,5 +329,9 @@ describe('Send Component', function () { } ) }) + + it('should pass showHexData to SendContent', () => { + assert.equal(wrapper.find(SendContent).props().showHexData, true) + }) }) }) diff --git a/ui/app/components/send/tests/send-container.test.js b/ui/app/components/send/tests/send-container.test.js index 57e332780..85eec6a53 100644 --- a/ui/app/components/send/tests/send-container.test.js +++ b/ui/app/components/send/tests/send-container.test.js @@ -39,6 +39,7 @@ proxyquire('../send.container.js', { getSelectedToken: (s) => `mockSelectedToken:${s}`, getSelectedTokenContract: (s) => `mockTokenContract:${s}`, getSelectedTokenToFiatRate: (s) => `mockTokenToFiatRate:${s}`, + getSendHexDataFeatureFlagState: (s) => `mockSendHexDataFeatureFlagState:${s}`, getSendAmount: (s) => `mockAmount:${s}`, getSendTo: (s) => `mockTo:${s}`, getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, @@ -73,6 +74,7 @@ describe('send container', () => { recentBlocks: 'mockRecentBlocks:mockState', selectedAddress: 'mockSelectedAddress:mockState', selectedToken: 'mockSelectedToken:mockState', + showHexData: 'mockSendHexDataFeatureFlagState:mockState', to: 'mockTo:mockState', tokenBalance: 'mockTokenBalance:mockState', tokenContract: 'mockTokenContract:mockState', diff --git a/ui/app/components/send/tests/send-selectors-test-data.js b/ui/app/components/send/tests/send-selectors-test-data.js index 8f9c19314..8b939dadb 100644 --- a/ui/app/components/send/tests/send-selectors-test-data.js +++ b/ui/app/components/send/tests/send-selectors-test-data.js @@ -2,7 +2,7 @@ module.exports = { 'metamask': { 'isInitialized': true, 'isUnlocked': true, - 'featureFlags': {'betaUI': true}, + 'featureFlags': {'betaUI': true, 'sendHexData': true}, 'rpcTarget': 'https://rawtestrpc.metamask.io/', 'identities': { '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { diff --git a/ui/app/components/send/tests/send-selectors.test.js b/ui/app/components/send/tests/send-selectors.test.js index 218da656b..1a47cd209 100644 --- a/ui/app/components/send/tests/send-selectors.test.js +++ b/ui/app/components/send/tests/send-selectors.test.js @@ -31,6 +31,7 @@ const { getSendFrom, getSendFromBalance, getSendFromObject, + getSendHexDataFeatureFlagState, getSendMaxModeState, getSendTo, getSendToAccounts, @@ -379,6 +380,15 @@ describe('send selectors', () => { }) }) + describe('getSendHexDataFeatureFlagState()', () => { + it('should return the sendHexData feature flag state', () => { + assert.deepEqual( + getSendHexDataFeatureFlagState(mockState), + true + ) + }) + }) + describe('getSendFrom()', () => { it('should return the send.from', () => { assert.deepEqual( diff --git a/ui/app/components/sender-to-recipient/index.scss b/ui/app/components/sender-to-recipient/index.scss index a97393b8f..656e30ddf 100644 --- a/ui/app/components/sender-to-recipient/index.scss +++ b/ui/app/components/sender-to-recipient/index.scss @@ -1,5 +1,5 @@ .sender-to-recipient { - &__container { + &--default { width: 100%; display: flex; flex-direction: row; @@ -8,67 +8,114 @@ position: relative; flex: 0 0 auto; height: 42px; - } - &__tooltip-wrapper { - min-width: 0; - } + .sender-to-recipient { + &__tooltip-wrapper { + min-width: 0; + } - &__tooltip-container { - max-width: 100%; - } + &__tooltip-container { + max-width: 100%; + } - &__sender, - &__recipient { - display: flex; - flex-direction: row; - align-items: center; - flex: 1; - padding: 0 16px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + &__party { + display: flex; + flex-direction: row; + align-items: center; + flex: 1; + padding: 0 16px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; - &__sender { - padding-right: 30px; - cursor: pointer; - } + &--sender { + padding-right: 30px; + cursor: pointer; + } + + &--recipient { + padding-left: 30px; + border-left: 1px solid $geyser; + + &-with-address { + cursor: pointer; + } + } + } - &__recipient { - padding-left: 30px; - border-left: 1px solid $geyser; + &__arrow-container { + position: absolute; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } - &--with-address { - cursor: pointer; + &__arrow-circle { + background: $white; + padding: 5px; + border: 1px solid $geyser; + border-radius: 20px; + height: 32px; + width: 32px; + display: flex; + justify-content: center; + align-items: center; + } + + &__name { + padding-left: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: .875rem; + } } } - &__arrow-container { - position: absolute; - height: 100%; + &--cards { + width: 100%; display: flex; - align-items: center; + flex-direction: row; justify-content: center; - } + position: relative; + flex: 0 0 auto; + padding: 8px; - &__arrow-circle { - background: $white; - padding: 5px; - border: 1px solid $geyser; - border-radius: 20px; - height: 32px; - width: 32px; - display: flex; - justify-content: center; - align-items: center; - } + .sender-to-recipient { + &__party { + display: flex; + flex-direction: row; + align-items: center; + flex: 1; + border-radius: 4px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08); + padding: 6px; + background: $white; + cursor: pointer; + min-width: 0; + color: $dusty-gray; + } + + &__tooltip-wrapper { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } - &__name { - padding-left: 14px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: .875rem; + &__name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: .5rem; + } + + &__arrow-container { + padding: 0 2px; + display: flex; + justify-content: center; + align-items: center; + } + } } } diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js index cae173b56..445a11d8a 100644 --- a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js @@ -1,16 +1,30 @@ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' +import classnames from 'classnames' import Identicon from '../identicon' import Tooltip from '../tooltip-v2' import copyToClipboard from 'copy-to-clipboard' +import { DEFAULT_VARIANT, CARDS_VARIANT } from './sender-to-recipient.constants' -export default class SenderToRecipient extends Component { +const variantHash = { + [DEFAULT_VARIANT]: 'sender-to-recipient--default', + [CARDS_VARIANT]: 'sender-to-recipient--cards', +} + +export default class SenderToRecipient extends PureComponent { static propTypes = { senderName: PropTypes.string, senderAddress: PropTypes.string, recipientName: PropTypes.string, recipientAddress: PropTypes.string, t: PropTypes.func, + variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT]), + addressOnly: PropTypes.bool, + assetImage: PropTypes.string, + } + + static defaultProps = { + variant: DEFAULT_VARIANT, } static contextTypes = { @@ -22,24 +36,63 @@ export default class SenderToRecipient extends Component { recipientAddressCopied: false, } + renderSenderIdenticon () { + return !this.props.addressOnly && ( +
+ +
+ ) + } + + renderSenderAddress () { + const { t } = this.context + const { senderName, senderAddress, addressOnly } = this.props + + return ( + this.setState({ senderAddressCopied: false })} + > +
+ { addressOnly ? `${t('from')}: ${senderAddress}` : senderName } +
+
+ ) + } + + renderRecipientIdenticon () { + const { recipientAddress, assetImage } = this.props + + return !this.props.addressOnly && ( +
+ +
+ ) + } + renderRecipientWithAddress () { const { t } = this.context - const { recipientName, recipientAddress } = this.props + const { recipientName, recipientAddress, addressOnly } = this.props return (
{ this.setState({ recipientAddressCopied: true }) copyToClipboard(recipientAddress) }} > -
- -
+ { this.renderRecipientIdenticon() } this.setState({ recipientAddressCopied: false })} > -
- { recipientName || this.context.t('newContract') } +
+ { + addressOnly + ? `${t('to')}: ${recipientAddress}` + : (recipientName || this.context.t('newContract')) + }
@@ -57,46 +114,25 @@ export default class SenderToRecipient extends Component { renderRecipientWithoutAddress () { return ( -
+
-
+
{ this.context.t('newContract') }
) } - render () { - const { t } = this.context - const { senderName, senderAddress, recipientAddress } = this.props - - return ( -
-
{ - this.setState({ senderAddressCopied: true }) - copyToClipboard(senderAddress) - }} - > -
- -
- this.setState({ senderAddressCopied: false })} - > -
- { senderName } -
-
+ renderArrow () { + return this.props.variant === CARDS_VARIANT + ? ( +
+
+ ) : (
+ ) + } + + render () { + const { senderAddress, recipientAddress, variant } = this.props + + return ( +
+
{ + this.setState({ senderAddressCopied: true }) + copyToClipboard(senderAddress) + }} + > + { this.renderSenderIdenticon() } + { this.renderSenderAddress() } +
+ { this.renderArrow() } { recipientAddress ? this.renderRecipientWithAddress() diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js b/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js new file mode 100644 index 000000000..166228932 --- /dev/null +++ b/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js @@ -0,0 +1,3 @@ +// Component design variants +export const DEFAULT_VARIANT = 'DEFAULT_VARIANT' +export const CARDS_VARIANT = 'CARDS_VARIANT' diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index 4334aacba..b87bf959e 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -35,12 +35,13 @@ function ShiftListItem () { } ShiftListItem.prototype.render = function () { - return h('div.tx-list-item.tx-list-clickable', { + return h('div.transaction-list-item.tx-list-clickable', { style: { paddingTop: '20px', paddingBottom: '20px', justifyContent: 'space-around', alignItems: 'center', + flexDirection: 'row', }, }, [ h('div', { diff --git a/ui/app/components/sidebars/index.js b/ui/app/components/sidebars/index.js new file mode 100644 index 000000000..732925f69 --- /dev/null +++ b/ui/app/components/sidebars/index.js @@ -0,0 +1 @@ +export { default } from './sidebar.component' diff --git a/ui/app/components/sidebars/index.scss b/ui/app/components/sidebars/index.scss new file mode 100644 index 000000000..5ab0664df --- /dev/null +++ b/ui/app/components/sidebars/index.scss @@ -0,0 +1,74 @@ +.sidebar-right-enter { + transition: transform 300ms ease-in-out; + transform: translateX(-100%); +} + +.sidebar-right-enter.sidebar-right-enter-active { + transition: transform 300ms ease-in-out; + transform: translateX(0%); +} + +.sidebar-right-leave { + transition: transform 200ms ease-out; + transform: translateX(0%); +} + +.sidebar-right-leave.sidebar-right-leave-active { + transition: transform 200ms ease-out; + transform: translateX(-100%); +} + +.sidebar-left-enter { + transition: transform 300ms ease-in-out; + transform: translateX(100%); +} + +.sidebar-left-enter.sidebar-left-enter-active { + transition: transform 300ms ease-in-out; + transform: translateX(0%); +} + +.sidebar-left-leave { + transition: transform 200ms ease-out; + transform: translateX(0%); +} + +.sidebar-left-leave.sidebar-left-leave-active { + transition: transform 200ms ease-out; + transform: translateX(100%); +} + +.sidebar-left { + flex: 1 0 230px; + background: rgb(250, 250, 250); + z-index: $sidebar-z-index; + position: fixed; + left: 15%; + right: 0; + bottom: 0; + opacity: 1; + visibility: visible; + will-change: transform; + overflow-y: auto; + box-shadow: rgba(0, 0, 0, .15) 2px 2px 4px; + width: 85%; + height: 100%; + + @media screen and (min-width: 769px) { + width: 408px; + left: calc(100% - 408px); + } +} + +.sidebar-overlay { + z-index: $sidebar-overlay-z-index; + position: fixed; + height: 100%; + width: 100%; + left: 0; + right: 0; + bottom: 0; + opacity: 1; + visibility: visible; + background-color: rgba(0, 0, 0, .3); +} \ No newline at end of file diff --git a/ui/app/components/sidebars/sidebar.component.js b/ui/app/components/sidebars/sidebar.component.js new file mode 100644 index 000000000..57cdd7111 --- /dev/null +++ b/ui/app/components/sidebars/sidebar.component.js @@ -0,0 +1,49 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import ReactCSSTransitionGroup from 'react-addons-css-transition-group' +import WalletView from '../wallet-view' +import { WALLET_VIEW_SIDEBAR } from './sidebar.constants' + +export default class Sidebar extends Component { + + static propTypes = { + sidebarOpen: PropTypes.bool, + hideSidebar: PropTypes.func, + transitionName: PropTypes.string, + type: PropTypes.string, + }; + + renderOverlay () { + return
this.props.hideSidebar()} /> + } + + renderSidebarContent () { + const { type } = this.props + + switch (type) { + case WALLET_VIEW_SIDEBAR: + return + default: + return null + } + + } + + render () { + const { transitionName, sidebarOpen } = this.props + + return ( +
+ + { sidebarOpen ? this.renderSidebarContent() : null } + + { sidebarOpen ? this.renderOverlay() : null } +
+ ) + } + +} diff --git a/ui/app/components/sidebars/sidebar.constants.js b/ui/app/components/sidebars/sidebar.constants.js new file mode 100644 index 000000000..1613a8245 --- /dev/null +++ b/ui/app/components/sidebars/sidebar.constants.js @@ -0,0 +1 @@ +export const WALLET_VIEW_SIDEBAR = 'wallet-view' diff --git a/ui/app/components/sidebars/tests/sidebars-component.test.js b/ui/app/components/sidebars/tests/sidebars-component.test.js new file mode 100644 index 000000000..e2d77518a --- /dev/null +++ b/ui/app/components/sidebars/tests/sidebars-component.test.js @@ -0,0 +1,88 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import ReactCSSTransitionGroup from 'react-addons-css-transition-group' +import Sidebar from '../sidebar.component.js' + +import WalletView from '../../wallet-view' + +const propsMethodSpies = { + hideSidebar: sinon.spy(), +} + +describe('Sidebar Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow() + }) + + afterEach(() => { + propsMethodSpies.hideSidebar.resetHistory() + }) + + describe('renderOverlay', () => { + let renderOverlay + + beforeEach(() => { + renderOverlay = shallow(wrapper.instance().renderOverlay()) + }) + + it('should render a overlay element', () => { + assert(renderOverlay.hasClass('sidebar-overlay')) + }) + + it('should pass the correct onClick function to the element', () => { + assert.equal(propsMethodSpies.hideSidebar.callCount, 0) + renderOverlay.props().onClick() + assert.equal(propsMethodSpies.hideSidebar.callCount, 1) + }) + }) + + describe('renderSidebarContent', () => { + let renderSidebarContent + + beforeEach(() => { + wrapper.setProps({ type: 'wallet-view' }) + renderSidebarContent = wrapper.instance().renderSidebarContent() + }) + + it('should render sidebar content with the correct props', () => { + wrapper.setProps({ type: 'wallet-view' }) + renderSidebarContent = wrapper.instance().renderSidebarContent() + assert.equal(renderSidebarContent.props.responsiveDisplayClassname, 'sidebar-right') + }) + + it('should not render with an unrecognized type', () => { + wrapper.setProps({ type: 'foobar' }) + renderSidebarContent = wrapper.instance().renderSidebarContent() + assert.equal(renderSidebarContent, undefined) + }) + }) + + describe('render', () => { + it('should render a div with one child', () => { + assert(wrapper.is('div')) + assert.equal(wrapper.children().length, 1) + }) + + it('should render the ReactCSSTransitionGroup without any children', () => { + assert(wrapper.children().at(0).is(ReactCSSTransitionGroup)) + assert.equal(wrapper.children().at(0).children().length, 0) + }) + + it('should render sidebar content and the overlay if sidebarOpen is true', () => { + wrapper.setProps({ sidebarOpen: true }) + assert.equal(wrapper.children().length, 2) + assert(wrapper.children().at(1).hasClass('sidebar-overlay')) + assert.equal(wrapper.children().at(0).children().length, 1) + assert(wrapper.children().at(0).children().at(0).is(WalletView)) + }) + }) +}) diff --git a/ui/app/components/tabs/tab/tab.component.js b/ui/app/components/tabs/tab/tab.component.js index a59da8904..9e590391c 100644 --- a/ui/app/components/tabs/tab/tab.component.js +++ b/ui/app/components/tabs/tab/tab.component.js @@ -3,13 +3,13 @@ import PropTypes from 'prop-types' import classnames from 'classnames' const Tab = props => { - const { name, onClick, isActive, tabIndex } = props + const { name, onClick, isActive, tabIndex, className, activeClassName } = props return (
  • { event.preventDefault() @@ -26,6 +26,13 @@ Tab.propTypes = { onClick: PropTypes.func, isActive: PropTypes.bool, tabIndex: PropTypes.number, + className: PropTypes.string, + activeClassName: PropTypes.string, +} + +Tab.defaultProps = { + className: 'tab', + activeClassName: 'tab--active', } export default Tab diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js deleted file mode 100644 index 99ca7335c..000000000 --- a/ui/app/components/token-balance.js +++ /dev/null @@ -1,120 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const TokenTracker = require('eth-token-tracker') -const connect = require('react-redux').connect -const selectors = require('../selectors') -const log = require('loglevel') - -function mapStateToProps (state) { - return { - userAddress: selectors.getSelectedAddress(state), - } -} - -module.exports = connect(mapStateToProps)(TokenBalance) - - -inherits(TokenBalance, Component) -function TokenBalance () { - this.state = { - string: '', - symbol: '', - isLoading: true, - error: null, - } - Component.call(this) -} - -TokenBalance.prototype.render = function () { - const state = this.state - const { symbol, string, isLoading } = state - const { balanceOnly } = this.props - - return isLoading - ? h('span', '') - : h('span.token-balance', [ - h('span.hide-text-overflow.token-balance__amount', string), - !balanceOnly && h('span.token-balance__symbol', symbol), - ]) -} - -TokenBalance.prototype.componentDidMount = function () { - this.createFreshTokenTracker() -} - -TokenBalance.prototype.createFreshTokenTracker = function () { - if (this.tracker) { - // Clean up old trackers when refreshing: - this.tracker.stop() - this.tracker.removeListener('update', this.balanceUpdater) - this.tracker.removeListener('error', this.showError) - } - - if (!global.ethereumProvider) return - const { userAddress, token } = this.props - - this.tracker = new TokenTracker({ - userAddress, - provider: global.ethereumProvider, - tokens: [token], - pollingInterval: 8000, - }) - - - // Set up listener instances for cleaning up - this.balanceUpdater = this.updateBalance.bind(this) - this.showError = error => { - this.setState({ error, isLoading: false }) - } - this.tracker.on('update', this.balanceUpdater) - this.tracker.on('error', this.showError) - - this.tracker.updateBalances() - .then(() => { - this.updateBalance(this.tracker.serialize()) - }) - .catch((reason) => { - log.error(`Problem updating balances`, reason) - this.setState({ isLoading: false }) - }) -} - -TokenBalance.prototype.componentDidUpdate = function (nextProps) { - const { - userAddress: oldAddress, - token: { address: oldTokenAddress }, - } = this.props - const { - userAddress: newAddress, - token: { address: newTokenAddress }, - } = nextProps - - if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) return - if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) return - - this.setState({ isLoading: true }) - this.createFreshTokenTracker() -} - -TokenBalance.prototype.updateBalance = function (tokens = []) { - if (!this.tracker.running) { - return - } - - const [{ string, symbol }] = tokens - - this.setState({ - string, - symbol, - isLoading: false, - }) -} - -TokenBalance.prototype.componentWillUnmount = function () { - if (!this.tracker) return - this.tracker.stop() - this.tracker.removeListener('update', this.balanceUpdater) - this.tracker.removeListener('error', this.showError) -} - diff --git a/ui/app/components/token-balance/index.js b/ui/app/components/token-balance/index.js new file mode 100644 index 000000000..f7da15cf8 --- /dev/null +++ b/ui/app/components/token-balance/index.js @@ -0,0 +1 @@ +export { default } from './token-balance.container' diff --git a/ui/app/components/token-balance/token-balance.component.js b/ui/app/components/token-balance/token-balance.component.js new file mode 100644 index 000000000..2b4f73980 --- /dev/null +++ b/ui/app/components/token-balance/token-balance.component.js @@ -0,0 +1,23 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +export default class TokenBalance extends PureComponent { + static propTypes = { + string: PropTypes.string, + symbol: PropTypes.string, + error: PropTypes.string, + className: PropTypes.string, + withSymbol: PropTypes.bool, + } + + render () { + const { className, string, withSymbol, symbol } = this.props + + return ( +
    + { string + (withSymbol ? ` ${symbol}` : '') } +
    + ) + } +} diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js b/ui/app/components/token-balance/token-balance.container.js similarity index 72% rename from ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js rename to ui/app/components/token-balance/token-balance.container.js index bc1289ce1..adc001f83 100644 --- a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js +++ b/ui/app/components/token-balance/token-balance.container.js @@ -1,8 +1,8 @@ import { connect } from 'react-redux' import { compose } from 'recompose' -import withTokenTracker from '../../../../helpers/with-token-tracker' +import withTokenTracker from '../../higher-order-components/with-token-tracker' import TokenBalance from './token-balance.component' -import selectors from '../../../../selectors' +import selectors from '../../selectors' const mapStateToProps = state => { return { diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 4100d76a5..477d97597 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -18,7 +18,7 @@ function mapStateToProps (state) { userAddress: selectors.getSelectedAddress(state), contractExchangeRates: state.metamask.contractExchangeRates, conversionRate: state.metamask.conversionRate, - sidebarOpen: state.appState.sidebarOpen, + sidebarOpen: state.appState.sidebar.isOpen, } } @@ -56,8 +56,8 @@ TokenCell.prototype.render = function () { sidebarOpen, currentCurrency, // userAddress, + image, } = props - let currentTokenToFiatRate let currentTokenInFiat let formattedFiat = '' @@ -97,6 +97,7 @@ TokenCell.prototype.render = function () { diameter: 50, address, network, + image, }), h('div.token-list-item__balance-ellipsis', null, [ diff --git a/ui/app/components/token-currency-display/index.js b/ui/app/components/token-currency-display/index.js new file mode 100644 index 000000000..6065cae1f --- /dev/null +++ b/ui/app/components/token-currency-display/index.js @@ -0,0 +1 @@ +export { default } from './token-currency-display.component' diff --git a/ui/app/components/token-currency-display/token-currency-display.component.js b/ui/app/components/token-currency-display/token-currency-display.component.js new file mode 100644 index 000000000..957aec376 --- /dev/null +++ b/ui/app/components/token-currency-display/token-currency-display.component.js @@ -0,0 +1,54 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import CurrencyDisplay from '../currency-display/currency-display.component' +import { getTokenData } from '../../helpers/transactions.util' +import { calcTokenAmount } from '../../token-util' + +export default class TokenCurrencyDisplay extends PureComponent { + static propTypes = { + transactionData: PropTypes.string, + token: PropTypes.object, + } + + state = { + displayValue: '', + } + + componentDidMount () { + this.setDisplayValue() + } + + componentDidUpdate (prevProps) { + const { transactionData } = this.props + const { transactionData: prevTransactionData } = prevProps + + if (transactionData !== prevTransactionData) { + this.setDisplayValue() + } + } + + setDisplayValue () { + const { transactionData: data, token } = this.props + const { decimals = '', symbol = '' } = token + const tokenData = getTokenData(data) + + let displayValue + + if (tokenData.params && tokenData.params.length === 2) { + const tokenValue = tokenData.params[1].value + const tokenAmount = calcTokenAmount(tokenValue, decimals) + displayValue = `${tokenAmount} ${symbol}` + } + + this.setState({ displayValue }) + } + + render () { + return ( + + ) + } +} diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 42351cf89..6a88f30bf 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -13,6 +13,7 @@ function mapStateToProps (state) { network: state.metamask.network, tokens: state.metamask.tokens, userAddress: selectors.getSelectedAddress(state), + assetImages: state.metamask.assetImages, } } @@ -44,10 +45,9 @@ function TokenList () { } TokenList.prototype.render = function () { - const { userAddress } = this.props + const { userAddress, assetImages } = this.props const state = this.state const { tokens, isLoading, error } = state - if (isLoading) { return this.message(this.context.t('loadingTokens')) } @@ -74,7 +74,10 @@ TokenList.prototype.render = function () { ]) } - return h('div', tokens.map((tokenData) => h(TokenCell, tokenData))) + return h('div', tokens.map((tokenData) => { + tokenData.image = assetImages[tokenData.address] + return h(TokenCell, tokenData) + })) } diff --git a/ui/app/components/transaction-action/index.js b/ui/app/components/transaction-action/index.js new file mode 100644 index 000000000..a6e9097f1 --- /dev/null +++ b/ui/app/components/transaction-action/index.js @@ -0,0 +1 @@ +export { default } from './transaction-action.component' diff --git a/ui/app/components/transaction-action/tests/transaction-action.component.test.js b/ui/app/components/transaction-action/tests/transaction-action.component.test.js new file mode 100644 index 000000000..218792847 --- /dev/null +++ b/ui/app/components/transaction-action/tests/transaction-action.component.test.js @@ -0,0 +1,112 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import TransactionAction from '../transaction-action.component' + +describe('TransactionAction Component', () => { + const tOrDefault = key => key + global.eth = { + getCode: sinon.stub().callsFake(address => { + console.log('CALLED') + const code = address === 'approveAddress' ? 'contract' : '0x' + return Promise.resolve(code) + }), + } + + describe('Outgoing transaction', () => { + it('should render -- when methodData is still fetching', () => { + const methodData = { data: {}, done: false, error: null } + const transaction = { + id: 1, + status: 'confirmed', + submittedTime: 1534045442919, + time: 1534045440641, + txParams: { + from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + gas: '0x5208', + gasPrice: '0x3b9aca00', + nonce: '0x96', + to: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', + value: '0x2386f26fc10000', + }, + } + + const wrapper = shallow(, { context: { tOrDefault }}) + + assert.equal(wrapper.find('.transaction-action').length, 1) + assert.equal(wrapper.text(), '--') + }) + + it('should render Sent Ether', () => { + const methodData = { data: {}, done: true, error: null } + const transaction = { + id: 1, + status: 'confirmed', + submittedTime: 1534045442919, + time: 1534045440641, + txParams: { + from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + gas: '0x5208', + gasPrice: '0x3b9aca00', + nonce: '0x96', + to: 'sentEtherAddress', + value: '0x2386f26fc10000', + }, + } + + const wrapper = shallow(, { context: { tOrDefault }}) + + assert.equal(wrapper.find('.transaction-action').length, 1) + wrapper.setState({ transactionAction: 'sentEther' }) + assert.equal(wrapper.text(), 'sentEther') + }) + + it('should render Approved', () => { + const methodData = { + data: { + name: 'Approve', + params: [ + { type: 'address' }, + { type: 'uint256' }, + ], + }, + done: true, + error: null, + } + const transaction = { + id: 1, + status: 'confirmed', + submittedTime: 1534045442919, + time: 1534045440641, + txParams: { + from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + gas: '0x5208', + gasPrice: '0x3b9aca00', + nonce: '0x96', + to: 'approveAddress', + value: '0x2386f26fc10000', + data: '0x095ea7b300000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000000003', + }, + } + + const wrapper = shallow(, { context: { tOrDefault }}) + + assert.equal(wrapper.find('.transaction-action').length, 1) + wrapper.setState({ transactionAction: 'approve' }) + assert.equal(wrapper.text(), 'approve') + }) + }) +}) diff --git a/ui/app/components/transaction-action/transaction-action.component.js b/ui/app/components/transaction-action/transaction-action.component.js new file mode 100644 index 000000000..81a1e96d0 --- /dev/null +++ b/ui/app/components/transaction-action/transaction-action.component.js @@ -0,0 +1,52 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { getTransactionActionKey } from '../../helpers/transactions.util' + +export default class TransactionAction extends PureComponent { + static contextTypes = { + tOrDefault: PropTypes.func, + } + + static propTypes = { + className: PropTypes.string, + transaction: PropTypes.object, + methodData: PropTypes.object, + } + + state = { + transactionAction: '', + } + + componentDidMount () { + this.getTransactionAction() + } + + componentDidUpdate () { + this.getTransactionAction() + } + + async getTransactionAction () { + const { transactionAction } = this.state + const { transaction, methodData } = this.props + const { data, done } = methodData + + if (!done || transactionAction) { + return + } + + const actionKey = await getTransactionActionKey(transaction, data) + const action = actionKey && this.context.tOrDefault(actionKey) + this.setState({ transactionAction: action }) + } + + render () { + const { className, methodData: { done } } = this.props + const { transactionAction } = this.state + + return ( +
    + { (done && transactionAction) || '--' } +
    + ) + } +} diff --git a/ui/app/components/transaction-list-item/index.js b/ui/app/components/transaction-list-item/index.js new file mode 100644 index 000000000..697cc55e9 --- /dev/null +++ b/ui/app/components/transaction-list-item/index.js @@ -0,0 +1 @@ +export { default } from './transaction-list-item.container' diff --git a/ui/app/components/transaction-list-item/index.scss b/ui/app/components/transaction-list-item/index.scss new file mode 100644 index 000000000..9c53c8960 --- /dev/null +++ b/ui/app/components/transaction-list-item/index.scss @@ -0,0 +1,117 @@ +.transaction-list-item { + box-sizing: border-box; + min-height: 74px; + padding: 8px 20px; + border-bottom: 1px solid $geyser; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + + @media screen and (max-width: $break-small) { + padding: 8px 20px 12px; + } + + &:hover { + background: rgba($alto, .2); + } + + &__grid { + width: 100%; + display: grid; + grid-template-columns: 45px 1fr 1fr 1fr; + grid-template-areas: + "identicon action status primary-amount" + "identicon nonce status secondary-amount"; + + @media screen and (max-width: $break-small) { + grid-template-columns: 45px 5fr 3fr; + grid-template-areas: + "nonce nonce nonce" + "identicon action primary-amount" + "identicon status secondary-amount"; + } + } + + &__identicon { + grid-area: identicon; + grid-row: 1 / span 2; + align-self: center; + + @media screen and (max-width: $break-small) { + grid-row: 2 / span 2; + } + } + + &__action { + text-transform: capitalize; + padding: 0 8px 2px 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + grid-area: action; + align-self: end; + } + + &__status { + grid-area: status; + grid-row: 1 / span 2; + align-self: center; + + @media screen and (max-width: $break-small) { + grid-row: 3; + } + } + + &__nonce { + font-size: .75rem; + color: #5e6064; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + grid-area: nonce; + align-self: start; + + @media screen and (max-width: $break-small) { + padding-bottom: 4px; + } + } + + &__amount { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &--primary { + text-align: end; + grid-area: primary-amount; + align-self: end; + + @media screen and (max-width: $break-small) { + padding-bottom: 2px; + } + } + + &--secondary { + text-align: end; + font-size: .75rem; + color: #5e6064; + grid-area: secondary-amount; + align-self: start; + } + } + + &__retry { + background: #d1edff; + border-radius: 12px; + font-size: .75rem; + padding: 4px 12px; + cursor: pointer; + margin-top: 8px; + + @media screen and (max-width: $break-small) { + font-size: .5rem; + } + } +} 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 new file mode 100644 index 000000000..4fddd45ef --- /dev/null +++ b/ui/app/components/transaction-list-item/transaction-list-item.component.js @@ -0,0 +1,151 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Identicon from '../identicon' +import TransactionStatus from '../transaction-status' +import TransactionAction from '../transaction-action' +import CurrencyDisplay from '../currency-display' +import TokenCurrencyDisplay from '../token-currency-display' +import prefixForNetwork from '../../../lib/etherscan-prefix-for-network' +import { CONFIRM_TRANSACTION_ROUTE } from '../../routes' +import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions' +import { ETH } from '../../constants/common' + +export default class TransactionListItem extends PureComponent { + static propTypes = { + history: PropTypes.object, + transaction: PropTypes.object, + value: PropTypes.string, + methodData: PropTypes.object, + showRetry: PropTypes.bool, + retryTransaction: PropTypes.func, + setSelectedToken: PropTypes.func, + nonceAndDate: PropTypes.string, + token: PropTypes.object, + assetImages: PropTypes.object, + } + + handleClick = () => { + const { transaction, history } = this.props + const { id, status, hash, metamaskNetworkId } = transaction + + if (status === UNAPPROVED_STATUS) { + history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`) + } else if (hash) { + const prefix = prefixForNetwork(metamaskNetworkId) + const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}` + global.platform.openWindow({ url: etherscanUrl }) + } + } + + handleRetryClick = event => { + event.stopPropagation() + + const { + transaction: { txParams: { to } = {} }, + methodData: { name } = {}, + setSelectedToken, + } = this.props + + if (name === TOKEN_METHOD_TRANSFER) { + setSelectedToken(to) + } + + this.resubmit() + } + + resubmit () { + const { transaction: { id }, retryTransaction, history } = this.props + retryTransaction(id) + .then(id => history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)) + } + + renderPrimaryCurrency () { + const { token, transaction: { txParams: { data } = {} } = {}, value } = this.props + + return token + ? ( + + ) : ( + + ) + } + + renderSecondaryCurrency () { + const { token, value } = this.props + + return token + ? null + : ( + + ) + } + + render () { + const { + transaction, + methodData, + showRetry, + nonceAndDate, + assetImages, + } = this.props + const { txParams = {} } = transaction + + return ( +
    +
    + + +
    + { nonceAndDate } +
    + + { this.renderPrimaryCurrency() } + { this.renderSecondaryCurrency() } +
    + { + showRetry && methodData.done && ( +
    + Taking too long? Increase the gas price on your transaction +
    + ) + } +
    + ) + } +} diff --git a/ui/app/components/transaction-list-item/transaction-list-item.container.js b/ui/app/components/transaction-list-item/transaction-list-item.container.js new file mode 100644 index 000000000..47644241a --- /dev/null +++ b/ui/app/components/transaction-list-item/transaction-list-item.container.js @@ -0,0 +1,32 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import withMethodData from '../../higher-order-components/with-method-data' +import TransactionListItem from './transaction-list-item.component' +import { setSelectedToken, retryTransaction } from '../../actions' +import { hexToDecimal } from '../../helpers/conversions.util' +import { formatDate } from '../../util' + +const mapStateToProps = (state, ownProps) => { + const { transaction: { txParams: { value, nonce } = {}, time } = {} } = ownProps + + const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time) + + return { + value, + nonceAndDate, + } +} + +const mapDispatchToProps = dispatch => { + return { + setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)), + retryTransaction: transactionId => dispatch(retryTransaction(transactionId)), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps), + withMethodData, +)(TransactionListItem) diff --git a/ui/app/components/transaction-list/index.js b/ui/app/components/transaction-list/index.js new file mode 100644 index 000000000..688994367 --- /dev/null +++ b/ui/app/components/transaction-list/index.js @@ -0,0 +1 @@ +export { default } from './transaction-list.container' diff --git a/ui/app/components/transaction-list/index.scss b/ui/app/components/transaction-list/index.scss new file mode 100644 index 000000000..0e8db485c --- /dev/null +++ b/ui/app/components/transaction-list/index.scss @@ -0,0 +1,46 @@ +.transaction-list { + display: flex; + flex-direction: column; + flex: 1; + overflow-y: hidden; + + &__completed-transactions { + display: flex; + flex-direction: column; + height: 100%; + } + + &__header { + flex: 0 0 auto; + font-size: .875rem; + color: $dusty-gray; + border-bottom: 1px solid $geyser; + padding: 16px 0 8px 20px; + + @media screen and (max-width: $break-small) { + padding: 8px 0 8px 16px; + } + } + + &__transactions { + flex: 1; + overflow-y: auto; + } + + &__pending-transactions { + margin-bottom: 16px; + } + + &__empty { + flex: 1; + display: grid; + grid-template-rows: 35% 1fr; + } + + &__empty-text { + grid-row-start: 2; + display: flex; + justify-content: center; + color: $silver; + } +} diff --git a/ui/app/components/transaction-list/transaction-list.component.js b/ui/app/components/transaction-list/transaction-list.component.js new file mode 100644 index 000000000..c864fea3b --- /dev/null +++ b/ui/app/components/transaction-list/transaction-list.component.js @@ -0,0 +1,118 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import TransactionListItem from '../transaction-list-item' +import ShapeShiftTransactionListItem from '../shift-list-item' +import { TRANSACTION_TYPE_SHAPESHIFT } from '../../constants/transactions' + +export default class TransactionList extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static defaultProps = { + pendingTransactions: [], + completedTransactions: [], + transactionToRetry: {}, + } + + static propTypes = { + pendingTransactions: PropTypes.array, + completedTransactions: PropTypes.array, + transactionToRetry: PropTypes.object, + selectedToken: PropTypes.object, + updateNetworkNonce: PropTypes.func, + assetImages: PropTypes.object, + } + + componentDidMount () { + this.props.updateNetworkNonce() + } + + componentDidUpdate (prevProps) { + const { pendingTransactions: prevPendingTransactions = [] } = prevProps + const { pendingTransactions = [], updateNetworkNonce } = this.props + + if (pendingTransactions.length > prevPendingTransactions.length) { + updateNetworkNonce() + } + } + + shouldShowRetry = transaction => { + const { transactionToRetry } = this.props + const { id, submittedTime } = transaction + return id === transactionToRetry.id && Date.now() - submittedTime > 30000 + } + + renderTransactions () { + const { t } = this.context + const { pendingTransactions = [], completedTransactions = [] } = this.props + return ( +
    + { + pendingTransactions.length > 0 && ( +
    +
    + { `${t('queue')} (${pendingTransactions.length})` } +
    + { + pendingTransactions.map((transaction, index) => ( + this.renderTransaction(transaction, index) + )) + } +
    + ) + } +
    +
    + { t('history') } +
    + { + completedTransactions.length > 0 + ? completedTransactions.map((transaction, index) => ( + this.renderTransaction(transaction, index) + )) + : this.renderEmpty() + } +
    +
    + ) + } + + renderTransaction (transaction, index) { + const { selectedToken, assetImages } = this.props + + return transaction.key === TRANSACTION_TYPE_SHAPESHIFT + ? ( + + ) : ( + + ) + } + + renderEmpty () { + return ( +
    +
    + { this.context.t('noTransactions') } +
    +
    + ) + } + + render () { + return ( +
    + { this.renderTransactions() } +
    + ) + } +} diff --git a/ui/app/components/transaction-list/transaction-list.container.js b/ui/app/components/transaction-list/transaction-list.container.js new file mode 100644 index 000000000..2e946c67d --- /dev/null +++ b/ui/app/components/transaction-list/transaction-list.container.js @@ -0,0 +1,51 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import TransactionList from './transaction-list.component' +import { + pendingTransactionsSelector, + submittedPendingTransactionsSelector, + completedTransactionsSelector, +} from '../../selectors/transactions' +import { getSelectedAddress, getAssetImages } from '../../selectors' +import { selectedTokenSelector } from '../../selectors/tokens' +import { getLatestSubmittedTxWithNonce } from '../../helpers/transactions.util' +import { updateNetworkNonce } from '../../actions' + +const mapStateToProps = state => { + const pendingTransactions = pendingTransactionsSelector(state) + const submittedPendingTransactions = submittedPendingTransactionsSelector(state) + const networkNonce = state.appState.networkNonce + + return { + completedTransactions: completedTransactionsSelector(state), + pendingTransactions, + transactionToRetry: getLatestSubmittedTxWithNonce(submittedPendingTransactions, networkNonce), + selectedToken: selectedTokenSelector(state), + selectedAddress: getSelectedAddress(state), + assetImages: getAssetImages(state), + } +} + +const mapDispatchToProps = dispatch => { + return { + updateNetworkNonce: address => dispatch(updateNetworkNonce(address)), + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { selectedAddress, ...restStateProps } = stateProps + const { updateNetworkNonce, ...restDispatchProps } = dispatchProps + + return { + ...restStateProps, + ...restDispatchProps, + ...ownProps, + updateNetworkNonce: () => updateNetworkNonce(selectedAddress), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(TransactionList) diff --git a/ui/app/components/transaction-status/index.js b/ui/app/components/transaction-status/index.js new file mode 100644 index 000000000..dece41e9c --- /dev/null +++ b/ui/app/components/transaction-status/index.js @@ -0,0 +1 @@ +export { default } from './transaction-status.component' diff --git a/ui/app/components/transaction-status/index.scss b/ui/app/components/transaction-status/index.scss new file mode 100644 index 000000000..35be550f7 --- /dev/null +++ b/ui/app/components/transaction-status/index.scss @@ -0,0 +1,28 @@ +.transaction-status { + height: 26px; + width: 81px; + border-radius: 4px; + background-color: #f0f0f0; + color: #5e6064; + font-size: .625rem; + text-transform: uppercase; + display: flex; + justify-content: center; + align-items: center; + + @media screen and (max-width: $break-small) { + height: 16px; + width: 70px; + font-size: .5rem; + } + + &--confirmed { + background-color: #eafad7; + color: #609a1c; + } + + &--approved, &--submitted { + background-color: #FFF2DB; + color: #CA810A; + } +} \ No newline at end of file diff --git a/ui/app/components/transaction-status/transaction-status.component.js b/ui/app/components/transaction-status/transaction-status.component.js new file mode 100644 index 000000000..a4c827ae8 --- /dev/null +++ b/ui/app/components/transaction-status/transaction-status.component.js @@ -0,0 +1,51 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { + UNAPPROVED_STATUS, + REJECTED_STATUS, + APPROVED_STATUS, + SIGNED_STATUS, + SUBMITTED_STATUS, + CONFIRMED_STATUS, + FAILED_STATUS, + DROPPED_STATUS, +} from '../../constants/transactions' + +const statusToClassNameHash = { + [UNAPPROVED_STATUS]: 'transaction-status--unapproved', + [REJECTED_STATUS]: 'transaction-status--rejected', + [APPROVED_STATUS]: 'transaction-status--approved', + [SIGNED_STATUS]: 'transaction-status--signed', + [SUBMITTED_STATUS]: 'transaction-status--submitted', + [CONFIRMED_STATUS]: 'transaction-status--confirmed', + [FAILED_STATUS]: 'transaction-status--failed', + [DROPPED_STATUS]: 'transaction-status--dropped', +} + +const statusToTextHash = { + [APPROVED_STATUS]: 'pending', + [SUBMITTED_STATUS]: 'pending', +} + +export default class TransactionStatus extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + statusKey: PropTypes.string, + className: PropTypes.string, + } + + render () { + const { className, statusKey } = this.props + const statusText = this.context.t(statusToTextHash[statusKey] || statusKey) + + return ( +
    + { statusText } +
    + ) + } +} diff --git a/ui/app/components/transaction-view-balance/index.js b/ui/app/components/transaction-view-balance/index.js new file mode 100644 index 000000000..8824737f7 --- /dev/null +++ b/ui/app/components/transaction-view-balance/index.js @@ -0,0 +1 @@ +export { default } from './transaction-view-balance.container' diff --git a/ui/app/components/transaction-view-balance/index.scss b/ui/app/components/transaction-view-balance/index.scss new file mode 100644 index 000000000..12045ab6d --- /dev/null +++ b/ui/app/components/transaction-view-balance/index.scss @@ -0,0 +1,76 @@ +.transaction-view-balance { + display: flex; + justify-content: space-between; + align-items: center; + flex: 1; + height: 54px; + + &__balance { + margin-left: 12px; + display: flex; + flex-direction: column; + + @media screen and (max-width: $break-small) { + align-items: center; + margin: 16px 0; + } + } + + &__token-balance { + margin-left: 12px; + font-size: 1.5rem; + + @media screen and (max-width: $break-small) { + margin-bottom: 12px; + font-size: 1.75rem; + } + } + + &__primary-balance { + font-size: 1.5rem; + + @media screen and (max-width: $break-small) { + margin-bottom: 12px; + font-size: 1.75rem; + } + } + + &__secondary-balance { + font-size: 1.15rem; + color: #a0a0a0; + } + + &__balance-container { + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + + @media screen and (max-width: $break-small) { + flex-direction: column; + } + } + + &__buttons { + display: flex; + flex-direction: row; + + @media screen and (max-width: $break-small) { + margin-bottom: 16px; + } + } + + &__button { + min-width: initial; + width: 100px; + + &:not(:last-child) { + margin-right: 12px; + } + } + + @media screen and (max-width: $break-small) { + flex-direction: column; + height: initial + } +} 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 new file mode 100644 index 000000000..bb95cb27e --- /dev/null +++ b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js @@ -0,0 +1,71 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import TokenBalance from '../../token-balance' +import CurrencyDisplay from '../../currency-display' +import { SEND_ROUTE } from '../../../routes' +import TransactionViewBalance from '../transaction-view-balance.component' + +const propsMethodSpies = { + showDepositModal: sinon.spy(), +} + +const historySpies = { + push: sinon.spy(), +} + +const t = (str1, str2) => str2 ? str1 + str2 : str1 + +describe('TransactionViewBalance Component', () => { + afterEach(() => { + propsMethodSpies.showDepositModal.resetHistory() + historySpies.push.resetHistory() + }) + + it('should render ETH balance properly', () => { + const wrapper = shallow(, { context: { t } }) + + 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) + + const buttons = wrapper.find('.transaction-view-balance__buttons') + assert.equal(propsMethodSpies.showDepositModal.callCount, 0) + buttons.childAt(0).simulate('click') + assert.equal(propsMethodSpies.showDepositModal.callCount, 1) + assert.equal(historySpies.push.callCount, 0) + buttons.childAt(1).simulate('click') + assert.equal(historySpies.push.callCount, 1) + assert.equal(historySpies.push.getCall(0).args[0], SEND_ROUTE) + }) + + it('should render token balance properly', () => { + const token = { + address: '0x35865238f0bec9d5ce6abff0fdaebe7b853dfcc5', + decimals: '2', + symbol: 'ABC', + } + + const wrapper = shallow(, { context: { t } }) + + assert.equal(wrapper.find('.transaction-view-balance').length, 1) + assert.equal(wrapper.find('.transaction-view-balance__button').length, 1) + assert.equal(wrapper.find(TokenBalance).length, 1) + }) +}) 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 new file mode 100644 index 000000000..1b7a29c87 --- /dev/null +++ b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js @@ -0,0 +1,96 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Button from '../button' +import Identicon from '../identicon' +import TokenBalance from '../token-balance' +import CurrencyDisplay from '../currency-display' +import { SEND_ROUTE } from '../../routes' +import { ETH } from '../../constants/common' + +export default class TransactionViewBalance extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + showDepositModal: PropTypes.func, + selectedToken: PropTypes.object, + history: PropTypes.object, + network: PropTypes.string, + balance: PropTypes.string, + assetImage: PropTypes.string, + } + + renderBalance () { + const { selectedToken, balance } = this.props + + return selectedToken + ? ( + + ) : ( +
    + + +
    + ) + } + + renderButtons () { + const { t } = this.context + const { selectedToken, showDepositModal, history } = this.props + + return ( +
    + { + !selectedToken && ( + + ) + } + +
    + ) + } + + render () { + const { network, selectedToken, assetImage } = this.props + + return ( +
    +
    + + { this.renderBalance() } +
    + { this.renderButtons() } +
    + ) + } +} diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js new file mode 100644 index 000000000..30c5cab16 --- /dev/null +++ b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import TransactionViewBalance from './transaction-view-balance.component' +import { getSelectedToken, getSelectedAddress, getSelectedTokenAssetImage } from '../../selectors' +import { showModal } from '../../actions' + +const mapStateToProps = state => { + const selectedAddress = getSelectedAddress(state) + const { metamask: { network, accounts } } = state + const account = accounts[selectedAddress] + const { balance } = account + + return { + selectedToken: getSelectedToken(state), + network, + balance, + assetImage: getSelectedTokenAssetImage(state), + } +} + +const mapDispatchToProps = dispatch => { + return { + showDepositModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(TransactionViewBalance) diff --git a/ui/app/components/transaction-view/index.js b/ui/app/components/transaction-view/index.js new file mode 100644 index 000000000..9eb0c3c83 --- /dev/null +++ b/ui/app/components/transaction-view/index.js @@ -0,0 +1 @@ +export { default } from './transaction-view.component' diff --git a/ui/app/components/transaction-view/index.scss b/ui/app/components/transaction-view/index.scss new file mode 100644 index 000000000..af9771ce0 --- /dev/null +++ b/ui/app/components/transaction-view/index.scss @@ -0,0 +1,27 @@ +.transaction-view { + flex: 1 1 66.5%; + background: $white; + min-width: 0; + display: flex; + flex-direction: column; + + &__balance-wrapper { + @media screen and (max-width: $break-small) { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + flex: 0 0 auto; + padding-top: 16px; + } + + @media screen and (min-width: $break-large) { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + margin: 2.3em 2.37em .8em; + flex: 0 0 auto; + } + } +} diff --git a/ui/app/components/transaction-view/transaction-view.component.js b/ui/app/components/transaction-view/transaction-view.component.js new file mode 100644 index 000000000..7014ca173 --- /dev/null +++ b/ui/app/components/transaction-view/transaction-view.component.js @@ -0,0 +1,27 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Media from 'react-media' +import MenuBar from '../menu-bar' +import TransactionViewBalance from '../transaction-view-balance' +import TransactionList from '../transaction-list' + +export default class TransactionView extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + render () { + return ( +
    + } + /> +
    + +
    + +
    + ) + } +} diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js deleted file mode 100644 index 474d62638..000000000 --- a/ui/app/components/tx-list-item.js +++ /dev/null @@ -1,356 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const { compose } = require('recompose') -const { withRouter } = require('react-router-dom') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const inherits = require('util').inherits -const classnames = require('classnames') -const abi = require('human-standard-token-abi') -const abiDecoder = require('abi-decoder') -abiDecoder.addABI(abi) -const Identicon = require('./identicon') -const contractMap = require('eth-contract-metadata') -const { checksumAddress } = require('../util') - -const actions = require('../actions') -const { conversionUtil, multiplyCurrencies } = require('../conversion-util') -const { calcTokenAmount } = require('../token-util') - -const { getCurrentCurrency } = require('../selectors') -const { CONFIRM_TRANSACTION_ROUTE } = require('../routes') - -TxListItem.contextTypes = { - t: PropTypes.func, -} - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(TxListItem) - -function mapStateToProps (state) { - return { - tokens: state.metamask.tokens, - currentCurrency: getCurrentCurrency(state), - contractExchangeRates: state.metamask.contractExchangeRates, - selectedAddressTxList: state.metamask.selectedAddressTxList, - networkNonce: state.appState.networkNonce, - } -} - -function mapDispatchToProps (dispatch) { - return { - setSelectedToken: tokenAddress => dispatch(actions.setSelectedToken(tokenAddress)), - retryTransaction: transactionId => dispatch(actions.retryTransaction(transactionId)), - } -} - -inherits(TxListItem, Component) -function TxListItem () { - Component.call(this) - - this.state = { - total: null, - fiatTotal: null, - isTokenTx: null, - } - - this.unmounted = false -} - -TxListItem.prototype.componentDidMount = async function () { - const { txParams = {} } = this.props - - const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) - const { name: txDataName } = decodedData || {} - const isTokenTx = txDataName === 'transfer' - - const { total, fiatTotal } = isTokenTx - ? await this.getSendTokenTotal() - : this.getSendEtherTotal() - - if (this.unmounted) { - return - } - this.setState({ total, fiatTotal, isTokenTx }) -} - -TxListItem.prototype.componentWillUnmount = function () { - this.unmounted = true -} - -TxListItem.prototype.getAddressText = function () { - const { - address, - txParams = {}, - isMsg, - } = this.props - - const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) - const { name: txDataName, params = [] } = decodedData || {} - const { value } = params[0] || {} - const checksummedAddress = checksumAddress(address) - const checksummedValue = checksumAddress(value) - - let addressText - if (txDataName === 'transfer' || address) { - const addressToRender = txDataName === 'transfer' ? checksummedValue : checksummedAddress - addressText = `${addressToRender.slice(0, 10)}...${addressToRender.slice(-4)}` - } else if (isMsg) { - addressText = this.context.t('sigRequest') - } else { - addressText = this.context.t('contractDeployment') - } - - return addressText -} - -TxListItem.prototype.getSendEtherTotal = function () { - const { - transactionAmount, - conversionRate, - address, - currentCurrency, - } = this.props - - if (!address) { - return {} - } - - const totalInFiat = conversionUtil(transactionAmount, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: currentCurrency, - fromDenomination: 'WEI', - numberOfDecimals: 2, - conversionRate, - }) - const totalInETH = conversionUtil(transactionAmount, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: 'ETH', - fromDenomination: 'WEI', - conversionRate, - numberOfDecimals: 6, - }) - - return { - total: `${totalInETH} ETH`, - fiatTotal: `${totalInFiat} ${currentCurrency.toUpperCase()}`, - } -} - -TxListItem.prototype.getTokenInfo = async function () { - const { txParams = {}, tokenInfoGetter, tokens } = this.props - const toAddress = txParams.to - - let decimals - let symbol - - ({ decimals, symbol } = tokens.filter(({ address }) => address === toAddress)[0] || {}) - - if (!decimals && !symbol) { - ({ decimals, symbol } = contractMap[toAddress] || {}) - } - - if (!decimals && !symbol) { - ({ decimals, symbol } = await tokenInfoGetter(toAddress)) - } - - return { decimals, symbol, address: toAddress } -} - -TxListItem.prototype.getSendTokenTotal = async function () { - const { - txParams = {}, - conversionRate, - contractExchangeRates, - currentCurrency, - } = this.props - - const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) - const { params = [] } = decodedData || {} - const { value } = params[1] || {} - const { decimals, symbol, address } = await this.getTokenInfo() - const total = calcTokenAmount(value, decimals) - - let tokenToFiatRate - let totalInFiat - - if (contractExchangeRates[address]) { - tokenToFiatRate = multiplyCurrencies( - contractExchangeRates[address], - conversionRate - ) - - totalInFiat = conversionUtil(total, { - fromNumericBase: 'dec', - toNumericBase: 'dec', - fromCurrency: symbol, - toCurrency: currentCurrency, - numberOfDecimals: 2, - conversionRate: tokenToFiatRate, - }) - } - - const showFiat = Boolean(totalInFiat) && currentCurrency.toUpperCase() !== symbol - - return { - total: `${total} ${symbol}`, - fiatTotal: showFiat && `${totalInFiat} ${currentCurrency.toUpperCase()}`, - } -} - -TxListItem.prototype.showRetryButton = function () { - const { - transactionSubmittedTime, - selectedAddressTxList, - transactionId, - txParams, - networkNonce, - } = this.props - if (!txParams) { - return false - } - let currentTxSharesEarliestNonce = false - const currentNonce = txParams.nonce - const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce) - const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted') - const currentSubmittedTxs = selectedAddressTxList.filter(tx => tx.status === 'submitted') - const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[currentNonceSubmittedTxs.length - 1] - const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce && - lastSubmittedTxWithCurrentNonce.id === transactionId - if (currentSubmittedTxs.length > 0) { - currentTxSharesEarliestNonce = currentNonce === networkNonce - } - - return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000 -} - -TxListItem.prototype.setSelectedToken = function (tokenAddress) { - this.props.setSelectedToken(tokenAddress) -} - -TxListItem.prototype.resubmit = function () { - const { transactionId } = this.props - this.props.retryTransaction(transactionId) - .then(id => this.props.history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)) -} - -TxListItem.prototype.render = function () { - const { - transactionStatus, - onClick, - transactionId, - dateString, - address, - className, - txParams, - } = this.props - const { total, fiatTotal, isTokenTx } = this.state - - return h(`div${className || ''}`, { - key: transactionId, - onClick: () => onClick && onClick(transactionId), - }, [ - h(`div.flex-column.tx-list-item-wrapper`, {}, [ - - h('div.tx-list-date-wrapper', { - style: {}, - }, [ - h('span.tx-list-date', {}, [ - dateString, - ]), - ]), - - h('div.flex-row.tx-list-content-wrapper', { - style: {}, - }, [ - - h('div.tx-list-identicon-wrapper', { - style: {}, - }, [ - h(Identicon, { - address, - diameter: 28, - }), - ]), - - h('div.tx-list-account-and-status-wrapper', {}, [ - h('div.tx-list-account-wrapper', { - style: {}, - }, [ - h('span.tx-list-account', {}, [ - this.getAddressText(address), - ]), - ]), - - h('div.tx-list-status-wrapper', { - style: {}, - }, [ - h('span', { - className: classnames('tx-list-status', { - 'tx-list-status--rejected': transactionStatus === 'rejected', - 'tx-list-status--failed': transactionStatus === 'failed', - 'tx-list-status--dropped': transactionStatus === 'dropped', - }), - }, - this.txStatusIndicator(), - ), - ]), - ]), - - h('div.flex-column.tx-list-details-wrapper', { - style: {}, - }, [ - - h('span.tx-list-value', total), - - fiatTotal && h('span.tx-list-fiat-value', fiatTotal), - - ]), - ]), - - this.showRetryButton() && h('.tx-list-item-retry-container', { - onClick: (event) => { - event.stopPropagation() - if (isTokenTx) { - this.setSelectedToken(txParams.to) - } - this.resubmit() - }, - }, [ - h('span', 'Taking too long? Increase the gas price on your transaction'), - ]), - - ]), // holding on icon from design - ]) -} - -TxListItem.prototype.txStatusIndicator = function () { - const { transactionStatus } = this.props - - let name - - if (transactionStatus === 'unapproved') { - name = this.context.t('unapproved') - } else if (transactionStatus === 'rejected') { - name = this.context.t('rejected') - } else if (transactionStatus === 'approved') { - name = this.context.t('approved') - } else if (transactionStatus === 'signed') { - name = this.context.t('signed') - } else if (transactionStatus === 'submitted') { - name = this.context.t('submitted') - } else if (transactionStatus === 'confirmed') { - name = this.context.t('confirmed') - } else if (transactionStatus === 'failed') { - name = this.context.t('failed') - } else if (transactionStatus === 'dropped') { - name = this.context.t('dropped') - } - return name -} diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js deleted file mode 100644 index d8c4a9d19..000000000 --- a/ui/app/components/tx-list.js +++ /dev/null @@ -1,171 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const inherits = require('util').inherits -const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') -const selectors = require('../selectors') -const TxListItem = require('./tx-list-item') -const ShiftListItem = require('./shift-list-item') -const { formatDate } = require('../util') -const { showConfTxPage, updateNetworkNonce } = require('../actions') -const classnames = require('classnames') -const { tokenInfoGetter } = require('../token-util') -const { withRouter } = require('react-router-dom') -const { compose } = require('recompose') -const { CONFIRM_TRANSACTION_ROUTE } = require('../routes') - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(TxList) - -TxList.contextTypes = { - t: PropTypes.func, -} - -function mapStateToProps (state) { - return { - txsToRender: selectors.transactionsSelector(state), - conversionRate: selectors.conversionRateSelector(state), - selectedAddress: selectors.getSelectedAddress(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })), - updateNetworkNonce: (address) => dispatch(updateNetworkNonce(address)), - } -} - -inherits(TxList, Component) -function TxList () { - Component.call(this) -} - -TxList.prototype.componentWillMount = function () { - this.tokenInfoGetter = tokenInfoGetter() - this.props.updateNetworkNonce(this.props.selectedAddress) -} - -TxList.prototype.componentDidUpdate = function (prevProps) { - const oldTxsToRender = prevProps.txsToRender - const { - txsToRender: newTxsToRender, - selectedAddress, - updateNetworkNonce, - } = this.props - - if (newTxsToRender.length > oldTxsToRender.length) { - updateNetworkNonce(selectedAddress) - } -} - -TxList.prototype.render = function () { - return h('div.flex-column', [ - h('div.flex-row.tx-list-header-wrapper', [ - h('div.flex-row.tx-list-header', [ - h('div', this.context.t('transactions')), - ]), - ]), - h('div.flex-column.tx-list-container', {}, [ - this.renderTransaction(), - ]), - ]) -} - -TxList.prototype.renderTransaction = function () { - const { txsToRender, conversionRate } = this.props - - return txsToRender.length - ? txsToRender.map((transaction, i) => this.renderTransactionListItem(transaction, conversionRate, i)) - : [h( - 'div.tx-list-item.tx-list-item--empty', - { key: 'tx-list-none' }, - [ this.context.t('noTransactions') ], - )] -} - -// TODO: Consider moving TxListItem into a separate component -TxList.prototype.renderTransactionListItem = function (transaction, conversionRate, index) { - // console.log({transaction}) - // refer to transaction-list.js:line 58 - - if (transaction.key === 'shapeshift') { - return h(ShiftListItem, { ...transaction, key: `shapeshift${index}` }) - } - - const props = { - dateString: formatDate(transaction.time), - address: transaction.txParams && transaction.txParams.to, - transactionStatus: transaction.status, - transactionAmount: transaction.txParams && transaction.txParams.value, - transactionId: transaction.id, - transactionHash: transaction.hash, - transactionNetworkId: transaction.metamaskNetworkId, - transactionSubmittedTime: transaction.submittedTime, - } - - const { - address, - transactionStatus, - transactionAmount, - dateString, - transactionId, - transactionHash, - transactionNetworkId, - transactionSubmittedTime, - } = props - const { history } = this.props - - const opts = { - key: transactionId || transactionHash, - txParams: transaction.txParams, - isMsg: Boolean(transaction.msgParams), - transactionStatus, - transactionId, - dateString, - address, - transactionAmount, - transactionHash, - conversionRate, - tokenInfoGetter: this.tokenInfoGetter, - transactionSubmittedTime, - } - - const isUnapproved = transactionStatus === 'unapproved' - - if (isUnapproved) { - opts.onClick = () => { - this.props.showConfTxPage({ id: transactionId }) - history.push(CONFIRM_TRANSACTION_ROUTE) - } - opts.transactionStatus = this.context.t('notStarted') - } else if (transactionHash) { - opts.onClick = () => this.view(transactionHash, transactionNetworkId) - } - - opts.className = classnames('.tx-list-item', { - '.tx-list-pending-item-container': isUnapproved, - '.tx-list-clickable': Boolean(transactionHash) || isUnapproved, - }) - - return h(TxListItem, opts) -} - -TxList.prototype.view = function (txHash, network) { - const url = etherscanLinkFor(txHash, network) - if (url) { - navigateTo(url) - } -} - -function navigateTo (url) { - global.platform.openWindow({ url }) -} - -function etherscanLinkFor (txHash, network) { - const prefix = prefixForNetwork(network) - return `https://${prefix}etherscan.io/tx/${txHash}` -} diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js deleted file mode 100644 index 654090da6..000000000 --- a/ui/app/components/tx-view.js +++ /dev/null @@ -1,156 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const inherits = require('util').inherits -const { withRouter } = require('react-router-dom') -const { compose } = require('recompose') -const actions = require('../actions') -const selectors = require('../selectors') -const { SEND_ROUTE } = require('../routes') -const { checksumAddress: toChecksumAddress } = require('../util') - -const BalanceComponent = require('./balance-component') -const Tooltip = require('./tooltip') -const TxList = require('./tx-list') -const SelectedAccount = require('./selected-account') - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(TxView) - -TxView.contextTypes = { - t: PropTypes.func, -} - -function mapStateToProps (state) { - const sidebarOpen = state.appState.sidebarOpen - const isMascara = state.appState.isMascara - - const identities = state.metamask.identities - const accounts = state.metamask.accounts - const network = state.metamask.network - const selectedTokenAddress = state.metamask.selectedTokenAddress - const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] - const checksumAddress = toChecksumAddress(selectedAddress) - const identity = identities[selectedAddress] - - return { - sidebarOpen, - selectedAddress, - checksumAddress, - selectedTokenAddress, - selectedToken: selectors.getSelectedToken(state), - identity, - network, - isMascara, - } -} - -function mapDispatchToProps (dispatch) { - return { - showSidebar: () => { dispatch(actions.showSidebar()) }, - hideSidebar: () => { dispatch(actions.hideSidebar()) }, - showModal: (payload) => { dispatch(actions.showModal(payload)) }, - showSendPage: () => { dispatch(actions.showSendPage()) }, - showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) }, - } -} - -inherits(TxView, Component) -function TxView () { - Component.call(this) -} - -TxView.prototype.renderHeroBalance = function () { - const { selectedToken } = this.props - - return h('div.hero-balance', {}, [ - - h(BalanceComponent, { token: selectedToken }), - - this.renderButtons(), - ]) -} - -TxView.prototype.renderButtons = function () { - const {selectedToken, showModal, history } = this.props - - return !selectedToken - ? ( - h('div.flex-row.flex-center.hero-balance-buttons', [ - h('button.btn-primary.hero-balance-button', { - onClick: () => showModal({ - name: 'DEPOSIT_ETHER', - }), - }, this.context.t('deposit')), - - h('button.btn-primary.hero-balance-button', { - style: { - marginLeft: '0.8em', - }, - onClick: () => history.push(SEND_ROUTE), - }, this.context.t('send')), - ]) - ) - : ( - h('div.flex-row.flex-center.hero-balance-buttons', [ - h('button.btn-primary.hero-balance-button', { - onClick: () => history.push(SEND_ROUTE), - }, this.context.t('send')), - ]) - ) -} - -TxView.prototype.render = function () { - const { hideSidebar, isMascara, showSidebar, sidebarOpen } = this.props - const { t } = this.context - - return h('div.tx-view.flex-column', { - style: {}, - }, [ - - h('div.flex-row.phone-visible', { - style: { - justifyContent: 'center', - alignItems: 'center', - flex: '0 0 auto', - marginBottom: '16px', - padding: '5px', - borderBottom: '1px solid #e5e5e5', - }, - }, [ - - h(Tooltip, { - title: t('menu'), - position: 'bottom', - }, [ - h('div.fa.fa-bars', { - style: { - fontSize: '1.3em', - cursor: 'pointer', - padding: '10px', - }, - onClick: () => sidebarOpen ? hideSidebar() : showSidebar(), - }), - ]), - - h(SelectedAccount), - - !isMascara && h(Tooltip, { - title: t('openInTab'), - position: 'bottom', - }, [ - h('div.open-in-browser', { - onClick: () => global.platform.openExtensionInBrowser(), - }, [h('img', { src: 'images/popout.svg' })]), - ]), - ]), - - this.renderHeroBalance(), - - h(TxList), - - ]) -} diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 8e092364c..6de265110 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -26,11 +26,15 @@ WalletView.contextTypes = { t: PropTypes.func, } +WalletView.defaultProps = { + responsiveDisplayClassname: '', +} + function mapStateToProps (state) { return { network: state.metamask.network, - sidebarOpen: state.appState.sidebarOpen, + sidebarOpen: state.appState.sidebar.isOpen, identities: state.metamask.identities, accounts: state.metamask.accounts, tokens: state.metamask.tokens, @@ -131,8 +135,9 @@ WalletView.prototype.render = function () { } } - return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), { + return h('div.wallet-view.flex-column', { style: {}, + className: responsiveDisplayClassname, }, [ // TODO: Separate component: wallet account details diff --git a/ui/app/constants/common.js b/ui/app/constants/common.js new file mode 100644 index 000000000..28731ce33 --- /dev/null +++ b/ui/app/constants/common.js @@ -0,0 +1 @@ +export const ETH = 'ETH' diff --git a/ui/app/constants/transactions.js b/ui/app/constants/transactions.js new file mode 100644 index 000000000..df6c4c8a4 --- /dev/null +++ b/ui/app/constants/transactions.js @@ -0,0 +1,22 @@ +export const UNAPPROVED_STATUS = 'unapproved' +export const REJECTED_STATUS = 'rejected' +export const APPROVED_STATUS = 'approved' +export const SIGNED_STATUS = 'signed' +export const SUBMITTED_STATUS = 'submitted' +export const CONFIRMED_STATUS = 'confirmed' +export const FAILED_STATUS = 'failed' +export const DROPPED_STATUS = 'dropped' + +export const TOKEN_METHOD_TRANSFER = 'transfer' +export const TOKEN_METHOD_APPROVE = 'approve' +export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom' + +export const SEND_ETHER_ACTION_KEY = 'sentEther' +export const DEPLOY_CONTRACT_ACTION_KEY = 'contractDeployment' +export const APPROVE_ACTION_KEY = 'approve' +export const SEND_TOKEN_ACTION_KEY = 'sentTokens' +export const TRANSFER_FROM_ACTION_KEY = 'transferFrom' +export const SIGNATURE_REQUEST_KEY = 'signatureRequest' +export const UNKNOWN_FUNCTION_KEY = 'unknownFunction' + +export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift' diff --git a/ui/app/css/itcss/components/hero-balance.scss b/ui/app/css/itcss/components/hero-balance.scss deleted file mode 100644 index eba93ecb4..000000000 --- a/ui/app/css/itcss/components/hero-balance.scss +++ /dev/null @@ -1,130 +0,0 @@ -.hero-balance { - - @media screen and (max-width: $break-small) { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - flex: 0 0 auto; - padding-top: 16px; - } - - @media screen and (min-width: $break-large) { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - margin: 2.3em 2.37em .8em; - flex: 0 0 auto; - } - - .balance-container { - display: flex; - margin: 0; - justify-content: flex-start; - align-items: center; - - @media screen and (max-width: $break-small) { - flex-direction: column; - flex: 0 0 auto; - max-width: 100%; - } - - @media screen and (min-width: $break-large) { - flex-direction: row; - flex-grow: 3; - min-width: 0; - } - } - - .balance-display { - .token-amount { - color: $black; - max-width: 100%; - - .token-balance { - display: flex; - } - } - - @media screen and (max-width: $break-small) { - max-width: 100%; - text-align: center; - - .token-amount { - font-size: 1.75rem; - margin-top: 1rem; - - .token-balance { - flex-direction: column; - } - } - - .fiat-amount { - font-size: 115%; - margin-top: 8.5%; - color: #a0a0a0; - } - } - - @media screen and (min-width: $break-large) { - margin: 0 .8em; - justify-content: flex-start; - align-items: flex-start; - min-width: 0; - - .token-amount { - font-size: 1.5rem; - } - - .fiat-amount { - margin-top: .25%; - font-size: 105%; - } - } - - @media #{$sub-mid-size-breakpoint-range} { - margin-left: .4em; - margin-right: .4em; - justify-content: flex-start; - align-items: flex-start; - - .token-amount { - font-size: 1rem; - } - - .fiat-amount { - margin-top: .25%; - font-size: 1rem; - } - } - } - - .hero-balance-buttons { - - @media screen and (max-width: $break-small) { - width: 100%; - // height: 100px; // needed a round number to set the heights of the buttons inside - flex: 0 0 auto; - padding: 16px 0; - } - - @media screen and (min-width: $break-large) { - flex-grow: 2; - justify-content: flex-end; - } - } -} - -.hero-balance-button { - min-width: initial; - width: 6rem; - - @media #{$sub-mid-size-breakpoint-range} { - padding: .4rem; - width: 4rem; - display: flex; - flex: 1; - justify-content: center; - } -} diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index 821a6b612..9e2008b54 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -19,8 +19,6 @@ @import './loading-overlay.scss'; // Balances -@import './hero-balance.scss'; - @import './wallet-balance.scss'; // Tx List and Sections diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index bbfd85c90..7eb193d6f 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -6,7 +6,6 @@ $sub-mid-size-breakpoint-range: "screen and (min-width: #{$break-large}) and (ma */ // Component Colors -$tx-view-bg: $white; $wallet-view-bg: $alabaster; // Main container @@ -30,32 +29,6 @@ $wallet-view-bg: $alabaster; min-width: 0; } -// tx view - -.tx-view { - flex: 1 1 66.5%; - background: $tx-view-bg; - min-width: 0; - - // No title on mobile - @media screen and (max-width: 575px) { - .identicon-wrapper { - display: none; - } - - .account-name { - display: none; - } - } -} - -.open-in-browser { - cursor: pointer; - display: flex; - justify-content: center; - padding: 10px; -} - // wallet view and sidebar .wallet-view { @@ -175,7 +148,7 @@ $wallet-view-bg: $alabaster; } } -.wallet-view.sidebar { +.wallet-view.sidebar-right { flex: 1 0 230px; background: rgb(250, 250, 250); z-index: $sidebar-z-index; @@ -193,20 +166,6 @@ $wallet-view-bg: $alabaster; height: calc(100% - 56px); } -.sidebar-overlay { - z-index: $sidebar-overlay-z-index; - position: fixed; - // top: 41px; - height: 100%; - width: 100%; - left: 0; - right: 0; - bottom: 0; - opacity: 1; - visibility: visible; - background-color: rgba(0, 0, 0, .3); -} - // main-container media queries @media screen and (min-width: 576px) { diff --git a/ui/app/css/itcss/components/transaction-list.scss b/ui/app/css/itcss/components/transaction-list.scss index 1d45ff13b..3435353d9 100644 --- a/ui/app/css/itcss/components/transaction-list.scss +++ b/ui/app/css/itcss/components/transaction-list.scss @@ -243,7 +243,7 @@ } .tx-list-item { - border-top: 1px solid rgb(231, 231, 231); + border-bottom: 1px solid $geyser; flex: 0 0 auto; display: flex; flex-flow: row nowrap; diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js index 1885e12d1..30c32f2bf 100644 --- a/ui/app/ducks/confirm-transaction.duck.js +++ b/ui/app/ducks/confirm-transaction.duck.js @@ -5,9 +5,7 @@ import { } from '../selectors/confirm-transaction' import { - getTokenData, - getMethodData, - getTransactionAmount, + getValueFromWeiHex, getTransactionFee, getHexGasTotal, addFiat, @@ -16,6 +14,7 @@ import { hexGreaterThan, } from '../helpers/confirm-transaction/util' +import { getTokenData, getMethodData, isSmartContractAddress } from '../helpers/transactions.util' import { getSymbolAndDecimals } from '../token-util' import { conversionUtil } from '../conversion-util' @@ -35,8 +34,9 @@ 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 FETCH_METHOD_DATA_START = createActionType('FETCH_METHOD_DATA_START') -const FETCH_METHOD_DATA_END = createActionType('FETCH_METHOD_DATA_END') +const UPDATE_TO_SMART_CONTRACT = createActionType('UPDATE_TO_SMART_CONTRACT') +const FETCH_DATA_START = createActionType('FETCH_DATA_START') +const FETCH_DATA_END = createActionType('FETCH_DATA_END') // Initial state const initState = { @@ -55,7 +55,8 @@ const initState = { ethTransactionTotal: '', hexGasTotal: '', nonce: '', - fetchingMethodData: false, + toSmartContract: false, + fetchingData: false, } // Reducer @@ -138,15 +139,20 @@ export default function reducer ({ confirmTransaction: confirmState = initState ...confirmState, nonce: action.payload, } - case FETCH_METHOD_DATA_START: + case UPDATE_TO_SMART_CONTRACT: return { ...confirmState, - fetchingMethodData: true, + toSmartContract: action.payload, } - case FETCH_METHOD_DATA_END: + case FETCH_DATA_START: return { ...confirmState, - fetchingMethodData: false, + fetchingData: true, + } + case FETCH_DATA_END: + return { + ...confirmState, + fetchingData: false, } case CLEAR_CONFIRM_TRANSACTION: return initState @@ -237,9 +243,16 @@ export function updateNonce (nonce) { } } -export function setFetchingMethodData (isFetching) { +export function updateToSmartContract (toSmartContract) { + return { + type: UPDATE_TO_SMART_CONTRACT, + payload: toSmartContract, + } +} + +export function setFetchingData (isFetching) { return { - type: isFetching ? FETCH_METHOD_DATA_START : FETCH_METHOD_DATA_END, + type: isFetching ? FETCH_DATA_START : FETCH_DATA_END, } } @@ -286,10 +299,10 @@ export function updateTxDataAndCalculate (txData) { const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData - const fiatTransactionAmount = getTransactionAmount({ + const fiatTransactionAmount = getValueFromWeiHex({ value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2, }) - const ethTransactionAmount = getTransactionAmount({ + const ethTransactionAmount = getValueFromWeiHex({ value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6, }) @@ -338,19 +351,22 @@ export function setTransactionToConfirm (transactionId) { dispatch(updateTxDataAndCalculate(txData)) const { txParams } = transaction + const { to } = txParams if (txParams.data) { const { tokens: existingTokens } = state const { data, to: tokenAddress } = txParams try { - dispatch(setFetchingMethodData(true)) + dispatch(setFetchingData(true)) const methodData = await getMethodData(data) dispatch(updateMethodData(methodData)) - dispatch(setFetchingMethodData(false)) + const toSmartContract = await isSmartContractAddress(to) + dispatch(updateToSmartContract(toSmartContract)) + dispatch(setFetchingData(false)) } catch (error) { dispatch(updateMethodData({})) - dispatch(setFetchingMethodData(false)) + dispatch(setFetchingData(false)) } const tokenData = getTokenData(data) diff --git a/ui/app/ducks/tests/confirm-transaction.duck.test.js b/ui/app/ducks/tests/confirm-transaction.duck.test.js index 111674e33..1bab0add0 100644 --- a/ui/app/ducks/tests/confirm-transaction.duck.test.js +++ b/ui/app/ducks/tests/confirm-transaction.duck.test.js @@ -1,6 +1,7 @@ import assert from 'assert' import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' +import sinon from 'sinon' import ConfirmTransactionReducer, * as actions from '../confirm-transaction.duck.js' @@ -20,7 +21,8 @@ const initialState = { ethTransactionTotal: '', hexGasTotal: '', nonce: '', - fetchingMethodData: false, + toSmartContract: false, + fetchingData: false, } const UPDATE_TX_DATA = 'metamask/confirm-transaction/UPDATE_TX_DATA' @@ -35,8 +37,9 @@ const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTI 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 FETCH_METHOD_DATA_START = 'metamask/confirm-transaction/FETCH_METHOD_DATA_START' -const FETCH_METHOD_DATA_END = 'metamask/confirm-transaction/FETCH_METHOD_DATA_END' +const UPDATE_TO_SMART_CONTRACT = 'metamask/confirm-transaction/UPDATE_TO_SMART_CONTRACT' +const FETCH_DATA_START = 'metamask/confirm-transaction/FETCH_DATA_START' +const FETCH_DATA_END = 'metamask/confirm-transaction/FETCH_DATA_END' const CLEAR_CONFIRM_TRANSACTION = 'metamask/confirm-transaction/CLEAR_CONFIRM_TRANSACTION' describe('Confirm Transaction Duck', () => { @@ -64,7 +67,8 @@ describe('Confirm Transaction Duck', () => { ethTransactionTotal: '469.27', hexGasTotal: '0x1319718a5000', nonce: '0x0', - fetchingMethodData: false, + toSmartContract: false, + fetchingData: false, }, } @@ -271,30 +275,43 @@ describe('Confirm Transaction Duck', () => { ) }) - it('should set fetchingMethodData to true when receiving a FETCH_METHOD_DATA_START action', () => { + it('should update nonce when receiving an UPDATE_TO_SMART_CONTRACT action', () => { assert.deepEqual( ConfirmTransactionReducer(mockState, { - type: FETCH_METHOD_DATA_START, + type: UPDATE_TO_SMART_CONTRACT, + payload: true, }), { ...mockState.confirmTransaction, - fetchingMethodData: true, + toSmartContract: true, } ) }) - it('should set fetchingMethodData to false when receiving a FETCH_METHOD_DATA_END action', () => { + it('should set fetchingData to true when receiving a FETCH_DATA_START action', () => { assert.deepEqual( - ConfirmTransactionReducer({ confirmTransaction: { fetchingMethodData: true } }, { - type: FETCH_METHOD_DATA_END, + ConfirmTransactionReducer(mockState, { + type: FETCH_DATA_START, }), { - fetchingMethodData: false, + ...mockState.confirmTransaction, + fetchingData: true, } ) }) - it('should clear confirmTransaction when receiving a FETCH_METHOD_DATA_END action', () => { + it('should set fetchingData to false when receiving a FETCH_DATA_END action', () => { + assert.deepEqual( + ConfirmTransactionReducer({ confirmTransaction: { fetchingData: true } }, { + type: FETCH_DATA_END, + }), + { + fetchingData: false, + } + ) + }) + + it('should clear confirmTransaction when receiving a FETCH_DATA_END action', () => { assert.deepEqual( ConfirmTransactionReducer(mockState, { type: CLEAR_CONFIRM_TRANSACTION, @@ -460,24 +477,24 @@ describe('Confirm Transaction Duck', () => { ) }) - it('should create an action to set fetchingMethodData to true', () => { + it('should create an action to set fetchingData to true', () => { const expectedAction = { - type: FETCH_METHOD_DATA_START, + type: FETCH_DATA_START, } assert.deepEqual( - actions.setFetchingMethodData(true), + actions.setFetchingData(true), expectedAction ) }) - it('should create an action to set fetchingMethodData to false', () => { + it('should create an action to set fetchingData to false', () => { const expectedAction = { - type: FETCH_METHOD_DATA_END, + type: FETCH_DATA_END, } assert.deepEqual( - actions.setFetchingMethodData(false), + actions.setFetchingData(false), expectedAction ) }) @@ -495,6 +512,18 @@ describe('Confirm Transaction Duck', () => { }) describe('Thunk actions', done => { + beforeEach(() => { + global.eth = { + getCode: sinon.stub().callsFake( + address => Promise.resolve(address && address.match(/isContract/) ? 'not-0x' : '0x') + ), + } + }) + + afterEach(() => { + global.eth.getCode.resetHistory() + }) + it('updates txData and gas on an existing transaction in confirmTransaction', () => { const mockState = { metamask: { @@ -505,7 +534,7 @@ describe('Confirm Transaction Duck', () => { ethTransactionAmount: '1', ethTransactionFee: '0.000021', ethTransactionTotal: '1.000021', - fetchingMethodData: false, + fetchingData: false, fiatTransactionAmount: '469.26', fiatTransactionFee: '0.01', fiatTransactionTotal: '469.27', @@ -581,7 +610,7 @@ describe('Confirm Transaction Duck', () => { ethTransactionAmount: '1', ethTransactionFee: '0.000021', ethTransactionTotal: '1.000021', - fetchingMethodData: false, + fetchingData: false, fiatTransactionAmount: '469.26', fiatTransactionFee: '0.01', fiatTransactionTotal: '469.27', @@ -667,6 +696,7 @@ describe('Confirm Transaction Duck', () => { .then(() => { const storeActions = store.getActions() assert.equal(storeActions.length, expectedActions.length) + storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index])) done() }) diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js index 76e80a8ac..d1a4994e4 100644 --- a/ui/app/helpers/confirm-transaction/util.js +++ b/ui/app/helpers/confirm-transaction/util.js @@ -1,15 +1,8 @@ import currencyFormatter from 'currency-formatter' import currencies from 'currency-formatter/currencies' -import abi from 'human-standard-token-abi' -import abiDecoder from 'abi-decoder' import ethUtil from 'ethereumjs-util' import BigNumber from 'bignumber.js' -abiDecoder.addABI(abi) - -import MethodRegistry from 'eth-method-registry' -const registry = new MethodRegistry({ provider: global.ethereumProvider }) - import { conversionUtil, addCurrencies, @@ -19,22 +12,6 @@ import { import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction' -export function getTokenData (data = {}) { - return abiDecoder.decodeMethod(data) -} - -export async function getMethodData (data = {}) { - const prefixedData = ethUtil.addHexPrefix(data) - const fourBytePrefix = prefixedData.slice(0, 10) - const sig = await registry.lookup(fourBytePrefix) - const parsedResult = registry.parse(sig) - - return { - name: parsedResult.name, - params: parsedResult.args, - } -} - export function increaseLastGasPrice (lastGasPrice) { return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, { multiplicandBase: 16, @@ -76,7 +53,7 @@ export function addFiat (...args) { }) } -export function getTransactionAmount ({ +export function getValueFromWeiHex ({ value, toCurrency, conversionRate, diff --git a/ui/app/helpers/confirm-transaction/util.test.js b/ui/app/helpers/confirm-transaction/util.test.js index a9c8fae34..4c1a3e16b 100644 --- a/ui/app/helpers/confirm-transaction/util.test.js +++ b/ui/app/helpers/confirm-transaction/util.test.js @@ -92,9 +92,9 @@ describe('Confirm Transaction utils', () => { }) }) - describe('getTransactionAmount', () => { + describe('getValueFromWeiHex', () => { it('should get the transaction amount in ETH', () => { - const ethTransactionAmount = utils.getTransactionAmount({ + const ethTransactionAmount = utils.getValueFromWeiHex({ value: '0xde0b6b3a7640000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6, }) @@ -102,7 +102,7 @@ describe('Confirm Transaction utils', () => { }) it('should get the transaction amount in fiat', () => { - const fiatTransactionAmount = utils.getTransactionAmount({ + const fiatTransactionAmount = utils.getValueFromWeiHex({ value: '0xde0b6b3a7640000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2, }) diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js new file mode 100644 index 000000000..1dec216fa --- /dev/null +++ b/ui/app/helpers/conversions.util.js @@ -0,0 +1,37 @@ +import { conversionUtil } from '../conversion-util' + +export function hexToDecimal (hexValue) { + return conversionUtil(hexValue, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + }) +} + +export function getEthFromWeiHex ({ + value, + conversionRate, +}) { + return getValueFromWeiHex({ + value, + conversionRate, + toCurrency: 'ETH', + numberOfDecimals: 6, + }) +} + +export function getValueFromWeiHex ({ + value, + toCurrency, + conversionRate, + numberOfDecimals, +}) { + return conversionUtil(value, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency, + numberOfDecimals, + fromDenomination: 'WEI', + conversionRate, + }) +} diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js new file mode 100644 index 000000000..54df54aa8 --- /dev/null +++ b/ui/app/helpers/transactions.util.js @@ -0,0 +1,105 @@ +import ethUtil from 'ethereumjs-util' +import MethodRegistry from 'eth-method-registry' +import abi from 'human-standard-token-abi' +import abiDecoder from 'abi-decoder' + +import { + TOKEN_METHOD_TRANSFER, + TOKEN_METHOD_APPROVE, + TOKEN_METHOD_TRANSFER_FROM, + SEND_ETHER_ACTION_KEY, + DEPLOY_CONTRACT_ACTION_KEY, + APPROVE_ACTION_KEY, + SEND_TOKEN_ACTION_KEY, + TRANSFER_FROM_ACTION_KEY, + SIGNATURE_REQUEST_KEY, + UNKNOWN_FUNCTION_KEY, +} from '../constants/transactions' + +abiDecoder.addABI(abi) + +export function getTokenData (data = {}) { + return abiDecoder.decodeMethod(data) +} + +const registry = new MethodRegistry({ provider: global.ethereumProvider }) + +export async function getMethodData (data = {}) { + const prefixedData = ethUtil.addHexPrefix(data) + const fourBytePrefix = prefixedData.slice(0, 10) + const sig = await registry.lookup(fourBytePrefix) + const parsedResult = registry.parse(sig) + + return { + name: parsedResult.name, + params: parsedResult.args, + } +} + +export function isConfirmDeployContract (txData = {}) { + const { txParams = {} } = txData + return !txParams.to +} + +export async function getTransactionActionKey (transaction, methodData) { + const { txParams: { data, to } = {}, msgParams } = transaction + + if (msgParams) { + return SIGNATURE_REQUEST_KEY + } + + if (isConfirmDeployContract(transaction)) { + return DEPLOY_CONTRACT_ACTION_KEY + } + + if (data) { + const toSmartContract = await isSmartContractAddress(to) + + if (!toSmartContract) { + return SEND_ETHER_ACTION_KEY + } + + const { name } = methodData + const methodName = name && name.toLowerCase() + + if (!methodName) { + return UNKNOWN_FUNCTION_KEY + } + + switch (methodName) { + case TOKEN_METHOD_TRANSFER: + return SEND_TOKEN_ACTION_KEY + case TOKEN_METHOD_APPROVE: + return APPROVE_ACTION_KEY + case TOKEN_METHOD_TRANSFER_FROM: + return TRANSFER_FROM_ACTION_KEY + default: + return name + } + } else { + return SEND_ETHER_ACTION_KEY + } +} + +export function getLatestSubmittedTxWithNonce (transactions = [], nonce = '0x0') { + if (!transactions.length) { + return {} + } + + return transactions.reduce((acc, current) => { + const { submittedTime, txParams: { nonce: currentNonce } = {} } = current + + if (currentNonce === nonce) { + return acc.submittedTime + ? submittedTime > acc.submittedTime ? current : acc + : current + } else { + return acc + } + }, {}) +} + +export async function isSmartContractAddress (address) { + const code = await global.eth.getCode(address) + return code && code !== '0x' +} diff --git a/ui/app/higher-order-components/with-method-data/index.js b/ui/app/higher-order-components/with-method-data/index.js new file mode 100644 index 000000000..f511e1ae7 --- /dev/null +++ b/ui/app/higher-order-components/with-method-data/index.js @@ -0,0 +1 @@ +export { default } from './with-method-data.component' diff --git a/ui/app/higher-order-components/with-method-data/with-method-data.component.js b/ui/app/higher-order-components/with-method-data/with-method-data.component.js new file mode 100644 index 000000000..fed7d9865 --- /dev/null +++ b/ui/app/higher-order-components/with-method-data/with-method-data.component.js @@ -0,0 +1,52 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { getMethodData } from '../../helpers/transactions.util' + +export default function withMethodData (WrappedComponent) { + return class MethodDataWrappedComponent extends PureComponent { + static propTypes = { + transaction: PropTypes.object, + } + + static defaultProps = { + transaction: {}, + } + + state = { + methodData: {}, + done: false, + error: null, + } + + componentDidMount () { + this.fetchMethodData() + } + + async fetchMethodData () { + const { transaction } = this.props + const { txParams: { data = '' } = {} } = transaction + + if (data) { + try { + const methodData = await getMethodData(data) + this.setState({ methodData, done: true }) + } catch (error) { + this.setState({ done: true, error }) + } + } else { + this.setState({ done: true }) + } + } + + render () { + const { methodData, done, error } = this.state + + return ( + + ) + } + } +} diff --git a/ui/app/higher-order-components/with-token-tracker/index.js b/ui/app/higher-order-components/with-token-tracker/index.js new file mode 100644 index 000000000..d401e81f1 --- /dev/null +++ b/ui/app/higher-order-components/with-token-tracker/index.js @@ -0,0 +1 @@ +export { default } from './with-token-tracker.component' diff --git a/ui/app/helpers/with-token-tracker.js b/ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js similarity index 96% rename from ui/app/helpers/with-token-tracker.js rename to ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js index 8608b15f4..36f6a6efd 100644 --- a/ui/app/helpers/with-token-tracker.js +++ b/ui/app/higher-order-components/with-token-tracker/with-token-tracker.component.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import TokenTracker from 'eth-token-tracker' -const withTokenTracker = WrappedComponent => { +export default function withTokenTracker (WrappedComponent) { return class TokenTrackerWrappedComponent extends Component { static propTypes = { userAddress: PropTypes.string.isRequired, @@ -104,5 +104,3 @@ const withTokenTracker = WrappedComponent => { } } } - -module.exports = withTokenTracker diff --git a/ui/app/i18n-provider.js b/ui/app/i18n-provider.js index d46911f7c..3419474c4 100644 --- a/ui/app/i18n-provider.js +++ b/ui/app/i18n-provider.js @@ -6,6 +6,11 @@ const { compose } = require('recompose') const t = require('../i18n-helper').getMessage class I18nProvider extends Component { + tOrDefault = (key, defaultValue, ...args) => { + const { localeMessages: { current, en } = {} } = this.props + return t(current, key, ...args) || t(en, key, ...args) || defaultValue + } + getChildContext () { const { localeMessages } = this.props const { current, en } = localeMessages @@ -13,6 +18,10 @@ class I18nProvider extends Component { t (key, ...args) { return t(current, key, ...args) || t(en, key, ...args) || `[${key}]` }, + tOrDefault: this.tOrDefault, + tOrKey (key, ...args) { + return this.tOrDefault(key, key, ...args) + }, } } @@ -28,6 +37,8 @@ I18nProvider.propTypes = { I18nProvider.childContextTypes = { t: PropTypes.func, + tOrDefault: PropTypes.func, + tOrKey: PropTypes.func, } const mapStateToProps = state => { diff --git a/ui/app/main-container.js b/ui/app/main-container.js deleted file mode 100644 index 8a0708025..000000000 --- a/ui/app/main-container.js +++ /dev/null @@ -1,49 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const AccountAndTransactionDetails = require('./account-and-transaction-details') -const Settings = require('./components/pages/settings') -const log = require('loglevel') - -import UnlockScreen from './components/pages/unlock-page' - -module.exports = MainContainer - -inherits(MainContainer, Component) -function MainContainer () { - Component.call(this) -} - -MainContainer.prototype.render = function () { - // 3. summarize: - // switch statement goes inside MainContainer, - // or a method in renderPrimary - // - pass resulting h() to MainContainer - // - error checking in separate func - // - router in separate func - const contents = { - component: AccountAndTransactionDetails, - key: 'account-detail', - style: {}, - } - - if (this.props.isUnlocked === false) { - switch (this.props.currentViewName) { - case 'config': - log.debug('rendering config screen from unlock screen.') - return h(Settings, {key: 'config'}) - default: - log.debug('rendering locked screen') - return h('.unlock-screen-container', {}, h(UnlockScreen, { key: 'locked' })) - } - } - - return h('div.main-container', { - style: contents.style, - }, [ - h(contents.component, { - key: contents.key, - }, []), - ]) -} - diff --git a/ui/app/new-keychain.js b/ui/app/new-keychain.js deleted file mode 100644 index cc9633166..000000000 --- a/ui/app/new-keychain.js +++ /dev/null @@ -1,29 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(NewKeychain) - -function mapStateToProps (state) { - return {} -} - -inherits(NewKeychain, Component) -function NewKeychain () { - Component.call(this) -} - -NewKeychain.prototype.render = function () { - // const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - h('h1', `Here's a list!!!!`), - ]) - ) -} diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index c246e7904..5c86d397d 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -48,7 +48,11 @@ function reduceApp (state, action) { name: null, }, }, - sidebarOpen: false, + sidebar: { + isOpen: false, + transitionName: '', + type: '', + }, alertOpen: false, alertMessage: null, qrCodeData: null, @@ -88,12 +92,18 @@ function reduceApp (state, action) { // sidebar methods case actions.SIDEBAR_OPEN: return extend(appState, { - sidebarOpen: true, + sidebar: { + ...action.value, + isOpen: true, + }, }) case actions.SIDEBAR_CLOSE: return extend(appState, { - sidebarOpen: false, + sidebar: { + ...appState.sidebar, + isOpen: false, + }, }) // alert methods @@ -209,6 +219,15 @@ function reduceApp (state, action) { transForward: action.value, }) + case actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'add-suggested-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + case actions.SHOW_IMPORT_PAGE: return extend(appState, { currentView: { diff --git a/ui/app/routes.js b/ui/app/routes.js index f6b2a7a55..76afed5db 100644 --- a/ui/app/routes.js +++ b/ui/app/routes.js @@ -7,6 +7,7 @@ const CONFIRM_SEED_ROUTE = '/confirm-seed' const RESTORE_VAULT_ROUTE = '/restore-vault' const ADD_TOKEN_ROUTE = '/add-token' const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token' +const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token' const NEW_ACCOUNT_ROUTE = '/new-account' const IMPORT_ACCOUNT_ROUTE = '/new-account/import' const CONNECT_HARDWARE_ROUTE = '/new-account/connect' @@ -41,6 +42,7 @@ module.exports = { RESTORE_VAULT_ROUTE, ADD_TOKEN_ROUTE, CONFIRM_ADD_TOKEN_ROUTE, + CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, CONNECT_HARDWARE_ROUTE, diff --git a/ui/app/selectors.js b/ui/app/selectors.js index d86462275..fb4517628 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -1,6 +1,9 @@ -const valuesFor = require('./util').valuesFor const abi = require('human-standard-token-abi') +import { + transactionsSelector, +} from './selectors/transactions' + const { multiplyCurrencies, } = require('./conversion-util') @@ -11,6 +14,8 @@ const selectors = { getSelectedAccount, getSelectedToken, getSelectedTokenExchangeRate, + getSelectedTokenAssetImage, + getAssetImages, getTokenExchangeRate, conversionRateSelector, transactionsSelector, @@ -68,6 +73,18 @@ function getSelectedTokenExchangeRate (state) { return contractExchangeRates[address] || 0 } +function getSelectedTokenAssetImage (state) { + const assetImages = state.metamask.assetImages || {} + const selectedToken = getSelectedToken(state) || {} + const { address } = selectedToken + return assetImages[address] +} + +function getAssetImages (state) { + const assetImages = state.metamask.assetImages || {} + return assetImages +} + function getTokenExchangeRate (state, address) { const contractExchangeRates = state.metamask.contractExchangeRates return contractExchangeRates[address] || 0 @@ -101,22 +118,6 @@ function getCurrentAccountWithSendEtherInfo (state) { return accounts.find(({ address }) => address === currentAddress) } -function transactionsSelector (state) { - const { network, selectedTokenAddress } = state.metamask - const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs) - const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined - const transactions = state.metamask.selectedAddressTxList || [] - const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) - - // console.log({txsToRender, selectedTokenAddress}) - return selectedTokenAddress - ? txsToRender - .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress) - .sort((a, b) => b.time - a.time) - : txsToRender - .sort((a, b) => b.time - a.time) -} - function getGasIsLoading (state) { return state.appState.gasIsLoading } diff --git a/ui/app/selectors/tokens.js b/ui/app/selectors/tokens.js new file mode 100644 index 000000000..47b6e0192 --- /dev/null +++ b/ui/app/selectors/tokens.js @@ -0,0 +1,11 @@ +import { createSelector } from 'reselect' + +export const selectedTokenAddressSelector = state => state.metamask.selectedTokenAddress +export const tokenSelector = state => state.metamask.tokens +export const selectedTokenSelector = createSelector( + tokenSelector, + selectedTokenAddressSelector, + (tokens = [], selectedTokenAddress = '') => { + return tokens.find(({ address }) => address === selectedTokenAddress) + } +) diff --git a/ui/app/selectors/transactions.js b/ui/app/selectors/transactions.js new file mode 100644 index 000000000..3e9843722 --- /dev/null +++ b/ui/app/selectors/transactions.js @@ -0,0 +1,58 @@ +import { createSelector } from 'reselect' +import { valuesFor } from '../util' +import { + UNAPPROVED_STATUS, + APPROVED_STATUS, + SUBMITTED_STATUS, +} from '../constants/transactions' + +import { selectedTokenAddressSelector } from './tokens' + +export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList +export const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs +export const selectedAddressTxListSelector = state => state.metamask.selectedAddressTxList + +const pendingStatusHash = { + [UNAPPROVED_STATUS]: true, + [APPROVED_STATUS]: true, + [SUBMITTED_STATUS]: true, +} + +export const transactionsSelector = createSelector( + selectedTokenAddressSelector, + unapprovedMsgsSelector, + shapeShiftTxListSelector, + selectedAddressTxListSelector, + (selectedTokenAddress, unapprovedMsgs = {}, shapeShiftTxList = [], transactions = []) => { + const unapprovedMsgsList = valuesFor(unapprovedMsgs) + const txsToRender = transactions.concat(unapprovedMsgsList, shapeShiftTxList) + + return selectedTokenAddress + ? txsToRender + .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress) + .sort((a, b) => b.time - a.time) + : txsToRender + .sort((a, b) => b.time - a.time) + } +) + +export const pendingTransactionsSelector = createSelector( + transactionsSelector, + (transactions = []) => ( + transactions.filter(transaction => transaction.status in pendingStatusHash) + ) +) + +export const submittedPendingTransactionsSelector = createSelector( + transactionsSelector, + (transactions = []) => ( + transactions.filter(transaction => transaction.status === SUBMITTED_STATUS) + ) +) + +export const completedTransactionsSelector = createSelector( + transactionsSelector, + (transactions = []) => ( + transactions.filter(transaction => !(transaction.status in pendingStatusHash)) + ) +) diff --git a/ui/app/token-util.js b/ui/app/token-util.js index 8798ed266..3d61ad1ca 100644 --- a/ui/app/token-util.js +++ b/ui/app/token-util.js @@ -1,55 +1,113 @@ const log = require('loglevel') const util = require('./util') const BigNumber = require('bignumber.js') +import contractMap from 'eth-contract-metadata' -function tokenInfoGetter () { - const tokens = {} +const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { + return { + ...acc, + [base.toLowerCase()]: contractMap[base], + } +}, {}) - return async (address) => { - if (tokens[address]) { - return tokens[address] +const DEFAULT_SYMBOL = '' +const DEFAULT_DECIMALS = '0' + +async function getSymbolFromContract (tokenAddress) { + const token = util.getContractAtAddress(tokenAddress) + + try { + const result = await token.symbol() + return result[0] + } catch (error) { + log.warn(`symbol() call for token at address ${tokenAddress} resulted in error:`, error) + } +} + +async function getDecimalsFromContract (tokenAddress) { + const token = util.getContractAtAddress(tokenAddress) + + try { + const result = await token.decimals() + const decimalsBN = result[0] + return decimalsBN && decimalsBN.toString() + } catch (error) { + log.warn(`decimals() call for token at address ${tokenAddress} resulted in error:`, error) + } +} + +function getContractMetadata (tokenAddress) { + return tokenAddress && casedContractMap[tokenAddress.toLowerCase()] +} + +async function getSymbol (tokenAddress) { + let symbol = await getSymbolFromContract(tokenAddress) + + if (!symbol) { + const contractMetadataInfo = getContractMetadata(tokenAddress) + + if (contractMetadataInfo) { + symbol = contractMetadataInfo.symbol } + } - tokens[address] = await getSymbolAndDecimals(address) + return symbol +} - return tokens[address] +async function getDecimals (tokenAddress) { + let decimals = await getDecimalsFromContract(tokenAddress) + + if (!decimals || decimals === '0') { + const contractMetadataInfo = getContractMetadata(tokenAddress) + + if (contractMetadataInfo) { + decimals = contractMetadataInfo.decimals + } } + + return decimals } -async function getSymbolAndDecimals (tokenAddress, existingTokens = []) { +export async function getSymbolAndDecimals (tokenAddress, existingTokens = []) { const existingToken = existingTokens.find(({ address }) => tokenAddress === address) + if (existingToken) { - return existingToken + return { + symbol: existingToken.symbol, + decimals: existingToken.decimals, + } } - let result = [] + let symbol, decimals + try { - const token = util.getContractAtAddress(tokenAddress) + symbol = await getSymbol(tokenAddress) + decimals = await getDecimals(tokenAddress) + } catch (error) { + log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, error) + } - result = await Promise.all([ - token.symbol(), - token.decimals(), - ]) - } catch (err) { - log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, err) + return { + symbol: symbol || DEFAULT_SYMBOL, + decimals: decimals || DEFAULT_DECIMALS, } +} + +export function tokenInfoGetter () { + const tokens = {} - const [ symbol = [], decimals = [] ] = result + return async (address) => { + if (tokens[address]) { + return tokens[address] + } - return { - symbol: symbol[0] || null, - decimals: decimals[0] && decimals[0].toString() || null, + tokens[address] = await getSymbolAndDecimals(address) + + return tokens[address] } } -function calcTokenAmount (value, decimals) { +export function calcTokenAmount (value, decimals) { const multiplier = Math.pow(10, Number(decimals || 0)) return new BigNumber(String(value)).div(multiplier).toNumber() } - - -module.exports = { - tokenInfoGetter, - calcTokenAmount, - getSymbolAndDecimals, -} diff --git a/ui/app/util.js b/ui/app/util.js index ade4fec8a..37c0fb698 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -9,7 +9,7 @@ const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) // formatData :: ( date: ) -> String function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') + return vreme.format(new Date(date), '3/16/2014 at 14:30') } var valueTable = {