1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

Merge branch 'NewUI-flat' into merge

This commit is contained in:
Chi Kei Chan 2017-10-18 22:58:46 -07:00
commit c8c773d641
182 changed files with 23628 additions and 2371 deletions

3
.gitignore vendored
View File

@ -24,6 +24,9 @@ test/background.js
test/bundle.js
test/test-bundle.js
#ignore css output and sourcemaps
ui/app/css/output/
notes.txt
.coveralls.yml

10
.stylelintignore Normal file
View File

@ -0,0 +1,10 @@
app/
development/
dist/
docs/
fonts/
images/
mascara/
node_modules/
notices/
test/

50
.stylelintrc Normal file
View File

@ -0,0 +1,50 @@
{
"extends": "stylelint-config-standard",
"rules": {
"color-named": "never",
"font-family-name-quotes": "always-where-recommended",
"font-weight-notation": "numeric",
"function-url-quotes": "always",
"number-leading-zero": "never",
"value-no-vendor-prefix": true,
"value-list-comma-newline-before": "never-multi-line",
"custom-property-empty-line-before": "never",
"property-no-unknown": [
true,
{
"ignoreProperties": [
"composes",
"all",
"-webkit-appearance"
]
}
],
"declaration-block-semicolon-newline-after": "always",
"block-opening-brace-newline-after": "always",
"selector-attribute-quotes": "always",
"selector-max-specificity": "0,5,2",
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["local", "global"]
}
],
"at-rule-empty-line-before": [
"always",
{
"ignore": [
"after-comment",
]
}
],
"indentation": [
2,
{
"indentInsideParens": "once-at-root-twice-in-block"
}
],
"max-nesting-depth": 3,
"no-duplicate-selectors": true,
"no-unknown-animations": true
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
app/fonts/Lato/Lato-Black.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
app/fonts/Lato/Lato-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
app/fonts/Lato/Lato-Hairline.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
app/fonts/Lato/Lato-Italic.ttf Executable file

Binary file not shown.

BIN
app/fonts/Lato/Lato-Light.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
app/fonts/Lato/Lato-Regular.ttf Executable file

Binary file not shown.

93
app/fonts/Lato/OFL.txt Executable file
View File

@ -0,0 +1,93 @@
Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
app/images/.DS_Store vendored

Binary file not shown.

11
app/images/eth_logo.svg Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="417px" viewBox="0 0 256 417" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<polygon fill="#343434" points="127.9611 0 125.1661 9.5 125.1661 285.168 127.9611 287.958 255.9231 212.32"/>
<polygon fill="#8C8C8C" points="127.962 0 0 212.32 127.962 287.959 127.962 154.158"/>
<polygon fill="#3C3C3B" points="127.9611 312.1866 126.3861 314.1066 126.3861 412.3056 127.9611 416.9066 255.9991 236.5866"/>
<polygon fill="#8C8C8C" points="127.962 416.9052 127.962 312.1852 0 236.5852"/>
<polygon fill="#141414" points="127.9611 287.9577 255.9211 212.3207 127.9611 154.1587"/>
<polygon fill="#393939" points="0.0009 212.3208 127.9609 287.9578 127.9609 154.1588"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 854 B

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="15px" height="15px" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 47 (45396) - http://www.bohemiancoding.com/sketch -->
<title>import-account</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="MetaMascara-v2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="account-dropdown-top-bar-IXD" transform="translate(-25.000000, -718.000000)">
<g id="Group-6" transform="translate(4.000000, 646.000000)">
<g id="import-account" transform="translate(21.000000, 72.000000)">
<rect id="Rectangle-49" fill="#FFFFFF" x="0" y="13.1721326" width="14.4893459" height="1.08397642"></rect>
<rect id="Rectangle" fill="#FFFFFF" x="6.5860663" y="0" width="1.08397642" height="10.5377061"></rect>
<polyline id="Path-12" stroke="#FFFFFF" points="2.63442652 6.5860663 7.24467293 10.5377061 11.8549193 6.5860663"></polyline>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

11
app/images/mm-bolt.svg Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 252 251.7" style="enable-background:new 0 0 252 251.7;" xml:space="preserve">
<style type="text/css">
.st0{fill:#757575;}
</style>
<path class="st0" d="M211.3,103.9h-60.7c-2,0-3.6-1.6-3.6-3.6V3.6c0-3.5-4.5-5-6.6-2.2l-102.7,140c-1.8,2.4,0,5.8,2.9,5.8h60.7
c2,0,3.6,1.6,3.6,3.6v96.6c0,3.5,4.5,5,6.6,2.2l102.7-140C216,107.3,214.3,103.9,211.3,103.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 732 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 10 10" style="enable-background:new 0 0 10 10;" xml:space="preserve">
<style type="text/css">
.st0{fill:#B8B8B8;}
</style>
<path class="st0" d="M5,0C2.2,0,0,2.2,0,5s2.2,5,5,5s5-2.2,5-5S7.8,0,5,0z M5,2c0.4,0,0.7,0.3,0.7,0.7c0,0.4-0.3,0.7-0.7,0.7
S4.3,3.2,4.3,2.8C4.3,2.4,4.6,2,5,2z M5.7,8H4.3V4.3h1.5V8z"/>
</svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 47 (45396) - http://www.bohemiancoding.com/sketch -->
<title>plus-btn-white</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="MetaMascara-v2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="account-dropdown-top-bar-IXD" transform="translate(-24.000000, -669.000000)" fill="#FFFFFF">
<g id="Group-6" transform="translate(4.000000, 646.000000)">
<g id="plus-btn-white" transform="translate(20.000000, 23.000000)">
<rect id="Rectangle-48" x="7.38461538" y="0" width="1.23076923" height="16"></rect>
<rect id="Rectangle-48" transform="translate(8.000000, 8.000000) rotate(-90.000000) translate(-8.000000, -8.000000) " x="7.38461538" y="0" width="1.23076923" height="16"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,24 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24.088px" height="24px" viewBox="0 0 24.088 24" enable-background="new 0 0 24.088 24" xml:space="preserve">
<path d="M21.525,10.147c-0.41-0.059-0.847-0.428-0.974-0.82l-0.608-1.481c-0.191-0.365-0.146-0.935,0.1-1.264l0.99-1.318
c0.246-0.33,0.227-0.854-0.047-1.162l-1.084-1.086c-0.31-0.272-0.832-0.293-1.164-0.045l-1.316,0.988
c-0.33,0.248-0.898,0.293-1.264,0.101l-1.48-0.609c-0.395-0.126-0.764-0.562-0.82-0.971l-0.233-1.629
c-0.058-0.409-0.44-0.778-0.851-0.822c0,0-0.254-0.026-0.77-0.026c-0.514,0-0.77,0.026-0.77,0.026
c-0.41,0.044-0.793,0.413-0.852,0.822L10.15,2.48c-0.059,0.409-0.428,0.845-0.82,0.971L7.85,4.06
C7.484,4.251,6.916,4.207,6.586,3.959L5.268,2.97c-0.33-0.248-0.854-0.228-1.162,0.045L3.021,4.101
C2.749,4.41,2.727,4.933,2.975,5.263l0.988,1.318c0.249,0.33,0.293,0.899,0.102,1.264l-0.61,1.482
c-0.125,0.393-0.562,0.762-0.972,0.82l-1.629,0.231c-0.408,0.059-0.776,0.442-0.82,0.853c0,0-0.026,0.255-0.026,0.77
c0,0.516,0.026,0.77,0.026,0.77c0.044,0.412,0.412,0.793,0.82,0.853l1.629,0.231c0.408,0.06,0.847,0.429,0.972,0.82l0.61,1.48
c0.191,0.365,0.146,0.936-0.102,1.264l-0.988,1.318c-0.248,0.33-0.308,0.779-0.132,0.994c0.175,0.217,0.677,0.752,0.679,0.754
c0,0.002,0.17,0.156,0.375,0.344c0.203,0.188,1.041,0.449,1.371,0.203l1.317-0.99c0.33-0.246,0.897-0.293,1.265-0.1l1.479,0.608
c0.394,0.125,0.763,0.562,0.819,0.972l0.233,1.629c0.058,0.408,0.44,0.779,0.853,0.822c0,0,0.254,0.026,0.769,0.026
s0.771-0.026,0.771-0.026c0.408-0.043,0.793-0.414,0.85-0.822l0.234-1.629c0.057-0.408,0.426-0.847,0.819-0.972l1.479-0.61
c0.365-0.191,0.935-0.146,1.265,0.102l1.317,0.99c0.332,0.246,0.854,0.227,1.164-0.047l1.082-1.084
c0.273-0.312,0.293-0.834,0.047-1.164l-0.989-1.318c-0.246-0.328-0.291-0.898-0.101-1.264l0.609-1.48
c0.127-0.393,0.562-0.762,0.973-0.82l1.627-0.231c0.41-0.06,0.779-0.44,0.822-0.853c0,0,0.027-0.254,0.027-0.77
c0-0.515-0.027-0.77-0.027-0.77c-0.043-0.41-0.412-0.794-0.822-0.853L21.525,10.147z M12.004,15.001c-1.657,0-3-1.344-3-3
c0-1.657,1.343-3,3-3s3,1.344,3,3S13.66,15.001,12.004,15.001z"/>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 47 (45396) - http://www.bohemiancoding.com/sketch -->
<title>settings</title>
<desc>Created with Sketch.</desc>
<defs>
<polygon id="path-1" points="20 10 20 19.9998 0 19.9998 0 10 0 0.0002 20 0.0002"></polygon>
</defs>
<g id="MetaMascara-v2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="account-dropdown-top-bar-IXD" transform="translate(-25.000000, -826.000000)">
<g id="Group-6" transform="translate(4.000000, 646.000000)">
<g id="settings" transform="translate(21.000000, 180.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Clip-2"></g>
<path d="M10,13.6602 C7.979,13.6602 6.34,12.0212 6.34,10.0002 C6.34,7.9782 7.979,6.3402 10,6.3402 C12.021,6.3402 13.66,7.9782 13.66,10.0002 C13.66,12.0212 12.021,13.6602 10,13.6602 L10,13.6602 Z M19.157,11.8112 C19.53,11.8112 19.878,11.5092 19.929,11.1392 C19.929,11.1392 20,10.6182 20,10.0002 C20,9.3822 19.929,8.8622 19.929,8.8622 C19.878,8.4922 19.53,8.1892 19.157,8.1892 L17.228,8.1892 C16.854,8.1892 16.466,7.9512 16.365,7.6602 C16.265,7.3682 16.127,6.4352 16.391,6.1712 L17.755,4.8072 C18.019,4.5432 18.039,4.0922 17.8,3.8052 L16.195,2.2002 C15.908,1.9602 15.458,1.9812 15.193,2.2452 L13.829,3.6092 C13.565,3.8732 13.125,3.9802 12.852,3.8462 C12.578,3.7122 11.812,3.1462 11.812,2.7732 L11.812,0.8432 C11.812,0.4702 11.509,0.1222 11.139,0.0722 C11.139,0.0722 10.619,0.0002 10,0.0002 C9.382,0.0002 8.862,0.0722 8.862,0.0722 C8.492,0.1222 8.189,0.4702 8.189,0.8432 L8.189,2.7732 C8.189,3.1462 7.951,3.5352 7.66,3.6352 C7.369,3.7352 6.435,3.8732 6.171,3.6092 L4.807,2.2452 C4.542,1.9812 4.092,1.9612 3.805,2.2002 L2.2,3.8052 C1.96,4.0922 1.981,4.5432 2.245,4.8072 L3.609,6.1712 C3.873,6.4352 3.98,6.8752 3.846,7.1482 C3.711,7.4222 3.146,8.1892 2.773,8.1892 L0.843,8.1892 C0.47,8.1892 0.123,8.4922 0.072,8.8622 C0.072,8.8622 0,9.3822 0,10.0002 C0,10.6182 0.072,11.1392 0.072,11.1392 C0.123,11.5092 0.47,11.8112 0.843,11.8112 L2.773,11.8112 C3.146,11.8112 3.535,12.0502 3.635,12.3412 C3.735,12.6322 3.874,13.5642 3.609,13.8292 L2.246,15.1932 C1.981,15.4572 1.961,15.9082 2.2,16.1952 L3.805,17.8002 C4.092,18.0392 4.542,18.0192 4.807,17.7552 L6.171,16.3902 C6.435,16.1272 6.875,16.0202 7.148,16.1542 C7.422,16.2882 8.189,16.8532 8.189,17.2272 L8.189,19.1572 C8.189,19.5302 8.492,19.8782 8.862,19.9292 C8.862,19.9292 9.382,20.0002 10,20.0002 C10.619,20.0002 11.139,19.9292 11.139,19.9292 C11.509,19.8772 11.812,19.5302 11.812,19.1572 L11.812,17.2272 C11.812,16.8532 12.05,16.4662 12.341,16.3652 C12.632,16.2642 13.565,16.1272 13.829,16.3902 L15.193,17.7552 C15.458,18.0182 15.908,18.0392 16.195,17.8002 L17.8,16.1952 C18.039,15.9082 18.02,15.4582 17.755,15.1932 L16.391,13.8292 C16.127,13.5652 16.021,13.1252 16.154,12.8512 C16.288,12.5782 16.854,11.8112 17.228,11.8112 L19.157,11.8112 Z" id="Fill-1" fill="#B3B3B3" mask="url(#mask-2)"></path>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -9,7 +9,7 @@
}
</style>
</head>
<body>
<body style="width:350px; height:500px;">
<div id="app-content"></div>
<script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script>
</body>

View File

@ -1,11 +1,11 @@
<!doctype html>
<html>
<html style="width:350px; height:600px;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=no">
<title>MetaMask Plugin</title>
</head>
<body style="width:357px; height:500px;">
<body style="width:350px; height:600px;">
<div id="app-content"></div>
<script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script>
</body>

View File

@ -42,7 +42,18 @@ class PreferencesController {
}
this.store.updateState({ tokens })
return Promise.resolve()
return Promise.resolve(tokens)
}
removeToken (rawAddress) {
const address = normalizeAddress(rawAddress)
const tokens = this.store.getState().tokens
const updatedTokens = tokens.filter(token => token.address !== rawAddress)
this.store.updateState({ tokens: updatedTokens })
return Promise.resolve(updatedTokens)
}
getTokens () {

View File

@ -0,0 +1,10 @@
module.exports = function environmentType () {
const url = window.location.href
if (url.match(/popup.html$/)) {
return 'popup'
} else if (url.match(/home.html$/)) {
return 'responsive'
} else {
return 'notification'
}
}

View File

@ -1,6 +1,9 @@
module.exports = function isPopupOrNotification () {
const url = window.location.href
if (url.match(/popup.html$/)) {
// if (url.match(/popup.html$/) || url.match(/home.html$/)) {
// Below regexes needed for feature toggles (e.g. see line ~340 in ui/app/app.js)
// Revert below regexes to above commented out regexes before merge to master
if (url.match(/popup.html(?:\?.+)*$/) || url.match(/home.html(?:\?.+)*$/)) {
return 'popup'
} else {
return 'notification'

View File

@ -344,6 +344,7 @@ module.exports = class MetamaskController extends EventEmitter {
// PreferencesController
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
addToken: nodeify(preferencesController.addToken, preferencesController),
removeToken: nodeify(preferencesController.removeToken, preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
// AddressController

View File

@ -1,6 +1,7 @@
const EventEmitter = require('events').EventEmitter
const async = require('async')
const Dnode = require('dnode')
const Eth = require('ethjs')
const EthQuery = require('eth-query')
const launchMetamaskUi = require('../../ui')
const StreamProvider = require('web3-stream-provider')
@ -34,6 +35,7 @@ function setupWeb3Connection (connectionStream) {
providerStream.on('error', console.error.bind(console))
global.ethereumProvider = providerStream
global.ethQuery = new EthQuery(providerStream)
global.eth = new Eth(providerStream)
}
function setupControllerConnection (connectionStream, cb) {

View File

@ -19,10 +19,16 @@ var manifest = require('./app/manifest.json')
var gulpif = require('gulp-if')
var replace = require('gulp-replace')
var mkdirp = require('mkdirp')
var sass = require('gulp-sass')
var autoprefixer = require('gulp-autoprefixer')
var gulpStylelint = require('gulp-stylelint')
var stylefmt = require('gulp-stylefmt')
var disableDebugTools = gutil.env.disableDebugTools
var debug = gutil.env.debug
// browser reload
gulp.task('dev:reload', function() {
@ -159,13 +165,7 @@ gulp.task('lint', function () {
// To have the process exit with an error code (1) on
// lint error, return the stream and pipe to failAfterError last.
.pipe(eslint.failAfterError())
});
/*
gulp.task('default', ['lint'], function () {
// This will only run if the lint task is successful...
});
*/
})
// build js
@ -176,6 +176,37 @@ const jsFiles = [
'popup',
]
// scss compilation and autoprefixing tasks
gulp.task('build:scss', function () {
return gulp.src('ui/app/css/index.scss')
.pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError))
.pipe(sourcemaps.write())
.pipe(autoprefixer())
.pipe(gulp.dest('ui/app/css/output'))
})
gulp.task('watch:scss', function() {
gulp.watch(['ui/app/css/**/*.scss'], gulp.series(['build:scss']))
})
gulp.task('lint-scss', function() {
return gulp
.src('ui/app/css/itcss/**/*.scss')
.pipe(gulpStylelint({
reporters: [
{formatter: 'string', console: true}
],
fix: true,
}));
});
gulp.task('fmt-scss', function () {
return gulp.src('ui/app/css/itcss/**/*.scss')
.pipe(stylefmt())
.pipe(gulp.dest('ui/app/css/itcss'));
});
// bundle tasks
var jsDevStrings = jsFiles.map(jsFile => `dev:js:${jsFile}`)
@ -219,9 +250,9 @@ gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox', 'zip:edge', 'zip:ope
// high level tasks
gulp.task('dev', gulp.series('dev:js', 'copy', gulp.parallel('copy:watch', 'dev:reload')))
gulp.task('dev', gulp.series('build:scss', 'dev:js', 'copy', gulp.parallel('watch:scss', 'copy:watch', 'dev:reload')))
gulp.task('build', gulp.series('clean', gulp.parallel('build:js', 'copy')))
gulp.task('build', gulp.series('clean', 'build:scss', gulp.parallel('build:js', 'copy')))
gulp.task('dist', gulp.series('build', 'zip'))
// task generators
@ -249,7 +280,7 @@ function zipTask(target) {
return () => {
return gulp.src(`dist/${target}/**`)
.pipe(zip(`metamask-${target}-${manifest.version}.zip`))
.pipe(gulp.dest('builds'));
.pipe(gulp.dest('builds'))
}
}

View File

@ -45,17 +45,24 @@
]
}
],
"reactify",
"envify",
"brfs"
]
},
"dependencies": {
"abi-decoder": "^1.0.8",
"async": "^2.5.0",
"await-semaphore": "^0.1.1",
"babel-runtime": "^6.23.0",
"bignumber.js": "^4.0.4",
"bip39": "^2.2.0",
"bluebird": "^3.5.0",
"bn.js": "^4.11.7",
"boron": "^0.2.3",
"browser-passworder": "^2.0.3",
"browserify-derequire": "^0.9.4",
"classnames": "^2.2.5",
"client-sw-ready-event": "^3.3.0",
"clone": "^2.1.1",
"copy-to-clipboard": "^3.0.8",
@ -68,19 +75,21 @@
"ensnare": "^1.0.0",
"eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^2.2.0",
"eth-contract-metadata": "^1.1.4",
"eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^1.2.2",
"eth-keyring-controller": "^2.1.0",
"eth-contract-metadata": "^1.1.5",
"eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2",
"eth-sig-util": "^1.4.0",
"eth-simple-keyring": "^1.2.0",
"eth-token-tracker": "^1.1.4",
"ethereumjs-abi": "^0.6.4",
"ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0",
"etherscan-link": "^1.0.2",
"ethjs": "^0.2.8",
"ethjs-contract": "^0.1.9",
"ethjs-ens": "^2.0.0",
"ethjs-query": "^0.2.9",
@ -89,8 +98,11 @@
"extensionizer": "^1.0.0",
"fast-json-patch": "^2.0.4",
"fast-levenshtein": "^2.0.6",
"fuse.js": "^3.1.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^4.0.0",
"gulp-eslint": "^4.0.0",
"gulp-sass": "^3.1.0",
"hat": "0.0.3",
"human-standard-token-abi": "^1.0.2",
"idb-global": "^2.1.0",
@ -120,6 +132,7 @@
"pump": "^1.0.2",
"pumpify": "^1.3.4",
"qrcode-npm": "0.0.3",
"ramda": "^0.24.1",
"react": "^15.0.2",
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.5.4",
@ -129,6 +142,8 @@
"react-select": "^1.0.0-rc.2",
"react-simple-file-input": "^2.0.0",
"react-tooltip-component": "^0.3.0",
"react-transition-group": "^2.2.0",
"reactify": "^1.1.1",
"readable-stream": "^2.3.3",
"redux": "^3.0.5",
"redux-logger": "^3.0.6",
@ -136,6 +151,7 @@
"request-promise": "^4.2.1",
"sandwich-expando": "^1.0.5",
"semaphore": "^1.0.5",
"shallow-copy": "0.0.1",
"sw-stream": "^2.0.0",
"textarea-caret": "^3.0.1",
"through2": "^2.0.3",
@ -174,6 +190,8 @@
"gulp-livereload": "^3.8.1",
"gulp-replace": "^0.6.1",
"gulp-sourcemaps": "^2.6.0",
"gulp-stylefmt": "^1.1.0",
"gulp-stylelint": "^4.0.0",
"gulp-util": "^3.0.7",
"gulp-watch": "^4.3.5",
"gulp-zip": "^4.0.0",
@ -201,6 +219,7 @@
"react-test-renderer": "^15.5.4",
"react-testutils-additions": "^15.2.0",
"sinon": "^4.0.0",
"stylelint-config-standard": "^17.0.0",
"tape": "^4.5.1",
"testem": "^1.10.3",
"uglifyify": "^4.0.2",

View File

@ -0,0 +1,32 @@
var assert = require('assert')
var BalanceComponent = require('../../../ui/app/components/balance-component')
describe('BalanceComponent', function () {
let balanceComponent
beforeEach(function () {
balanceComponent = new BalanceComponent()
})
it('shows token balance and convert to fiat value based on conversion rate', function () {
const formattedBalance = '1.23 ETH'
const tokenBalance = balanceComponent.getTokenBalance(formattedBalance, false)
const fiatDisplayNumber = balanceComponent.getFiatDisplayNumber(formattedBalance, 2)
assert.equal('1.23 ETH', tokenBalance)
assert.equal(2.46, fiatDisplayNumber)
})
it('shows only the token balance when conversion rate is not available', function () {
const formattedBalance = '1.23 ETH'
const tokenBalance = balanceComponent.getTokenBalance(formattedBalance, false)
const fiatDisplayNumber = balanceComponent.getFiatDisplayNumber(formattedBalance, 0)
assert.equal('1.23 ETH', tokenBalance)
assert.equal('N/A', fiatDisplayNumber)
})
})

View File

@ -0,0 +1,38 @@
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', {
style: {
display: 'flex',
flex: '1 0 auto',
},
}, [
// wallet
h(WalletView, {
style: {
},
responsiveDisplayClassname: '.lap-visible',
}, [
]),
// transaction
h(TxView, {
style: {
},
}, [
]),
])
}

