1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge branch 'develop' into network-remove-provider-engine

Override package-lock and fix merge conflicts
This commit is contained in:
Thomas 2018-08-14 10:44:42 -07:00
commit 96d789d2cf
67 changed files with 1460 additions and 427 deletions

34
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Bug Report
about: Using MetaMask, but it's not working as you expect?
---
<!--
BEFORE SUBMITTING: PLEASE SEARCH TO MAKE SURE THIS ISSUE HAS NOT BEEN SUBMITTED
-->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Browser details (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- MetaMask Version [e.g. 4.9.0]
- Old UI or New / Beta UI?
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,14 @@
---
name: Feature Request
about: Looking for a feature that doesn't exist? Let us know!
---
**What problem are you trying to solve?**
A short description of what you're trying to do. E.g., "My users need to wrap ETH, but they're intimidated by the confirm screen..." or "I'm trying to debug my application, and XYZ..."
**Describe the solution you'd like**
A clear and concise description of what you want to happen. Try to also include any alternative solutions you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,9 @@
---
name: Support Request or Question
about: Have a question about how to use MetaMask?
---
FOR USER QUESTIONS, PLEASE DO NOT OPEN A GITHUB ISSUE - IT WILL NOT BE HANDLED HERE.
INSTEAD, PLEASE EMAIL SUPPORT@METAMASK.IO WITH A DESCRIPTION OF YOUR PROBLEM.

1
.npmrc
View File

@ -1 +0,0 @@
engine-strict=true

View File

@ -1,13 +1,28 @@
# Changelog # Changelog
## Current Master ## Current Develop Branch
- Add new tokens auto detection ## 4.9.1 Mon Aug 09 2018
- Remove rejected transactions from transaction history
- Add Trezor Support - [#4884](https://github.com/MetaMask/metamask-extension/pull/4884): Allow to have tokens per account and network.
- Allow to remove accounts (Imported and Hardware Wallets) - [#4989](https://github.com/MetaMask/metamask-extension/pull/4989): Continue to use original signedTypedData.
- [#5010](https://github.com/MetaMask/metamask-extension/pull/5010): Fix ENS resolution issues.
- [#5000](https://github.com/MetaMask/metamask-extension/pull/5000): Show error while allowing confirmation of tx where simulation fails.
- [#4995](https://github.com/MetaMask/metamask-extension/pull/4995): Shows retry button on dApp initialized transactions.
## 4.9.0 Mon Aug 07 2018
- [#4926](https://github.com/MetaMask/metamask-extension/pull/4926): Show retry button on the latest tx of the earliest nonce.
- [#4888](https://github.com/MetaMask/metamask-extension/pull/4888): Suggest using the new user interface.
- [#4947](https://github.com/MetaMask/metamask-extension/pull/4947): Prevent sending multiple transasctions on multiple confirm clicks.
- [#4844](https://github.com/MetaMask/metamask-extension/pull/4844): Add new tokens auto detection.
- [#4667](https://github.com/MetaMask/metamask-extension/pull/4667): Remove rejected transactions from transaction history.
- [#4625](https://github.com/MetaMask/metamask-extension/pull/4625): Add Trezor Support.
- [#4625](https://github.com/MetaMask/metamask-extension/pull/4625/commits/523cf9ad33d88719520ae5e7293329d133b64d4d): Allow to remove accounts (Imported and Hardware Wallets)
- [#4814](https://github.com/MetaMask/metamask-extension/pull/4814): Add hex data input to send screen.
- [#4691](https://github.com/MetaMask/metamask-extension/pull/4691): Redesign of the Confirm Transaction Screen.
- [#4840](https://github.com/MetaMask/metamask-extension/pull/4840): Now shows notifications when transactions are completed. - [#4840](https://github.com/MetaMask/metamask-extension/pull/4840): Now shows notifications when transactions are completed.
- [#4855](https://github.com/MetaMask/metamask-extension/pull/4855): network.js: convert rpc protocol to lower case. - [#4855](https://github.com/MetaMask/metamask-extension/pull/4855): Allow the use of HTTP prefix for custom rpc urls.
## 4.8.0 Thur Jun 14 2018 ## 4.8.0 Thur Jun 14 2018

View File

@ -84,7 +84,7 @@
"message": "Auf Coinbase kaufen" "message": "Auf Coinbase kaufen"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbase ist die weltweit bekannteste Art und Weise um bitcoin, ethereum und litecoin zu kaufen und verkaufen." "message": "Coinbase ist die weltweit bekannteste Art und Weise um Bitcoin, Ethereum und Litecoin zu kaufen und verkaufen."
}, },
"ok": { "ok": {
"message": "Ok" "message": "Ok"
@ -828,7 +828,7 @@
"message": "Willkommen zur neuen Oberfläche (Beta)" "message": "Willkommen zur neuen Oberfläche (Beta)"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "Du verwendest nun die neue Metamask Oberfläche. Schau dich um, teste die neuen Features wie z.B. das Senden von Token und lass es uns wissen falls du irgendwelche Probleme hast." "message": "Du verwendest nun die neue MetaMask Oberfläche. Schau dich um, teste die neuen Features wie z.B. das Senden von Token und lass es uns wissen falls du irgendwelche Probleme hast."
}, },
"unapproved": { "unapproved": {
"message": "Nicht genehmigt" "message": "Nicht genehmigt"

View File

@ -2,6 +2,9 @@
"accept": { "accept": {
"message": "Accept" "message": "Accept"
}, },
"accessingYourCamera": {
"message": "Accesing your camera..."
},
"account": { "account": {
"message": "Account" "message": "Account"
}, },
@ -96,7 +99,7 @@
"message": "Buy on Coinbase" "message": "Buy on Coinbase"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbase is the worlds most popular way to buy and sell bitcoin, ethereum, and litecoin." "message": "Coinbase is the worlds most popular way to buy and sell Bitcoin, Ethereum, and Litecoin."
}, },
"bytes": { "bytes": {
"message": "Bytes" "message": "Bytes"
@ -117,7 +120,7 @@
"message": "Close" "message": "Close"
}, },
"chromeRequiredForTrezor":{ "chromeRequiredForTrezor":{
"message": "You need to use Metamask on Google Chrome in order to connect to your TREZOR device." "message": "You need to use MetaMask on Google Chrome in order to connect to your TREZOR device."
}, },
"confirm": { "confirm": {
"message": "Confirm" "message": "Confirm"
@ -147,7 +150,7 @@
"message": "Connect to Trezor" "message": "Connect to Trezor"
}, },
"connectToTrezorHelp": { "connectToTrezorHelp": {
"message": "Metamask is able to access your TREZOR ethereum accounts. First make sure your device is connected and unlocked." "message": "MetaMask is able to access your TREZOR Ethereum accounts. First make sure your device is connected and unlocked."
}, },
"connectToTrezorTrouble": { "connectToTrezorTrouble": {
"message": "If you are having trouble, please make sure you are using the latest version of the TREZOR firmware." "message": "If you are having trouble, please make sure you are using the latest version of the TREZOR firmware."
@ -656,6 +659,12 @@
"notStarted": { "notStarted": {
"message": "Not Started" "message": "Not Started"
}, },
"noWebcamFoundTitle": {
"message": "Webcam not found"
},
"noWebcamFound": {
"message": "Your computer's webcam was not found. Please try again."
},
"oldUI": { "oldUI": {
"message": "Old UI" "message": "Old UI"
}, },
@ -940,6 +949,12 @@
"info": { "info": {
"message": "Info" "message": "Info"
}, },
"scanInstructions": {
"message": "Place the QR code in front of your camera"
},
"scanQrCode": {
"message": "Scan QR Code"
},
"shapeshiftBuy": { "shapeshiftBuy": {
"message": "Buy with Shapeshift" "message": "Buy with Shapeshift"
}, },
@ -1059,6 +1074,9 @@
"message": "We had trouble loading your token balances. You can view them ", "message": "We had trouble loading your token balances. You can view them ",
"description": "Followed by a link (here) to view token balances" "description": "Followed by a link (here) to view token balances"
}, },
"tryAgain": {
"message": "Try again"
},
"twelveWords": { "twelveWords": {
"message": "These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret." "message": "These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret."
}, },
@ -1069,7 +1087,7 @@
"message": "Welcome to the New UI (Beta)" "message": "Welcome to the New UI (Beta)"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "You are now using the new Metamask UI." "message": "You are now using the new MetaMask UI."
}, },
"unapproved": { "unapproved": {
"message": "Unapproved" "message": "Unapproved"
@ -1089,6 +1107,15 @@
"unknownNetworkId": { "unknownNetworkId": {
"message": "Unknown network ID" "message": "Unknown network ID"
}, },
"unknownQrCode": {
"message": "Error: We couldn't identify that QR code"
},
"unknownCameraErrorTitle": {
"message": "Ooops! Something went wrong...."
},
"unknownCameraError": {
"message": "There was an error while trying to access you camera. Please try again..."
},
"unlock": { "unlock": {
"message": "Unlock" "message": "Unlock"
}, },
@ -1135,6 +1162,9 @@
"whatsThis": { "whatsThis": {
"message": "What's this?" "message": "What's this?"
}, },
"youNeedToAllowCameraAccess": {
"message": "You need to allow camera access to use this feature."
},
"yourSigRequested": { "yourSigRequested": {
"message": "Your signature is being requested" "message": "Your signature is being requested"
}, },

View File

@ -75,7 +75,7 @@
"message": "Pedir prestado con Dharma (Beta)" "message": "Pedir prestado con Dharma (Beta)"
}, },
"builtInCalifornia": { "builtInCalifornia": {
"message": "Metamask fue diseñado y construido en California" "message": "MetaMask fue diseñado y construido en California"
}, },
"buy": { "buy": {
"message": "Comprar" "message": "Comprar"
@ -874,7 +874,7 @@
"message": "Advertencia" "message": "Advertencia"
}, },
"welcomeBeta": { "welcomeBeta": {
"message": "Bienvenido a Metamask Beta" "message": "Bienvenido a MetaMask Beta"
}, },
"whatsThis": { "whatsThis": {
"message": "¿Qué es esto?" "message": "¿Qué es esto?"

View File

@ -63,7 +63,7 @@
"message": "Acheter sur Coinbase" "message": "Acheter sur Coinbase"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbase est le moyen le plus populaire au monde d'acheter et de vendre du bitcoin, de l'ethereum et du litecoin." "message": "Coinbase est le moyen le plus populaire au monde d'acheter et de vendre du Bitcoin, de l'Ethereum et du Litecoin."
}, },
"cancel": { "cancel": {
"message": "Annuler" "message": "Annuler"
@ -570,7 +570,7 @@
"message": "Bienvenue dans la nouvelle interface utilisateur (Beta)" "message": "Bienvenue dans la nouvelle interface utilisateur (Beta)"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "Vous utilisez maintenant la nouvelle interface utilisateur Metamask. Jetez un coup d'oeil, essayez de nouvelles fonctionnalités comme l'envoi de jetons, et faites-nous savoir si vous avez des problèmes." "message": "Vous utilisez maintenant la nouvelle interface utilisateur MetaMask. Jetez un coup d'oeil, essayez de nouvelles fonctionnalités comme l'envoi de jetons, et faites-nous savoir si vous avez des problèmes."
}, },
"unavailable": { "unavailable": {
"message": "Indisponible" "message": "Indisponible"

View File

@ -81,7 +81,7 @@
"message": "Compra su Coinbase" "message": "Compra su Coinbase"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbase è il servizio più popolare al mondo per comprare e vendere bitcoin, ethereum e litecoin." "message": "Coinbase è il servizio più popolare al mondo per comprare e vendere Bitcoin, Ethereum e Litecoin."
}, },
"cancel": { "cancel": {
"message": "Cancella" "message": "Cancella"
@ -178,7 +178,7 @@
"message": "La rete predefinita per transazioni in Ether è la Rete Ethereum Principale." "message": "La rete predefinita per transazioni in Ether è la Rete Ethereum Principale."
}, },
"denExplainer": { "denExplainer": {
"message": "Il DEN è il tuo archivio crittato con password dentro Metamask." "message": "Il DEN è il tuo archivio crittato con password dentro MetaMask."
}, },
"deposit": { "deposit": {
"message": "Deposita" "message": "Deposita"
@ -816,4 +816,4 @@
"youSign": { "youSign": {
"message": "Ti stai connettendo" "message": "Ti stai connettendo"
} }
} }

View File

@ -81,7 +81,7 @@
"message": "Koop op Coinbase" "message": "Koop op Coinbase"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbase is 's werelds populairste manier om bitcoin, ethereum en litecoin te kopen en verkopen." "message": "Coinbase is 's werelds populairste manier om Bitcoin, Ethereum en Litecoin te kopen en verkopen."
}, },
"cancel": { "cancel": {
"message": "Annuleer" "message": "Annuleer"
@ -435,7 +435,7 @@
"message": "back-up woorden hebben alleen kleine letters" "message": "back-up woorden hebben alleen kleine letters"
}, },
"mainnet": { "mainnet": {
"message": "belangrijkste ethereum-netwerk" "message": "belangrijkste Ethereum-netwerk"
}, },
"message": { "message": {
"message": "Bericht" "message": "Bericht"
@ -762,7 +762,7 @@
"message": "Welkom bij de nieuwe gebruikersinterface (bèta)" "message": "Welkom bij de nieuwe gebruikersinterface (bèta)"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "U gebruikt nu de nieuwe gebruikersinterface van Metamask. Kijk rond, probeer nieuwe functies uit zoals het verzenden van tokens en laat ons weten of u problemen ondervindt." "message": "U gebruikt nu de nieuwe gebruikersinterface van MetaMask. Kijk rond, probeer nieuwe functies uit zoals het verzenden van tokens en laat ons weten of u problemen ondervindt."
}, },
"unavailable": { "unavailable": {
"message": "Niet beschikbaar" "message": "Niet beschikbaar"

View File

@ -63,7 +63,7 @@
"message": "Bumili sa Coinbase" "message": "Bumili sa Coinbase"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Ang Coinbase ang pinakasikat na paraan upang bumili at magbenta ng bitcoin, ethereum, at litecoin sa buong mundo." "message": "Ang Coinbase ang pinakasikat na paraan upang bumili at magbenta ng Bitcoin, Ethereum, at Litecoin sa buong mundo."
}, },
"cancel": { "cancel": {
"message": "Kanselahin" "message": "Kanselahin"

View File

@ -81,7 +81,7 @@
"message": "Comprar no Coinbase" "message": "Comprar no Coinbase"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbase é a forma mais conhecida para comprar e vender bitcoin, ethereum, e litecoin." "message": "Coinbase é a forma mais conhecida para comprar e vender Bitcoin, Ethereum, e Litecoin."
}, },
"cancel": { "cancel": {
"message": "Cancelar" "message": "Cancelar"

View File

@ -84,7 +84,7 @@
"message": "Купить на Coinbase" "message": "Купить на Coinbase"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Биржа Coinbase это наиболее популярный способ купить или продать bitcoin, ethereum и litecoin." "message": "Биржа Coinbase это наиболее популярный способ купить или продать Bitcoin, Ethereum и Litecoin."
}, },
"ok": { "ok": {
"message": "ОК" "message": "ОК"

View File

@ -762,7 +762,7 @@
"message": "ยินดีต้อนรับสู่หน้าตาใหม่ (เบต้า)" "message": "ยินดีต้อนรับสู่หน้าตาใหม่ (เบต้า)"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "ขณะนี้คุณใช้งาน Metamask หน้าตาใหม่แล้ว ลองใช้ความสามรถใหม่ ๆ เช่นการส่งโทเค็นและหากพบปัญหากรุณาแจ้งให้เราทราบ" "message": "ขณะนี้คุณใช้งาน MetaMask หน้าตาใหม่แล้ว ลองใช้ความสามรถใหม่ ๆ เช่นการส่งโทเค็นและหากพบปัญหากรุณาแจ้งให้เราทราบ"
}, },
"unavailable": { "unavailable": {
"message": "ใช้งานไม่ได้" "message": "ใช้งานไม่ได้"

View File

@ -84,7 +84,7 @@
"message": "Coinbase'de satın al" "message": "Coinbase'de satın al"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbase bitcoin, ethereum, and litecoin alıp satmanın dünyadaki en popüler yolu" "message": "Coinbase Bitcoin, Ethereum, and Litecoin alıp satmanın dünyadaki en popüler yolu"
}, },
"ok": { "ok": {
"message": "Tamam" "message": "Tamam"
@ -852,7 +852,7 @@
"message": "Yeni UI (Beta)'ya hoşgeldiniz" "message": "Yeni UI (Beta)'ya hoşgeldiniz"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "Şu anda yeni Metamask UI kullanmaktasınız. Gözatın, jeton gönderme gibi yeni özellikleri deneyin ve herhangi bir sorunlar karşılaşırsanız bize haber verin" "message": "Şu anda yeni MetaMask UI kullanmaktasınız. Gözatın, jeton gönderme gibi yeni özellikleri deneyin ve herhangi bir sorunlar karşılaşırsanız bize haber verin"
}, },
"unapproved": { "unapproved": {
"message": "Onaylanmadı" "message": "Onaylanmadı"
@ -909,4 +909,4 @@
"youSign": { "youSign": {
"message": "İmzalıyorsunuz" "message": "İmzalıyorsunuz"
} }
} }

View File

@ -570,7 +570,7 @@
"message": "Chào mừng bạn đến với giao diện mới (Beta)" "message": "Chào mừng bạn đến với giao diện mới (Beta)"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "Bạn đang sử dụng giao diện mới của Metamask. Chúng tôi khuyến khích bạn thử nghiệm và khám phá các tính năng mới như gửi token, và nếu bạn có gặp phải vấn đề gì khó khăn, xin hãy liên hệ ngay để chúng tôi có thể giúp đỡ bạn." "message": "Bạn đang sử dụng giao diện mới của MetaMask. Chúng tôi khuyến khích bạn thử nghiệm và khám phá các tính năng mới như gửi token, và nếu bạn có gặp phải vấn đề gì khó khăn, xin hãy liên hệ ngay để chúng tôi có thể giúp đỡ bạn."
}, },
"unavailable": { "unavailable": {
"message": "Không có sẵn" "message": "Không có sẵn"

View File

@ -879,7 +879,7 @@
"message": "欢迎使用新版界面 Beta" "message": "欢迎使用新版界面 Beta"
}, },
"uiWelcomeMessage": { "uiWelcomeMessage": {
"message": "你现在正在使用新的 Metamask 界面。 尝试发送代币等新功能,有任何问题请告知我们。" "message": "你现在正在使用新的 MetaMask 界面。 尝试发送代币等新功能,有任何问题请告知我们。"
}, },
"unapproved": { "unapproved": {
"message": "未批准" "message": "未批准"

View File

@ -362,7 +362,7 @@
"message": "你想怎麼存入 Ether" "message": "你想怎麼存入 Ether"
}, },
"holdEther": { "holdEther": {
"message": "Metamask 讓您能保存 ether 和代幣, 並成為您接觸分散式應用程式的途徑." "message": "MetaMask 讓您能保存 ether 和代幣, 並成為您接觸分散式應用程式的途徑."
}, },
"import": { "import": {
"message": "導入", "message": "導入",

18
app/images/webcam.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="53px" height="53px" viewBox="0 0 53 53" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>webcam</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="QR-Code-Scan" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-4-Copy" transform="translate(-482.000000, -218.000000)">
<g id="webcam" transform="translate(482.000000, 218.000000)">
<circle id="Oval" fill="#D5ECFA" cx="26.5" cy="26.5" r="26.5"></circle>
<g id="Group" transform="translate(14.000000, 19.000000)" fill="#259DE5">
<rect id="Rectangle" x="0" y="0" width="18" height="16"></rect>
<polygon id="Triangle" points="19 6.57142857 26 3 26 13 19 9.42857143"></polygon>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1020 B

View File

@ -1,7 +1,7 @@
{ {
"name": "__MSG_appName__", "name": "__MSG_appName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "4.8.0", "version": "4.9.1",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "__MSG_appDescription__", "description": "__MSG_appDescription__",

View File

@ -47,8 +47,8 @@ const notificationManager = new NotificationManager()
global.METAMASK_NOTIFIER = notificationManager global.METAMASK_NOTIFIER = notificationManager
// setup sentry error reporting // setup sentry error reporting
const releaseVersion = platform.getVersion() const release = platform.getVersion()
const raven = setupRaven({ releaseVersion }) const raven = setupRaven({ release })
// browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser // browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
// Internet Explorer 6-11 // Internet Explorer 6-11

View File

@ -198,6 +198,6 @@ function blacklistedDomainCheck () {
*/ */
function redirectToPhishingWarning () { function redirectToPhishingWarning () {
console.log('MetaMask - routing to Phishing Warning component') console.log('MetaMask - routing to Phishing Warning component')
let extensionURL = extension.runtime.getURL('phishing.html') const extensionURL = extension.runtime.getURL('phishing.html')
window.location.href = extensionURL window.location.href = extensionURL
} }

View File

@ -85,7 +85,7 @@ class DetectTokensController {
set preferences (preferences) { set preferences (preferences) {
if (!preferences) { return } if (!preferences) { return }
this._preferences = preferences this._preferences = preferences
preferences.store.subscribe(({ tokens }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) }) preferences.store.subscribe(({ tokens = [] }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
preferences.store.subscribe(({ selectedAddress }) => { preferences.store.subscribe(({ selectedAddress }) => {
if (this.selectedAddress !== selectedAddress) { if (this.selectedAddress !== selectedAddress) {
this.selectedAddress = selectedAddress this.selectedAddress = selectedAddress

View File

@ -13,6 +13,7 @@ class PreferencesController {
* @property {array} store.frequentRpcList A list of custom rpcs to provide the user * @property {array} store.frequentRpcList A list of custom rpcs to provide the user
* @property {string} store.currentAccountTab Indicates the selected tab in the ui * @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 {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 {boolean} store.useBlockie The users preference for blockie identicons within the UI * @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 * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature * user wishes to see that feature
@ -24,6 +25,7 @@ class PreferencesController {
const initState = extend({ const initState = extend({
frequentRpcList: [], frequentRpcList: [],
currentAccountTab: 'history', currentAccountTab: 'history',
accountTokens: {},
tokens: [], tokens: [],
useBlockie: false, useBlockie: false,
featureFlags: {}, featureFlags: {},
@ -33,8 +35,9 @@ class PreferencesController {
}, opts.initState) }, opts.initState)
this.diagnostics = opts.diagnostics this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
this._subscribeProviderType()
} }
// PUBLIC METHODS // PUBLIC METHODS
@ -77,12 +80,19 @@ class PreferencesController {
*/ */
setAddresses (addresses) { setAddresses (addresses) {
const oldIdentities = this.store.getState().identities const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens
const identities = addresses.reduce((ids, address, index) => { const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {} const oldId = oldIdentities[address] || {}
ids[address] = {name: `Account ${index + 1}`, address, ...oldId} ids[address] = {name: `Account ${index + 1}`, address, ...oldId}
return ids return ids
}, {}) }, {})
this.store.updateState({ identities }) const accountTokens = addresses.reduce((tokens, address) => {
const oldTokens = oldAccountTokens[address] || {}
tokens[address] = oldTokens
return tokens
}, {})
this.store.updateState({ identities, accountTokens })
} }
/** /**
@ -93,11 +103,13 @@ class PreferencesController {
*/ */
removeAddress (address) { removeAddress (address) {
const identities = this.store.getState().identities const identities = this.store.getState().identities
const accountTokens = this.store.getState().accountTokens
if (!identities[address]) { if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`) throw new Error(`${address} can't be deleted cause it was not found`)
} }
delete identities[address] delete identities[address]
this.store.updateState({ identities }) delete accountTokens[address]
this.store.updateState({ identities, accountTokens })
// If the selected account is no longer valid, // If the selected account is no longer valid,
// select an arbitrary other account: // select an arbitrary other account:
@ -117,14 +129,17 @@ class PreferencesController {
*/ */
addAddresses (addresses) { addAddresses (addresses) {
const identities = this.store.getState().identities const identities = this.store.getState().identities
const accountTokens = this.store.getState().accountTokens
addresses.forEach((address) => { addresses.forEach((address) => {
// skip if already exists // skip if already exists
if (identities[address]) return if (identities[address]) return
// add missing identity // add missing identity
const identityCount = Object.keys(identities).length const identityCount = Object.keys(identities).length
accountTokens[address] = {}
identities[address] = { name: `Account ${identityCount + 1}`, address } identities[address] = { name: `Account ${identityCount + 1}`, address }
}) })
this.store.updateState({ identities }) this.store.updateState({ identities, accountTokens })
} }
/* /*
@ -175,15 +190,15 @@ class PreferencesController {
* Setter for the `selectedAddress` property * Setter for the `selectedAddress` property
* *
* @param {string} _address A new hex address for an account * @param {string} _address A new hex address for an account
* @returns {Promise<void>} Promise resolves with undefined * @returns {Promise<void>} Promise resolves with tokens
* *
*/ */
setSelectedAddress (_address) { setSelectedAddress (_address) {
return new Promise((resolve, reject) => { const address = normalizeAddress(_address)
const address = normalizeAddress(_address) this._updateTokens(address)
this.store.updateState({ selectedAddress: address }) this.store.updateState({ selectedAddress: address })
resolve() const tokens = this.store.getState().tokens
}) return Promise.resolve(tokens)
} }
/** /**
@ -232,9 +247,7 @@ class PreferencesController {
} else { } else {
tokens.push(newEntry) tokens.push(newEntry)
} }
this._updateAccountTokens(tokens)
this.store.updateState({ tokens })
return Promise.resolve(tokens) return Promise.resolve(tokens)
} }
@ -247,10 +260,8 @@ class PreferencesController {
*/ */
removeToken (rawAddress) { removeToken (rawAddress) {
const tokens = this.store.getState().tokens const tokens = this.store.getState().tokens
const updatedTokens = tokens.filter(token => token.address !== rawAddress) const updatedTokens = tokens.filter(token => token.address !== rawAddress)
this._updateAccountTokens(updatedTokens)
this.store.updateState({ tokens: updatedTokens })
return Promise.resolve(updatedTokens) return Promise.resolve(updatedTokens)
} }
@ -376,6 +387,57 @@ class PreferencesController {
// //
// PRIVATE METHODS // PRIVATE METHODS
// //
/**
* Subscription to network provider type.
*
*
*/
_subscribeProviderType () {
this.network.providerStore.subscribe(() => {
const { tokens } = this._getTokenRelatedStates()
this.store.updateState({ tokens })
})
}
/**
* Updates `accountTokens` and `tokens` of current account and network according to it.
*
* @param {array} tokens Array of tokens to be updated.
*
*/
_updateAccountTokens (tokens) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens })
}
/**
* Updates `tokens` of current account and network.
*
* @param {string} selectedAddress Account address to be updated with.
*
*/
_updateTokens (selectedAddress) {
const { tokens } = this._getTokenRelatedStates(selectedAddress)
this.store.updateState({ tokens })
}
/**
* A getter for `tokens` and `accountTokens` related states.
*
* @param {string} selectedAddress A new hex address for an account
* @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
*
*/
_getTokenRelatedStates (selectedAddress) {
const accountTokens = this.store.getState().accountTokens
if (!selectedAddress) selectedAddress = this.store.getState().selectedAddress
const providerType = this.network.providerStore.getState().type
if (!(selectedAddress in accountTokens)) accountTokens[selectedAddress] = {}
if (!(providerType in accountTokens[selectedAddress])) accountTokens[selectedAddress][providerType] = []
const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress }
}
} }
module.exports = PreferencesController module.exports = PreferencesController

View File

@ -2,8 +2,19 @@ const ENVIRONMENT_TYPE_POPUP = 'popup'
const ENVIRONMENT_TYPE_NOTIFICATION = 'notification' const ENVIRONMENT_TYPE_NOTIFICATION = 'notification'
const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen' const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen'
const PLATFORM_BRAVE = 'Brave'
const PLATFORM_CHROME = 'Chrome'
const PLATFORM_EDGE = 'Edge'
const PLATFORM_FIREFOX = 'Firefox'
const PLATFORM_OPERA = 'Opera'
module.exports = { module.exports = {
ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_FULLSCREEN,
PLATFORM_BRAVE,
PLATFORM_CHROME,
PLATFORM_EDGE,
PLATFORM_FIREFOX,
PLATFORM_OPERA,
} }

View File

@ -5,7 +5,7 @@ module.exports = function (provider) {
function ipfsContent (details) { function ipfsContent (details) {
const name = details.url.substring(7, details.url.length - 1) const name = details.url.substring(7, details.url.length - 1)
let clearTime = null let clearTime = null
extension.tabs.getSelected(null, tab => { extension.tabs.query({active: true}, tab => {
extension.tabs.update(tab.id, { url: 'loading.html' }) extension.tabs.update(tab.id, { url: 'loading.html' })
clearTime = setTimeout(() => { clearTime = setTimeout(() => {
@ -34,11 +34,11 @@ module.exports = function (provider) {
return { cancel: true } return { cancel: true }
} }
extension.webRequest.onBeforeRequest.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']}) extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']})
return { return {
remove () { remove () {
extension.webRequest.onBeforeRequest.removeListener(ipfsContent) extension.webRequest.onErrorOccurred.removeListener(ipfsContent)
}, },
} }
} }

View File

@ -8,7 +8,7 @@ module.exports = setupRaven
// Setup raven / sentry remote error reporting // Setup raven / sentry remote error reporting
function setupRaven (opts) { function setupRaven (opts) {
const { releaseVersion } = opts const { release } = opts
let ravenTarget let ravenTarget
// detect brave // detect brave
const isBrave = Boolean(window.chrome.ipcRenderer) const isBrave = Boolean(window.chrome.ipcRenderer)
@ -22,7 +22,7 @@ function setupRaven (opts) {
} }
const client = Raven.config(ravenTarget, { const client = Raven.config(ravenTarget, {
releaseVersion, release,
transport: function (opts) { transport: function (opts) {
opts.data.extra.isBrave = isBrave opts.data.extra.isBrave = isBrave
const report = opts.data const report = opts.data

View File

@ -5,6 +5,11 @@ const {
ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_FULLSCREEN,
PLATFORM_FIREFOX,
PLATFORM_OPERA,
PLATFORM_CHROME,
PLATFORM_EDGE,
PLATFORM_BRAVE,
} = require('./enums') } = require('./enums')
/** /**
@ -37,6 +42,29 @@ const getEnvironmentType = (url = window.location.href) => {
} }
} }
/**
* Returns the platform (browser) where the extension is running.
*
* @returns {string} the platform ENUM
*
*/
const getPlatform = _ => {
const ua = navigator.userAgent
if (ua.search('Firefox') !== -1) {
return PLATFORM_FIREFOX
} else {
if (window && window.chrome && window.chrome.ipcRenderer) {
return PLATFORM_BRAVE
} else if (ua.search('Edge') !== -1) {
return PLATFORM_EDGE
} else if (ua.search('OPR') !== -1) {
return PLATFORM_OPERA
} else {
return PLATFORM_CHROME
}
}
}
/** /**
* Checks whether a given balance of ETH, represented as a hex string, is sufficient to pay a value plus a gas fee * Checks whether a given balance of ETH, represented as a hex string, is sufficient to pay a value plus a gas fee
* *
@ -114,6 +142,7 @@ function removeListeners (listeners, emitter) {
module.exports = { module.exports = {
removeListeners, removeListeners,
applyListeners, applyListeners,
getPlatform,
getStack, getStack,
getEnvironmentType, getEnvironmentType,
sufficientBalance, sufficientBalance,

View File

@ -86,6 +86,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({ this.preferencesController = new PreferencesController({
initState: initState.PreferencesController, initState: initState.PreferencesController,
initLangCode: opts.initLangCode, initLangCode: opts.initLangCode,
network: this.networkController,
}) })
// currency controller // currency controller
@ -1405,7 +1406,7 @@ module.exports = class MetamaskController extends EventEmitter {
} }
/** /**
* A method for activating the retrieval of price data and auto detect tokens, * A method for activating the retrieval of price data,
* which should only be fetched when the UI is visible. * which should only be fetched when the UI is visible.
* @private * @private
* @param {boolean} active - True if price data should be getting fetched. * @param {boolean} active - True if price data should be getting fetched.

View File

@ -0,0 +1,40 @@
// next version number
const version = 28
/*
normalizes txParams on unconfirmed txs
*/
const clone = require('clone')
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
return versionedData
},
}
function transformState (state) {
const newState = state
if (newState.PreferencesController) {
if (newState.PreferencesController.tokens && newState.PreferencesController.identities) {
const identities = newState.PreferencesController.identities
const tokens = newState.PreferencesController.tokens
newState.PreferencesController.accountTokens = {}
for (const identity in identities) {
newState.PreferencesController.accountTokens[identity] = {'mainnet': tokens}
}
newState.PreferencesController.tokens = []
}
}
return newState
}

View File

@ -38,4 +38,5 @@ module.exports = [
require('./025'), require('./025'),
require('./026'), require('./026'),
require('./027'), require('./027'),
require('./028'),
] ]

View File

@ -24,8 +24,13 @@ class ExtensionPlatform {
return extension.runtime.getManifest().version return extension.runtime.getManifest().version
} }
openExtensionInBrowser (route = null) { openExtensionInBrowser (route = null, queryString = null) {
let extensionURL = extension.runtime.getURL('home.html') let extensionURL = extension.runtime.getURL('home.html')
if (queryString) {
extensionURL += `?${queryString}`
}
if (route) { if (route) {
extensionURL += `#${route}` extensionURL += `#${route}`
} }

View File

@ -340,6 +340,19 @@
min-width: 0; min-width: 0;
} }
.backup-phrase__tips-text--link {
color: #2f9ae0;
cursor: pointer;
}
.backup-phrase__tips-text--link:hover {
color: #2f9ae0;
}
.backup-phrase__tips-text--strong {
font-weight: bold;
}
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
.backup-phrase__content-wrapper { .backup-phrase__content-wrapper {
flex-direction: column; flex-direction: column;

View File

@ -5,6 +5,7 @@ import classnames from 'classnames'
import { withRouter } from 'react-router-dom' import { withRouter } from 'react-router-dom'
import { compose } from 'recompose' import { compose } from 'recompose'
import Identicon from '../../../../ui/app/components/identicon' import Identicon from '../../../../ui/app/components/identicon'
import {exportAsFile} from '../../../../ui/app/util'
import Breadcrumbs from './breadcrumbs' import Breadcrumbs from './breadcrumbs'
import LoadingScreen from './loading-screen' import LoadingScreen from './loading-screen'
import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes' import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes'
@ -65,6 +66,12 @@ class BackupPhraseScreen extends Component {
} }
} }
exportSeedWords = () => {
const { seedWords } = this.props
exportAsFile('MetaMask Secret Backup Phrase', seedWords, 'text/plain')
}
renderSecretWordsContainer () { renderSecretWordsContainer () {
const { isShowingSecret } = this.state const { isShowingSecret } = this.state
@ -111,7 +118,7 @@ class BackupPhraseScreen extends Component {
<div className="backup-phrase__tips"> <div className="backup-phrase__tips">
<div className="backup-phrase__tips-text">Tips:</div> <div className="backup-phrase__tips-text">Tips:</div>
<div className="backup-phrase__tips-text"> <div className="backup-phrase__tips-text">
Store this phrase in a password manager like 1password. Store this phrase in a password manager like 1Password.
</div> </div>
<div className="backup-phrase__tips-text"> <div className="backup-phrase__tips-text">
Write this phrase on a piece of paper and store in a secure location. If you want even more security, write it down on multiple pieces of paper and store each in 2 - 3 different locations. Write this phrase on a piece of paper and store in a secure location. If you want even more security, write it down on multiple pieces of paper and store each in 2 - 3 different locations.
@ -119,6 +126,13 @@ class BackupPhraseScreen extends Component {
<div className="backup-phrase__tips-text"> <div className="backup-phrase__tips-text">
Memorize this phrase. Memorize this phrase.
</div> </div>
<div className="backup-phrase__tips-text">
<strong>
<a className="backup-phrase__tips-text--link backup-phrase__tips-text--strong" onClick={this.exportSeedWords}>
Download this Secret Backup Phrase
</a>
</strong> and keep it stored safely on an external encrypted hard drive or storage medium.
</div>
</div> </div>
<div className="backup-phrase__next-button"> <div className="backup-phrase__next-button">
<button <button

View File

@ -40,7 +40,7 @@ TransactionListItem.prototype.showRetryButton = function () {
const currentNonce = txParams.nonce const currentNonce = txParams.nonce
const currentNonceTxs = transactions.filter(tx => tx.txParams.nonce === currentNonce) const currentNonceTxs = transactions.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted') const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
const currentSubmittedTxs = transactions.filter(tx => tx.status === 'submitted') const currentSubmittedTxs = transactions.filter(tx => tx.status === 'submitted')
const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[0] const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[0]
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce && const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
lastSubmittedTxWithCurrentNonce.id === transaction.id lastSubmittedTxWithCurrentNonce.id === transaction.id

620
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -74,7 +74,8 @@
] ]
}, },
"dependencies": { "dependencies": {
"@material-ui/core": "^1.0.0", "@material-ui/core": "1.0.0",
"@zxing/library": "^0.7.0",
"abi-decoder": "^1.0.9", "abi-decoder": "^1.0.9",
"asmcrypto.js": "0.22.0", "asmcrypto.js": "0.22.0",
"async": "^2.5.0", "async": "^2.5.0",
@ -97,6 +98,7 @@
"debounce-stream": "^2.0.0", "debounce-stream": "^2.0.0",
"deep-extend": "^0.5.1", "deep-extend": "^0.5.1",
"detect-node": "^2.0.3", "detect-node": "^2.0.3",
"detectrtc": "^1.3.6",
"disc": "^1.3.2", "disc": "^1.3.2",
"dnode": "^1.2.2", "dnode": "^1.2.2",
"end-of-stream": "^1.1.0", "end-of-stream": "^1.1.0",
@ -105,11 +107,11 @@
"eth-bin-to-ops": "^1.0.1", "eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^4.0.1", "eth-block-tracker": "^4.0.1",
"eth-contract-metadata": "github:MetaMask/eth-contract-metadata#master", "eth-contract-metadata": "github:MetaMask/eth-contract-metadata#master",
"eth-json-rpc-filters": "^2.1.1",
"eth-json-rpc-middleware": "^2.4.0", "eth-json-rpc-middleware": "^2.4.0",
"eth-keyring-controller": "^3.1.4", "eth-keyring-controller": "^3.1.4",
"eth-ens-namehash": "^2.0.8", "eth-ens-namehash": "^2.0.8",
"eth-hd-keyring": "^2.0.0", "eth-hd-keyring": "^1.2.2",
"eth-json-rpc-filters": "^1.2.6",
"eth-json-rpc-infura": "^3.0.0", "eth-json-rpc-infura": "^3.0.0",
"eth-method-registry": "^1.0.0", "eth-method-registry": "^1.0.0",
"eth-phishing-detect": "^1.1.4", "eth-phishing-detect": "^1.1.4",
@ -209,6 +211,7 @@
"vreme": "^3.0.2", "vreme": "^3.0.2",
"web3": "^0.20.1", "web3": "^0.20.1",
"web3-stream-provider": "^3.0.1", "web3-stream-provider": "^3.0.1",
"webrtc-adapter": "^6.3.0",
"xtend": "^4.0.1" "xtend": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
@ -247,7 +250,7 @@
"eslint-plugin-mocha": "^5.0.0", "eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0", "eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.6.0", "eth-json-rpc-middleware": "^1.6.0",
"eth-keyring-controller": "^4.0.0", "eth-keyring-controller": "^3.3.1",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"fs-extra": "^6.0.1", "fs-extra": "^6.0.1",
"fs-promise": "^2.0.3", "fs-promise": "^2.0.3",

View File

@ -14,6 +14,11 @@ QUnit.test('renders list items successfully', (assert) => {
}) })
}) })
global.ethQuery = global.ethQuery || {}
global.ethQuery.getTransactionCount = (_, cb) => {
cb(null, '0x3')
}
async function runTxListItemsTest (assert, done) { async function runTxListItemsTest (assert, done) {
console.log('*** start runTxListItemsTest') console.log('*** start runTxListItemsTest')
const selectState = await queryAsync($, 'select') const selectState = await queryAsync($, 'select')

View File

@ -1,16 +1,14 @@
const assert = require('assert') const assert = require('assert')
const sinon = require('sinon')
const nock = require('nock') const nock = require('nock')
const sinon = require('sinon')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const DetectTokensController = require('../../../../app/scripts/controllers/detect-tokens') const DetectTokensController = require('../../../../app/scripts/controllers/detect-tokens')
const NetworkController = require('../../../../app/scripts/controllers/network/network') const NetworkController = require('../../../../app/scripts/controllers/network/network')
const PreferencesController = require('../../../../app/scripts/controllers/preferences') const PreferencesController = require('../../../../app/scripts/controllers/preferences')
describe('DetectTokensController', () => { describe('DetectTokensController', () => {
let clock, network, preferences, controller, keyringMemStore
const sandbox = sinon.createSandbox() const sandbox = sinon.createSandbox()
let clock, keyringMemStore, network, preferences, controller
const noop = () => {} const noop = () => {}
@ -20,13 +18,14 @@ describe('DetectTokensController', () => {
beforeEach(async () => { beforeEach(async () => {
nock('https://api.infura.io') nock('https://api.infura.io')
.get(/.*/) .get(/.*/)
.reply(200) .reply(200)
keyringMemStore = new ObservableStore({ isUnlocked: false}) keyringMemStore = new ObservableStore({ isUnlocked: false})
network = new NetworkController() network = new NetworkController()
preferences = new PreferencesController() preferences = new PreferencesController({ network })
controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
network.initializeProvider(networkControllerProviderConfig) network.initializeProvider(networkControllerProviderConfig)
@ -34,8 +33,8 @@ describe('DetectTokensController', () => {
}) })
after(() => { after(() => {
sandbox.restore() sandbox.restore()
nock.cleanAll() nock.cleanAll()
}) })
it('should poll on correct interval', async () => { it('should poll on correct interval', async () => {
@ -50,7 +49,7 @@ describe('DetectTokensController', () => {
const network = new NetworkController() const network = new NetworkController()
network.initializeProvider(networkControllerProviderConfig) network.initializeProvider(networkControllerProviderConfig)
network.setProviderType('mainnet') network.setProviderType('mainnet')
const preferences = new PreferencesController() const preferences = new PreferencesController({ network })
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -68,7 +67,8 @@ describe('DetectTokensController', () => {
}) })
it('should not check tokens while in test network', async () => { it('should not check tokens while in test network', async () => {
network.setProviderType('rinkeby') // network.setProviderType('rinkeby')
// const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -81,7 +81,8 @@ describe('DetectTokensController', () => {
}) })
it('should only check and add tokens while in main network', async () => { it('should only check and add tokens while in main network', async () => {
network.setProviderType('mainnet') // network.setProviderType('mainnet')
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -97,7 +98,8 @@ describe('DetectTokensController', () => {
}) })
it('should not detect same token while in main network', async () => { it('should not detect same token while in main network', async () => {
network.setProviderType('mainnet') // network.setProviderType('mainnet')
preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -114,7 +116,8 @@ describe('DetectTokensController', () => {
}) })
it('should trigger detect new tokens when change address', async () => { it('should trigger detect new tokens when change address', async () => {
network.setProviderType('mainnet') // network.setProviderType('mainnet')
// const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
var stub = sandbox.stub(controller, 'detectNewTokens') var stub = sandbox.stub(controller, 'detectNewTokens')
@ -123,7 +126,8 @@ describe('DetectTokensController', () => {
}) })
it('should trigger detect new tokens when submit password', async () => { it('should trigger detect new tokens when submit password', async () => {
network.setProviderType('mainnet') // network.setProviderType('mainnet')
// const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.selectedAddress = '0x0' controller.selectedAddress = '0x0'
var stub = sandbox.stub(controller, 'detectNewTokens') var stub = sandbox.stub(controller, 'detectNewTokens')
@ -132,7 +136,8 @@ describe('DetectTokensController', () => {
}) })
it('should not trigger detect new tokens when not open or not unlocked', async () => { it('should not trigger detect new tokens when not open or not unlocked', async () => {
network.setProviderType('mainnet') // network.setProviderType('mainnet')
// const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = false controller.isUnlocked = false
var stub = sandbox.stub(controller, 'detectTokenBalance') var stub = sandbox.stub(controller, 'detectTokenBalance')
@ -143,4 +148,4 @@ describe('DetectTokensController', () => {
clock.tick(180000) clock.tick(180000)
sandbox.assert.notCalled(stub) sandbox.assert.notCalled(stub)
}) })
}) })

View File

@ -1,11 +1,14 @@
const assert = require('assert') const assert = require('assert')
const ObservableStore = require('obs-store')
const PreferencesController = require('../../../../app/scripts/controllers/preferences') const PreferencesController = require('../../../../app/scripts/controllers/preferences')
describe('preferences controller', function () { describe('preferences controller', function () {
let preferencesController let preferencesController
let network
beforeEach(() => { beforeEach(() => {
preferencesController = new PreferencesController() network = {providerStore: new ObservableStore({ type: 'mainnet' })}
preferencesController = new PreferencesController({ network })
}) })
describe('setAddresses', function () { describe('setAddresses', function () {
@ -28,6 +31,20 @@ describe('preferences controller', function () {
}) })
}) })
it('should create account tokens for each account in the store', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
const accountTokens = preferencesController.store.getState().accountTokens
assert.deepEqual(accountTokens, {
'0xda22le': {},
'0x7e57e2': {},
})
})
it('should replace its list of addresses', function () { it('should replace its list of addresses', function () {
preferencesController.setAddresses([ preferencesController.setAddresses([
'0xda22le', '0xda22le',
@ -64,6 +81,17 @@ describe('preferences controller', function () {
assert.equal(preferencesController.store.getState().identities['0xda22le'], undefined) assert.equal(preferencesController.store.getState().identities['0xda22le'], undefined)
}) })
it('should remove an address from state and respective tokens', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
preferencesController.removeAddress('0xda22le')
assert.equal(preferencesController.store.getState().accountTokens['0xda22le'], undefined)
})
it('should switch accounts if the selected address is removed', function () { it('should switch accounts if the selected address is removed', function () {
preferencesController.setAddresses([ preferencesController.setAddresses([
'0xda22le', '0xda22le',
@ -158,6 +186,42 @@ describe('preferences controller', function () {
await preferencesController.addToken(address, symbol, decimals) await preferencesController.addToken(address, symbol, decimals)
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address') assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address')
}) })
it('should add token per account', async function () {
const addressFirst = '0xabcdef1234567'
const addressSecond = '0xabcdef1234568'
const symbolFirst = 'ABBR'
const symbolSecond = 'ABBB'
const decimals = 5
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
const tokensFirstAddress = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0xda22le')
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
const tokensSeconAddress = preferencesController.getTokens()
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two account and tokens are equal')
})
it('should add token per network', async function () {
const addressFirst = '0xabcdef1234567'
const addressSecond = '0xabcdef1234568'
const symbolFirst = 'ABBR'
const symbolSecond = 'ABBB'
const decimals = 5
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
const tokensFirstAddress = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
const tokensSeconAddress = preferencesController.getTokens()
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two networks and tokens are equal')
})
}) })
describe('removeToken', function () { describe('removeToken', function () {
@ -182,6 +246,98 @@ describe('preferences controller', function () {
const [token1] = tokens const [token1] = tokens
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5}) assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
}) })
it('should remove a token from its state on corresponding address', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.setSelectedAddress('0x7e57e3')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensSecond = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.removeToken('0xa')
const tokensFirst = preferencesController.getTokens()
assert.equal(tokensFirst.length, 1, 'one token removed in account')
const [token1] = tokensFirst
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
await preferencesController.setSelectedAddress('0x7e57e3')
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for account')
})
it('should remove a token from its state on corresponding network', async function () {
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensSecond = preferencesController.getTokens()
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.removeToken('0xa')
const tokensFirst = preferencesController.getTokens()
assert.equal(tokensFirst.length, 1, 'one token removed in network')
const [token1] = tokensFirst
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
network.providerStore.updateState({ type: 'rinkeby' })
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for network')
})
})
describe('on setSelectedAddress', function () {
it('should update tokens from its state on corresponding address', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.setSelectedAddress('0x7e57e3')
await preferencesController.addToken('0xa', 'C', 4)
await preferencesController.addToken('0xb', 'D', 5)
await preferencesController.setSelectedAddress('0x7e57e2')
const initialTokensFirst = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e3')
const initialTokensSecond = preferencesController.getTokens()
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different accounts and tokens')
await preferencesController.setSelectedAddress('0x7e57e2')
const tokensFirst = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e3')
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same account')
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same account')
})
})
describe('on updateStateNetworkType', function () {
it('should remove a token from its state on corresponding network', async function () {
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensFirst = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken('0xa', 'C', 4)
await preferencesController.addToken('0xb', 'D', 5)
const initialTokensSecond = preferencesController.getTokens()
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different networks and tokens')
network.providerStore.updateState({ type: 'mainnet' })
const tokensFirst = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same network')
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network')
})
}) })
}) })

View File

@ -0,0 +1,46 @@
const assert = require('assert')
const migration28 = require('../../../app/scripts/migrations/028')
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'tokens': [{address: '0xa', symbol: 'A', decimals: 4}, {address: '0xb', symbol: 'B', decimals: 4}],
'identities': {
'0x6d14': {},
'0x3695': {},
},
},
},
}
describe('migration #28', () => {
it('should add corresponding tokens to accountTokens', (done) => {
migration28.migrate(oldStorage)
.then((newStorage) => {
const newTokens = newStorage.data.PreferencesController.tokens
const newAccountTokens = newStorage.data.PreferencesController.accountTokens
const testTokens = [{address: '0xa', symbol: 'A', decimals: 4}, {address: '0xb', symbol: 'B', decimals: 4}]
assert.equal(newTokens.length, 0, 'tokens is expected to have the length of 0')
assert.equal(newAccountTokens['0x6d14']['mainnet'].length, 2, 'tokens for address is expected to have the length of 2')
assert.equal(newAccountTokens['0x3695']['mainnet'].length, 2, 'tokens for address is expected to have the length of 2')
assert.equal(Object.keys(newAccountTokens).length, 2, 'account tokens should be created for all identities')
assert.deepEqual(newAccountTokens['0x6d14']['mainnet'], testTokens, 'tokens for address should be the same than before')
assert.deepEqual(newAccountTokens['0x3695']['mainnet'], testTokens, 'tokens for address should be the same than before')
done()
})
.catch(done)
})
it('should successfully migrate first time state', (done) => {
migration28.migrate({
meta: {},
data: require('../../../app/scripts/first-time-state'),
})
.then((migratedData) => {
assert.equal(migratedData.meta.version, migration28.version)
done()
}).catch(done)
})
})

View File

@ -12,6 +12,7 @@ const { fetchLocale } = require('../i18n-helper')
const log = require('loglevel') const log = require('loglevel')
const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../app/scripts/lib/enums') const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../app/scripts/lib/enums')
const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util') const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util')
const WebcamUtils = require('../lib/webcam-utils')
var actions = { var actions = {
_setBackgroundConnection: _setBackgroundConnection, _setBackgroundConnection: _setBackgroundConnection,
@ -33,6 +34,8 @@ var actions = {
ALERT_CLOSE: 'UI_ALERT_CLOSE', ALERT_CLOSE: 'UI_ALERT_CLOSE',
showAlert: showAlert, showAlert: showAlert,
hideAlert: hideAlert, hideAlert: hideAlert,
QR_CODE_DETECTED: 'UI_QR_CODE_DETECTED',
qrCodeDetected,
// network dropdown open // network dropdown open
NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN', NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN',
NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE', NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE',
@ -125,7 +128,8 @@ var actions = {
SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', SET_CURRENT_FIAT: 'SET_CURRENT_FIAT',
setCurrentCurrency: setCurrentCurrency, showQrScanner,
setCurrentCurrency,
setCurrentAccountTab, setCurrentAccountTab,
// account detail screen // account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
@ -143,6 +147,8 @@ var actions = {
exportAccountComplete, exportAccountComplete,
SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL', SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL',
setAccountLabel, setAccountLabel,
updateNetworkNonce,
SET_NETWORK_NONCE: 'SET_NETWORK_NONCE',
// tx conf screen // tx conf screen
COMPLETED_TX: 'COMPLETED_TX', COMPLETED_TX: 'COMPLETED_TX',
TRANSACTION_ERROR: 'TRANSACTION_ERROR', TRANSACTION_ERROR: 'TRANSACTION_ERROR',
@ -721,6 +727,28 @@ function showInfoPage () {
} }
} }
function showQrScanner (ROUTE) {
return (dispatch, getState) => {
return WebcamUtils.checkStatus()
.then(status => {
if (!status.environmentReady) {
// We need to switch to fullscreen mode to ask for permission
global.platform.openExtensionInBrowser(`${ROUTE}`, `scan=true`)
} else {
dispatch(actions.showModal({
name: 'QR_SCANNER',
}))
}
}).catch(e => {
dispatch(actions.showModal({
name: 'QR_SCANNER',
error: true,
errorType: e.type,
}))
})
}
}
function setCurrentCurrency (currencyCode) { function setCurrentCurrency (currencyCode) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
@ -1483,11 +1511,12 @@ function showAccountDetail (address) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
log.debug(`background.setSelectedAddress`) log.debug(`background.setSelectedAddress`)
background.setSelectedAddress(address, (err) => { background.setSelectedAddress(address, (err, tokens) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
return dispatch(actions.displayWarning(err.message)) return dispatch(actions.displayWarning(err.message))
} }
dispatch(updateTokens(tokens))
dispatch({ dispatch({
type: actions.SHOW_ACCOUNT_DETAIL, type: actions.SHOW_ACCOUNT_DETAIL,
value: address, value: address,
@ -1806,6 +1835,17 @@ function hideAlert () {
} }
} }
/**
* This action will receive two types of values via qrCodeData
* an object with the following structure {type, values}
* or null (used to clear the previous value)
*/
function qrCodeDetected (qrCodeData) {
return {
type: actions.QR_CODE_DETECTED,
value: qrCodeData,
}
}
function showLoadingIndication (message) { function showLoadingIndication (message) {
return { return {
@ -2115,6 +2155,24 @@ function updateFeatureFlags (updatedFeatureFlags) {
} }
} }
function setNetworkNonce (networkNonce) {
return {
type: actions.SET_NETWORK_NONCE,
value: networkNonce,
}
}
function updateNetworkNonce (address) {
return (dispatch) => {
return new Promise((resolve, reject) => {
global.ethQuery.getTransactionCount(address, (err, data) => {
dispatch(setNetworkNonce(data))
resolve(data)
})
})
}
}
function setMouseUserState (isMouseUser) { function setMouseUserState (isMouseUser) {
return { return {
type: actions.SET_MOUSE_USER_STATE, type: actions.SET_MOUSE_USER_STATE,

View File

@ -27,6 +27,7 @@ function EnsInput () {
} }
EnsInput.prototype.onChange = function (recipient) { EnsInput.prototype.onChange = function (recipient) {
const network = this.props.network const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network) const networkHasEnsSupport = getNetworkEnsSupport(network)
@ -54,6 +55,7 @@ EnsInput.prototype.render = function () {
const opts = extend(props, { const opts = extend(props, {
list: 'addresses', list: 'addresses',
onChange: this.onChange.bind(this), onChange: this.onChange.bind(this),
qrScanner: true,
}) })
return h('div', { return h('div', {
style: { width: '100%', position: 'relative' }, style: { width: '100%', position: 'relative' },

View File

@ -1,5 +1,7 @@
@import './customize-gas/index'; @import './customize-gas/index';
@import './qr-scanner/index';
.modal-container { .modal-container {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -21,6 +21,7 @@ const CustomizeGasModal = require('../customize-gas-modal')
const NotifcationModal = require('./notification-modal') const NotifcationModal = require('./notification-modal')
const ConfirmResetAccount = require('./confirm-reset-account') const ConfirmResetAccount = require('./confirm-reset-account')
const ConfirmRemoveAccount = require('./confirm-remove-account') const ConfirmRemoveAccount = require('./confirm-remove-account')
const QRScanner = require('./qr-scanner')
const TransactionConfirmed = require('./transaction-confirmed') const TransactionConfirmed = require('./transaction-confirmed')
const WelcomeBeta = require('./welcome-beta') const WelcomeBeta = require('./welcome-beta')
const Notification = require('./notification') const Notification = require('./notification')
@ -346,6 +347,18 @@ const MODALS = {
borderRadius: '8px', borderRadius: '8px',
}, },
}, },
QR_SCANNER: {
contents: h(QRScanner),
mobileModalStyle: {
...modalContainerMobileStyle,
},
laptopModalStyle: {
...modalContainerLaptopStyle,
},
contentStyle: {
borderRadius: '8px',
},
},
DEFAULT: { DEFAULT: {
contents: [], contents: [],

View File

@ -0,0 +1,2 @@
import QrScanner from './qr-scanner.container'
module.exports = QrScanner

View File

@ -0,0 +1,83 @@
.qr-scanner {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-flow: column;
border-radius: 8px;
&__title {
font-size: 1.5rem;
font-weight: 500;
padding: 16px 0;
text-align: center;
}
&__content {
padding-left: 20px;
padding-right: 20px;
&__video-wrapper {
overflow: hidden;
width: 100%;
height: 275px;
display: flex;
align-items: center;
justify-content: center;
video {
transform: scaleX(-1);
width: auto;
height: 275px;
}
}
}
&__status {
text-align: center;
font-size: 14px;
padding: 15px;
}
&__image {
font-size: 1.5rem;
font-weight: 500;
padding: 16px 0 0;
text-align: center;
}
&__error {
text-align: center;
font-size: 16px;
padding: 15px;
}
&__footer {
padding: 20px;
flex-direction: row;
display: flex;
button {
margin-right: 15px;
}
button:last-of-type {
margin-right: 0;
background-color: #009eec;
border: none;
color: #fff;
}
}
&__close::after {
content: '\00D7';
font-size: 35px;
color: #9b9b9b;
position: absolute;
top: 4px;
right: 20px;
cursor: pointer;
font-weight: 300;
}
}

View File

@ -0,0 +1,216 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { BrowserQRCodeReader } from '@zxing/library'
import adapter from 'webrtc-adapter' // eslint-disable-line import/no-nodejs-modules, no-unused-vars
import Spinner from '../../spinner'
import WebcamUtils from '../../../../lib/webcam-utils'
import PageContainerFooter from '../../page-container/page-container-footer/page-container-footer.component'
export default class QrScanner extends Component {
static propTypes = {
hideModal: PropTypes.func.isRequired,
qrCodeDetected: PropTypes.func,
scanQrCode: PropTypes.func,
error: PropTypes.bool,
errorType: PropTypes.string,
}
static contextTypes = {
t: PropTypes.func,
}
constructor (props, context) {
super(props)
this.state = {
ready: false,
msg: context.t('accessingYourCamera'),
}
this.codeReader = null
this.permissionChecker = null
this.needsToReinit = false
// Clear pre-existing qr code data before scanning
this.props.qrCodeDetected(null)
}
componentDidMount () {
this.initCamera()
}
async checkPermisisions () {
const { permissions } = await WebcamUtils.checkStatus()
if (permissions) {
clearTimeout(this.permissionChecker)
// Let the video stream load first...
setTimeout(_ => {
this.setState({
ready: true,
msg: this.context.t('scanInstructions'),
})
if (this.needsToReinit) {
this.initCamera()
this.needsToReinit = false
}
}, 2000)
} else {
// Keep checking for permissions
this.permissionChecker = setTimeout(_ => {
this.checkPermisisions()
}, 1000)
}
}
componentWillUnmount () {
clearTimeout(this.permissionChecker)
if (this.codeReader) {
this.codeReader.reset()
}
}
initCamera () {
this.codeReader = new BrowserQRCodeReader()
this.codeReader.getVideoInputDevices()
.then(videoInputDevices => {
clearTimeout(this.permissionChecker)
this.checkPermisisions()
this.codeReader.decodeFromInputVideoDevice(undefined, 'video')
.then(content => {
const result = this.parseContent(content.text)
if (result.type !== 'unknown') {
this.props.qrCodeDetected(result)
this.stopAndClose()
} else {
this.setState({msg: this.context.t('unknownQrCode')})
}
})
.catch(err => {
if (err && err.name === 'NotAllowedError') {
this.setState({msg: this.context.t('youNeedToAllowCameraAccess')})
clearTimeout(this.permissionChecker)
this.needsToReinit = true
this.checkPermisisions()
}
})
}).catch(err => {
console.error('[QR-SCANNER]: getVideoInputDevices threw an exception: ', err)
})
}
parseContent (content) {
let type = 'unknown'
let values = {}
// Here we could add more cases
// To parse other type of links
// For ex. EIP-681 (https://eips.ethereum.org/EIPS/eip-681)
// Ethereum address links - fox ex. ethereum:0x.....1111
if (content.split('ethereum:').length > 1) {
type = 'address'
values = {'address': content.split('ethereum:')[1] }
// Regular ethereum addresses - fox ex. 0x.....1111
} else if (content.substring(0, 2).toLowerCase() === '0x') {
type = 'address'
values = {'address': content }
}
return {type, values}
}
stopAndClose = () => {
if (this.codeReader) {
this.codeReader.reset()
}
this.setState({ ready: false })
this.props.hideModal()
}
tryAgain = () => {
// close the modal
this.stopAndClose()
// wait for the animation and try again
setTimeout(_ => {
this.props.scanQrCode()
}, 1000)
}
renderVideo () {
return (
<div className={'qr-scanner__content__video-wrapper'}>
<video
id="video"
style={{
display: this.state.ready ? 'block' : 'none',
}}
/>
{ !this.state.ready ? <Spinner color={'#F7C06C'} /> : null}
</div>
)
}
renderErrorModal () {
let title, msg
if (this.props.error) {
if (this.props.errorType === 'NO_WEBCAM_FOUND') {
title = this.context.t('noWebcamFoundTitle')
msg = this.context.t('noWebcamFound')
} else {
title = this.context.t('unknownCameraErrorTitle')
msg = this.context.t('unknownCameraError')
}
}
return (
<div className="qr-scanner">
<div className="qr-scanner__close" onClick={this.stopAndClose}></div>
<div className="qr-scanner__image">
<img src={'images/webcam.svg'} width={70} height={70} />
</div>
<div className="qr-scanner__title">
{ title }
</div>
<div className={'qr-scanner__error'}>
{msg}
</div>
<PageContainerFooter
onCancel={this.stopAndClose}
onSubmit={this.tryAgain}
cancelText={this.context.t('cancel')}
submitText={this.context.t('tryAgain')}
submitButtonType="confirm"
/>
</div>
)
}
render () {
const { t } = this.context
if (this.props.error) {
return this.renderErrorModal()
}
return (
<div className="qr-scanner">
<div className="qr-scanner__close" onClick={this.stopAndClose}></div>
<div className="qr-scanner__title">
{ `${t('scanQrCode')}` }
</div>
<div className="qr-scanner__content">
{ this.renderVideo() }
</div>
<div className={'qr-scanner__status'}>
{this.state.msg}
</div>
</div>
)
}
}

View File

@ -0,0 +1,24 @@
import { connect } from 'react-redux'
import QrScanner from './qr-scanner.component'
const { hideModal, qrCodeDetected, showQrScanner } = require('../../../actions')
import {
SEND_ROUTE,
} from '../../../routes'
const mapStateToProps = state => {
return {
error: state.appState.modal.modalState.props.error,
errorType: state.appState.modal.modalState.props.errorType,
}
}
const mapDispatchToProps = dispatch => {
return {
hideModal: () => dispatch(hideModal()),
qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(QrScanner)

View File

@ -124,7 +124,7 @@ export default class ConfirmTransactionBase extends Component {
if (simulationFails) { if (simulationFails) {
return { return {
valid: false, valid: true,
errorKey: TRANSACTION_ERROR_KEY, errorKey: TRANSACTION_ERROR_KEY,
} }
} }

View File

@ -11,6 +11,7 @@ export default class SendContent extends Component {
static propTypes = { static propTypes = {
updateGas: PropTypes.func, updateGas: PropTypes.func,
scanQrCode: PropTypes.func,
}; };
render () { render () {
@ -18,7 +19,10 @@ export default class SendContent extends Component {
<PageContainerContent> <PageContainerContent>
<div className="send-v2__form"> <div className="send-v2__form">
<SendFromRow /> <SendFromRow />
<SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendToRow
updateGas={(updateData) => this.props.updateGas(updateData)}
scanQrCode={ _ => this.props.scanQrCode()}
/>
<SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendGasRow /> <SendGasRow />
<SendHexDataRow /> <SendHexDataRow />

View File

@ -17,6 +17,7 @@ export default class SendToRow extends Component {
updateGas: PropTypes.func, updateGas: PropTypes.func,
updateSendTo: PropTypes.func, updateSendTo: PropTypes.func,
updateSendToError: PropTypes.func, updateSendToError: PropTypes.func,
scanQrCode: PropTypes.func,
}; };
static contextTypes = { static contextTypes = {
@ -51,6 +52,7 @@ export default class SendToRow extends Component {
showError={inError} showError={inError}
> >
<EnsInput <EnsInput
scanQrCode={_ => this.props.scanQrCode()}
accounts={toAccounts} accounts={toAccounts}
closeDropdown={() => closeToDropdown()} closeDropdown={() => closeToDropdown()}
dropdownOpen={toDropdownOpen} dropdownOpen={toDropdownOpen}

View File

@ -38,12 +38,30 @@ export default class SendTransactionScreen extends PersistentForm {
updateAndSetGasTotal: PropTypes.func, updateAndSetGasTotal: PropTypes.func,
updateSendErrors: PropTypes.func, updateSendErrors: PropTypes.func,
updateSendTokenBalance: PropTypes.func, updateSendTokenBalance: PropTypes.func,
scanQrCode: PropTypes.func,
qrCodeDetected: PropTypes.func,
qrCodeData: PropTypes.object,
}; };
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
}; };
componentWillReceiveProps (nextProps) {
if (nextProps.qrCodeData) {
if (nextProps.qrCodeData.type === 'address') {
const scannedAddress = nextProps.qrCodeData.values.address.toLowerCase()
const currentAddress = this.props.to && this.props.to.toLowerCase()
if (currentAddress !== scannedAddress) {
this.props.updateSendTo(scannedAddress)
this.updateGas({ to: scannedAddress })
// Clean up QR code data after handling
this.props.qrCodeDetected(null)
}
}
}
}
updateGas ({ to: updatedToAddress, amount: value } = {}) { updateGas ({ to: updatedToAddress, amount: value } = {}) {
const { const {
amount, amount,
@ -158,6 +176,16 @@ export default class SendTransactionScreen extends PersistentForm {
address, address,
}) })
this.updateGas() this.updateGas()
// Show QR Scanner modal if ?scan=true
if (window.location.search === '?scan=true') {
this.props.scanQrCode()
// Clear the queryString param after showing the modal
const cleanUrl = location.href.split('?')[0]
history.pushState({}, null, `${cleanUrl}`)
window.location.hash = '#send'
}
} }
componentWillUnmount () { componentWillUnmount () {
@ -170,7 +198,10 @@ export default class SendTransactionScreen extends PersistentForm {
return ( return (
<div className="page-container"> <div className="page-container">
<SendHeader history={history}/> <SendHeader history={history}/>
<SendContent updateGas={(updateData) => this.updateGas(updateData)}/> <SendContent
updateGas={(updateData) => this.updateGas(updateData)}
scanQrCode={_ => this.props.scanQrCode()}
/>
<SendFooter history={history}/> <SendFooter history={history}/>
</div> </div>
) )

View File

@ -21,11 +21,15 @@ import {
getSendFromObject, getSendFromObject,
getSendTo, getSendTo,
getTokenBalance, getTokenBalance,
getQrCodeData,
} from './send.selectors' } from './send.selectors'
import { import {
updateSendTo,
updateSendTokenBalance, updateSendTokenBalance,
updateGasData, updateGasData,
setGasTotal, setGasTotal,
showQrScanner,
qrCodeDetected,
} from '../../actions' } from '../../actions'
import { import {
resetSendState, resetSendState,
@ -35,6 +39,10 @@ import {
calcGasTotal, calcGasTotal,
} from './send.utils.js' } from './send.utils.js'
import {
SEND_ROUTE,
} from '../../routes'
module.exports = compose( module.exports = compose(
withRouter, withRouter,
connect(mapStateToProps, mapDispatchToProps) connect(mapStateToProps, mapDispatchToProps)
@ -60,6 +68,7 @@ function mapStateToProps (state) {
tokenBalance: getTokenBalance(state), tokenBalance: getTokenBalance(state),
tokenContract: getSelectedTokenContract(state), tokenContract: getSelectedTokenContract(state),
tokenToFiatRate: getSelectedTokenToFiatRate(state), tokenToFiatRate: getSelectedTokenToFiatRate(state),
qrCodeData: getQrCodeData(state),
} }
} }
@ -89,5 +98,8 @@ function mapDispatchToProps (dispatch) {
}, },
updateSendErrors: newError => dispatch(updateSendErrors(newError)), updateSendErrors: newError => dispatch(updateSendErrors(newError)),
resetSendState: () => dispatch(resetSendState()), resetSendState: () => dispatch(resetSendState()),
scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
} }
} }

View File

@ -46,6 +46,7 @@ const selectors = {
getTokenExchangeRate, getTokenExchangeRate,
getUnapprovedTxs, getUnapprovedTxs,
transactionsSelector, transactionsSelector,
getQrCodeData,
} }
module.exports = selectors module.exports = selectors
@ -282,3 +283,7 @@ function transactionsSelector (state) {
: txsToRender : txsToRender
.sort((a, b) => b.time - a.time) .sort((a, b) => b.time - a.time)
} }
function getQrCodeData (state) {
return state.appState.qrCodeData
}

View File

@ -44,6 +44,7 @@ proxyquire('../send.container.js', {
getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
getSendFromObject: (s) => `mockFrom:${s}`, getSendFromObject: (s) => `mockFrom:${s}`,
getTokenBalance: (s) => `mockTokenBalance:${s}`, getTokenBalance: (s) => `mockTokenBalance:${s}`,
getQrCodeData: (s) => `mockQrCodeData:${s}`,
}, },
'../../actions': actionSpies, '../../actions': actionSpies,
'../../ducks/send.duck': duckActionSpies, '../../ducks/send.duck': duckActionSpies,
@ -76,6 +77,7 @@ describe('send container', () => {
tokenBalance: 'mockTokenBalance:mockState', tokenBalance: 'mockTokenBalance:mockState',
tokenContract: 'mockTokenContract:mockState', tokenContract: 'mockTokenContract:mockState',
tokenToFiatRate: 'mockTokenToFiatRate:mockState', tokenToFiatRate: 'mockTokenToFiatRate:mockState',
qrCodeData: 'mockQrCodeData:mockState',
}) })
}) })

View File

@ -4,6 +4,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const AccountListItem = require('../account-list-item/account-list-item.component').default const AccountListItem = require('../account-list-item/account-list-item.component').default
const connect = require('react-redux').connect const connect = require('react-redux').connect
const Tooltip = require('../../tooltip')
ToAutoComplete.contextTypes = { ToAutoComplete.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
@ -94,11 +95,12 @@ ToAutoComplete.prototype.render = function () {
dropdownOpen, dropdownOpen,
onChange, onChange,
inError, inError,
qrScanner,
} = this.props } = this.props
return h('div.send-v2__to-autocomplete', {}, [ return h('div.send-v2__to-autocomplete', {}, [
h('input.send-v2__to-autocomplete__input', { h(`input.send-v2__to-autocomplete__input${qrScanner ? '.with-qr' : ''}`, {
placeholder: this.context.t('recipientAddress'), placeholder: this.context.t('recipientAddress'),
className: inError ? `send-v2__error-border` : '', className: inError ? `send-v2__error-border` : '',
value: to, value: to,
@ -108,7 +110,13 @@ ToAutoComplete.prototype.render = function () {
borderColor: inError ? 'red' : null, borderColor: inError ? 'red' : null,
}, },
}), }),
qrScanner && h(Tooltip, {
title: this.context.t('scanQrCode'),
position: 'bottom',
}, h(`i.fa.fa-qrcode.fa-lg.send-v2__to-autocomplete__qr-code`, {
style: { color: '#33333' },
onClick: () => this.props.scanQrCode(),
})),
!to && h(`i.fa.fa-caret-down.fa-lg.send-v2__to-autocomplete__down-caret`, { !to && h(`i.fa.fa-caret-down.fa-lg.send-v2__to-autocomplete__down-caret`, {
style: { color: '#dedede' }, style: { color: '#dedede' },
onClick: () => this.handleInputEvent(), onClick: () => this.handleInputEvent(),

View File

@ -35,6 +35,7 @@ function mapStateToProps (state) {
currentCurrency: getCurrentCurrency(state), currentCurrency: getCurrentCurrency(state),
contractExchangeRates: state.metamask.contractExchangeRates, contractExchangeRates: state.metamask.contractExchangeRates,
selectedAddressTxList: state.metamask.selectedAddressTxList, selectedAddressTxList: state.metamask.selectedAddressTxList,
networkNonce: state.appState.networkNonce,
} }
} }
@ -209,6 +210,7 @@ TxListItem.prototype.showRetryButton = function () {
selectedAddressTxList, selectedAddressTxList,
transactionId, transactionId,
txParams, txParams,
networkNonce,
} = this.props } = this.props
if (!txParams) { if (!txParams) {
return false return false
@ -222,11 +224,7 @@ TxListItem.prototype.showRetryButton = function () {
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce && const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
lastSubmittedTxWithCurrentNonce.id === transactionId lastSubmittedTxWithCurrentNonce.id === transactionId
if (currentSubmittedTxs.length > 0) { if (currentSubmittedTxs.length > 0) {
const earliestSubmitted = currentSubmittedTxs.reduce((tx1, tx2) => { currentTxSharesEarliestNonce = currentNonce === networkNonce
if (tx1.submittedTime < tx2.submittedTime) return tx1
return tx2
})
currentTxSharesEarliestNonce = currentNonce === earliestSubmitted.txParams.nonce
} }
return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000 return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000

View File

@ -8,7 +8,7 @@ const selectors = require('../selectors')
const TxListItem = require('./tx-list-item') const TxListItem = require('./tx-list-item')
const ShiftListItem = require('./shift-list-item') const ShiftListItem = require('./shift-list-item')
const { formatDate } = require('../util') const { formatDate } = require('../util')
const { showConfTxPage } = require('../actions') const { showConfTxPage, updateNetworkNonce } = require('../actions')
const classnames = require('classnames') const classnames = require('classnames')
const { tokenInfoGetter } = require('../token-util') const { tokenInfoGetter } = require('../token-util')
const { withRouter } = require('react-router-dom') const { withRouter } = require('react-router-dom')
@ -28,12 +28,14 @@ function mapStateToProps (state) {
return { return {
txsToRender: selectors.transactionsSelector(state), txsToRender: selectors.transactionsSelector(state),
conversionRate: selectors.conversionRateSelector(state), conversionRate: selectors.conversionRateSelector(state),
selectedAddress: selectors.getSelectedAddress(state),
} }
} }
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })), showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })),
updateNetworkNonce: (address) => dispatch(updateNetworkNonce(address)),
} }
} }
@ -44,6 +46,20 @@ function TxList () {
TxList.prototype.componentWillMount = function () { TxList.prototype.componentWillMount = function () {
this.tokenInfoGetter = tokenInfoGetter() 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 () { TxList.prototype.render = function () {

View File

@ -1,3 +1,3 @@
export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds' export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds'
export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow' export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow'
export const TRANSACTION_ERROR = 'transactionError' export const TRANSACTION_ERROR_KEY = 'transactionError'

View File

@ -626,6 +626,23 @@
top: 18px; top: 18px;
right: 12px; right: 12px;
} }
&__qr-code {
position: absolute;
top: 13px;
right: 33px;
cursor: pointer;
padding: 8px 5px 5px;
border-radius: 4px;
}
&__qr-code:hover {
background: #f1f1f1;
}
&__input.with-qr {
padding-right: 65px;
}
} }
&__to-autocomplete, &__memo-text-area, &__hex-data { &__to-autocomplete, &__memo-text-area, &__hex-data {

View File

@ -130,7 +130,7 @@ class InitializeMenuScreen extends Component {
textDecoration: 'underline', textDecoration: 'underline',
marginTop: '32px', marginTop: '32px',
}, },
}, 'Use classic interface'), }, this.context.t('classicInterface')),
]), ]),
]) ])

View File

@ -51,6 +51,7 @@ function reduceApp (state, action) {
sidebarOpen: false, sidebarOpen: false,
alertOpen: false, alertOpen: false,
alertMessage: null, alertMessage: null,
qrCodeData: null,
networkDropdownOpen: false, networkDropdownOpen: false,
currentView: seedWords ? seedConfView : defaultView, currentView: seedWords ? seedConfView : defaultView,
accountDetail: { accountDetail: {
@ -65,6 +66,7 @@ function reduceApp (state, action) {
buyView: {}, buyView: {},
isMouseUser: false, isMouseUser: false,
gasIsLoading: false, gasIsLoading: false,
networkNonce: null,
}, state.appState) }, state.appState)
switch (action.type) { switch (action.type) {
@ -90,7 +92,7 @@ function reduceApp (state, action) {
sidebarOpen: false, sidebarOpen: false,
}) })
// sidebar methods // alert methods
case actions.ALERT_OPEN: case actions.ALERT_OPEN:
return extend(appState, { return extend(appState, {
alertOpen: true, alertOpen: true,
@ -103,6 +105,13 @@ function reduceApp (state, action) {
alertMessage: null, alertMessage: null,
}) })
// qr scanner methods
case actions.QR_CODE_DETECTED:
return extend(appState, {
qrCodeData: action.value,
})
// modal methods: // modal methods:
case actions.MODAL_OPEN: case actions.MODAL_OPEN:
const { name, ...modalProps } = action.payload const { name, ...modalProps } = action.payload
@ -701,6 +710,11 @@ function reduceApp (state, action) {
gasIsLoading: false, gasIsLoading: false,
}) })
case actions.SET_NETWORK_NONCE:
return extend(appState, {
networkNonce: action.value,
})
default: default:
return appState return appState
} }

View File

@ -147,14 +147,20 @@ export const tokenAmountAndToAddressSelector = createSelector(
export const approveTokenAmountAndToAddressSelector = createSelector( export const approveTokenAmountAndToAddressSelector = createSelector(
tokenDataParamsSelector, tokenDataParamsSelector,
params => { tokenDecimalsSelector,
(params, tokenDecimals) => {
let toAddress = '' let toAddress = ''
let tokenAmount = 0 let tokenAmount = 0
if (params && params.length) { if (params && params.length) {
toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value
const value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value) const value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
tokenAmount = roundExponential(value)
if (tokenDecimals) {
tokenAmount = calcTokenAmount(value, tokenDecimals)
}
tokenAmount = roundExponential(tokenAmount)
} }
return { return {

View File

@ -271,9 +271,9 @@ function getContractAtAddress (tokenAddress) {
return global.eth.contract(abi).at(tokenAddress) return global.eth.contract(abi).at(tokenAddress)
} }
function exportAsFile (filename, data) { function exportAsFile (filename, data, type = 'text/csv') {
// source: https://stackoverflow.com/a/33542499 by Ludovic Feltz // source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
const blob = new Blob([data], {type: 'text/csv'}) const blob = new Blob([data], {type})
if (window.navigator.msSaveOrOpenBlob) { if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, filename) window.navigator.msSaveBlob(blob, filename)
} else { } else {

36
ui/lib/webcam-utils.js Normal file
View File

@ -0,0 +1,36 @@
'use strict'
import DetectRTC from 'detectrtc'
const { ENVIRONMENT_TYPE_POPUP } = require('../../app/scripts/lib/enums')
const { getEnvironmentType, getPlatform } = require('../../app/scripts/lib/util')
const { PLATFORM_BRAVE, PLATFORM_FIREFOX } = require('../../app/scripts/lib/enums')
class WebcamUtils {
static checkStatus () {
return new Promise((resolve, reject) => {
const isPopup = getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP
const isFirefoxOrBrave = getPlatform() === (PLATFORM_FIREFOX || PLATFORM_BRAVE)
try {
DetectRTC.load(_ => {
if (DetectRTC.hasWebcam) {
let environmentReady = true
if ((isFirefoxOrBrave && isPopup) || (isPopup && !DetectRTC.isWebsiteHasWebcamPermissions)) {
environmentReady = false
}
resolve({
permissions: DetectRTC.isWebsiteHasWebcamPermissions,
environmentReady,
})
} else {
reject({type: 'NO_WEBCAM_FOUND'})
}
})
} catch (e) {
reject({type: 'UNKNOWN_ERROR'})
}
})
}
}
module.exports = WebcamUtils