View File

@ -5,15 +5,10 @@ const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const valuesFor = require('./util').valuesFor
const Identicon = require('./components/identicon')
const EthBalance = require('./components/eth-balance')
const TransactionList = require('./components/transaction-list')
const ExportAccountView = require('./components/account-export')
const ethUtil = require('ethereumjs-util')
const EditableLabel = require('./components/editable-label')
const TabBar = require('./components/tab-bar')
const TokenList = require('./components/token-list')
const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns
module.exports = connect(mapStateToProps)(AccountDetailScreen)
@ -41,180 +36,11 @@ function AccountDetailScreen () {
Component.call(this)
}
AccountDetailScreen.prototype.render = function () {
var props = this.props
var selected = props.address || Object.keys(props.accounts)[0]
var checksumAddress = selected && ethUtil.toChecksumAddress(selected)
var identity = props.identities[selected]
var account = props.accounts[selected]
const { network, conversionRate, currentCurrency } = props
return (
h('.account-detail-section.full-flex-height', [
// identicon, label, balance, etc
h('.account-data-subsection', {
style: {
margin: '0 20px',
flex: '1 0 auto',
},
}, [
// header - identicon + nav
h('div', {
style: {
paddingTop: '20px',
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'flex-start',
},
}, [
// large identicon and addresses
h('.identicon-wrapper.select-none', [
h(Identicon, {
diameter: 62,
address: selected,
}),
]),
h('flex-column', {
style: {
lineHeight: '10px',
marginLeft: '15px',
width: '100%',
},
}, [
h(EditableLabel, {
textValue: identity ? identity.name : '',
state: {
isEditingLabel: false,
},
saveText: (text) => {
props.dispatch(actions.saveAccountLabel(selected, text))
},
}, [
// What is shown when not editing + edit text:
h('label.editing-label', [h('.edit-text', 'edit')]),
h(
'div',
{
style: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
},
},
[
h(
'div.font-medium.color-forest',
{
name: 'edit',
style: {
},
},
[
h('h2', {
style: {
maxWidth: '180px',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '5px 0px',
lineHeight: '25px',
},
}, [
identity && identity.name,
]),
]
),
h(
AccountDropdowns,
{
style: {
marginRight: '8px',
marginLeft: 'auto',
cursor: 'pointer',
},
selected,
network,
identities: props.identities,
enableAccountOptions: true,
},
),
]
),
]),
h('.flex-row', {
style: {
width: '15em',
justifyContent: 'space-between',
alignItems: 'baseline',
},
}, [
// address
h('div', {
style: {
overflow: 'hidden',
textOverflow: 'ellipsis',
paddingTop: '3px',
width: '5em',
fontSize: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
marginBottom: '15px',
color: '#AEAEAE',
},
}, checksumAddress),
]),
// account ballence
]),
]),
h('.flex-row', {
style: {
justifyContent: 'space-between',
alignItems: 'flex-start',
},
}, [
h(EthBalance, {
value: account && account.balance,
conversionRate,
currentCurrency,
style: {
lineHeight: '7px',
marginTop: '10px',
},
}),
h('.flex-grow'),
h('button', {
onClick: () => props.dispatch(actions.buyEthView(selected)),
style: { marginRight: '10px' },
}, 'BUY'),
h('button', {
onClick: () => props.dispatch(actions.showSendPage()),
style: {
marginBottom: '20px',
marginRight: '8px',
},
}, 'SEND'),
]),
]),
// subview (tx history, pk export confirm, buy eth warning)
this.subview(),
])
)
}
// Note: This component is no longer used. Leaving the file for reference:
// - structuring routing for add token
// - state required for TxList
// Delete file when those features are complete
AccountDetailScreen.prototype.render = function () {}
AccountDetailScreen.prototype.subview = function () {
var subview

View File

@ -34,8 +34,10 @@ AccountImportSubview.prototype.render = function () {
const { type } = state
return (
h('div', {
h('div.flex-center', {
style: {
flexDirection: 'column',
marginTop: '32px',
},
}, [
h('.section-title.flex-row.flex-center', [
@ -48,7 +50,8 @@ AccountImportSubview.prototype.render = function () {
]),
h('div', {
style: {
padding: '10px',
padding: '10px 0',
width: '260px',
color: 'rgb(174, 174, 174)',
},
}, [

View File

@ -1,3 +1,4 @@
const abi = require('human-standard-token-abi')
const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
var actions = {
@ -5,6 +6,21 @@ var actions = {
GO_HOME: 'GO_HOME',
goHome: goHome,
// modal state
MODAL_OPEN: 'UI_MODAL_OPEN',
MODAL_CLOSE: 'UI_MODAL_CLOSE',
showModal: showModal,
hideModal: hideModal,
// sidebar state
SIDEBAR_OPEN: 'UI_SIDEBAR_OPEN',
SIDEBAR_CLOSE: 'UI_SIDEBAR_CLOSE',
showSidebar: showSidebar,
hideSidebar: hideSidebar,
// network dropdown open
NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN',
NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE',
showNetworkDropdown: showNetworkDropdown,
hideNetworkDropdown: hideNetworkDropdown,
// menu state
getNetworkStatus: 'getNetworkStatus',
// transition state
@ -68,6 +84,8 @@ var actions = {
hideWarning: hideWarning,
// accounts screen
SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT',
SET_SELECTED_TOKEN: 'SET_SELECTED_TOKEN',
setSelectedToken,
SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL',
SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE',
SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
@ -78,6 +96,8 @@ var actions = {
// account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
showSendPage: showSendPage,
SHOW_SEND_TOKEN_PAGE: 'SHOW_SEND_TOKEN_PAGE',
showSendTokenPage,
ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK',
addToAddressBook: addToAddressBook,
REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT',
@ -86,6 +106,7 @@ var actions = {
exportAccount: exportAccount,
SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY',
showPrivateKey: showPrivateKey,
exportAccountComplete,
SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL',
saveAccountLabel: saveAccountLabel,
// tx conf screen
@ -99,7 +120,9 @@ var actions = {
cancelPersonalMsg,
signTypedMsg,
cancelTypedMsg,
sendTx: sendTx,
signTx: signTx,
signTokenTx: signTokenTx,
updateAndApproveTx,
cancelTx: cancelTx,
completedTx: completedTx,
@ -109,6 +132,26 @@ var actions = {
cancelAllTx: cancelAllTx,
viewPendingTx: viewPendingTx,
VIEW_PENDING_TX: 'VIEW_PENDING_TX',
// send screen
estimateGas,
getGasPrice,
UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT',
UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE',
UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL',
UPDATE_SEND_FROM: 'UPDATE_SEND_FROM',
UPDATE_SEND_TO: 'UPDATE_SEND_TO',
UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT',
UPDATE_SEND_MEMO: 'UPDATE_SEND_MEMO',
UPDATE_SEND_ERRORS: 'UPDATE_SEND_ERRORS',
updateGasLimit,
updateGasPrice,
updateGasTotal,
updateSendFrom,
updateSendTo,
updateSendAmount,
updateSendMemo,
updateSendErrors,
setSelectedAddress,
// app messages
confirmSeedWords: confirmSeedWords,
showAccountDetail: showAccountDetail,
@ -125,8 +168,13 @@ var actions = {
SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
showAddTokenPage,
addToken,
addTokens,
removeToken,
updateTokens,
UPDATE_TOKENS: 'UPDATE_TOKENS',
setRpcTarget: setRpcTarget,
setProviderType: setProviderType,
updateProviderType,
// loading overlay
SHOW_LOADING: 'SHOW_LOADING_INDICATION',
HIDE_LOADING: 'HIDE_LOADING_INDICATION',
@ -141,6 +189,8 @@ var actions = {
coinBaseSubview: coinBaseSubview,
SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW',
shapeShiftSubview: shapeShiftSubview,
UPDATE_TOKEN_EXCHANGE_RATE: 'UPDATE_TOKEN_EXCHANGE_RATE',
updateTokenExchangeRate,
PAIR_UPDATE: 'PAIR_UPDATE',
pairUpdate: pairUpdate,
coinShiftRquest: coinShiftRquest,
@ -165,6 +215,9 @@ var actions = {
callBackgroundThenUpdate,
forceUpdateMetamaskState,
TOGGLE_ACCOUNT_MENU: 'TOGGLE_ACCOUNT_MENU',
toggleAccountMenu,
}
module.exports = actions
@ -325,7 +378,24 @@ function navigateToNewAccountScreen () {
function addNewAccount () {
log.debug(`background.addNewAccount`)
return callBackgroundThenUpdate(background.addNewAccount)
return (dispatch, getState) => {
const oldIdentities = getState().metamask.identities
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
background.addNewAccount((err, { identities: newIdentities}) => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
const newAccountAddress = Object.keys(newIdentities).find(address => !oldIdentities[address])
dispatch(actions.hideLoadingIndication())
forceUpdateMetamaskState(dispatch)
return resolve(newAccountAddress)
})
});
}
}
function showInfoPage () {
@ -336,16 +406,16 @@ function showInfoPage () {
function setCurrentCurrency (currencyCode) {
return (dispatch) => {
dispatch(this.showLoadingIndication())
dispatch(actions.showLoadingIndication())
log.debug(`background.setCurrentCurrency`)
background.setCurrentCurrency(currencyCode, (err, data) => {
dispatch(this.hideLoadingIndication())
dispatch(actions.hideLoadingIndication())
if (err) {
log.error(err.stack)
return dispatch(actions.displayWarning(err.message))
}
dispatch({
type: this.SET_CURRENT_FIAT,
type: actions.SET_CURRENT_FIAT,
value: {
currentCurrency: data.currentCurrency,
conversionRate: data.conversionRate,
@ -418,10 +488,127 @@ function signTx (txData) {
dispatch(actions.showLoadingIndication())
global.ethQuery.sendTransaction(txData, (err, data) => {
dispatch(actions.hideLoadingIndication())
if (err) dispatch(actions.displayWarning(err.message))
dispatch(this.goHome())
if (err) return dispatch(actions.displayWarning(err.message))
dispatch(actions.hideWarning())
})
dispatch(actions.showConfTxPage())
dispatch(actions.showConfTxPage({}))
}
}
function estimateGas (params = {}) {
return (dispatch) => {
return new Promise((resolve, reject) => {
global.ethQuery.estimateGas(params, (err, data) => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.hideWarning())
dispatch(actions.updateGasLimit(data))
return resolve(data)
})
})
}
}
function updateGasLimit (gasLimit) {
return {
type: actions.UPDATE_GAS_LIMIT,
value: gasLimit,
}
}
function getGasPrice () {
return (dispatch) => {
return new Promise((resolve, reject) => {
global.ethQuery.gasPrice((err, data) => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.hideWarning())
dispatch(actions.updateGasPrice(data))
return resolve(data)
})
})
}
}
function updateGasPrice (gasPrice) {
return {
type: actions.UPDATE_GAS_PRICE,
value: gasPrice,
}
}
function updateGasTotal (gasTotal) {
return {
type: actions.UPDATE_GAS_TOTAL,
value: gasTotal,
}
}
function updateSendFrom (from) {
return {
type: actions.UPDATE_SEND_FROM,
value: from,
}
}
function updateSendTo (to) {
return {
type: actions.UPDATE_SEND_TO,
value: to,
}
}
function updateSendAmount (amount) {
return {
type: actions.UPDATE_SEND_AMOUNT,
value: amount,
}
}
function updateSendMemo (memo) {
return {
type: actions.UPDATE_SEND_MEMO,
value: memo,
}
}
function updateSendErrors (error) {
console.log(`updateSendErrors error`, error);
return {
type: actions.UPDATE_SEND_ERRORS,
value: error,
}
}
function sendTx (txData) {
log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`)
return (dispatch) => {
log.debug(`actions calling background.approveTransaction`)
background.approveTransaction(txData.id, (err) => {
if (err) {
dispatch(actions.txError(err))
return log.error(err.message)
}
dispatch(actions.completedTx(txData.id))
})
}
}
function signTokenTx (tokenAddress, toAddress, amount, txData) {
return dispatch => {
dispatch(actions.showLoadingIndication())
const token = global.eth.contract(abi).at(tokenAddress)
token.transfer(toAddress, amount, txData)
.catch(err => {
dispatch(actions.hideLoadingIndication())
dispatch(actions.displayWarning(err.message))
})
dispatch(actions.showConfTxPage({}))
}
}
@ -592,6 +779,26 @@ function setCurrentAccountTab (newTabName) {
return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName)
}
function setSelectedToken (tokenAddress) {
return {
type: actions.SET_SELECTED_TOKEN,
value: tokenAddress || null,
}
}
function setSelectedAddress (address) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
log.debug(`background.setSelectedAddress`)
background.setSelectedAddress(address, (err) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
}
})
}
}
function showAccountDetail (address) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
@ -605,6 +812,7 @@ function showAccountDetail (address) {
type: actions.SHOW_ACCOUNT_DETAIL,
value: address,
})
dispatch(actions.setSelectedToken())
})
}
}
@ -622,10 +830,11 @@ function showAccountsPage () {
}
}
function showConfTxPage (transForward = true) {
function showConfTxPage ({transForward = true, id}) {
return {
type: actions.SHOW_CONF_TX_PAGE,
transForward: transForward,
transForward,
id,
}
}
@ -655,28 +864,71 @@ function showConfigPage (transitionForward = true) {
}
}
function showAddTokenPage (transitionForward = true) {
function showAddTokenPage () {
return {
type: actions.SHOW_ADD_TOKEN_PAGE,
value: transitionForward,
}
}
function addToken (address, symbol, decimals) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
background.addToken(address, symbol, decimals, (err) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
}
setTimeout(() => {
dispatch(actions.goHome())
}, 250)
return new Promise((resolve, reject) => {
background.addToken(address, symbol, decimals, (err, tokens) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
reject(err)
}
dispatch(actions.updateTokens(tokens))
resolve(tokens)
})
})
}
}
function removeToken (address) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
background.removeToken(address, (err, tokens) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
reject(err)
}
dispatch(actions.updateTokens(tokens))
resolve(tokens)
})
})
}
}
function addTokens (tokens) {
return dispatch => {
if (Array.isArray(tokens)) {
return Promise.all(tokens.map(({ address, symbol, decimals }) => (
dispatch(addToken(address, symbol, decimals))
)))
} else {
return Promise.all(
Object
.entries(tokens)
.map(([_, { address, symbol, decimals }]) => (
dispatch(addToken(address, symbol, decimals))
))
)
}
}
}
function updateTokens(newTokens) {
return {
type: actions.UPDATE_TOKENS,
newTokens
}
}
function goBackToInitView () {
return {
type: actions.BACK_TO_INIT_MENU,
@ -738,11 +990,17 @@ function setProviderType (type) {
log.error(err)
return dispatch(self.displayWarning('Had a problem changing networks!'))
}
dispatch(actions.updateProviderType(type))
dispatch(actions.setSelectedToken())
})
return {
type: actions.SET_PROVIDER_TYPE,
value: type,
}
}
}
function updateProviderType(type) {
return {
type: actions.SET_PROVIDER_TYPE,
value: type,
}
}
@ -759,7 +1017,7 @@ function setRpcTarget (newRpc) {
}
// Calls the addressBookController to add a new address.
function addToAddressBook (recipient, nickname) {
function addToAddressBook (recipient, nickname = '') {
log.debug(`background.addToAddressBook`)
return (dispatch) => {
background.setAddressBook(recipient, nickname, (err, result) => {
@ -771,6 +1029,54 @@ function addToAddressBook (recipient, nickname) {
}
}
function useEtherscanProvider () {
log.debug(`background.useEtherscanProvider`)
background.useEtherscanProvider()
return {
type: actions.USE_ETHERSCAN_PROVIDER,
}
}
function showNetworkDropdown () {
return {
type: actions.NETWORK_DROPDOWN_OPEN,
}
}
function hideNetworkDropdown () {
return {
type: actions.NETWORK_DROPDOWN_CLOSE,
}
}
function showModal (payload) {
return {
type: actions.MODAL_OPEN,
payload,
}
}
function hideModal (payload) {
return {
type: actions.MODAL_CLOSE,
payload,
}
}
function showSidebar () {
return {
type: actions.SIDEBAR_OPEN,
}
}
function hideSidebar () {
return {
type: actions.SIDEBAR_CLOSE,
}
}
function showLoadingIndication (message) {
return {
type: actions.SHOW_LOADING,
@ -822,27 +1128,39 @@ function exportAccount (password, address) {
dispatch(self.showLoadingIndication())
log.debug(`background.submitPassword`)
background.submitPassword(password, function (err) {
if (err) {
log.error('Error in submiting password.')
dispatch(self.hideLoadingIndication())
return dispatch(self.displayWarning('Incorrect Password.'))
}
log.debug(`background.exportAccount`)
background.exportAccount(address, function (err, result) {
dispatch(self.hideLoadingIndication())
return new Promise((resolve, reject) => {
background.submitPassword(password, function (err) {
if (err) {
log.error(err)
return dispatch(self.displayWarning('Had a problem exporting the account.'))
log.error('Error in submiting password.')
dispatch(self.hideLoadingIndication())
dispatch(self.displayWarning('Incorrect Password.'))
return reject(err)
}
log.debug(`background.exportAccount`)
return background.exportAccount(address, function (err, result) {
dispatch(self.hideLoadingIndication())
dispatch(self.showPrivateKey(result))
if (err) {
log.error(err)
dispatch(self.displayWarning('Had a problem exporting the account.'))
return reject(err)
}
dispatch(self.exportAccountComplete())
return resolve(result)
})
})
})
}
}
function exportAccountComplete() {
return {
type: actions.EXPORT_ACCOUNT,
}
}
function showPrivateKey (key) {
return {
type: actions.SHOW_PRIVATE_KEY,
@ -873,6 +1191,12 @@ function showSendPage () {
}
}
function showSendTokenPage () {
return {
type: actions.SHOW_SEND_TOKEN_PAGE,
}
}
function buyEth (opts) {
return (dispatch) => {
const url = getBuyEthUrl(opts)
@ -970,7 +1294,10 @@ function reshowQrCode (data, coin) {
]
dispatch(actions.hideLoadingIndication())
return dispatch(actions.showQrView(data, message))
return dispatch(actions.showModal({
name: 'SHAPESHIFT_DEPOSIT_TX',
Qr: { data, message },
}))
})
}
}
@ -999,6 +1326,28 @@ function shapeShiftRequest (query, options, cb) {
}
}
function updateTokenExchangeRate (token = '') {
const pair = `${token.toLowerCase()}_eth`
return dispatch => {
if (!token) {
return
}
shapeShiftRequest('marketinfo', { pair }, marketinfo => {
if (!marketinfo.error) {
dispatch({
type: actions.UPDATE_TOKEN_EXCHANGE_RATE,
payload: {
pair,
marketinfo,
},
})
}
})
}
}
// Call Background Then Update
//
// A function generator for a common pattern wherein:
@ -1040,3 +1389,9 @@ function forceUpdateMetamaskState (dispatch) {
dispatch(actions.updateMetamaskState(newState))
})
}
function toggleAccountMenu () {
return {
type: actions.TOGGLE_ACCOUNT_MENU,
}
}

View File

@ -1,169 +1,65 @@
const inherits = require('util').inherits
const Component = require('react').Component
const classnames = require('classnames')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const Fuse = require('fuse.js')
const contractMap = require('eth-contract-metadata')
const TokenBalance = require('./components/token-balance')
const Identicon = require('./components/identicon')
const contractList = Object.entries(contractMap).map(([ _, tokenData]) => tokenData)
const fuse = new Fuse(contractList, {
shouldSort: true,
threshold: 0.45,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ['address', 'name', 'symbol'],
})
const actions = require('./actions')
const Tooltip = require('./components/tooltip.js')
const ethUtil = require('ethereumjs-util')
const abi = require('human-standard-token-abi')
const Eth = require('ethjs-query')
const EthContract = require('ethjs-contract')
const R = require('ramda')
const emptyAddr = '0x0000000000000000000000000000000000000000'
module.exports = connect(mapStateToProps)(AddTokenScreen)
module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen)
function mapStateToProps (state) {
const { identities, tokens } = state.metamask
return {
identities: state.metamask.identities,
identities,
tokens,
}
}
function mapDispatchToProps (dispatch) {
return {
goHome: () => dispatch(actions.goHome()),
addTokens: tokens => dispatch(actions.addTokens(tokens)),
}
}
inherits(AddTokenScreen, Component)
function AddTokenScreen () {
this.state = {
warning: null,
address: null,
symbol: 'TOKEN',
decimals: 18,
isShowingConfirmation: false,
customAddress: '',
customSymbol: '',
customDecimals: 0,
searchQuery: '',
isCollapsed: true,
selectedTokens: {},
errors: {},
}
this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this)
this.onNext = this.onNext.bind(this)
Component.call(this)
}
AddTokenScreen.prototype.render = function () {
const state = this.state
const props = this.props
const { warning, symbol, decimals } = state
return (
h('.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
props.dispatch(actions.goHome())
},
}),
h('h2.page-subtitle', 'Add 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('section.flex-row.flex-center', [
h('input#token-address', {
name: 'address',
placeholder: 'Token Contract Address',
onChange: this.tokenAddressDidChange.bind(this),
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
}),
]),
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Token Symbol'),
]),
h('div', { style: {display: 'flex'} }, [
h('input#token_symbol', {
placeholder: `Like "ETH"`,
value: symbol,
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onChange: (event) => {
var element = event.target
var symbol = element.value
this.setState({ symbol })
},
}),
]),
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Decimals of Precision'),
]),
h('div', { style: {display: 'flex'} }, [
h('input#token_decimals', {
value: decimals,
type: 'number',
min: 0,
max: 36,
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onChange: (event) => {
var element = event.target
var decimals = element.value.trim()
this.setState({ decimals })
},
}),
]),
h('button', {
style: {
alignSelf: 'center',
},
onClick: (event) => {
const valid = this.validateInputs()
if (!valid) return
const { address, symbol, decimals } = this.state
this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals))
},
}, 'Add'),
]),
]),
])
)
}
AddTokenScreen.prototype.componentWillMount = function () {
if (typeof global.ethereumProvider === 'undefined') return
@ -172,54 +68,96 @@ AddTokenScreen.prototype.componentWillMount = function () {
this.TokenContract = this.contract(abi)
}
AddTokenScreen.prototype.tokenAddressDidChange = function (event) {
const el = event.target
const address = el.value.trim()
if (ethUtil.isValidAddress(address) && address !== emptyAddr) {
this.setState({ address })
this.attemptToAutoFillTokenParams(address)
AddTokenScreen.prototype.toggleToken = function (address, token) {
const { selectedTokens, errors } = this.state
const { [address]: selectedToken } = selectedTokens
this.setState({
selectedTokens: {
...selectedTokens,
[address]: selectedToken ? null : token,
},
errors: {
...errors,
tokenSelector: null,
},
})
}
AddTokenScreen.prototype.onNext = function () {
const { isValid, errors } = this.validate()
return !isValid
? this.setState({ errors })
: this.setState({ isShowingConfirmation: true })
}
AddTokenScreen.prototype.tokenAddressDidChange = function (e) {
const customAddress = e.target.value.trim()
this.setState({ customAddress })
if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) {
this.attemptToAutoFillTokenParams(customAddress)
} else {
this.setState({
customSymbol: '',
customDecimals: 0,
})
}
}
AddTokenScreen.prototype.validateInputs = function () {
let msg = ''
const state = this.state
AddTokenScreen.prototype.checkExistingAddresses = function (address) {
const tokensList = this.props.tokens
const matchesAddress = existingToken => {
return existingToken.address.toLowerCase() === address.toLowerCase()
}
return R.any(matchesAddress)(tokensList)
}
AddTokenScreen.prototype.validate = function () {
const errors = {}
const identitiesList = Object.keys(this.props.identities)
const { address, symbol, decimals } = state
const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state
const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
const validAddress = ethUtil.isValidAddress(address)
if (!validAddress) {
msg += 'Address is invalid. '
if (customAddress) {
const validAddress = ethUtil.isValidAddress(customAddress)
if (!validAddress) {
errors.customAddress = 'Address is invalid. '
}
const validDecimals = customDecimals >= 0 && customDecimals < 36
if (!validDecimals) {
errors.customDecimals = 'Decimals must be at least 0, and not over 36.'
}
const symbolLen = customSymbol.trim().length
const validSymbol = symbolLen > 0 && symbolLen < 10
if (!validSymbol) {
errors.customSymbol = 'Symbol must be between 0 and 10 characters.'
}
const ownAddress = identitiesList.includes(standardAddress)
if (ownAddress) {
errors.customAddress = 'Personal address detected. Input the token contract address.'
}
const tokenAlreadyAdded = this.checkExistingAddresses(customAddress)
if (tokenAlreadyAdded) {
errors.customAddress = 'Token has already been added.'
}
} else if (
Object.entries(selectedTokens)
.reduce((isEmpty, [ symbol, isSelected ]) => (
isEmpty && !isSelected
), true)
) {
errors.tokenSelector = 'Must select at least 1 token.'
}
const validDecimals = decimals >= 0 && decimals < 36
if (!validDecimals) {
msg += 'Decimals must be at least 0, and not over 36. '
return {
isValid: !Object.keys(errors).length,
errors,
}
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
}
AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
@ -232,7 +170,195 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address)
const [ symbol, decimals ] = results
if (symbol && decimals) {
console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals })
this.setState({ symbol: symbol[0], decimals: decimals[0].toString() })
this.setState({
customSymbol: symbol[0],
customDecimals: decimals[0].toString(),
})
}
}
AddTokenScreen.prototype.renderCustomForm = function () {
const { customAddress, customSymbol, customDecimals, errors } = this.state
return !this.state.isCollapsed && (
h('div.add-token__add-custom-form', [
h('div', {
className: classnames('add-token__add-custom-field', {
'add-token__add-custom-field--error': errors.customAddress,
}),
}, [
h('div.add-token__add-custom-label', 'Token Address'),
h('input.add-token__add-custom-input', {
type: 'text',
onChange: this.tokenAddressDidChange,
value: customAddress,
}),
h('div.add-token__add-custom-error-message', errors.customAddress),
]),
h('div', {
className: classnames('add-token__add-custom-field', {
'add-token__add-custom-field--error': errors.customSymbol,
}),
}, [
h('div.add-token__add-custom-label', 'Token Symbol'),
h('input.add-token__add-custom-input', {
type: 'text',
value: customSymbol,
disabled: true,
}),
h('div.add-token__add-custom-error-message', errors.customSymbol),
]),
h('div', {
className: classnames('add-token__add-custom-field', {
'add-token__add-custom-field--error': errors.customDecimals,
}),
}, [
h('div.add-token__add-custom-label', 'Decimals of Precision'),
h('input.add-token__add-custom-input', {
type: 'number',
value: customDecimals,
disabled: true,
}),
h('div.add-token__add-custom-error-message', errors.customDecimals),
]),
])
)
}
AddTokenScreen.prototype.renderTokenList = function () {
const { searchQuery = '', selectedTokens } = this.state
const results = searchQuery
? fuse.search(searchQuery) || []
: contractList
return Array(6).fill(undefined)
.map((_, i) => {
const { logo, symbol, name, address } = results[i] || {}
const tokenAlreadyAdded = this.checkExistingAddresses(address)
return Boolean(logo || symbol || name) && (
h('div.add-token__token-wrapper', {
className: classnames({
'add-token__token-wrapper--selected': selectedTokens[address],
'add-token__token-wrapper--disabled': tokenAlreadyAdded,
}),
onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]),
}, [
h('div.add-token__token-icon', {
style: {
backgroundImage: `url(images/contract/${logo})`,
},
}),
h('div.add-token__token-data', [
h('div.add-token__token-symbol', symbol),
h('div.add-token__token-name', name),
]),
tokenAlreadyAdded && (
h('div.add-token__token-message', 'Already added')
),
])
)
})
}
AddTokenScreen.prototype.renderConfirmation = function () {
const {
customAddress: address,
customSymbol: symbol,
customDecimals: decimals,
selectedTokens,
} = this.state
const { addTokens, goHome } = this.props
const customToken = {
address,
symbol,
decimals,
}
const tokens = address && symbol && decimals
? { ...selectedTokens, [address]: customToken }
: selectedTokens
return (
h('div.add-token', [
h('div.add-token__wrapper', [
h('div.add-token__title-container.add-token__confirmation-title', [
h('div.add-token__title', 'Add Token'),
h('div.add-token__description', 'Would you like to add these tokens?'),
]),
h('div.add-token__content-container.add-token__confirmation-content', [
h('div.add-token__description.add-token__confirmation-description', 'Your balances'),
h('div.add-token__confirmation-token-list',
Object.entries(tokens)
.map(([ address, token ]) => (
h('span.add-token__confirmation-token-list-item', [
h(Identicon, {
className: 'add-token__confirmation-token-icon',
diameter: 75,
address,
}),
h(TokenBalance, { token }),
])
))
),
]),
]),
h('div.add-token__buttons', [
h('button.btn-secondary', {
onClick: () => addTokens(tokens).then(goHome),
}, 'Add Tokens'),
h('button.btn-tertiary', {
onClick: () => this.setState({ isShowingConfirmation: false }),
}, 'Back'),
]),
])
)
}
AddTokenScreen.prototype.render = function () {
const { isCollapsed, errors, isShowingConfirmation } = this.state
const { goHome } = this.props
return isShowingConfirmation
? this.renderConfirmation()
: (
h('div.add-token', [
h('div.add-token__wrapper', [
h('div.add-token__title-container', [
h('div.add-token__title', 'Add Token'),
h('div.add-token__description', 'Keep track of the tokens youve bought with your MetaMask account. If you bought tokens using a different account, those tokens will not appear here.'),
h('div.add-token__description', 'Search for tokens or select from our list of popular tokens.'),
]),
h('div.add-token__content-container', [
h('div.add-token__input-container', [
h('input.add-token__input', {
type: 'text',
placeholder: 'Search',
onChange: e => this.setState({ searchQuery: e.target.value }),
}),
h('div.add-token__search-input-error-message', errors.tokenSelector),
]),
h(
'div.add-token__token-icons-container',
this.renderTokenList(),
),
]),
h('div.add-token__footers', [
h('div.add-token__add-custom', {
onClick: () => this.setState({ isCollapsed: !isCollapsed }),
}, 'Add custom token'),
this.renderCustomForm(),
]),
]),
h('div.add-token__buttons', [
h('button.btn-secondary', {
onClick: this.onNext,
}, 'Next'),
h('button.btn-tertiary', {
onClick: goHome,
}, 'Cancel'),
]),
])
)
}

View File

@ -2,37 +2,44 @@ const inherits = require('util').inherits
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const { checkFeatureToggle } = require('../lib/feature-toggle-utils')
const actions = require('./actions')
// init
const InitializeMenuScreen = require('./first-time/init-menu')
const NewKeyChainScreen = require('./new-keychain')
// unlock
const UnlockScreen = require('./unlock')
// accounts
const AccountDetailScreen = require('./account-detail')
const MainContainer = require('./main-container')
const SendTransactionScreen = require('./send')
const SendTransactionScreen2 = require('./components/send/send-v2-container')
const SendTokenScreen = require('./components/send-token')
const ConfirmTxScreen = require('./conf-tx')
// notice
const NoticeScreen = require('./components/notice')
const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// slideout menu
const WalletView = require('./components/wallet-view')
// other views
const ConfigScreen = require('./config')
const AddTokenScreen = require('./add-token')
const Import = require('./accounts/import')
const InfoScreen = require('./info')
const Loading = require('./components/loading')
const SandwichExpando = require('sandwich-expando')
const Dropdown = require('./components/dropdown').Dropdown
const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem
const NetworkIndicator = require('./components/network')
const Identicon = require('./components/identicon')
const BuyView = require('./components/buy-button-subview')
const QrView = require('./components/qr-code')
const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete')
const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const NetworkDropdown = require('./components/dropdowns/network-dropdown')
const AccountMenu = require('./components/account-menu')
module.exports = connect(mapStateToProps)(App)
// Global Modals
const Modal = require('./components/modals/index').Modal
module.exports = connect(mapStateToProps, mapDispatchToProps)(App)
inherits(App, Component)
function App () { Component.call(this) }
@ -48,11 +55,14 @@ function mapStateToProps (state) {
return {
// state from plugin
networkDropdownOpen: state.appState.networkDropdownOpen,
sidebarOpen: state.appState.sidebarOpen,
isLoading: state.appState.isLoading,
loadingMessage: state.appState.loadingMessage,
noActiveNotices: state.metamask.noActiveNotices,
isInitialized: state.metamask.isInitialized,
isUnlocked: state.metamask.isUnlocked,
selectedAddress: state.metamask.selectedAddress,
currentView: state.appState.currentView,
activeAddress: state.appState.activeAddress,
transForward: state.appState.transForward,
@ -74,9 +84,24 @@ function mapStateToProps (state) {
}
}
function mapDispatchToProps (dispatch, ownProps) {
return {
dispatch,
hideSidebar: () => dispatch(actions.hideSidebar()),
showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
}
}
App.prototype.componentWillMount = function () {
this.props.setCurrentCurrencyToUSD()
}
App.prototype.render = function () {
var props = this.props
const { isLoading, loadingMessage, transForward, network } = props
const { isLoading, loadingMessage, network } = props
const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ?
`Connecting to ${this.getNetworkName()}` : null
@ -86,307 +111,166 @@ App.prototype.render = function () {
h('.flex-column.full-height', {
style: {
// Windows was showing a vertical scroll bar:
overflow: 'hidden',
overflowX: 'hidden',
position: 'relative',
alignItems: 'center',
},
}, [
// global modal
h(Modal, {}, []),
// app bar
this.renderAppBar(),
this.renderNetworkDropdown(),
this.renderDropdown(),
// sidebar
this.renderSidebar(),
// network dropdown
h(NetworkDropdown, {
provider: this.props.provider,
frequentRpcList: this.props.frequentRpcList,
}, []),
h(AccountMenu),
h(Loading, {
isLoading: isLoading || isLoadingNetwork,
loadingMessage: loadMessage,
}),
// panel content
h('.app-primary' + (transForward ? '.from-right' : '.from-left'), {
style: {
width: '100%',
},
}, [
this.renderPrimary(),
]),
// content
this.renderPrimary(),
])
)
}
App.prototype.renderGlobalModal = function () {
return h(Modal, {
ref: 'modalRef',
}, [
// h(BuyOptions, {}, []),
])
}
App.prototype.renderSidebar = function () {
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,
])
}
App.prototype.renderAppBar = function () {
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
const props = this.props
const state = this.state || {}
const isNetworkMenuOpen = state.isNetworkMenuOpen || false
console.log(this.props)
return (
h('.full-width', {
height: '38px',
style: {},
}, [
h('.app-header.flex-row.flex-space-between', {
style: {
alignItems: 'center',
visibility: props.isUnlocked ? 'visible' : 'none',
background: props.isUnlocked ? 'white' : 'none',
height: '38px',
position: 'relative',
zIndex: 12,
},
style: {},
}, [
h('div.left-menu-section', {
style: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
}, [
// mini logo
h('img', {
height: 24,
width: 24,
src: '/images/icon-128.png',
}),
h(NetworkIndicator, {
network: this.props.network,
provider: this.props.provider,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
},
}),
]),
props.isUnlocked && h('div', {
style: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
}, [
props.isUnlocked && h(AccountDropdowns, {
h('div.app-header-contents', {}, [
h('div.left-menu-wrapper', {
style: {},
enableAccountsSelector: true,
identities: this.props.identities,
selected: this.props.currentView.context,
network: this.props.network,
keyrings: this.props.keyrings,
}, []),
}, [
// mini logo
h('img', {
height: 24,
width: 24,
src: '/images/icon-128.png',
}),
// hamburger
props.isUnlocked && h(SandwichExpando, {
className: 'sandwich-expando',
width: 16,
barHeight: 2,
padding: 0,
isOpen: state.isMainMenuOpen,
color: 'rgb(247,146,30)',
onClick: () => {
this.setState({
isMainMenuOpen: !state.isMainMenuOpen,
})
},
}),
// metamask name
h('h1', {
style: {
position: 'relative',
paddingLeft: '9px',
color: '#5B5D67',
},
}, 'MetaMask'),
]),
h('div.header__right-actions', [
h('div.network-component-wrapper', {
style: {},
}, [
// Network Indicator
h(NetworkIndicator, {
network: this.props.network,
provider: this.props.provider,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
if (this.props.networkDropdownOpen === false) {
this.props.showNetworkDropdown()
} else {
this.props.hideNetworkDropdown()
}
},
}),
]),
h('div.account-menu__icon', { onClick: this.props.toggleAccountMenu }, [
h(Identicon, {
address: this.props.selectedAddress,
diameter: 32,
}),
]),
]),
]),
]),
])
)
}
App.prototype.renderNetworkDropdown = function () {
const props = this.props
const { provider: { type: providerType, rpcTarget: activeNetwork } } = props
const rpcList = props.frequentRpcList
const state = this.state || {}
const isOpen = state.isNetworkMenuOpen
return h(Dropdown, {
useCssTransition: true,
isOpen,
onClickOutside: (event) => {
const { classList } = event.target
const isNotToggleElement = [
classList.contains('menu-icon'),
classList.contains('network-name'),
classList.contains('network-indicator'),
].filter(bool => bool).length === 0
// classes from three constituent nodes of the toggle element
if (isNotToggleElement) {
this.setState({ isNetworkMenuOpen: false })
}
},
zIndex: 11,
style: {
position: 'absolute',
left: '2px',
top: '36px',
},
innerStyle: {
padding: '2px 16px 2px 0px',
},
}, [
h(
DropdownMenuItem,
{
key: 'main',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('mainnet')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.diamond'),
'Main Ethereum Network',
providerType === 'mainnet' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'ropsten',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('ropsten')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.red-dot'),
'Ropsten Test Network',
providerType === 'ropsten' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'kovan',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('kovan')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.hollow-diamond'),
'Kovan Test Network',
providerType === 'kovan' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'rinkeby',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('rinkeby')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.golden-square'),
'Rinkeby Test Network',
providerType === 'rinkeby' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'default',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('localhost')),
style: {
fontSize: '18px',
},
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
'Localhost 8545',
activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null,
]
),
this.renderCustomOption(props.provider),
this.renderCommonRpc(rpcList, props.provider),
h(
DropdownMenuItem,
{
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => this.props.dispatch(actions.showConfigPage()),
style: {
fontSize: '18px',
},
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
'Custom RPC',
activeNetwork === 'custom' ? h('.check', '✓') : null,
]
),
])
}
App.prototype.renderDropdown = function () {
const state = this.state || {}
const isOpen = state.isMainMenuOpen
return h(Dropdown, {
useCssTransition: true,
isOpen: isOpen,
zIndex: 11,
onClickOutside: (event) => {
const classList = event.target.classList
const parentClassList = event.target.parentElement.classList
const isToggleElement = classList.contains('sandwich-expando') ||
parentClassList.contains('sandwich-expando')
if (isOpen && !isToggleElement) {
this.setState({ isMainMenuOpen: false })
}
},
style: {
position: 'absolute',
right: '2px',
top: '38px',
},
innerStyle: {},
}, [
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { this.props.dispatch(actions.showConfigPage()) },
}, 'Settings'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { this.props.dispatch(actions.lockMetamask()) },
}, 'Lock'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { this.props.dispatch(actions.showInfoPage()) },
}, 'Info/Help'),
])
}
App.prototype.renderBackButton = function (style, justArrow = false) {
var props = this.props
@ -451,32 +335,38 @@ App.prototype.renderPrimary = function () {
// show unlock screen
if (!props.isUnlocked) {
switch (props.currentView.name) {
case 'restoreVault':
log.debug('rendering restore vault screen')
return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
case 'config':
log.debug('rendering config screen from unlock screen.')
return h(ConfigScreen, {key: 'config'})
default:
log.debug('rendering locked screen')
return h(UnlockScreen, {key: 'locked'})
}
return h(MainContainer, {
currentViewName: props.currentView.name,
isUnlocked: props.isUnlocked,
})
}
// show current view
switch (props.currentView.name) {
case 'accountDetail':
log.debug('rendering account detail screen')
return h(AccountDetailScreen, {key: 'account-detail'})
log.debug('rendering main container')
return h(MainContainer, {key: 'account-detail'})
case 'sendTransaction':
log.debug('rendering send tx screen')
return h(SendTransactionScreen, {key: 'send-transaction'})
// Going to leave this here until we are ready to delete SendTransactionScreen v1
// const SendComponentToRender = checkFeatureToggle('send-v2')
// ? SendTransactionScreen2
// : SendTransactionScreen
return h(SendTransactionScreen2, {key: 'send-transaction'})
case 'sendToken':
log.debug('rendering send token screen')
// Going to leave this here until we are ready to delete SendTransactionScreen v1
// const SendTokenComponentToRender = checkFeatureToggle('send-v2')
// ? SendTransactionScreen2
// : SendTokenScreen
return h(SendTransactionScreen2, {key: 'sendToken'})
case 'newKeychain':
log.debug('rendering new keychain screen')
@ -510,37 +400,9 @@ App.prototype.renderPrimary = function () {
log.debug('rendering buy ether screen')
return h(BuyView, {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: () => props.dispatch(actions.backToAccountDetail(props.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(AccountDetailScreen, {key: 'account-detail'})
return h(MainContainer, {key: 'account-detail'})
}
}
@ -556,40 +418,6 @@ App.prototype.toggleMetamaskActive = function () {
}
}
App.prototype.renderCustomOption = function (provider) {
const { rpcTarget, type } = provider
const props = this.props
if (type !== 'rpc') return null
// Concatenate long URLs
let label = rpcTarget
if (rpcTarget.length > 31) {
label = label.substr(0, 34) + '...'
}
switch (rpcTarget) {
case 'http://localhost:8545':
return null
default:
return h(
DropdownMenuItem,
{
key: rpcTarget,
onClick: () => props.dispatch(actions.setRpcTarget(rpcTarget)),
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
label,
h('.check', '✓'),
]
)
}
}
App.prototype.getNetworkName = function () {
const { provider } = this.props
const providerName = provider.type
@ -610,28 +438,3 @@ App.prototype.getNetworkName = function () {
return name
}
App.prototype.renderCommonRpc = function (rpcList, provider) {
const props = this.props
const rpcTarget = provider.rpcTarget
return rpcList.map((rpc) => {
if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) {
return null
} else {
return h(
DropdownMenuItem,
{
key: `common${rpc}`,
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
onClick: () => props.dispatch(actions.setRpcTarget(rpc)),
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
rpc,
rpcTarget === rpc ? h('.check', '✓') : null,
]
)
}
})
}

View File

@ -0,0 +1,154 @@
const inherits = require('util').inherits
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const actions = require('../../actions')
const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu')
const Identicon = require('../identicon')
const { formatBalance } = require('../../util')
module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountMenu)
inherits(AccountMenu, Component)
function AccountMenu () { Component.call(this) }
function mapStateToProps (state) {
return {
selectedAddress: state.metamask.selectedAddress,
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
keyrings: state.metamask.keyrings,
identities: state.metamask.identities,
accounts: state.metamask.accounts,
}
}
function mapDispatchToProps (dispatch) {
return {
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
showAccountDetail: address => {
dispatch(actions.showAccountDetail(address))
dispatch(actions.toggleAccountMenu())
},
lockMetamask: () => {
dispatch(actions.lockMetamask())
dispatch(actions.toggleAccountMenu())
},
showConfigPage: () => {
console.log('hihihih')
dispatch(actions.showConfigPage())
dispatch(actions.toggleAccountMenu())
},
showNewAccountModal: () => {
dispatch(actions.showModal({ name: 'NEW_ACCOUNT' }))
dispatch(actions.toggleAccountMenu())
},
showImportPage: () => {
dispatch(actions.showImportPage())
dispatch(actions.toggleAccountMenu())
},
}
}
AccountMenu.prototype.render = function () {
const {
isAccountMenuOpen,
toggleAccountMenu,
showNewAccountModal,
showImportPage,
lockMetamask,
showConfigPage,
} = this.props
console.log(showConfigPage)
return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [
h(CloseArea, { onClick: toggleAccountMenu }),
h(Item, {
className: 'account-menu__header',
onClick: lockMetamask,
}, [
'My Accounts',
h('button.account-menu__logout-button', 'Log out'),
]),
h(Divider),
h('div.account-menu__accounts', this.renderAccounts()),
h(Divider),
h(Item, {
onClick: showNewAccountModal,
icon: h('img', { src: 'images/plus-btn-white.svg' }),
text: 'Create Account',
}),
h(Item, {
onClick: showImportPage,
icon: h('img', { src: 'images/import-account.svg' }),
text: 'Import Account',
}),
h(Divider),
h(Item, {
icon: h('img', { src: 'images/mm-info-icon.svg' }),
text: 'Info & Help',
}),
h(Item, {
onClick: showConfigPage,
icon: h('img', { src: 'images/settings.svg' }),
text: 'Settings',
}),
])
}
AccountMenu.prototype.renderAccounts = function () {
const {
identities,
accounts,
selected,
keyrings,
showAccountDetail,
} = this.props
return Object.keys(identities).map((key, index) => {
const identity = identities[key]
const isSelected = identity.address === selected
const balanceValue = accounts[key].balance
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...'
const simpleAddress = identity.address.substring(2).toLowerCase()
const keyring = keyrings.find((kr) => {
return kr.accounts.includes(simpleAddress) ||
kr.accounts.includes(identity.address)
})
return h(
'div.account-menu__account.menu__item--clickable',
{ onClick: () => showAccountDetail(identity.address) },
[
h('div.account-menu__check-mark', [
isSelected ? h('i.fa.fa-check') : null,
]),
h(
Identicon,
{
address: identity.address,
diameter: 24,
},
),
h('div.account-menu__account-info', [
h('div.account-menu__name', identity.name || ''),
h('div.account-menu__balance', formattedBalance),
]),
this.indicateIfLoose(keyring),
],
)
})
}
AccountMenu.prototype.indicateIfLoose = function (keyring) {
try { // Sometimes keyrings aren't loaded yet:
const type = keyring.type
const isLoose = type !== 'HD Key Tree'
return isLoose ? h('.keyring-label', 'LOOSE') : null
} catch (e) { return }
}

View File

@ -0,0 +1,120 @@
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenBalance = require('./token-balance')
const Identicon = require('./identicon')
const { formatBalance, generateBalanceObject } = require('../util')
module.exports = connect(mapStateToProps)(BalanceComponent)
function mapStateToProps (state) {
const accounts = state.metamask.accounts
const network = state.metamask.network
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
const account = accounts[selectedAddress]
return {
account,
network,
conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency,
}
}
inherits(BalanceComponent, Component)
function BalanceComponent () {
Component.call(this)
}
BalanceComponent.prototype.render = function () {
const props = this.props
const { token, network } = props
return h('div.balance-container', {}, [
// TODO: balance icon needs to be passed in
// h('img.balance-icon', {
// src: '../images/eth_logo.svg',
// style: {},
// }),
h(Identicon, {
diameter: 45,
address: token && token.address,
network,
}),
token ? this.renderTokenBalance() : this.renderBalance(),
])
}
BalanceComponent.prototype.renderTokenBalance = function () {
const { token } = this.props
return h('div.flex-column.balance-display', [
h('div.token-amount', [ h(TokenBalance, { token }) ]),
])
}
BalanceComponent.prototype.renderBalance = function () {
const props = this.props
const { shorten, account } = props
const balanceValue = account && account.balance
const needsParse = 'needsParse' in props ? props.needsParse : true
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...'
const showFiat = 'showFiat' in props ? props.showFiat : true
if (formattedBalance === 'None' || formattedBalance === '...') {
return h('div.flex-column.balance-display', {}, [
h('div.token-amount', {
style: {},
}, formattedBalance),
])
}
return h('div.flex-column.balance-display', {}, [
h('div.token-amount', {
style: {},
}, this.getTokenBalance(formattedBalance, shorten)),
showFiat ? this.renderFiatValue(formattedBalance) : null,
])
}
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) {
if (fiatDisplayNumber === 'N/A') return null
return h('div.fiat-amount', {
style: {},
}, `${fiatPrefix}${fiatDisplayNumber} ${fiatSuffix}`)
}
BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) {
const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3)
const balanceValue = shorten ? balanceObj.shortBalance : balanceObj.balance
const label = balanceObj.label
return `${balanceValue} ${label}`
}
BalanceComponent.prototype.getFiatDisplayNumber = function (formattedBalance, conversionRate) {
if (formattedBalance === 'None') return formattedBalance
if (conversionRate === 0) return 'N/A'
const splitBalance = formattedBalance.split(' ')
return (Number(splitBalance[0]) * conversionRate).toFixed(2)
}

View File

@ -245,7 +245,7 @@ BuyButtonSubview.prototype.navigateTo = function (url) {
BuyButtonSubview.prototype.backButtonContext = function () {
if (this.props.context === 'confTx') {
this.props.dispatch(actions.showConfTxPage(false))
this.props.dispatch(actions.showConfTxPage({transForward: false}))
} else {
this.props.dispatch(actions.goHome())
}

View File

@ -0,0 +1,55 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const InputNumber = require('../input-number.js')
const GasSlider = require('./gas-slider.js')
module.exports = GasModalCard
inherits(GasModalCard, Component)
function GasModalCard () {
Component.call(this)
}
GasModalCard.prototype.render = function () {
const {
memo,
identities,
onChange,
unitLabel,
value,
min,
max,
step,
title,
copy
} = this.props
return h('div.send-v2__gas-modal-card', [
h('div.send-v2__gas-modal-card__title', {}, title),
h('div.send-v2__gas-modal-card__copy', {}, copy),
h(InputNumber, {
unitLabel,
step,
max,
min,
placeholder: '0',
value,
onChange,
}),
h(GasSlider, {
value,
step,
max,
min,
onChange,
}),
])
}

View File

@ -0,0 +1,50 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = GasSlider
inherits(GasSlider, Component)
function GasSlider () {
Component.call(this)
}
GasSlider.prototype.render = function () {
const {
memo,
identities,
onChange,
unitLabel,
value,
id,
step,
max,
min,
} = this.props
return h('div.gas-slider', [
h('input.gas-slider__input', {
type: 'range',
step,
max,
min,
value,
id: 'gasSlider',
onChange: event => onChange(event.target.value),
}, []),
h('div.gas-slider__bar', [
h('div.gas-slider__low'),
h('div.gas-slider__mid'),
h('div.gas-slider__high'),
]),
])
}

View File

@ -0,0 +1,158 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const GasModalCard = require('./gas-modal-card')
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
const {
getGasPrice,
getGasLimit,
conversionRateSelector,
} = require('../../selectors')
function mapStateToProps (state) {
return {
gasPrice: getGasPrice(state),
gasLimit: getGasLimit(state),
conversionRate: conversionRateSelector(state),
}
}
function mapDispatchToProps (dispatch) {
return {
hideModal: () => dispatch(actions.hideModal()),
updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)),
updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)),
updateGasTotal: newGasTotal => dispatch(actions.updateGasTotal(newGasTotal)),
}
}
inherits(CustomizeGasModal, Component)
function CustomizeGasModal (props) {
Component.call(this)
this.state = {
gasPrice: props.gasPrice,
gasLimit: props.gasLimit,
}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
CustomizeGasModal.prototype.save = function (gasPrice, gasLimit) {
const {
updateGasPrice,
updateGasLimit,
hideModal,
updateGasTotal
} = this.props
const newGasTotal = multiplyCurrencies(gasLimit, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 16,
})
updateGasPrice(gasPrice)
updateGasLimit(gasLimit)
updateGasTotal(newGasTotal)
hideModal()
}
CustomizeGasModal.prototype.convertAndSetGasLimit = function (newGasLimit) {
const convertedGasLimit = conversionUtil(newGasLimit, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
})
this.setState({ gasLimit: convertedGasLimit })
}
CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
const convertedGasPrice = conversionUtil(newGasPrice, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
fromDenomination: 'GWEI',
toDenomination: 'WEI',
})
this.setState({ gasPrice: convertedGasPrice })
}
CustomizeGasModal.prototype.render = function () {
const { hideModal, conversionRate } = this.props
const { gasPrice, gasLimit } = this.state
const convertedGasPrice = conversionUtil(gasPrice, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
toDenomination: 'GWEI',
})
const convertedGasLimit = conversionUtil(gasLimit, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
})
return h('div.send-v2__customize-gas', {}, [
h('div', {
}, [
h('div.send-v2__customize-gas__header', {}, [
h('div.send-v2__customize-gas__title', 'Customize Gas'),
h('div.send-v2__customize-gas__close', {
onClick: hideModal,
}),
]),
h('div.send-v2__customize-gas__body', {}, [
h(GasModalCard, {
value: convertedGasPrice,
min: 0,
max: 1000,
step: 1,
onChange: value => this.convertAndSetGasPrice(value),
title: 'Gas Price',
copy: 'We calculate the suggested gas prices based on network success rates.',
}),
h(GasModalCard, {
value: convertedGasLimit,
min: 20000,
max: 100000,
step: 1,
onChange: value => this.convertAndSetGasLimit(value),
title: 'Gas Limit',
copy: 'We calculate the suggested gas limit based on network success rates.',
}),
]),
h('div.send-v2__customize-gas__footer', {}, [
h('div.send-v2__customize-gas__revert', {
onClick: () => console.log('Revert'),
}, ['Revert']),
h('div.send-v2__customize-gas__buttons', [
h('div.send-v2__customize-gas__cancel', {
onClick: this.props.hideModal,
}, ['CANCEL']),
h('div.send-v2__customize-gas__save', {
onClick: () => this.save(gasPrice, gasLimit),
}, ['SAVE']),
])
]),
]),
])
}

View File

@ -0,0 +1,29 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountDropdowns = require('./components/account-dropdowns')
inherits(AccountOptionsDropdown, Component)
function AccountOptionsDropdown () {
Component.call(this)
}
module.exports = AccountOptionsDropdown
// TODO: specify default props and proptypes
// TODO: hook up to state, connect to redux to clean up API
// TODO: selectedAddress is not defined... should we use selected?
AccountOptionsDropdown.prototype.render = function () {
const { selected, network, identities, style, dropdownWrapperStyle, menuItemStyles } = this.props
return h(AccountDropdowns, {
enableAccountOptions: true,
enableAccountsSelector: false,
selected: selectedAddress,
network,
identities,
style: style || {},
dropdownWrapperStyle: dropdownWrapperStyle || {},
menuItemStyles: menuItemStyles || {},
}, [])
}

View File

@ -0,0 +1,29 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountDropdowns = require('./components/account-dropdowns')
inherits(AccountSelectionDropdown, Component)
function AccountSelectionDropdown () {
Component.call(this)
}
module.exports = AccountSelectionDropdown
// TODO: specify default props and proptypes
// TODO: hook up to state, connect to redux to clean up API
// TODO: selectedAddress is not defined... should we use selected?
AccountSelectionDropdown.prototype.render = function () {
const { selected, network, identities, style, dropdownWrapperStyle, menuItemStyles } = this.props
return h(AccountDropdowns, {
enableAccountOptions: false,
enableAccountsSelector: true,
selected: selectedAddress,
network,
identities,
style: style || {},
dropdownWrapperStyle: dropdownWrapperStyle || {},
menuItemStyles: menuItemStyles || {},
}, [])
}

View File

@ -0,0 +1,469 @@
const Component = require('react').Component
const PropTypes = require('react').PropTypes
const h = require('react-hyperscript')
const actions = require('../../../actions')
const genAccountLink = require('../../../../lib/account-link.js')
const connect = require('react-redux').connect
const Dropdown = require('./dropdown').Dropdown
const DropdownMenuItem = require('./dropdown').DropdownMenuItem
const Identicon = require('../../identicon')
const ethUtil = require('ethereumjs-util')
const copyToClipboard = require('copy-to-clipboard')
const { formatBalance } = require('../../../util')
class AccountDropdowns extends Component {
constructor (props) {
super(props)
this.state = {
accountSelectorActive: false,
optionsMenuActive: false,
}
// Used for orangeaccount selector icon
// this.accountSelectorToggleClassName = 'accounts-selector'
this.accountSelectorToggleClassName = 'fa-angle-down'
this.optionsMenuToggleClassName = 'fa-ellipsis-h'
}
renderAccounts () {
const { identities, accounts, selected, menuItemStyles, actions, keyrings } = this.props
return Object.keys(identities).map((key, index) => {
const identity = identities[key]
const isSelected = identity.address === selected
const balanceValue = accounts[key].balance
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...'
const simpleAddress = identity.address.substring(2).toLowerCase()
const keyring = keyrings.find((kr) => {
return kr.accounts.includes(simpleAddress) ||
kr.accounts.includes(identity.address)
})
return h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
this.props.actions.showAccountDetail(identity.address)
},
style: Object.assign(
{
marginTop: index === 0 ? '5px' : '',
fontSize: '24px',
width: '260px',
},
menuItemStyles,
),
},
[
h('div.flex-row.flex-center', {}, [
h('span', {
style: {
flex: '1 1 0',
minWidth: '20px',
minHeight: '30px',
},
}, [
h('span', {
style: {
flex: '1 1 auto',
fontSize: '14px',
},
}, isSelected ? h('i.fa.fa-check') : null),
]),
h(
Identicon,
{
address: identity.address,
diameter: 24,
style: {
flex: '1 1 auto',
marginLeft: '10px',
},
},
),
h('span.flex-column', {
style: {
flex: '10 10 auto',
width: '175px',
alignItems: 'flex-start',
justifyContent: 'center',
marginLeft: '10px',
position: 'relative',
},
}, [
this.indicateIfLoose(keyring),
h('span.account-dropdown-name', {
style: {
fontSize: '18px',
maxWidth: '145px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}, identity.name || ''),
h('span.account-dropdown-balance', {
style: {
fontSize: '14px',
fontFamily: 'Avenir',
fontWeight: 500,
},
}, formattedBalance),
]),
h('span', {
style: {
flex: '3 3 auto',
},
}, [
h('span.account-dropdown-edit-button', {
style: {
fontSize: '16px',
},
onClick: () => {
actions.showEditAccountModal(identity)
},
}, [
'Edit',
]),
]),
]),
// =======
// },
// ),
// this.indicateIfLoose(keyring),
// h('span', {
// style: {
// marginLeft: '20px',
// fontSize: '24px',
// maxWidth: '145px',
// whiteSpace: 'nowrap',
// overflow: 'hidden',
// textOverflow: 'ellipsis',
// },
// }, identity.name || ''),
// h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
// >>>>>>> master:ui/app/components/account-dropdowns.js
]
)
})
}
indicateIfLoose (keyring) {
try { // Sometimes keyrings aren't loaded yet:
const type = keyring.type
const isLoose = type !== 'HD Key Tree'
return isLoose ? h('.keyring-label', 'LOOSE') : null
} catch (e) { return }
}
renderAccountSelector () {
const { actions, useCssTransition, innerStyle, sidebarOpen } = this.props
const { accountSelectorActive, menuItemStyles } = this.state
return h(
Dropdown,
{
useCssTransition,
style: {
marginLeft: '-185px',
marginTop: '50px',
minWidth: '180px',
overflowY: 'auto',
maxHeight: '300px',
width: '300px',
},
innerStyle,
isOpen: accountSelectorActive,
onClickOutside: (event) => {
const { classList } = event.target
const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName)
if (accountSelectorActive && isNotToggleElement) {
this.setState({ accountSelectorActive: false })
}
},
},
[
...this.renderAccounts(),
h(
DropdownMenuItem,
{
closeMenu: () => {},
style: Object.assign(
{},
menuItemStyles,
),
onClick: () => actions.showNewAccountModal(),
},
[
h(
'i.fa.fa-plus.fa-lg',
{
style: {
marginLeft: '8px',
},
}
),
h('span', {
style: {
marginLeft: '14px',
fontFamily: 'DIN OT',
fontSize: '16px',
lineHeight: '23px',
},
}, 'Create Account'),
],
),
h(
DropdownMenuItem,
{
closeMenu: () => {
if (sidebarOpen) {
actions.hideSidebar()
}
},
onClick: () => actions.showImportPage(),
style: Object.assign(
{},
menuItemStyles,
),
},
[
h(
'i.fa.fa-download.fa-lg',
{
style: {
marginLeft: '8px',
},
}
),
h('span', {
style: {
marginLeft: '20px',
marginBottom: '5px',
fontFamily: 'DIN OT',
fontSize: '16px',
lineHeight: '23px',
},
}, 'Import Account'),
]
),
]
)
}
renderAccountOptions () {
const { actions, dropdownWrapperStyle, useCssTransition } = this.props
const { optionsMenuActive, menuItemStyles } = this.state
const dropdownMenuItemStyle = {
fontFamily: 'DIN OT',
fontSize: 16,
lineHeight: '24px',
padding: '8px',
}
return h(
Dropdown,
{
useCssTransition,
style: Object.assign(
{
marginLeft: '-10px',
position: 'absolute',
width: '29vh', // affects both mobile and laptop views
},
dropdownWrapperStyle,
),
isOpen: optionsMenuActive,
onClickOutside: () => {
const { classList } = event.target
const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName)
if (optionsMenuActive && isNotToggleElement) {
this.setState({ optionsMenuActive: false })
}
},
},
[
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
this.props.actions.showAccountDetailModal()
},
style: Object.assign(
dropdownMenuItemStyle,
menuItemStyles,
),
},
'Account Details',
),
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
const { selected, network } = this.props
const url = genAccountLink(selected, network)
global.platform.openWindow({ url })
},
style: Object.assign(
dropdownMenuItemStyle,
menuItemStyles,
),
},
'View account on Etherscan',
),
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
const { selected } = this.props
const checkSumAddress = selected && ethUtil.toChecksumAddress(selected)
copyToClipboard(checkSumAddress)
},
style: Object.assign(
dropdownMenuItemStyle,
menuItemStyles,
),
},
'Copy Address to clipboard',
),
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => this.props.actions.showExportPrivateKeyModal(),
style: Object.assign(
dropdownMenuItemStyle,
menuItemStyles,
),
},
'Export Private Key',
),
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
actions.hideSidebar()
actions.showAddTokenPage()
},
style: Object.assign(
dropdownMenuItemStyle,
menuItemStyles,
),
},
'Add Token',
),
]
)
}
render () {
const { style, enableAccountsSelector, enableAccountOptions } = this.props
const { optionsMenuActive, accountSelectorActive } = this.state
return h(
'span',
{
style: style,
},
[
enableAccountsSelector && h(
'i.fa.fa-angle-down',
{
style: {
cursor: 'pointer',
},
onClick: (event) => {
event.stopPropagation()
this.setState({
accountSelectorActive: !accountSelectorActive,
optionsMenuActive: false,
})
},
},
this.renderAccountSelector(),
),
enableAccountOptions && h(
'i.fa.fa-ellipsis-h',
{
style: {
fontSize: '135%',
cursor: 'pointer',
},
onClick: (event) => {
event.stopPropagation()
this.setState({
accountSelectorActive: false,
optionsMenuActive: !optionsMenuActive,
})
},
},
this.renderAccountOptions()
),
]
)
}
}
AccountDropdowns.defaultProps = {
enableAccountsSelector: false,
enableAccountOptions: false,
}
AccountDropdowns.propTypes = {
identities: PropTypes.objectOf(PropTypes.object),
selected: PropTypes.string,
keyrings: PropTypes.array,
}
const mapDispatchToProps = (dispatch) => {
return {
actions: {
hideSidebar: () => dispatch(actions.hideSidebar()),
showConfigPage: () => dispatch(actions.showConfigPage()),
showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)),
showAccountDetailModal: () => {
dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
},
showEditAccountModal: (identity) => {
dispatch(actions.showModal({
name: 'EDIT_ACCOUNT_NAME',
identity,
}))
},
showNewAccountModal: () => {
dispatch(actions.showModal({ name: 'NEW_ACCOUNT' }))
},
showExportPrivateKeyModal: () => {
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
},
showAddTokenPage: () => {
dispatch(actions.showAddTokenPage())
},
addNewAccount: () => dispatch(actions.addNewAccount()),
showImportPage: () => dispatch(actions.showImportPage()),
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
},
}
}
function mapStateToProps (state) {
return {
keyrings: state.metamask.keyrings,
sidebarOpen: state.appState.sidebarOpen,
}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDropdowns)

View File

@ -1,14 +1,22 @@
const Component = require('react').Component
const PropTypes = require('react').PropTypes
const h = require('react-hyperscript')
const MenuDroppo = require('./menu-droppo')
const MenuDroppo = require('../../menu-droppo')
const extend = require('xtend')
const noop = () => {}
class Dropdown extends Component {
render () {
const { isOpen, onClickOutside, style, innerStyle, children, useCssTransition } = this.props
const {
containerClassName,
isOpen,
onClickOutside,
style,
innerStyle,
children,
useCssTransition,
} = this.props
const innerStyleDefaults = extend({
borderRadius: '4px',
@ -20,9 +28,10 @@ class Dropdown extends Component {
return h(
MenuDroppo,
{
containerClassName,
useCssTransition,
isOpen,
zIndex: 11,
zIndex: 30,
onClickOutside,
style,
innerStyle: innerStyleDefaults,
@ -31,8 +40,12 @@ class Dropdown extends Component {
h(
'style',
`
li.dropdown-menu-item:hover { color:rgb(225, 225, 225); }
li.dropdown-menu-item { color: rgb(185, 185, 185); position: relative }
li.dropdown-menu-item:hover {
color:rgb(225, 225, 225);
background-color: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
li.dropdown-menu-item { color: rgb(185, 185, 185); }
`
),
...children,
@ -67,7 +80,7 @@ class DropdownMenuItem extends Component {
},
style: Object.assign({
listStyle: 'none',
padding: '8px 0px 8px 0px',
padding: '8px 0px',
fontSize: '18px',
fontStyle: 'normal',
fontFamily: 'Montserrat Regular',
@ -75,6 +88,7 @@ class DropdownMenuItem extends Component {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
color: 'white',
}, style),
},
children

View File

@ -0,0 +1,51 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
inherits(Menu, Component)
function Menu () { Component.call(this) }
Menu.prototype.render = function () {
const { className = '', children, isShowing } = this.props
return isShowing
? h('div', { className: `menu ${className}` }, children)
: h('noscript')
}
inherits(Item, Component)
function Item () { Component.call(this) }
Item.prototype.render = function () {
const {
icon,
children,
text,
className = '',
onClick,
} = this.props
const itemClassName = `menu__item ${className} ${onClick ? 'menu__item--clickable' : ''}`
const iconComponent = icon ? h('div.menu__item__icon', [icon]) : null
const textComponent = text ? h('div.menu__item__text', text) : null
return children
? h('div', { className: itemClassName, onClick }, children)
: h('div.menu__item', { className: itemClassName, onClick }, [ iconComponent, textComponent ]
.filter(d => Boolean(d))
)
}
inherits(Divider, Component)
function Divider () { Component.call(this) }
Divider.prototype.render = function () {
return h('div.menu__divider')
}
inherits(CloseArea, Component)
function CloseArea () { Component.call(this) }
CloseArea.prototype.render = function () {
return h('div.menu__close-area', { onClick: this.props.onClick })
}
module.exports = { Menu, Item, Divider, CloseArea }

View File

@ -0,0 +1,28 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
inherits(NetworkDropdownIcon, Component)
module.exports = NetworkDropdownIcon
function NetworkDropdownIcon () {
Component.call(this)
}
NetworkDropdownIcon.prototype.render = function () {
const {
backgroundColor,
isSelected,
innerBorder = 'none',
} = this.props
return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {},
h('div', {
style: {
background: backgroundColor,
border: innerBorder,
},
})
)
}

View File

@ -0,0 +1,17 @@
// Reusable Dropdown Components
// TODO: Refactor into separate components
const Dropdown = require('./components/dropdown').Dropdown
const AccountDropdowns = require('./components/account-dropdowns')
// App-Specific Instances
const AccountSelectionDropdown = require('./account-selection-dropdown')
const AccountOptionsDropdown = require('./account-options-dropdown')
const NetworkDropdown = require('./network-dropdown').default
module.exports = {
AccountSelectionDropdown,
AccountOptionsDropdown,
NetworkDropdown,
Dropdown,
AccountDropdowns,
}

View File

@ -0,0 +1,316 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const Dropdown = require('./components/dropdown').Dropdown
const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem
const NetworkDropdownIcon = require('./components/network-dropdown-icon')
function mapStateToProps (state) {
return {
provider: state.metamask.provider,
frequentRpcList: state.metamask.frequentRpcList || [],
networkDropdownOpen: state.appState.networkDropdownOpen,
}
}
function mapDispatchToProps (dispatch) {
return {
hideModal: () => {
dispatch(actions.hideModal())
},
setProviderType: (type) => {
dispatch(actions.setProviderType(type))
},
setDefaultRpcTarget: type => {
dispatch(actions.setDefaultRpcTarget(type))
},
setRpcTarget: (target) => {
dispatch(actions.setRpcTarget(target))
},
showConfigPage: () => {
dispatch(actions.showConfigPage())
},
showNetworkDropdown: () => { dispatch(actions.showNetworkDropdown()) },
hideNetworkDropdown: () => { dispatch(actions.hideNetworkDropdown()) },
}
}
inherits(NetworkDropdown, Component)
function NetworkDropdown () {
Component.call(this)
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(NetworkDropdown)
// TODO: specify default props and proptypes
NetworkDropdown.prototype.render = function () {
const props = this.props
const { provider: { type: providerType, rpcTarget: activeNetwork } } = props
const rpcList = props.frequentRpcList
const isOpen = this.props.networkDropdownOpen
const dropdownMenuItemStyle = {
fontFamily: 'DIN OT',
fontSize: '16px',
lineHeight: '20px',
padding: '12px 0',
}
return h(Dropdown, {
useCssTransition: true,
isOpen,
onClickOutside: (event) => {
const { classList } = event.target
const isNotToggleElement = [
classList.contains('menu-icon'),
classList.contains('network-name'),
classList.contains('network-indicator'),
].filter(bool => bool).length === 0
// classes from three constituent nodes of the toggle element
if (isNotToggleElement) {
this.props.hideNetworkDropdown()
}
},
containerClassName: 'network-droppo',
zIndex: 11,
style: {
position: 'absolute',
top: '38px',
minWidth: '309px',
},
innerStyle: {
padding: '18px 8px',
},
}, [
h('div.network-dropdown-header', {}, [
h('div.network-dropdown-title', {}, 'Networks'),
h('div.network-dropdown-divider'),
h('div.network-dropdown-content',
{},
'The default network for Ether transactions is Main Net.'
),
]),
h(
DropdownMenuItem,
{
key: 'main',
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setProviderType('mainnet'),
style: { ...dropdownMenuItemStyle, borderColor: '#038789' },
},
[
providerType === 'mainnet' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, {
backgroundColor: '#038789', // $blue-lagoon
isSelected: providerType === 'mainnet',
}),
h('span.network-name-item', {
style: {
color: providerType === 'mainnet' ? '#ffffff' : '#9b9b9b',
},
}, 'Main Ethereum Network'),
]
),
h(
DropdownMenuItem,
{
key: 'ropsten',
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setProviderType('ropsten'),
style: dropdownMenuItemStyle,
},
[
providerType === 'ropsten' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, {
backgroundColor: '#e91550', // $crimson
isSelected: providerType === 'ropsten',
}),
h('span.network-name-item', {
style: {
color: providerType === 'ropsten' ? '#ffffff' : '#9b9b9b',
},
}, 'Ropsten Test Network'),
]
),
h(
DropdownMenuItem,
{
key: 'kovan',
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setProviderType('kovan'),
style: dropdownMenuItemStyle,
},
[
providerType === 'kovan' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, {
backgroundColor: '#690496', // $purple
isSelected: providerType === 'kovan',
}),
h('span.network-name-item', {
style: {
color: providerType === 'kovan' ? '#ffffff' : '#9b9b9b',
},
}, 'Kovan Test Network'),
]
),
h(
DropdownMenuItem,
{
key: 'rinkeby',
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setProviderType('rinkeby'),
style: dropdownMenuItemStyle,
},
[
providerType === 'rinkeby' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, {
backgroundColor: '#ebb33f', // $tulip-tree
isSelected: providerType === 'rinkeby',
}),
h('span.network-name-item', {
style: {
color: providerType === 'rinkeby' ? '#ffffff' : '#9b9b9b',
},
}, 'Rinkeby Test Network'),
]
),
h(
DropdownMenuItem,
{
key: 'default',
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setRpcTarget('http://localhost:8545'),
style: dropdownMenuItemStyle,
},
[
activeNetwork === 'http://localhost:8545' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, {
isSelected: activeNetwork === 'http://localhost:8545',
innerBorder: '1px solid #9b9b9b',
}),
h('span.network-name-item', {
style: {
color: activeNetwork === 'http://localhost:8545' ? '#ffffff' : '#9b9b9b',
},
}, 'Localhost 8545'),
]
),
this.renderCustomOption(props.provider),
this.renderCommonRpc(rpcList, props.provider),
h(
DropdownMenuItem,
{
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => this.props.showConfigPage(),
style: dropdownMenuItemStyle,
},
[
activeNetwork === 'custom' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, {
isSelected: activeNetwork === 'custom',
innerBorder: '1px solid #9b9b9b',
}),
h('span.network-name-item', {
style: {
color: activeNetwork === 'custom' ? '#ffffff' : '#9b9b9b',
},
}, 'Custom RPC'),
]
),
])
}
NetworkDropdown.prototype.getNetworkName = function () {
const { provider } = this.props
const providerName = provider.type
let name
if (providerName === 'mainnet') {
name = 'Main Ethereum Network'
} else if (providerName === 'ropsten') {
name = 'Ropsten Test Network'
} else if (providerName === 'kovan') {
name = 'Kovan Test Network'
} else if (providerName === 'rinkeby') {
name = 'Rinkeby Test Network'
} else {
name = 'Unknown Private Network'
}
return name
}
NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
const props = this.props
const rpcTarget = provider.rpcTarget
return rpcList.map((rpc) => {
if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) {
return null
} else {
return h(
DropdownMenuItem,
{
key: `common${rpc}`,
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setRpcTarget(rpc),
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
rpc,
rpcTarget === rpc ? h('.check', '✓') : null,
]
)
}
})
}
NetworkDropdown.prototype.renderCustomOption = function (provider) {
const { rpcTarget, type } = provider
const props = this.props
if (type !== 'rpc') return null
// Concatenate long URLs
let label = rpcTarget
if (rpcTarget.length > 31) {
label = label.substr(0, 34) + '...'
}
switch (rpcTarget) {
case 'http://localhost:8545':
return null
default:
return h(
DropdownMenuItem,
{
key: rpcTarget,
onClick: () => props.setRpcTarget(rpcTarget),
closeMenu: () => this.props.hideNetworkDropdown(),
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
label,
h('.check', '✓'),
]
)
}
}

View File

@ -0,0 +1,51 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
module.exports = connect(null, mapDispatchToProps)(TokenMenuDropdown)
function mapDispatchToProps (dispatch) {
return {
showHideTokenConfirmationModal: (token) => {
dispatch(actions.showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token }))
}
}
}
inherits(TokenMenuDropdown, Component)
function TokenMenuDropdown () {
Component.call(this)
this.onClose = this.onClose.bind(this)
}
TokenMenuDropdown.prototype.onClose = function (e) {
e.stopPropagation()
this.props.onClose()
}
TokenMenuDropdown.prototype.render = function () {
const { showHideTokenConfirmationModal } = this.props
return h('div.token-menu-dropdown', {}, [
h('div.token-menu-dropdown__close-area', {
onClick: this.onClose,
}),
h('div.token-menu-dropdown__container', {}, [
h('div.token-menu-dropdown__options', {}, [
h('div.token-menu-dropdown__option', {
onClick: (e) => {
e.stopPropagation()
showHideTokenConfirmationModal(this.props.token)
this.props.onClose()
},
}, 'Hide Token')
]),
]),
])
}

View File

@ -44,7 +44,7 @@ EnsInput.prototype.render = function () {
return h('div', {
style: { width: '100%' },
}, [
h('input.large-input', opts),
h('input.large-input.send-screen-input', opts),
// The address book functionality.
h('datalist#addresses',
[
@ -125,7 +125,7 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
EnsInput.prototype.ensIcon = function (recipient) {
const { hoverText } = this.state || {}
return h('span', {
return h('span.#ensIcon', {
title: hoverText,
style: {
position: 'absolute',

View File

@ -1,8 +1,10 @@
const Component = require('react').Component
const { Component } = require('react')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const formatBalance = require('../util').formatBalance
const generateBalanceObject = require('../util').generateBalanceObject
const { inherits } = require('util')
const {
formatBalance,
generateBalanceObject,
} = require('../util')
const Tooltip = require('./tooltip.js')
const FiatValue = require('./fiat-value.js')
@ -14,11 +16,10 @@ function EthBalanceComponent () {
}
EthBalanceComponent.prototype.render = function () {
var props = this.props
let { value } = props
const { style, width } = props
var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true
value = value ? formatBalance(value, 6, needsParse) : '...'
const props = this.props
const { value, style, width, needsParse = true } = props
const formattedValue = value ? formatBalance(value, 6, needsParse) : '...'
return (
@ -30,60 +31,66 @@ EthBalanceComponent.prototype.render = function () {
display: 'inline',
width,
},
}, this.renderBalance(value)),
}, this.renderBalance(formattedValue)),
])
)
}
EthBalanceComponent.prototype.renderBalance = function (value) {
var props = this.props
const { conversionRate, shorten, incoming, currentCurrency } = props
if (value === 'None') return value
if (value === '...') return value
var balanceObj = generateBalanceObject(value, shorten ? 1 : 3)
var balance
var splitBalance = value.split(' ')
var ethNumber = splitBalance[0]
var ethSuffix = splitBalance[1]
const showFiat = 'showFiat' in props ? props.showFiat : true
if (shorten) {
balance = balanceObj.shortBalance
} else {
balance = balanceObj.balance
const {
conversionRate,
shorten,
incoming,
currentCurrency,
hideTooltip,
styleOveride,
showFiat = true,
} = this.props
const { fontSize, color, fontFamily, lineHeight } = styleOveride
const { shortBalance, balance, label } = generateBalanceObject(value, shorten ? 1 : 3)
const balanceToRender = shorten ? shortBalance : balance
const [ethNumber, ethSuffix] = value.split(' ')
const containerProps = hideTooltip ? {} : {
position: 'bottom',
title: `${ethNumber} ${ethSuffix}`,
}
var label = balanceObj.label
return (
h(Tooltip, {
position: 'bottom',
title: `${ethNumber} ${ethSuffix}`,
}, h('div.flex-column', [
h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, [
h('div', {
h(hideTooltip ? 'div' : Tooltip,
containerProps,
h('div.flex-column', [
h('.flex-row', {
style: {
width: '100%',
textAlign: 'right',
alignItems: 'flex-end',
lineHeight: lineHeight || '13px',
fontFamily: fontFamily || 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, incoming ? `+${balance}` : balance),
h('div', {
style: {
color: ' #AEAEAE',
fontSize: '12px',
marginLeft: '5px',
},
}, label),
]),
}, [
h('div', {
style: {
width: '100%',
textAlign: 'right',
fontSize: fontSize || 'inherit',
color: color || 'inherit',
},
}, incoming ? `+${balanceToRender}` : balanceToRender),
h('div', {
style: {
color: color || '#AEAEAE',
fontSize: fontSize || '12px',
marginLeft: '5px',
},
}, label),
]),
showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null,
]))
showFiat ? h(FiatValue, { value: this.props.value, conversionRate, currentCurrency }) : null,
])
)
)
}

View File

@ -12,7 +12,7 @@ function FiatValue () {
FiatValue.prototype.render = function () {
const props = this.props
const { conversionRate, currentCurrency } = props
const { conversionRate, currentCurrency, style } = props
const renderedCurrency = currentCurrency || ''
const value = formatBalance(props.value, 6)
@ -29,16 +29,18 @@ FiatValue.prototype.render = function () {
fiatTooltipNumber = 'Unknown'
}
return fiatDisplay(fiatDisplayNumber, renderedCurrency.toUpperCase())
return fiatDisplay(fiatDisplayNumber, renderedCurrency.toUpperCase(), style)
}
function fiatDisplay (fiatDisplayNumber, fiatSuffix) {
function fiatDisplay (fiatDisplayNumber, fiatSuffix, styleOveride = {}) {
const { fontSize, color, fontFamily, lineHeight } = styleOveride
if (fiatDisplayNumber !== 'N/A') {
return h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
lineHeight: lineHeight || '13px',
fontFamily: fontFamily || 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, [
@ -46,15 +48,15 @@ function fiatDisplay (fiatDisplayNumber, fiatSuffix) {
style: {
width: '100%',
textAlign: 'right',
fontSize: '12px',
color: '#333333',
fontSize: fontSize || '12px',
color: color || '#333333',
},
}, fiatDisplayNumber),
h('div', {
style: {
color: '#AEAEAE',
color: color || '#AEAEAE',
marginLeft: '5px',
fontSize: '12px',
fontSize: fontSize || '12px',
},
}, fiatSuffix),
])

View File

@ -18,21 +18,35 @@ function IdenticonComponent () {
IdenticonComponent.prototype.render = function () {
var props = this.props
const { className = '', address } = props
var diameter = props.diameter || this.defaultDiameter
return (
h('div', {
key: 'identicon-' + this.props.address,
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: diameter,
width: diameter,
borderRadius: diameter / 2,
overflow: 'hidden',
},
})
)
return address
? (
h('div', {
className: `${className} identicon`,
key: 'identicon-' + address,
style: {
display: 'flex',
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,
},
})
)
}
IdenticonComponent.prototype.componentDidMount = function () {

View File

@ -0,0 +1,46 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const { addCurrencies } = require('../conversion-util')
module.exports = InputNumber
inherits(InputNumber, Component)
function InputNumber () {
Component.call(this)
this.setValue = this.setValue.bind(this)
}
InputNumber.prototype.setValue = function (newValue) {
const { fixed, min = -1, max = Infinity, onChange } = this.props
newValue = Number(fixed ? newValue.toFixed(4) : newValue)
if (newValue >= min && newValue <= max) {
onChange(newValue)
}
}
InputNumber.prototype.render = function () {
const { unitLabel, step = 1, placeholder, value = 0 } = this.props
return h('div.customize-gas-input-wrapper', {}, [
h('input.customize-gas-input', {
placeholder,
type: 'number',
value: value,
onChange: (e) => this.setValue(e.target.value),
}),
h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [
h('i.fa.fa-angle-up', {
onClick: () => this.setValue(addCurrencies(value, step)),
}),
h('i.fa.fa-angle-down', {
style: { cursor: 'pointer' },
onClick: () => this.setValue(addCurrencies(value, step * -1)),
}),
]),
])
}

View File

@ -13,21 +13,23 @@ function MenuDroppoComponent () {
}
MenuDroppoComponent.prototype.render = function () {
const { containerClassName = '' } = this.props
const speed = this.props.speed || '300ms'
const useCssTransition = this.props.useCssTransition
const zIndex = ('zIndex' in this.props) ? this.props.zIndex : 0
this.manageListeners()
let style = this.props.style || {}
const style = this.props.style || {}
if (!('position' in style)) {
style.position = 'fixed'
}
style.zIndex = zIndex
return (
h('.menu-droppo-container', {
h('div', {
style,
className: `.menu-droppo-container ${containerClassName}`,
}, [
h('style', `
.menu-droppo-enter {

View File

@ -0,0 +1,70 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const AccountModalContainer = require('./account-modal-container')
const { getSelectedIdentity, getSelectedAddress } = require('../../selectors')
const genAccountLink = require('../../../lib/account-link.js')
const QrView = require('../qr-code')
function mapStateToProps (state) {
return {
network: state.metamask.network,
selectedIdentity: getSelectedIdentity(state),
}
}
function mapDispatchToProps (dispatch) {
return {
// Is this supposed to be used somewhere?
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
showExportPrivateKeyModal: () => {
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
},
hideModal: () => dispatch(actions.hideModal()),
}
}
inherits(AccountDetailsModal, Component)
function AccountDetailsModal () {
Component.call(this)
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsModal)
// Not yet pixel perfect todos:
// fonts of qr-header
AccountDetailsModal.prototype.render = function () {
const {
selectedIdentity,
network,
showExportPrivateKeyModal,
hideModal,
} = this.props
const { name, address } = selectedIdentity
return h(AccountModalContainer, {}, [
h(QrView, {
Qr: {
message: name,
data: address,
},
}),
h('div.account-modal-divider'),
h('button.btn-clear', {
onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }),
}, [ 'View account on Etherscan' ]),
// Holding on redesign for Export Private Key functionality
h('button.btn-clear', {
onClick: () => {
showExportPrivateKeyModal()
},
}, [ 'Export private key' ]),
])
}

View File

@ -0,0 +1,74 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const { getSelectedIdentity } = require('../../selectors')
const Identicon = require('../identicon')
function mapStateToProps (state) {
return {
selectedIdentity: getSelectedIdentity(state),
}
}
function mapDispatchToProps (dispatch) {
return {
hideModal: () => {
dispatch(actions.hideModal())
},
}
}
inherits(AccountModalContainer, Component)
function AccountModalContainer () {
Component.call(this)
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountModalContainer)
AccountModalContainer.prototype.render = function () {
const {
selectedIdentity,
showBackButton = false,
backButtonAction,
} = this.props
let { children } = this.props
if (children.constructor !== Array) {
children = [children]
}
return h('div', { style: { borderRadius: '4px' }}, [
h('div.account-modal-container', [
h('div', [
// Needs a border; requires changes to svg
h(Identicon, {
address: selectedIdentity.address,
diameter: 64,
style: {},
}),
]),
showBackButton && h('div.account-modal-back', {
onClick: backButtonAction,
}, [
h('i.fa.fa-angle-left.fa-lg'),
h('span.account-modal-back__text', ' Back'),
]),
h('div.account-modal-close', {
onClick: this.props.hideModal,
}),
...children,
]),
])
}

View File

@ -0,0 +1,87 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
function mapStateToProps (state) {
return {
network: state.metamask.network,
address: state.metamask.selectedAddress,
}
}
function mapDispatchToProps (dispatch) {
return {
toCoinbase: (address) => {
dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
},
hideModal: () => {
dispatch(actions.hideModal())
},
showAccountDetailModal: () => {
dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
},
}
}
inherits(BuyOptions, Component)
function BuyOptions () {
Component.call(this)
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(BuyOptions)
BuyOptions.prototype.render = function () {
return h('div', {}, [
h('div.buy-modal-content.transfers-subview', {
}, [
h('div.buy-modal-content-title-wrapper.flex-column.flex-center', {
style: {},
}, [
h('div.buy-modal-content-title', {
style: {},
}, 'Transfers'),
h('div', {}, 'How would you like to buy Ether?'),
]),
h('div.buy-modal-content-options.flex-column.flex-center', {}, [
h('div.buy-modal-content-option', {
onClick: () => {
const { toCoinbase, address } = this.props
toCoinbase(address)
},
}, [
h('div.buy-modal-content-option-title', {}, 'Coinbase'),
h('div.buy-modal-content-option-subtitle', {}, 'Buy with Fiat'),
]),
// h('div.buy-modal-content-option', {}, [
// h('div.buy-modal-content-option-title', {}, 'Shapeshift'),
// h('div.buy-modal-content-option-subtitle', {}, 'Trade any digital asset for any other'),
// ]),
h('div.buy-modal-content-option', {
onClick: () => this.goToAccountDetailsModal(),
}, [
h('div.buy-modal-content-option-title', {}, 'Direct Deposit'),
h('div.buy-modal-content-option-subtitle', {}, 'Deposit from another account'),
]),
]),
h('button', {
style: {
background: 'white',
},
onClick: () => { this.props.hideModal() },
}, h('div.buy-modal-content-footer#buy-modal-content-footer-text', {}, 'Cancel')),
]),
])
}
BuyOptions.prototype.goToAccountDetailsModal = function () {
this.props.hideModal()
this.props.showAccountDetailModal()
}

View File

@ -0,0 +1,77 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const { getSelectedAccount } = require('../../selectors')
function mapStateToProps (state) {
return {
selectedAccount: getSelectedAccount(state),
identity: state.appState.modal.modalState.identity,
}
}
function mapDispatchToProps (dispatch) {
return {
hideModal: () => {
dispatch(actions.hideModal())
},
saveAccountLabel: (account, label) => {
dispatch(actions.saveAccountLabel(account, label))
},
}
}
inherits(EditAccountNameModal, Component)
function EditAccountNameModal (props) {
Component.call(this)
this.state = {
inputText: props.identity.name,
}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(EditAccountNameModal)
EditAccountNameModal.prototype.render = function () {
const { hideModal, saveAccountLabel, identity } = this.props
return h('div', {}, [
h('div.flex-column.edit-account-name-modal-content', {
}, [
h('div.edit-account-name-modal-cancel', {
onClick: () => {
hideModal()
},
}, [
h('i.fa.fa-times'),
]),
h('div.edit-account-name-modal-title', {
}, ['Edit Account Name']),
h('input.edit-account-name-modal-input', {
placeholder: identity.name,
onChange: (event) => {
this.setState({ inputText: event.target.value })
},
value: this.state.inputText,
}, []),
h('button.btn-clear.edit-account-name-modal-save-button', {
onClick: () => {
if (this.state.inputText.length !== 0) {
saveAccountLabel(identity.address, this.state.inputText)
hideModal()
}
},
disabled: this.state.inputText.length === 0,
}, [
'SAVE',
]),
]),
])
}

View File

@ -0,0 +1,139 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const ethUtil = require('ethereumjs-util')
const actions = require('../../actions')
const AccountModalContainer = require('./account-modal-container')
const { getSelectedIdentity } = require('../../selectors')
const ReadOnlyInput = require('../readonly-input')
const copyToClipboard = require('copy-to-clipboard')
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 mapDispatchToProps (dispatch) {
return {
exportAccount: (password, address) => dispatch(actions.exportAccount(password, address)),
showAccountDetailModal: () => dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })),
hideModal: () => dispatch(actions.hideModal()),
}
}
inherits(ExportPrivateKeyModal, Component)
function ExportPrivateKeyModal () {
Component.call(this)
this.state = {
password: '',
privateKey: null,
}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(ExportPrivateKeyModal)
ExportPrivateKeyModal.prototype.exportAccountAndGetPrivateKey = function (password, address) {
const { exportAccount } = this.props
exportAccount(password, address)
.then(privateKey => this.setState({ privateKey }))
}
ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) {
return h('span.private-key-password-label', privateKey
? 'This is your private key (click to copy)'
: 'Type Your Password'
)
}
ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) {
const plainKey = privateKey && ethUtil.stripHexPrefix(privateKey)
return privateKey
? h(ReadOnlyInput, {
wrapperClass: 'private-key-password-display-wrapper',
inputClass: 'private-key-password-display-textarea',
textarea: true,
value: plainKey,
onClick: () => copyToClipboard(plainKey),
})
: h('input.private-key-password-input', {
type: 'password',
placeholder: 'Type password',
onChange: event => this.setState({ password: event.target.value }),
})
}
ExportPrivateKeyModal.prototype.renderButton = function (className, onClick, label) {
return h('button', {
className,
onClick,
}, label)
}
ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) {
return h('div.export-private-key-buttons', {}, [
!privateKey && this.renderButton('btn-clear btn-cancel', () => hideModal(), 'Cancel'),
(privateKey
? this.renderButton('btn-clear', () => hideModal(), 'Done')
: this.renderButton('btn-clear', () => this.exportAccountAndGetPrivateKey(this.state.password, address), 'Download')
),
])
}
ExportPrivateKeyModal.prototype.render = function () {
const {
selectedIdentity,
network,
warning,
showAccountDetailModal,
hideModal,
previousModalState,
} = this.props
const { name, address } = selectedIdentity
const { privateKey } = this.state
return h(AccountModalContainer, {
showBackButton: previousModalState === 'ACCOUNT_DETAILS',
backButtonAction: () => showAccountDetailModal(),
}, [
h('span.account-name', name),
h(ReadOnlyInput, {
wrapperClass: 'ellip-address-wrapper',
inputClass: 'qr-ellip-address ellip-address',
value: address,
}),
h('div.account-modal-divider'),
h('span.modal-body-title', 'Download Private Keys'),
h('div.private-key-password', {}, [
this.renderPasswordLabel(privateKey),
this.renderPasswordInput(privateKey),
!warning ? null : h('span.private-key-password-error', warning),
]),
h('div.private-key-password-warning', `Warning: Never disclose this key.
Anyone with your private keys can take steal any assets held in your
account.`
),
this.renderButtons(privateKey, this.state.password, address, hideModal),
])
}

View File

@ -0,0 +1,74 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const Identicon = require('../identicon')
function mapStateToProps (state) {
return {
network: state.metamask.network,
token: state.appState.modal.modalState.token,
}
}
function mapDispatchToProps (dispatch) {
return {
hideModal: () => dispatch(actions.hideModal()),
hideToken: address => {
dispatch(actions.removeToken(address))
.then(() => {
dispatch(actions.hideModal())
})
},
}
}
inherits(HideTokenConfirmationModal, Component)
function HideTokenConfirmationModal () {
Component.call(this)
this.state = {}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(HideTokenConfirmationModal)
HideTokenConfirmationModal.prototype.render = function () {
const { token, network, hideToken, hideModal } = this.props
const { symbol, address } = token
return h('div.hide-token-confirmation', {}, [
h('div.hide-token-confirmation__container', {
}, [
h('div.hide-token-confirmation__title', {}, [
'Hide Token?',
]),
h(Identicon, {
className: 'hide-token-confirmation__identicon',
diameter: 45,
address,
network,
}),
h('div.hide-token-confirmation__symbol', {}, symbol),
h('div.hide-token-confirmation__copy', {}, [
'You can add this token back in the future by going go to “Add token” in your accounts options menu.',
]),
h('div.hide-token-confirmation__buttons', {}, [
h('button.btn-clear', {
onClick: () => hideModal(),
}, [
'CANCEL',
]),
h('button.btn-clear', {
onClick: () => hideToken(address),
}, [
'HIDE',
]),
]),
]),
])
}

View File

@ -0,0 +1,5 @@
const Modal = require('./modal')
module.exports = {
Modal,
}

View File

@ -0,0 +1,263 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const FadeModal = require('boron').FadeModal
const actions = require('../../actions')
const isMobileView = require('../../../lib/is-mobile-view')
const isPopupOrNotification = require('../../../../app/scripts/lib/is-popup-or-notification')
// Modal Components
const BuyOptions = require('./buy-options-modal')
const AccountDetailsModal = require('./account-details-modal')
const EditAccountNameModal = require('./edit-account-name-modal')
const ExportPrivateKeyModal = require('./export-private-key-modal')
const NewAccountModal = require('./new-account-modal')
const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js')
const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
const CustomizeGasModal = require('../customize-gas-modal')
const accountModalStyle = {
mobileModalStyle: {
width: '95%',
// top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh',
boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
borderRadius: '4px',
top: '10%',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
laptopModalStyle: {
width: '360px',
// top: 'calc(33% + 45px)',
boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
borderRadius: '4px',
top: '10%',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
contentStyle: {
borderRadius: '4px',
},
}
const MODALS = {
BUY: {
contents: [
h(BuyOptions, {}, []),
],
mobileModalStyle: {
width: '95%',
// top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)',
top: '10%',
},
laptopModalStyle: {
width: '66%',
maxWidth: '550px',
top: 'calc(10% + 10px)',
left: '0',
right: '0',
margin: '0 auto',
boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)',
transform: 'none',
},
},
EDIT_ACCOUNT_NAME: {
contents: [
h(EditAccountNameModal, {}, []),
],
mobileModalStyle: {
width: '95%',
// top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh',
top: '10%',
boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
laptopModalStyle: {
width: '375px',
// top: 'calc(30% + 10px)',
top: '10%',
boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
},
ACCOUNT_DETAILS: {
contents: [
h(AccountDetailsModal, {}, []),
],
...accountModalStyle,
},
EXPORT_PRIVATE_KEY: {
contents: [
h(ExportPrivateKeyModal, {}, []),
],
...accountModalStyle,
},
SHAPESHIFT_DEPOSIT_TX: {
contents: [
h(ShapeshiftDepositTxModal),
],
...accountModalStyle,
},
HIDE_TOKEN_CONFIRMATION: {
contents: [
h(HideTokenConfirmationModal, {}, []),
],
mobileModalStyle: {
width: '95%',
top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh',
},
laptopModalStyle: {
width: '449px',
top: 'calc(33% + 45px)',
},
},
NEW_ACCOUNT: {
contents: [
h(NewAccountModal, {}, []),
],
mobileModalStyle: {
width: '95%',
// top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh',
top: '10%',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
laptopModalStyle: {
width: '449px',
// top: 'calc(33% + 45px)',
top: '10%',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
},
CUSTOMIZE_GAS: {
contents: [
h(CustomizeGasModal, {}, []),
],
mobileModalStyle: {
width: '355px',
height: '598px',
// top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh',
top: '5%',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
laptopModalStyle: {
width: '720px',
height: '377px',
top: '80px',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
},
DEFAULT: {
contents: [],
mobileModalStyle: {},
laptopModalStyle: {},
},
}
const BACKDROPSTYLE = {
backgroundColor: 'rgba(245, 245, 245, 0.85)',
}
function mapStateToProps (state) {
return {
active: state.appState.modal.open,
modalState: state.appState.modal.modalState,
}
}
function mapDispatchToProps (dispatch) {
return {
hideModal: () => {
dispatch(actions.hideModal())
},
}
}
// Global Modal Component
inherits(Modal, Component)
function Modal () {
Component.call(this)
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(Modal)
Modal.prototype.render = function () {
const modal = MODALS[this.props.modalState.name || 'DEFAULT']
const children = modal.contents
const modalStyle = modal[isMobileView() ? 'mobileModalStyle' : 'laptopModalStyle']
const contentStyle = modal.contentStyle || {};
return h(FadeModal,
{
className: 'modal',
keyboard: false,
onHide: () => { this.onHide() },
ref: (ref) => {
this.modalRef = ref
},
modalStyle,
contentStyle,
backdropStyle: BACKDROPSTYLE,
},
children,
)
}
Modal.prototype.componentWillReceiveProps = function (nextProps) {
if (nextProps.active) {
this.show()
} else if (this.props.active) {
this.hide()
}
}
Modal.prototype.onHide = function () {
if (this.props.onHideCallback) {
this.props.onHideCallback()
}
this.props.hideModal()
}
Modal.prototype.hide = function () {
this.modalRef.hide()
}
Modal.prototype.show = function () {
this.modalRef.show()
}

View File

@ -0,0 +1,87 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
function mapStateToProps (state) {
return {
network: state.metamask.network,
address: state.metamask.selectedAddress,
}
}
function mapDispatchToProps (dispatch) {
return {
toCoinbase: (address) => {
dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
},
hideModal: () => {
dispatch(actions.hideModal())
},
createAccount: (newAccountName) => {
dispatch(actions.addNewAccount())
.then((newAccountAddress) => {
if (newAccountName) {
dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName))
}
dispatch(actions.hideModal())
})
},
}
}
inherits(NewAccountModal, Component)
function NewAccountModal () {
Component.call(this)
this.state = {
newAccountName: ''
}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountModal)
NewAccountModal.prototype.render = function () {
const { newAccountName } = this.state
return h('div', {}, [
h('div.new-account-modal-wrapper', {
}, [
h('div.new-account-modal-header', {}, [
'New Account',
]),
h('div.modal-close-x', {
onClick: this.props.hideModal,
}),
h('div.new-account-modal-content', {}, [
'Account Name',
]),
h('div.new-account-input-wrapper', {}, [
h('input.new-account-input', {
placeholder: 'E.g. My new account',
onChange: (event) => this.setState({ newAccountName: event.target.value })
}, []),
]),
h('div.new-account-modal-content.after-input', {}, [
'or',
]),
h('div.new-account-modal-content.after-input', {}, [
'Import an account',
]),
h('div.new-account-modal-content.button', {}, [
h('button.btn-clear', {
onClick: () => this.props.createAccount(newAccountName)
}, [
'SAVE',
]),
]),
]),
])
}

View File

@ -0,0 +1,40 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const QrView = require('../qr-code')
const AccountModalContainer = require('./account-modal-container')
function mapStateToProps (state) {
return {
Qr: state.appState.modal.modalState.Qr,
}
}
function mapDispatchToProps (dispatch) {
return {
hideModal: () => {
dispatch(actions.hideModal())
},
}
}
inherits(ShapeshiftDepositTxModal, Component)
function ShapeshiftDepositTxModal () {
Component.call(this)
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(ShapeshiftDepositTxModal)
ShapeshiftDepositTxModal.prototype.render = function () {
const { Qr } = this.props
return h(AccountModalContainer, {
}, [
h('div', {}, [
h(QrView, {key: 'qr', Qr}),
])
])
}

View File

@ -1,6 +1,8 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const classnames = require('classnames');
const inherits = require('util').inherits
const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon')
module.exports = Network
@ -60,7 +62,13 @@ Network.prototype.render = function () {
}
return (
h('#network_component.pointer', {
h('div.network-component.pointer', {
className: classnames('network-component pointer', {
'ethereum-network': providerName === 'mainnet',
'ropsten-test-network': providerName === 'ropsten' || parseInt(networkNumber) === 3,
'kovan-test-network': providerName === 'kovan',
'rinkeby-test-network': providerName === 'rinkeby',
}),
title: hoverText,
onClick: (event) => this.props.onClick(event),
}, [
@ -68,7 +76,10 @@ Network.prototype.render = function () {
switch (iconName) {
case 'ethereum-network':
return h('.network-indicator', [
h('.menu-icon.diamond'),
h(NetworkDropdownIcon, {
backgroundColor: '#038789', // $blue-lagoon
nonSelectBackgroundColor: '#15afb2',
}),
h('.network-name', {
style: {
color: '#039396',
@ -78,7 +89,10 @@ Network.prototype.render = function () {
])
case 'ropsten-test-network':
return h('.network-indicator', [
h('.menu-icon.red-dot'),
h(NetworkDropdownIcon, {
backgroundColor: '#e91550', // $crimson
nonSelectBackgroundColor: '#ec2c50',
}),
h('.network-name', {
style: {
color: '#ff6666',
@ -88,7 +102,10 @@ Network.prototype.render = function () {
])
case 'kovan-test-network':
return h('.network-indicator', [
h('.menu-icon.hollow-diamond'),
h(NetworkDropdownIcon, {
backgroundColor: '#690496', // $purple
nonSelectBackgroundColor: '#b039f3',
}),
h('.network-name', {
style: {
color: '#690496',
@ -98,7 +115,10 @@ Network.prototype.render = function () {
])
case 'rinkeby-test-network':
return h('.network-indicator', [
h('.menu-icon.golden-square'),
h(NetworkDropdownIcon, {
backgroundColor: '#ebb33f', // $tulip-tree
nonSelectBackgroundColor: '#ecb23e',
}),
h('.network-name', {
style: {
color: '#e7a218',

View File

@ -102,7 +102,7 @@ Notice.prototype.render = function () {
}),
]),
h('button', {
h('button.primary', {
disabled,
onClick: () => {
this.setState({disclaimerDisabled: true})

Some files were not shown because too many files have changed in this diff Show More