aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.eslintignore3
-rw-r--r--.gitignore5
-rw-r--r--.stylelintignore10
-rw-r--r--.stylelintrc50
-rw-r--r--app/fonts/DIN Next/DIN Next W01 Bold.otfbin0 -> 106032 bytes
-rw-r--r--app/fonts/DIN Next/DIN Next W01 Regular.otfbin0 -> 106580 bytes
-rw-r--r--app/fonts/DIN Next/DIN Next W10 Black.otfbin0 -> 105972 bytes
-rw-r--r--app/fonts/DIN Next/DIN Next W10 Italic.otfbin0 -> 115984 bytes
-rw-r--r--app/fonts/DIN Next/DIN Next W10 Light.otfbin0 -> 108672 bytes
-rw-r--r--app/fonts/DIN Next/DIN Next W10 Medium.otfbin0 -> 105684 bytes
-rw-r--r--app/fonts/DIN_OT/DINOT-2.otfbin0 -> 44144 bytes
-rw-r--r--app/fonts/DIN_OT/DINOT-Bold 2.otfbin0 -> 45564 bytes
-rw-r--r--app/fonts/DIN_OT/DINOT-BoldItalic.otfbin0 -> 49684 bytes
-rw-r--r--app/fonts/DIN_OT/DINOT-Italic 2.otfbin0 -> 47956 bytes
-rw-r--r--app/fonts/DIN_OT/DINOT-Medium 2.otfbin0 -> 44652 bytes
-rw-r--r--app/fonts/DIN_OT/DINOT-MediumItalic 2.otfbin0 -> 47732 bytes
-rwxr-xr-xapp/fonts/Lato/Lato-Black.ttfbin0 -> 114588 bytes
-rwxr-xr-xapp/fonts/Lato/Lato-BlackItalic.ttfbin0 -> 111616 bytes
-rwxr-xr-xapp/fonts/Lato/Lato-Bold.ttfbin0 -> 121788 bytes
-rwxr-xr-xapp/fonts/Lato/Lato-BoldItalic.ttfbin0 -> 120312 bytes
-rwxr-xr-xapp/fonts/Lato/Lato-Hairline.ttfbin0 -> 115316 bytes
-rwxr-xr-xapp/fonts/Lato/Lato-HairlineItalic.ttfbin0 -> 91460 bytes
-rwxr-xr-xapp/fonts/Lato/Lato-Italic.ttfbin0 -> 118352 bytes
-rwxr-xr-xapp/fonts/Lato/Lato-Light.ttfbin0 -> 122524 bytes
-rwxr-xr-xapp/fonts/Lato/Lato-LightItalic.ttfbin0 -> 91600 bytes
-rwxr-xr-xapp/fonts/Lato/Lato-Regular.ttfbin0 -> 120196 bytes
-rwxr-xr-xapp/fonts/Lato/OFL.txt93
-rw-r--r--app/fonts/Roboto/Roboto-Black.ttfbin0 -> 142472 bytes
-rw-r--r--app/fonts/Roboto/Roboto-BlackItalic.ttfbin0 -> 149644 bytes
-rw-r--r--app/fonts/Roboto/Roboto-Bold.ttfbin0 -> 135820 bytes
-rw-r--r--app/fonts/Roboto/Roboto-BoldItalic.ttfbin0 -> 144700 bytes
-rw-r--r--app/fonts/Roboto/Roboto-Italic.ttfbin0 -> 148540 bytes
-rw-r--r--app/fonts/Roboto/Roboto-Light.ttfbin0 -> 140276 bytes
-rw-r--r--app/fonts/Roboto/Roboto-LightItalic.ttfbin0 -> 145932 bytes
-rw-r--r--app/fonts/Roboto/Roboto-Medium.ttfbin0 -> 137308 bytes
-rw-r--r--app/fonts/Roboto/Roboto-MediumItalic.ttfbin0 -> 147876 bytes
-rw-r--r--app/fonts/Roboto/Roboto-Regular.ttfbin0 -> 145348 bytes
-rw-r--r--app/fonts/Roboto/Roboto-Thin.ttfbin0 -> 130044 bytes
-rw-r--r--app/fonts/Roboto/Roboto-ThinItalic.ttfbin0 -> 132376 bytes
-rw-r--r--app/fonts/Roboto/RobotoCondensed-Bold.ttfbin0 -> 141796 bytes
-rw-r--r--app/fonts/Roboto/RobotoCondensed-BoldItalic.ttfbin0 -> 145256 bytes
-rw-r--r--app/fonts/Roboto/RobotoCondensed-Italic.ttfbin0 -> 144404 bytes
-rw-r--r--app/fonts/Roboto/RobotoCondensed-Light.ttfbin0 -> 141384 bytes
-rw-r--r--app/fonts/Roboto/RobotoCondensed-LightItalic.ttfbin0 -> 145104 bytes
-rw-r--r--app/fonts/Roboto/RobotoCondensed-Regular.ttfbin0 -> 140396 bytes
-rw-r--r--app/images/.DS_Storebin6148 -> 0 bytes
-rw-r--r--app/images/check-white.svg14
-rw-r--r--app/images/eth_logo.svg11
-rw-r--r--app/images/import-account.svg18
-rw-r--r--app/images/info-logo.pngbin0 -> 32567 bytes
-rw-r--r--app/images/mm-bolt.svg11
-rw-r--r--app/images/mm-info-icon.svg11
-rw-r--r--app/images/plus-btn-white.svg17
-rw-r--r--app/images/settings.svg46
-rw-r--r--app/manifest.json2
-rw-r--r--app/notification.html4
-rw-r--r--app/popup.html4
-rw-r--r--app/scripts/controllers/preferences.js20
-rw-r--r--app/scripts/lib/environment-type.js10
-rw-r--r--app/scripts/lib/is-popup-or-notification.js5
-rw-r--r--app/scripts/lib/notification-manager.js2
-rw-r--r--app/scripts/metamask-controller.js11
-rw-r--r--app/scripts/popup-core.js2
-rw-r--r--gulpfile.js43
-rw-r--r--mascara/src/app/buy-ether-widget/index.js197
-rw-r--r--package.json28
-rw-r--r--test/helper.js4
-rw-r--r--test/integration/lib/first-time.js35
-rw-r--r--test/integration/lib/mascara-first-time.js24
-rw-r--r--test/lib/shallow-with-store.js16
-rw-r--r--test/unit/components/balance-component-test.js45
-rw-r--r--test/unit/components/pending-tx-test.js92
-rw-r--r--test/unit/pending-tx-test.js1
-rw-r--r--test/unit/responsive/components/dropdown-test.js116
-rw-r--r--ui/app/account-and-transaction-details.js38
-rw-r--r--ui/app/account-detail.js184
-rw-r--r--ui/app/accounts/import/index.js7
-rw-r--r--ui/app/accounts/import/json.js2
-rw-r--r--ui/app/accounts/import/private-key.js4
-rw-r--r--ui/app/actions.js567
-rw-r--r--ui/app/add-token.js499
-rw-r--r--ui/app/app.js558
-rw-r--r--ui/app/components/account-menu/index.js160
-rw-r--r--ui/app/components/balance-component.js120
-rw-r--r--ui/app/components/buy-button-subview.js6
-rw-r--r--ui/app/components/currency-input.js95
-rw-r--r--ui/app/components/customize-gas-modal/gas-modal-card.js54
-rw-r--r--ui/app/components/customize-gas-modal/gas-slider.js50
-rw-r--r--ui/app/components/customize-gas-modal/index.js278
-rw-r--r--ui/app/components/dropdowns/account-dropdown-mini.js75
-rw-r--r--ui/app/components/dropdowns/account-options-dropdown.js29
-rw-r--r--ui/app/components/dropdowns/account-selection-dropdown.js29
-rw-r--r--ui/app/components/dropdowns/components/account-dropdowns.js484
-rw-r--r--ui/app/components/dropdowns/components/dropdown.js (renamed from ui/app/components/dropdown.js)27
-rw-r--r--ui/app/components/dropdowns/components/menu.js51
-rw-r--r--ui/app/components/dropdowns/components/network-dropdown-icon.js28
-rw-r--r--ui/app/components/dropdowns/index.js17
-rw-r--r--ui/app/components/dropdowns/network-dropdown.js317
-rw-r--r--ui/app/components/dropdowns/simple-dropdown.js92
-rw-r--r--ui/app/components/dropdowns/token-menu-dropdown.js51
-rw-r--r--ui/app/components/editable-label.js115
-rw-r--r--ui/app/components/ens-input.js4
-rw-r--r--ui/app/components/eth-balance.js107
-rw-r--r--ui/app/components/fiat-value.js20
-rw-r--r--ui/app/components/identicon.js109
-rw-r--r--ui/app/components/input-number.js73
-rw-r--r--ui/app/components/loading.js65
-rw-r--r--ui/app/components/menu-droppo.js4
-rw-r--r--ui/app/components/modals/account-details-modal.js75
-rw-r--r--ui/app/components/modals/account-modal-container.js74
-rw-r--r--ui/app/components/modals/buy-options-modal.js95
-rw-r--r--ui/app/components/modals/edit-account-name-modal.js77
-rw-r--r--ui/app/components/modals/export-private-key-modal.js137
-rw-r--r--ui/app/components/modals/hide-token-confirmation-modal.js74
-rw-r--r--ui/app/components/modals/index.js5
-rw-r--r--ui/app/components/modals/modal.js262
-rw-r--r--ui/app/components/modals/new-account-modal.js106
-rw-r--r--ui/app/components/modals/shapeshift-deposit-tx-modal.js40
-rw-r--r--ui/app/components/network.js37
-rw-r--r--ui/app/components/notice.js2
-rw-r--r--ui/app/components/pending-personal-msg.js47
-rw-r--r--ui/app/components/pending-tx.js500
-rw-r--r--ui/app/components/pending-tx/confirm-deploy-contract.js348
-rw-r--r--ui/app/components/pending-tx/confirm-send-ether.js489
-rw-r--r--ui/app/components/pending-tx/confirm-send-token.js495
-rw-r--r--ui/app/components/pending-tx/index.js145
-rw-r--r--ui/app/components/qr-code.js45
-rw-r--r--ui/app/components/readonly-input.js33
-rw-r--r--ui/app/components/send-token/index.js439
-rw-r--r--ui/app/components/send/account-list-item.js73
-rw-r--r--ui/app/components/send/currency-display.js116
-rw-r--r--ui/app/components/send/currency-toggle.js44
-rw-r--r--ui/app/components/send/eth-fee-display.js37
-rw-r--r--ui/app/components/send/from-dropdown.js72
-rw-r--r--ui/app/components/send/gas-fee-display-v2.js43
-rw-r--r--ui/app/components/send/gas-fee-display.js62
-rw-r--r--ui/app/components/send/gas-tooltip.js100
-rw-r--r--ui/app/components/send/memo-textarea.js33
-rw-r--r--ui/app/components/send/send-constants.js30
-rw-r--r--ui/app/components/send/send-utils.js68
-rw-r--r--ui/app/components/send/send-v2-container.js82
-rw-r--r--ui/app/components/send/to-autocomplete.js114
-rw-r--r--ui/app/components/send/usd-fee-display.js35
-rw-r--r--ui/app/components/shift-list-item.js2
-rw-r--r--ui/app/components/signature-request.js253
-rw-r--r--ui/app/components/tab-bar.js70
-rw-r--r--ui/app/components/token-balance.js113
-rw-r--r--ui/app/components/token-cell.js114
-rw-r--r--ui/app/components/token-list.js158
-rw-r--r--ui/app/components/transaction-list-item.js54
-rw-r--r--ui/app/components/tx-list-item.js247
-rw-r--r--ui/app/components/tx-list.js137
-rw-r--r--ui/app/components/tx-view.js149
-rw-r--r--ui/app/components/wallet-content-display.js56
-rw-r--r--ui/app/components/wallet-view.js171
-rw-r--r--ui/app/conf-tx.js189
-rw-r--r--ui/app/config.js220
-rw-r--r--ui/app/conversion-util.js221
-rw-r--r--ui/app/css/debug.css21
-rw-r--r--ui/app/css/fonts.css36
-rw-r--r--ui/app/css/index.scss13
-rw-r--r--ui/app/css/itcss/base/index.scss1
-rw-r--r--ui/app/css/itcss/components/account-dropdown-mini.scss48
-rw-r--r--ui/app/css/itcss/components/account-dropdown.scss83
-rw-r--r--ui/app/css/itcss/components/account-menu.scss132
-rw-r--r--ui/app/css/itcss/components/add-token.scss341
-rw-r--r--ui/app/css/itcss/components/buttons.scss108
-rw-r--r--ui/app/css/itcss/components/confirm.scss330
-rw-r--r--ui/app/css/itcss/components/currency-display.scss56
-rw-r--r--ui/app/css/itcss/components/editable-label.scss35
-rw-r--r--ui/app/css/itcss/components/footer.scss4
-rw-r--r--ui/app/css/itcss/components/gas-slider.scss51
-rw-r--r--ui/app/css/itcss/components/header.scss100
-rw-r--r--ui/app/css/itcss/components/hero-balance.scss114
-rw-r--r--ui/app/css/itcss/components/index.scss53
-rw-r--r--ui/app/css/itcss/components/loading-overlay.scss21
-rw-r--r--ui/app/css/itcss/components/menu.scss59
-rw-r--r--ui/app/css/itcss/components/modal.scss565
-rw-r--r--ui/app/css/itcss/components/network.scss169
-rw-r--r--ui/app/css/itcss/components/newui-sections.scss277
-rw-r--r--ui/app/css/itcss/components/request-signature.scss230
-rw-r--r--ui/app/css/itcss/components/sections.scss (renamed from ui/app/css/index.css)403
-rw-r--r--ui/app/css/itcss/components/send.scss892
-rw-r--r--ui/app/css/itcss/components/settings.scss201
-rw-r--r--ui/app/css/itcss/components/simple-dropdown.scss65
-rw-r--r--ui/app/css/itcss/components/tab-bar.scss23
-rw-r--r--ui/app/css/itcss/components/token-list.scss101
-rw-r--r--ui/app/css/itcss/components/transaction-list.scss270
-rw-r--r--ui/app/css/itcss/components/wallet-balance.scss71
-rw-r--r--ui/app/css/itcss/generic/index.scss71
-rw-r--r--ui/app/css/itcss/generic/reset.scss147
-rw-r--r--ui/app/css/itcss/objects/index.scss1
-rw-r--r--ui/app/css/itcss/settings/index.scss3
-rw-r--r--ui/app/css/itcss/settings/typography.scss71
-rw-r--r--ui/app/css/itcss/settings/variables.scss77
-rw-r--r--ui/app/css/itcss/tools/index.scss1
-rw-r--r--ui/app/css/itcss/tools/utilities.scss (renamed from ui/app/css/lib.css)103
-rw-r--r--ui/app/css/itcss/trumps/index.scss (renamed from ui/app/css/transitions.css)40
-rw-r--r--ui/app/css/reset.css48
-rw-r--r--ui/app/main-container.js67
-rw-r--r--ui/app/reducers/app.js133
-rw-r--r--ui/app/reducers/metamask.js175
-rw-r--r--ui/app/selectors.js160
-rw-r--r--ui/app/send-v2.js603
-rw-r--r--ui/app/send.js840
-rw-r--r--ui/app/settings.js402
-rw-r--r--ui/app/token-tracker.js0
-rw-r--r--ui/app/token-util.js45
-rw-r--r--ui/app/unlock.js2
-rw-r--r--ui/app/util.js40
-rw-r--r--ui/css.js6
-rw-r--r--ui/index.js7
-rw-r--r--ui/lib/account-link.js26
-rw-r--r--ui/lib/blockies.js364
-rw-r--r--ui/lib/feature-toggle-utils.js11
-rw-r--r--ui/lib/icon-factory.js2
-rw-r--r--ui/lib/is-mobile-view.js5
-rw-r--r--yarn.lock1318
218 files changed, 18334 insertions, 3164 deletions
diff --git a/.eslintignore b/.eslintignore
index b96f79011..e4cade21c 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,4 +2,5 @@ app/scripts/lib/extension-instance.js
test/integration/bundle.js
test/integration/jquery-3.1.0.min.js
test/integration/helpers.js
-test/integration/lib/first-time.js \ No newline at end of file
+test/integration/lib/first-time.js
+ui/lib/blockies.js \ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 1806b1932..92b3f2875 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@ app/bower_components
test/bower_components
package
+.idea
+
temp
.tmp
.sass-cache
@@ -24,6 +26,9 @@ test/background.js
test/bundle.js
test/test-bundle.js
+#ignore css output and sourcemaps
+ui/app/css/output/
+
notes.txt
.coveralls.yml
diff --git a/.stylelintignore b/.stylelintignore
new file mode 100644
index 000000000..854829a54
--- /dev/null
+++ b/.stylelintignore
@@ -0,0 +1,10 @@
+app/
+development/
+dist/
+docs/
+fonts/
+images/
+mascara/
+node_modules/
+notices/
+test/
diff --git a/.stylelintrc b/.stylelintrc
new file mode 100644
index 000000000..d080d68d9
--- /dev/null
+++ b/.stylelintrc
@@ -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
+ }
+}
diff --git a/app/fonts/DIN Next/DIN Next W01 Bold.otf b/app/fonts/DIN Next/DIN Next W01 Bold.otf
new file mode 100644
index 000000000..2b78d1ff4
--- /dev/null
+++ b/app/fonts/DIN Next/DIN Next W01 Bold.otf
Binary files differ
diff --git a/app/fonts/DIN Next/DIN Next W01 Regular.otf b/app/fonts/DIN Next/DIN Next W01 Regular.otf
new file mode 100644
index 000000000..09f6ee297
--- /dev/null
+++ b/app/fonts/DIN Next/DIN Next W01 Regular.otf
Binary files differ
diff --git a/app/fonts/DIN Next/DIN Next W10 Black.otf b/app/fonts/DIN Next/DIN Next W10 Black.otf
new file mode 100644
index 000000000..08eb73373
--- /dev/null
+++ b/app/fonts/DIN Next/DIN Next W10 Black.otf
Binary files differ
diff --git a/app/fonts/DIN Next/DIN Next W10 Italic.otf b/app/fonts/DIN Next/DIN Next W10 Italic.otf
new file mode 100644
index 000000000..73f2b9e8c
--- /dev/null
+++ b/app/fonts/DIN Next/DIN Next W10 Italic.otf
Binary files differ
diff --git a/app/fonts/DIN Next/DIN Next W10 Light.otf b/app/fonts/DIN Next/DIN Next W10 Light.otf
new file mode 100644
index 000000000..700450e49
--- /dev/null
+++ b/app/fonts/DIN Next/DIN Next W10 Light.otf
Binary files differ
diff --git a/app/fonts/DIN Next/DIN Next W10 Medium.otf b/app/fonts/DIN Next/DIN Next W10 Medium.otf
new file mode 100644
index 000000000..b73f2e43f
--- /dev/null
+++ b/app/fonts/DIN Next/DIN Next W10 Medium.otf
Binary files differ
diff --git a/app/fonts/DIN_OT/DINOT-2.otf b/app/fonts/DIN_OT/DINOT-2.otf
new file mode 100644
index 000000000..4a5e13127
--- /dev/null
+++ b/app/fonts/DIN_OT/DINOT-2.otf
Binary files differ
diff --git a/app/fonts/DIN_OT/DINOT-Bold 2.otf b/app/fonts/DIN_OT/DINOT-Bold 2.otf
new file mode 100644
index 000000000..6ed5b6c3d
--- /dev/null
+++ b/app/fonts/DIN_OT/DINOT-Bold 2.otf
Binary files differ
diff --git a/app/fonts/DIN_OT/DINOT-BoldItalic.otf b/app/fonts/DIN_OT/DINOT-BoldItalic.otf
new file mode 100644
index 000000000..148c90588
--- /dev/null
+++ b/app/fonts/DIN_OT/DINOT-BoldItalic.otf
Binary files differ
diff --git a/app/fonts/DIN_OT/DINOT-Italic 2.otf b/app/fonts/DIN_OT/DINOT-Italic 2.otf
new file mode 100644
index 000000000..e365e77ab
--- /dev/null
+++ b/app/fonts/DIN_OT/DINOT-Italic 2.otf
Binary files differ
diff --git a/app/fonts/DIN_OT/DINOT-Medium 2.otf b/app/fonts/DIN_OT/DINOT-Medium 2.otf
new file mode 100644
index 000000000..a87a2df37
--- /dev/null
+++ b/app/fonts/DIN_OT/DINOT-Medium 2.otf
Binary files differ
diff --git a/app/fonts/DIN_OT/DINOT-MediumItalic 2.otf b/app/fonts/DIN_OT/DINOT-MediumItalic 2.otf
new file mode 100644
index 000000000..14eddfc76
--- /dev/null
+++ b/app/fonts/DIN_OT/DINOT-MediumItalic 2.otf
Binary files differ
diff --git a/app/fonts/Lato/Lato-Black.ttf b/app/fonts/Lato/Lato-Black.ttf
new file mode 100755
index 000000000..6848db0d1
--- /dev/null
+++ b/app/fonts/Lato/Lato-Black.ttf
Binary files differ
diff --git a/app/fonts/Lato/Lato-BlackItalic.ttf b/app/fonts/Lato/Lato-BlackItalic.ttf
new file mode 100755
index 000000000..5decf1297
--- /dev/null
+++ b/app/fonts/Lato/Lato-BlackItalic.ttf
Binary files differ
diff --git a/app/fonts/Lato/Lato-Bold.ttf b/app/fonts/Lato/Lato-Bold.ttf
new file mode 100755
index 000000000..74343694e
--- /dev/null
+++ b/app/fonts/Lato/Lato-Bold.ttf
Binary files differ
diff --git a/app/fonts/Lato/Lato-BoldItalic.ttf b/app/fonts/Lato/Lato-BoldItalic.ttf
new file mode 100755
index 000000000..684aacf5b
--- /dev/null
+++ b/app/fonts/Lato/Lato-BoldItalic.ttf
Binary files differ
diff --git a/app/fonts/Lato/Lato-Hairline.ttf b/app/fonts/Lato/Lato-Hairline.ttf
new file mode 100755
index 000000000..288be2955
--- /dev/null
+++ b/app/fonts/Lato/Lato-Hairline.ttf
Binary files differ
diff --git a/app/fonts/Lato/Lato-HairlineItalic.ttf b/app/fonts/Lato/Lato-HairlineItalic.ttf
new file mode 100755
index 000000000..c2bfd3353
--- /dev/null
+++ b/app/fonts/Lato/Lato-HairlineItalic.ttf
Binary files differ
diff --git a/app/fonts/Lato/Lato-Italic.ttf b/app/fonts/Lato/Lato-Italic.ttf
new file mode 100755
index 000000000..3d3b7a298
--- /dev/null
+++ b/app/fonts/Lato/Lato-Italic.ttf
Binary files differ
diff --git a/app/fonts/Lato/Lato-Light.ttf b/app/fonts/Lato/Lato-Light.ttf
new file mode 100755
index 000000000..a958067a8
--- /dev/null
+++ b/app/fonts/Lato/Lato-Light.ttf
Binary files differ
diff --git a/app/fonts/Lato/Lato-LightItalic.ttf b/app/fonts/Lato/Lato-LightItalic.ttf
new file mode 100755
index 000000000..5e45ad9a6
--- /dev/null
+++ b/app/fonts/Lato/Lato-LightItalic.ttf
Binary files differ
diff --git a/app/fonts/Lato/Lato-Regular.ttf b/app/fonts/Lato/Lato-Regular.ttf
new file mode 100755
index 000000000..04ea8efb1
--- /dev/null
+++ b/app/fonts/Lato/Lato-Regular.ttf
Binary files differ
diff --git a/app/fonts/Lato/OFL.txt b/app/fonts/Lato/OFL.txt
new file mode 100755
index 000000000..dfca0da4b
--- /dev/null
+++ b/app/fonts/Lato/OFL.txt
@@ -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.
diff --git a/app/fonts/Roboto/Roboto-Black.ttf b/app/fonts/Roboto/Roboto-Black.ttf
new file mode 100644
index 000000000..71f01ac2b
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-Black.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-BlackItalic.ttf b/app/fonts/Roboto/Roboto-BlackItalic.ttf
new file mode 100644
index 000000000..ec309c785
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-BlackItalic.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-Bold.ttf b/app/fonts/Roboto/Roboto-Bold.ttf
new file mode 100644
index 000000000..aaf374d2c
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-Bold.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-BoldItalic.ttf b/app/fonts/Roboto/Roboto-BoldItalic.ttf
new file mode 100644
index 000000000..dcd0f8007
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-BoldItalic.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-Italic.ttf b/app/fonts/Roboto/Roboto-Italic.ttf
new file mode 100644
index 000000000..f382c6874
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-Italic.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-Light.ttf b/app/fonts/Roboto/Roboto-Light.ttf
new file mode 100644
index 000000000..664e1b2f9
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-Light.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-LightItalic.ttf b/app/fonts/Roboto/Roboto-LightItalic.ttf
new file mode 100644
index 000000000..b8f529637
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-LightItalic.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-Medium.ttf b/app/fonts/Roboto/Roboto-Medium.ttf
new file mode 100644
index 000000000..aa00de0ef
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-Medium.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-MediumItalic.ttf b/app/fonts/Roboto/Roboto-MediumItalic.ttf
new file mode 100644
index 000000000..67e25f019
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-MediumItalic.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-Regular.ttf b/app/fonts/Roboto/Roboto-Regular.ttf
new file mode 100644
index 000000000..3e6e2e761
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-Regular.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-Thin.ttf b/app/fonts/Roboto/Roboto-Thin.ttf
new file mode 100644
index 000000000..d262d1446
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-Thin.ttf
Binary files differ
diff --git a/app/fonts/Roboto/Roboto-ThinItalic.ttf b/app/fonts/Roboto/Roboto-ThinItalic.ttf
new file mode 100644
index 000000000..63e9f9718
--- /dev/null
+++ b/app/fonts/Roboto/Roboto-ThinItalic.ttf
Binary files differ
diff --git a/app/fonts/Roboto/RobotoCondensed-Bold.ttf b/app/fonts/Roboto/RobotoCondensed-Bold.ttf
new file mode 100644
index 000000000..48dd63534
--- /dev/null
+++ b/app/fonts/Roboto/RobotoCondensed-Bold.ttf
Binary files differ
diff --git a/app/fonts/Roboto/RobotoCondensed-BoldItalic.ttf b/app/fonts/Roboto/RobotoCondensed-BoldItalic.ttf
new file mode 100644
index 000000000..ad728646a
--- /dev/null
+++ b/app/fonts/Roboto/RobotoCondensed-BoldItalic.ttf
Binary files differ
diff --git a/app/fonts/Roboto/RobotoCondensed-Italic.ttf b/app/fonts/Roboto/RobotoCondensed-Italic.ttf
new file mode 100644
index 000000000..a232513d5
--- /dev/null
+++ b/app/fonts/Roboto/RobotoCondensed-Italic.ttf
Binary files differ
diff --git a/app/fonts/Roboto/RobotoCondensed-Light.ttf b/app/fonts/Roboto/RobotoCondensed-Light.ttf
new file mode 100644
index 000000000..a6e368d40
--- /dev/null
+++ b/app/fonts/Roboto/RobotoCondensed-Light.ttf
Binary files differ
diff --git a/app/fonts/Roboto/RobotoCondensed-LightItalic.ttf b/app/fonts/Roboto/RobotoCondensed-LightItalic.ttf
new file mode 100644
index 000000000..5b2b6ae08
--- /dev/null
+++ b/app/fonts/Roboto/RobotoCondensed-LightItalic.ttf
Binary files differ
diff --git a/app/fonts/Roboto/RobotoCondensed-Regular.ttf b/app/fonts/Roboto/RobotoCondensed-Regular.ttf
new file mode 100644
index 000000000..65bf32a19
--- /dev/null
+++ b/app/fonts/Roboto/RobotoCondensed-Regular.ttf
Binary files differ
diff --git a/app/images/.DS_Store b/app/images/.DS_Store
deleted file mode 100644
index d28ef2089..000000000
--- a/app/images/.DS_Store
+++ /dev/null
Binary files differ
diff --git a/app/images/check-white.svg b/app/images/check-white.svg
new file mode 100644
index 000000000..0f15667da
--- /dev/null
+++ b/app/images/check-white.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="13px" viewBox="0 0 16 13" 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>check-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(-17.000000, -80.000000)" fill-rule="nonzero" fill="#FFFFFF">
+ <g id="Group-11" transform="translate(18.000000, 74.000000)">
+ <polygon id="check-white" points="4.2 15.5712828 0.714212839 12.0143571 -0.714212839 13.4142143 4.2 18.4287172 14.7142128 7.69992858 13.2857872 6.30007142"></polygon>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/eth_logo.svg b/app/images/eth_logo.svg
new file mode 100644
index 000000000..894bd70dd
--- /dev/null
+++ b/app/images/eth_logo.svg
@@ -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> \ No newline at end of file
diff --git a/app/images/import-account.svg b/app/images/import-account.svg
new file mode 100644
index 000000000..d6a81b70c
--- /dev/null
+++ b/app/images/import-account.svg
@@ -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> \ No newline at end of file
diff --git a/app/images/info-logo.png b/app/images/info-logo.png
new file mode 100644
index 000000000..f654ed5b1
--- /dev/null
+++ b/app/images/info-logo.png
Binary files differ
diff --git a/app/images/mm-bolt.svg b/app/images/mm-bolt.svg
new file mode 100644
index 000000000..bbf0abcc7
--- /dev/null
+++ b/app/images/mm-bolt.svg
@@ -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>
diff --git a/app/images/mm-info-icon.svg b/app/images/mm-info-icon.svg
new file mode 100644
index 000000000..825f0f200
--- /dev/null
+++ b/app/images/mm-info-icon.svg
@@ -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>
diff --git a/app/images/plus-btn-white.svg b/app/images/plus-btn-white.svg
new file mode 100644
index 000000000..2672d39dd
--- /dev/null
+++ b/app/images/plus-btn-white.svg
@@ -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> \ No newline at end of file
diff --git a/app/images/settings.svg b/app/images/settings.svg
index fe61320a5..cf9b298dd 100644
--- a/app/images/settings.svg
+++ b/app/images/settings.svg
@@ -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> \ No newline at end of file
diff --git a/app/manifest.json b/app/manifest.json
index 4219f3298..ff595c717 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "3.12.1",
+ "version": "4.0.4",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
diff --git a/app/notification.html b/app/notification.html
index cc485da7f..f10cbbf41 100644
--- a/app/notification.html
+++ b/app/notification.html
@@ -1,5 +1,5 @@
<!doctype html>
-<html>
+<html style="height:600px;">
<head>
<meta charset="utf-8">
<title>MetaMask Notification</title>
@@ -9,7 +9,7 @@
}
</style>
</head>
- <body>
+ <body class="notification" style="height:600px;">
<div id="app-content"></div>
<script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script>
</body>
diff --git a/app/popup.html b/app/popup.html
index d09b09315..c4e5188e5 100644
--- a/app/popup.html
+++ b/app/popup.html
@@ -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>
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index bc4848421..0aed4dbdf 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -9,11 +9,20 @@ class PreferencesController {
frequentRpcList: [],
currentAccountTab: 'history',
tokens: [],
+ useBlockie: false,
}, opts.initState)
this.store = new ObservableStore(initState)
}
// PUBLIC METHODS
+ setUseBlockie (val) {
+ this.store.updateState({ useBlockie: val })
+ }
+
+ getUseBlockie () {
+ return this.store.getState().useBlockie
+ }
+
setSelectedAddress (_address) {
return new Promise((resolve, reject) => {
const address = normalizeAddress(_address)
@@ -42,7 +51,16 @@ class PreferencesController {
}
this.store.updateState({ tokens })
- return Promise.resolve()
+ return Promise.resolve(tokens)
+ }
+
+ removeToken (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 () {
diff --git a/app/scripts/lib/environment-type.js b/app/scripts/lib/environment-type.js
new file mode 100644
index 000000000..7966926eb
--- /dev/null
+++ b/app/scripts/lib/environment-type.js
@@ -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'
+ }
+}
diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js
index 693fa8751..e2999411f 100644
--- a/app/scripts/lib/is-popup-or-notification.js
+++ b/app/scripts/lib/is-popup-or-notification.js
@@ -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'
diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js
index 7846ef7f0..adaf60c65 100644
--- a/app/scripts/lib/notification-manager.js
+++ b/app/scripts/lib/notification-manager.js
@@ -1,5 +1,5 @@
const extension = require('extensionizer')
-const height = 520
+const height = 620
const width = 360
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 130ad1471..a96d20d3b 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -324,6 +324,7 @@ module.exports = class MetamaskController extends EventEmitter {
// etc
getState: (cb) => cb(null, this.getState()),
setCurrentCurrency: this.setCurrentCurrency.bind(this),
+ setUseBlockie: this.setUseBlockie.bind(this),
markAccountsFound: this.markAccountsFound.bind(this),
// coinbase
@@ -347,6 +348,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
@@ -801,6 +803,15 @@ module.exports = class MetamaskController extends EventEmitter {
return rpcTarget
}
+ setUseBlockie (val, cb) {
+ try {
+ this.preferencesController.setUseBlockie(val)
+ cb(null)
+ } catch (err) {
+ cb(err)
+ }
+ }
+
recordFirstTimeInfo (initState) {
if (!('firstTimeInfo' in initState)) {
initState.firstTimeInfo = {
diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js
index f1eb394d7..2e4334bb1 100644
--- a/app/scripts/popup-core.js
+++ b/app/scripts/popup-core.js
@@ -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) {
diff --git a/gulpfile.js b/gulpfile.js
index 293179892..195e3f3ca 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -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() {
@@ -189,6 +195,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}`)
@@ -232,9 +269,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
@@ -262,7 +299,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'))
}
}
diff --git a/mascara/src/app/buy-ether-widget/index.js b/mascara/src/app/buy-ether-widget/index.js
new file mode 100644
index 000000000..9fe659daa
--- /dev/null
+++ b/mascara/src/app/buy-ether-widget/index.js
@@ -0,0 +1,197 @@
+import React, {Component, PropTypes} from 'react'
+import classnames from 'classnames'
+import {connect} from 'react-redux'
+import {qrcode} from 'qrcode-npm'
+import copyToClipboard from 'copy-to-clipboard'
+import ShapeShiftForm from '../shapeshift-form'
+import {buyEth, showAccountDetail} from '../../../../ui/app/actions'
+
+const OPTION_VALUES = {
+ COINBASE: 'coinbase',
+ SHAPESHIFT: 'shapeshift',
+ QR_CODE: 'qr_code',
+}
+
+const OPTIONS = [
+ {
+ name: 'Direct Deposit',
+ value: OPTION_VALUES.QR_CODE,
+ },
+ {
+ name: 'Buy with Dollars',
+ value: OPTION_VALUES.COINBASE,
+ },
+ {
+ name: 'Buy with Cryptos',
+ value: OPTION_VALUES.SHAPESHIFT,
+ },
+]
+
+class BuyEtherWidget extends Component {
+
+ static propTypes = {
+ address: PropTypes.string,
+ skipText: PropTypes.string,
+ className: PropTypes.string,
+ onSkip: PropTypes.func,
+ goToCoinbase: PropTypes.func,
+ showAccountDetail: PropTypes.func,
+ };
+
+ state = {
+ selectedOption: OPTION_VALUES.QR_CODE,
+ };
+
+
+ copyToClipboard = () => {
+ const { address } = this.props
+
+ this.setState({ justCopied: true }, () => copyToClipboard(address))
+
+ setTimeout(() => this.setState({ justCopied: false }), 1000)
+ }
+
+ renderSkip () {
+ const {showAccountDetail, address, skipText, onSkip} = this.props
+
+ return (
+ <div
+ className="buy-ether__do-it-later"
+ onClick={() => {
+ if (onSkip) return onSkip()
+ showAccountDetail(address)
+ }}
+ >
+ {skipText || 'Do it later'}
+ </div>
+ )
+ }
+
+ renderCoinbaseLogo () {
+ return (
+ <svg width="140px" height="49px" viewBox="0 0 579 126" version="1.1">
+ <g id="Page-1" stroke="none" strokeWidth={1} fill="none" fillRule="evenodd">
+ <g id="Imported-Layers" fill="#0081C9">
+ <path d="M37.752,125.873 C18.824,125.873 0.369,112.307 0.369,81.549 C0.369,50.79 18.824,37.382 37.752,37.382 C47.059,37.382 54.315,39.749 59.52,43.219 L53.841,55.68 C50.371,53.156 45.166,51.579 39.961,51.579 C28.604,51.579 18.193,60.57 18.193,81.391 C18.193,102.212 28.919,111.361 39.961,111.361 C45.166,111.361 50.371,109.783 53.841,107.26 L59.52,120.036 C54.157,123.664 47.059,125.873 37.752,125.873" id="Fill-1" />
+ <path d="M102.898,125.873 C78.765,125.873 65.515,106.786 65.515,81.549 C65.515,56.311 78.765,37.382 102.898,37.382 C127.032,37.382 140.282,56.311 140.282,81.549 C140.282,106.786 127.032,125.873 102.898,125.873 L102.898,125.873 Z M102.898,51.105 C89.491,51.105 82.866,63.093 82.866,81.391 C82.866,99.688 89.491,111.834 102.898,111.834 C116.306,111.834 122.931,99.688 122.931,81.391 C122.931,63.093 116.306,51.105 102.898,51.105 L102.898,51.105 Z" id="Fill-2" />
+ <path d="M163.468,23.659 C157.79,23.659 153.215,19.243 153.215,13.88 C153.215,8.517 157.79,4.1 163.468,4.1 C169.146,4.1 173.721,8.517 173.721,13.88 C173.721,19.243 169.146,23.659 163.468,23.659 L163.468,23.659 Z M154.793,39.118 L172.144,39.118 L172.144,124.138 L154.793,124.138 L154.793,39.118 Z" id="Fill-3" />
+ <path d="M240.443,124.137 L240.443,67.352 C240.443,57.415 234.449,51.263 222.619,51.263 C216.31,51.263 210.473,52.367 207.003,53.787 L207.003,124.137 L189.81,124.137 L189.81,43.376 C198.328,39.906 209.212,37.382 222.461,37.382 C246.28,37.382 257.794,47.793 257.794,65.775 L257.794,124.137 L240.443,124.137" id="Fill-4" />
+ <path d="M303.536,125.873 C292.494,125.873 281.611,123.191 274.986,119.879 L274.986,0.314 L292.179,0.314 L292.179,41.326 C296.28,39.433 302.905,37.856 308.741,37.856 C330.667,37.856 345.494,53.629 345.494,79.656 C345.494,111.676 328.931,125.873 303.536,125.873 L303.536,125.873 Z M305.744,51.263 C301.012,51.263 295.491,52.367 292.179,54.103 L292.179,109.941 C294.703,111.045 299.593,112.149 304.482,112.149 C318.205,112.149 328.301,102.685 328.301,80.918 C328.301,62.305 319.467,51.263 305.744,51.263 L305.744,51.263 Z" id="Fill-5" />
+ <path d="M392.341,125.873 C367.892,125.873 355.589,115.935 355.589,99.215 C355.589,75.555 380.826,71.296 406.537,69.876 L406.537,64.513 C406.537,53.787 399.439,50.001 388.555,50.001 C380.511,50.001 370.731,52.525 365.053,55.207 L360.636,43.376 C367.419,40.379 378.933,37.382 390.29,37.382 C410.638,37.382 422.942,45.269 422.942,66.248 L422.942,119.879 C416.79,123.191 404.329,125.873 392.341,125.873 L392.341,125.873 Z M406.537,81.391 C389.186,82.337 371.835,83.757 371.835,98.9 C371.835,107.89 378.776,113.411 391.868,113.411 C397.389,113.411 403.856,112.465 406.537,111.203 L406.537,81.391 L406.537,81.391 Z" id="Fill-6" />
+ <path d="M461.743,125.873 C451.806,125.873 441.395,123.191 435.244,119.879 L441.08,106.629 C445.496,109.31 454.803,112.149 461.27,112.149 C470.576,112.149 476.728,107.575 476.728,100.477 C476.728,92.748 470.261,89.751 461.586,86.596 C450.228,82.337 437.452,77.132 437.452,61.201 C437.452,47.162 448.336,37.382 467.264,37.382 C477.517,37.382 486.035,39.906 492.029,43.376 L486.665,55.364 C482.88,52.998 475.309,50.317 469.157,50.317 C460.166,50.317 455.118,55.049 455.118,61.201 C455.118,68.93 461.428,71.611 469.788,74.766 C481.618,79.183 494.71,84.072 494.71,100.635 C494.71,115.935 483.038,125.873 461.743,125.873" id="Fill-7" />
+ <path d="M578.625,81.233 L522.155,89.12 C523.89,104.42 533.828,112.149 548.182,112.149 C556.699,112.149 565.848,110.099 571.684,106.944 L576.732,119.879 C570.107,123.349 558.75,125.873 547.078,125.873 C520.262,125.873 505.277,108.679 505.277,81.549 C505.277,55.522 519.789,37.382 543.607,37.382 C565.69,37.382 578.782,51.894 578.782,74.766 C578.782,76.816 578.782,79.025 578.625,81.233 L578.625,81.233 Z M543.292,50.001 C530.042,50.001 521.367,60.097 521.051,77.763 L562.22,72.084 C562.062,57.257 554.649,50.001 543.292,50.001 L543.292,50.001 Z" id="Fill-8" />
+ </g>
+ </g>
+ </svg>
+ )
+ }
+
+ renderCoinbaseForm () {
+ const {goToCoinbase, address} = this.props
+
+ return (
+ <div className="buy-ether__action-content-wrapper">
+ <div>{this.renderCoinbaseLogo()}</div>
+ <div className="buy-ether__body-text">Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin.</div>
+ <a className="first-time-flow__link buy-ether__faq-link">What is Ethereum?</a>
+ <div className="buy-ether__buttons">
+ <button
+ className="first-time-flow__button"
+ onClick={() => goToCoinbase(address)}
+ >
+ Buy
+ </button>
+ </div>
+ </div>
+ )
+ }
+
+ renderContent () {
+ const { address } = this.props
+ const { justCopied } = this.state
+ const qrImage = qrcode(4, 'M')
+ qrImage.addData(address)
+ qrImage.make()
+
+ switch (this.state.selectedOption) {
+ case OPTION_VALUES.COINBASE:
+ return this.renderCoinbaseForm()
+ case OPTION_VALUES.SHAPESHIFT:
+ return (
+ <div className="buy-ether__action-content-wrapper">
+ <div className="shapeshift-logo" />
+ <div className="buy-ether__body-text">
+ Trade any leading blockchain asset for any other. Protection by Design. No Account Needed.
+ </div>
+ <ShapeShiftForm btnClass="first-time-flow__button" />
+ </div>
+ )
+ case OPTION_VALUES.QR_CODE:
+ return (
+ <div className="buy-ether__action-content-wrapper">
+ <div dangerouslySetInnerHTML={{ __html: qrImage.createTableTag(4) }} />
+ <div className="buy-ether__body-text">Deposit Ether directly into your account.</div>
+ <div className="buy-ether__small-body-text">(This is the account address that MetaMask created for you to recieve funds.)</div>
+ <div className="buy-ether__buttons">
+ <button
+ className="first-time-flow__button"
+ onClick={this.copyToClipboard}
+ disabled={justCopied}
+ >
+ { justCopied ? 'Copied' : 'Copy' }
+ </button>
+ </div>
+ </div>
+ )
+ default:
+ return null
+ }
+ }
+
+ render () {
+ const { className = '' } = this.props
+ const { selectedOption } = this.state
+
+ return (
+ <div className={`${className} buy-ether__content-wrapper`}>
+ <div className="buy-ether__content-headline-wrapper">
+ <div className="buy-ether__content-headline">Deposit Options</div>
+ {this.renderSkip()}
+ </div>
+ <div className="buy-ether__content">
+ <div className="buy-ether__side-panel">
+ {OPTIONS.map(({ name, value }) => (
+ <div
+ key={value}
+ className={classnames('buy-ether__side-panel-item', {
+ 'buy-ether__side-panel-item--selected': value === selectedOption,
+ })}
+ onClick={() => this.setState({ selectedOption: value })}
+ >
+ <div className="buy-ether__side-panel-item-name">{name}</div>
+ {value === selectedOption && (
+ <svg viewBox="0 0 574 1024" id="si-ant-right" width="15px" height="15px">
+ <path d="M10 9Q0 19 0 32t10 23l482 457L10 969Q0 979 0 992t10 23q10 9 24 9t24-9l506-480q10-10 10-23t-10-23L58 9Q48 0 34 0T10 9z" />
+ </svg>
+ )}
+ </div>
+ ))}
+ </div>
+ <div className="buy-ether__action-content">
+ {this.renderContent()}
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
+
+export default connect(
+ ({ metamask: { selectedAddress } }) => ({
+ address: selectedAddress,
+ }),
+ dispatch => ({
+ goToCoinbase: address => dispatch(buyEth({ network: '1', address, amount: 0 })),
+ showAccountDetail: address => dispatch(showAccountDetail(address)),
+ })
+)(BuyEtherWidget)
diff --git a/package.json b/package.json
index 5129dd3cb..72b4951db 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"test": "npm run lint && npm run test:coverage && npm run test:integration",
"test:unit": "METAMASK_ENV=test mocha --compilers js:babel-core/register --require test/helper.js --recursive \"test/unit/**/*.js\"",
"test:single": "METAMASK_ENV=test mocha --require test/helper.js",
- "test:integration": "npm run test:flat && npm run test:mascara",
+ "test:integration": "gulp build:scss && npm run test:flat && npm run test:mascara",
"test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload",
"test:coveralls-upload": "if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi",
"test:flat": "npm run test:flat:build && karma start test/flat.conf.js",
@@ -46,16 +46,22 @@
]
}
],
+ "reactify",
"envify",
"brfs"
]
},
"dependencies": {
+ "abi-decoder": "^1.0.9",
"async": "^2.5.0",
"await-semaphore": "^0.1.1",
"babel-runtime": "^6.23.0",
+ "bignumber.js": "^4.1.0",
+ "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",
@@ -71,7 +77,7 @@
"eslint-plugin-react": "^7.4.0",
"eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^2.2.0",
- "eth-contract-metadata": "^1.1.4",
+ "eth-contract-metadata": "^1.1.5",
"eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^1.2.4",
"eth-keyring-controller": "^2.1.2",
@@ -80,10 +86,12 @@
"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.3.1",
@@ -92,8 +100,11 @@
"extensionizer": "^1.0.0",
"fast-json-patch": "^2.0.4",
"fast-levenshtein": "^2.0.6",
+ "fuse.js": "^3.2.0",
+ "gulp-autoprefixer": "^4.0.0",
"gulp": "github:gulpjs/gulp#4.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",
@@ -127,16 +138,20 @@
"pump": "^1.0.2",
"pumpify": "^1.3.4",
"qrcode-npm": "0.0.3",
+ "ramda": "^0.24.1",
"react": "^15.6.2",
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.6.2",
"react-hyperscript": "^3.0.0",
"react-markdown": "^3.0.0",
"react-redux": "^5.0.5",
- "react-select": "^1.0.0-rc.2",
+ "react-select": "^1.0.0",
"react-simple-file-input": "^2.0.0",
+ "react-toggle-button": "^2.2.0",
"react-tooltip-component": "^0.3.0",
+ "react-transition-group": "^2.2.1",
"react-trigger-change": "^1.0.2",
+ "reactify": "^1.1.1",
"readable-stream": "^2.3.3",
"recompose": "^0.25.0",
"redux": "^3.0.5",
@@ -145,6 +160,7 @@
"request-promise": "^4.2.1",
"sandwich-expando": "^1.1.3",
"semaphore": "^1.0.5",
+ "shallow-copy": "0.0.1",
"semver": "^5.4.1",
"sw-stream": "^2.0.0",
"textarea-caret": "^3.0.1",
@@ -175,8 +191,10 @@
"del": "^3.0.0",
"envify": "^4.0.0",
"enzyme": "^3.2.0",
+ "enzyme-adapter-react-15": "^1.0.5",
"eslint-plugin-chai": "0.0.1",
"eslint-plugin-mocha": "^4.9.0",
+ "eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.2.7",
"fs-promise": "^2.0.3",
"gulp": "github:gulpjs/gulp#4.0",
@@ -185,6 +203,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",
@@ -211,7 +231,9 @@
"react-addons-test-utils": "^15.5.1",
"react-test-renderer": "^15.6.2",
"react-testutils-additions": "^15.2.0",
+ "redux-test-utils": "^0.1.3",
"sinon": "^4.0.0",
+ "stylelint-config-standard": "^17.0.0",
"tape": "^4.5.1",
"testem": "^1.10.3",
"uglifyify": "^4.0.2",
diff --git a/test/helper.js b/test/helper.js
index 1c5934a89..a3abbebf2 100644
--- a/test/helper.js
+++ b/test/helper.js
@@ -1,3 +1,7 @@
+import Enzyme from 'enzyme'
+import Adapter from 'enzyme-adapter-react-15'
+
+Enzyme.configure({ adapter: new Adapter() })
// disallow promises from swallowing errors
enableFailureOnUnhandledPromiseRejection()
diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js
index 61b38897e..e59897713 100644
--- a/test/integration/lib/first-time.js
+++ b/test/integration/lib/first-time.js
@@ -40,7 +40,8 @@ async function runFirstTimeUsageTest(assert, done) {
// Scroll through terms
const title = app.find('h1').text()
- assert.equal(title, 'MetaMask', 'title screen')
+ // TODO Find where Metamask is getting added twice in the title
+ assert.equal(title, 'MetaMaskMetaMask', 'title screen')
// enter password
const pwBox = app.find('#password-box')[0]
@@ -66,17 +67,17 @@ async function runFirstTimeUsageTest(assert, done) {
await timeout(1000)
- const detail = app.find('.account-detail-section')[0]
+ const detail = app.find('.wallet-view')[0]
assert.ok(detail, 'Account detail section loaded.')
- const sandwich = app.find('.sandwich-expando')[0]
- sandwich.click()
+ await timeout(1000)
- await timeout()
+ const menu = app.find('.account-menu__icon')[0]
+ menu.click()
+
+ await timeout(1000)
- const menu = app.find('.menu-droppo')[0]
- const children = menu.children
- const lock = children[children.length - 2]
+ const lock = app.find('.account-menu__logout-button')[0]
assert.ok(lock, 'Lock menu item found')
lock.click()
@@ -90,36 +91,30 @@ async function runFirstTimeUsageTest(assert, done) {
await timeout(1000)
- const detail2 = app.find('.account-detail-section')[0]
+ const detail2 = app.find('.wallet-view')[0]
assert.ok(detail2, 'Account detail section loaded again.')
await timeout()
// open account settings dropdown
- const qrButton = app.find('.fa.fa-ellipsis-h')[0]
+ const qrButton = app.find('.wallet-view__details-button')[0]
qrButton.click()
await timeout(1000)
- // qr code item
- const qrButton2 = app.find('.dropdown-menu-item')[1]
- qrButton2.click()
-
- await timeout(1000)
-
- const qrHeader = app.find('.qr-header')[0]
- const qrContainer = app.find('#qr-container')[0]
+ const qrHeader = app.find('.editable-label__value')[0]
+ const qrContainer = app.find('.qr-wrapper')[0]
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
assert.ok(qrContainer, 'QR Container found')
await timeout()
- const networkMenu = app.find('.network-indicator')[0]
+ const networkMenu = app.find('.network-component')[0]
networkMenu.click()
await timeout()
- const networkMenu2 = app.find('.network-indicator')[0]
+ const networkMenu2 = app.find('.menu-droppo')[0]
const children2 = networkMenu2.children
children2.length[3]
assert.ok(children2, 'All network options present')
diff --git a/test/integration/lib/mascara-first-time.js b/test/integration/lib/mascara-first-time.js
index 5c18cd254..515c7f383 100644
--- a/test/integration/lib/mascara-first-time.js
+++ b/test/integration/lib/mascara-first-time.js
@@ -71,14 +71,12 @@ async function runFirstTimeUsageTest (assert, done) {
app.find('.buy-ether__do-it-later').click()
await timeout(1000)
- const sandwich = app.find('.sandwich-expando')[0]
- sandwich.click()
+ const menu = app.find('.account-menu__icon')[0]
+ menu.click()
await timeout()
- const menu = app.find('.menu-droppo')[0]
- const children = menu.children
- const lock = children[children.length - 2]
+ const lock = app.find('.account-menu__logout-button')[0]
assert.ok(lock, 'Lock menu item found')
lock.click()
@@ -92,31 +90,25 @@ async function runFirstTimeUsageTest (assert, done) {
await timeout(1000)
- const detail2 = app.find('.account-detail-section')[0]
+ const detail2 = app.find('.wallet-view')[0]
assert.ok(detail2, 'Account detail section loaded again.')
await timeout()
// open account settings dropdown
- const qrButton = app.find('.fa.fa-ellipsis-h')[0]
+ const qrButton = app.find('.wallet-view__details-button')[0]
qrButton.click()
await timeout(1000)
- // qr code item
- const qrButton2 = app.find('.dropdown-menu-item')[1]
- qrButton2.click()
-
- await timeout(1000)
-
- const qrHeader = app.find('.qr-header')[0]
- const qrContainer = app.find('#qr-container')[0]
+ const qrHeader = app.find('.editable-label__value')[0]
+ const qrContainer = app.find('.qr-wrapper')[0]
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
assert.ok(qrContainer, 'QR Container found')
await timeout()
- const networkMenu = app.find('.network-indicator')[0]
+ const networkMenu = app.find('.network-component')[0]
networkMenu.click()
await timeout()
diff --git a/test/lib/shallow-with-store.js b/test/lib/shallow-with-store.js
new file mode 100644
index 000000000..2a66adb17
--- /dev/null
+++ b/test/lib/shallow-with-store.js
@@ -0,0 +1,16 @@
+const { shallow, mount } = require('enzyme')
+
+exports.shallowWithStore = function shallowWithStore (component, store) {
+ const context = {
+ store,
+ }
+
+ return shallow(component, { context })
+}
+
+exports.mountWithStore = function mountWithStore (component, store) {
+ const context = {
+ store,
+ }
+ return mount(component, { context })
+}
diff --git a/test/unit/components/balance-component-test.js b/test/unit/components/balance-component-test.js
new file mode 100644
index 000000000..9b1e82acf
--- /dev/null
+++ b/test/unit/components/balance-component-test.js
@@ -0,0 +1,45 @@
+const assert = require('assert')
+const h = require('react-hyperscript')
+const { createMockStore } = require('redux-test-utils')
+const { shallowWithStore } = require('../../lib/shallow-with-store')
+const BalanceComponent = require('../../../ui/app/components/balance-component')
+const mockState = {
+ metamask: {
+ accounts: { abc: {} },
+ network: 1,
+ selectedAddress: 'abc',
+ }
+}
+
+describe('BalanceComponent', function () {
+ let balanceComponent
+ let store
+ let component
+ beforeEach(function () {
+ store = createMockStore(mockState)
+ component = shallowWithStore(h(BalanceComponent), store)
+ balanceComponent = component.dive()
+ })
+
+ it('shows token balance and convert to fiat value based on conversion rate', function () {
+ const formattedBalance = '1.23 ETH'
+
+ const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false)
+ const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 2)
+
+ assert.equal('1.23 ETH', tokenBalance)
+ assert.equal(2.46, fiatDisplayNumber)
+ })
+
+ it('shows only the token balance when conversion rate is not available', function () {
+ const formattedBalance = '1.23 ETH'
+
+ const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false)
+ const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 0)
+
+ assert.equal('1.23 ETH', tokenBalance)
+ assert.equal('N/A', fiatDisplayNumber)
+ })
+
+})
+
diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js
index 20feba2a3..c6c588e1c 100644
--- a/test/unit/components/pending-tx-test.js
+++ b/test/unit/components/pending-tx-test.js
@@ -1,18 +1,22 @@
const assert = require('assert')
-const additions = require('react-testutils-additions')
const h = require('react-hyperscript')
const PendingTx = require('../../../ui/app/components/pending-tx')
-const ReactTestUtils = require('react-addons-test-utils')
const ethUtil = require('ethereumjs-util')
-describe('PendingTx', function () {
- const identities = {
- '0xfdea65c8e26263f6d9a1b5de9555d2931a33b826': {
- name: 'Main Account 1',
- balance: '0x00000000000000056bc75e2d63100000',
- },
+const { createMockStore } = require('redux-test-utils')
+const { shallowWithStore } = require('../../lib/shallow-with-store')
+
+const identities = { abc: {}, def: {} }
+const mockState = {
+ metamask: {
+ accounts: { abc: {} },
+ identities,
+ conversionRate: 10,
+ selectedAddress: 'abc',
}
+}
+describe('PendingTx', function () {
const gasPrice = '0x4A817C800' // 20 Gwei
const txData = {
'id': 5021615666270214,
@@ -29,55 +33,35 @@ describe('PendingTx', function () {
'gasLimitSpecified': false,
'estimatedGas': '0x5208',
}
+ const newGasPrice = '0x77359400'
+ const computedBalances = {}
+ computedBalances[Object.keys(identities)[0]] = {
+ ethBalance: '0x00000000000000056bc75e2d63100000',
+ }
+ const props = {
+ txData,
+ computedBalances,
+ sendTransaction: (txMeta, event) => {
+ // Assert changes:
+ const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice)
+ assert.notEqual(result, gasPrice, 'gas price should change')
+ assert.equal(result, newGasPrice, 'gas price assigned.')
+ },
+ }
- it('should use updated values when edited.', function (done) {
- const renderer = ReactTestUtils.createRenderer()
- const newGasPrice = '0x77359400'
-
- const computedBalances = {}
- computedBalances[Object.keys(identities)[0]] = {
- ethBalance: '0x00000000000000056bc75e2d63100000',
- }
- const props = {
- identities,
- accounts: identities,
- txData,
- computedBalances,
- sendTransaction: (txMeta, event) => {
- // Assert changes:
- const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice)
- assert.notEqual(result, gasPrice, 'gas price should change')
- assert.equal(result, newGasPrice, 'gas price assigned.')
- done()
- },
- }
-
- const pendingTxComponent = h(PendingTx, props)
- const component = additions.renderIntoDocument(pendingTxComponent)
- renderer.render(pendingTxComponent)
- const result = renderer.getRenderOutput()
- assert.equal(result.type, 'div', 'should create a div')
-
- try {
- const input = additions.find(component, '.cell.row input[type="number"]')[1]
- ReactTestUtils.Simulate.change(input, {
- target: {
- value: 2,
- checkValidity () { return true },
- },
- })
+ let pendingTxComponent
+ let store
+ let component
+ beforeEach(function () {
+ store = createMockStore(mockState)
+ component = shallowWithStore(h(PendingTx, props), store)
+ pendingTxComponent = component
+ })
- const form = additions.find(component, 'form')[0]
- form.checkValidity = () => true
- form.getFormEl = () => { return { checkValidity () { return true } } }
- ReactTestUtils.Simulate.submit(form, { preventDefault () {}, target: { checkValidity () {
- return true
- } } })
- } catch (e) {
- console.log('WHAAAA')
- console.error(e)
- }
+ it('should render correctly', function (done) {
+ assert.equal(pendingTxComponent.props().identities, identities)
+ done()
})
})
diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js
index 393601a57..961fa6baf 100644
--- a/test/unit/pending-tx-test.js
+++ b/test/unit/pending-tx-test.js
@@ -12,6 +12,7 @@ const currentNetworkId = 42
const otherNetworkId = 36
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
+
describe('PendingTransactionTracker', function () {
let pendingTxTracker, txMeta, txMetaNoHash, txMetaNoRawTx, providerResultStub,
provider, txMeta3, txList, knownErrors
diff --git a/test/unit/responsive/components/dropdown-test.js b/test/unit/responsive/components/dropdown-test.js
index 3ad2c390e..982d8c6ec 100644
--- a/test/unit/responsive/components/dropdown-test.js
+++ b/test/unit/responsive/components/dropdown-test.js
@@ -1,40 +1,45 @@
-var assert = require('assert');
+const assert = require('assert');
-const additions = require('react-testutils-additions');
const h = require('react-hyperscript');
-const ReactTestUtils = require('react-addons-test-utils');
const sinon = require('sinon');
const path = require('path');
-const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdown.js')).Dropdown;
-const DropdownMenuItem = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdown.js')).DropdownMenuItem;
+const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdowns', 'index.js')).Dropdown;
+
+const { createMockStore } = require('redux-test-utils')
+const { mountWithStore } = require('../../../lib/shallow-with-store')
+
+const mockState = {
+ metamask: {
+ }
+}
describe('Dropdown components', function () {
let onClickOutside;
let closeMenu;
let onClick;
- let dropdownComponentProps;
- const renderer = ReactTestUtils.createRenderer()
+ let dropdownComponentProps = {
+ isOpen: true,
+ zIndex: 11,
+ onClickOutside,
+ style: {
+ position: 'absolute',
+ right: 0,
+ top: '36px',
+ },
+ innerStyle: {},
+ }
+
+ let dropdownComponent
+ let store
+ let component
beforeEach(function () {
onClickOutside = sinon.spy();
closeMenu = sinon.spy();
onClick = sinon.spy();
- dropdownComponentProps = {
- isOpen: true,
- zIndex: 11,
- onClickOutside,
- style: {
- position: 'absolute',
- right: 0,
- top: '36px',
- },
- innerStyle: {},
- }
- });
-
- it('can render two items', function () {
- const dropdownComponent = h(
+ store = createMockStore(mockState)
+ component = mountWithStore(h(
Dropdown,
dropdownComponentProps,
[
@@ -42,74 +47,35 @@ describe('Dropdown components', function () {
.drop-menu-item:hover { background:rgb(235, 235, 235); }
.drop-menu-item i { margin: 11px; }
`),
- h(DropdownMenuItem, {
+ h('li', {
closeMenu,
onClick,
}, 'Item 1'),
- h(DropdownMenuItem, {
+ h('li', {
closeMenu,
onClick,
}, 'Item 2'),
]
- )
+ ), store)
+ dropdownComponent = component
+ })
- const component = additions.renderIntoDocument(dropdownComponent);
- renderer.render(dropdownComponent);
- const items = additions.find(component, 'li');
+ it('can render two items', function () {
+ const items = dropdownComponent.find('li');
assert.equal(items.length, 2);
});
it('closes when item clicked', function() {
- const dropdownComponent = h(
- Dropdown,
- dropdownComponentProps,
- [
- h('style', `
- .drop-menu-item:hover { background:rgb(235, 235, 235); }
- .drop-menu-item i { margin: 11px; }
- `),
- h(DropdownMenuItem, {
- closeMenu,
- onClick,
- }, 'Item 1'),
- h(DropdownMenuItem, {
- closeMenu,
- onClick,
- }, 'Item 2'),
- ]
- )
- const component = additions.renderIntoDocument(dropdownComponent);
- renderer.render(dropdownComponent);
- const items = additions.find(component, 'li');
- const node = items[0];
- ReactTestUtils.Simulate.click(node);
- assert.equal(closeMenu.calledOnce, true);
+ const items = dropdownComponent.find('li');
+ const node = items.at(0);
+ node.simulate('click');
+ assert.equal(node.props().closeMenu, closeMenu);
});
it('invokes click handler when item clicked', function() {
- const dropdownComponent = h(
- Dropdown,
- dropdownComponentProps,
- [
- h('style', `
- .drop-menu-item:hover { background:rgb(235, 235, 235); }
- .drop-menu-item i { margin: 11px; }
- `),
- h(DropdownMenuItem, {
- closeMenu,
- onClick,
- }, 'Item 1'),
- h(DropdownMenuItem, {
- closeMenu,
- onClick,
- }, 'Item 2'),
- ]
- )
- const component = additions.renderIntoDocument(dropdownComponent);
- renderer.render(dropdownComponent);
- const items = additions.find(component, 'li');
- const node = items[0];
- ReactTestUtils.Simulate.click(node);
+ const items = dropdownComponent.find('li');
+ const node = items.at(0);
+ node.simulate('click');
assert.equal(onClick.calledOnce, true);
});
});
diff --git a/ui/app/account-and-transaction-details.js b/ui/app/account-and-transaction-details.js
new file mode 100644
index 000000000..60293de77
--- /dev/null
+++ b/ui/app/account-and-transaction-details.js
@@ -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: {
+ },
+ }, [
+ ]),
+ ])
+}
+
diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js
index d4f707e0b..0da435298 100644
--- a/ui/app/account-detail.js
+++ b/ui/app/account-detail.js
@@ -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
diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js
index 46260c3e7..b7d9a9537 100644
--- a/ui/app/accounts/import/index.js
+++ b/ui/app/accounts/import/index.js
@@ -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)',
},
}, [
diff --git a/ui/app/accounts/import/json.js b/ui/app/accounts/import/json.js
index 158a3c923..486ed8886 100644
--- a/ui/app/accounts/import/json.js
+++ b/ui/app/accounts/import/json.js
@@ -5,7 +5,7 @@ const connect = require('react-redux').connect
const actions = require('../../actions')
const FileInput = require('react-simple-file-input').default
-const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file'
+const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
module.exports = connect(mapStateToProps)(JsonImportSubview)
diff --git a/ui/app/accounts/import/private-key.js b/ui/app/accounts/import/private-key.js
index 68ccee58e..e214bcbbe 100644
--- a/ui/app/accounts/import/private-key.js
+++ b/ui/app/accounts/import/private-key.js
@@ -17,6 +17,10 @@ function PrivateKeyImportView () {
Component.call(this)
}
+PrivateKeyImportView.prototype.componentWillUnmount = function () {
+ this.props.dispatch(actions.displayWarning(null))
+}
+
PrivateKeyImportView.prototype.render = function () {
const { error } = this.props
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 04fd35b20..e79f4373e 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -1,10 +1,27 @@
+const abi = require('human-standard-token-abi')
const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
+const ethUtil = require('ethereumjs-util')
var actions = {
_setBackgroundConnection: _setBackgroundConnection,
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 +85,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 +97,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 +107,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
@@ -93,22 +115,54 @@ var actions = {
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
NEXT_TX: 'NEXT_TX',
PREVIOUS_TX: 'PREV_TX',
+ EDIT_TX: 'EDIT_TX',
signMsg: signMsg,
cancelMsg: cancelMsg,
signPersonalMsg,
cancelPersonalMsg,
signTypedMsg,
cancelTypedMsg,
+ sendTx: sendTx,
signTx: signTx,
+ signTokenTx: signTokenTx,
updateAndApproveTx,
cancelTx: cancelTx,
completedTx: completedTx,
txError: txError,
nextTx: nextTx,
+ editTx,
previousTx: previousTx,
cancelAllTx: cancelAllTx,
viewPendingTx: viewPendingTx,
VIEW_PENDING_TX: 'VIEW_PENDING_TX',
+ updateTransactionParams,
+ UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS',
+ // 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_TOKEN_BALANCE: 'UPDATE_SEND_TOKEN_BALANCE',
+ UPDATE_SEND_TO: 'UPDATE_SEND_TO',
+ UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT',
+ UPDATE_SEND_MEMO: 'UPDATE_SEND_MEMO',
+ UPDATE_SEND_ERRORS: 'UPDATE_SEND_ERRORS',
+ UPDATE_SEND: 'UPDATE_SEND',
+ CLEAR_SEND: 'CLEAR_SEND',
+ updateGasLimit,
+ updateGasPrice,
+ updateGasTotal,
+ updateSendTokenBalance,
+ updateSendFrom,
+ updateSendTo,
+ updateSendAmount,
+ updateSendMemo,
+ updateSendErrors,
+ updateSend,
+ clearSend,
+ setSelectedAddress,
// app messages
confirmSeedWords: confirmSeedWords,
showAccountDetail: showAccountDetail,
@@ -125,8 +179,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',
@@ -144,6 +203,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,
@@ -168,6 +229,14 @@ var actions = {
callBackgroundThenUpdate,
forceUpdateMetamaskState,
+
+ TOGGLE_ACCOUNT_MENU: 'TOGGLE_ACCOUNT_MENU',
+ toggleAccountMenu,
+
+ useEtherscanProvider,
+
+ SET_USE_BLOCKIE: 'SET_USE_BLOCKIE',
+ setUseBlockie,
}
module.exports = actions
@@ -356,7 +425,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 () {
@@ -367,16 +453,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,
@@ -449,10 +535,146 @@ 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({}))
+ }
+}
+
+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)
+ })
})
- dispatch(actions.showConfTxPage())
+ }
+}
+
+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 updateSendTokenBalance (tokenBalance) {
+ return {
+ type: actions.UPDATE_SEND_TOKEN_BALANCE,
+ value: tokenBalance,
+ }
+}
+
+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) {
+ return {
+ type: actions.UPDATE_SEND_ERRORS,
+ value: error,
+ }
+}
+
+function updateSend (newSend) {
+ return {
+ type: actions.UPDATE_SEND,
+ value: newSend,
+ }
+}
+
+function clearSend () {
+ return {
+ type: actions.CLEAR_SEND,
+ }
+}
+
+
+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, ethUtil.addHexPrefix(amount), txData)
+ .catch(err => {
+ dispatch(actions.hideLoadingIndication())
+ dispatch(actions.displayWarning(err.message))
+ })
+ dispatch(actions.showConfTxPage({}))
}
}
@@ -462,6 +684,8 @@ function updateAndApproveTx (txData) {
log.debug(`actions calling background.updateAndApproveTx`)
background.updateAndApproveTransaction(txData, (err) => {
dispatch(actions.hideLoadingIndication())
+ dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
+ dispatch(actions.clearSend())
if (err) {
dispatch(actions.txError(err))
dispatch(actions.goHome())
@@ -479,6 +703,14 @@ function completedTx (id) {
}
}
+function updateTransactionParams (id, txParams) {
+ return {
+ type: actions.UPDATE_TRANSACTION_PARAMS,
+ id,
+ value: txParams,
+ }
+}
+
function txError (err) {
return {
type: actions.TRANSACTION_ERROR,
@@ -613,9 +845,50 @@ function updateMetamaskState (newState) {
}
}
+const backgroundSetLocked = () => {
+ return new Promise((resolve, reject) => {
+ background.setLocked(error => {
+ if (error) {
+ return reject(error)
+ }
+
+ resolve()
+ })
+ })
+}
+
+const updateMetamaskStateFromBackground = () => {
+ log.debug(`background.getState`)
+
+ return new Promise((resolve, reject) => {
+ background.getState((error, newState) => {
+ if (error) {
+ return reject(error)
+ }
+
+ resolve(newState)
+ })
+ })
+}
+
function lockMetamask () {
log.debug(`background.setLocked`)
- return callBackgroundThenUpdate(background.setLocked)
+
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+
+ return backgroundSetLocked()
+ .then(() => updateMetamaskStateFromBackground())
+ .catch(error => {
+ dispatch(actions.displayWarning(error.message))
+ return Promise.reject(error)
+ })
+ .then(newState => {
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch({ type: actions.LOCK_METAMASK })
+ })
+ .catch(() => dispatch({ type: actions.LOCK_METAMASK }))
+ }
}
function setCurrentAccountTab (newTabName) {
@@ -623,6 +896,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())
@@ -636,6 +929,7 @@ function showAccountDetail (address) {
type: actions.SHOW_ACCOUNT_DETAIL,
value: address,
})
+ dispatch(actions.setSelectedToken())
})
}
}
@@ -653,10 +947,11 @@ function showAccountsPage () {
}
}
-function showConfTxPage (transForward = true) {
+function showConfTxPage ({transForward = true, id}) {
return {
type: actions.SHOW_CONF_TX_PAGE,
- transForward: transForward,
+ transForward,
+ id,
}
}
@@ -679,6 +974,13 @@ function previousTx () {
}
}
+function editTx (txId) {
+ return {
+ type: actions.EDIT_TX,
+ value: txId,
+ }
+}
+
function showConfigPage (transitionForward = true) {
return {
type: actions.SHOW_CONFIG_PAGE,
@@ -686,28 +988,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,
@@ -771,11 +1116,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,
}
}
@@ -792,7 +1143,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) => {
@@ -804,6 +1155,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,
@@ -855,27 +1254,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,
@@ -887,14 +1298,22 @@ function saveAccountLabel (account, label) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
log.debug(`background.saveAccountLabel`)
- background.saveAccountLabel(account, label, (err) => {
- dispatch(actions.hideLoadingIndication())
- if (err) {
- return dispatch(actions.displayWarning(err.message))
- }
- dispatch({
- type: actions.SAVE_ACCOUNT_LABEL,
- value: { account, label },
+
+ return new Promise((resolve, reject) => {
+ background.saveAccountLabel(account, label, (err) => {
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ reject(err)
+ }
+
+ dispatch({
+ type: actions.SAVE_ACCOUNT_LABEL,
+ value: { account, label },
+ })
+
+ resolve(account)
})
})
}
@@ -906,6 +1325,12 @@ function showSendPage () {
}
}
+function showSendTokenPage () {
+ return {
+ type: actions.SHOW_SEND_TOKEN_PAGE,
+ }
+}
+
function buyEth (opts) {
return (dispatch) => {
const url = getBuyEthUrl(opts)
@@ -1022,7 +1447,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 },
+ }))
})
}
}
@@ -1056,6 +1484,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:
@@ -1097,3 +1547,26 @@ function forceUpdateMetamaskState (dispatch) {
dispatch(actions.updateMetamaskState(newState))
})
}
+
+function toggleAccountMenu () {
+ return {
+ type: actions.TOGGLE_ACCOUNT_MENU,
+ }
+}
+
+function setUseBlockie (val) {
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+ log.debug(`background.setUseBlockie`)
+ background.setUseBlockie(val, (err) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ })
+ dispatch({
+ type: actions.SET_USE_BLOCKIE,
+ value: val,
+ })
+ }
+}
diff --git a/ui/app/add-token.js b/ui/app/add-token.js
index 9354a4cad..10aaae103 100644
--- a/ui/app/add-token.js
+++ b/ui/app/add-token.js
@@ -1,238 +1,357 @@
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)
+ .filter(tokenData => Boolean(tokenData.erc20))
+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 { tokenInfoGetter } = require('./token-util')
+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
+AddTokenScreen.prototype.componentWillMount = function () {
+ this.tokenInfoGetter = tokenInfoGetter()
+}
- const { address, symbol, decimals } = this.state
- this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals))
- },
- }, 'Add'),
- ]),
- ]),
- ])
- )
+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.componentWillMount = function () {
- if (typeof global.ethereumProvider === 'undefined') return
+AddTokenScreen.prototype.onNext = function () {
+ const { isValid, errors } = this.validate()
- this.eth = new Eth(global.ethereumProvider)
- this.contract = new EthContract(this.eth)
- this.TokenContract = this.contract(abi)
+ return !isValid
+ ? this.setState({ errors })
+ : this.setState({ isShowingConfirmation: true })
}
-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.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
- const identitiesList = Object.keys(this.props.identities)
- const { address, symbol, decimals } = state
- const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
-
- const validAddress = ethUtil.isValidAddress(address)
- if (!validAddress) {
- msg += 'Address is invalid. '
+AddTokenScreen.prototype.checkExistingAddresses = function (address) {
+ if (!address) return false
+ const tokensList = this.props.tokens
+ const matchesAddress = existingToken => {
+ return existingToken.address.toLowerCase() === address.toLowerCase()
}
- const validDecimals = decimals >= 0 && decimals < 36
- if (!validDecimals) {
- msg += 'Decimals must be at least 0, and not over 36. '
- }
+ return R.any(matchesAddress)(tokensList)
+}
- const symbolLen = symbol.trim().length
- const validSymbol = symbolLen > 0 && symbolLen < 10
- if (!validSymbol) {
- msg += 'Symbol must be between 0 and 10 characters.'
+AddTokenScreen.prototype.validate = function () {
+ const errors = {}
+ const identitiesList = Object.keys(this.props.identities)
+ const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state
+ const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
+
+ 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 ownAddress = identitiesList.includes(standardAddress)
- if (ownAddress) {
- msg = 'Personal address detected. Input the token contract address.'
+ return {
+ isValid: !Object.keys(errors).length,
+ errors,
}
+}
- const isValid = validAddress && validDecimals && !ownAddress
-
- if (!isValid) {
+AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
+ const { symbol, decimals } = await this.tokenInfoGetter(address)
+ if (symbol && decimals) {
this.setState({
- warning: msg,
+ customSymbol: symbol,
+ customDecimals: decimals.toString(),
})
- } else {
- this.setState({ warning: null })
}
+}
+
+AddTokenScreen.prototype.renderCustomForm = function () {
+ const { customAddress, customSymbol, customDecimals, errors } = this.state
- return isValid
+ 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.attemptToAutoFillTokenParams = async function (address) {
- const contract = this.TokenContract.at(address)
+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')
+ // ),
+ ])
+ )
+ })
+}
- const results = await Promise.all([
- contract.symbol(),
- contract.decimals(),
- ])
+AddTokenScreen.prototype.renderConfirmation = function () {
+ const {
+ customAddress: address,
+ customSymbol: symbol,
+ customDecimals: decimals,
+ selectedTokens,
+ } = this.state
- const [ symbol, decimals ] = results
- if (symbol && decimals) {
- console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals })
- this.setState({ symbol: symbol[0], decimals: decimals[0].toString() })
+ 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 you’ve 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',
+ h(`i.fa.fa-angle-${isCollapsed ? 'down' : 'up'}`),
+ ]),
+ this.renderCustomForm(),
+ ]),
+ ]),
+ h('div.add-token__buttons', [
+ h('button.btn-secondary', {
+ onClick: this.onNext,
+ }, 'Next'),
+ h('button.btn-tertiary', {
+ onClick: goHome,
+ }, 'Cancel'),
+ ]),
+ ])
+ )
}
diff --git a/ui/app/app.js b/ui/app/app.js
index bd0ccb0a2..e90c3e98e 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -9,33 +9,37 @@ const MascaraBuyEtherScreen = require('../../mascara/src/app/first-time/buy-ethe
// 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 SendTransactionScreen = require('./send')
+const MainContainer = require('./main-container')
+const SendTransactionScreen2 = require('./components/send/send-v2-container')
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 Settings = require('./settings')
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')
+const QrView = require('./components/qr-code')
-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) }
@@ -54,11 +58,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,6 +81,7 @@ function mapStateToProps (state) {
lastUnreadNotice: state.metamask.lastUnreadNotice,
lostAccounts: state.metamask.lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
+ currentCurrency: state.metamask.currentCurrency,
// state needed to get account dropdown temporarily rendering from app bar
identities,
@@ -82,9 +90,26 @@ 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 () {
+ if (!this.props.currentCurrency) {
+ 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
@@ -94,40 +119,112 @@ 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(),
- this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }),
+ // sidebar
+ this.renderSidebar(),
- // panel content
- h('.app-primary' + (transForward ? '.from-right' : '.from-left'), {
- style: {
- width: '100%',
- },
- }, [
- this.renderPrimary(),
- ]),
+ // network dropdown
+ h(NetworkDropdown, {
+ provider: this.props.provider,
+ frequentRpcList: this.props.frequentRpcList,
+ }, []),
+
+ h(AccountMenu),
+
+ (isLoading || isLoadingNetwork) && h(Loading, {
+ loadingMessage: loadMessage,
+ }),
+
+ // this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }),
+
+ // 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 () {
+ const {
+ isUnlocked,
+ network,
+ provider,
+ networkDropdownOpen,
+ showNetworkDropdown,
+ hideNetworkDropdown,
+ currentView,
+ } = this.props
+
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
const props = this.props
- const state = this.state || {}
- const isNetworkMenuOpen = state.isNetworkMenuOpen || false
const {isMascara, isOnboarding} = props
// Do not render header if user is in mascara onboarding
@@ -143,266 +240,70 @@ App.prototype.renderAppBar = function () {
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, {
- style: {},
- enableAccountsSelector: true,
- identities: this.props.identities,
- selected: this.props.currentView.context,
- network: this.props.network,
- keyrings: this.props.keyrings,
- }, []),
-
- // hamburger
- props.isUnlocked && h(SandwichExpando, {
- className: 'sandwich-expando',
- width: 16,
- barHeight: 2,
- padding: 0,
- isOpen: state.isMainMenuOpen,
- color: 'rgb(247,146,30)',
+ h('div.app-header-contents', {}, [
+ h('div.left-menu-wrapper', {
onClick: () => {
- this.setState({
- isMainMenuOpen: !state.isMainMenuOpen,
- })
+ props.dispatch(actions.backToAccountDetail(props.activeAddress))
},
- }),
+ }, [
+ // mini logo
+ h('img.metafox-icon', {
+ height: 29,
+ width: 29,
+ src: '/images/icon-128.png',
+ }),
+
+ // 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,
+ provider,
+ disabled: currentView.name === 'confTx',
+ onClick: (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ return networkDropdownOpen === false
+ ? showNetworkDropdown()
+ : hideNetworkDropdown()
+ },
+ }),
+
+ ]),
+
+ isUnlocked && 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.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork, loadMessage }) {
const { isMascara } = this.props
@@ -483,32 +384,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')
@@ -524,7 +431,7 @@ App.prototype.renderPrimary = function () {
case 'config':
log.debug('rendering config screen')
- return h(ConfigScreen, {key: 'config'})
+ return h(Settings, {key: 'config'})
case 'import-menu':
log.debug('rendering import screen')
@@ -536,7 +443,7 @@ App.prototype.renderPrimary = function () {
case 'info':
log.debug('rendering info screen')
- return h(InfoScreen, {key: 'info'})
+ return h(Settings, {key: 'info', tab: 'info'})
case 'buyEth':
log.debug('rendering buy ether screen')
@@ -576,7 +483,7 @@ App.prototype.renderPrimary = function () {
default:
log.debug('rendering default, account detail screen')
- return h(AccountDetailScreen, {key: 'account-detail'})
+ return h(MainContainer, {key: 'account-detail'})
}
}
@@ -592,40 +499,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
@@ -646,28 +519,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,
- ]
- )
- }
- })
-}
diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js
new file mode 100644
index 000000000..a9f075ec7
--- /dev/null
+++ b/ui/app/components/account-menu/index.js
@@ -0,0 +1,160 @@
+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.displayWarning(null))
+ dispatch(actions.toggleAccountMenu())
+ },
+ showConfigPage: () => {
+ dispatch(actions.showConfigPage())
+ dispatch(actions.toggleAccountMenu())
+ },
+ showNewAccountModal: () => {
+ dispatch(actions.showModal({ name: 'NEW_ACCOUNT' }))
+ dispatch(actions.toggleAccountMenu())
+ },
+ showImportPage: () => {
+ dispatch(actions.showImportPage())
+ dispatch(actions.toggleAccountMenu())
+ },
+ showInfoPage: () => {
+ dispatch(actions.showInfoPage())
+ dispatch(actions.toggleAccountMenu())
+ },
+ }
+}
+
+AccountMenu.prototype.render = function () {
+ const {
+ isAccountMenuOpen,
+ toggleAccountMenu,
+ showNewAccountModal,
+ showImportPage,
+ lockMetamask,
+ showConfigPage,
+ showInfoPage,
+ } = this.props
+
+ return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [
+ h(CloseArea, { onClick: toggleAccountMenu }),
+ h(Item, {
+ className: 'account-menu__header',
+ }, [
+ 'My Accounts',
+ h('button.account-menu__logout-button', {
+ onClick: lockMetamask,
+ }, '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, {
+ onClick: showInfoPage,
+ 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,
+ selectedAddress,
+ keyrings,
+ showAccountDetail,
+ } = this.props
+
+ return Object.keys(identities).map((key, index) => {
+ const identity = identities[key]
+ const isSelected = identity.address === selectedAddress
+
+ const balanceValue = accounts[key] ? 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('div.account-menu__check-mark-icon') : 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', 'IMPORTED') : null
+ } catch (e) { return }
+}
diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js
new file mode 100644
index 000000000..d14aa675f
--- /dev/null
+++ b/ui/app/components/balance-component.js
@@ -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)
+}
diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js
index 15281171c..d5958787b 100644
--- a/ui/app/components/buy-button-subview.js
+++ b/ui/app/components/buy-button-subview.js
@@ -76,7 +76,7 @@ BuyButtonSubview.prototype.headerSubview = function () {
paddingTop: '4px',
paddingBottom: '4px',
},
- }, 'Buy Eth'),
+ }, 'Deposit Eth'),
]),
// loading indication
@@ -87,7 +87,7 @@ BuyButtonSubview.prototype.headerSubview = function () {
left: '49vw',
},
}, [
- h(Loading, { isLoading }),
+ isLoading && h(Loading),
]),
// account panel
@@ -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())
}
diff --git a/ui/app/components/currency-input.js b/ui/app/components/currency-input.js
new file mode 100644
index 000000000..66880091f
--- /dev/null
+++ b/ui/app/components/currency-input.js
@@ -0,0 +1,95 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+
+module.exports = CurrencyInput
+
+inherits(CurrencyInput, Component)
+function CurrencyInput (props) {
+ Component.call(this)
+
+ this.state = {
+ value: sanitizeValue(props.value),
+ }
+}
+
+function removeNonDigits (str) {
+ return str.match(/\d|$/g).join('')
+}
+
+// Removes characters that are not digits, then removes leading zeros
+function sanitizeInteger (val) {
+ return String(parseInt(removeNonDigits(val) || '0', 10))
+}
+
+function sanitizeDecimal (val) {
+ return removeNonDigits(val)
+}
+
+// Take a single string param and returns a non-negative integer or float as a string.
+// Breaks the input into three parts: the integer, the decimal point, and the decimal/fractional part.
+// Removes leading zeros from the integer, and non-digits from the integer and decimal
+// The integer is returned as '0' in cases where it would be empty. A decimal point is
+// included in the returned string if one is included in the param
+// Examples:
+// sanitizeValue('0') -> '0'
+// sanitizeValue('a') -> '0'
+// sanitizeValue('010.') -> '10.'
+// sanitizeValue('0.005') -> '0.005'
+// sanitizeValue('22.200') -> '22.200'
+// sanitizeValue('.200') -> '0.200'
+// sanitizeValue('a.b.1.c,89.123') -> '0.189123'
+function sanitizeValue (value) {
+ let [ , integer, point, decimal] = (/([^.]*)([.]?)([^.]*)/).exec(value)
+
+ integer = sanitizeInteger(integer) || '0'
+ decimal = sanitizeDecimal(decimal)
+
+ return `${integer}${point}${decimal}`
+}
+
+CurrencyInput.prototype.handleChange = function (newValue) {
+ const { onInputChange } = this.props
+
+ this.setState({ value: sanitizeValue(newValue) })
+
+ onInputChange(sanitizeValue(newValue))
+}
+
+// If state.value === props.value plus a decimal point, or at least one
+// zero or a decimal point and at least one zero, then this returns state.value
+// after it is sanitized with getValueParts
+CurrencyInput.prototype.getValueToRender = function () {
+ const { value } = this.props
+ const { value: stateValue } = this.state
+
+ const trailingStateString = (new RegExp(`^${value}(.+)`)).exec(stateValue)
+ const trailingDecimalAndZeroes = trailingStateString && (/^[.0]0*/).test(trailingStateString[1])
+
+ return sanitizeValue(trailingDecimalAndZeroes
+ ? stateValue
+ : value)
+}
+
+CurrencyInput.prototype.render = function () {
+ const {
+ className,
+ placeholder,
+ readOnly,
+ inputRef,
+ } = this.props
+
+ const inputSizeMultiplier = readOnly ? 1 : 1.2
+
+ const valueToRender = this.getValueToRender()
+
+ return h('input', {
+ className,
+ value: valueToRender,
+ placeholder,
+ size: valueToRender.length * inputSizeMultiplier,
+ readOnly,
+ onChange: e => this.handleChange(e.target.value),
+ ref: inputRef,
+ })
+}
diff --git a/ui/app/components/customize-gas-modal/gas-modal-card.js b/ui/app/components/customize-gas-modal/gas-modal-card.js
new file mode 100644
index 000000000..23754d819
--- /dev/null
+++ b/ui/app/components/customize-gas-modal/gas-modal-card.js
@@ -0,0 +1,54 @@
+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,
+ 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,
+ // }),
+
+ ])
+
+}
+
diff --git a/ui/app/components/customize-gas-modal/gas-slider.js b/ui/app/components/customize-gas-modal/gas-slider.js
new file mode 100644
index 000000000..69fd6f985
--- /dev/null
+++ b/ui/app/components/customize-gas-modal/gas-slider.js
@@ -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'),
+
+// ]),
+
+// ])
+
+// }
+
diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js
new file mode 100644
index 000000000..485dacf90
--- /dev/null
+++ b/ui/app/components/customize-gas-modal/index.js
@@ -0,0 +1,278 @@
+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 {
+ MIN_GAS_PRICE_DEC,
+ MIN_GAS_LIMIT_DEC,
+ MIN_GAS_PRICE_GWEI,
+} = require('../send/send-constants')
+
+const {
+ isBalanceSufficient,
+} = require('../send/send-utils')
+
+const {
+ conversionUtil,
+ multiplyCurrencies,
+ conversionGreaterThan,
+} = require('../../conversion-util')
+
+const {
+ getGasPrice,
+ getGasLimit,
+ conversionRateSelector,
+ getSendAmount,
+ getSelectedToken,
+ getSendFrom,
+ getCurrentAccountWithSendEtherInfo,
+ getSelectedTokenToFiatRate,
+} = require('../../selectors')
+
+function mapStateToProps (state) {
+ const selectedToken = getSelectedToken(state)
+ const currentAccount = getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state)
+ const conversionRate = conversionRateSelector(state)
+
+ return {
+ gasPrice: getGasPrice(state),
+ gasLimit: getGasLimit(state),
+ conversionRate,
+ amount: getSendAmount(state),
+ balance: currentAccount.balance,
+ primaryCurrency: selectedToken && selectedToken.symbol,
+ selectedToken,
+ amountConversionRate: selectedToken ? getSelectedTokenToFiatRate(state) : conversionRate,
+ }
+}
+
+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)),
+ }
+}
+
+function getOriginalState (props) {
+ const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC
+ const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC
+
+ const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+
+ return {
+ gasPrice,
+ gasLimit,
+ gasTotal,
+ error: null,
+ priceSigZeros: '',
+ priceSigDec: '',
+ }
+}
+
+inherits(CustomizeGasModal, Component)
+function CustomizeGasModal (props) {
+ Component.call(this)
+
+ this.state = getOriginalState(props)
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
+
+CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
+ const {
+ updateGasPrice,
+ updateGasLimit,
+ hideModal,
+ updateGasTotal,
+ } = this.props
+
+ updateGasPrice(gasPrice)
+ updateGasLimit(gasLimit)
+ updateGasTotal(gasTotal)
+ hideModal()
+}
+
+CustomizeGasModal.prototype.revert = function () {
+ this.setState(getOriginalState(this.props))
+}
+
+CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) {
+ const {
+ amount,
+ balance,
+ selectedToken,
+ amountConversionRate,
+ conversionRate,
+ } = this.props
+
+ let error = null
+
+ const balanceIsSufficient = isBalanceSufficient({
+ amount: selectedToken ? '0' : amount,
+ gasTotal,
+ balance,
+ selectedToken,
+ amountConversionRate,
+ conversionRate,
+ })
+
+ if (!balanceIsSufficient) {
+ error = 'Insufficient balance for current gas total'
+ }
+
+ const gasLimitTooLow = gasLimit && conversionGreaterThan(
+ {
+ value: MIN_GAS_LIMIT_DEC,
+ fromNumericBase: 'dec',
+ conversionRate,
+ },
+ {
+ value: gasLimit,
+ fromNumericBase: 'hex',
+ },
+ )
+
+ if (gasLimitTooLow) {
+ error = 'Gas limit must be at least 21000'
+ }
+
+ this.setState({ error })
+ return error
+}
+
+CustomizeGasModal.prototype.convertAndSetGasLimit = function (newGasLimit) {
+ const { gasPrice } = this.state
+
+ const gasLimit = conversionUtil(newGasLimit, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ })
+
+ const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+
+ this.validate({ gasTotal, gasLimit })
+
+ this.setState({ gasTotal, gasLimit })
+}
+
+CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
+ const { gasLimit } = this.state
+ const sigZeros = String(newGasPrice).match(/^\d+[.]\d*?(0+)$/)
+ const sigDec = String(newGasPrice).match(/^\d+([.])0*$/)
+
+ this.setState({
+ priceSigZeros: sigZeros && sigZeros[1] || '',
+ priceSigDec: sigDec && sigDec[1] || '',
+ })
+
+ const gasPrice = conversionUtil(newGasPrice, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ fromDenomination: 'GWEI',
+ toDenomination: 'WEI',
+ })
+
+ const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+
+ this.validate({ gasTotal })
+
+ this.setState({ gasTotal, gasPrice })
+}
+
+CustomizeGasModal.prototype.render = function () {
+ const { hideModal } = this.props
+ const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state
+
+ let convertedGasPrice = conversionUtil(gasPrice, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ toDenomination: 'GWEI',
+ })
+
+ convertedGasPrice += convertedGasPrice.match(/[.]/) ? priceSigZeros : `${priceSigDec}${priceSigZeros}`
+
+ const convertedGasLimit = conversionUtil(gasLimit, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+
+ return h('div.send-v2__customize-gas', {}, [
+ h('div.send-v2__customize-gas__content', {
+ }, [
+ 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: MIN_GAS_PRICE_GWEI,
+ // max: 1000,
+ step: multiplyCurrencies(MIN_GAS_PRICE_GWEI, 10),
+ onChange: value => this.convertAndSetGasPrice(value),
+ title: 'Gas Price (GWEI)',
+ copy: 'We calculate the suggested gas prices based on network success rates.',
+ }),
+
+ h(GasModalCard, {
+ value: convertedGasLimit,
+ min: 1,
+ // 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', {}, [
+
+ error && h('div.send-v2__customize-gas__error-message', [
+ error,
+ ]),
+
+ h('div.send-v2__customize-gas__revert', {
+ onClick: () => this.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${error ? '__error' : ''}`, {
+ onClick: () => !error && this.save(gasPrice, gasLimit, gasTotal),
+ }, ['SAVE']),
+ ]),
+
+ ]),
+
+ ]),
+ ])
+}
diff --git a/ui/app/components/dropdowns/account-dropdown-mini.js b/ui/app/components/dropdowns/account-dropdown-mini.js
new file mode 100644
index 000000000..a3d41af90
--- /dev/null
+++ b/ui/app/components/dropdowns/account-dropdown-mini.js
@@ -0,0 +1,75 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const AccountListItem = require('../send/account-list-item')
+
+module.exports = AccountDropdownMini
+
+inherits(AccountDropdownMini, Component)
+function AccountDropdownMini () {
+ Component.call(this)
+}
+
+AccountDropdownMini.prototype.getListItemIcon = function (currentAccount, selectedAccount) {
+ const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } })
+
+ return currentAccount.address === selectedAccount.address
+ ? listItemIcon
+ : null
+}
+
+AccountDropdownMini.prototype.renderDropdown = function () {
+ const {
+ accounts,
+ selectedAccount,
+ closeDropdown,
+ onSelect,
+ } = this.props
+
+ return h('div', {}, [
+
+ h('div.account-dropdown-mini__close-area', {
+ onClick: closeDropdown,
+ }),
+
+ h('div.account-dropdown-mini__list', {}, [
+
+ ...accounts.map(account => h(AccountListItem, {
+ account,
+ displayBalance: false,
+ displayAddress: false,
+ handleClick: () => {
+ onSelect(account)
+ closeDropdown()
+ },
+ icon: this.getListItemIcon(account, selectedAccount),
+ })),
+
+ ]),
+
+ ])
+}
+
+AccountDropdownMini.prototype.render = function () {
+ const {
+ selectedAccount,
+ openDropdown,
+ dropdownOpen,
+ } = this.props
+
+ return h('div.account-dropdown-mini', {}, [
+
+ h(AccountListItem, {
+ account: selectedAccount,
+ handleClick: openDropdown,
+ displayBalance: false,
+ displayAddress: false,
+ icon: h(`i.fa.fa-caret-down.fa-lg`, { style: { color: '#dedede' } }),
+ }),
+
+ dropdownOpen && this.renderDropdown(),
+
+ ])
+
+}
+
diff --git a/ui/app/components/dropdowns/account-options-dropdown.js b/ui/app/components/dropdowns/account-options-dropdown.js
new file mode 100644
index 000000000..f74c0a2d4
--- /dev/null
+++ b/ui/app/components/dropdowns/account-options-dropdown.js
@@ -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,
+ network,
+ identities,
+ style: style || {},
+ dropdownWrapperStyle: dropdownWrapperStyle || {},
+ menuItemStyles: menuItemStyles || {},
+ }, [])
+}
diff --git a/ui/app/components/dropdowns/account-selection-dropdown.js b/ui/app/components/dropdowns/account-selection-dropdown.js
new file mode 100644
index 000000000..2f6452b15
--- /dev/null
+++ b/ui/app/components/dropdowns/account-selection-dropdown.js
@@ -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,
+ network,
+ identities,
+ style: style || {},
+ dropdownWrapperStyle: dropdownWrapperStyle || {},
+ menuItemStyles: menuItemStyles || {},
+ }, [])
+}
diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js
new file mode 100644
index 000000000..58326b13c
--- /dev/null
+++ b/ui/app/components/dropdowns/components/account-dropdowns.js
@@ -0,0 +1,484 @@
+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,
+ accounts: PropTypes.object,
+ menuItemStyles: PropTypes.object,
+ actions: PropTypes.object,
+ // actions.showAccountDetail: ,
+ useCssTransition: PropTypes.bool,
+ innerStyle: PropTypes.object,
+ sidebarOpen: PropTypes.bool,
+ dropdownWrapperStyle: PropTypes.string,
+ // actions.showAccountDetailModal: ,
+ network: PropTypes.number,
+ // actions.showExportPrivateKeyModal: ,
+ style: PropTypes.object,
+ enableAccountsSelector: PropTypes.bool,
+ enableAccountOption: PropTypes.bool,
+ enableAccountOptions: PropTypes.bool,
+}
+
+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)
+
diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdowns/components/dropdown.js
index cdd864cc3..15d064be8 100644
--- a/ui/app/components/dropdown.js
+++ b/ui/app/components/dropdowns/components/dropdown.js
@@ -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: 55,
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,
@@ -55,6 +68,7 @@ Dropdown.propTypes = {
onClickOutside: PropTypes.func,
innerStyle: PropTypes.object,
useCssTransition: PropTypes.bool,
+ containerClassName: PropTypes.string,
}
class DropdownMenuItem extends Component {
@@ -70,7 +84,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',
@@ -78,6 +92,7 @@ class DropdownMenuItem extends Component {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
+ color: 'white',
}, style),
},
children
diff --git a/ui/app/components/dropdowns/components/menu.js b/ui/app/components/dropdowns/components/menu.js
new file mode 100644
index 000000000..f6d8a139e
--- /dev/null
+++ b/ui/app/components/dropdowns/components/menu.js
@@ -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 }
diff --git a/ui/app/components/dropdowns/components/network-dropdown-icon.js b/ui/app/components/dropdowns/components/network-dropdown-icon.js
new file mode 100644
index 000000000..7e94e0af5
--- /dev/null
+++ b/ui/app/components/dropdowns/components/network-dropdown-icon.js
@@ -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,
+ },
+ })
+ )
+}
diff --git a/ui/app/components/dropdowns/index.js b/ui/app/components/dropdowns/index.js
new file mode 100644
index 000000000..fa66f5000
--- /dev/null
+++ b/ui/app/components/dropdowns/index.js
@@ -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,
+}
diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js
new file mode 100644
index 000000000..0908faf01
--- /dev/null
+++ b/ui/app/components/dropdowns/network-dropdown.js
@@ -0,0 +1,317 @@
+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: 55,
+ style: {
+ position: 'absolute',
+ top: '58px',
+ minWidth: '309px',
+ zIndex: '55px',
+ },
+ 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', '✓'),
+ ]
+ )
+ }
+}
diff --git a/ui/app/components/dropdowns/simple-dropdown.js b/ui/app/components/dropdowns/simple-dropdown.js
new file mode 100644
index 000000000..7bb48e57b
--- /dev/null
+++ b/ui/app/components/dropdowns/simple-dropdown.js
@@ -0,0 +1,92 @@
+const { Component } = require('react')
+const PropTypes = require('react').PropTypes
+const h = require('react-hyperscript')
+const classnames = require('classnames')
+const R = require('ramda')
+
+class SimpleDropdown extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ isOpen: false,
+ }
+ }
+
+ getDisplayValue () {
+ const { selectedOption, options } = this.props
+ const matchesOption = option => option.value === selectedOption
+ const matchingOption = R.find(matchesOption)(options)
+ return matchingOption
+ ? matchingOption.displayValue || matchingOption.value
+ : selectedOption
+ }
+
+ handleClose () {
+ this.setState({ isOpen: false })
+ }
+
+ toggleOpen () {
+ const { isOpen } = this.state
+ this.setState({ isOpen: !isOpen })
+ }
+
+ renderOptions () {
+ const { options, onSelect, selectedOption } = this.props
+
+ return h('div', [
+ h('div.simple-dropdown__close-area', {
+ onClick: event => {
+ event.stopPropagation()
+ this.handleClose()
+ },
+ }),
+ h('div.simple-dropdown__options', [
+ ...options.map(option => {
+ return h(
+ 'div.simple-dropdown__option',
+ {
+ className: classnames({
+ 'simple-dropdown__option--selected': option.value === selectedOption,
+ }),
+ key: option.value,
+ onClick: () => {
+ if (option.value !== selectedOption) {
+ onSelect(option.value)
+ }
+
+ this.handleClose()
+ },
+ },
+ option.displayValue || option.value,
+ )
+ }),
+ ]),
+ ])
+ }
+
+ render () {
+ const { placeholder } = this.props
+ const { isOpen } = this.state
+
+ return h(
+ 'div.simple-dropdown',
+ {
+ onClick: () => this.toggleOpen(),
+ },
+ [
+ h('div.simple-dropdown__selected', this.getDisplayValue() || placeholder || 'Select'),
+ h('i.fa.fa-caret-down.fa-lg.simple-dropdown__caret'),
+ isOpen && this.renderOptions(),
+ ]
+ )
+ }
+}
+
+SimpleDropdown.propTypes = {
+ options: PropTypes.array.isRequired,
+ placeholder: PropTypes.string,
+ onSelect: PropTypes.func,
+ selectedOption: PropTypes.string,
+}
+
+module.exports = SimpleDropdown
diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js
new file mode 100644
index 000000000..dc7a985e3
--- /dev/null
+++ b/ui/app/components/dropdowns/token-menu-dropdown.js
@@ -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'),
+
+ ]),
+ ]),
+ ])
+}
diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js
index 8a5c8954f..eb41ec50c 100644
--- a/ui/app/components/editable-label.js
+++ b/ui/app/components/editable-label.js
@@ -1,57 +1,88 @@
-const Component = require('react').Component
+const { Component } = require('react')
+const PropTypes = require('prop-types')
const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const findDOMNode = require('react-dom').findDOMNode
+const classnames = require('classnames')
-module.exports = EditableLabel
+class EditableLabel extends Component {
+ constructor (props) {
+ super(props)
-inherits(EditableLabel, Component)
-function EditableLabel () {
- Component.call(this)
-}
+ this.state = {
+ isEditing: false,
+ value: props.defaultValue || '',
+ }
+ }
+
+ handleSubmit () {
+ const { value } = this.state
+
+ if (value === '') {
+ return
+ }
+
+ Promise.resolve(this.props.onSubmit(value))
+ .then(() => this.setState({ isEditing: false }))
+ }
+
+ saveIfEnter (event) {
+ if (event.key === 'Enter') {
+ this.handleSubmit()
+ }
+ }
-EditableLabel.prototype.render = function () {
- const props = this.props
- const state = this.state
+ renderEditing () {
+ const { value } = this.state
- if (state && state.isEditingLabel) {
- return h('div.editable-label', [
- h('input.sizing-input', {
- defaultValue: props.textValue,
- maxLength: '20',
+ return ([
+ h('input.large-input.editable-label__input', {
+ type: 'text',
+ required: true,
+ value: this.state.value,
onKeyPress: (event) => {
- this.saveIfEnter(event)
+ if (event.key === 'Enter') {
+ this.handleSubmit()
+ }
},
+ onChange: event => this.setState({ value: event.target.value }),
+ className: classnames({ 'editable-label__input--error': value === '' }),
}),
- h('button.editable-button', {
- onClick: () => this.saveText(),
- }, 'Save'),
+ h('div.editable-label__icon-wrapper', [
+ h('i.fa.fa-check.editable-label__icon', {
+ onClick: () => this.handleSubmit(),
+ }),
+ ]),
])
- } else {
- return h('div.name-label', {
- onClick: (event) => {
- const nameAttribute = event.target.getAttribute('name')
- // checks for class to handle smaller CTA above the account name
- const classAttribute = event.target.getAttribute('class')
- if (nameAttribute === 'edit' || classAttribute === 'edit-text') {
- this.setState({ isEditingLabel: true })
- }
- },
- }, this.props.children)
}
-}
-EditableLabel.prototype.saveIfEnter = function (event) {
- if (event.key === 'Enter') {
- this.saveText()
+ renderReadonly () {
+ return ([
+ h('div.editable-label__value', this.state.value),
+ h('div.editable-label__icon-wrapper', [
+ h('i.fa.fa-pencil.editable-label__icon', {
+ onClick: () => this.setState({ isEditing: true }),
+ }),
+ ]),
+ ])
+ }
+
+ render () {
+ const { isEditing } = this.state
+ const { className } = this.props
+
+ return (
+ h('div.editable-label', { className: classnames(className) },
+ isEditing
+ ? this.renderEditing()
+ : this.renderReadonly()
+ )
+ )
}
}
-EditableLabel.prototype.saveText = function () {
- // eslint-disable-next-line react/no-find-dom-node
- var container = findDOMNode(this)
- var text = container.querySelector('.editable-label input').value
- var truncatedText = text.substring(0, 20)
- this.props.saveText(truncatedText)
- this.setState({ isEditingLabel: false, textLabel: truncatedText })
+EditableLabel.propTypes = {
+ onSubmit: PropTypes.func.isRequired,
+ defaultValue: PropTypes.string,
+ className: PropTypes.string,
}
+
+module.exports = EditableLabel
diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js
index c85a23514..6553053f7 100644
--- a/ui/app/components/ens-input.js
+++ b/ui/app/components/ens-input.js
@@ -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',
diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js
index 4f538fd31..1be8c9731 100644
--- a/ui/app/components/eth-balance.js
+++ b/ui/app/components/eth-balance.js
@@ -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
- var label = balanceObj.label
+ 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}`,
+ }
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', {
- style: {
- width: '100%',
- textAlign: 'right',
- },
- }, incoming ? `+${balance}` : balance),
- h('div', {
+ h(hideTooltip ? 'div' : Tooltip,
+ containerProps,
+ h('div.flex-column', [
+ h('.flex-row', {
style: {
- color: ' #AEAEAE',
- fontSize: '12px',
- marginLeft: '5px',
+ alignItems: 'flex-end',
+ lineHeight: lineHeight || '13px',
+ fontFamily: fontFamily || 'Montserrat Light',
+ textRendering: 'geometricPrecision',
},
- }, 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,
+ ])
+ )
)
}
diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js
index d69f41d11..56465fc9d 100644
--- a/ui/app/components/fiat-value.js
+++ b/ui/app/components/fiat-value.js
@@ -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),
])
diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js
index bb476ca7b..b803b7ceb 100644
--- a/ui/app/components/identicon.js
+++ b/ui/app/components/identicon.js
@@ -1,13 +1,15 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
+const connect = require('react-redux').connect
const isNode = require('detect-node')
const findDOMNode = require('react-dom').findDOMNode
const jazzicon = require('jazzicon')
const iconFactoryGen = require('../../lib/icon-factory')
const iconFactory = iconFactoryGen(jazzicon)
+const { toDataUrl } = require('../../lib/blockies')
-module.exports = IdenticonComponent
+module.exports = connect(mapStateToProps)(IdenticonComponent)
inherits(IdenticonComponent, Component)
function IdenticonComponent () {
@@ -16,59 +18,100 @@ function IdenticonComponent () {
this.defaultDiameter = 46
}
+function mapStateToProps (state) {
+ return {
+ useBlockie: state.metamask.useBlockie,
+ }
+}
+
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 () {
var props = this.props
- const { address } = props
+ const { address, useBlockie } = props
if (!address) return
- // eslint-disable-next-line react/no-find-dom-node
- var container = findDOMNode(this)
-
- var diameter = props.diameter || this.defaultDiameter
if (!isNode) {
- var img = iconFactory.iconForAddress(address, diameter)
- container.appendChild(img)
+ // eslint-disable-next-line react/no-find-dom-node
+ var container = findDOMNode(this)
+
+ const diameter = props.diameter || this.defaultDiameter
+
+ if (useBlockie) {
+ _generateBlockie(container, address, diameter)
+ } else {
+ _generateJazzicon(container, address, diameter)
+ }
}
}
IdenticonComponent.prototype.componentDidUpdate = function () {
var props = this.props
- const { address } = props
+ const { address, useBlockie } = props
if (!address) return
- // eslint-disable-next-line react/no-find-dom-node
- var container = findDOMNode(this)
+ if (!isNode) {
+ // eslint-disable-next-line react/no-find-dom-node
+ var container = findDOMNode(this)
- var children = container.children
- for (var i = 0; i < children.length; i++) {
- container.removeChild(children[i])
- }
+ var children = container.children
+ for (var i = 0; i < children.length; i++) {
+ container.removeChild(children[i])
+ }
- var diameter = props.diameter || this.defaultDiameter
- if (!isNode) {
- var img = iconFactory.iconForAddress(address, diameter)
- container.appendChild(img)
+ const diameter = props.diameter || this.defaultDiameter
+
+ if (useBlockie) {
+ _generateBlockie(container, address, diameter)
+ } else {
+ _generateJazzicon(container, address, diameter)
+ }
}
}
+function _generateBlockie (container, address, diameter) {
+ const img = new Image()
+ img.src = toDataUrl(address)
+ const dia = !diameter || diameter < 50 ? 50 : diameter
+ img.height = dia * 1.25
+ img.width = dia * 1.25
+ container.appendChild(img)
+}
+
+function _generateJazzicon (container, address, diameter) {
+ const img = iconFactory.iconForAddress(address, diameter)
+ container.appendChild(img)
+}
diff --git a/ui/app/components/input-number.js b/ui/app/components/input-number.js
new file mode 100644
index 000000000..fd8c5c309
--- /dev/null
+++ b/ui/app/components/input-number.js
@@ -0,0 +1,73 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const CurrencyInput = require('./currency-input')
+const {
+ addCurrencies,
+ conversionGTE,
+ conversionLTE,
+ subtractCurrencies,
+} = require('../conversion-util')
+
+module.exports = InputNumber
+
+inherits(InputNumber, Component)
+function InputNumber () {
+ Component.call(this)
+
+ this.setValue = this.setValue.bind(this)
+}
+
+function isValidInput (text) {
+ const re = /^([1-9]\d*|0)(\.|\.\d*)?$/
+ return re.test(text)
+}
+
+InputNumber.prototype.setValue = function (newValue) {
+ if (newValue && !isValidInput(newValue)) return
+ const { fixed, min = -1, max = Infinity, onChange } = this.props
+
+ newValue = fixed ? newValue.toFixed(4) : newValue
+
+ const newValueGreaterThanMin = conversionGTE(
+ { value: newValue || '0', fromNumericBase: 'dec' },
+ { value: min, fromNumericBase: 'hex' },
+ )
+
+ const newValueLessThanMax = conversionLTE(
+ { value: newValue || '0', fromNumericBase: 'dec' },
+ { value: max, fromNumericBase: 'hex' },
+ )
+ if (newValueGreaterThanMin && newValueLessThanMax) {
+ onChange(newValue)
+ } else if (!newValueGreaterThanMin) {
+ onChange(min)
+ } else if (!newValueLessThanMax) {
+ onChange(max)
+ }
+}
+
+InputNumber.prototype.render = function () {
+ const { unitLabel, step = 1, placeholder, value = 0 } = this.props
+
+ return h('div.customize-gas-input-wrapper', {}, [
+ h(CurrencyInput, {
+ className: 'customize-gas-input',
+ value,
+ placeholder,
+ onInputChange: newValue => {
+ this.setValue(newValue)
+ },
+ }),
+ 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(subtractCurrencies(value, step)),
+ }),
+ ]),
+ ])
+}
diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js
index 163792584..9442121fe 100644
--- a/ui/app/components/loading.js
+++ b/ui/app/components/loading.js
@@ -1,45 +1,30 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
+const { Component } = require('react')
const h = require('react-hyperscript')
-
-
-inherits(LoadingIndicator, Component)
-module.exports = LoadingIndicator
-
-function LoadingIndicator () {
- Component.call(this)
+const PropTypes = require('react').PropTypes
+
+class LoadingIndicator extends Component {
+ renderMessage () {
+ const { loadingMessage } = this.props
+ return loadingMessage && h('span', loadingMessage)
+ }
+
+ render () {
+ return (
+ h('.full-flex-height.loading-overlay', {}, [
+ h('img', {
+ src: 'images/loading.svg',
+ }),
+
+ h('br'),
+
+ this.renderMessage(),
+ ])
+ )
+ }
}
-LoadingIndicator.prototype.render = function () {
- const { isLoading, loadingMessage } = this.props
-
- return (
- isLoading ? h('.full-flex-height', {
- style: {
- left: '0px',
- zIndex: 10,
- position: 'absolute',
- flexDirection: 'column',
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- height: '100%',
- width: '100%',
- background: 'rgba(255, 255, 255, 0.8)',
- },
- }, [
- h('img', {
- src: 'images/loading.svg',
- }),
-
- h('br'),
-
- showMessageIfAny(loadingMessage),
- ]) : null
- )
+LoadingIndicator.propTypes = {
+ loadingMessage: PropTypes.string,
}
-function showMessageIfAny (loadingMessage) {
- if (!loadingMessage) return null
- return h('span', loadingMessage)
-}
+module.exports = LoadingIndicator
diff --git a/ui/app/components/menu-droppo.js b/ui/app/components/menu-droppo.js
index e6276f3b1..c80bee2be 100644
--- a/ui/app/components/menu-droppo.js
+++ b/ui/app/components/menu-droppo.js
@@ -13,6 +13,7 @@ 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
@@ -26,8 +27,9 @@ MenuDroppoComponent.prototype.render = function () {
style.zIndex = zIndex
return (
- h('.menu-droppo-container', {
+ h('div', {
style,
+ className: `.menu-droppo-container ${containerClassName}`,
}, [
h('style', `
.menu-droppo-enter {
diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js
new file mode 100644
index 000000000..4bf671834
--- /dev/null
+++ b/ui/app/components/modals/account-details-modal.js
@@ -0,0 +1,75 @@
+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 } = require('../../selectors')
+const genAccountLink = require('../../../lib/account-link.js')
+const QrView = require('../qr-code')
+const EditableLabel = require('../editable-label')
+
+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()),
+ saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)),
+ }
+}
+
+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,
+ saveAccountLabel,
+ } = this.props
+ const { name, address } = selectedIdentity
+
+ return h(AccountModalContainer, {}, [
+ h(EditableLabel, {
+ className: 'account-modal__name',
+ defaultValue: name,
+ onSubmit: label => saveAccountLabel(address, label),
+ }),
+
+ h(QrView, {
+ Qr: {
+ 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'),
+
+ ])
+}
diff --git a/ui/app/components/modals/account-modal-container.js b/ui/app/components/modals/account-modal-container.js
new file mode 100644
index 000000000..c548fb7b3
--- /dev/null
+++ b/ui/app/components/modals/account-modal-container.js
@@ -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,
+
+ ]),
+ ])
+}
diff --git a/ui/app/components/modals/buy-options-modal.js b/ui/app/components/modals/buy-options-modal.js
new file mode 100644
index 000000000..d735983f9
--- /dev/null
+++ b/ui/app/components/modals/buy-options-modal.js
@@ -0,0 +1,95 @@
+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 networkNames = require('../../../../app/scripts/config.js').networkNames
+
+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' }))
+ },
+ toFaucet: network => dispatch(actions.buyEth({ network })),
+ }
+}
+
+inherits(BuyOptions, Component)
+function BuyOptions () {
+ Component.call(this)
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(BuyOptions)
+
+BuyOptions.prototype.renderModalContentOption = function (title, header, onClick) {
+ return h('div.buy-modal-content-option', {
+ onClick,
+ }, [
+ h('div.buy-modal-content-option-title', {}, title),
+ h('div.buy-modal-content-option-subtitle', {}, header),
+ ])
+}
+
+BuyOptions.prototype.render = function () {
+ const { network, toCoinbase, address, toFaucet } = this.props
+ const isTestNetwork = ['3', '4', '42'].find(n => n === network)
+ const networkName = networkNames[network]
+
+ 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 deposit Ether?'),
+ ]),
+
+ h('div.buy-modal-content-options.flex-column.flex-center', {}, [
+
+ isTestNetwork
+ ? this.renderModalContentOption(networkName, 'Test Faucet', () => toFaucet(network))
+ : this.renderModalContentOption('Coinbase', 'Deposit with Fiat', () => toCoinbase(address)),
+
+ // 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'),
+ // ]),
+
+ this.renderModalContentOption(
+ 'Direct Deposit',
+ 'Deposit from another account',
+ () => this.goToAccountDetailsModal()
+ ),
+
+ ]),
+
+ 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()
+}
diff --git a/ui/app/components/modals/edit-account-name-modal.js b/ui/app/components/modals/edit-account-name-modal.js
new file mode 100644
index 000000000..e2361140d
--- /dev/null
+++ b/ui/app/components/modals/edit-account-name-modal.js
@@ -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',
+ ]),
+
+ ]),
+ ])
+}
diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js
new file mode 100644
index 000000000..193755df5
--- /dev/null
+++ b/ui/app/components/modals/export-private-key-modal.js
@@ -0,0 +1,137 @@
+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',
+ 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), 'Show')
+ ),
+
+ ])
+}
+
+ExportPrivateKeyModal.prototype.render = function () {
+ const {
+ selectedIdentity,
+ 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', 'Show 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),
+
+ ])
+}
diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/modals/hide-token-confirmation-modal.js
new file mode 100644
index 000000000..fa3ad0b1e
--- /dev/null
+++ b/ui/app/components/modals/hide-token-confirmation-modal.js
@@ -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',
+ ]),
+ ]),
+ ]),
+ ])
+}
diff --git a/ui/app/components/modals/index.js b/ui/app/components/modals/index.js
new file mode 100644
index 000000000..1db1d33d4
--- /dev/null
+++ b/ui/app/components/modals/index.js
@@ -0,0 +1,5 @@
+const Modal = require('./modal')
+
+module.exports = {
+ Modal,
+}
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
new file mode 100644
index 000000000..f2909f3c3
--- /dev/null
+++ b/ui/app/components/modals/modal.js
@@ -0,0 +1,262 @@
+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: '100vw',
+ height: '100vh',
+ top: '0',
+ 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(0, 0, 0, 0.5)',
+}
+
+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()
+}
diff --git a/ui/app/components/modals/new-account-modal.js b/ui/app/components/modals/new-account-modal.js
new file mode 100644
index 000000000..fc1fd413d
--- /dev/null
+++ b/ui/app/components/modals/new-account-modal.js
@@ -0,0 +1,106 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const { connect } = require('react-redux')
+const actions = require('../../actions')
+
+class NewAccountModal extends Component {
+ constructor (props) {
+ super(props)
+ const { numberOfExistingAccounts = 0 } = props
+ const newAccountNumber = numberOfExistingAccounts + 1
+
+ this.state = {
+ newAccountName: `Account ${newAccountNumber}`,
+ }
+ }
+
+ render () {
+ 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', {
+ value: this.state.newAccountName,
+ 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.pointer', {
+ onClick: () => {
+ this.props.hideModal()
+ this.props.showImportPage()
+ },
+ }, 'Import an account'),
+
+ h('div.new-account-modal-content.button', {}, [
+ h('button.btn-clear', {
+ onClick: () => this.props.createAccount(newAccountName),
+ }, [
+ 'SAVE',
+ ]),
+ ]),
+ ]),
+ ])
+ }
+}
+
+NewAccountModal.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: PropTypes.func,
+ createAccount: PropTypes.func,
+ numberOfExistingAccounts: PropTypes.number,
+}
+
+const mapStateToProps = state => {
+ const { metamask: { network, selectedAddress, identities = {} } } = state
+ const numberOfExistingAccounts = Object.keys(identities).length
+
+ return {
+ network,
+ address: selectedAddress,
+ numberOfExistingAccounts,
+ }
+}
+
+const 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())
+ })
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountModal)
diff --git a/ui/app/components/modals/shapeshift-deposit-tx-modal.js b/ui/app/components/modals/shapeshift-deposit-tx-modal.js
new file mode 100644
index 000000000..24af5a0de
--- /dev/null
+++ b/ui/app/components/modals/shapeshift-deposit-tx-modal.js
@@ -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}),
+ ]),
+ ])
+}
diff --git a/ui/app/components/network.js b/ui/app/components/network.js
index 0dbe37cdd..915818009 100644
--- a/ui/app/components/network.js
+++ b/ui/app/components/network.js
@@ -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,15 +62,29 @@ Network.prototype.render = function () {
}
return (
- h('#network_component.pointer', {
+ h('div.network-component.pointer', {
+ className: classnames('network-component pointer', {
+ 'network-component--disabled': this.props.disabled,
+ '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),
+ onClick: (event) => {
+ if (!this.props.disabled) {
+ this.props.onClick(event)
+ }
+ },
}, [
(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 +94,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 +107,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 +120,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',
diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js
index 09d461c7b..941ac33e6 100644
--- a/ui/app/components/notice.js
+++ b/ui/app/components/notice.js
@@ -102,7 +102,7 @@ Notice.prototype.render = function () {
}),
]),
- h('button', {
+ h('button.primary', {
disabled,
onClick: () => {
this.setState({disclaimerDisabled: true})
diff --git a/ui/app/components/pending-personal-msg.js b/ui/app/components/pending-personal-msg.js
deleted file mode 100644
index 4542adb28..000000000
--- a/ui/app/components/pending-personal-msg.js
+++ /dev/null
@@ -1,47 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const PendingTxDetails = require('./pending-personal-msg-details')
-
-module.exports = PendingMsg
-
-inherits(PendingMsg, Component)
-function PendingMsg () {
- Component.call(this)
-}
-
-PendingMsg.prototype.render = function () {
- var state = this.props
- var msgData = state.txData
-
- return (
-
- h('div', {
- key: msgData.id,
- }, [
-
- // header
- h('h3', {
- style: {
- fontWeight: 'bold',
- textAlign: 'center',
- },
- }, 'Sign Message'),
-
- // message details
- h(PendingTxDetails, state),
-
- // sign + cancel
- h('.flex-row.flex-space-around', [
- h('button', {
- onClick: state.cancelPersonalMessage,
- }, 'Cancel'),
- h('button', {
- onClick: state.signPersonalMessage,
- }, 'Sign'),
- ]),
- ])
-
- )
-}
-
diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js
deleted file mode 100644
index 5b1b367c6..000000000
--- a/ui/app/components/pending-tx.js
+++ /dev/null
@@ -1,500 +0,0 @@
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const actions = require('../actions')
-const clone = require('clone')
-
-const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const hexToBn = require('../../../app/scripts/lib/hex-to-bn')
-const util = require('../util')
-const MiniAccountPanel = require('./mini-account-panel')
-const Copyable = require('./copyable')
-const EthBalance = require('./eth-balance')
-const addressSummary = util.addressSummary
-const nameForAddress = require('../../lib/contract-namer')
-const BNInput = require('./bn-as-decimal-input')
-
-// corresponds with 0.1 GWEI
-const MIN_GAS_PRICE_BN = new BN('100000000')
-const MIN_GAS_LIMIT_BN = new BN('21000')
-
-module.exports = PendingTx
-inherits(PendingTx, Component)
-function PendingTx () {
- Component.call(this)
- this.state = {
- valid: true,
- txData: null,
- submitting: false,
- }
-}
-
-PendingTx.prototype.render = function () {
- const props = this.props
- const { currentCurrency, blockGasLimit } = props
-
- const conversionRate = props.conversionRate
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- // Account Details
- const address = txParams.from || props.selectedAddress
- const identity = props.identities[address] || { address: address }
- const account = props.accounts[address]
- const balance = account ? account.balance : '0x0'
-
- // recipient check
- const isValidAddress = !txParams.to || util.isValidAddress(txParams.to)
-
- // Gas
- const gas = txParams.gas
- const gasBn = hexToBn(gas)
- const gasLimit = new BN(parseInt(blockGasLimit))
- const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20)
- const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20)
- const safeGasLimit = safeGasLimitBN.toString(10)
-
- // Gas Price
- const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16)
- const gasPriceBn = hexToBn(gasPrice)
-
- const txFeeBn = gasBn.mul(gasPriceBn)
- const valueBn = hexToBn(txParams.value)
- const maxCost = txFeeBn.add(valueBn)
-
- const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0
-
- const balanceBn = hexToBn(balance)
- const insufficientBalance = balanceBn.lt(maxCost)
- const dangerousGasLimit = gasBn.gte(saferGasLimitBN)
- const gasLimitSpecified = txMeta.gasLimitSpecified
- const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
- const showRejectAll = props.unconfTxListLength > 1
-
- this.inputs = []
-
- return (
-
- h('div', {
- key: txMeta.id,
- }, [
-
- h('form#pending-tx-form', {
- onSubmit: this.onSubmit.bind(this),
-
- }, [
-
- // tx info
- h('div', [
-
- h('.flex-row.flex-center', {
- style: {
- maxWidth: '100%',
- },
- }, [
-
- h(MiniAccountPanel, {
- imageSeed: address,
- picOrder: 'right',
- }, [
- h('span.font-small', {
- style: {
- fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
- },
- }, identity.name),
-
- h(Copyable, {
- value: ethUtil.toChecksumAddress(address),
- }, [
- h('span.font-small', {
- style: {
- fontFamily: 'Montserrat Light, Montserrat, sans-serif',
- },
- }, addressSummary(address, 6, 4, false)),
- ]),
-
- h('span.font-small', {
- style: {
- fontFamily: 'Montserrat Light, Montserrat, sans-serif',
- },
- }, [
- h(EthBalance, {
- value: balance,
- conversionRate,
- currentCurrency,
- inline: true,
- labelColor: '#F7861C',
- }),
- ]),
- ]),
-
- forwardCarrat(),
-
- this.miniAccountPanelForRecipient(),
- ]),
-
- h('style', `
- .table-box {
- margin: 7px 0px 0px 0px;
- width: 100%;
- }
- .table-box .row {
- margin: 0px;
- background: rgb(236,236,236);
- display: flex;
- justify-content: space-between;
- font-family: Montserrat Light, sans-serif;
- font-size: 13px;
- padding: 5px 25px;
- }
- .table-box .row .value {
- font-family: Montserrat Regular;
- }
- `),
-
- h('.table-box', [
-
- // Ether Value
- // Currently not customizable, but easily modified
- // in the way that gas and gasLimit currently are.
- h('.row', [
- h('.cell.label', 'Amount'),
- h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }),
- ]),
-
- // Gas Limit (customizable)
- h('.cell.row', [
- h('.cell.label', 'Gas Limit'),
- h('.cell.value', {
- }, [
- h(BNInput, {
- name: 'Gas Limit',
- value: gasBn,
- precision: 0,
- scale: 0,
- // The hard lower limit for gas.
- min: MIN_GAS_LIMIT_BN,
- max: safeGasLimit,
- suffix: 'UNITS',
- style: {
- position: 'relative',
- top: '5px',
- },
- onChange: this.gasLimitChanged.bind(this),
-
- ref: (hexInput) => { this.inputs.push(hexInput) },
- }),
- ]),
- ]),
-
- // Gas Price (customizable)
- h('.cell.row', [
- h('.cell.label', 'Gas Price'),
- h('.cell.value', {
- }, [
- h(BNInput, {
- name: 'Gas Price',
- value: gasPriceBn,
- precision: 9,
- scale: 9,
- suffix: 'GWEI',
- min: MIN_GAS_PRICE_BN,
- style: {
- position: 'relative',
- top: '5px',
- },
- onChange: this.gasPriceChanged.bind(this),
- ref: (hexInput) => { this.inputs.push(hexInput) },
- }),
- ]),
- ]),
-
- // Max Transaction Fee (calculated)
- h('.cell.row', [
- h('.cell.label', 'Max Transaction Fee'),
- h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }),
- ]),
-
- h('.cell.row', {
- style: {
- fontFamily: 'Montserrat Regular',
- background: 'white',
- padding: '10px 25px',
- },
- }, [
- h('.cell.label', 'Max Total'),
- h('.cell.value', {
- style: {
- display: 'flex',
- alignItems: 'center',
- },
- }, [
- h(EthBalance, {
- value: maxCost.toString(16),
- currentCurrency,
- conversionRate,
- inline: true,
- labelColor: 'black',
- fontSize: '16px',
- }),
- ]),
- ]),
-
- // Data size row:
- h('.cell.row', {
- style: {
- background: '#f7f7f7',
- paddingBottom: '0px',
- },
- }, [
- h('.cell.label'),
- h('.cell.value', {
- style: {
- fontFamily: 'Montserrat Light',
- fontSize: '11px',
- },
- }, `Data included: ${dataLength} bytes`),
- ]),
- ]), // End of Table
-
- ]),
-
- h('style', `
- .conf-buttons button {
- margin-left: 10px;
- text-transform: uppercase;
- }
- `),
- h('.cell.row', {
- style: {
- textAlign: 'center',
- },
- }, [
- txMeta.simulationFails ?
- h('.error', {
- style: {
- fontSize: '0.9em',
- },
- }, 'Transaction Error. Exception thrown in contract code.')
- : null,
-
- !isValidAddress ?
- h('.error', {
- style: {
- fontSize: '0.9em',
- },
- }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
- : null,
-
- insufficientBalance ?
- h('span.error', {
- style: {
- fontSize: '0.9em',
- },
- }, 'Insufficient balance for transaction')
- : null,
-
- (dangerousGasLimit && !gasLimitSpecified) ?
- h('span.error', {
- style: {
- fontSize: '0.9em',
- },
- }, 'Gas limit set dangerously high. Approving this transaction is likely to fail.')
- : null,
- ]),
-
-
- // send + cancel
- h('.flex-row.flex-space-around.conf-buttons', {
- style: {
- display: 'flex',
- justifyContent: 'flex-end',
- margin: '14px 25px',
- },
- }, [
- h('button', {
- onClick: (event) => {
- this.resetGasFields()
- event.preventDefault()
- },
- }, 'Reset'),
-
- // Accept Button or Buy Button
- insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') :
- h('input.confirm.btn-green', {
- type: 'submit',
- value: 'SUBMIT',
- style: { marginLeft: '10px' },
- disabled: buyDisabled,
- }),
-
- h('button.cancel.btn-red', {
- onClick: props.cancelTransaction,
- }, 'Reject'),
- ]),
- showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', {
- style: {
- display: 'flex',
- justifyContent: 'flex-end',
- margin: '14px 25px',
- },
- }, [
- h('button.cancel.btn-red', {
- onClick: props.cancelAllTransactions,
- }, 'Reject All'),
- ]) : null,
- ]),
- ])
- )
-}
-
-PendingTx.prototype.miniAccountPanelForRecipient = function () {
- const props = this.props
- const txData = props.txData
- const txParams = txData.txParams || {}
- const isContractDeploy = !('to' in txParams)
-
- // If it's not a contract deploy, send to the account
- if (!isContractDeploy) {
- return h(MiniAccountPanel, {
- imageSeed: txParams.to,
- picOrder: 'left',
- }, [
-
- h('span.font-small', {
- style: {
- fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
- },
- }, nameForAddress(txParams.to, props.identities)),
-
- h(Copyable, {
- value: ethUtil.toChecksumAddress(txParams.to),
- }, [
- h('span.font-small', {
- style: {
- fontFamily: 'Montserrat Light, Montserrat, sans-serif',
- },
- }, addressSummary(txParams.to, 6, 4, false)),
- ]),
-
- ])
- } else {
- return h(MiniAccountPanel, {
- picOrder: 'left',
- }, [
-
- h('span.font-small', {
- style: {
- fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
- },
- }, 'New Contract'),
-
- ])
- }
-}
-
-PendingTx.prototype.gasPriceChanged = function (newBN, valid) {
- log.info(`Gas price changed to: ${newBN.toString(10)}`)
- const txMeta = this.gatherTxMeta()
- txMeta.txParams.gasPrice = '0x' + newBN.toString('hex')
- this.setState({
- txData: clone(txMeta),
- valid,
- })
-}
-
-PendingTx.prototype.gasLimitChanged = function (newBN, valid) {
- log.info(`Gas limit changed to ${newBN.toString(10)}`)
- const txMeta = this.gatherTxMeta()
- txMeta.txParams.gas = '0x' + newBN.toString('hex')
- this.setState({
- txData: clone(txMeta),
- valid,
- })
-}
-
-PendingTx.prototype.resetGasFields = function () {
- log.debug(`pending-tx resetGasFields`)
-
- this.inputs.forEach((hexInput) => {
- if (hexInput) {
- hexInput.setValid()
- }
- })
-
- this.setState({
- txData: null,
- valid: true,
- })
-}
-
-PendingTx.prototype.onSubmit = function (event) {
- event.preventDefault()
- const txMeta = this.gatherTxMeta()
- const valid = this.checkValidity()
- this.setState({ valid, submitting: true })
- if (valid && this.verifyGasParams()) {
- this.props.sendTransaction(txMeta, event)
- } else {
- this.props.dispatch(actions.displayWarning('Invalid Gas Parameters'))
- this.setState({ submitting: false })
- }
-}
-
-PendingTx.prototype.checkValidity = function () {
- const form = this.getFormEl()
- const valid = form.checkValidity()
- return valid
-}
-
-PendingTx.prototype.getFormEl = function () {
- const form = document.querySelector('form#pending-tx-form')
- // Stub out form for unit tests:
- if (!form) {
- return { checkValidity () { return true } }
- }
- return form
-}
-
-// After a customizable state value has been updated,
-PendingTx.prototype.gatherTxMeta = function () {
- log.debug(`pending-tx gatherTxMeta`)
- const props = this.props
- const state = this.state
- const txData = clone(state.txData) || clone(props.txData)
-
- log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
- return txData
-}
-
-PendingTx.prototype.verifyGasParams = function () {
- // We call this in case the gas has not been modified at all
- if (!this.state) { return true }
- return (
- this._notZeroOrEmptyString(this.state.gas) &&
- this._notZeroOrEmptyString(this.state.gasPrice)
- )
-}
-
-PendingTx.prototype._notZeroOrEmptyString = function (obj) {
- return obj !== '' && obj !== '0x0'
-}
-
-PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
- const numBN = new BN(numerator)
- const denomBN = new BN(denominator)
- return targetBN.mul(numBN).div(denomBN)
-}
-
-function forwardCarrat () {
- return (
- h('img', {
- src: 'images/forward-carrat.svg',
- style: {
- padding: '5px 6px 0px 10px',
- height: '37px',
- },
- })
- )
-}
diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js
new file mode 100644
index 000000000..ae6c6ef7b
--- /dev/null
+++ b/ui/app/components/pending-tx/confirm-deploy-contract.js
@@ -0,0 +1,348 @@
+const Component = require('react').Component
+const { connect } = require('react-redux')
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const actions = require('../../actions')
+const clone = require('clone')
+const Identicon = require('../identicon')
+const ethUtil = require('ethereumjs-util')
+const BN = ethUtil.BN
+const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
+const { conversionUtil } = require('../../conversion-util')
+
+const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
+
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmDeployContract)
+
+function mapStateToProps (state) {
+ const {
+ conversionRate,
+ identities,
+ currentCurrency,
+ } = state.metamask
+ const accounts = state.metamask.accounts
+ const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
+ return {
+ currentCurrency,
+ conversionRate,
+ identities,
+ selectedAddress,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
+ cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
+ }
+}
+
+
+inherits(ConfirmDeployContract, Component)
+function ConfirmDeployContract () {
+ Component.call(this)
+ this.state = {}
+ this.onSubmit = this.onSubmit.bind(this)
+}
+
+ConfirmDeployContract.prototype.onSubmit = function (event) {
+ event.preventDefault()
+ const txMeta = this.gatherTxMeta()
+ const valid = this.checkValidity()
+ this.setState({ valid, submitting: true })
+
+ if (valid && this.verifyGasParams()) {
+ this.props.sendTransaction(txMeta, event)
+ } else {
+ this.props.dispatch(actions.displayWarning('Invalid Gas Parameters'))
+ this.setState({ submitting: false })
+ }
+}
+
+ConfirmDeployContract.prototype.cancel = function (event, txMeta) {
+ event.preventDefault()
+ this.props.cancelTransaction(txMeta)
+}
+
+ConfirmDeployContract.prototype.checkValidity = function () {
+ const form = this.getFormEl()
+ const valid = form.checkValidity()
+ return valid
+}
+
+ConfirmDeployContract.prototype.getFormEl = function () {
+ const form = document.querySelector('form#pending-tx-form')
+ // Stub out form for unit tests:
+ if (!form) {
+ return { checkValidity () { return true } }
+ }
+ return form
+}
+
+// After a customizable state value has been updated,
+ConfirmDeployContract.prototype.gatherTxMeta = function () {
+ const props = this.props
+ const state = this.state
+ const txData = clone(state.txData) || clone(props.txData)
+
+ // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
+ return txData
+}
+
+ConfirmDeployContract.prototype.verifyGasParams = function () {
+ // We call this in case the gas has not been modified at all
+ if (!this.state) { return true }
+ return (
+ this._notZeroOrEmptyString(this.state.gas) &&
+ this._notZeroOrEmptyString(this.state.gasPrice)
+ )
+}
+
+ConfirmDeployContract.prototype._notZeroOrEmptyString = function (obj) {
+ return obj !== '' && obj !== '0x0'
+}
+
+ConfirmDeployContract.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
+ const numBN = new BN(numerator)
+ const denomBN = new BN(denominator)
+ return targetBN.mul(numBN).div(denomBN)
+}
+
+ConfirmDeployContract.prototype.getData = function () {
+ const { identities } = this.props
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+
+ return {
+ from: {
+ address: txParams.from,
+ name: identities[txParams.from].name,
+ },
+ memo: txParams.memo || '',
+ }
+}
+
+ConfirmDeployContract.prototype.getAmount = function () {
+ const { conversionRate, currentCurrency } = this.props
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+
+ const FIAT = conversionUtil(txParams.value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency: 'ETH',
+ toCurrency: currentCurrency,
+ numberOfDecimals: 2,
+ fromDenomination: 'WEI',
+ conversionRate,
+ })
+ const ETH = conversionUtil(txParams.value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency: 'ETH',
+ toCurrency: 'ETH',
+ fromDenomination: 'WEI',
+ conversionRate,
+ numberOfDecimals: 6,
+ })
+
+ return {
+ fiat: Number(FIAT),
+ token: Number(ETH),
+ }
+
+}
+
+ConfirmDeployContract.prototype.getGasFee = function () {
+ const { conversionRate, currentCurrency } = this.props
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+
+ // Gas
+ const gas = txParams.gas
+ const gasBn = hexToBn(gas)
+
+ // Gas Price
+ const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
+ const gasPriceBn = hexToBn(gasPrice)
+
+ const txFeeBn = gasBn.mul(gasPriceBn)
+
+ const FIAT = conversionUtil(txFeeBn, {
+ fromNumericBase: 'BN',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ fromCurrency: 'ETH',
+ toCurrency: currentCurrency,
+ numberOfDecimals: 2,
+ conversionRate,
+ })
+ const ETH = conversionUtil(txFeeBn, {
+ fromNumericBase: 'BN',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ fromCurrency: 'ETH',
+ toCurrency: 'ETH',
+ numberOfDecimals: 6,
+ conversionRate,
+ })
+
+ return {
+ fiat: Number(FIAT),
+ eth: Number(ETH),
+ }
+}
+
+ConfirmDeployContract.prototype.renderGasFee = function () {
+ const { currentCurrency } = this.props
+ const { fiat: fiatGas, eth: ethGas } = this.getGasFee()
+
+ return (
+ h('section.flex-row.flex-center.confirm-screen-row', [
+ h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]),
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', `${fiatGas} ${currentCurrency.toUpperCase()}`),
+
+ h(
+ 'div.confirm-screen-row-detail',
+ `${ethGas} ETH`
+ ),
+ ]),
+ ])
+ )
+}
+
+ConfirmDeployContract.prototype.renderHeroAmount = function () {
+ const { currentCurrency } = this.props
+ const { fiat: fiatAmount } = this.getAmount()
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+ const { memo = '' } = txParams
+
+ return (
+ h('div.confirm-send-token__hero-amount-wrapper', [
+ h('h3.flex-center.confirm-screen-send-amount', `${fiatAmount}`),
+ h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency.toUpperCase()),
+ h('div.flex-center.confirm-memo-wrapper', [
+ h('h3.confirm-screen-send-memo', memo),
+ ]),
+ ])
+ )
+}
+
+ConfirmDeployContract.prototype.renderTotalPlusGas = function () {
+ const { currentCurrency } = this.props
+ const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
+ const { fiat: fiatGas, eth: ethGas } = this.getGasFee()
+
+ return (
+ h('section.flex-row.flex-center.confirm-screen-total-box ', [
+ h('div.confirm-screen-section-column', [
+ h('span.confirm-screen-label', [ 'Total ' ]),
+ h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]),
+ ]),
+
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', `${fiatAmount + fiatGas} ${currentCurrency.toUpperCase()}`),
+ h('div.confirm-screen-row-detail', `${tokenAmount + ethGas} ETH`),
+ ]),
+ ])
+ )
+}
+
+ConfirmDeployContract.prototype.render = function () {
+ const { backToAccountDetail, selectedAddress } = this.props
+ const txMeta = this.gatherTxMeta()
+
+ const {
+ from: {
+ address: fromAddress,
+ name: fromName,
+ },
+ } = this.getData()
+
+ this.inputs = []
+
+ return (
+ h('div.flex-column.flex-grow.confirm-screen-container', {
+ style: { minWidth: '355px' },
+ }, [
+ // Main Send token Card
+ h('div.confirm-screen-wrapper.flex-column.flex-grow', [
+ h('h3.flex-center.confirm-screen-header', [
+ h('button.confirm-screen-back-button', {
+ onClick: () => backToAccountDetail(selectedAddress),
+ }, 'BACK'),
+ h('div.confirm-screen-title', 'Confirm Contract'),
+ h('div.confirm-screen-header-tip'),
+ ]),
+ h('div.flex-row.flex-center.confirm-screen-identicons', [
+ h('div.confirm-screen-account-wrapper', [
+ h(
+ Identicon,
+ {
+ address: fromAddress,
+ diameter: 60,
+ },
+ ),
+ h('span.confirm-screen-account-name', fromName),
+ // h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)),
+ ]),
+ h('i.fa.fa-arrow-right.fa-lg'),
+ h('div.confirm-screen-account-wrapper', [
+ h('i.fa.fa-file-text-o'),
+ h('span.confirm-screen-account-name', 'New Contract'),
+ h('span.confirm-screen-account-number', ' '),
+ ]),
+ ]),
+
+ // h('h3.flex-center.confirm-screen-sending-to-message', {
+ // style: {
+ // textAlign: 'center',
+ // fontSize: '16px',
+ // },
+ // }, [
+ // `You're deploying a new contract.`,
+ // ]),
+
+ this.renderHeroAmount(),
+
+ h('div.confirm-screen-rows', [
+ h('section.flex-row.flex-center.confirm-screen-row', [
+ h('span.confirm-screen-label.confirm-screen-section-column', [ 'From' ]),
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', fromName),
+ h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
+ ]),
+ ]),
+
+ h('section.flex-row.flex-center.confirm-screen-row', [
+ h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]),
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', 'New Contract'),
+ ]),
+ ]),
+
+ this.renderGasFee(),
+
+ this.renderTotalPlusGas(),
+
+ ]),
+ ]),
+
+ h('form#pending-tx-form', {
+ onSubmit: this.onSubmit,
+ }, [
+ // Cancel Button
+ h('div.cancel.btn-light.confirm-screen-cancel-button', {
+ onClick: (event) => this.cancel(event, txMeta),
+ }, 'CANCEL'),
+
+ // Accept Button
+ h('button.confirm-screen-confirm-button', ['CONFIRM']),
+
+ ]),
+ ])
+ )
+}
diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js
new file mode 100644
index 000000000..1264da153
--- /dev/null
+++ b/ui/app/components/pending-tx/confirm-send-ether.js
@@ -0,0 +1,489 @@
+const Component = require('react').Component
+const { connect } = require('react-redux')
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const actions = require('../../actions')
+const clone = require('clone')
+const Identicon = require('../identicon')
+const ethUtil = require('ethereumjs-util')
+const BN = ethUtil.BN
+const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
+const { conversionUtil, addCurrencies } = require('../../conversion-util')
+
+const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendEther)
+
+function mapStateToProps (state) {
+ const {
+ conversionRate,
+ identities,
+ currentCurrency,
+ send,
+ } = state.metamask
+ const accounts = state.metamask.accounts
+ const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
+ return {
+ conversionRate,
+ identities,
+ selectedAddress,
+ currentCurrency,
+ send,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ clearSend: () => dispatch(actions.clearSend()),
+ editTransaction: txMeta => {
+ const { id, txParams } = txMeta
+ const {
+ gas: gasLimit,
+ gasPrice,
+ to,
+ value: amount,
+ } = txParams
+ dispatch(actions.updateSend({
+ gasLimit,
+ gasPrice,
+ gasTotal: null,
+ to,
+ amount,
+ errors: { to: null, amount: null },
+ editingTransactionId: id,
+ }))
+ dispatch(actions.showSendPage())
+ },
+ cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
+ }
+}
+
+inherits(ConfirmSendEther, Component)
+function ConfirmSendEther () {
+ Component.call(this)
+ this.state = {}
+ this.onSubmit = this.onSubmit.bind(this)
+}
+
+ConfirmSendEther.prototype.getAmount = function () {
+ const { conversionRate, currentCurrency } = this.props
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+
+ const FIAT = conversionUtil(txParams.value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency: 'ETH',
+ toCurrency: currentCurrency,
+ numberOfDecimals: 2,
+ fromDenomination: 'WEI',
+ conversionRate,
+ })
+ const ETH = conversionUtil(txParams.value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency: 'ETH',
+ toCurrency: 'ETH',
+ fromDenomination: 'WEI',
+ conversionRate,
+ numberOfDecimals: 6,
+ })
+
+ return {
+ FIAT,
+ ETH,
+ }
+
+}
+
+ConfirmSendEther.prototype.getGasFee = function () {
+ const { conversionRate, currentCurrency } = this.props
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+
+ // Gas
+ const gas = txParams.gas
+ const gasBn = hexToBn(gas)
+
+ // From latest master
+// const gasLimit = new BN(parseInt(blockGasLimit))
+// const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20)
+// const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20)
+// const safeGasLimit = safeGasLimitBN.toString(10)
+
+ // Gas Price
+ const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
+ const gasPriceBn = hexToBn(gasPrice)
+
+ const txFeeBn = gasBn.mul(gasPriceBn)
+
+ const FIAT = conversionUtil(txFeeBn, {
+ fromNumericBase: 'BN',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ fromCurrency: 'ETH',
+ toCurrency: currentCurrency,
+ numberOfDecimals: 2,
+ conversionRate,
+ })
+ const ETH = conversionUtil(txFeeBn, {
+ fromNumericBase: 'BN',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ fromCurrency: 'ETH',
+ toCurrency: 'ETH',
+ numberOfDecimals: 6,
+ conversionRate,
+ })
+
+ return {
+ FIAT,
+ ETH,
+ }
+}
+
+ConfirmSendEther.prototype.getData = function () {
+ const { identities } = this.props
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+ const { FIAT: gasFeeInFIAT, ETH: gasFeeInETH } = this.getGasFee()
+ const { FIAT: amountInFIAT, ETH: amountInETH } = this.getAmount()
+
+ const totalInFIAT = addCurrencies(gasFeeInFIAT, amountInFIAT, {
+ toNumericBase: 'dec',
+ numberOfDecimals: 2,
+ })
+ const totalInETH = addCurrencies(gasFeeInETH, amountInETH, {
+ toNumericBase: 'dec',
+ numberOfDecimals: 6,
+ })
+
+ return {
+ from: {
+ address: txParams.from,
+ name: identities[txParams.from].name,
+ },
+ to: {
+ address: txParams.to,
+ name: identities[txParams.to] ? identities[txParams.to].name : 'New Recipient',
+ },
+ memo: txParams.memo || '',
+ gasFeeInFIAT,
+ gasFeeInETH,
+ amountInFIAT,
+ amountInETH,
+ totalInFIAT,
+ totalInETH,
+ }
+}
+
+ConfirmSendEther.prototype.render = function () {
+ const { editTransaction, currentCurrency, clearSend } = this.props
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+
+ const {
+ from: {
+ address: fromAddress,
+ name: fromName,
+ },
+ to: {
+ address: toAddress,
+ name: toName,
+ },
+ memo,
+ gasFeeInFIAT,
+ gasFeeInETH,
+ amountInFIAT,
+ totalInFIAT,
+ totalInETH,
+ } = this.getData()
+
+ // This is from the latest master
+ // It handles some of the errors that we are not currently handling
+ // Leaving as comments fo reference
+
+ // const balanceBn = hexToBn(balance)
+ // const insufficientBalance = balanceBn.lt(maxCost)
+ // const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
+ // const showRejectAll = props.unconfTxListLength > 1
+// const dangerousGasLimit = gasBn.gte(saferGasLimitBN)
+// const gasLimitSpecified = txMeta.gasLimitSpecified
+
+ this.inputs = []
+
+ return (
+ h('div.confirm-screen-container.confirm-send-ether', {
+ style: { minWidth: '355px' },
+ }, [
+ // Main Send token Card
+ h('div.confirm-screen-wrapper.flex-column.flex-grow', [
+ h('h3.flex-center.confirm-screen-header', [
+ h('button.confirm-screen-back-button', {
+ onClick: () => editTransaction(txMeta),
+ }, 'EDIT'),
+ h('div.confirm-screen-title', 'Confirm Transaction'),
+ h('div.confirm-screen-header-tip'),
+ ]),
+ h('div.flex-row.flex-center.confirm-screen-identicons', [
+ h('div.confirm-screen-account-wrapper', [
+ h(
+ Identicon,
+ {
+ address: fromAddress,
+ diameter: 60,
+ },
+ ),
+ h('span.confirm-screen-account-name', fromName),
+ // h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)),
+ ]),
+ h('i.fa.fa-arrow-right.fa-lg'),
+ h('div.confirm-screen-account-wrapper', [
+ h(
+ Identicon,
+ {
+ address: txParams.to,
+ diameter: 60,
+ },
+ ),
+ h('span.confirm-screen-account-name', toName),
+ // h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)),
+ ]),
+ ]),
+
+ // h('h3.flex-center.confirm-screen-sending-to-message', {
+ // style: {
+ // textAlign: 'center',
+ // fontSize: '16px',
+ // },
+ // }, [
+ // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`,
+ // ]),
+
+ h('h3.flex-center.confirm-screen-send-amount', [`${amountInFIAT}`]),
+ h('h3.flex-center.confirm-screen-send-amount-currency', [ currentCurrency.toUpperCase() ]),
+ h('div.flex-center.confirm-memo-wrapper', [
+ h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
+ ]),
+
+ h('div.confirm-screen-rows', [
+ h('section.flex-row.flex-center.confirm-screen-row', [
+ h('span.confirm-screen-label.confirm-screen-section-column', [ 'From' ]),
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', fromName),
+ h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
+ ]),
+ ]),
+
+ h('section.flex-row.flex-center.confirm-screen-row', [
+ h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]),
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', toName),
+ h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`),
+ ]),
+ ]),
+
+ h('section.flex-row.flex-center.confirm-screen-row', [
+ h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]),
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', `${gasFeeInFIAT} ${currentCurrency.toUpperCase()}`),
+
+ h('div.confirm-screen-row-detail', `${gasFeeInETH} ETH`),
+ ]),
+ ]),
+
+
+ h('section.flex-row.flex-center.confirm-screen-total-box ', [
+ h('div.confirm-screen-section-column', [
+ h('span.confirm-screen-label', [ 'Total ' ]),
+ h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]),
+ ]),
+
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', `${totalInFIAT} ${currentCurrency.toUpperCase()}`),
+ h('div.confirm-screen-row-detail', `${totalInETH} ETH`),
+ ]),
+ ]),
+ ]),
+
+// These are latest errors handling from master
+// Leaving as comments as reference when we start implementing error handling
+// h('style', `
+// .conf-buttons button {
+// margin-left: 10px;
+// text-transform: uppercase;
+// }
+// `),
+
+// txMeta.simulationFails ?
+// h('.error', {
+// style: {
+// marginLeft: 50,
+// fontSize: '0.9em',
+// },
+// }, 'Transaction Error. Exception thrown in contract code.')
+// : null,
+
+// !isValidAddress ?
+// h('.error', {
+// style: {
+// marginLeft: 50,
+// fontSize: '0.9em',
+// },
+// }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
+// : null,
+
+// insufficientBalance ?
+// h('span.error', {
+// style: {
+// marginLeft: 50,
+// fontSize: '0.9em',
+// },
+// }, 'Insufficient balance for transaction')
+// : null,
+
+// // send + cancel
+// h('.flex-row.flex-space-around.conf-buttons', {
+// style: {
+// display: 'flex',
+// justifyContent: 'flex-end',
+// margin: '14px 25px',
+// },
+// }, [
+// h('button', {
+// onClick: (event) => {
+// this.resetGasFields()
+// event.preventDefault()
+// },
+// }, 'Reset'),
+
+// // Accept Button or Buy Button
+// insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') :
+// h('input.confirm.btn-green', {
+// type: 'submit',
+// value: 'SUBMIT',
+// style: { marginLeft: '10px' },
+// disabled: buyDisabled,
+// }),
+
+// h('button.cancel.btn-red', {
+// onClick: props.cancelTransaction,
+// }, 'Reject'),
+// ]),
+// showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', {
+// style: {
+// display: 'flex',
+// justifyContent: 'flex-end',
+// margin: '14px 25px',
+// },
+// }, [
+// h('button.cancel.btn-red', {
+// onClick: props.cancelAllTransactions,
+// }, 'Reject All'),
+// ]) : null,
+// ]),
+// ])
+// )
+// }
+ ]),
+
+ h('form#pending-tx-form', {
+ onSubmit: this.onSubmit,
+ }, [
+ // Cancel Button
+ h('div.cancel.btn-light.confirm-screen-cancel-button', {
+ onClick: (event) => {
+ clearSend()
+ this.cancel(event, txMeta)
+ },
+ }, 'CANCEL'),
+
+ // Accept Button
+ h('button.confirm-screen-confirm-button', ['CONFIRM']),
+ ]),
+ ])
+ )
+}
+
+ConfirmSendEther.prototype.onSubmit = function (event) {
+ event.preventDefault()
+ const txMeta = this.gatherTxMeta()
+ const valid = this.checkValidity()
+ this.setState({ valid, submitting: true })
+
+ if (valid && this.verifyGasParams()) {
+ this.props.sendTransaction(txMeta, event)
+ } else {
+ this.props.dispatch(actions.displayWarning('Invalid Gas Parameters'))
+ this.setState({ submitting: false })
+ }
+}
+
+ConfirmSendEther.prototype.cancel = function (event, txMeta) {
+ event.preventDefault()
+ this.props.cancelTransaction(txMeta)
+}
+
+ConfirmSendEther.prototype.checkValidity = function () {
+ const form = this.getFormEl()
+ const valid = form.checkValidity()
+ return valid
+}
+
+ConfirmSendEther.prototype.getFormEl = function () {
+ const form = document.querySelector('form#pending-tx-form')
+ // Stub out form for unit tests:
+ if (!form) {
+ return { checkValidity () { return true } }
+ }
+ return form
+}
+
+// After a customizable state value has been updated,
+ConfirmSendEther.prototype.gatherTxMeta = function () {
+ const props = this.props
+ const state = this.state
+ const txData = clone(state.txData) || clone(props.txData)
+
+ if (props.send.editingTransactionId) {
+ const {
+ send: {
+ memo,
+ amount: value,
+ gasLimit: gas,
+ gasPrice,
+ },
+ } = props
+ const { txParams: { from, to } } = txData
+ txData.txParams = {
+ from: ethUtil.addHexPrefix(from),
+ to: ethUtil.addHexPrefix(to),
+ memo: memo && ethUtil.addHexPrefix(memo),
+ value: ethUtil.addHexPrefix(value),
+ gas: ethUtil.addHexPrefix(gas),
+ gasPrice: ethUtil.addHexPrefix(gasPrice),
+ }
+ }
+
+ // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
+ return txData
+}
+
+ConfirmSendEther.prototype.verifyGasParams = function () {
+ // We call this in case the gas has not been modified at all
+ if (!this.state) { return true }
+ return (
+ this._notZeroOrEmptyString(this.state.gas) &&
+ this._notZeroOrEmptyString(this.state.gasPrice)
+ )
+}
+
+ConfirmSendEther.prototype._notZeroOrEmptyString = function (obj) {
+ return obj !== '' && obj !== '0x0'
+}
+
+ConfirmSendEther.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
+ const numBN = new BN(numerator)
+ const denomBN = new BN(denominator)
+ return targetBN.mul(numBN).div(denomBN)
+}
diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js
new file mode 100644
index 000000000..cc2df8299
--- /dev/null
+++ b/ui/app/components/pending-tx/confirm-send-token.js
@@ -0,0 +1,495 @@
+const Component = require('react').Component
+const { connect } = require('react-redux')
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const ethAbi = require('ethereumjs-abi')
+const tokenAbi = require('human-standard-token-abi')
+const abiDecoder = require('abi-decoder')
+abiDecoder.addABI(tokenAbi)
+const actions = require('../../actions')
+const clone = require('clone')
+const Identicon = require('../identicon')
+const ethUtil = require('ethereumjs-util')
+const BN = ethUtil.BN
+const {
+ conversionUtil,
+ multiplyCurrencies,
+ addCurrencies,
+} = require('../../conversion-util')
+const {
+ calcTokenAmount,
+} = require('../../token-util')
+
+const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
+
+const {
+ getTokenExchangeRate,
+ getSelectedAddress,
+ getSelectedTokenContract,
+} = require('../../selectors')
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendToken)
+
+function mapStateToProps (state, ownProps) {
+ const { token: { symbol }, txData } = ownProps
+ const { txParams } = txData || {}
+ const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
+
+ const {
+ conversionRate,
+ identities,
+ currentCurrency,
+ } = state.metamask
+ const selectedAddress = getSelectedAddress(state)
+ const tokenExchangeRate = getTokenExchangeRate(state, symbol)
+
+ return {
+ conversionRate,
+ identities,
+ selectedAddress,
+ tokenExchangeRate,
+ tokenData: tokenData || {},
+ currentCurrency: currentCurrency.toUpperCase(),
+ send: state.metamask.send,
+ tokenContract: getSelectedTokenContract(state),
+ }
+}
+
+function mapDispatchToProps (dispatch, ownProps) {
+ const { token: { symbol } } = ownProps
+
+ return {
+ backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
+ cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
+ updateTokenExchangeRate: () => dispatch(actions.updateTokenExchangeRate(symbol)),
+ editTransaction: txMeta => {
+ const { token: { address } } = ownProps
+ const { txParams, id } = txMeta
+ const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
+ const { params = [] } = tokenData
+ const { value } = params[1] || {}
+ const amount = conversionUtil(value, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ })
+ const {
+ gas: gasLimit,
+ gasPrice,
+ to,
+ } = txParams
+ dispatch(actions.setSelectedToken(address))
+ dispatch(actions.updateSend({
+ gasLimit,
+ gasPrice,
+ gasTotal: null,
+ to,
+ amount,
+ errors: { to: null, amount: null },
+ editingTransactionId: id,
+ }))
+ dispatch(actions.showSendTokenPage())
+ },
+ }
+}
+
+inherits(ConfirmSendToken, Component)
+function ConfirmSendToken () {
+ Component.call(this)
+ this.state = {}
+ this.onSubmit = this.onSubmit.bind(this)
+}
+
+ConfirmSendToken.prototype.componentWillMount = function () {
+ const { tokenContract, selectedAddress } = this.props
+ tokenContract && tokenContract
+ .balanceOf(selectedAddress)
+ .then(usersToken => {
+ })
+ this.props.updateTokenExchangeRate()
+}
+
+ConfirmSendToken.prototype.getAmount = function () {
+ const {
+ conversionRate,
+ tokenExchangeRate,
+ token,
+ tokenData,
+ send: { amount, editingTransactionId },
+ } = this.props
+ const { params = [] } = tokenData
+ let { value } = params[1] || {}
+ const { decimals } = token
+
+ if (editingTransactionId) {
+ value = conversionUtil(amount, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+ }
+
+ const sendTokenAmount = calcTokenAmount(value, decimals)
+
+ return {
+ fiat: tokenExchangeRate
+ ? +(sendTokenAmount * tokenExchangeRate * conversionRate).toFixed(2)
+ : null,
+ token: typeof value === 'undefined'
+ ? 'Unknown'
+ : +sendTokenAmount.toFixed(decimals),
+ }
+
+}
+
+ConfirmSendToken.prototype.getGasFee = function () {
+ const { conversionRate, tokenExchangeRate, token, currentCurrency } = this.props
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+ const { decimals } = token
+
+ const gas = txParams.gas
+ const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
+ const gasTotal = multiplyCurrencies(gas, gasPrice, {
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+
+ const FIAT = conversionUtil(gasTotal, {
+ fromNumericBase: 'BN',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ fromCurrency: 'ETH',
+ toCurrency: currentCurrency,
+ numberOfDecimals: 2,
+ conversionRate,
+ })
+ const ETH = conversionUtil(gasTotal, {
+ fromNumericBase: 'BN',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ fromCurrency: 'ETH',
+ toCurrency: 'ETH',
+ numberOfDecimals: 6,
+ conversionRate,
+ })
+ const tokenGas = multiplyCurrencies(gas, gasPrice, {
+ toNumericBase: 'dec',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ toCurrency: 'BAT',
+ conversionRate: tokenExchangeRate,
+ invertConversionRate: true,
+ fromDenomination: 'WEI',
+ numberOfDecimals: decimals || 4,
+ })
+
+ return {
+ fiat: +Number(FIAT).toFixed(2),
+ eth: ETH,
+ token: tokenExchangeRate
+ ? tokenGas
+ : null,
+ }
+}
+
+ConfirmSendToken.prototype.getData = function () {
+ const { identities, tokenData } = this.props
+ const { params = [] } = tokenData
+ const { value } = params[0] || {}
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+
+ return {
+ from: {
+ address: txParams.from,
+ name: identities[txParams.from].name,
+ },
+ to: {
+ address: value,
+ name: identities[value] ? identities[value].name : 'New Recipient',
+ },
+ memo: txParams.memo || '',
+ }
+}
+
+ConfirmSendToken.prototype.renderHeroAmount = function () {
+ const { token: { symbol }, currentCurrency } = this.props
+ const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+ const { memo = '' } = txParams
+
+ return fiatAmount
+ ? (
+ h('div.confirm-send-token__hero-amount-wrapper', [
+ h('h3.flex-center.confirm-screen-send-amount', `${fiatAmount}`),
+ h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency),
+ h('div.flex-center.confirm-memo-wrapper', [
+ h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
+ ]),
+ ])
+ )
+ : (
+ h('div.confirm-send-token__hero-amount-wrapper', [
+ h('h3.flex-center.confirm-screen-send-amount', tokenAmount),
+ h('h3.flex-center.confirm-screen-send-amount-currency', symbol),
+ h('div.flex-center.confirm-memo-wrapper', [
+ h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
+ ]),
+ ])
+ )
+}
+
+ConfirmSendToken.prototype.renderGasFee = function () {
+ const { token: { symbol }, currentCurrency } = this.props
+ const { fiat: fiatGas, token: tokenGas, eth: ethGas } = this.getGasFee()
+
+ return (
+ h('section.flex-row.flex-center.confirm-screen-row', [
+ h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]),
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', `${fiatGas} ${currentCurrency}`),
+
+ h(
+ 'div.confirm-screen-row-detail',
+ tokenGas ? `${tokenGas} ${symbol}` : `${ethGas} ETH`
+ ),
+ ]),
+ ])
+ )
+}
+
+ConfirmSendToken.prototype.renderTotalPlusGas = function () {
+ const { token: { symbol }, currentCurrency } = this.props
+ const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
+ const { fiat: fiatGas, token: tokenGas } = this.getGasFee()
+
+ return fiatAmount && fiatGas
+ ? (
+ h('section.flex-row.flex-center.confirm-screen-total-box ', [
+ h('div.confirm-screen-section-column', [
+ h('span.confirm-screen-label', [ 'Total ' ]),
+ h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]),
+ ]),
+
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', `${addCurrencies(fiatAmount, fiatGas)} ${currentCurrency}`),
+ h('div.confirm-screen-row-detail', `${addCurrencies(tokenAmount, tokenGas || '0')} ${symbol}`),
+ ]),
+ ])
+ )
+ : (
+ h('section.flex-row.flex-center.confirm-screen-total-box ', [
+ h('div.confirm-screen-section-column', [
+ h('span.confirm-screen-label', [ 'Total ' ]),
+ h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]),
+ ]),
+
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', `${tokenAmount} ${symbol}`),
+ h('div.confirm-screen-row-detail', `+ ${fiatGas} ${currentCurrency} Gas`),
+ ]),
+ ])
+ )
+}
+
+ConfirmSendToken.prototype.render = function () {
+ const { editTransaction } = this.props
+ const txMeta = this.gatherTxMeta()
+ const {
+ from: {
+ address: fromAddress,
+ name: fromName,
+ },
+ to: {
+ address: toAddress,
+ name: toName,
+ },
+ } = this.getData()
+
+ this.inputs = []
+
+ return (
+ h('div.confirm-screen-container.confirm-send-token', {
+ style: { minWidth: '355px' },
+ }, [
+ // Main Send token Card
+ h('div.confirm-screen-wrapper.flex-column.flex-grow', [
+ h('h3.flex-center.confirm-screen-header', [
+ h('button.confirm-screen-back-button', {
+ onClick: () => editTransaction(txMeta),
+ }, 'EDIT'),
+ h('div.confirm-screen-title', 'Confirm Transaction'),
+ h('div.confirm-screen-header-tip'),
+ ]),
+ h('div.flex-row.flex-center.confirm-screen-identicons', [
+ h('div.confirm-screen-account-wrapper', [
+ h(
+ Identicon,
+ {
+ address: fromAddress,
+ diameter: 60,
+ },
+ ),
+ h('span.confirm-screen-account-name', fromName),
+ // h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)),
+ ]),
+ h('i.fa.fa-arrow-right.fa-lg'),
+ h('div.confirm-screen-account-wrapper', [
+ h(
+ Identicon,
+ {
+ address: toAddress,
+ diameter: 60,
+ },
+ ),
+ h('span.confirm-screen-account-name', toName),
+ // h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)),
+ ]),
+ ]),
+
+ // h('h3.flex-center.confirm-screen-sending-to-message', {
+ // style: {
+ // textAlign: 'center',
+ // fontSize: '16px',
+ // },
+ // }, [
+ // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`,
+ // ]),
+
+ this.renderHeroAmount(),
+
+ h('div.confirm-screen-rows', [
+ h('section.flex-row.flex-center.confirm-screen-row', [
+ h('span.confirm-screen-label.confirm-screen-section-column', [ 'From' ]),
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', fromName),
+ h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
+ ]),
+ ]),
+
+ toAddress && h('section.flex-row.flex-center.confirm-screen-row', [
+ h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]),
+ h('div.confirm-screen-section-column', [
+ h('div.confirm-screen-row-info', toName),
+ h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`),
+ ]),
+ ]),
+
+ this.renderGasFee(),
+
+ this.renderTotalPlusGas(),
+
+ ]),
+ ]),
+
+ h('form#pending-tx-form', {
+ onSubmit: this.onSubmit,
+ }, [
+ // Cancel Button
+ h('div.cancel.btn-light.confirm-screen-cancel-button', {
+ onClick: (event) => this.cancel(event, txMeta),
+ }, 'CANCEL'),
+
+ // Accept Button
+ h('button.confirm-screen-confirm-button', ['CONFIRM']),
+ ]),
+
+
+ ])
+ )
+}
+
+ConfirmSendToken.prototype.onSubmit = function (event) {
+ event.preventDefault()
+ const txMeta = this.gatherTxMeta()
+ const valid = this.checkValidity()
+ this.setState({ valid, submitting: true })
+
+ if (valid && this.verifyGasParams()) {
+ this.props.sendTransaction(txMeta, event)
+ } else {
+ this.props.dispatch(actions.displayWarning('Invalid Gas Parameters'))
+ this.setState({ submitting: false })
+ }
+}
+
+ConfirmSendToken.prototype.cancel = function (event, txMeta) {
+ event.preventDefault()
+ this.props.cancelTransaction(txMeta)
+}
+
+ConfirmSendToken.prototype.checkValidity = function () {
+ const form = this.getFormEl()
+ const valid = form.checkValidity()
+ return valid
+}
+
+ConfirmSendToken.prototype.getFormEl = function () {
+ const form = document.querySelector('form#pending-tx-form')
+ // Stub out form for unit tests:
+ if (!form) {
+ return { checkValidity () { return true } }
+ }
+ return form
+}
+
+// After a customizable state value has been updated,
+ConfirmSendToken.prototype.gatherTxMeta = function () {
+ const props = this.props
+ const state = this.state
+ const txData = clone(state.txData) || clone(props.txData)
+
+ if (props.send.editingTransactionId) {
+ const {
+ send: {
+ memo,
+ amount,
+ gasLimit: gas,
+ gasPrice,
+ },
+ } = props
+
+ const { txParams: { from, to } } = txData
+
+ const tokenParams = {
+ from: ethUtil.addHexPrefix(from),
+ value: '0',
+ gas: ethUtil.addHexPrefix(gas),
+ gasPrice: ethUtil.addHexPrefix(gasPrice),
+ }
+
+ const data = '0xa9059cbb' + Array.prototype.map.call(
+ ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]),
+ x => ('00' + x.toString(16)).slice(-2)
+ ).join('')
+
+ txData.txParams = {
+ ...tokenParams,
+ to: ethUtil.addHexPrefix(to),
+ memo: memo && ethUtil.addHexPrefix(memo),
+ data,
+ }
+ }
+
+ // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
+ return txData
+}
+
+ConfirmSendToken.prototype.verifyGasParams = function () {
+ // We call this in case the gas has not been modified at all
+ if (!this.state) { return true }
+ return (
+ this._notZeroOrEmptyString(this.state.gas) &&
+ this._notZeroOrEmptyString(this.state.gasPrice)
+ )
+}
+
+ConfirmSendToken.prototype._notZeroOrEmptyString = function (obj) {
+ return obj !== '' && obj !== '0x0'
+}
+
+ConfirmSendToken.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
+ const numBN = new BN(numerator)
+ const denomBN = new BN(denominator)
+ return targetBN.mul(numBN).div(denomBN)
+}
diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js
new file mode 100644
index 000000000..f4f6afb8f
--- /dev/null
+++ b/ui/app/components/pending-tx/index.js
@@ -0,0 +1,145 @@
+const Component = require('react').Component
+const { connect } = require('react-redux')
+const h = require('react-hyperscript')
+const clone = require('clone')
+const abi = require('human-standard-token-abi')
+const abiDecoder = require('abi-decoder')
+abiDecoder.addABI(abi)
+const inherits = require('util').inherits
+const actions = require('../../actions')
+const util = require('../../util')
+const ConfirmSendEther = require('./confirm-send-ether')
+const ConfirmSendToken = require('./confirm-send-token')
+const ConfirmDeployContract = require('./confirm-deploy-contract')
+
+const TX_TYPES = {
+ DEPLOY_CONTRACT: 'deploy_contract',
+ SEND_ETHER: 'send_ether',
+ SEND_TOKEN: 'send_token',
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(PendingTx)
+
+function mapStateToProps (state) {
+ const {
+ conversionRate,
+ identities,
+ } = state.metamask
+ const accounts = state.metamask.accounts
+ const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
+ return {
+ conversionRate,
+ identities,
+ selectedAddress,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
+ cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
+ }
+}
+
+inherits(PendingTx, Component)
+function PendingTx () {
+ Component.call(this)
+ this.state = {
+ isFetching: true,
+ transactionType: '',
+ tokenAddress: '',
+ tokenSymbol: '',
+ tokenDecimals: '',
+ }
+}
+
+PendingTx.prototype.componentWillMount = async function () {
+ const txMeta = this.gatherTxMeta()
+ const txParams = txMeta.txParams || {}
+
+ if (!txParams.to) {
+ return this.setState({
+ transactionType: TX_TYPES.DEPLOY_CONTRACT,
+ isFetching: false,
+ })
+ }
+
+ try {
+ const token = util.getContractAtAddress(txParams.to)
+ const results = await Promise.all([
+ token.symbol(),
+ token.decimals(),
+ ])
+
+ const [ symbol, decimals ] = results
+
+ if (symbol[0] && decimals[0]) {
+ this.setState({
+ transactionType: TX_TYPES.SEND_TOKEN,
+ tokenAddress: txParams.to,
+ tokenSymbol: symbol[0],
+ tokenDecimals: decimals[0],
+ isFetching: false,
+ })
+ } else {
+ this.setState({
+ transactionType: TX_TYPES.SEND_ETHER,
+ isFetching: false,
+ })
+ }
+ } catch (e) {
+ this.setState({
+ transactionType: TX_TYPES.SEND_ETHER,
+ isFetching: false,
+ })
+ }
+}
+
+PendingTx.prototype.gatherTxMeta = function () {
+ const props = this.props
+ const state = this.state
+ const txData = clone(state.txData) || clone(props.txData)
+
+ return txData
+}
+
+PendingTx.prototype.render = function () {
+ const {
+ isFetching,
+ transactionType,
+ tokenAddress,
+ tokenSymbol,
+ tokenDecimals,
+ } = this.state
+
+ const { sendTransaction } = this.props
+
+ if (isFetching) {
+ return h('noscript')
+ }
+
+ switch (transactionType) {
+ case TX_TYPES.SEND_ETHER:
+ return h(ConfirmSendEther, {
+ txData: this.gatherTxMeta(),
+ sendTransaction,
+ })
+ case TX_TYPES.SEND_TOKEN:
+ return h(ConfirmSendToken, {
+ txData: this.gatherTxMeta(),
+ sendTransaction,
+ token: {
+ address: tokenAddress,
+ symbol: tokenSymbol,
+ decimals: tokenDecimals,
+ },
+ })
+ case TX_TYPES.DEPLOY_CONTRACT:
+ return h(ConfirmDeployContract, {
+ txData: this.gatherTxMeta(),
+ sendTransaction,
+ })
+ default:
+ return h('noscript')
+ }
+}
diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js
index 06b9aed9b..83885539c 100644
--- a/ui/app/components/qr-code.js
+++ b/ui/app/components/qr-code.js
@@ -4,13 +4,13 @@ const qrCode = require('qrcode-npm').qrcode
const inherits = require('util').inherits
const connect = require('react-redux').connect
const isHexPrefixed = require('ethereumjs-util').isHexPrefixed
-const CopyButton = require('./copyButton')
+const ReadOnlyInput = require('./readonly-input')
module.exports = connect(mapStateToProps)(QrCodeView)
function mapStateToProps (state) {
return {
- Qr: state.appState.Qr,
+ // Qr code is not fetched from state. 'message' and 'data' props are passed instead.
buyView: state.appState.buyView,
warning: state.appState.warning,
}
@@ -29,46 +29,29 @@ QrCodeView.prototype.render = function () {
const qrImage = qrCode(4, 'M')
qrImage.addData(address)
qrImage.make()
- return h('.main-container.flex-column', {
- key: 'qr',
- style: {
- justifyContent: 'center',
- paddingBottom: '45px',
- paddingLeft: '45px',
- paddingRight: '45px',
- alignItems: 'center',
- },
- }, [
- Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message),
+
+ return h('.div.flex-column.flex-center', [
+ Array.isArray(Qr.message)
+ ? h('.message-container', this.renderMultiMessage())
+ : Qr.message && h('.qr-header', Qr.message),
this.props.warning ? this.props.warning && h('span.error.flex-center', {
style: {
- textAlign: 'center',
- width: '229px',
- height: '82px',
},
},
this.props.warning) : null,
- h('#qr-container.flex-column', {
- style: {
- marginTop: '25px',
- marginBottom: '15px',
- },
+ h('.div.qr-wrapper', {
+ style: {},
dangerouslySetInnerHTML: {
__html: qrImage.createTableTag(4),
},
}),
- h('.flex-row', [
- h('h3.ellip-address', {
- style: {
- width: '247px',
- },
- }, Qr.data),
- h(CopyButton, {
- value: Qr.data,
- }),
- ]),
+ h(ReadOnlyInput, {
+ wrapperClass: 'ellip-address-wrapper',
+ inputClass: 'qr-ellip-address',
+ value: Qr.data,
+ }),
])
}
diff --git a/ui/app/components/readonly-input.js b/ui/app/components/readonly-input.js
new file mode 100644
index 000000000..fcf05fb9e
--- /dev/null
+++ b/ui/app/components/readonly-input.js
@@ -0,0 +1,33 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+
+module.exports = ReadOnlyInput
+
+inherits(ReadOnlyInput, Component)
+function ReadOnlyInput () {
+ Component.call(this)
+}
+
+ReadOnlyInput.prototype.render = function () {
+ const {
+ wrapperClass = '',
+ inputClass = '',
+ value,
+ textarea,
+ onClick,
+ } = this.props
+
+ const inputType = textarea ? 'textarea' : 'input'
+
+ return h('div', {className: wrapperClass}, [
+ h(inputType, {
+ className: inputClass,
+ value,
+ readOnly: true,
+ onFocus: event => event.target.select(),
+ onClick,
+ }),
+ ])
+}
+
diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js
new file mode 100644
index 000000000..99d078251
--- /dev/null
+++ b/ui/app/components/send-token/index.js
@@ -0,0 +1,439 @@
+const Component = require('react').Component
+const connect = require('react-redux').connect
+const h = require('react-hyperscript')
+const classnames = require('classnames')
+const abi = require('ethereumjs-abi')
+const inherits = require('util').inherits
+const actions = require('../../actions')
+const selectors = require('../../selectors')
+const { isValidAddress, allNull } = require('../../util')
+
+// const BalanceComponent = require('./balance-component')
+const Identicon = require('../identicon')
+const TokenBalance = require('../token-balance')
+const CurrencyToggle = require('../send/currency-toggle')
+const GasTooltip = require('../send/gas-tooltip')
+const GasFeeDisplay = require('../send/gas-fee-display')
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(SendTokenScreen)
+
+function mapStateToProps (state) {
+ // const sidebarOpen = state.appState.sidebarOpen
+
+ const { warning } = state.appState
+ const identities = state.metamask.identities
+ const addressBook = state.metamask.addressBook
+ const conversionRate = state.metamask.conversionRate
+ const currentBlockGasLimit = state.metamask.currentBlockGasLimit
+ const accounts = state.metamask.accounts
+ const selectedTokenAddress = state.metamask.selectedTokenAddress
+ const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
+ const selectedToken = selectors.getSelectedToken(state)
+ const tokenExchangeRates = state.metamask.tokenExchangeRates
+ const pair = `${selectedToken.symbol.toLowerCase()}_eth`
+ const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
+
+ return {
+ selectedAddress,
+ selectedTokenAddress,
+ identities,
+ addressBook,
+ conversionRate,
+ tokenExchangeRate,
+ currentBlockGasLimit,
+ selectedToken,
+ warning,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
+ hideWarning: () => dispatch(actions.hideWarning()),
+ addToAddressBook: (recipient, nickname) => dispatch(
+ actions.addToAddressBook(recipient, nickname)
+ ),
+ signTx: txParams => dispatch(actions.signTx(txParams)),
+ signTokenTx: (tokenAddress, toAddress, amount, txData) => (
+ dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData))
+ ),
+ updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)),
+ estimateGas: params => dispatch(actions.estimateGas(params)),
+ getGasPrice: () => dispatch(actions.getGasPrice()),
+ }
+}
+
+inherits(SendTokenScreen, Component)
+function SendTokenScreen () {
+ Component.call(this)
+ this.state = {
+ to: '',
+ amount: '0x0',
+ amountToSend: '0x0',
+ selectedCurrency: 'USD',
+ isGasTooltipOpen: false,
+ gasPrice: null,
+ gasLimit: null,
+ errors: {},
+ }
+}
+
+SendTokenScreen.prototype.componentWillMount = function () {
+ const {
+ updateTokenExchangeRate,
+ selectedToken: { symbol },
+ getGasPrice,
+ estimateGas,
+ selectedAddress,
+ } = this.props
+
+ updateTokenExchangeRate(symbol)
+
+ const data = Array.prototype.map.call(
+ abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']),
+ x => ('00' + x.toString(16)).slice(-2)
+ ).join('')
+
+ console.log(data)
+ Promise.all([
+ getGasPrice(),
+ estimateGas({
+ from: selectedAddress,
+ value: '0x0',
+ gas: '746a528800',
+ data,
+ }),
+ ])
+ .then(([blockGasPrice, estimatedGas]) => {
+ console.log({ blockGasPrice, estimatedGas})
+ this.setState({
+ gasPrice: blockGasPrice,
+ gasLimit: estimatedGas,
+ })
+ })
+}
+
+SendTokenScreen.prototype.validate = function () {
+ const {
+ to,
+ amount: stringAmount,
+ gasPrice: hexGasPrice,
+ gasLimit: hexGasLimit,
+ } = this.state
+
+ const gasPrice = parseInt(hexGasPrice, 16)
+ const gasLimit = parseInt(hexGasLimit, 16) / 1000000000
+ const amount = Number(stringAmount)
+
+ const errors = {
+ to: !to ? 'Required' : null,
+ amount: !amount ? 'Required' : null,
+ gasPrice: !gasPrice ? 'Gas Price Required' : null,
+ gasLimit: !gasLimit ? 'Gas Limit Required' : null,
+ }
+
+ if (to && !isValidAddress(to)) {
+ errors.to = 'Invalid address'
+ }
+
+ const isValid = Object.entries(errors).every(([key, value]) => value === null)
+ return {
+ isValid,
+ errors: isValid ? {} : errors,
+ }
+}
+
+SendTokenScreen.prototype.setErrorsFor = function (field) {
+ const { errors: previousErrors } = this.state
+
+ const {
+ isValid,
+ errors: newErrors,
+ } = this.validate()
+
+ const nextErrors = Object.assign({}, previousErrors, {
+ [field]: newErrors[field] || null,
+ })
+
+ if (!isValid) {
+ this.setState({
+ errors: nextErrors,
+ isValid,
+ })
+ }
+}
+
+SendTokenScreen.prototype.clearErrorsFor = function (field) {
+ const { errors: previousErrors } = this.state
+ const nextErrors = Object.assign({}, previousErrors, {
+ [field]: null,
+ })
+
+ this.setState({
+ errors: nextErrors,
+ isValid: allNull(nextErrors),
+ })
+}
+
+SendTokenScreen.prototype.getAmountToSend = function (amount, selectedToken) {
+ const { decimals } = selectedToken || {}
+ const multiplier = Math.pow(10, Number(decimals || 0))
+ const sendAmount = '0x' + Number(amount * multiplier).toString(16)
+ return sendAmount
+}
+
+SendTokenScreen.prototype.submit = function () {
+ const {
+ to,
+ amount,
+ gasPrice,
+ gasLimit,
+ } = this.state
+
+ const {
+ identities,
+ selectedAddress,
+ selectedTokenAddress,
+ hideWarning,
+ addToAddressBook,
+ signTokenTx,
+ selectedToken,
+ } = this.props
+
+ const { nickname = ' ' } = identities[to] || {}
+
+ hideWarning()
+ addToAddressBook(to, nickname)
+
+ const txParams = {
+ from: selectedAddress,
+ value: '0',
+ gas: gasLimit,
+ gasPrice: gasPrice,
+ }
+
+ const sendAmount = this.getAmountToSend(amount, selectedToken)
+
+ signTokenTx(selectedTokenAddress, to, sendAmount, txParams)
+}
+
+SendTokenScreen.prototype.renderToAddressInput = function () {
+ const {
+ identities,
+ addressBook,
+ } = this.props
+
+ const {
+ to,
+ errors: { to: errorMessage },
+ } = this.state
+
+ return h('div', {
+ className: classnames('send-screen-input-wrapper', {
+ 'send-screen-input-wrapper--error': errorMessage,
+ }),
+ }, [
+ h('div', ['To:']),
+ h('input.large-input.send-screen-input', {
+ name: 'address',
+ list: 'addresses',
+ placeholder: 'Address',
+ value: to,
+ onChange: e => this.setState({
+ to: e.target.value,
+ errors: {},
+ }),
+ onBlur: () => {
+ this.setErrorsFor('to')
+ },
+ onFocus: event => {
+ if (to) event.target.select()
+ this.clearErrorsFor('to')
+ },
+ }),
+ h('datalist#addresses', [
+ // Corresponds to the addresses owned.
+ Object.entries(identities).map(([key, { address, name }]) => {
+ return h('option', {
+ value: address,
+ label: name,
+ key: address,
+ })
+ }),
+ addressBook.map(({ address, name }) => {
+ return h('option', {
+ value: address,
+ label: name,
+ key: address,
+ })
+ }),
+ ]),
+ h('div.send-screen-input-wrapper__error-message', [ errorMessage ]),
+ ])
+}
+
+SendTokenScreen.prototype.renderAmountInput = function () {
+ const {
+ selectedCurrency,
+ amount,
+ errors: { amount: errorMessage },
+ } = this.state
+
+ const {
+ tokenExchangeRate,
+ selectedToken: {symbol},
+ } = this.props
+
+ return h('div.send-screen-input-wrapper', {
+ className: classnames('send-screen-input-wrapper', {
+ 'send-screen-input-wrapper--error': errorMessage,
+ }),
+ }, [
+ h('div.send-screen-amount-labels', [
+ h('span', ['Amount']),
+ h(CurrencyToggle, {
+ currentCurrency: tokenExchangeRate ? selectedCurrency : 'USD',
+ currencies: tokenExchangeRate ? [ symbol, 'USD' ] : [],
+ onClick: currency => this.setState({ selectedCurrency: currency }),
+ }),
+ ]),
+ h('input.large-input.send-screen-input', {
+ placeholder: `0 ${symbol}`,
+ type: 'number',
+ value: amount,
+ onChange: e => this.setState({
+ amount: e.target.value,
+ }),
+ onBlur: () => {
+ this.setErrorsFor('amount')
+ },
+ onFocus: () => this.clearErrorsFor('amount'),
+ }),
+ h('div.send-screen-input-wrapper__error-message', [ errorMessage ]),
+ ])
+}
+
+SendTokenScreen.prototype.renderGasInput = function () {
+ const {
+ isGasTooltipOpen,
+ gasPrice,
+ gasLimit,
+ selectedCurrency,
+ errors: {
+ gasPrice: gasPriceErrorMessage,
+ gasLimit: gasLimitErrorMessage,
+ },
+ } = this.state
+
+ const {
+ conversionRate,
+ tokenExchangeRate,
+ currentBlockGasLimit,
+ } = this.props
+
+ return h('div.send-screen-input-wrapper', {
+ className: classnames('send-screen-input-wrapper', {
+ 'send-screen-input-wrapper--error': gasPriceErrorMessage || gasLimitErrorMessage,
+ }),
+ }, [
+ isGasTooltipOpen && h(GasTooltip, {
+ className: 'send-tooltip',
+ gasPrice: gasPrice || '0x0',
+ gasLimit: gasLimit || '0x0',
+ onClose: () => this.setState({ isGasTooltipOpen: false }),
+ onFeeChange: ({ gasLimit, gasPrice }) => {
+ this.setState({ gasLimit, gasPrice, errors: {} })
+ },
+ onBlur: () => {
+ this.setErrorsFor('gasLimit')
+ this.setErrorsFor('gasPrice')
+ },
+ onFocus: () => {
+ this.clearErrorsFor('gasLimit')
+ this.clearErrorsFor('gasPrice')
+ },
+ }),
+
+ h('div.send-screen-gas-labels', {}, [
+ h('span', [ h('i.fa.fa-bolt'), 'Gas fee:']),
+ h('span', ['What\'s this?']),
+ ]),
+ h('div.large-input.send-screen-gas-input', [
+ h(GasFeeDisplay, {
+ conversionRate,
+ tokenExchangeRate,
+ gasPrice: gasPrice || '0x0',
+ activeCurrency: selectedCurrency,
+ gas: gasLimit || '0x0',
+ blockGasLimit: currentBlockGasLimit,
+ }),
+ h(
+ 'div.send-screen-gas-input-customize',
+ { onClick: () => this.setState({ isGasTooltipOpen: !isGasTooltipOpen }) },
+ ['Customize']
+ ),
+ ]),
+ h('div.send-screen-input-wrapper__error-message', [
+ gasPriceErrorMessage || gasLimitErrorMessage,
+ ]),
+ ])
+}
+
+SendTokenScreen.prototype.renderMemoInput = function () {
+ return h('div.send-screen-input-wrapper', [
+ h('div', {}, ['Transaction memo (optional)']),
+ h(
+ 'input.large-input.send-screen-input',
+ { onChange: e => this.setState({ memo: e.target.value }) }
+ ),
+ ])
+}
+
+SendTokenScreen.prototype.renderButtons = function () {
+ const { selectedAddress, backToAccountDetail } = this.props
+ const { isValid } = this.validate()
+
+ return h('div.send-token__button-group', [
+ h('button.send-token__button-next.btn-secondary', {
+ className: !isValid && 'send-screen__send-button__disabled',
+ onClick: () => isValid && this.submit(),
+ }, ['Next']),
+ h('button.send-token__button-cancel.btn-tertiary', {
+ onClick: () => backToAccountDetail(selectedAddress),
+ }, ['Cancel']),
+ ])
+}
+
+SendTokenScreen.prototype.render = function () {
+ const {
+ selectedTokenAddress,
+ selectedToken,
+ warning,
+ } = this.props
+
+ return h('div.send-token', [
+ h('div.send-token__content', [
+ h(Identicon, {
+ diameter: 75,
+ address: selectedTokenAddress,
+ }),
+ h('div.send-token__title', ['Send Tokens']),
+ h('div.send-token__description', ['Send Tokens to anyone with an Ethereum account']),
+ h('div.send-token__balance-text', ['Your Token Balance is:']),
+ h('div.send-token__token-balance', [
+ h(TokenBalance, { token: selectedToken, balanceOnly: true }),
+ ]),
+ h('div.send-token__token-symbol', [selectedToken.symbol]),
+ this.renderToAddressInput(),
+ this.renderAmountInput(),
+ this.renderGasInput(),
+ this.renderMemoInput(),
+ warning && h('div.send-screen-input-wrapper--error', {},
+ h('div.send-screen-input-wrapper__error-message', [
+ warning,
+ ])
+ ),
+ ]),
+ this.renderButtons(),
+ ])
+}
diff --git a/ui/app/components/send/account-list-item.js b/ui/app/components/send/account-list-item.js
new file mode 100644
index 000000000..1ad3f69c1
--- /dev/null
+++ b/ui/app/components/send/account-list-item.js
@@ -0,0 +1,73 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const connect = require('react-redux').connect
+const Identicon = require('../identicon')
+const CurrencyDisplay = require('./currency-display')
+const { conversionRateSelector, getCurrentCurrency } = require('../../selectors')
+
+inherits(AccountListItem, Component)
+function AccountListItem () {
+ Component.call(this)
+}
+
+function mapStateToProps (state) {
+ return {
+ conversionRate: conversionRateSelector(state),
+ currentCurrency: getCurrentCurrency(state),
+ }
+}
+
+module.exports = connect(mapStateToProps)(AccountListItem)
+
+AccountListItem.prototype.render = function () {
+ const {
+ className,
+ account,
+ handleClick,
+ icon = null,
+ conversionRate,
+ currentCurrency,
+ displayBalance = true,
+ displayAddress = false,
+ } = this.props
+
+ const { name, address, balance } = account || {}
+
+ return h('div.account-list-item', {
+ className,
+ onClick: () => handleClick({ name, address, balance }),
+ }, [
+
+ h('div.account-list-item__top-row', {}, [
+
+ h(
+ Identicon,
+ {
+ address,
+ diameter: 18,
+ className: 'account-list-item__identicon',
+ },
+ ),
+
+ h('div.account-list-item__account-name', {}, name || address),
+
+ icon && h('div.account-list-item__icon', [icon]),
+
+ ]),
+
+ displayAddress && name && h('div.account-list-item__account-address', address),
+
+ displayBalance && h(CurrencyDisplay, {
+ primaryCurrency: 'ETH',
+ convertedCurrency: currentCurrency,
+ value: balance,
+ conversionRate,
+ readOnly: true,
+ className: 'account-list-item__account-balances',
+ primaryBalanceClassName: 'account-list-item__account-primary-balance',
+ convertedBalanceClassName: 'account-list-item__account-secondary-balance',
+ }, name),
+
+ ])
+}
diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js
new file mode 100644
index 000000000..819fee0a0
--- /dev/null
+++ b/ui/app/components/send/currency-display.js
@@ -0,0 +1,116 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const CurrencyInput = require('../currency-input')
+const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
+
+module.exports = CurrencyDisplay
+
+inherits(CurrencyDisplay, Component)
+function CurrencyDisplay () {
+ Component.call(this)
+}
+
+function toHexWei (value) {
+ return conversionUtil(value, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ toDenomination: 'WEI',
+ })
+}
+
+CurrencyDisplay.prototype.getAmount = function (value) {
+ const { selectedToken } = this.props
+ const { decimals } = selectedToken || {}
+ const multiplier = Math.pow(10, Number(decimals || 0))
+
+ const sendAmount = multiplyCurrencies(value, multiplier, {toNumericBase: 'hex'})
+
+ return selectedToken
+ ? sendAmount
+ : toHexWei(value)
+}
+
+CurrencyDisplay.prototype.getValueToRender = function () {
+ const { selectedToken, conversionRate, value } = this.props
+
+ const { decimals, symbol } = selectedToken || {}
+ const multiplier = Math.pow(10, Number(decimals || 0))
+
+ return selectedToken
+ ? conversionUtil(value, {
+ fromNumericBase: 'hex',
+ toCurrency: symbol,
+ conversionRate: multiplier,
+ invertConversionRate: true,
+ })
+ : conversionUtil(value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ numberOfDecimals: 6,
+ conversionRate,
+ })
+}
+
+CurrencyDisplay.prototype.render = function () {
+ const {
+ className = 'currency-display',
+ primaryBalanceClassName = 'currency-display__input',
+ convertedBalanceClassName = 'currency-display__converted-value',
+ conversionRate,
+ primaryCurrency,
+ convertedCurrency,
+ readOnly = false,
+ inError = false,
+ handleChange,
+ } = this.props
+
+ const valueToRender = this.getValueToRender()
+
+ let convertedValue = conversionUtil(valueToRender, {
+ fromNumericBase: 'dec',
+ fromCurrency: primaryCurrency,
+ toCurrency: convertedCurrency,
+ numberOfDecimals: 2,
+ conversionRate,
+ })
+ convertedValue = Number(convertedValue).toFixed(2)
+
+ return h('div', {
+ className,
+ style: {
+ borderColor: inError ? 'red' : null,
+ },
+ onClick: () => this.currencyInput.focus(),
+ }, [
+
+ h('div.currency-display__primary-row', [
+
+ h('div.currency-display__input-wrapper', [
+
+ h(CurrencyInput, {
+ className: primaryBalanceClassName,
+ value: `${valueToRender}`,
+ placeholder: '0',
+ readOnly,
+ onInputChange: newValue => {
+ handleChange(this.getAmount(newValue))
+ },
+ inputRef: input => { this.currencyInput = input },
+ }),
+
+ h('span.currency-display__currency-symbol', primaryCurrency),
+
+ ]),
+
+ ]),
+
+ h('div', {
+ className: convertedBalanceClassName,
+ }, `${convertedValue} ${convertedCurrency.toUpperCase()}`),
+
+ ])
+
+}
+
diff --git a/ui/app/components/send/currency-toggle.js b/ui/app/components/send/currency-toggle.js
new file mode 100644
index 000000000..7aaccd490
--- /dev/null
+++ b/ui/app/components/send/currency-toggle.js
@@ -0,0 +1,44 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const classnames = require('classnames')
+
+module.exports = CurrencyToggle
+
+inherits(CurrencyToggle, Component)
+function CurrencyToggle () {
+ Component.call(this)
+}
+
+const defaultCurrencies = [ 'ETH', 'USD' ]
+
+CurrencyToggle.prototype.renderToggles = function () {
+ const { onClick, activeCurrency } = this.props
+ const [currencyA, currencyB] = this.props.currencies || defaultCurrencies
+
+ return [
+ h('span', {
+ className: classnames('currency-toggle__item', {
+ 'currency-toggle__item--selected': currencyA === activeCurrency,
+ }),
+ onClick: () => onClick(currencyA),
+ }, [ currencyA ]),
+ '<>',
+ h('span', {
+ className: classnames('currency-toggle__item', {
+ 'currency-toggle__item--selected': currencyB === activeCurrency,
+ }),
+ onClick: () => onClick(currencyB),
+ }, [ currencyB ]),
+ ]
+}
+
+CurrencyToggle.prototype.render = function () {
+ const currencies = this.props.currencies || defaultCurrencies
+
+ return h('span.currency-toggle', currencies.length
+ ? this.renderToggles()
+ : []
+ )
+}
+
diff --git a/ui/app/components/send/eth-fee-display.js b/ui/app/components/send/eth-fee-display.js
new file mode 100644
index 000000000..9eda5ec62
--- /dev/null
+++ b/ui/app/components/send/eth-fee-display.js
@@ -0,0 +1,37 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const EthBalance = require('../eth-balance')
+const { getTxFeeBn } = require('../../util')
+
+module.exports = EthFeeDisplay
+
+inherits(EthFeeDisplay, Component)
+function EthFeeDisplay () {
+ Component.call(this)
+}
+
+EthFeeDisplay.prototype.render = function () {
+ const {
+ activeCurrency,
+ conversionRate,
+ gas,
+ gasPrice,
+ blockGasLimit,
+ } = this.props
+
+ return h(EthBalance, {
+ value: getTxFeeBn(gas, gasPrice, blockGasLimit),
+ currentCurrency: activeCurrency,
+ conversionRate,
+ showFiat: false,
+ hideTooltip: true,
+ styleOveride: {
+ color: '#5d5d5d',
+ fontSize: '16px',
+ fontFamily: 'DIN OT',
+ lineHeight: '22.4px',
+ },
+ })
+}
+
diff --git a/ui/app/components/send/from-dropdown.js b/ui/app/components/send/from-dropdown.js
new file mode 100644
index 000000000..0686fbe73
--- /dev/null
+++ b/ui/app/components/send/from-dropdown.js
@@ -0,0 +1,72 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const AccountListItem = require('./account-list-item')
+
+module.exports = FromDropdown
+
+inherits(FromDropdown, Component)
+function FromDropdown () {
+ Component.call(this)
+}
+
+FromDropdown.prototype.getListItemIcon = function (currentAccount, selectedAccount) {
+ const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } })
+
+ return currentAccount.address === selectedAccount.address
+ ? listItemIcon
+ : null
+}
+
+FromDropdown.prototype.renderDropdown = function () {
+ const {
+ accounts,
+ selectedAccount,
+ closeDropdown,
+ onSelect,
+ } = this.props
+
+ return h('div', {}, [
+
+ h('div.send-v2__from-dropdown__close-area', {
+ onClick: closeDropdown,
+ }),
+
+ h('div.send-v2__from-dropdown__list', {}, [
+
+ ...accounts.map(account => h(AccountListItem, {
+ className: 'account-list-item__dropdown',
+ account,
+ handleClick: () => {
+ onSelect(account)
+ closeDropdown()
+ },
+ icon: this.getListItemIcon(account, selectedAccount),
+ })),
+
+ ]),
+
+ ])
+}
+
+FromDropdown.prototype.render = function () {
+ const {
+ selectedAccount,
+ openDropdown,
+ dropdownOpen,
+ } = this.props
+
+ return h('div.send-v2__from-dropdown', {}, [
+
+ h(AccountListItem, {
+ account: selectedAccount,
+ handleClick: openDropdown,
+ icon: h(`i.fa.fa-caret-down.fa-lg`, { style: { color: '#dedede' } }),
+ }),
+
+ dropdownOpen && this.renderDropdown(),
+
+ ])
+
+}
+
diff --git a/ui/app/components/send/gas-fee-display-v2.js b/ui/app/components/send/gas-fee-display-v2.js
new file mode 100644
index 000000000..806c33f0a
--- /dev/null
+++ b/ui/app/components/send/gas-fee-display-v2.js
@@ -0,0 +1,43 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const CurrencyDisplay = require('./currency-display')
+
+module.exports = GasFeeDisplay
+
+inherits(GasFeeDisplay, Component)
+function GasFeeDisplay () {
+ Component.call(this)
+}
+
+GasFeeDisplay.prototype.render = function () {
+ const {
+ conversionRate,
+ gasTotal,
+ onClick,
+ primaryCurrency = 'ETH',
+ convertedCurrency,
+ } = this.props
+
+ return h('div.send-v2__gas-fee-display', [
+
+ gasTotal
+ ? h(CurrencyDisplay, {
+ primaryCurrency,
+ convertedCurrency,
+ value: gasTotal,
+ conversionRate,
+ convertedPrefix: '$',
+ readOnly: true,
+ })
+ : h('div.currency-display', 'Loading...'),
+
+ h('div.send-v2__sliders-icon-container', {
+ onClick,
+ }, [
+ h('i.fa.fa-sliders.send-v2__sliders-icon'),
+ ]),
+
+ ])
+}
+
diff --git a/ui/app/components/send/gas-fee-display.js b/ui/app/components/send/gas-fee-display.js
new file mode 100644
index 000000000..a9a3f3f49
--- /dev/null
+++ b/ui/app/components/send/gas-fee-display.js
@@ -0,0 +1,62 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const USDFeeDisplay = require('./usd-fee-display')
+const EthFeeDisplay = require('./eth-fee-display')
+const { getTxFeeBn, formatBalance, shortenBalance } = require('../../util')
+
+module.exports = GasFeeDisplay
+
+inherits(GasFeeDisplay, Component)
+function GasFeeDisplay () {
+ Component.call(this)
+}
+
+GasFeeDisplay.prototype.getTokenValue = function () {
+ const {
+ tokenExchangeRate,
+ gas,
+ gasPrice,
+ blockGasLimit,
+ } = this.props
+
+ const value = formatBalance(getTxFeeBn(gas, gasPrice, blockGasLimit), 6, true)
+ const [ethNumber] = value.split(' ')
+
+ return shortenBalance(Number(ethNumber) / tokenExchangeRate, 6)
+}
+
+GasFeeDisplay.prototype.render = function () {
+ const {
+ activeCurrency,
+ conversionRate,
+ gas,
+ gasPrice,
+ blockGasLimit,
+ } = this.props
+
+ switch (activeCurrency) {
+ case 'USD':
+ return h(USDFeeDisplay, {
+ activeCurrency,
+ conversionRate,
+ gas,
+ gasPrice,
+ blockGasLimit,
+ })
+ case 'ETH':
+ return h(EthFeeDisplay, {
+ activeCurrency,
+ conversionRate,
+ gas,
+ gasPrice,
+ blockGasLimit,
+ })
+ default:
+ return h('div.token-gas', [
+ h('div.token-gas__amount', this.getTokenValue()),
+ h('div.token-gas__symbol', activeCurrency),
+ ])
+ }
+}
+
diff --git a/ui/app/components/send/gas-tooltip.js b/ui/app/components/send/gas-tooltip.js
new file mode 100644
index 000000000..46aff3499
--- /dev/null
+++ b/ui/app/components/send/gas-tooltip.js
@@ -0,0 +1,100 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const InputNumber = require('../input-number.js')
+
+module.exports = GasTooltip
+
+inherits(GasTooltip, Component)
+function GasTooltip () {
+ Component.call(this)
+ this.state = {
+ gasLimit: 0,
+ gasPrice: 0,
+ }
+
+ this.updateGasPrice = this.updateGasPrice.bind(this)
+ this.updateGasLimit = this.updateGasLimit.bind(this)
+ this.onClose = this.onClose.bind(this)
+}
+
+GasTooltip.prototype.componentWillMount = function () {
+ const { gasPrice = 0, gasLimit = 0} = this.props
+
+ this.setState({
+ gasPrice: parseInt(gasPrice, 16) / 1000000000,
+ gasLimit: parseInt(gasLimit, 16),
+ })
+}
+
+GasTooltip.prototype.updateGasPrice = function (newPrice) {
+ const { onFeeChange } = this.props
+ const { gasLimit } = this.state
+
+ this.setState({ gasPrice: newPrice })
+ onFeeChange({
+ gasLimit: gasLimit.toString(16),
+ gasPrice: (newPrice * 1000000000).toString(16),
+ })
+}
+
+GasTooltip.prototype.updateGasLimit = function (newLimit) {
+ const { onFeeChange } = this.props
+ const { gasPrice } = this.state
+
+ this.setState({ gasLimit: newLimit })
+ onFeeChange({
+ gasLimit: newLimit.toString(16),
+ gasPrice: (gasPrice * 1000000000).toString(16),
+ })
+}
+
+GasTooltip.prototype.onClose = function (e) {
+ e.stopPropagation()
+ this.props.onClose()
+}
+
+GasTooltip.prototype.render = function () {
+ const { gasPrice, gasLimit } = this.state
+
+ return h('div.gas-tooltip', {}, [
+ h('div.gas-tooltip-close-area', {
+ onClick: this.onClose,
+ }),
+ h('div.customize-gas-tooltip-container', {}, [
+ h('div.customize-gas-tooltip', {}, [
+ h('div.gas-tooltip-header.gas-tooltip-label', {}, ['Customize Gas']),
+ h('div.gas-tooltip-input-label', {}, [
+ h('span.gas-tooltip-label', {}, ['Gas Price']),
+ h('i.fa.fa-info-circle'),
+ ]),
+ h(InputNumber, {
+ unitLabel: 'GWEI',
+ step: 1,
+ min: 0,
+ placeholder: '0',
+ value: gasPrice,
+ onChange: (newPrice) => this.updateGasPrice(newPrice),
+ }),
+ h('div.gas-tooltip-input-label', {
+ style: {
+ 'marginTop': '81px',
+ },
+ }, [
+ h('span.gas-tooltip-label', {}, ['Gas Limit']),
+ h('i.fa.fa-info-circle'),
+ ]),
+ h(InputNumber, {
+ unitLabel: 'UNITS',
+ step: 1,
+ min: 0,
+ placeholder: '0',
+ value: gasLimit,
+ onChange: (newLimit) => this.updateGasLimit(newLimit),
+ }),
+ ]),
+ h('div.gas-tooltip-arrow', {}),
+ ]),
+ ])
+}
+
diff --git a/ui/app/components/send/memo-textarea.js b/ui/app/components/send/memo-textarea.js
new file mode 100644
index 000000000..f4bb24bf8
--- /dev/null
+++ b/ui/app/components/send/memo-textarea.js
@@ -0,0 +1,33 @@
+// const Component = require('react').Component
+// const h = require('react-hyperscript')
+// const inherits = require('util').inherits
+// const Identicon = require('../identicon')
+
+// module.exports = MemoTextArea
+
+// inherits(MemoTextArea, Component)
+// function MemoTextArea () {
+// Component.call(this)
+// }
+
+// MemoTextArea.prototype.render = function () {
+// const { memo, identities, onChange } = this.props
+
+// return h('div.send-v2__memo-text-area', [
+
+// h('textarea.send-v2__memo-text-area__input', {
+// placeholder: 'Optional',
+// value: memo,
+// onChange,
+// // onBlur: () => {
+// // this.setErrorsFor('memo')
+// // },
+// onFocus: event => {
+// // this.clearErrorsFor('memo')
+// },
+// }),
+
+// ])
+
+// }
+
diff --git a/ui/app/components/send/send-constants.js b/ui/app/components/send/send-constants.js
new file mode 100644
index 000000000..9c240972f
--- /dev/null
+++ b/ui/app/components/send/send-constants.js
@@ -0,0 +1,30 @@
+const ethUtil = require('ethereumjs-util')
+const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
+
+const MIN_GAS_PRICE_HEX = (100000000).toString(16)
+const MIN_GAS_PRICE_DEC = '100000000'
+const MIN_GAS_LIMIT_DEC = '21000'
+const MIN_GAS_LIMIT_HEX = (parseInt(MIN_GAS_LIMIT_DEC)).toString(16)
+
+const MIN_GAS_PRICE_GWEI = ethUtil.addHexPrefix(conversionUtil(MIN_GAS_PRICE_HEX, {
+ fromDenomination: 'WEI',
+ toDenomination: 'GWEI',
+ fromNumericBase: 'hex',
+ toNumericBase: 'hex',
+ numberOfDecimals: 1,
+}))
+
+const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT_HEX, MIN_GAS_PRICE_HEX, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+})
+
+module.exports = {
+ MIN_GAS_PRICE_GWEI,
+ MIN_GAS_PRICE_HEX,
+ MIN_GAS_PRICE_DEC,
+ MIN_GAS_LIMIT_HEX,
+ MIN_GAS_LIMIT_DEC,
+ MIN_GAS_TOTAL,
+}
diff --git a/ui/app/components/send/send-utils.js b/ui/app/components/send/send-utils.js
new file mode 100644
index 000000000..d8211930d
--- /dev/null
+++ b/ui/app/components/send/send-utils.js
@@ -0,0 +1,68 @@
+const {
+ addCurrencies,
+ conversionUtil,
+ conversionGTE,
+} = require('../../conversion-util')
+const {
+ calcTokenAmount,
+} = require('../../token-util')
+
+function isBalanceSufficient ({
+ amount = '0x0',
+ gasTotal = '0x0',
+ balance,
+ primaryCurrency,
+ amountConversionRate,
+ conversionRate,
+}) {
+ const totalAmount = addCurrencies(amount, gasTotal, {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ })
+
+ const balanceIsSufficient = conversionGTE(
+ {
+ value: balance,
+ fromNumericBase: 'hex',
+ fromCurrency: primaryCurrency,
+ conversionRate,
+ },
+ {
+ value: totalAmount,
+ fromNumericBase: 'hex',
+ conversionRate: amountConversionRate,
+ fromCurrency: primaryCurrency,
+ },
+ )
+
+ return balanceIsSufficient
+}
+
+function isTokenBalanceSufficient ({
+ amount = '0x0',
+ tokenBalance,
+ decimals,
+}) {
+ const amountInDec = conversionUtil(amount, {
+ fromNumericBase: 'hex',
+ })
+
+ const tokenBalanceIsSufficient = conversionGTE(
+ {
+ value: tokenBalance,
+ fromNumericBase: 'dec',
+ },
+ {
+ value: calcTokenAmount(amountInDec, decimals),
+ fromNumericBase: 'dec',
+ },
+ )
+
+ return tokenBalanceIsSufficient
+}
+
+module.exports = {
+ isBalanceSufficient,
+ isTokenBalanceSufficient,
+}
diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js
new file mode 100644
index 000000000..4451a6113
--- /dev/null
+++ b/ui/app/components/send/send-v2-container.js
@@ -0,0 +1,82 @@
+const connect = require('react-redux').connect
+const actions = require('../../actions')
+const abi = require('ethereumjs-abi')
+const SendEther = require('../../send-v2')
+
+const {
+ accountsWithSendEtherInfoSelector,
+ getCurrentAccountWithSendEtherInfo,
+ conversionRateSelector,
+ getSelectedToken,
+ getSelectedAddress,
+ getAddressBook,
+ getSendFrom,
+ getCurrentCurrency,
+ getSelectedTokenToFiatRate,
+ getSelectedTokenContract,
+} = require('../../selectors')
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther)
+
+function mapStateToProps (state) {
+ const fromAccounts = accountsWithSendEtherInfoSelector(state)
+ const selectedAddress = getSelectedAddress(state)
+ const selectedToken = getSelectedToken(state)
+ const conversionRate = conversionRateSelector(state)
+
+ let data
+ let primaryCurrency
+ let tokenToFiatRate
+ if (selectedToken) {
+ data = Array.prototype.map.call(
+ abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']),
+ x => ('00' + x.toString(16)).slice(-2)
+ ).join('')
+
+ primaryCurrency = selectedToken.symbol
+
+ tokenToFiatRate = getSelectedTokenToFiatRate(state)
+ }
+
+ return {
+ ...state.metamask.send,
+ from: getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state),
+ fromAccounts,
+ toAccounts: [...fromAccounts, ...getAddressBook(state)],
+ conversionRate,
+ selectedToken,
+ primaryCurrency,
+ convertedCurrency: getCurrentCurrency(state),
+ data,
+ amountConversionRate: selectedToken ? tokenToFiatRate : conversionRate,
+ tokenContract: getSelectedTokenContract(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showCustomizeGasModal: () => dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })),
+ estimateGas: params => dispatch(actions.estimateGas(params)),
+ getGasPrice: () => dispatch(actions.getGasPrice()),
+ updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)),
+ signTokenTx: (tokenAddress, toAddress, amount, txData) => (
+ dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData))
+ ),
+ signTx: txParams => dispatch(actions.signTx(txParams)),
+ updateAndApproveTx: txParams => dispatch(actions.updateAndApproveTx(txParams)),
+ setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)),
+ addToAddressBook: address => dispatch(actions.addToAddressBook(address)),
+ updateGasTotal: newTotal => dispatch(actions.updateGasTotal(newTotal)),
+ updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)),
+ updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)),
+ updateSendTokenBalance: tokenBalance => dispatch(actions.updateSendTokenBalance(tokenBalance)),
+ updateSendFrom: newFrom => dispatch(actions.updateSendFrom(newFrom)),
+ updateSendTo: newTo => dispatch(actions.updateSendTo(newTo)),
+ updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
+ updateSendMemo: newMemo => dispatch(actions.updateSendMemo(newMemo)),
+ updateSendErrors: newError => dispatch(actions.updateSendErrors(newError)),
+ goHome: () => dispatch(actions.goHome()),
+ clearSend: () => dispatch(actions.clearSend()),
+ backToConfirmScreen: editingTransactionId => dispatch(actions.showConfTxPage({ id: editingTransactionId })),
+ }
+}
diff --git a/ui/app/components/send/to-autocomplete.js b/ui/app/components/send/to-autocomplete.js
new file mode 100644
index 000000000..e0cdd0a58
--- /dev/null
+++ b/ui/app/components/send/to-autocomplete.js
@@ -0,0 +1,114 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const AccountListItem = require('./account-list-item')
+
+module.exports = ToAutoComplete
+
+inherits(ToAutoComplete, Component)
+function ToAutoComplete () {
+ Component.call(this)
+
+ this.state = { accountsToRender: [] }
+}
+
+ToAutoComplete.prototype.getListItemIcon = function (listItemAddress, toAddress) {
+ const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } })
+
+ return toAddress && listItemAddress === toAddress
+ ? listItemIcon
+ : null
+}
+
+ToAutoComplete.prototype.renderDropdown = function () {
+ const {
+ closeDropdown,
+ onChange,
+ to,
+ } = this.props
+ const { accountsToRender } = this.state
+
+ return accountsToRender.length && h('div', {}, [
+
+ h('div.send-v2__from-dropdown__close-area', {
+ onClick: closeDropdown,
+ }),
+
+ h('div.send-v2__from-dropdown__list', {}, [
+
+ ...accountsToRender.map(account => h(AccountListItem, {
+ account,
+ className: 'account-list-item__dropdown',
+ handleClick: () => {
+ onChange(account.address)
+ closeDropdown()
+ },
+ icon: this.getListItemIcon(account.address, to),
+ displayBalance: false,
+ displayAddress: true,
+ })),
+
+ ]),
+
+ ])
+}
+
+ToAutoComplete.prototype.handleInputEvent = function (event = {}, cb) {
+ const {
+ to,
+ accounts,
+ closeDropdown,
+ openDropdown,
+ } = this.props
+
+ const matchingAccounts = accounts.filter(({ address }) => address.match(to || ''))
+ const matches = matchingAccounts.length
+
+ if (!matches || matchingAccounts[0].address === to) {
+ this.setState({ accountsToRender: [] })
+ event.target && event.target.select()
+ closeDropdown()
+ } else {
+ this.setState({ accountsToRender: matchingAccounts })
+ openDropdown()
+ }
+ cb && cb(event.target.value)
+}
+
+ToAutoComplete.prototype.componentDidUpdate = function (nextProps, nextState) {
+ if (this.props.to !== nextProps.to) {
+ this.handleInputEvent()
+ }
+}
+
+ToAutoComplete.prototype.render = function () {
+ const {
+ to,
+ dropdownOpen,
+ onChange,
+ inError,
+ } = this.props
+
+ return h('div.send-v2__to-autocomplete', {}, [
+
+ h('input.send-v2__to-autocomplete__input', {
+ placeholder: 'Recipient Address',
+ className: inError ? `send-v2__error-border` : '',
+ value: to,
+ onChange: event => onChange(event.target.value),
+ onFocus: event => this.handleInputEvent(event),
+ style: {
+ borderColor: inError ? 'red' : null,
+ },
+ }),
+
+ !to && h(`i.fa.fa-caret-down.fa-lg.send-v2__to-autocomplete__down-caret`, {
+ style: { color: '#dedede' },
+ onClick: () => this.handleInputEvent(),
+ }),
+
+ dropdownOpen && this.renderDropdown(),
+
+ ])
+}
+
diff --git a/ui/app/components/send/usd-fee-display.js b/ui/app/components/send/usd-fee-display.js
new file mode 100644
index 000000000..4cf31a493
--- /dev/null
+++ b/ui/app/components/send/usd-fee-display.js
@@ -0,0 +1,35 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const FiatValue = require('../fiat-value')
+const { getTxFeeBn } = require('../../util')
+
+module.exports = USDFeeDisplay
+
+inherits(USDFeeDisplay, Component)
+function USDFeeDisplay () {
+ Component.call(this)
+}
+
+USDFeeDisplay.prototype.render = function () {
+ const {
+ activeCurrency,
+ conversionRate,
+ gas,
+ gasPrice,
+ blockGasLimit,
+ } = this.props
+
+ return h(FiatValue, {
+ value: getTxFeeBn(gas, gasPrice, blockGasLimit),
+ conversionRate,
+ currentCurrency: activeCurrency,
+ style: {
+ color: '#5d5d5d',
+ fontSize: '16px',
+ fontFamily: 'DIN OT',
+ lineHeight: '22.4px',
+ },
+ })
+}
+
diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js
index b555dee84..43973de63 100644
--- a/ui/app/components/shift-list-item.js
+++ b/ui/app/components/shift-list-item.js
@@ -29,7 +29,7 @@ function ShiftListItem () {
ShiftListItem.prototype.render = function () {
return (
- h('.transaction-list-item.flex-row', {
+ h('div.tx-list-item.tx-list-clickable', {
style: {
paddingTop: '20px',
paddingBottom: '20px',
diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js
new file mode 100644
index 000000000..c5cc23aa8
--- /dev/null
+++ b/ui/app/components/signature-request.js
@@ -0,0 +1,253 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const Identicon = require('./identicon')
+const connect = require('react-redux').connect
+const ethUtil = require('ethereumjs-util')
+const classnames = require('classnames')
+
+const AccountDropdownMini = require('./dropdowns/account-dropdown-mini')
+
+const actions = require('../actions')
+const { conversionUtil } = require('../conversion-util')
+
+const {
+ getSelectedAccount,
+ getCurrentAccountWithSendEtherInfo,
+ getSelectedAddress,
+ accountsWithSendEtherInfoSelector,
+ conversionRateSelector,
+} = require('../selectors.js')
+
+function mapStateToProps (state) {
+ return {
+ balance: getSelectedAccount(state).balance,
+ selectedAccount: getCurrentAccountWithSendEtherInfo(state),
+ selectedAddress: getSelectedAddress(state),
+ requester: null,
+ requesterAddress: null,
+ accounts: accountsWithSendEtherInfoSelector(state),
+ conversionRate: conversionRateSelector(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ goHome: () => dispatch(actions.goHome()),
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(SignatureRequest)
+
+inherits(SignatureRequest, Component)
+function SignatureRequest (props) {
+ Component.call(this)
+
+ this.state = {
+ selectedAccount: props.selectedAccount,
+ accountDropdownOpen: false,
+ }
+}
+
+SignatureRequest.prototype.renderHeader = function () {
+ return h('div.request-signature__header', [
+
+ h('div.request-signature__header-background'),
+
+ h('div.request-signature__header__text', 'Signature Request'),
+
+ h('div.request-signature__header__tip-container', [
+ h('div.request-signature__header__tip'),
+ ]),
+
+ ])
+}
+
+SignatureRequest.prototype.renderAccountDropdown = function () {
+ const {
+ selectedAccount,
+ accountDropdownOpen,
+ } = this.state
+
+ const {
+ accounts,
+ } = this.props
+
+ return h('div.request-signature__account', [
+
+ h('div.request-signature__account-text', ['Account:']),
+
+ h(AccountDropdownMini, {
+ selectedAccount,
+ accounts,
+ onSelect: selectedAccount => this.setState({ selectedAccount }),
+ dropdownOpen: accountDropdownOpen,
+ openDropdown: () => this.setState({ accountDropdownOpen: true }),
+ closeDropdown: () => this.setState({ accountDropdownOpen: false }),
+ }),
+
+ ])
+}
+
+SignatureRequest.prototype.renderBalance = function () {
+ const { balance, conversionRate } = this.props
+
+ const balanceInEther = conversionUtil(balance, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ numberOfDecimals: 6,
+ conversionRate,
+ })
+
+ return h('div.request-signature__balance', [
+
+ h('div.request-signature__balance-text', ['Balance:']),
+
+ h('div.request-signature__balance-value', `${balanceInEther} ETH`),
+
+ ])
+}
+
+SignatureRequest.prototype.renderAccountInfo = function () {
+ return h('div.request-signature__account-info', [
+
+ this.renderAccountDropdown(),
+
+ this.renderRequestIcon(),
+
+ this.renderBalance(),
+
+ ])
+}
+
+SignatureRequest.prototype.renderRequestIcon = function () {
+ const { requesterAddress } = this.props
+
+ return h('div.request-signature__request-icon', [
+ h(Identicon, {
+ diameter: 40,
+ address: requesterAddress,
+ }),
+ ])
+}
+
+SignatureRequest.prototype.renderRequestInfo = function () {
+ return h('div.request-signature__request-info', [
+
+ h('div.request-signature__headline', [
+ `Your signature is being requested`,
+ ]),
+
+ ])
+}
+
+SignatureRequest.prototype.msgHexToText = function (hex) {
+ try {
+ const stripped = ethUtil.stripHexPrefix(hex)
+ const buff = Buffer.from(stripped, 'hex')
+ return buff.toString('utf8')
+ } catch (e) {
+ return hex
+ }
+}
+
+SignatureRequest.prototype.renderBody = function () {
+ let rows
+ let notice = 'You are signing:'
+
+ const { txData } = this.props
+ const { type, msgParams: { data } } = txData
+
+ if (type === 'personal_sign') {
+ rows = [{ name: 'Message', value: this.msgHexToText(data) }]
+ } else if (type === 'eth_signTypedData') {
+ rows = data
+ } else if (type === 'eth_sign') {
+ rows = [{ name: 'Message', value: data }]
+ notice = `Signing this message can have
+ dangerous side effects. Only sign messages from
+ sites you fully trust with your entire account.
+ This dangerous method will be removed in a future version. `
+ }
+
+ return h('div.request-signature__body', {}, [
+
+ this.renderAccountInfo(),
+
+ this.renderRequestInfo(),
+
+ h('div.request-signature__notice', {
+ className: classnames({
+ 'request-signature__notice': type === 'personal_sign' || type === 'eth_signTypedData',
+ 'request-signature__warning': type === 'eth_sign',
+ }),
+ }, [notice]),
+
+ h('div.request-signature__rows', [
+
+ ...rows.map(({ name, value }) => {
+ return h('div.request-signature__row', [
+ h('div.request-signature__row-title', [`${name}:`]),
+ h('div.request-signature__row-value', value),
+ ])
+ }),
+
+ ]),
+
+ ])
+}
+
+SignatureRequest.prototype.renderFooter = function () {
+ const {
+ signPersonalMessage,
+ signTypedMessage,
+ cancelPersonalMessage,
+ cancelTypedMessage,
+ signMessage,
+ cancelMessage,
+ } = this.props
+
+ const { txData } = this.props
+ const { type } = txData
+
+ let cancel
+ let sign
+ if (type === 'personal_sign') {
+ cancel = cancelPersonalMessage
+ sign = signPersonalMessage
+ } else if (type === 'eth_signTypedData') {
+ cancel = cancelTypedMessage
+ sign = signTypedMessage
+ } else if (type === 'eth_sign') {
+ cancel = cancelMessage
+ sign = signMessage
+ }
+
+ return h('div.request-signature__footer', [
+ h('button.request-signature__footer__cancel-button', {
+ onClick: cancel,
+ }, 'CANCEL'),
+ h('button.request-signature__footer__sign-button', {
+ onClick: sign,
+ }, 'SIGN'),
+ ])
+}
+
+SignatureRequest.prototype.render = function () {
+ return (
+
+ h('div.request-signature__container', [
+
+ this.renderHeader(),
+
+ this.renderBody(),
+
+ this.renderFooter(),
+
+ ])
+
+ )
+
+}
+
diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js
index bef444a48..0edced119 100644
--- a/ui/app/components/tab-bar.js
+++ b/ui/app/components/tab-bar.js
@@ -1,37 +1,47 @@
-const Component = require('react').Component
+const { Component } = require('react')
const h = require('react-hyperscript')
-const inherits = require('util').inherits
+const PropTypes = require('react').PropTypes
+const classnames = require('classnames')
-module.exports = TabBar
+class TabBar extends Component {
+ constructor (props) {
+ super(props)
+ const { defaultTab, tabs } = props
-inherits(TabBar, Component)
-function TabBar () {
- Component.call(this)
-}
+ this.state = {
+ subview: defaultTab || tabs[0].key,
+ }
+ }
-TabBar.prototype.render = function () {
- const props = this.props
- const state = this.state || {}
- const { tabs = [], defaultTab, tabSelected } = props
- const { subview = defaultTab } = state
+ render () {
+ const { tabs = [], tabSelected } = this.props
+ const { subview } = this.state
- return (
- h('.flex-row.space-around.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- paddingTop: '4px',
- minHeight: '30px',
- },
- }, tabs.map((tab) => {
- const { key, content } = tab
- return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', {
- onClick: () => {
- this.setState({ subview: key })
- tabSelected(key)
- },
- }, content)
- }))
- )
+ return (
+ h('.tab-bar', {}, [
+ tabs.map((tab) => {
+ const { key, content } = tab
+ return h('div', {
+ className: classnames('tab-bar__tab pointer', {
+ 'tab-bar__tab--active': subview === key,
+ }),
+ onClick: () => {
+ this.setState({ subview: key })
+ tabSelected(key)
+ },
+ key,
+ }, content)
+ }),
+ h('div.tab-bar__tab.tab-bar__grow-tab'),
+ ])
+ )
+ }
}
+TabBar.propTypes = {
+ defaultTab: PropTypes.string,
+ tabs: PropTypes.array,
+ tabSelected: PropTypes.func,
+}
+
+module.exports = TabBar
diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js
new file mode 100644
index 000000000..2f71c0687
--- /dev/null
+++ b/ui/app/components/token-balance.js
@@ -0,0 +1,113 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const TokenTracker = require('eth-token-tracker')
+const connect = require('react-redux').connect
+const selectors = require('../selectors')
+
+function mapStateToProps (state) {
+ return {
+ userAddress: selectors.getSelectedAddress(state),
+ }
+}
+
+module.exports = connect(mapStateToProps)(TokenBalance)
+
+
+inherits(TokenBalance, Component)
+function TokenBalance () {
+ this.state = {
+ string: '',
+ symbol: '',
+ isLoading: true,
+ error: null,
+ }
+ Component.call(this)
+}
+
+TokenBalance.prototype.render = function () {
+ const state = this.state
+ const { symbol, string, isLoading } = state
+ const { balanceOnly } = this.props
+
+ return isLoading
+ ? h('span', '')
+ : h('span.token-balance', [
+ h('span.token-balance__amount', string),
+ !balanceOnly && h('span.token-balance__symbol', symbol),
+ ])
+}
+
+TokenBalance.prototype.componentDidMount = function () {
+ this.createFreshTokenTracker()
+}
+
+TokenBalance.prototype.createFreshTokenTracker = function () {
+ if (this.tracker) {
+ // Clean up old trackers when refreshing:
+ this.tracker.stop()
+ this.tracker.removeListener('update', this.balanceUpdater)
+ this.tracker.removeListener('error', this.showError)
+ }
+
+ if (!global.ethereumProvider) return
+ const { userAddress, token } = this.props
+
+ this.tracker = new TokenTracker({
+ userAddress,
+ provider: global.ethereumProvider,
+ tokens: [token],
+ pollingInterval: 8000,
+ })
+
+
+ // Set up listener instances for cleaning up
+ this.balanceUpdater = this.updateBalance.bind(this)
+ this.showError = error => {
+ this.setState({ error, isLoading: false })
+ }
+ this.tracker.on('update', this.balanceUpdater)
+ this.tracker.on('error', this.showError)
+
+ this.tracker.updateBalances()
+ .then(() => {
+ this.updateBalance(this.tracker.serialize())
+ })
+ .catch((reason) => {
+ log.error(`Problem updating balances`, reason)
+ this.setState({ isLoading: false })
+ })
+}
+
+TokenBalance.prototype.componentDidUpdate = function (nextProps) {
+ const {
+ userAddress: oldAddress,
+ token: { address: oldTokenAddress },
+ } = this.props
+ const {
+ userAddress: newAddress,
+ token: { address: newTokenAddress },
+ } = nextProps
+
+ if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) return
+ if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) return
+
+ this.setState({ isLoading: true })
+ this.createFreshTokenTracker()
+}
+
+TokenBalance.prototype.updateBalance = function (tokens = []) {
+ const [{ string, symbol }] = tokens
+
+ this.setState({
+ string,
+ symbol,
+ isLoading: false,
+ })
+}
+
+TokenBalance.prototype.componentWillUnmount = function () {
+ if (!this.tracker) return
+ this.tracker.stop()
+}
+
diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js
index 19d7139bb..b40c0ec0d 100644
--- a/ui/app/components/token-cell.js
+++ b/ui/app/components/token-cell.js
@@ -1,35 +1,133 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
+const connect = require('react-redux').connect
const Identicon = require('./identicon')
const prefixForNetwork = require('../../lib/etherscan-prefix-for-network')
+const selectors = require('../selectors')
+const actions = require('../actions')
+const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
-module.exports = TokenCell
+const TokenMenuDropdown = require('./dropdowns/token-menu-dropdown.js')
+
+function mapStateToProps (state) {
+ return {
+ network: state.metamask.network,
+ currentCurrency: state.metamask.currentCurrency,
+ selectedTokenAddress: state.metamask.selectedTokenAddress,
+ userAddress: selectors.getSelectedAddress(state),
+ tokenExchangeRates: state.metamask.tokenExchangeRates,
+ conversionRate: state.metamask.conversionRate,
+ sidebarOpen: state.appState.sidebarOpen,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ setSelectedToken: address => dispatch(actions.setSelectedToken(address)),
+ updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)),
+ hideSidebar: () => dispatch(actions.hideSidebar()),
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenCell)
inherits(TokenCell, Component)
function TokenCell () {
Component.call(this)
+
+ this.state = {
+ tokenMenuOpen: false,
+ }
+}
+
+TokenCell.prototype.componentWillMount = function () {
+ const {
+ updateTokenExchangeRate,
+ symbol,
+ } = this.props
+
+ updateTokenExchangeRate(symbol)
}
TokenCell.prototype.render = function () {
+ const { tokenMenuOpen } = this.state
const props = this.props
- const { address, symbol, string, network, userAddress } = props
+ const {
+ address,
+ symbol,
+ string,
+ network,
+ setSelectedToken,
+ selectedTokenAddress,
+ tokenExchangeRates,
+ conversionRate,
+ hideSidebar,
+ sidebarOpen,
+ currentCurrency,
+ // userAddress,
+ } = props
+
+ const pair = `${symbol.toLowerCase()}_eth`
+
+ let currentTokenToFiatRate
+ let currentTokenInFiat
+ let formattedFiat = ''
+
+ if (tokenExchangeRates[pair]) {
+ currentTokenToFiatRate = multiplyCurrencies(
+ tokenExchangeRates[pair].rate,
+ conversionRate
+ )
+ currentTokenInFiat = conversionUtil(string, {
+ fromNumericBase: 'dec',
+ fromCurrency: symbol,
+ toCurrency: currentCurrency.toUpperCase(),
+ numberOfDecimals: 2,
+ conversionRate: currentTokenToFiatRate,
+ })
+ formattedFiat = `${currentTokenInFiat} ${currentCurrency.toUpperCase()}`
+ }
+
+ const showFiat = Boolean(currentTokenInFiat) && currentCurrency.toUpperCase() !== symbol
return (
- h('li.token-cell', {
- style: { cursor: network === '1' ? 'pointer' : 'default' },
- onClick: this.view.bind(this, address, userAddress, network),
+ h('div.token-list-item', {
+ className: `token-list-item ${selectedTokenAddress === address ? 'token-list-item--active' : ''}`,
+ // style: { cursor: network === '1' ? 'pointer' : 'default' },
+ // onClick: this.view.bind(this, address, userAddress, network),
+ onClick: () => {
+ setSelectedToken(address)
+ selectedTokenAddress !== address && sidebarOpen && hideSidebar()
+ },
}, [
h(Identicon, {
- diameter: 50,
+ className: 'token-list-item__identicon',
+ diameter: 45,
address,
network,
}),
- h('h3', `${string || 0} ${symbol}`),
+ h('h.token-list-item__balance-wrapper', null, [
+ h('h3.token-list-item__token-balance', `${string || 0} ${symbol}`),
+
+ showFiat && h('div.token-list-item__fiat-amount', {
+ style: {},
+ }, formattedFiat),
+ ]),
- h('span', { style: { flex: '1 0 auto' } }),
+ h('i.fa.fa-ellipsis-h.fa-lg.token-list-item__ellipsis.cursor-pointer', {
+ onClick: (e) => {
+ e.stopPropagation()
+ this.setState({ tokenMenuOpen: true })
+ },
+ }),
+
+ tokenMenuOpen && h(TokenMenuDropdown, {
+ onClose: () => this.setState({ tokenMenuOpen: false }),
+ token: { symbol, address },
+ }),
/*
h('button', {
diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js
index 998ec901d..8e06e0f27 100644
--- a/ui/app/components/token-list.js
+++ b/ui/app/components/token-list.js
@@ -3,8 +3,28 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenTracker = require('eth-token-tracker')
const TokenCell = require('./token-cell.js')
+const connect = require('react-redux').connect
+const selectors = require('../selectors')
+
+function mapStateToProps (state) {
+ return {
+ network: state.metamask.network,
+ tokens: state.metamask.tokens,
+ userAddress: selectors.getSelectedAddress(state),
+ }
+}
+
+const defaultTokens = []
+const contracts = require('eth-contract-metadata')
+for (const address in contracts) {
+ const contract = contracts[address]
+ if (contract.erc20) {
+ contract.address = address
+ defaultTokens.push(contract)
+ }
+}
-module.exports = TokenList
+module.exports = connect(mapStateToProps)(TokenList)
inherits(TokenList, Component)
function TokenList () {
@@ -17,12 +37,12 @@ function TokenList () {
}
TokenList.prototype.render = function () {
+ const { userAddress } = this.props
const state = this.state
const { tokens, isLoading, error } = state
- const { userAddress, network } = this.props
if (isLoading) {
- return this.message('Loading')
+ return this.message('Loading Tokens...')
}
if (error) {
@@ -47,87 +67,8 @@ TokenList.prototype.render = function () {
])
}
- const tokenViews = tokens.map((tokenData) => {
- tokenData.network = network
- tokenData.userAddress = userAddress
- return h(TokenCell, tokenData)
- })
-
- return h('.full-flex-height', [
- this.renderTokenStatusBar(),
-
- h('ol.full-flex-height.flex-column', {
- style: {
- overflowY: 'auto',
- display: 'flex',
- flexDirection: 'column',
- },
- }, [
- h('style', `
-
- li.token-cell {
- display: flex;
- flex-direction: row;
- align-items: center;
- padding: 10px;
- min-height: 50px;
- }
-
- li.token-cell > h3 {
- margin-left: 12px;
- }
-
- li.token-cell:hover {
- background: white;
- cursor: pointer;
- }
-
- `),
- ...tokenViews,
- h('.flex-grow'),
- ]),
- ])
-}
-
-TokenList.prototype.renderTokenStatusBar = function () {
- const { tokens } = this.state
-
- let msg
- if (tokens.length === 1) {
- msg = `You own 1 token`
- } else if (tokens.length > 1) {
- msg = `You own ${tokens.length} tokens`
- } else {
- msg = `No tokens found`
- }
+ return h('div', tokens.map((tokenData) => h(TokenCell, tokenData)))
- return h('div', {
- style: {
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- minHeight: '70px',
- padding: '10px',
- },
- }, [
- h('span', msg),
- h('button', {
- key: 'reveal-account-bar',
- onClick: (event) => {
- event.preventDefault()
- this.props.addToken()
- },
- style: {
- display: 'flex',
- height: '40px',
- padding: '10px',
- justifyContent: 'center',
- alignItems: 'center',
- },
- }, [
- 'ADD TOKEN',
- ]),
- ])
}
TokenList.prototype.message = function (body) {
@@ -156,6 +97,7 @@ TokenList.prototype.createFreshTokenTracker = function () {
if (!global.ethereumProvider) return
const { userAddress } = this.props
+
this.tracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
@@ -182,22 +124,34 @@ TokenList.prototype.createFreshTokenTracker = function () {
})
}
-TokenList.prototype.componentWillUpdate = function (nextProps) {
- if (nextProps.network === 'loading') return
- const oldNet = this.props.network
- const newNet = nextProps.network
-
- if (oldNet && newNet && newNet !== oldNet) {
- this.setState({ isLoading: true })
- this.createFreshTokenTracker()
- }
+TokenList.prototype.componentDidUpdate = function (nextProps) {
+ const {
+ network: oldNet,
+ userAddress: oldAddress,
+ tokens,
+ } = this.props
+ const {
+ network: newNet,
+ userAddress: newAddress,
+ tokens: newTokens,
+ } = nextProps
+
+ const isLoading = newNet === 'loading'
+ const missingInfo = !oldNet || !newNet || !oldAddress || !newAddress
+ const sameUserAndNetwork = oldAddress === newAddress && oldNet === newNet
+ const shouldUpdateTokens = isLoading || missingInfo || sameUserAndNetwork
+
+ const oldTokensLength = tokens ? tokens.length : 0
+ const tokensLengthUnchanged = oldTokensLength === newTokens.length
+
+ if (tokensLengthUnchanged && shouldUpdateTokens) return
+
+ this.setState({ isLoading: true })
+ this.createFreshTokenTracker()
}
TokenList.prototype.updateBalances = function (tokens) {
- const heldTokens = tokens.filter(token => {
- return token.balance !== '0' && token.string !== '0.000'
- })
- this.setState({ tokens: heldTokens, isLoading: false })
+ this.setState({ tokens, isLoading: false })
}
TokenList.prototype.componentWillUnmount = function () {
@@ -205,3 +159,15 @@ TokenList.prototype.componentWillUnmount = function () {
this.tracker.stop()
}
+// function uniqueMergeTokens (tokensA, tokensB = []) {
+// const uniqueAddresses = []
+// const result = []
+// tokensA.concat(tokensB).forEach((token) => {
+// const normal = normalizeAddress(token.address)
+// if (!uniqueAddresses.includes(normal)) {
+// uniqueAddresses.push(normal)
+// result.push(token)
+// }
+// })
+// return result
+// }
diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js
index 891d5e227..255f0e5eb 100644
--- a/ui/app/components/transaction-list-item.js
+++ b/ui/app/components/transaction-list-item.js
@@ -142,34 +142,40 @@ function formatDate (date) {
}
function renderErrorOrWarning (transaction) {
- const { status, err, warning } = transaction
+ const { status } = transaction
// show rejected
if (status === 'rejected') {
return h('span.error', ' (Rejected)')
}
-
- // show error
- if (err) {
- const message = err.message || ''
- return (
- h(Tooltip, {
- title: message,
- position: 'bottom',
- }, [
- h(`span.error`, ` (Failed)`),
- ])
- )
- }
-
- // show warning
- if (warning) {
- const message = warning.message
- return h(Tooltip, {
- title: message,
- position: 'bottom',
- }, [
- h(`span.warning`, ` (Warning)`),
- ])
+ if (transaction.err || transaction.warning) {
+ const { err, warning = {} } = transaction
+ const errFirst = !!((err && warning) || err)
+
+ errFirst ? err.message : warning.message
+
+ // show error
+ if (err) {
+ const message = err.message || ''
+ return (
+ h(Tooltip, {
+ title: message,
+ position: 'bottom',
+ }, [
+ h(`span.error`, ` (Failed)`),
+ ])
+ )
+ }
+
+ // show warning
+ if (warning) {
+ const message = warning.message
+ return h(Tooltip, {
+ title: message,
+ position: 'bottom',
+ }, [
+ h(`span.warning`, ` (Warning)`),
+ ])
+ }
}
}
diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js
new file mode 100644
index 000000000..4e76147a1
--- /dev/null
+++ b/ui/app/components/tx-list-item.js
@@ -0,0 +1,247 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const inherits = require('util').inherits
+const classnames = require('classnames')
+const abi = require('human-standard-token-abi')
+const abiDecoder = require('abi-decoder')
+abiDecoder.addABI(abi)
+const Identicon = require('./identicon')
+const contractMap = require('eth-contract-metadata')
+
+const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
+const { calcTokenAmount } = require('../token-util')
+
+const { getCurrentCurrency } = require('../selectors')
+
+module.exports = connect(mapStateToProps)(TxListItem)
+
+function mapStateToProps (state) {
+ return {
+ tokens: state.metamask.tokens,
+ currentCurrency: getCurrentCurrency(state),
+ tokenExchangeRates: state.metamask.tokenExchangeRates,
+ }
+}
+
+inherits(TxListItem, Component)
+function TxListItem () {
+ Component.call(this)
+
+ this.state = {
+ total: null,
+ fiatTotal: null,
+ }
+}
+
+TxListItem.prototype.componentDidMount = async function () {
+ const { txParams = {} } = this.props
+
+ const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
+ const { name: txDataName } = decodedData || {}
+
+ const { total, fiatTotal } = txDataName === 'transfer'
+ ? await this.getSendTokenTotal()
+ : this.getSendEtherTotal()
+
+ this.setState({ total, fiatTotal })
+}
+
+TxListItem.prototype.getAddressText = function () {
+ const {
+ address,
+ txParams = {},
+ } = this.props
+
+ const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
+ const { name: txDataName, params = [] } = decodedData || {}
+ const { value } = params[0] || {}
+
+ switch (txDataName) {
+ case 'transfer':
+ return `${value.slice(0, 10)}...${value.slice(-4)}`
+ default:
+ return address
+ ? `${address.slice(0, 10)}...${address.slice(-4)}`
+ : 'Contract Published'
+ }
+}
+
+TxListItem.prototype.getSendEtherTotal = function () {
+ const {
+ transactionAmount,
+ conversionRate,
+ address,
+ currentCurrency,
+ } = this.props
+
+ if (!address) {
+ return {}
+ }
+
+ const totalInFiat = conversionUtil(transactionAmount, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency: 'ETH',
+ toCurrency: currentCurrency,
+ fromDenomination: 'WEI',
+ numberOfDecimals: 2,
+ conversionRate,
+ })
+ const totalInETH = conversionUtil(transactionAmount, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency: 'ETH',
+ toCurrency: 'ETH',
+ fromDenomination: 'WEI',
+ conversionRate,
+ numberOfDecimals: 6,
+ })
+
+ return {
+ total: `${totalInETH} ETH`,
+ fiatTotal: `${totalInFiat} ${currentCurrency.toUpperCase()}`,
+ }
+}
+
+TxListItem.prototype.getTokenInfo = async function () {
+ const { txParams = {}, tokenInfoGetter, tokens } = this.props
+ const toAddress = txParams.to
+
+ let decimals
+ let symbol
+
+ ({ decimals, symbol } = tokens.filter(({ address }) => address === toAddress)[0] || {})
+
+ if (!decimals && !symbol) {
+ ({ decimals, symbol } = contractMap[toAddress] || {})
+ }
+
+ if (!decimals && !symbol) {
+ ({ decimals, symbol } = await tokenInfoGetter(toAddress))
+ }
+
+ return { decimals, symbol }
+}
+
+TxListItem.prototype.getSendTokenTotal = async function () {
+ const {
+ txParams = {},
+ conversionRate,
+ tokenExchangeRates,
+ currentCurrency,
+ } = this.props
+
+ const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
+ const { params = [] } = decodedData || {}
+ const { value } = params[1] || {}
+ const { decimals, symbol } = await this.getTokenInfo()
+ const total = calcTokenAmount(value, decimals)
+
+ const pair = symbol && `${symbol.toLowerCase()}_eth`
+
+ let tokenToFiatRate
+ let totalInFiat
+
+ if (tokenExchangeRates[pair]) {
+ tokenToFiatRate = multiplyCurrencies(
+ tokenExchangeRates[pair].rate,
+ conversionRate
+ )
+
+ totalInFiat = conversionUtil(total, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'dec',
+ fromCurrency: symbol,
+ toCurrency: currentCurrency,
+ numberOfDecimals: 2,
+ conversionRate: tokenToFiatRate,
+ })
+ }
+
+ const showFiat = Boolean(totalInFiat) && currentCurrency.toUpperCase() !== symbol
+
+ return {
+ total: `${total} ${symbol}`,
+ fiatTotal: showFiat && `${totalInFiat} ${currentCurrency.toUpperCase()}`,
+ }
+}
+
+TxListItem.prototype.render = function () {
+ const {
+ transactionStatus,
+ onClick,
+ transActionId,
+ dateString,
+ address,
+ className,
+ } = this.props
+ const { total, fiatTotal } = this.state
+
+ return h(`div${className || ''}`, {
+ key: transActionId,
+ onClick: () => onClick && onClick(transActionId),
+ }, [
+ h(`div.flex-column.tx-list-item-wrapper`, {}, [
+
+ h('div.tx-list-date-wrapper', {
+ style: {},
+ }, [
+ h('span.tx-list-date', {}, [
+ dateString,
+ ]),
+ ]),
+
+ h('div.flex-row.tx-list-content-wrapper', {
+ style: {},
+ }, [
+
+ h('div.tx-list-identicon-wrapper', {
+ style: {},
+ }, [
+ h(Identicon, {
+ address,
+ diameter: 28,
+ }),
+ ]),
+
+ h('div.tx-list-account-and-status-wrapper', {}, [
+ h('div.tx-list-account-wrapper', {
+ style: {},
+ }, [
+ h('span.tx-list-account', {}, [
+ this.getAddressText(address),
+ ]),
+ ]),
+
+ h('div.tx-list-status-wrapper', {
+ style: {},
+ }, [
+ h('span', {
+ className: classnames('tx-list-status', {
+ 'tx-list-status--rejected': transactionStatus === 'rejected',
+ 'tx-list-status--failed': transactionStatus === 'failed',
+ }),
+ },
+ transactionStatus,
+ ),
+ ]),
+ ]),
+
+ h('div.flex-column.tx-list-details-wrapper', {
+ style: {},
+ }, [
+
+ h('span', {
+ className: classnames('tx-list-value', {
+ 'tx-list-value--confirmed': transactionStatus === 'confirmed',
+ }),
+ }, total),
+
+ fiatTotal && h('span.tx-list-fiat-value', fiatTotal),
+
+ ]),
+ ]),
+ ]), // holding on icon from design
+ ])
+}
diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js
new file mode 100644
index 000000000..70722f43e
--- /dev/null
+++ b/ui/app/components/tx-list.js
@@ -0,0 +1,137 @@
+const Component = require('react').Component
+const connect = require('react-redux').connect
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const prefixForNetwork = require('../../lib/etherscan-prefix-for-network')
+const selectors = require('../selectors')
+const TxListItem = require('./tx-list-item')
+const ShiftListItem = require('./shift-list-item')
+const { formatDate } = require('../util')
+const { showConfTxPage } = require('../actions')
+const classnames = require('classnames')
+const { tokenInfoGetter } = require('../token-util')
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(TxList)
+
+function mapStateToProps (state) {
+ return {
+ txsToRender: selectors.transactionsSelector(state),
+ conversionRate: selectors.conversionRateSelector(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })),
+ }
+}
+
+inherits(TxList, Component)
+function TxList () {
+ Component.call(this)
+}
+
+TxList.prototype.componentWillMount = function () {
+ this.tokenInfoGetter = tokenInfoGetter()
+}
+
+TxList.prototype.render = function () {
+ return h('div.flex-column.tx-list-container', {}, [
+
+ h('div.flex-row.tx-list-header-wrapper', [
+ h('div.flex-row.tx-list-header', [
+ h('div', 'transactions'),
+ ]),
+ ]),
+
+ this.renderTransaction(),
+
+ ])
+}
+
+TxList.prototype.renderTransaction = function () {
+ const { txsToRender, conversionRate } = this.props
+ return txsToRender.length
+ ? txsToRender.map((transaction, i) => this.renderTransactionListItem(transaction, conversionRate))
+ : [h(
+ 'div.tx-list-item.tx-list-item--empty',
+ { key: 'tx-list-none' },
+ [ 'No Transactions' ],
+ )]
+}
+
+// TODO: Consider moving TxListItem into a separate component
+TxList.prototype.renderTransactionListItem = function (transaction, conversionRate) {
+ // console.log({transaction})
+ // refer to transaction-list.js:line 58
+
+ if (transaction.key === 'shapeshift') {
+ return h(ShiftListItem, transaction)
+ }
+
+ const props = {
+ dateString: formatDate(transaction.time),
+ address: transaction.txParams.to,
+ transactionStatus: transaction.status,
+ transactionAmount: transaction.txParams.value,
+ transActionId: transaction.id,
+ transactionHash: transaction.hash,
+ transactionNetworkId: transaction.metamaskNetworkId,
+ }
+
+ const {
+ address,
+ transactionStatus,
+ transactionAmount,
+ dateString,
+ transActionId,
+ transactionHash,
+ transactionNetworkId,
+ } = props
+ const { showConfTxPage } = this.props
+
+ const opts = {
+ key: transActionId || transactionHash,
+ txParams: transaction.txParams,
+ transactionStatus,
+ transActionId,
+ dateString,
+ address,
+ transactionAmount,
+ transactionHash,
+ conversionRate,
+ tokenInfoGetter: this.tokenInfoGetter,
+ }
+
+ const isUnapproved = transactionStatus === 'unapproved'
+
+ if (isUnapproved) {
+ opts.onClick = () => showConfTxPage({id: transActionId})
+ opts.transactionStatus = 'Not Started'
+ } else if (transactionHash) {
+ opts.onClick = () => this.view(transactionHash, transactionNetworkId)
+ }
+
+ opts.className = classnames('.tx-list-item', {
+ '.tx-list-pending-item-container': isUnapproved,
+ '.tx-list-clickable': Boolean(transactionHash) || isUnapproved,
+ })
+
+ return h(TxListItem, opts)
+}
+
+TxList.prototype.view = function (txHash, network) {
+ const url = etherscanLinkFor(txHash, network)
+ if (url) {
+ navigateTo(url)
+ }
+}
+
+function navigateTo (url) {
+ global.platform.openWindow({ url })
+}
+
+function etherscanLinkFor (txHash, network) {
+ const prefix = prefixForNetwork(network)
+ return `https://${prefix}etherscan.io/tx/${txHash}`
+}
diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js
new file mode 100644
index 000000000..ebef22680
--- /dev/null
+++ b/ui/app/components/tx-view.js
@@ -0,0 +1,149 @@
+const Component = require('react').Component
+const connect = require('react-redux').connect
+const h = require('react-hyperscript')
+const ethUtil = require('ethereumjs-util')
+const inherits = require('util').inherits
+const actions = require('../actions')
+const selectors = require('../selectors')
+
+const BalanceComponent = require('./balance-component')
+const TxList = require('./tx-list')
+const Identicon = require('./identicon')
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(TxView)
+
+function mapStateToProps (state) {
+ const sidebarOpen = state.appState.sidebarOpen
+
+ const identities = state.metamask.identities
+ const accounts = state.metamask.accounts
+ const network = state.metamask.network
+ const selectedTokenAddress = state.metamask.selectedTokenAddress
+ const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
+ const checksumAddress = selectedAddress && ethUtil.toChecksumAddress(selectedAddress)
+ const identity = identities[selectedAddress]
+
+ return {
+ sidebarOpen,
+ selectedAddress,
+ checksumAddress,
+ selectedTokenAddress,
+ selectedToken: selectors.getSelectedToken(state),
+ identity,
+ network,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showSidebar: () => { dispatch(actions.showSidebar()) },
+ hideSidebar: () => { dispatch(actions.hideSidebar()) },
+ showModal: (payload) => { dispatch(actions.showModal(payload)) },
+ showSendPage: () => { dispatch(actions.showSendPage()) },
+ showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) },
+ }
+}
+
+inherits(TxView, Component)
+function TxView () {
+ Component.call(this)
+}
+
+TxView.prototype.renderHeroBalance = function () {
+ const { selectedToken } = this.props
+
+ return h('div.hero-balance', {}, [
+
+ h(BalanceComponent, { token: selectedToken }),
+
+ this.renderButtons(),
+ ])
+}
+
+TxView.prototype.renderButtons = function () {
+ const {selectedToken, showModal, showSendPage, showSendTokenPage } = this.props
+
+ return !selectedToken
+ ? (
+ h('div.flex-row.flex-center.hero-balance-buttons', [
+ h('button.btn-clear', {
+ style: {
+ textAlign: 'center',
+ },
+ onClick: () => showModal({
+ name: 'BUY',
+ }),
+ }, 'DEPOSIT'),
+
+ h('button.btn-clear', {
+ style: {
+ textAlign: 'center',
+ marginLeft: '0.8em',
+ },
+ onClick: showSendPage,
+ }, 'SEND'),
+ ])
+ )
+ : (
+ h('div.flex-row.flex-center.hero-balance-buttons', [
+ h('button.btn-clear', {
+ style: {
+ textAlign: 'center',
+ marginLeft: '0.8em',
+ },
+ onClick: showSendTokenPage,
+ }, 'SEND'),
+ ])
+ )
+}
+
+TxView.prototype.render = function () {
+ const { selectedAddress, identity, network } = this.props
+
+ return h('div.tx-view.flex-column', {
+ style: {},
+ }, [
+
+ h('div.flex-row.phone-visible', {
+ style: {
+ margin: '1em 0.9em',
+ alignItems: 'center',
+ },
+ }, [
+
+ h('div.fa.fa-bars', {
+ style: {
+ fontSize: '1.3em',
+ cursor: 'pointer',
+ },
+ onClick: () => {
+ this.props.sidebarOpen ? this.props.hideSidebar() : this.props.showSidebar()
+ },
+ }, []),
+
+ h('.identicon-wrapper.select-none', {
+ style: {
+ marginLeft: '0.9em',
+ },
+ }, [
+ h(Identicon, {
+ diameter: 24,
+ address: selectedAddress,
+ network,
+ }),
+ ]),
+
+ h('span.account-name', {
+ style: {},
+ }, [
+ identity.name,
+ ]),
+
+ ]),
+
+ this.renderHeroBalance(),
+
+ h(TxList),
+
+ ])
+}
diff --git a/ui/app/components/wallet-content-display.js b/ui/app/components/wallet-content-display.js
new file mode 100644
index 000000000..bfa061be4
--- /dev/null
+++ b/ui/app/components/wallet-content-display.js
@@ -0,0 +1,56 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+
+module.exports = WalletContentDisplay
+
+inherits(WalletContentDisplay, Component)
+function WalletContentDisplay () {
+ Component.call(this)
+}
+
+WalletContentDisplay.prototype.render = function () {
+ const { title, amount, fiatValue, active, style } = this.props
+
+ // TODO: Separate component: wallet-content-account
+ return h('div.flex-column', {
+ style: {
+ marginLeft: '1.3em',
+ alignItems: 'flex-start',
+ ...style,
+ },
+ }, [
+
+ h('span', {
+ style: {
+ fontSize: '1.1em',
+ },
+ }, title),
+
+ h('span', {
+ style: {
+ fontSize: '1.8em',
+ margin: '0.4em 0em',
+ },
+ }, amount),
+
+ h('span', {
+ style: {
+ fontSize: '1.3em',
+ },
+ }, fiatValue),
+
+ active && h('div', {
+ style: {
+ position: 'absolute',
+ marginLeft: '-1.3em',
+ height: '6em',
+ width: '0.3em',
+ background: '#D8D8D8', // $alto
+ },
+ }, [
+ ]),
+ ])
+
+}
+
diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js
new file mode 100644
index 000000000..3cb7a8b76
--- /dev/null
+++ b/ui/app/components/wallet-view.js
@@ -0,0 +1,171 @@
+const Component = require('react').Component
+const connect = require('react-redux').connect
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const Identicon = require('./identicon')
+// const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns
+const copyToClipboard = require('copy-to-clipboard')
+const actions = require('../actions')
+const BalanceComponent = require('./balance-component')
+const TokenList = require('./token-list')
+const selectors = require('../selectors')
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(WalletView)
+
+function mapStateToProps (state) {
+
+ return {
+ network: state.metamask.network,
+ sidebarOpen: state.appState.sidebarOpen,
+ identities: state.metamask.identities,
+ accounts: state.metamask.accounts,
+ tokens: state.metamask.tokens,
+ keyrings: state.metamask.keyrings,
+ selectedAddress: selectors.getSelectedAddress(state),
+ selectedIdentity: selectors.getSelectedIdentity(state),
+ selectedAccount: selectors.getSelectedAccount(state),
+ selectedTokenAddress: state.metamask.selectedTokenAddress,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showSendPage: () => dispatch(actions.showSendPage()),
+ hideSidebar: () => dispatch(actions.hideSidebar()),
+ unsetSelectedToken: () => dispatch(actions.setSelectedToken()),
+ showAccountDetailModal: () => {
+ dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
+ },
+ showAddTokenPage: () => dispatch(actions.showAddTokenPage()),
+ }
+}
+
+inherits(WalletView, Component)
+function WalletView () {
+ Component.call(this)
+ this.state = {
+ hasCopied: false,
+ }
+}
+
+WalletView.prototype.renderWalletBalance = function () {
+ const {
+ selectedTokenAddress,
+ selectedAccount,
+ unsetSelectedToken,
+ hideSidebar,
+ sidebarOpen,
+ } = this.props
+
+ const selectedClass = selectedTokenAddress
+ ? ''
+ : 'wallet-balance-wrapper--active'
+ const className = `flex-column wallet-balance-wrapper ${selectedClass}`
+
+ return h('div', { className }, [
+ h('div.wallet-balance',
+ {
+ onClick: () => {
+ unsetSelectedToken()
+ selectedTokenAddress && sidebarOpen && hideSidebar()
+ },
+ },
+ [
+ h(BalanceComponent, {
+ balanceValue: selectedAccount ? selectedAccount.balance : '',
+ style: {},
+ }),
+ ]
+ ),
+ ])
+}
+
+WalletView.prototype.render = function () {
+ const {
+ responsiveDisplayClassname,
+ selectedAddress,
+ selectedIdentity,
+ keyrings,
+ showAccountDetailModal,
+ hideSidebar,
+ showAddTokenPage,
+ } = this.props
+ // temporary logs + fake extra wallets
+ // console.log('walletview, selectedAccount:', selectedAccount)
+
+ const keyring = keyrings.find((kr) => {
+ return kr.accounts.includes(selectedAddress) ||
+ kr.accounts.includes(selectedIdentity.address)
+ })
+
+ const type = keyring.type
+ const isLoose = type !== 'HD Key Tree'
+
+ return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), {
+ style: {},
+ }, [
+
+ // TODO: Separate component: wallet account details
+ h('div.flex-column.wallet-view-account-details', {
+ style: {},
+ }, [
+ h('div.wallet-view__sidebar-close', {
+ onClick: hideSidebar,
+ }),
+
+ h('div.wallet-view__keyring-label', isLoose ? 'IMPORTED' : ''),
+
+ h('div.flex-column.flex-center.wallet-view__name-container', {
+ style: { margin: '0 auto' },
+ onClick: showAccountDetailModal,
+ }, [
+ h(Identicon, {
+ diameter: 54,
+ address: selectedAddress,
+ }),
+
+ h('span.account-name', {
+ style: {},
+ }, [
+ selectedIdentity.name,
+ ]),
+
+ h('button.wallet-view__details-button', 'DETAILS'),
+ ]),
+ ]),
+
+
+ h('div.wallet-view__address', {
+ onClick: () => {
+ copyToClipboard(selectedAddress)
+ this.setState({ hasCopied: true })
+ setTimeout(() => this.setState({ hasCopied: false }), 3000)
+ },
+ }, [
+ this.state.hasCopied && 'Copied to Clipboard',
+ !this.state.hasCopied && `${selectedAddress.slice(0, 4)}...${selectedAddress.slice(-4)}`,
+ h('i.fa.fa-clipboard', { style: { marginLeft: '8px' } }),
+ ]),
+
+ this.renderWalletBalance(),
+
+ h(TokenList),
+
+ h('button.wallet-view__add-token-button', {
+ onClick: () => {
+ showAddTokenPage()
+ hideSidebar()
+ },
+ }, 'Add Token'),
+ ])
+}
+
+// TODO: Extra wallets, for dev testing. Remove when PRing to master.
+// const extraWallet = h('div.flex-column.wallet-balance-wrapper', {}, [
+// h('div.wallet-balance', {}, [
+// h(BalanceComponent, {
+// balanceValue: selectedAccount.balance,
+// style: {},
+// }),
+// ]),
+// ])
diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js
index cb1afedfe..9f273aaec 100644
--- a/ui/app/conf-tx.js
+++ b/ui/app/conf-tx.js
@@ -3,16 +3,24 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
-const NetworkIndicator = require('./components/network')
const txHelper = require('../lib/tx-helper')
-const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification')
const PendingTx = require('./components/pending-tx')
-const PendingMsg = require('./components/pending-msg')
-const PendingPersonalMsg = require('./components/pending-personal-msg')
-const PendingTypedMsg = require('./components/pending-typed-msg')
+const SignatureRequest = require('./components/signature-request')
+// const PendingMsg = require('./components/pending-msg')
+// const PendingPersonalMsg = require('./components/pending-personal-msg')
+// const PendingTypedMsg = require('./components/pending-typed-msg')
const Loading = require('./components/loading')
+// const contentDivider = h('div', {
+// style: {
+// marginLeft: '16px',
+// marginRight: '16px',
+// height:'1px',
+// background:'#E7E7E7',
+// },
+// })
+
module.exports = connect(mapStateToProps)(ConfirmTxScreen)
function mapStateToProps (state) {
@@ -42,91 +50,71 @@ function ConfirmTxScreen () {
ConfirmTxScreen.prototype.render = function () {
const props = this.props
- const { network, provider, unapprovedTxs, currentCurrency, computedBalances,
- unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, conversionRate, blockGasLimit } = props
+ const {
+ network,
+ unapprovedTxs,
+ currentCurrency,
+ unapprovedMsgs,
+ unapprovedPersonalMsgs,
+ unapprovedTypedMessages,
+ conversionRate,
+ blockGasLimit,
+ // provider,
+ // computedBalances,
+ } = props
var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network)
var txData = unconfTxList[props.index] || {}
var txParams = txData.params || {}
- var isNotification = isPopupOrNotification() === 'notification'
+
+ // var isNotification = isPopupOrNotification() === 'notification'
+ /*
+ Client is using the flag above to render the following in conf screen
+ // subtitle and nav
+ h('.section-title.flex-row.flex-center', [
+ !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: this.goHome.bind(this),
+ }) : null,
+ h('h2.page-subtitle', 'Confirm Transaction'),
+ isNotification ? h(NetworkIndicator, {
+ network: network,
+ provider: provider,
+ }) : null,
+ ]),
+ */
log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`)
- if (unconfTxList.length === 0) return h(Loading, { isLoading: true })
-
- const unconfTxListLength = unconfTxList.length
-
- return (
-
- h('.flex-column.flex-grow', [
-
- // subtitle and nav
- h('.section-title.flex-row.flex-center', [
- !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
- onClick: this.goHome.bind(this),
- }) : null,
- h('h2.page-subtitle', 'Confirm Transaction'),
- isNotification ? h(NetworkIndicator, {
- network: network,
- provider: provider,
- }) : null,
- ]),
-
- h('h3', {
- style: {
- alignSelf: 'center',
- display: unconfTxList.length > 1 ? 'block' : 'none',
- },
- }, [
- h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
- style: {
- display: props.index === 0 ? 'none' : 'inline-block',
- },
- onClick: () => props.dispatch(actions.previousTx()),
- }),
- ` ${props.index + 1} of ${unconfTxList.length} `,
- h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', {
- style: {
- display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block',
- },
- onClick: () => props.dispatch(actions.nextTx()),
- }),
- ]),
-
- warningIfExists(props.warning),
-
- currentTxView({
- // Properties
- txData: txData,
- key: txData.id,
- selectedAddress: props.selectedAddress,
- accounts: props.accounts,
- identities: props.identities,
- conversionRate,
- currentCurrency,
- blockGasLimit,
- unconfTxListLength,
- computedBalances,
- // Actions
- buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
- sendTransaction: this.sendTransaction.bind(this),
- cancelTransaction: this.cancelTransaction.bind(this, txData),
- cancelAllTransactions: this.cancelAllTransactions.bind(this, unconfTxList),
- signMessage: this.signMessage.bind(this, txData),
- signPersonalMessage: this.signPersonalMessage.bind(this, txData),
- signTypedMessage: this.signTypedMessage.bind(this, txData),
- cancelMessage: this.cancelMessage.bind(this, txData),
- cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
- cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
- }),
- ])
- )
+ if (unconfTxList.length === 0) return h(Loading)
+
+ return currentTxView({
+ // Properties
+ txData: txData,
+ key: txData.id,
+ selectedAddress: props.selectedAddress,
+ accounts: props.accounts,
+ identities: props.identities,
+ conversionRate,
+ currentCurrency,
+ blockGasLimit,
+ // Actions
+ buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
+ sendTransaction: this.sendTransaction.bind(this),
+ cancelTransaction: this.cancelTransaction.bind(this, txData),
+ signMessage: this.signMessage.bind(this, txData),
+ signPersonalMessage: this.signPersonalMessage.bind(this, txData),
+ signTypedMessage: this.signTypedMessage.bind(this, txData),
+ cancelMessage: this.cancelMessage.bind(this, txData),
+ cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
+ cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
+ })
+
}
function currentTxView (opts) {
log.info('rendering current tx view')
const { txData } = opts
- const { txParams, msgParams, type } = txData
+ const { txParams, msgParams } = txData
if (txParams) {
log.debug('txParams detected, rendering pending tx')
@@ -134,17 +122,20 @@ function currentTxView (opts) {
} else if (msgParams) {
log.debug('msgParams detected, rendering pending msg')
- if (type === 'eth_sign') {
- log.debug('rendering eth_sign message')
- return h(PendingMsg, opts)
- } else if (type === 'personal_sign') {
- log.debug('rendering personal_sign message')
- return h(PendingPersonalMsg, opts)
- } else if (type === 'eth_signTypedData') {
- log.debug('rendering eth_signTypedData message')
- return h(PendingTypedMsg, opts)
- }
+ return h(SignatureRequest, opts)
+
+ // if (type === 'eth_sign') {
+ // log.debug('rendering eth_sign message')
+ // return h(PendingMsg, opts)
+ // } else if (type === 'personal_sign') {
+ // log.debug('rendering personal_sign message')
+ // return h(PendingPersonalMsg, opts)
+ // } else if (type === 'eth_signTypedData') {
+ // log.debug('rendering eth_signTypedData message')
+ // return h(PendingTypedMsg, opts)
+ // }
}
+ return h(Loading)
}
ConfirmTxScreen.prototype.buyEth = function (address, event) {
@@ -222,14 +213,14 @@ ConfirmTxScreen.prototype.goHome = function (event) {
this.props.dispatch(actions.goHome())
}
-function warningIfExists (warning) {
- if (warning &&
- // Do not display user rejections on this screen:
- warning.indexOf('User denied transaction signature') === -1) {
- return h('.error', {
- style: {
- margin: 'auto',
- },
- }, warning)
- }
-}
+// function warningIfExists (warning) {
+// if (warning &&
+// // Do not display user rejections on this screen:
+// warning.indexOf('User denied transaction signature') === -1) {
+// return h('.error', {
+// style: {
+// margin: 'auto',
+// },
+// }, warning)
+// }
+// }
diff --git a/ui/app/config.js b/ui/app/config.js
deleted file mode 100644
index c14fa1d28..000000000
--- a/ui/app/config.js
+++ /dev/null
@@ -1,220 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const actions = require('./actions')
-const infuraCurrencies = require('./infura-conversion.json').objects.sort((a, b) => {
- return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
- })
-const validUrl = require('valid-url')
-const exportAsFile = require('./util').exportAsFile
-
-
-module.exports = connect(mapStateToProps)(ConfigScreen)
-
-function mapStateToProps (state) {
- return {
- metamask: state.metamask,
- warning: state.appState.warning,
- }
-}
-
-inherits(ConfigScreen, Component)
-function ConfigScreen () {
- Component.call(this)
-}
-
-ConfigScreen.prototype.render = function () {
- var state = this.props
- var metamaskState = state.metamask
- var warning = state.warning
-
- 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) => {
- state.dispatch(actions.goHome())
- },
- }),
- h('h2.page-subtitle', 'Settings'),
- ]),
-
- 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',
- },
- }, [
-
- currentProviderDisplay(metamaskState),
-
- h('div', { style: {display: 'flex'} }, [
- h('input#new_rpc', {
- placeholder: 'New RPC URL',
- style: {
- width: 'inherit',
- flex: '1 0 auto',
- height: '30px',
- margin: '8px',
- },
- onKeyPress (event) {
- if (event.key === 'Enter') {
- var element = event.target
- var newRpc = element.value
- rpcValidation(newRpc, state)
- }
- },
- }),
- h('button', {
- style: {
- alignSelf: 'center',
- },
- onClick (event) {
- event.preventDefault()
- var element = document.querySelector('input#new_rpc')
- var newRpc = element.value
- rpcValidation(newRpc, state)
- },
- }, 'Save'),
- ]),
-
- h('hr.horizontal-line'),
-
- currentConversionInformation(metamaskState, state),
-
- h('hr.horizontal-line'),
-
- h('div', {
- style: {
- marginTop: '20px',
- },
- }, [
- h('p', {
- style: {
- fontFamily: 'Montserrat Light',
- fontSize: '13px',
- },
- }, `State logs contain your public account addresses and sent transactions.`),
- h('br'),
- h('button', {
- style: {
- alignSelf: 'center',
- },
- onClick (event) {
- window.logStateString((err, result) => {
- if (err) {
- state.dispatch(actions.displayWarning('Error in retrieving state logs.'))
- } else {
- exportAsFile('MetaMask State Logs', result)
- }
- })
- },
- }, 'Download State Logs'),
- ]),
-
- h('hr.horizontal-line'),
-
- h('div', {
- style: {
- marginTop: '20px',
- },
- }, [
- h('button', {
- style: {
- alignSelf: 'center',
- },
- onClick (event) {
- event.preventDefault()
- state.dispatch(actions.revealSeedConfirmation())
- },
- }, 'Reveal Seed Words'),
- ]),
-
- ]),
- ]),
- ])
- )
-}
-
-function rpcValidation (newRpc, state) {
- if (validUrl.isWebUri(newRpc)) {
- state.dispatch(actions.setRpcTarget(newRpc))
- } else {
- var appendedRpc = `http://${newRpc}`
- if (validUrl.isWebUri(appendedRpc)) {
- state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.'))
- } else {
- state.dispatch(actions.displayWarning('Invalid RPC URI'))
- }
- }
-}
-
-function currentConversionInformation (metamaskState, state) {
- var currentCurrency = metamaskState.currentCurrency
- var conversionDate = metamaskState.conversionDate
- return h('div', [
- h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'),
- h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`),
- h('select#currentCurrency', {
- onChange (event) {
- event.preventDefault()
- var element = document.getElementById('currentCurrency')
- var newCurrency = element.value
- state.dispatch(actions.setCurrentCurrency(newCurrency))
- },
- defaultValue: currentCurrency,
- }, infuraCurrencies.map((currency) => {
- return h('option', {key: currency.quote.code, value: currency.quote.code}, `${currency.quote.code.toUpperCase()} - ${currency.quote.name}`)
- })
- ),
- ])
-}
-
-function currentProviderDisplay (metamaskState) {
- var provider = metamaskState.provider
- var title, value
-
- switch (provider.type) {
-
- case 'mainnet':
- title = 'Current Network'
- value = 'Main Ethereum Network'
- break
-
- case 'ropsten':
- title = 'Current Network'
- value = 'Ropsten Test Network'
- break
-
- case 'kovan':
- title = 'Current Network'
- value = 'Kovan Test Network'
- break
-
- case 'rinkeby':
- title = 'Current Network'
- value = 'Rinkeby Test Network'
- break
-
- default:
- title = 'Current RPC'
- value = metamaskState.provider.rpcTarget
- }
-
- return h('div', [
- h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title),
- h('span', value),
- ])
-}
diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js
new file mode 100644
index 000000000..ee42ebea1
--- /dev/null
+++ b/ui/app/conversion-util.js
@@ -0,0 +1,221 @@
+/* Currency Conversion Utility
+* This utility function can be used for converting currency related values within metamask.
+* The caller should be able to pass it a value, along with information about the value's
+* numeric base, denomination and currency, and the desired numeric base, denomination and
+* currency. It should return a single value.
+*
+* @param {(number | string | BN)} value The value to convert.
+* @param {Object} [options] Options to specify details of the conversion
+* @param {string} [options.fromCurrency = 'ETH' | 'USD'] The currency of the passed value
+* @param {string} [options.toCurrency = 'ETH' | 'USD'] The desired currency of the result
+* @param {string} [options.fromNumericBase = 'hex' | 'dec' | 'BN'] The numeric basic of the passed value.
+* @param {string} [options.toNumericBase = 'hex' | 'dec' | 'BN'] The desired numeric basic of the result.
+* @param {string} [options.fromDenomination = 'WEI'] The denomination of the passed value
+* @param {number} [options.numberOfDecimals] The desired number of in the result
+* @param {number} [options.conversionRate] The rate to use to make the fromCurrency -> toCurrency conversion
+* @returns {(number | string | BN)}
+*
+* The utility passes value along with the options as a single object to the `converter` function.
+* `converter` uses Ramda.js to apply a composition of conditional setters to the `value` property, depending
+* on the accompanying options. Some of these conditional setters are selected via key-value maps, where
+* the keys are specified in the options parameters and the values are setter functions.
+*/
+
+const BigNumber = require('bignumber.js')
+const ethUtil = require('ethereumjs-util')
+const BN = ethUtil.BN
+const R = require('ramda')
+const { stripHexPrefix } = require('ethereumjs-util')
+
+BigNumber.config({
+ ROUNDING_MODE: BigNumber.ROUND_HALF_DOWN,
+})
+
+// Big Number Constants
+const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000')
+const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000')
+
+// Individual Setters
+const convert = R.invoker(1, 'times')
+const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_HALF_DOWN)
+const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate)
+const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec'])
+
+// Setter Maps
+const toBigNumber = {
+ hex: n => new BigNumber(stripHexPrefix(n), 16),
+ dec: n => new BigNumber(n, 10),
+ BN: n => new BigNumber(n.toString(16), 16),
+}
+const toNormalizedDenomination = {
+ WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER),
+ GWEI: bigNumber => bigNumber.div(BIG_NUMBER_GWEI_MULTIPLIER),
+}
+const toSpecifiedDenomination = {
+ WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round(),
+ GWEI: bigNumber => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(9),
+}
+const baseChange = {
+ hex: n => n.toString(16),
+ dec: n => Number(n).toString(10),
+ BN: n => new BN(n.toString(16)),
+}
+
+// Predicates
+const fromAndToCurrencyPropsNotEqual = R.compose(
+ R.not,
+ R.eqBy(R.__, 'fromCurrency', 'toCurrency'),
+ R.flip(R.prop)
+)
+
+// Lens
+const valuePropertyLens = R.over(R.lensProp('value'))
+const conversionRateLens = R.over(R.lensProp('conversionRate'))
+
+// conditional conversionRate setting wrapper
+const whenPredSetCRWithPropAndSetter = (pred, prop, setter) => R.when(
+ pred,
+ R.converge(
+ conversionRateLens,
+ [R.pipe(R.prop(prop), setter), R.identity]
+ )
+)
+
+// conditional 'value' setting wrappers
+const whenPredSetWithPropAndSetter = (pred, prop, setter) => R.when(
+ pred,
+ R.converge(
+ valuePropertyLens,
+ [R.pipe(R.prop(prop), setter), R.identity]
+ )
+)
+const whenPropApplySetterMap = (prop, setterMap) => whenPredSetWithPropAndSetter(
+ R.prop(prop),
+ prop,
+ R.prop(R.__, setterMap)
+)
+
+// Conversion utility function
+const converter = R.pipe(
+ whenPredSetCRWithPropAndSetter(R.prop('conversionRate'), 'conversionRate', decToBigNumberViaString),
+ whenPredSetCRWithPropAndSetter(R.prop('invertConversionRate'), 'conversionRate', invertConversionRate),
+ whenPropApplySetterMap('fromNumericBase', toBigNumber),
+ whenPropApplySetterMap('fromDenomination', toNormalizedDenomination),
+ whenPredSetWithPropAndSetter(fromAndToCurrencyPropsNotEqual, 'conversionRate', convert),
+ whenPropApplySetterMap('toDenomination', toSpecifiedDenomination),
+ whenPredSetWithPropAndSetter(R.prop('numberOfDecimals'), 'numberOfDecimals', round),
+ whenPropApplySetterMap('toNumericBase', baseChange),
+ R.view(R.lensProp('value'))
+)
+
+const conversionUtil = (value, {
+ fromCurrency = null,
+ toCurrency = fromCurrency,
+ fromNumericBase,
+ toNumericBase,
+ fromDenomination,
+ toDenomination,
+ numberOfDecimals,
+ conversionRate,
+ invertConversionRate,
+}) => converter({
+ fromCurrency,
+ toCurrency,
+ fromNumericBase,
+ toNumericBase,
+ fromDenomination,
+ toDenomination,
+ numberOfDecimals,
+ conversionRate,
+ invertConversionRate,
+ value: value || '0',
+})
+
+const addCurrencies = (a, b, options = {}) => {
+ const {
+ aBase,
+ bBase,
+ ...conversionOptions
+ } = options
+ const value = (new BigNumber(a, aBase)).add(b, bBase)
+
+ return converter({
+ value,
+ ...conversionOptions,
+ })
+}
+
+const subtractCurrencies = (a, b, options = {}) => {
+ const {
+ aBase,
+ bBase,
+ ...conversionOptions
+ } = options
+ const value = (new BigNumber(a, aBase)).minus(b, bBase)
+
+ return converter({
+ value,
+ ...conversionOptions,
+ })
+}
+
+const multiplyCurrencies = (a, b, options = {}) => {
+ const {
+ multiplicandBase,
+ multiplierBase,
+ ...conversionOptions
+ } = options
+
+ const bigNumberA = new BigNumber(String(a), multiplicandBase)
+ const bigNumberB = new BigNumber(String(b), multiplierBase)
+
+ const value = bigNumberA.times(bigNumberB)
+
+ return converter({
+ value,
+ ...conversionOptions,
+ })
+}
+
+const conversionGreaterThan = (
+ { ...firstProps },
+ { ...secondProps },
+) => {
+ const firstValue = converter({ ...firstProps })
+ const secondValue = converter({ ...secondProps })
+
+ return firstValue.gt(secondValue)
+}
+
+const conversionGTE = (
+ { ...firstProps },
+ { ...secondProps },
+) => {
+ const firstValue = converter({ ...firstProps })
+ const secondValue = converter({ ...secondProps })
+ return firstValue.greaterThanOrEqualTo(secondValue)
+}
+
+const conversionLTE = (
+ { ...firstProps },
+ { ...secondProps },
+) => {
+ const firstValue = converter({ ...firstProps })
+ const secondValue = converter({ ...secondProps })
+ return firstValue.lessThanOrEqualTo(secondValue)
+}
+
+const toNegative = (n, options = {}) => {
+ return multiplyCurrencies(n, -1, options)
+}
+
+module.exports = {
+ conversionUtil,
+ addCurrencies,
+ multiplyCurrencies,
+ conversionGreaterThan,
+ conversionGTE,
+ conversionLTE,
+ toNegative,
+ subtractCurrencies,
+}
diff --git a/ui/app/css/debug.css b/ui/app/css/debug.css
deleted file mode 100644
index 3e125bcd4..000000000
--- a/ui/app/css/debug.css
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
-debug / dev
-*/
-
-#app-content {
- border: 2px solid green;
-}
-
-#design-container {
- position: absolute;
- left: 360px;
- top: -42px;
- width: calc(100vw - 360px);
- height: 100vh;
- overflow: scroll;
-}
-
-#design-container img {
- width: 2000px;
- margin-right: 600px;
-} \ No newline at end of file
diff --git a/ui/app/css/fonts.css b/ui/app/css/fonts.css
deleted file mode 100644
index 3b9f581b9..000000000
--- a/ui/app/css/fonts.css
+++ /dev/null
@@ -1,36 +0,0 @@
-@import url(https://fonts.googleapis.com/css?family=Roboto:300,500);
-@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css);
-
-@font-face {
- font-family: 'Montserrat Regular';
- src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff');
- src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
- font-size: 'small';
-
-}
-
-@font-face {
- font-family: 'Montserrat Bold';
- src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff');
- src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-
-@font-face {
- font-family: 'Montserrat Light';
- src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff');
- src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-
-@font-face {
- font-family: 'Montserrat UltraLight';
- src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff');
- src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
diff --git a/ui/app/css/index.scss b/ui/app/css/index.scss
new file mode 100644
index 000000000..01899ccad
--- /dev/null
+++ b/ui/app/css/index.scss
@@ -0,0 +1,13 @@
+/*
+ ITCSS
+
+ http://www.creativebloq.com/web-design/manage-large-css-projects-itcss-101517528
+ https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/
+ */
+@import './itcss/settings/index.scss';
+@import './itcss/tools/index.scss';
+@import './itcss/generic/index.scss';
+@import './itcss/base/index.scss';
+@import './itcss/objects/index.scss';
+@import './itcss/components/index.scss';
+@import './itcss/trumps/index.scss';
diff --git a/ui/app/css/itcss/base/index.scss b/ui/app/css/itcss/base/index.scss
new file mode 100644
index 000000000..baa6ea037
--- /dev/null
+++ b/ui/app/css/itcss/base/index.scss
@@ -0,0 +1 @@
+// Base
diff --git a/ui/app/css/itcss/components/account-dropdown-mini.scss b/ui/app/css/itcss/components/account-dropdown-mini.scss
new file mode 100644
index 000000000..996993db7
--- /dev/null
+++ b/ui/app/css/itcss/components/account-dropdown-mini.scss
@@ -0,0 +1,48 @@
+.account-dropdown-mini {
+ height: 22px;
+ background-color: $white;
+ font-family: Roboto;
+ line-height: 16px;
+ font-size: 12px;
+ width: 124px;
+
+ &__close-area {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1000;
+ width: 100%;
+ height: 100%;
+ }
+
+ &__list {
+ z-index: 1050;
+ position: absolute;
+ height: 180px;
+ width: 96pxpx;
+ border: 1px solid $geyser;
+ border-radius: 4px;
+ background-color: $white;
+ box-shadow: 0 3px 6px 0 rgba(0 ,0 ,0 ,.11);
+ overflow-y: scroll;
+ }
+
+ .account-list-item {
+ margin-top: 6px;
+ }
+
+ .account-list-item__account-name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 80px;
+ }
+
+ .account-list-item__top-row {
+ margin: 0;
+ }
+
+ .account-list-item__icon {
+ position: initial;
+ }
+} \ No newline at end of file
diff --git a/ui/app/css/itcss/components/account-dropdown.scss b/ui/app/css/itcss/components/account-dropdown.scss
new file mode 100644
index 000000000..725da9d39
--- /dev/null
+++ b/ui/app/css/itcss/components/account-dropdown.scss
@@ -0,0 +1,83 @@
+.account-dropdown-name {
+ font-family: Roboto;
+}
+
+.account-dropdown-balance {
+ color: $dusty-gray;
+ line-height: 19px;
+}
+
+.account-dropdown-edit-button {
+ color: $dusty-gray;
+ font-family: Roboto;
+
+ &:hover {
+ color: $white;
+ }
+}
+
+.account-list-item {
+ &__top-row {
+ display: flex;
+ margin-top: 10px;
+ margin-left: 8px;
+ position: relative;
+ }
+
+ &__account-balances {
+ height: auto;
+ border: none;
+ background-color: transparent;
+ color: #9b9b9b;
+ margin-left: 34px;
+ margin-top: 4px;
+ position: relative;
+ }
+
+ &__account-name {
+ font-size: 16px;
+ margin-left: 8px;
+ }
+
+ &__icon {
+ position: absolute;
+ right: 12px;
+ top: 1px;
+ }
+
+ &__account-primary-balance,
+ &__account-secondary-balance {
+ font-family: Roboto;
+ line-height: 16px;
+ font-size: 12px;
+ font-weight: 300;
+ }
+
+ &__account-primary-balance {
+ color: $scorpion;
+ border: none;
+ outline: 0 !important;
+ }
+
+ &__account-secondary-balance {
+ color: $dusty-gray;
+ }
+
+ &__account-address {
+ margin-left: 35px;
+ width: 80%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__dropdown {
+ &:hover {
+ background: rgba($alto, .2);
+ cursor: pointer;
+
+ input {
+ background: rgba($alto, .1);
+ }
+ }
+ }
+}
diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss
new file mode 100644
index 000000000..e40e5a8c0
--- /dev/null
+++ b/ui/app/css/itcss/components/account-menu.scss
@@ -0,0 +1,132 @@
+.account-menu {
+ position: fixed;
+ z-index: 100;
+ top: 58px;
+ width: 310px;
+
+ @media screen and (max-width: 575px) {
+ right: calc(((100vw - 100%) / 2) + 8px);
+ }
+
+ @media screen and (min-width: 576px) {
+ right: calc((100vw - 85vw) / 2);
+ }
+
+ @media screen and (min-width: 769px) {
+ right: calc((100vw - 80vw) / 2);
+ }
+
+ @media screen and (min-width: 1281px) {
+ right: calc((100vw - 65vw) / 2);
+ }
+
+ &__icon {
+ margin-left: 20px;
+ cursor: pointer;
+ }
+
+ &__header {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &__logout-button {
+ border: 1px solid $dusty-gray;
+ background-color: transparent;
+ color: $white;
+ border-radius: 4px;
+ font-size: 12px;
+ line-height: 23px;
+ padding: 0 24px;
+ font-weight: 200;
+ }
+
+ img {
+ width: 16px;
+ height: 16px;
+ }
+
+ &__accounts {
+ display: flex;
+ flex-flow: column nowrap;
+ overflow-y: auto;
+ max-height: 240px;
+ position: relative;
+ z-index: 200;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ @media screen and (max-width: 575px) {
+ max-height: 215px;
+ }
+
+ .keyring-label {
+ margin-top: 5px;
+ background-color: $black;
+ color: $dusty-gray;
+ }
+ }
+
+ &__account {
+ display: flex;
+ flex-flow: row nowrap;
+ padding: 16px 14px;
+ flex: 0 0 auto;
+
+ @media screen and (max-width: 575px) {
+ padding: 12px 14px;
+ }
+ }
+
+ &__account-info {
+ flex: 1 0 auto;
+ display: flex;
+ flex-flow: column nowrap;
+ padding-top: 4px;
+ }
+
+ &__check-mark {
+ width: 14px;
+ margin-right: 12px;
+ flex: 0 0 auto;
+ }
+
+ &__check-mark-icon {
+ background-image: url("images/check-white.svg");
+ height: 18px;
+ width: 18px;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ margin: 3px 0;
+ }
+
+ .identicon {
+ margin: 0 12px 0 0;
+ flex: 0 0 auto;
+ }
+
+ &__name {
+ color: $white;
+ font-size: 18px;
+ font-weight: 200;
+ line-height: 16px;
+ }
+
+ &__balance {
+ color: $dusty-gray;
+ font-size: 14px;
+ line-height: 19px;
+ }
+
+ &__action {
+ font-size: 16px;
+ line-height: 18px;
+ font-weight: 200;
+ cursor: pointer;
+ }
+}
diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss
new file mode 100644
index 000000000..5f6d0fcff
--- /dev/null
+++ b/ui/app/css/itcss/components/add-token.scss
@@ -0,0 +1,341 @@
+.add-token {
+ width: 498px;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ position: relative;
+ z-index: 12;
+ font-family: 'DIN Next Light';
+
+ &__wrapper {
+ background-color: $white;
+ box-shadow: 0 2px 4px 0 rgba($black, .08);
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ flex: 0 0 auto;
+ }
+
+ &__title-container {
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ padding: 30px 60px 12px;
+ border-bottom: 1px solid $gallery;
+ flex: 0 0 auto;
+ }
+
+ &__title {
+ color: $scorpion;
+ font-size: 20px;
+ line-height: 26px;
+ text-align: center;
+ font-weight: 600;
+ margin-bottom: 12px;
+ }
+
+ &__description {
+ text-align: center;
+ }
+
+ &__description + &__description {
+ margin-top: 24px;
+ }
+
+ &__confirmation-description {
+ margin: 12px 0;
+ }
+
+ &__content-container {
+ width: 100%;
+ border-bottom: 1px solid $gallery;
+ }
+
+ &__input-container {
+ padding: 11px 0;
+ width: 263px;
+ margin: 0 auto;
+ position: relative;
+ }
+
+ &__search-input-error-message {
+ position: absolute;
+ bottom: -10px;
+ font-size: 12px;
+ width: 100%;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ color: $red;
+ }
+
+ &__input {
+ width: 100%;
+ border: 2px solid $gallery;
+ border-radius: 4px;
+ padding: 5px 15px;
+ font-size: 14px;
+ line-height: 19px;
+
+ &::placeholder {
+ color: $silver;
+ }
+ }
+
+ &__footers {
+ width: 100%;
+ }
+
+ &__add-custom {
+ color: $scorpion;
+ font-size: 18px;
+ line-height: 24px;
+ text-align: center;
+ padding: 12px 0;
+ font-weight: 600;
+ cursor: pointer;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, .05);
+ }
+
+ &:active {
+ background-color: rgba(0, 0, 0, .1);
+ }
+
+ .fa {
+ position: absolute;
+ right: 24px;
+ font-size: 24px;
+ line-height: 24px;
+ }
+ }
+
+ &__add-custom-form {
+ display: flex;
+ flex-flow: column nowrap;
+ margin: 8px 0 51px;
+ }
+
+ &__add-custom-field {
+ width: 290px;
+ margin: 0 auto;
+ position: relative;
+
+ &--error {
+ .add-token__add-custom-input {
+ border-color: $red;
+ }
+ }
+ }
+
+ &__add-custom-error-message {
+ position: absolute;
+ bottom: -21px;
+ font-size: 12px;
+ width: 100%;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ color: $red;
+ }
+
+ &__add-custom-label {
+ font-size: 16px;
+ line-height: 21px;
+ margin-bottom: 8px;
+ }
+
+ &__add-custom-input {
+ width: 100%;
+ border: 1px solid $silver;
+ padding: 5px 15px;
+ font-size: 14px;
+ line-height: 19px;
+
+ &::placeholder {
+ color: $silver;
+ }
+ }
+
+ &__add-custom-field + &__add-custom-field {
+ margin-top: 21px;
+ }
+
+ &__buttons {
+ display: flex;
+ flex-flow: column nowrap;
+ margin: 30px 0 51px;
+ flex: 0 0 auto;
+ }
+
+ &__token-icons-container {
+ display: flex;
+ flex-flow: row wrap;
+ }
+
+ &__token-wrapper {
+ transition: 200ms ease-in-out;
+ display: flex;
+ flex-flow: row nowrap;
+ flex: 0 0 42.5%;
+ align-items: center;
+ padding: 12px;
+ margin: 2.5%;
+ box-sizing: border-box;
+ border-radius: 10px;
+ cursor: pointer;
+ border: 2px solid transparent;
+ position: relative;
+
+ &:hover {
+ border: 2px solid rgba($malibu-blue, .5);
+ }
+
+ &--selected {
+ border: 2px solid $malibu-blue !important;
+ }
+
+ &--disabled {
+ opacity: .4;
+ pointer-events: none;
+ }
+ }
+
+ &__token-data {
+ align-self: flex-start;
+ }
+
+ &__token-name {
+ font-size: 14px;
+ line-height: 19px;
+ }
+
+ &__token-symbol {
+ font-size: 22px;
+ line-height: 29px;
+ font-weight: 600;
+ }
+
+ &__token-icon {
+ width: 60px;
+ height: 60px;
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+ border-radius: 50%;
+ background-color: $white;
+ box-shadow: 0 2px 4px 0 rgba($black, .24);
+ margin-right: 12px;
+ flex: 0 0 auto;
+ }
+
+ &__token-message {
+ position: absolute;
+ color: $caribbean-green;
+ font-size: 11px;
+ bottom: 0;
+ left: 85px;
+ }
+
+ &__confirmation-token-list {
+ display: flex;
+ flex-flow: column nowrap;
+
+ .token-balance {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: flex-start;
+
+ &__amount {
+ color: $scorpion;
+ font-size: 43px;
+ font-weight: 300;
+ line-height: 43px;
+ margin-right: 8px;
+ }
+
+ &__symbol {
+ color: $scorpion;
+ font-size: 16px;
+ line-height: 24px;
+ }
+ }
+ }
+
+ &__confirmation-title {
+ padding: 30px 120px 12px;
+
+ @media screen and (max-width: $break-small) {
+ padding: 20px 0;
+ width: 100%;
+ }
+ }
+
+ &__confirmation-content {
+ padding-bottom: 60px;
+ }
+
+ &__confirmation-token-list-item {
+ display: flex;
+ flex-flow: row nowrap;
+ margin: 0 auto;
+ align-items: center;
+ }
+
+ &__confirmation-token-list-item + &__confirmation-token-list-item {
+ margin-top: 30px;
+ }
+
+ &__confirmation-token-icon {
+ margin-right: 18px;
+ }
+
+ @media screen and (max-width: $break-small) {
+ top: 0;
+ width: 100%;
+ overflow: hidden;
+ height: 100%;
+
+ &__wrapper {
+ box-shadow: none !important;
+ flex: 1 1 auto;
+ width: 100%;
+ overflow-y: auto;
+ }
+
+ &__footers {
+ border-bottom: 1px solid $gallery;
+ }
+
+ &__token-icon {
+ width: 50px;
+ height: 50px;
+ }
+
+ &__token-symbol {
+ font-size: 18px;
+ line-height: 24px;
+ }
+
+ &__token-name {
+ font-size: 12px;
+ line-height: 16px;
+ }
+
+ &__buttons {
+ flex-flow: row nowrap;
+ width: 100%;
+ align-items: center;
+ justify-content: center;
+ padding: 12px 0;
+ margin: 0;
+ border-top: 1px solid $gallery;
+
+ button {
+ flex: 1 0 auto;
+ margin: 0 12px;
+ }
+ }
+ }
+}
diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss
new file mode 100644
index 000000000..8ba084b4a
--- /dev/null
+++ b/ui/app/css/itcss/components/buttons.scss
@@ -0,0 +1,108 @@
+/*
+ Buttons
+ */
+
+.btn-green {
+ background-color: #02c9b1; // TODO: reusable color in colors.css
+}
+
+button.btn-clear {
+ background: $white;
+ border: 1px solid;
+}
+
+// No longer used in flat design, remove when modal buttons done
+// div.wallet-btn {
+// border: 1px solid rgb(91, 93, 103);
+// border-radius: 2px;
+// height: 30px;
+// width: 75px;
+// font-size: 0.8em;
+// text-align: center;
+// line-height: 25px;
+// }
+
+// .btn-red {
+// background: rgba(254, 35, 17, 1);
+// box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36);
+// }
+
+button[disabled],
+input[type="submit"][disabled] {
+ cursor: not-allowed;
+ opacity: .5;
+ // background: rgba(197, 197, 197, 1);
+ // box-shadow: 0 3px 6px rgba(197, 197, 197, .36);
+}
+
+// button.spaced {
+// margin: 2px;
+// }
+
+// button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover {
+// transform: scale(1.1);
+// }
+// button:not([disabled]):active, input[type="submit"]:not([disabled]):active {
+// transform: scale(0.95);
+// }
+
+button.primary {
+ padding: 8px 12px;
+ background: #f7861c;
+ box-shadow: 0 3px 6px rgba(247, 134, 28, .36);
+ color: $white;
+ font-size: 1.1em;
+ font-family: Roboto;
+ text-transform: uppercase;
+}
+
+.btn-light {
+ padding: 8px 12px;
+ // background: #FFFFFF; // $bg-white
+ box-shadow: 0 3px 6px rgba(247, 134, 28, .36);
+ color: #585d67; // TODO: make reusable light button color
+ font-size: 1.1em;
+ font-family: Roboto;
+ text-transform: uppercase;
+ text-align: center;
+ line-height: 20px;
+ border-radius: 2px;
+ border: 1px solid #979797; // #TODO: make reusable light border color
+ opacity: .5;
+}
+
+// TODO: cleanup: not used anywhere
+button.btn-thin {
+ border: 1px solid;
+ border-color: #4d4d4d;
+ color: #4d4d4d;
+ background: rgb(255, 174, 41);
+ border-radius: 4px;
+ min-width: 200px;
+ margin: 12px 0;
+ padding: 6px;
+ font-size: 13px;
+}
+
+.btn-secondary {
+ border: 1px solid #979797;
+ border-radius: 2px;
+ background-color: $white;
+ font-size: 16px;
+ line-height: 24px;
+ padding: 16px 42px;
+
+ &[disabled] {
+ background-color: $white !important;
+ opacity: .5;
+ }
+}
+
+.btn-tertiary {
+ border: 1px solid transparent;
+ border-radius: 2px;
+ background-color: transparent;
+ font-size: 16px;
+ line-height: 24px;
+ padding: 16px 42px;
+}
diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss
new file mode 100644
index 000000000..4a8232e39
--- /dev/null
+++ b/ui/app/css/itcss/components/confirm.scss
@@ -0,0 +1,330 @@
+.confirm-screen-container {
+ position: relative;
+ align-items: center;
+ font-family: Roboto;
+ flex: 0 0 auto;
+ flex-flow: column nowrap;
+ box-shadow: 0 2px 4px 0 rgba($black, .08);
+ border-radius: 8px;
+
+ @media screen and (max-width: 575px) {
+ width: 100%;
+ }
+
+ @media screen and (min-width: 576px) {
+ // top: -26px;
+ }
+}
+
+.notification {
+ .confirm-screen-wrapper {
+
+ @media screen and (max-width: $break-small) {
+ height: calc(100vh - 85px);
+ }
+ }
+}
+
+.confirm-screen-wrapper {
+ height: 100%;
+ width: 380px;
+ background-color: $white;
+ display: flex;
+ flex-flow: column nowrap;
+ z-index: 25;
+ align-items: center;
+ font-family: Roboto;
+ position: relative;
+ overflow-y: auto;
+ overflow-x: hidden;
+ border-top-left-radius: 8px;
+ border-top-right-radius: 8px;
+
+ @media screen and (max-width: $break-small) {
+ width: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
+ top: 0;
+ box-shadow: none;
+ height: calc(100vh - 58px - 85px);
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+}
+
+.confirm-screen-wrapper > .confirm-screen-total-box {
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+.confirm-screen-wrapper > .confirm-memo-wrapper {
+ margin: 0;
+}
+
+.confirm-screen-header {
+ height: 88px;
+ background-color: $athens-grey;
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 22px;
+ line-height: 29px;
+ width: 100%;
+ padding: 25px 0;
+ flex: 0 0 auto;
+
+ @media screen and (max-width: $break-small) {
+ font-size: 20px;
+ }
+}
+
+.confirm-screen-header-tip {
+ height: 25px;
+ width: 25px;
+ background: $athens-grey;
+ position: absolute;
+ transform: rotate(45deg);
+ top: 71px;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+}
+
+.confirm-screen-title {
+ line-height: 27px;
+
+ @media screen and (max-width: $break-small) {
+ margin-left: 22px;
+ margin-right: 8px;
+ }
+}
+
+.confirm-screen-back-button {
+ background: transparent;
+ border: 1px solid $curious-blue;
+ left: 24px;
+ position: absolute;
+ text-align: center;
+ color: $curious-blue;
+ padding: 6px 13px 7px 12px;
+ border-radius: 2px;
+ height: 30px;
+ width: 54px;
+
+ @media screen and (max-width: $break-small) {
+ margin-right: 12px;
+ }
+}
+
+.confirm-screen-account-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.confirm-screen-account-name {
+ margin-top: 12px;
+ font-size: 14px;
+ line-height: 19px;
+ color: $scorpion;
+ text-align: center;
+}
+
+.confirm-screen-row-info {
+ font-size: 16px;
+ line-height: 21px;
+}
+
+.confirm-screen-account-number {
+ font-size: 10px;
+ line-height: 16px;
+ color: $dusty-gray;
+ text-align: center;
+ height: 16px;
+}
+
+.confirm-send-ether,
+.confirm-send-token {
+ i.fa-arrow-right {
+ align-self: start;
+ margin: 24px 14px 0 !important;
+ }
+}
+
+.confirm-screen-identicons {
+ margin-top: 24px;
+ flex: 0 0 auto;
+
+ i.fa-arrow-right {
+ align-self: start;
+ margin: 42px 14px 0;
+ }
+
+ i.fa-file-text-o {
+ font-size: 60px;
+ margin: 16px 8px 0 8px;
+ text-align: center;
+ }
+}
+
+.confirm-screen-sending-to-message {
+ text-align: center;
+ font-size: 16px;
+ margin-top: 30px;
+ font-family: 'DIN NEXT Light';
+}
+
+.confirm-screen-send-amount {
+ color: $scorpion;
+ margin-top: 12px;
+ text-align: center;
+ font-size: 40px;
+ font-weight: 300;
+ line-height: 53px;
+ flex: 0 0 auto;
+}
+
+.confirm-screen-send-amount-currency {
+ font-size: 20px;
+ line-height: 20px;
+ text-align: center;
+ flex: 0 0 auto;
+}
+
+.confirm-memo-wrapper {
+ min-height: 24px;
+ width: 100%;
+ border-bottom: 1px solid $alto;
+ flex: 0 0 auto;
+}
+
+.confirm-screen-send-memo {
+ color: $scorpion;
+ font-size: 16px;
+ line-height: 19px;
+ font-weight: 400;
+}
+
+.confirm-screen-label {
+ font-size: 18px;
+ line-height: 40px;
+ color: $scorpion;
+ text-align: left;
+}
+
+section .confirm-screen-account-name,
+section .confirm-screen-account-number,
+.confirm-screen-row-info,
+.confirm-screen-row-detail {
+ text-align: left;
+}
+
+.confirm-screen-rows {
+ display: flex;
+ flex-flow: column nowrap;
+ width: 100%;
+ flex: 0 0 auto;
+}
+
+.confirm-screen-section-column {
+ flex: .5;
+}
+
+.confirm-screen-row {
+ display: flex;
+ flex-flow: row nowrap;
+ border-bottom: 1px solid $alto;
+ width: 100%;
+ align-items: center;
+ padding: 12px;
+ padding-left: 35px;
+ font-size: 16px;
+ line-height: 22px;
+ font-weight: 300;
+}
+
+.confirm-screen-row-detail {
+ font-size: 12px;
+ line-height: 16px;
+ color: $dusty-gray;
+}
+
+.confirm-screen-total-box {
+ background-color: $wild-sand;
+ padding: 20px;
+ padding-left: 35px;
+ border-bottom: 1px solid $alto;
+
+ .confirm-screen-label {
+ line-height: 18px;
+ }
+
+ .confirm-screen-row-detail {
+ color: $scorpion;
+ }
+
+ &__subtitle {
+ font-size: 12px;
+ line-height: 22px;
+ }
+
+ .confirm-screen-row-info {
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 21px;
+ }
+}
+
+.confirm-screen-confirm-button {
+ height: 62px;
+ border-radius: 2px;
+ background-color: #02c9b1;
+ font-size: 16px;
+ color: $white;
+ text-align: center;
+ font-family: Roboto;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ border-width: 0;
+ box-shadow: none;
+ flex: 1 0 auto;
+ font-weight: 300;
+ margin: 0 8px;
+}
+
+.btn-light.confirm-screen-cancel-button {
+ height: 62px;
+ background: none;
+ border: none;
+ opacity: 1;
+ font-family: Roboto;
+ border-width: 0;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ font-size: 16px;
+ line-height: 32px;
+ box-shadow: none;
+ cursor: pointer;
+ flex: 1 0 auto;
+ font-weight: 300;
+ margin: 0 8px;
+}
+
+#pending-tx-form {
+ flex: 1 0 auto;
+ position: relative;
+ display: flex;
+ flex-flow: row nowrap;
+ background-color: $white;
+ padding: 12px 18px;
+ border-bottom-left-radius: 8px;
+ border-bottom-right-radius: 8px;
+ width: 100%;
+
+ @media screen and (max-width: $break-small) {
+ border-top: 1px solid $alto;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+}
diff --git a/ui/app/css/itcss/components/currency-display.scss b/ui/app/css/itcss/components/currency-display.scss
new file mode 100644
index 000000000..9459629b6
--- /dev/null
+++ b/ui/app/css/itcss/components/currency-display.scss
@@ -0,0 +1,56 @@
+.currency-display {
+ height: 54px;
+ width: 100%ß;
+ border: 1px solid $alto;
+ border-radius: 4px;
+ background-color: $white;
+ color: $dusty-gray;
+ font-family: Roboto;
+ font-size: 16px;
+ font-weight: 300;
+ padding: 8px 10px;
+ position: relative;
+
+ &__primary-row {
+ display: flex;
+ }
+
+ &__input {
+ color: $scorpion;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 22px;
+ border: none;
+ outline: 0 !important;
+ max-width: 100%;
+ }
+
+ &__primary-currency {
+ color: $scorpion;
+ font-weight: 400;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 22px;
+ }
+
+ &__converted-row {
+ display: flex;
+ }
+
+ &__converted-value,
+ &__converted-currency {
+ color: $dusty-gray;
+ font-family: Roboto;
+ font-size: 12px;
+ line-height: 12px;
+ }
+
+ &__input-wrapper {
+ position: relative;
+ display: flex;
+ }
+
+ &__currency-symbol {
+ margin-top: 1px;
+ }
+} \ No newline at end of file
diff --git a/ui/app/css/itcss/components/editable-label.scss b/ui/app/css/itcss/components/editable-label.scss
new file mode 100644
index 000000000..c69ea1428
--- /dev/null
+++ b/ui/app/css/itcss/components/editable-label.scss
@@ -0,0 +1,35 @@
+.editable-label {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+
+ &__value {
+ max-width: 250px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ &__input {
+ width: 250px;
+ font-size: 14px;
+ text-align: center;
+ border: 1px solid $alto;
+
+ &--error {
+ border: 1px solid $monzo;
+ }
+ }
+
+ &__icon-wrapper {
+ position: absolute;
+ margin-left: 10px;
+ left: 100%;
+ }
+
+ &__icon {
+ cursor: pointer;
+ color: $dusty-gray;
+ }
+}
diff --git a/ui/app/css/itcss/components/footer.scss b/ui/app/css/itcss/components/footer.scss
new file mode 100644
index 000000000..000a53eed
--- /dev/null
+++ b/ui/app/css/itcss/components/footer.scss
@@ -0,0 +1,4 @@
+.app-footer {
+ padding-bottom: 10px;
+ align-items: center;
+}
diff --git a/ui/app/css/itcss/components/gas-slider.scss b/ui/app/css/itcss/components/gas-slider.scss
new file mode 100644
index 000000000..c27a560bd
--- /dev/null
+++ b/ui/app/css/itcss/components/gas-slider.scss
@@ -0,0 +1,51 @@
+.gas-slider {
+ position: relative;
+ width: 313px;
+
+ &__input {
+ width: 317px;
+ margin-left: -2px;
+ z-index: 2;
+ }
+
+ input[type=range] {
+ -webkit-appearance: none !important;
+ }
+
+ input[type=range]::-webkit-slider-thumb {
+ -webkit-appearance: none !important;
+ height: 26px;
+ width: 26px;
+ border: 2px solid #B8B8B8;
+ background-color: #FFFFFF;
+ box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08);
+ border-radius: 50%;
+ position: relative;
+ z-index: 10;
+ }
+
+ &__bar {
+ height: 6px;
+ width: 313px;
+ background: $alto;
+ display: flex;
+ justify-content: space-between;
+ position: absolute;
+ top: 11px;
+ z-index: 0;
+ }
+
+ &__low, &__high {
+ height: 6px;
+ width: 49px;
+ z-index: 1;
+ }
+
+ &__low {
+ background-color: $crimson;
+ }
+
+ &__high {
+ background-color: $caribbean-green;
+ }
+} \ No newline at end of file
diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/css/itcss/components/header.scss
new file mode 100644
index 000000000..a6332f819
--- /dev/null
+++ b/ui/app/css/itcss/components/header.scss
@@ -0,0 +1,100 @@
+.app-header {
+ align-items: center;
+ visibility: visible;
+ background: $gallery;
+ position: relative;
+ z-index: $header-z-index;
+ display: flex;
+ flex-flow: column nowrap;
+
+ @media screen and (max-width: 575px) {
+ padding: 12px;
+ width: 100%;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, .08);
+ z-index: $mobile-header-z-index;
+ }
+
+ @media screen and (min-width: 576px) {
+ height: 75px;
+ justify-content: center;
+
+ &::after {
+ content: '';
+ position: absolute;
+ width: 100%;
+ height: 32px;
+ background: $gallery;
+ bottom: -32px;
+ }
+ }
+
+ .metafox-icon {
+ cursor: pointer;
+ }
+}
+
+.app-header-contents {
+ display: flex;
+ justify-content: space-between;
+ flex-flow: row nowrap;
+ width: 100%;
+ height: 6.9vh;
+
+ @media screen and (max-width: 575px) {
+ height: 100%;
+ }
+
+ @media screen and (min-width: 576px) {
+ width: 85vw;
+ }
+
+ @media screen and (min-width: 769px) {
+ width: 80vw;
+ }
+
+ @media screen and (min-width: 1281px) {
+ width: 65vw;
+ }
+}
+
+.app-header h1 {
+ font-family: Roboto;
+ text-transform: uppercase;
+ font-weight: 400;
+ color: #22232c; // $shark
+ line-height: 29px;
+
+ @media screen and (max-width: 575px) {
+ display: none;
+ }
+}
+
+h2.page-subtitle {
+ text-transform: uppercase;
+ color: #aeaeae;
+ font-size: 1em;
+ margin: 12px;
+}
+
+.network-component-wrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.left-menu-wrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ cursor: pointer;
+}
+
+.header__right-actions {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+
+ .identicon {
+ cursor: pointer;
+ }
+}
diff --git a/ui/app/css/itcss/components/hero-balance.scss b/ui/app/css/itcss/components/hero-balance.scss
new file mode 100644
index 000000000..bdbdd2645
--- /dev/null
+++ b/ui/app/css/itcss/components/hero-balance.scss
@@ -0,0 +1,114 @@
+.hero-balance {
+
+ @media screen and (max-width: $break-small) {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ margin: .3em .9em 0;
+ // height: 80vh;
+ // max-height: 225px;
+ flex: 0 0 auto;
+ }
+
+ @media screen and (min-width: $break-large) {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ margin: 2.8em 2.37em .8em;
+ }
+
+ .balance-container {
+ display: flex;
+ margin: 0;
+ justify-content: flex-start;
+ align-items: center;
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ flex: 0 0 auto;
+ }
+
+ @media screen and (min-width: $break-large) {
+ flex-direction: row;
+ flex-grow: 3;
+ }
+ }
+
+ .balance-display {
+
+ @media screen and (max-width: $break-small) {
+ text-align: center;
+
+ .token-amount {
+ font-size: 175%;
+ margin-top: 12.5%;
+ }
+
+ .fiat-amount {
+ font-size: 115%;
+ margin-top: 8.5%;
+ color: #a0a0a0;
+ }
+ }
+
+ @media screen and (min-width: $break-large) {
+ margin-left: 3%;
+ justify-content: flex-start;
+ align-items: flex-start;
+
+ .token-amount {
+ font-size: 135%;
+ }
+
+ .fiat-amount {
+ margin-top: .25%;
+ font-size: 105%;
+ }
+ }
+ }
+
+ .balance-icon {
+ border-radius: 25px;
+ width: 45px;
+ height: 45px;
+ border: 1px solid $alto;
+ }
+
+ .hero-balance-buttons {
+
+ @media screen and (max-width: $break-small) {
+ width: 100%;
+ // height: 100px; // needed a round number to set the heights of the buttons inside
+ flex: 0 0 auto;
+ padding: 16px 0;
+ }
+
+ @media screen and (min-width: $break-large) {
+ flex-grow: 2;
+ justify-content: flex-end;
+ }
+
+ button.btn-clear {
+ background: $white;
+ border: 1px solid;
+ border-radius: 2px;
+ font-size: 12px;
+
+ @media screen and (max-width: $break-small) {
+ border-color: $curious-blue;
+ color: $curious-blue;
+ height: 36px;
+ }
+
+ @media screen and (min-width: $break-large) {
+ border-color: $curious-blue;
+ color: $curious-blue;
+ padding: 0;
+ width: 85px;
+ height: 34px;
+ }
+ }
+ }
+}
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
new file mode 100644
index 000000000..dfb4f23f0
--- /dev/null
+++ b/ui/app/css/itcss/components/index.scss
@@ -0,0 +1,53 @@
+@import './buttons.scss';
+
+@import './header.scss';
+
+@import './footer.scss';
+
+@import './network.scss';
+
+@import './modal.scss';
+
+@import './newui-sections.scss';
+
+@import './account-dropdown.scss';
+
+@import './send.scss';
+
+@import './confirm.scss';
+
+@import './loading-overlay.scss';
+
+// Balances
+@import './hero-balance.scss';
+
+@import './wallet-balance.scss';
+
+// Tx List and Sections
+@import './transaction-list.scss';
+
+@import './sections.scss';
+
+@import './token-list.scss';
+
+@import './add-token.scss';
+
+@import './currency-display.scss';
+
+@import './account-menu.scss';
+
+@import './menu.scss';
+
+@import './gas-slider.scss';
+
+@import './settings.scss';
+
+@import './tab-bar.scss';
+
+@import './simple-dropdown.scss';
+
+@import './request-signature.scss';
+
+@import './account-dropdown-mini.scss';
+
+@import './editable-label.scss';
diff --git a/ui/app/css/itcss/components/loading-overlay.scss b/ui/app/css/itcss/components/loading-overlay.scss
new file mode 100644
index 000000000..15009c1e6
--- /dev/null
+++ b/ui/app/css/itcss/components/loading-overlay.scss
@@ -0,0 +1,21 @@
+.loading-overlay {
+ left: 0px;
+ z-index: 50;
+ position: absolute;
+ flex-direction: column;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ background: rgba(255, 255, 255, 0.8);
+
+ @media screen and (max-width: 575px) {
+ margin-top: 56px;
+ height: calc(100% - 56px);
+ }
+
+ @media screen and (min-width: 576px) {
+ margin-top: 75px;
+ height: calc(100% - 75px);
+ }
+}
diff --git a/ui/app/css/itcss/components/menu.scss b/ui/app/css/itcss/components/menu.scss
new file mode 100644
index 000000000..17e24de98
--- /dev/null
+++ b/ui/app/css/itcss/components/menu.scss
@@ -0,0 +1,59 @@
+.menu {
+ border-radius: 4px;
+ background: rgba($black, .8);
+ box-shadow: rgba($black, .15) 0 2px 2px 2px;
+ min-width: 150px;
+ color: $white;
+
+ &__item {
+ padding: 18px;
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ position: relative;
+ z-index: 200;
+ font-weight: 200;
+
+ @media screen and (max-width: 575px) {
+ padding: 14px;
+ }
+
+ &--clickable {
+ cursor: pointer;
+
+ &:hover {
+ background-color: rgba($white, .05);
+ }
+
+ &:active {
+ background-color: rgba($white, .1);
+ }
+ }
+
+ &__icon {
+ height: 16px;
+ width: 16px;
+ margin-right: 14px;
+ }
+
+ &__text {
+ font-size: 16px;
+ line-height: 21px;
+ }
+ }
+
+ &__divider {
+ background-color: $scorpion;
+ width: 100%;
+ height: 1px;
+ }
+
+ &__close-area {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ z-index: 100;
+ }
+}
diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss
new file mode 100644
index 000000000..b69bd5c7e
--- /dev/null
+++ b/ui/app/css/itcss/components/modal.scss
@@ -0,0 +1,565 @@
+.modal > div:focus {
+ outline: none !important;
+}
+
+// Buy Modal
+.buy-modal-content {
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ font-family: Roboto;
+ padding: 0 16px;
+}
+
+.buy-modal-content-option {
+ cursor: pointer;
+ color: #5B5D67;
+}
+
+.qr-ellip-address, .ellip-address {
+ width: 247px;
+ border: none;
+ font-family: Roboto;
+ font-size: 14px;
+}
+
+@media screen and (max-width: 575px) {
+ .buy-modal-content-title-wrapper {
+ justify-content: space-around;
+ width: 100%;
+ height: 100px;
+ }
+
+ .buy-modal-content-title {
+ font-size: 26px;
+ margin-top: 15px;
+ }
+
+ .buy-modal-content-options {
+ flex-direction: column;
+ padding: 5% 33%;
+ }
+
+ .buy-modal-content-footer {
+ text-transform: uppercase;
+ width: 100%;
+ height: 50px;
+ }
+
+ div.buy-modal-content-option {
+ display: flex;
+ flex-direction: column;
+ width: 80vw;
+ height: 15vh;
+ margin: 10px;
+ text-align: center;
+ border-radius: 6px;
+ border: 1px solid $black;
+ padding: 0% 7%;
+ justify-content: center;
+
+ div.buy-modal-content-option-title {
+ font-size: 20px;
+ }
+
+ div.buy-modal-content-option-subtitle {
+ font-size: 16px;
+ }
+ }
+}
+
+@media screen and (min-width: 576px) {
+ .buy-modal-content-title-wrapper {
+ justify-content: space-around;
+ width: 100%;
+ height: 110px;
+ }
+
+ .buy-modal-content-title {
+ font-size: 26px;
+ margin-top: 15px;
+ }
+
+ .buy-modal-content-footer {
+ text-transform: uppercase;
+ width: 100%;
+ height: 50px;
+ }
+
+ .buy-modal-content-options {
+ flex-direction: row;
+ margin: 20px 0 60px;
+ }
+
+ div.buy-modal-content-option {
+ display: flex;
+ flex-direction: column;
+ width: 20vw;
+ height: 120px;
+ text-align: center;
+ border-radius: 6px;
+ border: 1px solid $black;
+ margin: 0 8px;
+ padding: 18px 0;
+
+ div.buy-modal-content-option-title {
+ font-size: 20px;
+ margin-bottom: 12px;
+
+ @media screen and (max-width: 679px) {
+ font-size: 14px;
+ }
+
+ @media screen and (min-width: 1281px) {
+ font-size: 20px;
+ }
+ }
+
+ div.buy-modal-content-option-subtitle {
+ font-size: 16px;
+ padding: 0 10px;
+ height: 25%;
+
+ @media screen and (max-width: 679px) {
+ font-size: 10px;
+ padding: 0 10px;
+ margin-bottom: 5px;
+ line-height: 15px;
+ }
+
+ @media screen and (min-width: 680px) {
+ font-size: 14px;
+ padding: 0 4px;
+ margin-bottom: 2px;
+ }
+
+ @media screen and (min-width: 1281px) {
+ font-size: 16px;
+ padding: 0;
+ }
+ }
+
+ div.buy-modal-content-footer {
+ margin-top: 8vh;
+ }
+ }
+}
+
+// Edit Account Name Modal
+.edit-account-name-modal-content {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ position: relative;
+}
+
+.edit-account-name-modal-cancel {
+ position: absolute;
+ top: 12px;
+ right: 20px;
+ font-size: 25px;
+}
+
+.edit-account-name-modal-title {
+ margin: 15px;
+}
+
+.edit-account-name-modal-save-button {
+ width: 33%;
+ height: 45px;
+ margin: 15px;
+ font-weight: 700;
+ margin-top: 25px;
+}
+
+.edit-account-name-modal-input {
+ width: 90%;
+ height: 50px;
+ text-align: left;
+ margin: 10px;
+ padding: 10px;
+ font-size: 18px;
+}
+
+// Account Modal Container
+.account-modal-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ position: relative;
+ padding: 5px 0 31px 0;
+ border: 1px solid $silver;
+ border-radius: 4px;
+ font-family: Roboto;
+
+ button {
+ cursor: pointer;
+ }
+}
+
+.account-modal-back {
+ color: $dusty-gray;
+ position: absolute;
+ top: 13px;
+ left: 17px;
+ cursor: pointer;
+
+ &__text {
+ margin-top: 2px;
+ font-family: Roboto;
+ font-size: 14px;
+ line-height: 18px;
+ }
+}
+
+.account-modal-close::after {
+ content: '\00D7';
+ font-size: 40px;
+ color: $dusty-gray;
+ position: absolute;
+ top: 10px;
+ right: 12px;
+ cursor: pointer;
+}
+
+.account-modal-container .identicon {
+ position: relative;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+ top: -32px;
+ margin-bottom: -32px;
+}
+
+
+// Account Details Modal
+
+.account-modal-container {
+
+ .qr-header {
+ margin-top: 9px;
+ font-size: 20px;
+ }
+
+ .qr-wrapper {
+ margin-top: 5px;
+ }
+
+ .ellip-address-wrapper {
+ display: flex;
+ justify-content: center;
+ border: 1px solid $alto;
+ padding: 5px 10px;
+ font-family: Roboto;
+ margin-top: 7px;
+ width: 286px;
+ }
+
+ .btn-clear {
+ min-height: 28px;
+ font-size: 14px;
+ border-color: $curious-blue;
+ color: $curious-blue;
+ border-radius: 2px;
+ flex-basis: 100%;
+ width: 75%;
+ margin-top: 17px;
+ padding: 10px 22px;
+ height: 44px;
+ width: 235px;
+ font-family: Roboto;
+ }
+}
+
+.account-modal-divider {
+ width: 100%;
+ height: 1px;
+ margin: 19px 0 8px 0;
+ background-color: $alto;
+}
+
+// Export Private Key Modal
+
+.account-modal-container .account-name {
+ margin-top: 9px;
+ font-size: 20px;
+}
+
+.account-modal-container .modal-body-title {
+ margin-top: 16px;
+ margin-bottom: 16px;
+ font-size: 18px;
+}
+
+.account-modal__name {
+ margin-top: 9px;
+ font-size: 20px;
+}
+
+.private-key-password {
+ display: flex;
+ flex-direction: column;
+}
+
+.private-key-password-label, .private-key-password-error {
+ color: $scorpion;
+ font-size: 14px;
+ line-height: 18px;
+ margin-bottom: 10px;
+}
+
+.private-key-password-error {
+ color: $crimson;
+ margin-bottom: 0;
+}
+
+.private-key-password-input {
+ padding: 10px 0 13px 17px;
+ font-size: 16px;
+ line-height: 21px;
+ width: 291px;
+ height: 44px;
+}
+
+.private-key-password::-webkit-input-placeholder {
+ color: $dusty-gray;
+ font-family: Roboto;
+}
+
+.private-key-password-warning {
+ border-radius: 8px;
+ background-color: #FFF6F6;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 15px;
+ color: $crimson;
+ width: 292px;
+ padding: 9px 15px;
+ margin-top: 18px;
+ font-family: Roboto;
+}
+
+.export-private-key-buttons {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+
+ .btn-clear {
+ width: 141px;
+ height: 54px;
+ }
+
+ .btn-cancel {
+ margin-right: 15px;
+ border-color: $dusty-gray;
+ color: $scorpion;
+ }
+}
+
+.private-key-password-display-wrapper {
+ height: 80px;
+ width: 291px;
+ border: 1px solid $silver;
+ border-radius: 2px;
+}
+
+.private-key-password-display-textarea {
+ color: $crimson;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 21px;
+ border: none;
+ height: 75px;
+ width: 100%;
+ overflow: hidden;
+ resize: none;
+ padding: 9px 13px 8px;
+ text-transform: uppercase;
+ font-weight: 300;
+}
+
+
+// New Account Modal
+.new-account-modal-wrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ position: relative;
+ border: 1px solid $alto;
+ box-shadow: 0 0 2px 2px $alto;
+ font-family: Roboto;
+}
+
+.new-account-modal-header {
+ background: $wild-sand;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ padding: 30px;
+ font-size: 22px;
+ color: $nile-blue;
+ height: 79px;
+}
+
+.modal-close-x::after {
+ content: '\00D7';
+ font-size: 2em;
+ color: $dusty-gray;
+ position: absolute;
+ top: 25px;
+ right: 17.5px;
+ font-family: sans-serif;
+ cursor: pointer;
+}
+
+.new-account-modal-content {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ margin-top: 15px;
+ font-size: 17px;
+ color: $nile-blue;
+}
+
+.new-account-modal-content.after-input {
+ margin-top: 15px;
+ line-height: 25px;
+}
+
+.new-account-input-wrapper {
+ display: flex;
+ width: 100%;
+ justify-content: center;
+ padding-bottom: 2px;
+ margin-top: 13px;
+}
+
+.new-account-input {
+ padding: 15px;
+ padding-bottom: 20px;
+ border-radius: 8px;
+ border: 1px solid $alto;
+ width: 100%;
+ font-size: 1em;
+ color: $dusty-gray;
+ font-family: Roboto;
+ font-size: 17px;
+ margin: 0 60px;
+}
+
+// For reference on below placeholder selectors: https://stackoverflow.com/questions/2610497/change-an-html5-inputs-placeholder-color-with-css
+.new-account-input::-webkit-input-placeholder {
+ color: $dusty-gray;
+}
+
+.new-account-input:-moz-placeholder {
+ color: $dusty-gray;
+ opacity: 1;
+}
+
+.new-account-input::-moz-placeholder {
+ color: $dusty-gray;
+ opacity: 1;
+}
+
+.new-account-input:-ms-input-placeholder {
+ color: $dusty-gray;
+}
+
+.new-account-input::-ms-input-placeholder {
+ color: $dusty-gray;
+}
+
+.new-account-modal-content.button {
+ margin-top: 22px;
+ margin-bottom: 30px;
+ width: 113px;
+ height: 44px;
+}
+
+.new-account-modal-wrapper .btn-clear {
+ font-size: 14px;
+ font-weight: 700;
+ background: $white;
+ border: 1px solid;
+ border-radius: 2px;
+ color: $tundora;
+ flex: 1;
+}
+
+// Hide token confirmation
+
+.hide-token-confirmation {
+ min-height: 250.72px;
+ width: 374.49px;
+ border-radius: 4px;
+ background-color: #FFFFFF;
+ box-shadow: 0 1px 7px 0 rgba(0,0,0,0.5);
+
+ &__container {
+ padding: 24px 27px 21px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ &__identicon {
+ margin-bottom: 10px
+ }
+
+ &__symbol {
+ color: $tundora;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 24px;
+ text-align: center;
+ margin-bottom: 7.5px;
+ }
+
+ &__title {
+ height: 30px;
+ width: 271.28px;
+ color: $tundora;
+ font-family: Roboto;
+ font-size: 22px;
+ line-height: 30px;
+ text-align: center;
+ margin-bottom: 10.5px;
+ }
+
+ &__copy {
+ height: 41px;
+ width: 318px;
+ color: $scorpion;
+ font-family: Roboto;
+ font-size: 14px;
+ line-height: 18px;
+ text-align: center;
+ }
+
+ &__buttons {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ margin-top: 15px;
+ width: 100%;
+
+ button {
+ height: 44px;
+ width: 113px;
+ border: 1px solid $scorpion;
+ border-radius: 2px;
+ color: $tundora;
+ font-family: Roboto;
+ font-size: 14px;
+ line-height: 20px;
+ text-align: center;
+ margin-left: 4px;
+ margin-right: 4px;
+ }
+ }
+}
diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss
new file mode 100644
index 000000000..98dbdffb2
--- /dev/null
+++ b/ui/app/css/itcss/components/network.scss
@@ -0,0 +1,169 @@
+.network-component--disabled {
+ // border-color: transparent !important;
+ cursor: default;
+
+ .fa-caret-down {
+ opacity: 0;
+ }
+}
+
+.network-component.pointer {
+ border: 1px solid $shark;
+ border-radius: 82px;
+ padding: 6px;
+ flex: 0 0 auto;
+
+ &.ethereum-network {
+ border-color: rgb(3, 135, 137);
+
+ .menu-icon-circle div {
+ background-color: rgba(3, 135, 137, .7) !important;
+ }
+ }
+
+ &.ropsten-test-network {
+ border-color: rgb(233, 21, 80);
+
+ .menu-icon-circle div {
+ background-color: rgba(233, 21, 80, .7) !important;
+ }
+ }
+
+ &.kovan-test-network {
+ border-color: rgb(105, 4, 150);
+
+ .menu-icon-circle div {
+ background-color: rgba(105, 4, 150, .7) !important;
+ }
+ }
+
+ &.rinkeby-test-network {
+ border-color: rgb(235, 179, 63);
+
+ .menu-icon-circle div {
+ background-color: rgba(235, 179, 63, .7) !important;
+ }
+ }
+}
+
+.dropdown-menu-item {
+ .menu-icon-circle,
+ .menu-icon-circle--active {
+ margin: 0 14px;
+ }
+}
+
+.network-indicator {
+ display: flex;
+ align-items: center;
+ font-size: .6em;
+
+ .fa-caret-down {
+ line-height: 15px;
+ font-size: 12px;
+ padding: 0 4px;
+ }
+}
+
+.network-name {
+ line-height: 15px;
+ padding: 0 4px;
+ font-family: Roboto;
+ font-size: 12px;
+ flex: 1 0 auto;
+}
+
+.network-droppo {
+ right: 2px;
+
+ @media screen and (min-width: 576px) {
+ right: calc(((100% - 85vw) / 2) + 2px);
+ }
+
+ @media screen and (min-width: 769px) {
+ right: calc(((100% - 80vw) / 2) + 2px);
+ }
+
+ @media screen and (min-width: 1281px) {
+ right: calc(((100% - 65vw) / 2) + 2px);
+ }
+}
+
+.network-name-item {
+ font-weight: 100;
+ flex: 1 0 auto;
+ color: $dusty-gray;
+}
+
+.network-check,
+.network-check__transparent {
+ color: $white;
+ margin-left: 7px;
+}
+
+.network-check__transparent {
+ opacity: 0;
+ width: 16px;
+ margin: 0;
+}
+
+.menu-icon-circle,
+.menu-icon-circle--active {
+ background: none;
+ border-radius: 22px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 1px solid transparent;
+ margin: 0 4px;
+}
+
+.menu-icon-circle--active {
+ border: 1px solid $white;
+ background: rgba(100, 100, 100, .4);
+}
+
+.menu-icon-circle div,
+.menu-icon-circle--active div {
+ height: 12px;
+ width: 12px;
+ border-radius: 17px;
+}
+
+.menu-icon-circle--active div {
+ opacity: 1;
+}
+
+.network-dropdown-header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+}
+
+.network-dropdown-divider {
+ width: 100%;
+ height: 1px;
+ margin: 10px 0;
+ background-color: $scorpion;
+}
+
+.network-dropdown-title {
+ height: 25px;
+ width: 75px;
+ color: $white;
+ font-family: Roboto;
+ font-size: 18px;
+ line-height: 25px;
+ text-align: center;
+}
+
+.network-dropdown-content {
+ height: 36px;
+ width: 265px;
+ color: $dusty-gray;
+ font-family: Roboto;
+ font-size: 14px;
+ line-height: 18px;
+}
+
diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss
new file mode 100644
index 000000000..244de2ba0
--- /dev/null
+++ b/ui/app/css/itcss/components/newui-sections.scss
@@ -0,0 +1,277 @@
+/*
+ NewUI Container Elements
+ */
+
+// Component Colors
+$tx-view-bg: $white;
+$wallet-view-bg: $wild-sand;
+
+// Main container
+.main-container {
+ // position: absolute;
+ z-index: $main-container-z-index;
+ font-family: Roboto;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: stretch;
+}
+
+.main-container::-webkit-scrollbar {
+ display: none;
+}
+
+// tx view
+
+.tx-view {
+ flex: 63.5 0 66.5%;
+ background: $tx-view-bg;
+
+ // No title on mobile
+ @media screen and (max-width: 575px) {
+ .identicon-wrapper {
+ display: none;
+ }
+
+ .account-name {
+ display: none;
+ }
+ }
+}
+
+// wallet view and sidebar
+
+.wallet-view {
+ display: flex;
+ flex-direction: column;
+ flex: 33.5 1 33.5%;
+ width: 0;
+ background: $wallet-view-bg;
+ z-index: 200;
+ position: relative;
+
+ @media screen and (min-width: 576px) {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ }
+
+ .wallet-view-account-details {
+ flex: 0 0 auto;
+ }
+
+ &__name-container {
+ flex: 0 0 auto;
+ cursor: pointer;
+ width: 100%;
+ }
+
+ &__keyring-label {
+ height: 40px;
+ color: $dusty-gray;
+ font-family: Roboto;
+ font-size: 10px;
+ line-height: 40px;
+ text-align: right;
+ padding: 0 20px;
+ }
+
+ &__details-button {
+ color: $curious-blue;
+ font-size: 10px;
+ line-height: 13px;
+ text-align: center;
+ border: 1px solid $curious-blue;
+ border-radius: 10.5px;
+ background-color: transparent;
+ margin: 0 auto;
+ padding: 4px 12px;
+ flex: 0 0 auto;
+ }
+
+ &__address {
+ border-radius: 3px;
+ background-color: $alto;
+ color: $scorpion;
+ font-size: 14px;
+ line-height: 12px;
+ padding: 4px 12px;
+ margin: 24px auto;
+ font-weight: 300;
+ cursor: pointer;
+ flex: 0 0 auto;
+ }
+
+ &__sidebar-close {
+
+ @media screen and (max-width: 575px) {
+ &::after {
+ content: '\00D7';
+ font-size: 40px;
+ color: $tundora;
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ cursor: pointer;
+ }
+ }
+ }
+
+ &__add-token-button {
+ flex: 0 0 auto;
+ color: $dusty-gray;
+ font-size: 14px;
+ line-height: 19px;
+ text-align: center;
+ margin: 36px auto;
+ border: 1px solid $dusty-gray;
+ border-radius: 2px;
+ font-weight: 300;
+ background: none;
+ padding: 9px 30px;
+ }
+}
+
+@media screen and (min-width: 576px) {
+ .wallet-view::-webkit-scrollbar {
+ display: none;
+ }
+}
+
+.wallet-view-title-wrapper {
+ flex: 0 0 25px;
+}
+
+.wallet-view-title {
+ margin-left: 15px;
+ font-size: 16px;
+
+ // No title on mobile
+ @media screen and (max-width: 575px) {
+ display: none;
+ }
+}
+
+.wallet-view.sidebar {
+ flex: 1 0 230px;
+ background: rgb(250, 250, 250);
+ z-index: $sidebar-z-index;
+ position: fixed;
+ top: 56px;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ opacity: 1;
+ visibility: visible;
+ will-change: transform;
+ overflow-y: auto;
+ box-shadow: rgba(0, 0, 0, .15) 2px 2px 4px;
+ width: 85%;
+ height: calc(100% - 56px);
+}
+
+.sidebar-overlay {
+ z-index: $sidebar-overlay-z-index;
+ position: fixed;
+ // top: 41px;
+ height: 100%;
+ width: 100%;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ opacity: 1;
+ visibility: visible;
+ background-color: rgba(0, 0, 0, .3);
+}
+
+// main-container media queries
+
+@media screen and (min-width: 576px) {
+ .lap-visible {
+ display: flex;
+ }
+
+ .phone-visible {
+ display: none;
+ }
+
+ .main-container {
+ // margin-top: 6.9vh;
+ width: 85%;
+ height: 90vh;
+ box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
+ }
+}
+
+@media screen and (min-width: 769px) {
+ .main-container {
+ // margin-top: 6.9vh;
+ width: 80%;
+ height: 82vh;
+ box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
+ }
+}
+
+@media screen and (min-width: 1281px) {
+ .main-container {
+ // margin-top: 6.9vh;
+ width: 65%;
+ height: 82vh;
+ box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
+ }
+}
+
+@media screen and (max-width: 575px) {
+ .lap-visible {
+ display: none;
+ }
+
+ .phone-visible {
+ display: flex;
+ }
+
+ .main-container {
+ // margin-top: 41px;
+ height: 100%;
+ width: 100%;
+ overflow-y: auto;
+ background-color: $white;
+ }
+
+ button.btn-clear {
+ width: 93px;
+ height: 50px;
+ font-size: .7em;
+ background: $white;
+ border: 1px solid;
+ }
+}
+
+// wallet view
+.account-name {
+ font-size: 24px;
+ font-weight: 200;
+ line-height: 20px;
+ color: $scorpion;
+ margin-top: 8px;
+ margin-bottom: 24px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ width: 100%;
+ padding: 0 8px;
+ text-align: center;
+}
+
+// account options dropdown
+.account-options-menu {
+ align-items: center;
+ justify-content: flex-start;
+ margin: 5% 7% 0%;
+}
+
+.fiat-amount {
+ text-transform: uppercase;
+}
+
+.token-balance__amount {
+ padding-right: 6px;
+}
diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss
new file mode 100644
index 000000000..d81099bfa
--- /dev/null
+++ b/ui/app/css/itcss/components/request-signature.scss
@@ -0,0 +1,230 @@
+.request-signature {
+ &__container {
+ width: 380px;
+ border-radius: 8px;
+ background-color: $white;
+ box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08);
+ display: flex;
+ flex-flow: column nowrap;
+ z-index: 25;
+ align-items: center;
+ font-family: Roboto;
+ position: relative;
+ height: 100%;
+
+ @media screen and (max-width: $break-small) {
+ width: 100%;
+ top: 0;
+ box-shadow: none;
+ }
+
+ @media screen and (min-width: $break-large) {
+ max-height: 620px;
+ }
+ }
+
+ &__header {
+ height: 64px;
+ width: 100%;
+ position: relative;
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
+ align-items: center;
+ flex: 0 0 auto;
+ }
+
+ &__header-background {
+ position: absolute;
+ background-color: $athens-grey;
+ z-index: 2;
+ width: 100%;
+ height: 100%;
+ }
+
+ &__header__text {
+ height: 29px;
+ width: 179px;
+ color: #5B5D67;
+ font-family: Roboto;
+ font-size: 22px;
+ font-weight: 300;
+ line-height: 29px;
+ z-index: 3;
+ }
+
+ &__header__tip-container {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ }
+
+ &__header__tip {
+ height: 25px;
+ width: 25px;
+ background: $athens-grey;
+ transform: rotate(45deg);
+ position: absolute;
+ bottom: -8px;
+ z-index: 1;
+ }
+
+ &__account-info {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 18px;
+ margin-bottom: 20px;
+ }
+
+ &__account {
+ color: $dusty-gray;
+ margin-left: 17px;
+ }
+
+ &__account-text {
+ font-size: 14px;
+ }
+
+ &__balance {
+ color: $dusty-gray;
+ margin-right: 17px;
+ width: 124px;
+ }
+
+ &__balance-text {
+ text-align: right;
+ font-size: 14px;
+ }
+
+ &__balance-value {
+ text-align: right;
+ margin-top: 2.5px;
+ }
+
+ &__request-icon {
+ margin-top: 25px;
+ }
+
+ &__body {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-flow: column;
+ flex: 1 1 auto;
+ height: 0;
+ }
+
+ &__request-info {
+ display: flex;
+ justify-content: center;
+ }
+
+ &__headline {
+ height: 48px;
+ width: 240px;
+ color: $tundora;
+ font-family: Roboto;
+ font-size: 18px;
+ font-weight: 300;
+ line-height: 24px;
+ text-align: center;
+ margin-top: 20px;
+ }
+
+ &__notice,
+ &__warning {
+ font-family: "Avenir Next";
+ font-size: 14px;
+ line-height: 19px;
+ text-align: center;
+ margin-top: 41px;
+ margin-bottom: 11px;
+ width: 100%;
+ }
+
+ &__notice {
+ color: $dusty-gray;
+ }
+
+ &__warning {
+ color: $crimson;
+ }
+
+ &__rows {
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ border-top: 1px solid $geyser;
+ display: flex;
+ flex-flow: column;
+ }
+
+ &__row {
+ display: flex;
+ flex-flow: column;
+ }
+
+ &__row-title {
+ width: 80px;
+ color: $dusty-gray;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 22px;
+ margin-top: 12px;
+ margin-left: 18px;
+ width: 100%;
+ }
+
+ &__row-value {
+ color: $scorpion;
+ font-family: Roboto;
+ font-size: 14px;
+ line-height: 19px;
+ width: 100%;
+ overflow-wrap: break-word;
+ border-bottom: 1px solid #d2d8dd;
+ padding: 6px 18px 15px;
+ }
+
+ &__footer {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-evenly;
+ font-size: 22px;
+ position: relative;
+ flex: 0 0 auto;
+ border-top: 1px solid $geyser;
+
+ &__cancel-button,
+ &__sign-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1 0 auto;
+ font-family: Roboto;
+ font-size: 16px;
+ font-weight: 300;
+ height: 55px;
+ line-height: 32px;
+ cursor: pointer;
+ border-radius: 2px;
+ box-shadow: none;
+ max-width: 162px;
+ margin: 12px;
+ }
+
+ &__cancel-button {
+ background: none;
+ border: 1px solid $dusty-gray;
+ margin-right: 6px;
+ }
+
+ &__sign-button {
+ background-color: $caribbean-green;
+ border-width: 0;
+ color: $white;
+ margin-left: 6px;
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/app/css/index.css b/ui/app/css/itcss/components/sections.scss
index 0630c4c12..388aea175 100644
--- a/ui/app/css/index.css
+++ b/ui/app/css/itcss/components/sections.scss
@@ -1,222 +1,26 @@
-/*
-faint orange (textfield shades) #FAF6F0
-light orange (button shades): #F5C26D
-dark orange (text): #F5A623
-borders/font/any gray: #4A4A4A
-*/
-
-/*
-application specific styles
-*/
-
-* {
- box-sizing: border-box;
-}
-
-html, body {
- font-family: 'Montserrat Regular', Arial;
- color: #4D4D4D;
- font-weight: 300;
- line-height: 1.4em;
- background: #F7F7F7;
- margin: 0;
- padding: 0;
-}
-
-html {
- min-height: 500px;
-}
-
-.app-root {
- overflow: hidden;
- position: relative
-}
-
-.app-primary {
- display: flex;
-}
-
-input:focus, textarea:focus {
- outline: none;
-}
-
-.full-size {
- height: 100%;
- width: 100%;
-}
-
-.full-width {
- width: 100%;
-}
+// Old scss, do not lint - clean up later
+/* stylelint-disable */
-.full-height {
- height: 100%;
-}
-
-.full-flex-height {
- display: flex;
- flex: 1 1 auto;
- flex-direction: column;
-}
-
-#app-content {
- overflow-x: hidden;
- min-width: 357px;
- height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-button, input[type="submit"] {
- font-family: 'Montserrat Bold';
- outline: none;
- cursor: pointer;
- padding: 8px 12px;
- border: none;
- color: white;
- transform-origin: center center;
- transition: transform 50ms ease-in;
- /* default orange */
- background: rgba(247, 134, 28, 1);
- box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36);
-}
-
-.btn-green, input[type="submit"].btn-green {
- background: rgba(106, 195, 96, 1);
- box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36);
-}
-
-.btn-red {
- background: rgba(254, 35, 17, 1);
- box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36);
-}
-
-button[disabled], input[type="submit"][disabled] {
- cursor: not-allowed;
- background: rgba(197, 197, 197, 1);
- box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36);
-}
-
-button.spaced {
- margin: 2px;
-}
-
-button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover {
- transform: scale(1.1);
-}
-button:not([disabled]):active, input[type="submit"]:not([disabled]):active {
- transform: scale(0.95);
-}
-
-a {
- text-decoration: none;
- color: inherit;
-}
-
-a:hover{
- color: #df6b0e;
-}
/*
-app
+App Sections
+ TODO: Move into separate files.
*/
-.active {
- color: #909090;
-}
-
-button.primary {
- padding: 8px 12px;
- background: #F7861C;
- box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36);
- color: white;
- font-size: 1.1em;
- font-family: 'Montserrat Regular';
- text-transform: uppercase;
-}
-
-button.btn-thin {
- border: 1px solid;
- border-color: #4D4D4D;
- color: #4D4D4D;
- background: rgb(255, 174, 41);
- border-radius: 4px;
- min-width: 200px;
- margin: 12px 0;
- padding: 6px;
- font-size: 13px;
-}
-
-.app-header {
- padding: 6px 8px;
-}
-
-.app-header h1 {
- font-family: 'Montserrat Regular';
- text-transform: uppercase;
- color: #AEAEAE;
-}
-
-h2.page-subtitle {
- font-family: 'Montserrat Regular';
- text-transform: uppercase;
- color: #AEAEAE;
- font-size: 1em;
- margin: 12px;
-}
-
-.app-footer {
- padding-bottom: 10px;
- align-items: center;
-}
-
-.identicon {
- height: 46px;
- width: 46px;
- background-size: cover;
- border-radius: 100%;
- border: 3px solid gray;
-}
-
+/* initialize */
textarea.twelve-word-phrase {
padding: 12px;
width: 300px;
height: 140px;
font-size: 16px;
- background: white;
+ background: $white;
resize: none;
}
-.network-indicator {
- display: flex;
- align-items: center;
- font-size: 0.6em;
-
-}
-
-.network-name {
- width: 5.2em;
- line-height: 9px;
- text-rendering: geometricPrecision;
-}
-
-.check {
- margin-left: 12px;
- color: #F7861C;
- flex: 1 0 auto;
- display: flex;
- justify-content: flex-end;
-}
-/*
-app sections
-*/
-
-/* initialize */
-
.initialize-screen hr {
width: 60px;
margin: 12px;
- border-color: #F7861C;
+ border-color: #f7861c;
border-style: solid;
}
@@ -235,12 +39,13 @@ app sections
/* unlock */
.error {
+ // color: #e20202;
color: #f7861c;
margin-bottom: 9px;
}
.warning {
- color: #FFAE00;
+ color: #ffae00;
}
.lock {
@@ -250,9 +55,10 @@ app sections
.lock.locked {
transform: scale(1.5);
- opacity: 0.0;
+ opacity: 0;
transition: opacity 400ms ease-in, transform 400ms ease-in;
}
+
.lock.unlocked {
transform: scale(1);
opacity: 1;
@@ -263,15 +69,18 @@ app sections
transform: scaleX(1) translateX(0);
transition: transform 250ms ease-in;
}
+
.lock.unlocked .lock-top {
transform: scaleX(-1) translateX(-12px);
transition: transform 250ms ease-in;
}
+
.lock.unlocked:hover {
border-radius: 4px;
background: #e5e5e5;
border: 1px solid #b1b1b1;
}
+
.lock.unlocked:active {
background: #c3c3c3;
}
@@ -291,55 +100,46 @@ app sections
.unlock-screen input[type=password] {
width: 260px;
- /*height: 36px;
- margin-bottom: 24px;
- padding: 8px;*/
}
-.sizing-input{
+.sizing-input {
font-size: 14px;
height: 30px;
padding-left: 5px;
}
-.editable-label{
+
+.editable-label {
display: flex;
}
+
/* Webkit */
+
.unlock-screen input::-webkit-input-placeholder {
text-align: center;
font-size: 1.2em;
}
+
/* Firefox 18- */
+
.unlock-screen input:-moz-placeholder {
text-align: center;
font-size: 1.2em;
}
+
/* Firefox 19+ */
+
.unlock-screen input::-moz-placeholder {
text-align: center;
font-size: 1.2em;
}
+
/* IE */
+
.unlock-screen input:-ms-input-placeholder {
text-align: center;
font-size: 1.2em;
}
-input.large-input, textarea.large-input {
- /*margin-bottom: 24px;*/
- padding: 8px;
-}
-
-input.large-input {
- height: 36px;
-}
-
-.letter-spacey {
- letter-spacing: 0.1em;
-}
-
-
-
/* accounts */
.accounts-section {
@@ -347,7 +147,7 @@ input.large-input {
}
.accounts-section .horizontal-line {
- margin: 0px 18px;
+ margin: 0 18px;
}
.accounts-list-option {
@@ -364,7 +164,7 @@ input.large-input {
}
.unconftx-link .fa-arrow-right {
- margin: 0px -8px 0px 8px;
+ margin: 0 -8px 0px 8px;
}
/* identity panel */
@@ -391,7 +191,7 @@ input.large-input {
.identity-panel i {
margin-top: 32px;
margin-right: 6px;
- color: #B9B9B9;
+ color: #b9b9b9;
}
.identity-panel .arrow-right {
@@ -402,34 +202,33 @@ input.large-input {
}
.identity-copy.flex-column {
- flex: 0.25 0 auto;
+ flex: .25 0 auto;
justify-content: center;
}
/* accounts screen */
.identity-section {
-
}
.identity-section .identity-panel {
- background: #E9E9E9;
- border-bottom: 1px solid #B1B1B1;
+ background: #e9e9e9;
+ border-bottom: 1px solid #b1b1b1;
cursor: pointer;
}
.identity-section .identity-panel.selected {
- background: white;
- color: #F3C83E;
+ background: $white;
+ color: #f3c83e;
}
.identity-section .identity-panel.selected .identicon {
- border-color: orange;
+ border-color: $orange;
}
.identity-section .accounts-list-option:hover,
.identity-section .accounts-list-option.selected {
- background:white;
+ background: $white;
}
/* account detail screen */
@@ -445,15 +244,14 @@ input.large-input {
flex-grow: 10;
}
-.name-label{
-
+.name-label {
}
.unapproved-tx-icon {
height: 16px;
width: 16px;
background: rgb(47, 174, 244);
- border-color: #AEAEAE;
+ border-color: $silver-chalice;
border-radius: 13px;
}
@@ -461,6 +259,7 @@ input.large-input {
height: 100%;
visibility: hidden;
}
+
.editing-label {
display: flex;
justify-content: flex-start;
@@ -468,8 +267,9 @@ input.large-input {
margin-bottom: 2px;
font-size: 11px;
text-rendering: geometricPrecision;
- color: #F7861C;
+ color: #f7861c;
}
+
.name-label:hover .edit-text {
visibility: visible;
}
@@ -481,48 +281,33 @@ input.large-input {
margin: 12px;
margin-bottom: 24px;
border-radius: 4px;
- border: 2px solid #F3C83E;
- background: #FAF6F0;
-}
-
-/* Send Screen */
-
-.send-screen {
-
-}
-
-.send-screen section {
- margin: 8px 16px;
-}
-
-.send-screen input {
- width: 100%;
- font-size: 12px;
+ border: 2px solid #f3c83e;
+ background: #faf6f0;
}
/* Ether Balance Widget */
.ether-balance-amount {
- color: #F7861C;
+ color: #f7861c;
}
.ether-balance-label {
- color: #ABA9AA;
+ color: #aba9aa;
}
/* Info screen */
-.info-gray{
- font-family: 'Montserrat Regular';
+.info-gray {
+ font-family: Roboto;
text-transform: uppercase;
- color: #AEAEAE;
+ color: $silver-chalice;
}
-.icon-size{
+.icon-size {
width: 20px;
}
-.info{
- font-family: 'Montserrat Regular', Arial;
+.info {
+ font-family: Roboto, Arial;
padding-bottom: 10px;
display: inline-block;
padding-left: 5px;
@@ -534,7 +319,6 @@ input.large-input {
align-items: center;
}
-
.custom-radio-selected {
width: 17px;
height: 17px;
@@ -543,7 +327,7 @@ input.large-input {
border-radius: 15px;
border-width: 5px;
background: rgba(247, 134, 28, 1);
- border-color: #F7F7F7;
+ border-color: #f7f7f7;
}
.custom-radio-inactive {
@@ -552,72 +336,59 @@ input.large-input {
border: solid;
border-width: 1px;
border-radius: 24px;
- border-color: #AEAEAE;
+ border-color: $silver-chalice;
}
.radio-titles {
color: rgba(247, 134, 28, 1);
}
-.radio-titles-subtext {
-
-}
-
-.selected-exchange {
-
-}
-
-.buy-radio {
-
-}
-
-.eth-warning{
+.eth-warning {
transition: opacity 400ms ease-in, transform 400ms ease-in;
}
-.buy-subview{
+.buy-subview {
transition: opacity 400ms ease-in, transform 400ms ease-in;
}
-.input-container:hover .edit-text{
+.input-container:hover .edit-text {
visibility: visible;
}
-.buy-inputs{
- font-family: 'Montserrat Light';
+.buy-inputs {
+ font-family: Roboto;
font-size: 13px;
height: 20px;
background: transparent;
box-sizing: border-box;
border: solid;
border-color: transparent;
- border-width: 0.5px;
+ border-width: .5px;
border-radius: 2px;
-
}
-.input-container:hover .buy-inputs{
+
+.input-container:hover .buy-inputs {
box-sizing: inherit;
border: solid;
- border-color: #F7861C;
- border-width: 0.5px;
+ border-color: #f7861c;
+ border-width: .5px;
border-radius: 2px;
}
-.buy-inputs:focus{
+.buy-inputs:focus {
border: solid;
- border-color: #F7861C;
- border-width: 0.5px;
+ border-color: #f7861c;
+ border-width: .5px;
border-radius: 2px;
}
.activeForm {
- background: #F7F7F7;
+ background: #f7f7f7;
border: none;
border-radius: 8px 8px 0px 0px;
width: 50%;
text-align: center;
padding-bottom: 4px;
-
}
.inactiveForm {
@@ -629,19 +400,19 @@ input.large-input {
}
.ex-coins {
- font-family: 'Montserrat Regular';
+ font-family: Roboto;
text-transform: uppercase;
text-align: center;
font-size: 33px;
width: 118px;
height: 42px;
padding: 1px;
- color: #4D4D4D;
+ color: #4d4d4d;
}
-.marketinfo{
- font-family: 'Montserrat light';
- color: #AEAEAE;
+.marketinfo {
+ font-family: Roboto;
+ color: $silver-chalice;
font-size: 15px;
line-height: 17px;
}
@@ -656,52 +427,50 @@ input.large-input {
overflow: scroll;
}
-.icon-control .fa-refresh{
+.icon-control .fa-refresh {
visibility: hidden;
}
-.icon-control:hover .fa-refresh{
+.icon-control:hover .fa-refresh {
visibility: visible;
}
-.icon-control:hover .fa-chevron-right{
+.icon-control:hover .fa-chevron-right {
visibility: hidden;
}
.inactive {
- color: #AEAEAE;
+ color: $silver-chalice;
}
-.inactive button{
- background: #AEAEAE;
- color: white;
+.inactive button {
+ background: $silver-chalice;
+ color: $white;
}
-.ellip-address {
+.qr-ellip-address, .ellip-address {
overflow: hidden;
text-overflow: ellipsis;
- width: 5em;
- font-size: 14px;
- font-family: "Montserrat Light";
- margin-left: 5px;
}
.qr-header {
- font-size: 25px;
- margin-top: 40px;
+ font-size: 25px;
+ margin-top: 40px;
}
.qr-message {
font-size: 12px;
- color: #F7861C;
+ color: #f7861c;
}
div.message-container > div:first-child {
margin-top: 18px;
font-size: 15px;
- color: #4D4D4D;
+ color: #4d4d4d;
}
.pop-hover:hover {
- transform: scale(1.1);
+ transform: scale(1.1);
}
+
+/* stylelint-enable */
diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss
new file mode 100644
index 000000000..2bd192788
--- /dev/null
+++ b/ui/app/css/itcss/components/send.scss
@@ -0,0 +1,892 @@
+.send-screen-wrapper {
+ display: flex;
+ flex-flow: column nowrap;
+ z-index: 25;
+ font-family: Roboto;
+
+ @media screen and (max-width: $break-small) {
+ width: 100%;
+ overflow-y: auto;
+ }
+
+ section {
+ flex: 0 0 auto;
+ }
+}
+
+.send-screen-card {
+ background-color: #fff;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08);
+ padding: 46px 40.5px 26px;
+ position: relative;
+ // top: -26px;
+ align-items: center;
+ display: flex;
+ flex-flow: column nowrap;
+ width: 498px;
+ flex: 1 0 auto;
+
+ @media screen and (max-width: $break-small) {
+ top: 0;
+ width: 100%;
+ box-shadow: none;
+ padding: 12px;
+ }
+}
+
+/* Send Screen */
+
+.send-screen section {
+ margin: 4px 16px;
+}
+
+.send-screen input {
+ width: 100%;
+ font-size: 12px;
+}
+
+.send-eth-icon {
+ border-radius: 50%;
+ width: 70px;
+ height: 70px;
+ border: 1px solid $alto;
+ box-shadow: 0 0 4px 0 rgba(0, 0, 0, .2);
+ position: absolute;
+ top: -35px;
+ z-index: 25;
+ padding: 4px;
+ background-color: $white;
+
+ @media screen and (max-width: $break-small) {
+ position: relative;
+ top: 0;
+ }
+}
+
+.send-screen-input-wrapper {
+ width: 95%;
+ position: relative;
+
+ .fa-bolt {
+ padding-right: 4px;
+ }
+
+ .large-input {
+ border: 1px solid $dusty-gray;
+ border-radius: 4px;
+ margin: 4px 0 20px;
+ font-size: 16px;
+ line-height: 22.4px;
+ font-family: Roboto;
+ }
+
+ .send-screen-gas-input {
+ border: 1px solid transparent;
+ }
+
+ &__error-message {
+ display: none;
+ }
+
+ &--error {
+ input,
+ .send-screen-gas-input {
+ border-color: $red !important;
+ }
+
+ .send-screen-input-wrapper__error-message {
+ display: block;
+ position: absolute;
+ bottom: 4px;
+ font-size: 12px;
+ line-height: 12px;
+ left: 8px;
+ color: $red;
+ }
+ }
+
+ .send-screen-input-wrapper__error-message {
+ display: block;
+ position: absolute;
+ bottom: 4px;
+ font-size: 12px;
+ line-height: 12px;
+ left: 8px;
+ color: $red;
+ }
+}
+
+.send-screen-input {
+ width: 100%;
+}
+
+.send-screen-gas-input {
+ width: 100%;
+ height: 41px;
+ border-radius: 3px;
+ background-color: #f3f3f3;
+ border-width: 0;
+ border-style: none;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-left: 10px;
+ padding-right: 12px;
+ font-size: 16px;
+ color: $scorpion;
+}
+
+.send-screen-amount-labels {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.send-screen-gas-labels {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.currency-toggle {
+ &__item {
+ color: $curious-blue;
+ cursor: pointer;
+
+ &--selected {
+ color: $black;
+ cursor: default;
+ }
+ }
+}
+
+.send-screen-gas-input-customize {
+ color: $curious-blue;
+ font-size: 12px;
+ cursor: pointer;
+}
+
+.gas-tooltip-close-area {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1000;
+ width: 100%;
+ height: 100%;
+}
+
+.customize-gas-tooltip-container {
+ position: absolute;
+ bottom: 50px;
+ width: 237px;
+ height: 307px;
+ background-color: $white;
+ opacity: 1;
+ box-shadow: $alto 0 0 5px;
+ z-index: 1050;
+ padding: 13px 19px;
+ font-size: 16px;
+ border-radius: 4px;
+ font-family: "Lato";
+ font-weight: 500;
+}
+
+.gas-tooltip-arrow {
+ height: 25px;
+ width: 25px;
+ z-index: 1200;
+ background: $white;
+ position: absolute;
+ transform: rotate(45deg);
+ left: 107px;
+ top: 294px;
+ box-shadow: 2px 2px 2px $alto;
+}
+
+.customize-gas-tooltip-container input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+}
+
+.customize-gas-tooltip-container input[type="number"]:hover::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+}
+
+.customize-gas-tooltip {
+ position: relative;
+}
+
+.gas-tooltip {
+ display: flex;
+ justify-content: center;
+}
+
+.gas-tooltip-label {
+ font-size: 16px;
+ color: $tundora;
+}
+
+.gas-tooltip-header {
+ padding-bottom: 12px;
+}
+
+.gas-tooltip-input-label {
+ margin-bottom: 5px;
+}
+
+.gas-tooltip-input-label i {
+ color: $silver-chalice;
+ margin-left: 6px;
+}
+
+.customize-gas-input {
+ width: 178px;
+ height: 28px;
+ border: 1px solid $alto;
+ font-size: 16px;
+ color: $nile-blue;
+ padding-left: 8px;
+}
+
+.customize-gas-input-wrapper {
+ position: relative;
+}
+
+.gas-tooltip-input-detail {
+ position: absolute;
+ top: 4px;
+ right: 26px;
+ font-size: 12px;
+ color: $silver-chalice;
+}
+
+.gas-tooltip-input-arrows {
+ position: absolute;
+ top: 0;
+ right: 4px;
+ width: 17px;
+ height: 28px;
+ border: 1px solid #dadada;
+ border-left: 0;
+ display: flex;
+ flex-direction: column;
+ color: #9b9b9b;
+ font-size: .8em;
+ padding: 1px 4px;
+ cursor: pointer;
+}
+
+.token-gas {
+ &__amount {
+ display: inline-block;
+ margin-right: 4px;
+ }
+
+ &__symbol {
+ display: inline-block;
+ }
+}
+
+.send-screen {
+ &__title {
+ color: $scorpion;
+ font-size: 18px;
+ line-height: 29px;
+ }
+
+ &__subtitle {
+ margin: 10px 0 20px;
+ font-size: 14px;
+ line-height: 24px;
+ }
+
+ &__send-button,
+ &__cancel-button {
+ width: 163px;
+ text-align: center;
+ }
+
+ &__send-button__disabled {
+ opacity: .5;
+ cursor: auto;
+ }
+}
+
+.send-token {
+ display: flex;
+ flex-flow: column nowrap;
+ z-index: 25;
+ font-family: Roboto;
+
+ &__content {
+ width: 498px;
+ height: 605px;
+ background-color: #fff;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08);
+ padding: 46px 40.5px 26px;
+ position: relative;
+ // top: -26px;
+ align-items: center;
+ display: flex;
+ flex-flow: column nowrap;
+ flex: 1 0 auto;
+
+ @media screen and (max-width: $break-small) {
+ top: 0;
+ width: 100%;
+ box-shadow: none;
+ padding: 12px;
+ }
+ }
+
+ .identicon {
+ position: absolute;
+ top: -35px;
+ z-index: 25;
+
+ @media screen and (max-width: $break-small) {
+ position: relative;
+ top: 0;
+ flex: 0 0 auto;
+ }
+ }
+
+ &__title {
+ color: $scorpion;
+ font-size: 18px;
+ line-height: 29px;
+ }
+
+ &__description,
+ &__balance-text,
+ &__token-symbol {
+ margin-top: 10px;
+ font-size: 14px;
+ line-height: 24px;
+ text-align: center;
+ }
+
+ &__token-balance {
+ font-size: 40px;
+ line-height: 40px;
+ margin-top: 13px;
+
+ .token-balance__amount {
+ padding-right: 12px;
+ }
+ }
+
+ &__button-group {
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ flex: 0 0 auto;
+
+ @media screen and (max-width: $break-small) {
+ margin-top: 24px;
+ }
+
+ button {
+ width: 163px;
+ }
+ }
+}
+
+.confirm-send-token {
+ &__hero-amount-wrapper {
+ width: 100%;
+ }
+}
+
+.send-v2 {
+ &__container {
+ // height: 701px;
+ width: 380px;
+ border-radius: 8px;
+ background-color: $white;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08);
+ display: flex;
+ flex-flow: column nowrap;
+ z-index: 25;
+ align-items: center;
+ font-family: Roboto;
+ position: relative;
+
+ @media screen and (max-width: $break-small) {
+ width: 100%;
+ top: 0;
+ box-shadow: none;
+ flex: 1 1 auto;
+ }
+ }
+
+ &__send-header-icon-container {
+ z-index: 25;
+
+ @media screen and (max-width: $break-small) {
+ position: relative;
+ top: 0;
+ }
+ }
+
+ &__send-header-icon {
+ border-radius: 50%;
+ width: 48px;
+ height: 48px;
+ border: 1px solid $alto;
+ z-index: 25;
+ padding: 4px;
+ background-color: $white;
+ }
+
+ &__send-arrow-icon {
+ color: #f28930;
+ transform: rotate(-45deg);
+ position: absolute;
+ top: -2px;
+ left: 0;
+ font-size: 1.12em;
+ }
+
+ &__arrow-background {
+ background-color: $white;
+ height: 14px;
+ width: 14px;
+ position: absolute;
+ top: 52px;
+ left: 199px;
+ border-radius: 50%;
+ z-index: 100;
+
+ @media screen and (max-width: $break-small) {
+ top: 36px;
+ }
+ }
+
+ &__header {
+ height: 88px;
+ width: 380px;
+ background-color: $athens-grey;
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ @media screen and (max-width: $break-small) {
+ height: 59px;
+ width: 100vw;
+ }
+ }
+
+ &__header-tip {
+ height: 25px;
+ width: 25px;
+ background: $athens-grey;
+ position: absolute;
+ transform: rotate(45deg);
+ left: 178px;
+ top: 75px;
+
+ @media screen and (max-width: $break-small) {
+ top: 46px;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+ }
+ }
+
+ &__title {
+ color: $scorpion;
+ font-size: 22px;
+ line-height: 29px;
+ text-align: center;
+ margin-top: 25px;
+ }
+
+ &__copy {
+ color: $gray;
+ font-size: 14px;
+ font-weight: 300;
+ line-height: 19px;
+ text-align: center;
+ margin-top: 10px;
+ width: 287px;
+ }
+
+ &__error {
+ font-size: 12px;
+ line-height: 12px;
+ left: 8px;
+ color: $red;
+ }
+
+ &__error-border {
+ color: $red;
+ }
+
+ &__form {
+ margin: 13px 0;
+ width: 100%;
+
+ @media screen and (max-width: $break-small) {
+ padding: 13px 0;
+ margin: 0;
+ height: 0;
+ overflow-y: auto;
+ flex: 1 1 auto;
+ }
+ }
+
+ &__form-header,
+ &__form-header-copy {
+ width: 100%;
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ }
+
+ &__form-row {
+ margin: 14.5px 18px 0px;
+ position: relative;
+ display: flex;
+ flex-flow: row;
+ flex: 1 0 auto;
+ justify-content: space-between;
+ }
+
+ &__form-field {
+ flex: 1 1 auto;
+ }
+
+ &__form-label {
+ color: $scorpion;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 22px;
+ width: 88px;
+ }
+
+ &__from-dropdown {
+ height: 73px;
+ width: 100%;
+ border: 1px solid $alto;
+ border-radius: 4px;
+ background-color: $white;
+ font-family: Roboto;
+ line-height: 16px;
+ font-size: 12px;
+ color: $tundora;
+ position: relative;
+
+ &__close-area {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1000;
+ width: 100%;
+ height: 100%;
+ }
+
+ &__list {
+ z-index: 1050;
+ position: absolute;
+ height: 220px;
+ width: 100%;
+ border: 1px solid $geyser;
+ border-radius: 4px;
+ background-color: $white;
+ box-shadow: 0 3px 6px 0 rgba(0 ,0 ,0 ,.11);
+ margin-top: 11px;
+ margin-left: -1px;
+ overflow-y: scroll;
+ }
+ }
+
+ &__to-autocomplete {
+ position: relative;
+
+ &__down-caret {
+ position: absolute;
+ top: 18px;
+ right: 12px;
+ }
+ }
+
+ &__to-autocomplete, &__memo-text-area {
+ &__input {
+ height: 54px;
+ width: 100%;
+ border: 1px solid $alto;
+ border-radius: 4px;
+ background-color: $white;
+ color: $dusty-gray;
+ padding: 10px;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 21px;
+ font-weight: 300;
+ }
+ }
+
+ &__amount-max {
+ color: $curious-blue;
+ font-family: Roboto;
+ font-size: 12px;
+ left: 8px;
+ border: none;
+ cursor: pointer;
+ }
+
+ &__gas-fee-display {
+ width: 100%;
+ }
+
+ &__sliders-icon-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 24px;
+ width: 24px;
+ border: 1px solid $curious-blue;
+ border-radius: 4px;
+ background-color: $white;
+ padding: 5px;
+ position: absolute;
+ right: 15px;
+ top: 14px;
+ cursor: pointer;
+ }
+
+ &__sliders-icon {
+ color: $curious-blue;
+ }
+
+ &__memo-text-area {
+ &__input {
+ padding: 6px 10px;
+ }
+ }
+
+ &__footer {
+ height: 92px;
+ width: 100%;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ border-top: 1px solid $alto;
+ background: $white;
+ padding: 0 12px;
+ }
+
+ &__next-btn,
+ &__cancel-btn,
+ &__next-btn__disabled {
+ width: 163px;
+ text-align: center;
+ height: 55px;
+ border-radius: 2px;
+ background-color: $white;
+ font-family: Roboto;
+ font-size: 16px;
+ font-weight: 300;
+ line-height: 21px;
+ border: 1px solid;
+ margin: 0 4px;
+ }
+
+ &__next-btn,
+ &__next-btn__disabled {
+ color: $curious-blue;
+ border-color: $curious-blue;
+ }
+
+ &__next-btn__disabled {
+ opacity: .5;
+ cursor: auto;
+ }
+
+ &__cancel-btn {
+ color: $dusty-gray;
+ border-color: $dusty-gray;
+ }
+
+ &__customize-gas {
+ border: 1px solid #D8D8D8;
+ border-radius: 4px;
+ background-color: #FFFFFF;
+ box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14);
+ font-family: Roboto;
+ display: flex;
+ flex-flow: column;
+
+ @media screen and (max-width: $break-small) {
+ width: 100vw;
+ height: 100vh;
+ }
+
+ &__header {
+ height: 52px;
+ border-bottom: 1px solid $alto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 22px;
+
+ @media screen and (max-width: $break-small) {
+ flex: 0 0 auto;
+ }
+ }
+
+ &__title {
+ margin-left: 19.25px;
+ }
+
+ &__close::after {
+ content: '\00D7';
+ font-size: 1.8em;
+ color: $dusty-gray;
+ font-family: sans-serif;
+ cursor: pointer;
+ margin-right: 19.25px;
+ }
+
+ &__content {
+ display: flex;
+ flex-flow: column nowrap;
+ height: 100%;
+ }
+
+ &__body {
+ display: flex;
+ margin-bottom: 24px;
+
+ @media screen and (max-width: $break-small) {
+ flex-flow: column;
+ flex: 1 1 auto;
+ }
+ }
+
+ &__footer {
+ height: 75px;
+ border-top: 1px solid $alto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 22px;
+ position: relative;
+
+ @media screen and (max-width: $break-small) {
+ flex: 0 0 auto;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ justify-content: space-between;
+ width: 181.75px;
+ margin-right: 21.25px;
+ }
+
+ &__revert, &__cancel, &__save, &__save__error {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ }
+
+ &__revert {
+ color: $silver-chalice;
+ font-size: 16px;
+ margin-left: 21.25px;
+ }
+
+ &__cancel, &__save, &__save__error {
+ height: 34.64px;
+ width: 85.74px;
+ border: 1px solid $dusty-gray;
+ border-radius: 2px;
+ font-family: 'DIN OT';
+ font-size: 12px;
+ color: $dusty-gray;
+ }
+
+ &__save__error {
+ opacity: 0.5;
+ cursor: auto;
+ }
+
+ &__error-message {
+ display: block;
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ font-size: 12px;
+ line-height: 12px;
+ color: $red;
+ }
+ }
+
+ &__gas-modal-card {
+ width: 360px;
+ display: flex;
+ flex-flow: column;
+ align-items: flex-start;
+ padding-left: 20px;
+
+ &__title {
+ height: 26px;
+ color: $tundora;
+ font-family: Roboto;
+ font-size: 20px;
+ font-weight: 300;
+ line-height: 26px;
+ margin-top: 17px;
+ }
+
+ &__copy {
+ height: 38px;
+ width: 314px;
+ color: $tundora;
+ font-family: Roboto;
+ font-size: 14px;
+ line-height: 19px;
+ margin-top: 17px;
+ }
+
+ .customize-gas-input-wrapper {
+ margin-top: 17px;
+ }
+
+ .customize-gas-input {
+ height: 54px;
+ width: 315px;
+ border: 1px solid $geyser;
+ background-color: $white;
+ padding-left: 15px;
+ }
+
+ .gas-tooltip-input-arrows {
+ width: 32px;
+ height: 54px;
+ border-left: 1px solid #dadada;
+ font-size: 18px;
+ color: $tundora;
+ right: 0px;
+ padding: 1px 4px;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ }
+
+ input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+
+ input[type="number"]:hover::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+ }
+}
diff --git a/ui/app/css/itcss/components/settings.scss b/ui/app/css/itcss/components/settings.scss
new file mode 100644
index 000000000..2f29d8017
--- /dev/null
+++ b/ui/app/css/itcss/components/settings.scss
@@ -0,0 +1,201 @@
+.settings {
+ position: relative;
+ background: $white;
+ display: flex;
+ flex-flow: column nowrap;
+ height: auto;
+ overflow: auto;
+}
+
+.settings__header {
+ padding: 25px;
+}
+
+.settings__close-button::after {
+ content: '\00D7';
+ font-size: 40px;
+ color: $dusty-gray;
+ position: absolute;
+ top: 25px;
+ right: 30px;
+ cursor: pointer;
+}
+
+.settings__error {
+ padding-bottom: 20px;
+ text-align: center;
+ color: $crimson;
+}
+
+.settings__content {
+ padding: 0 25px;
+}
+
+.settings__content-row {
+ display: flex;
+ flex-direction: row;
+ padding: 10px 0 20px;
+
+ @media screen and (max-width: 575px) {
+ flex-direction: column;
+ padding: 10px 0;
+ }
+}
+
+.settings__content-item {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ padding: 0 5px;
+ height: 71px;
+
+ @media screen and (max-width: 575px) {
+ height: initial;
+ padding: 5px 0;
+ }
+
+ &--without-height {
+ height: initial;
+ }
+}
+
+.settings__content-item-col {
+ max-width: 300px;
+ display: flex;
+ flex-direction: column;
+
+ @media screen and (max-width: 575px) {
+ max-width: 100%;
+ width: 100%;
+ }
+}
+
+.settings__content-description {
+ font-size: 14px;
+ color: $dusty-gray;
+ padding-top: 5px;
+}
+
+.settings__input {
+ padding-left: 10px;
+ font-size: 14px;
+ height: 40px;
+ border: 1px solid $alto;
+}
+
+.settings__input::-webkit-input-placeholder {
+ font-weight: 100;
+ color: $dusty-gray;
+}
+
+.settings__input::-moz-placeholder {
+ font-weight: 100;
+ color: $dusty-gray;
+}
+
+.settings__input:-ms-input-placeholder {
+ font-weight: 100;
+ color: $dusty-gray;
+}
+
+.settings__input:-moz-placeholder {
+ font-weight: 100;
+ color: $dusty-gray;
+}
+
+.settings__provider-wrapper {
+ font-size: 16px;
+ border: 1px solid $alto;
+ border-radius: 2px;
+ padding: 15px;
+ background-color: $white;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.settings__provider-icon {
+ height: 10px;
+ width: 10px;
+ margin-right: 10px;
+ border-radius: 10px;
+}
+
+.settings__rpc-save-button {
+ align-self: flex-end;
+ padding: 5px;
+ text-transform: uppercase;
+ color: $dusty-gray;
+ cursor: pointer;
+}
+
+.settings__clear-button {
+ font-size: 16px;
+ border: 1px solid $curious-blue;
+ color: $curious-blue;
+ border-radius: 2px;
+ padding: 18px;
+ background-color: $white;
+ text-transform: uppercase;
+}
+
+.settings__clear-button--red {
+ border: 1px solid $monzo;
+ color: $monzo;
+}
+
+.settings__info-logo-wrapper {
+ height: 80px;
+ margin-bottom: 20px;
+}
+
+.settings__info-logo {
+ max-height: 100%;
+ max-width: 100%;
+}
+
+.settings__info-item {
+ padding: 10px 0;
+}
+
+.settings__info-link-header {
+ padding-bottom: 15px;
+
+ @media screen and (max-width: 575px) {
+ padding-bottom: 5px;
+ }
+}
+
+.settings__info-link-item {
+ padding: 15px 0;
+
+ @media screen and (max-width: 575px) {
+ padding: 5px 0;
+ }
+}
+
+.settings__info-version-number {
+ padding-top: 5px;
+ font-size: 13px;
+ color: $dusty-gray;
+}
+
+.settings__info-about {
+ color: $dusty-gray;
+ margin-bottom: 15px;
+}
+
+.settings__info-link {
+ color: $curious-blue;
+}
+
+.settings__info-separator {
+ margin: 15px 0;
+ width: 80px;
+ border-color: $alto;
+ border: none;
+ height: 1px;
+ background-color: $alto;
+ color: $alto;
+}
diff --git a/ui/app/css/itcss/components/simple-dropdown.scss b/ui/app/css/itcss/components/simple-dropdown.scss
new file mode 100644
index 000000000..a21095a3e
--- /dev/null
+++ b/ui/app/css/itcss/components/simple-dropdown.scss
@@ -0,0 +1,65 @@
+.simple-dropdown {
+ height: 56px;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ border: 1px solid $alto;
+ border-radius: 4px;
+ background-color: $white;
+ font-size: 16px;
+ color: #4d4d4d;
+ cursor: pointer;
+ position: relative;
+}
+
+.simple-dropdown__caret {
+ color: $silver;
+ padding: 0 10px;
+}
+
+.simple-dropdown__selected {
+ flex-grow: 1;
+ padding: 0 15px;
+}
+
+.simple-dropdown__options {
+ z-index: 1050;
+ position: absolute;
+ height: 220px;
+ width: 100%;
+ border: 1px solid #d2d8dd;
+ border-radius: 4px;
+ background-color: #fff;
+ -webkit-box-shadow: 0 3px 6px 0 rgba(0, 0, 0, .11);
+ box-shadow: 0 3px 6px 0 rgba(0, 0, 0, .11);
+ margin-top: 10px;
+ overflow-y: scroll;
+ left: 0;
+ top: 100%;
+}
+
+.simple-dropdown__option {
+ padding: 10px;
+
+ &:hover {
+ background-color: $gallery;
+ }
+}
+
+.simple-dropdown__option--selected {
+ background-color: $alto;
+
+ &:hover {
+ background-color: $alto;
+ cursor: default;
+ }
+}
+
+.simple-dropdown__close-area {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1000;
+ width: 100%;
+ height: 100%;
+}
diff --git a/ui/app/css/itcss/components/tab-bar.scss b/ui/app/css/itcss/components/tab-bar.scss
new file mode 100644
index 000000000..4f3077974
--- /dev/null
+++ b/ui/app/css/itcss/components/tab-bar.scss
@@ -0,0 +1,23 @@
+.tab-bar {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: flex-end;
+}
+
+.tab-bar__tab {
+ min-width: 0;
+ flex: 0 0 auto;
+ padding: 15px 25px;
+ border-bottom: 1px solid $alto;
+ box-sizing: border-box;
+ font-size: 18px;
+}
+
+.tab-bar__tab--active {
+ border-color: $black;
+}
+
+.tab-bar__grow-tab {
+ flex-grow: 1;
+}
diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss
new file mode 100644
index 000000000..d4add71b1
--- /dev/null
+++ b/ui/app/css/itcss/components/token-list.scss
@@ -0,0 +1,101 @@
+$wallet-balance-breakpoint: 890px;
+$wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (max-width: #{$wallet-balance-breakpoint})";
+
+.token-list-item {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ padding: 20px 24px;
+ cursor: pointer;
+ transition: linear 200ms;
+ background-color: rgba($wallet-balance-bg, 0);
+ position: relative;
+
+ &__token-balance {
+ font-size: 130%;
+
+ @media #{$wallet-balance-breakpoint-range} {
+ font-size: 105%;
+ }
+ }
+
+ &__fiat-amount {
+ margin-top: .25%;
+ font-size: 105%;
+ text-transform: uppercase;
+
+ @media #{$wallet-balance-breakpoint-range} {
+ font-size: 95%;
+ }
+ }
+
+ @media #{$wallet-balance-breakpoint-range} {
+ padding: 10% 4%;
+ }
+
+ &--active {
+ background-color: rgba($wallet-balance-bg, 1);
+ }
+
+ &__identicon {
+ margin-right: 15px;
+ border: '1px solid #dedede';
+
+ @media #{$wallet-balance-breakpoint-range} {
+ margin-right: 4%;
+ }
+ }
+
+ &__ellipsis {
+ // position: absolute;
+ // top: 20px;
+ // right: 24px;
+ line-height: 45px;
+ }
+
+ &__balance-wrapper {
+ flex: 1 1 auto;
+ }
+}
+
+.token-menu-dropdown {
+ height: 55px;
+ width: 191px;
+ border-radius: 4px;
+ background-color: rgba(0,0,0,0.82);
+ box-shadow: 0 2px 4px 0 rgba(0,0,0,0.5);
+ position: fixed;
+ margin-top: 20px;
+ margin-left: 105px;
+ z-index: 2000;
+
+ &__close-area {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 2100;
+ width: 100%;
+ height: 100%;
+ cursor: default;
+ }
+
+ &__container {
+ padding: 16px 34px 32px;
+ z-index: 2200;
+ position: relative;
+ }
+
+ &__options {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ &__option {
+ color: $white;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 21px;
+ text-align: center;
+ }
+} \ No newline at end of file
diff --git a/ui/app/css/itcss/components/transaction-list.scss b/ui/app/css/itcss/components/transaction-list.scss
new file mode 100644
index 000000000..a5d508f11
--- /dev/null
+++ b/ui/app/css/itcss/components/transaction-list.scss
@@ -0,0 +1,270 @@
+.tx-list-container {
+ height: 87.5%;
+
+ @media screen and (min-width: $break-large) {
+ overflow-y: scroll;
+ }
+}
+
+.tx-list-header {
+ text-transform: capitalize;
+}
+
+@media screen and (max-width: $break-small) {
+ .tx-list-header-wrapper {
+ margin-top: .2em;
+ margin-bottom: .6em;
+ // TODO: Resolve Layout Conflicst in Wallet View
+ // - This fixes txlist "transactions" title dispay
+ // margin-top: 0.2em;
+ // margin-bottom: 0.6em;
+ justify-content: center;
+ flex: 0 0 auto;
+ }
+
+ .tx-list-header {
+ align-self: center;
+ font-size: 12px;
+ color: $dusty-gray;
+ font-family: Roboto;
+ text-transform: uppercase;
+ }
+}
+
+@media screen and (min-width: $break-large) {
+ .tx-list-header-wrapper {
+ flex: 0 0 55px;
+ }
+
+ .tx-list-header {
+ font-size: 16px;
+ margin: 1.5em 2.37em;
+ }
+
+ .tx-list-container::-webkit-scrollbar {
+ display: none;
+ }
+}
+
+.tx-list-content-divider {
+ height: 1px;
+ background: rgb(231, 231, 231);
+ flex: 0 0 1px;
+
+ @media screen and (max-width: $break-small) {
+ margin: .1em 0;
+ }
+
+ @media screen and (min-width: $break-large) {
+ margin: .1em 2.37em;
+ }
+}
+
+.tx-list-item-wrapper {
+ flex: 1 1 auto;
+ width: 0;
+ align-items: stretch;
+ justify-content: flex-start;
+ display: flex;
+ flex-flow: column nowrap;
+
+ @media screen and (max-width: $break-small) {
+ padding: 0 1.3em .8em;
+ }
+
+ @media screen and (min-width: $break-large) {
+ padding-bottom: 12px;
+ }
+}
+
+.tx-list-clickable {
+ cursor: pointer;
+
+ &:hover {
+ background: rgba($alto, .2);
+ }
+}
+
+.tx-list-pending-item-container {
+ cursor: pointer;
+ opacity: .5;
+}
+
+.tx-list-date-wrapper {
+ flex: 1 1 auto;
+
+ @media screen and (max-width: $break-small) {
+ margin-top: 6px;
+ }
+
+ @media screen and (min-width: $break-large) {
+ margin-top: 12px;
+ }
+}
+
+.tx-list-content-wrapper {
+ align-items: stretch;
+ margin-bottom: 4px;
+ margin-top: 2px;
+ flex: 1 0 auto;
+ width: 100%;
+ display: flex;
+ flex-flow: row nowrap;
+
+ @media screen and (max-width: $break-small) {
+ font-size: 12px;
+
+ .tx-list-status {
+ font-size: 14px !important;
+ }
+
+ .tx-list-account {
+ font-size: 14px !important;
+ }
+
+ .tx-list-value {
+ font-size: 14px;
+ line-height: 18px;
+ }
+
+ .tx-list-fiat-value {
+ font-size: 12px;
+ line-height: 16px;
+ }
+ }
+}
+
+.tx-list-date {
+ color: $dusty-gray;
+ font-size: 12px;
+ font-family: Roboto;
+}
+
+.tx-list-identicon-wrapper {
+ align-self: center;
+ flex: 0 0 auto;
+ margin-right: 16px;
+}
+
+.tx-list-account-and-status-wrapper {
+ display: flex;
+ flex: 1 1 auto;
+ flex-flow: row wrap;
+ width: 0;
+
+ @media screen and (max-width: $break-small) {
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ align-self: center;
+
+ .tx-list-account-wrapper {
+ height: 18px;
+
+ .tx-list-account {
+ line-height: 14px;
+ }
+ }
+ }
+
+ @media screen and (min-width: $break-large) {
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+
+ .tx-list-account-wrapper {
+ flex: 1.3 2 auto;
+ min-width: 153px;
+ }
+
+ .tx-list-status-wrapper {
+ flex: 6 6 auto;
+ }
+ }
+
+ .tx-list-account {
+ font-size: 16px;
+ color: $scorpion;
+ }
+
+ .tx-list-status {
+ color: $dusty-gray;
+ font-size: 16px;
+ text-transform: capitalize;
+ }
+
+ .tx-list-status--rejected,
+ .tx-list-status--failed {
+ color: $monzo;
+ }
+}
+
+.tx-list-item {
+ border-top: 1px solid rgb(231, 231, 231);
+ flex: 0 0 auto;
+ display: flex;
+ flex-flow: row nowrap;
+
+ @media screen and (max-width: $break-small) {
+ // margin: 0 1.3em .95em; !important
+ }
+
+ @media screen and (min-width: $break-large) {
+ margin: 0 2.37em;
+ }
+
+ &:last-of-type {
+ border-bottom: 1px solid rgb(231, 231, 231);
+ margin-bottom: 32px;
+ }
+
+ &__wrapper {
+ align-self: center;
+ flex: 2 2 auto;
+ color: $dusty-gray;
+
+ .tx-list-value {
+ font-size: 16px;
+ text-align: right;
+ }
+
+ .tx-list-value--confirmed {
+ color: $caribbean-green;
+ }
+
+ .tx-list-fiat-value {
+ font-size: 12px;
+ text-align: right;
+ }
+ }
+
+ &--empty {
+ text-align: center;
+ border-bottom: none !important;
+ padding: 16px;
+ }
+}
+
+.tx-list-details-wrapper {
+ overflow: hidden;
+ flex: 0 0 35%;
+}
+
+.tx-list-value {
+ font-size: 16px;
+ text-align: right;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.tx-list-fiat-value {
+ text-align: right;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.tx-list-value--confirmed {
+ color: $caribbean-green;
+}
diff --git a/ui/app/css/itcss/components/wallet-balance.scss b/ui/app/css/itcss/components/wallet-balance.scss
new file mode 100644
index 000000000..64b291b89
--- /dev/null
+++ b/ui/app/css/itcss/components/wallet-balance.scss
@@ -0,0 +1,71 @@
+$wallet-balance-bg: #e7e7e7;
+$wallet-balance-breakpoint: 890px;
+$wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (max-width: #{$wallet-balance-breakpoint})";
+
+.wallet-balance-wrapper {
+ flex: 0 0 auto;
+ transition: linear 200ms;
+ background: rgba($wallet-balance-bg, 0);
+
+ &--active {
+ background: rgba($wallet-balance-bg, 1);
+ }
+}
+
+.wallet-balance {
+ background: inherit;
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ flex: 0 0 auto;
+ cursor: pointer;
+ border-top: 1px solid $wallet-balance-bg;
+
+ .balance-container {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ margin: 20px 24px;
+ flex-direction: row;
+ flex-grow: 3;
+
+ @media #{$wallet-balance-breakpoint-range} {
+ margin: 10% 4%;
+ }
+ }
+
+ .balance-display {
+ margin-left: 15px;
+ justify-content: flex-start;
+ align-items: flex-start;
+
+ .token-amount {
+ font-size: 135%;
+ }
+
+ .fiat-amount {
+ margin-top: .25%;
+ font-size: 105%;
+ }
+
+ @media #{$wallet-balance-breakpoint-range} {
+ margin-left: 4%;
+
+ .token-amount {
+ font-size: 105%;
+ }
+
+ .fiat-amount {
+ font-size: 95%;
+ }
+ }
+ }
+
+ .balance-icon {
+ border-radius: 25px;
+ width: 45px;
+ height: 45px;
+ border: 1px solid $alto;
+ }
+}
diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss
new file mode 100644
index 000000000..9d55324e3
--- /dev/null
+++ b/ui/app/css/itcss/generic/index.scss
@@ -0,0 +1,71 @@
+/*
+ Generic
+ */
+
+@import './reset.scss';
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ font-family: Roboto, Arial;
+ color: #4d4d4d;
+ font-weight: 300;
+ line-height: 1.4em;
+ background: #f7f7f7;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ min-height: 500px;
+}
+
+.app-root {
+ overflow: hidden;
+ position: relative;
+}
+
+.app-primary {
+ display: flex;
+}
+
+input:focus,
+textarea:focus {
+ outline: none;
+}
+
+/* stylelint-disable */
+#app-content {
+ overflow-x: hidden;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ @media screen and (max-width: $break-small) {
+ background-color: $white;
+ }
+}
+/* stylelint-enable */
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+a:hover {
+ color: #df6b0e;
+}
+
+input.large-input,
+textarea.large-input {
+ padding: 8px;
+}
+
+input.large-input {
+ height: 36px;
+}
diff --git a/ui/app/css/itcss/generic/reset.scss b/ui/app/css/itcss/generic/reset.scss
new file mode 100644
index 000000000..e054d533e
--- /dev/null
+++ b/ui/app/css/itcss/generic/reset.scss
@@ -0,0 +1,147 @@
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ License: none (public domain)
+*/
+
+html,
+body,
+div,
+span,
+applet,
+object,
+iframe,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+blockquote,
+pre,
+a,
+abbr,
+acronym,
+address,
+big,
+cite,
+code,
+del,
+dfn,
+em,
+img,
+ins,
+kbd,
+q,
+s,
+samp,
+small,
+strike,
+strong,
+sub,
+sup,
+tt,
+var,
+b,
+u,
+i,
+center,
+dl,
+dt,
+dd,
+ol,
+ul,
+li,
+fieldset,
+form,
+label,
+legend,
+table,
+caption,
+tbody,
+tfoot,
+thead,
+tr,
+th,
+td,
+article,
+aside,
+canvas,
+details,
+embed,
+figure,
+figcaption,
+footer,
+header,
+hgroup,
+menu,
+nav,
+output,
+ruby,
+section,
+summary,
+time,
+mark,
+audio,
+video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ /* stylelint-disable */
+ font: inherit;
+ /* stylelint-enable */
+ vertical-align: baseline;
+}
+
+/* HTML5 display-role reset for older browsers */
+
+/* stylelint-disable */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav,
+section {
+ display: block;
+}
+
+body {
+ line-height: 1;
+}
+
+ol,
+ul {
+ list-style: none;
+}
+
+blockquote,
+q {
+ quotes: none;
+}
+
+blockquote:before,
+blockquote:after,
+q:before,
+q:after {
+ content: '';
+ content: none;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+button {
+ border-style: none;
+ cursor: pointer;
+}
+
+/* stylelint-enable */
diff --git a/ui/app/css/itcss/objects/index.scss b/ui/app/css/itcss/objects/index.scss
new file mode 100644
index 000000000..220775682
--- /dev/null
+++ b/ui/app/css/itcss/objects/index.scss
@@ -0,0 +1 @@
+// Objects
diff --git a/ui/app/css/itcss/settings/index.scss b/ui/app/css/itcss/settings/index.scss
new file mode 100644
index 000000000..58a7ca7b7
--- /dev/null
+++ b/ui/app/css/itcss/settings/index.scss
@@ -0,0 +1,3 @@
+@import './variables.scss';
+
+@import './typography.scss';
diff --git a/ui/app/css/itcss/settings/typography.scss b/ui/app/css/itcss/settings/typography.scss
new file mode 100644
index 000000000..58e2d444e
--- /dev/null
+++ b/ui/app/css/itcss/settings/typography.scss
@@ -0,0 +1,71 @@
+@import url('https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900');
+
+@import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css');
+
+@font-face {
+ font-family: 'Montserrat Regular';
+ src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff');
+ src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype');
+ font-weight: 400;
+ font-style: normal;
+ font-size: 'small';
+}
+
+@font-face {
+ font-family: 'Montserrat Bold';
+ src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff');
+ src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Montserrat Light';
+ src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff');
+ src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Montserrat UltraLight';
+ src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff');
+ src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'DIN OT';
+ src: url('/fonts/DIN_OT/DINOT-2.otf') format('opentype');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'DIN OT Light';
+ src: url('/fonts/DIN_OT/DINOT-2.otf') format('opentype');
+ font-weight: 200;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'DIN NEXT';
+ src: url('/fonts/DIN NEXT/DIN NEXT W01 Regular.otf') format('opentype');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'DIN NEXT Light';
+ src: url('/fonts/DIN NEXT/DIN NEXT W10 Light.otf') format('opentype');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Lato';
+ src: url('/fonts/Lato/Lato-Regular.ttf') format('truetype');
+ font-weight: 400;
+ font-style: normal;
+}
diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss
new file mode 100644
index 000000000..387d14b5f
--- /dev/null
+++ b/ui/app/css/itcss/settings/variables.scss
@@ -0,0 +1,77 @@
+/*
+ Variables
+ */
+
+// Base Colors
+$white: #fff;
+$black: #000;
+$orange: #ffa500;
+$red: #f00;
+$gray: #808080;
+
+/*
+ Colors
+ http://chir.ag/projects/name-that-color
+ */
+$white-linen: #faf6f0; // formerly 'faint orange (textfield shades)'
+$rajah: #f5c26d; // formerly 'light orange (button shades)'
+$buttercup: #f5a623; // formerly 'dark orange (text)'
+$tundora: #4a4a4a; // formerly 'borders/font/any gray'
+$gallery: #efefef;
+$alabaster: #f7f7f7;
+$shark: #22232c;
+$wild-sand: #f6f6f6;
+$white: #fff;
+$dusty-gray: #9b9b9b;
+$alto: #dedede;
+$alabaster: #fafafa;
+$silver-chalice: #aeaeae;
+$curious-blue: #2f9ae0;
+$concrete: #f3f3f3;
+$tundora: #4d4d4d;
+$nile-blue: #1b344d;
+$scorpion: #5d5d5d;
+$silver: #cdcdcd;
+$caribbean-green: #02c9b1;
+$monzo: #d0021b;
+$crimson: #e91550;
+$blue-lagoon: #038789;
+$purple: #690496;
+$tulip-tree: #ebb33f;
+$malibu-blue: #7ac9fd;
+$athens-grey: #e9edf0;
+$jaffa: #f28930;
+$geyser: #d2d8dd;
+
+/*
+ Z-Indicies
+ */
+$dropdown-z-index: 30;
+$token-icon-z-index: 15;
+$container-z-index: 15;
+$header-z-index: 12;
+$mobile-header-z-index: 26;
+$main-container-z-index: 18;
+$send-card-z-index: 20;
+$sidebar-z-index: 26;
+$sidebar-overlay-z-index: 25;
+
+/*
+ Z Indicies - Current
+ app - 11
+ hex/bn as decimal input - 1 - remove?
+ dropdown - 11
+ loading - 10 - higher?
+ mascot - 0 - remove?
+ */
+
+/*
+ Responsive Breakpoints
+ */
+$break-small: 575px;
+$break-midpoint: 780px;
+$break-large: 576px;
+
+
+$primary-font-type: Roboto;
+
diff --git a/ui/app/css/itcss/tools/index.scss b/ui/app/css/itcss/tools/index.scss
new file mode 100644
index 000000000..2236729e8
--- /dev/null
+++ b/ui/app/css/itcss/tools/index.scss
@@ -0,0 +1 @@
+@import './utilities.scss';
diff --git a/ui/app/css/lib.css b/ui/app/css/itcss/tools/utilities.scss
index f3acbee76..ee867640d 100644
--- a/ui/app/css/lib.css
+++ b/ui/app/css/itcss/tools/utilities.scss
@@ -1,19 +1,34 @@
+/*
+ Utility Classes
+ */
+
/* color */
.color-orange {
- color: #F7861C;
+ color: #f7861c; // TODO: move to settings/variables
}
.color-forest {
- color: #0A5448;
+ color: #0a5448; // TODO: move to settings/variables
}
/* lib */
+.full-size {
+ height: 100%;
+ width: 100%;
+}
+
.full-width {
width: 100%;
}
+.full-flex-height {
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+}
+
.full-height {
height: 100%;
}
@@ -118,16 +133,19 @@
.pointer {
cursor: pointer;
}
+
.cursor-pointer {
cursor: pointer;
transform-origin: center center;
transition: transform 50ms ease-in-out;
}
+
.cursor-pointer:hover {
transform: scale(1.1);
}
+
.cursor-pointer:active {
- transform: scale(0.95);
+ transform: scale(.95);
}
.cursor-disabled {
@@ -147,7 +165,7 @@
}
.bold {
- font-weight: bold;
+ font-weight: 700;
}
.text-transform-uppercase {
@@ -172,12 +190,12 @@ hr.horizontal-line {
}
.hover-white:hover {
- background: white;
+ background: $white;
}
.red-dot {
- background: #E91550;
- color: white;
+ background: #e91550;
+ color: $white;
border-radius: 10px;
}
@@ -192,14 +210,14 @@ hr.horizontal-line {
}
.golden-square {
- background: #EBB33F;
+ background: #ebb33f;
}
.pending-dot {
- background: red;
+ background: $red;
left: 14px;
top: 14px;
- color: white;
+ color: $white;
border-radius: 10px;
height: 20px;
min-width: 20px;
@@ -213,19 +231,14 @@ hr.horizontal-line {
.keyring-label {
z-index: 1;
- font-size: 11px;
- background: rgba(255,0,0,0.8);
- color: white;
- bottom: 0px;
- left: -8px;
+ font-size: 8px;
+ line-height: 8px;
+ background: rgba(255, 255, 255, 0.4);
+ color: #fff;
border-radius: 10px;
- height: 20px;
- min-width: 20px;
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: center;
padding: 4px;
+ text-align: center;
+ height: 15px;
}
.ether-balance {
@@ -244,16 +257,13 @@ hr.horizontal-line {
margin: 13px;
}
-i.fa.fa-question-circle.fa-lg.menu-icon {
- font-size: 18px;
-}
-
.ether-icon {
background: rgb(0, 163, 68);
border-radius: 20px;
}
+
.testnet-icon {
- background: #2465E1;
+ background: #2465e1;
}
.drop-menu-item {
@@ -274,33 +284,26 @@ i.fa.fa-question-circle.fa-lg.menu-icon {
.critical-error {
text-align: center;
margin-top: 20px;
- color: red;
+ color: $red;
}
/*
- Hacky breakpoint fix for account + tab sections
- Resolves issue from @frankiebee in
- https://github.com/MetaMask/metamask-extension/pull/1835
- Please remove this when integrating new designs
+ Misc
*/
-@media screen and (min-width: 575px) and (max-width: 800px) {
- .account-data-subsection {
- flex: 0 0 auto !important; // reset flex
- margin-left: 10px !important; // create additional horizontal space
- margin-right: 10px !important;
- width: 40%;
- }
-
- .tabSection {
- flex: 0 0 auto !important;
- margin-left: 10px !important;
- margin-right: 10px !important;
- min-width: 285px;
- width: 49%;
- }
-
- .name-label {
- width: 80%;
- }
+// TODO: move into component-level contextual 'active' state
+.letter-spacey {
+ letter-spacing: .1em;
+}
+
+.active {
+ color: #909090;
+}
+
+.check {
+ margin-left: 7px;
+ color: #f7861c;
+ flex: 1 0 auto;
+ display: flex;
+ justify-content: flex-end;
}
diff --git a/ui/app/css/transitions.css b/ui/app/css/itcss/trumps/index.scss
index 393a944f9..d9a4202a4 100644
--- a/ui/app/css/transitions.css
+++ b/ui/app/css/itcss/trumps/index.scss
@@ -1,3 +1,9 @@
+/*
+ Trumps
+ */
+
+// Transitions
+
/* universal */
.app-primary .main-enter {
position: absolute;
@@ -8,7 +14,7 @@
.app-primary.from-right .main-enter-active,
.app-primary.from-left .main-enter-active {
overflow-x: hidden;
- transform: translateX(0px);
+ transform: translateX(0);
transition: transform 300ms ease-in;
}
@@ -17,18 +23,27 @@
transform: translateX(360px);
transition: transform 300ms ease-in;
}
+
.app-primary.from-right .main-leave-active {
transform: translateX(-360px);
transition: transform 300ms ease-in;
}
+.sidebar.from-left {
+ transform: translateX(-320px);
+ transition: transform 300ms ease-in;
+}
+
/* loader transitions */
-.loader-enter, .loader-leave-active {
- opacity: 0.0;
+.loader-enter,
+.loader-leave-active {
+ opacity: 0;
transition: opacity 150 ease-in;
}
-.loader-enter-active, .loader-leave {
- opacity: 1.0;
+
+.loader-enter-active,
+.loader-leave {
+ opacity: 1;
transition: opacity 150 ease-in;
}
@@ -36,7 +51,22 @@
.app-primary.from-right .main-enter:not(.main-enter-active) {
transform: translateX(360px);
}
+
.app-primary.from-left .main-enter:not(.main-enter-active) {
transform: translateX(-360px);
}
+i.fa.fa-question-circle.fa-lg.menu-icon {
+ font-size: 18px;
+}
+
+// This text is contained inside a div.
+// ID needed to override user agent stylesheet.
+// See components/modal.scss
+
+/* stylelint-disable */
+#buy-modal-content-footer-text {
+ font-family: 'DIN OT';
+ font-size: 16px;
+}
+/* stylelint-enable */
diff --git a/ui/app/css/reset.css b/ui/app/css/reset.css
deleted file mode 100644
index 9ce89e8bc..000000000
--- a/ui/app/css/reset.css
+++ /dev/null
@@ -1,48 +0,0 @@
-/* http://meyerweb.com/eric/tools/css/reset/
- v2.0 | 20110126
- License: none (public domain)
-*/
-
-html, body, div, span, applet, object, iframe,
-h1, h2, h3, h4, h5, h6, p, blockquote, pre,
-a, abbr, acronym, address, big, cite, code,
-del, dfn, em, img, ins, kbd, q, s, samp,
-small, strike, strong, sub, sup, tt, var,
-b, u, i, center,
-dl, dt, dd, ol, ul, li,
-fieldset, form, label, legend,
-table, caption, tbody, tfoot, thead, tr, th, td,
-article, aside, canvas, details, embed,
-figure, figcaption, footer, header, hgroup,
-menu, nav, output, ruby, section, summary,
-time, mark, audio, video {
- margin: 0;
- padding: 0;
- border: 0;
- font-size: 100%;
- font: inherit;
- vertical-align: baseline;
-}
-/* HTML5 display-role reset for older browsers */
-article, aside, details, figcaption, figure,
-footer, header, hgroup, menu, nav, section {
- display: block;
-}
-body {
- line-height: 1;
-}
-ol, ul {
- list-style: none;
-}
-blockquote, q {
- quotes: none;
-}
-blockquote:before, blockquote:after,
-q:before, q:after {
- content: '';
- content: none;
-}
-table {
- border-collapse: collapse;
- border-spacing: 0;
-} \ No newline at end of file
diff --git a/ui/app/main-container.js b/ui/app/main-container.js
new file mode 100644
index 000000000..031f61e84
--- /dev/null
+++ b/ui/app/main-container.js
@@ -0,0 +1,67 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const AccountAndTransactionDetails = require('./account-and-transaction-details')
+const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
+const Settings = require('./settings')
+const UnlockScreen = require('./unlock')
+
+module.exports = MainContainer
+
+inherits(MainContainer, Component)
+function MainContainer () {
+ Component.call(this)
+}
+
+MainContainer.prototype.render = function () {
+ // 3. summarize:
+ // switch statement goes inside MainContainer,
+ // or a method in renderPrimary
+ // - pass resulting h() to MainContainer
+ // - error checking in separate func
+ // - router in separate func
+ let contents = {
+ component: AccountAndTransactionDetails,
+ key: 'account-detail',
+ style: {},
+ }
+
+ if (this.props.isUnlocked === false) {
+ switch (this.props.currentViewName) {
+ case 'restoreVault':
+ log.debug('rendering restore vault screen')
+ contents = {
+ component: HDRestoreVaultScreen,
+ key: 'HDRestoreVaultScreen',
+ }
+ break
+ case 'config':
+ log.debug('rendering config screen from unlock screen.')
+ return h(Settings, {key: 'config'})
+ default:
+ log.debug('rendering locked screen')
+ contents = {
+ component: UnlockScreen,
+ style: {
+ boxShadow: 'none',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ background: '#F7F7F7',
+ // must force 100%, because lock screen is full-width
+ width: '100%',
+ },
+ key: 'locked',
+ }
+ }
+ }
+
+ return h('div.main-container', {
+ style: contents.style,
+ }, [
+ h(contents.component, {
+ key: contents.key,
+ }, []),
+ ])
+}
+
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index 8558d6dca..3a4fb536d 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -14,6 +14,7 @@ function reduceApp (state, action) {
if (selectedAddress) {
name = 'accountDetail'
}
+
if (hasUnconfActions) {
log.debug('pending txs detected, defaulting to conf-tx view.')
name = 'confTx'
@@ -36,6 +37,17 @@ function reduceApp (state, action) {
var appState = extend({
shouldClose: false,
menuOpen: false,
+ modal: {
+ open: false,
+ modalState: {
+ name: null,
+ },
+ previousModalState: {
+ name: null,
+ },
+ },
+ sidebarOpen: false,
+ networkDropdownOpen: false,
currentView: seedWords ? seedConfView : defaultView,
accountDetail: {
subview: 'transactions',
@@ -49,9 +61,50 @@ function reduceApp (state, action) {
}, state.appState)
switch (action.type) {
+ // dropdown methods
+ case actions.NETWORK_DROPDOWN_OPEN:
+ return extend(appState, {
+ networkDropdownOpen: true,
+ })
- // transition methods
+ case actions.NETWORK_DROPDOWN_CLOSE:
+ return extend(appState, {
+ networkDropdownOpen: false,
+ })
+
+ // sidebar methods
+ case actions.SIDEBAR_OPEN:
+ return extend(appState, {
+ sidebarOpen: true,
+ })
+ case actions.SIDEBAR_CLOSE:
+ return extend(appState, {
+ sidebarOpen: false,
+ })
+
+ // modal methods:
+ case actions.MODAL_OPEN:
+ return extend(appState, {
+ modal: Object.assign(
+ state.appState.modal,
+ { open: true },
+ { modalState: action.payload },
+ { previousModalState: appState.modal.modalState},
+ ),
+ })
+
+ case actions.MODAL_CLOSE:
+ return extend(appState, {
+ modal: Object.assign(
+ state.appState.modal,
+ { open: false },
+ { modalState: { name: null } },
+ { previousModalState: appState.modal.modalState},
+ ),
+ })
+
+ // transition methods
case actions.TRANSITION_FORWARD:
return extend(appState, {
transForward: true,
@@ -134,7 +187,7 @@ function reduceApp (state, action) {
transForward: true,
})
- case actions.CREATE_NEW_VAULT_IN_PROGRESS:
+ case actions.CREATE_NEW_VAULT_IN_PROGRESS:
return extend(appState, {
currentView: {
name: 'createVault',
@@ -173,6 +226,16 @@ function reduceApp (state, action) {
warning: null,
})
+ case actions.SHOW_SEND_TOKEN_PAGE:
+ return extend(appState, {
+ currentView: {
+ name: 'sendToken',
+ context: appState.currentView.context,
+ },
+ transForward: true,
+ warning: null,
+ })
+
case actions.SHOW_NEW_KEYCHAIN:
return extend(appState, {
currentView: {
@@ -308,7 +371,7 @@ function reduceApp (state, action) {
return extend(appState, {
currentView: {
name: 'confTx',
- context: 0,
+ context: action.id ? indexForPending(state, action.id) : indexForLastPending(state),
},
transForward: action.transForward,
warning: null,
@@ -328,36 +391,36 @@ function reduceApp (state, action) {
case actions.COMPLETED_TX:
log.debug('reducing COMPLETED_TX for tx ' + action.value)
- const otherUnconfActions = getUnconfActionList(state)
- .filter(tx => tx.id !== action.value)
- const hasOtherUnconfActions = otherUnconfActions.length > 0
-
- if (hasOtherUnconfActions) {
- log.debug('reducer detected txs - rendering confTx view')
- return extend(appState, {
- transForward: false,
- currentView: {
- name: 'confTx',
- context: 0,
- },
- warning: null,
- })
- } else {
- log.debug('attempting to close popup')
- return extend(appState, {
- // indicate notification should close
- shouldClose: true,
- transForward: false,
- warning: null,
- currentView: {
- name: 'accountDetail',
- context: state.metamask.selectedAddress,
- },
- accountDetail: {
- subview: 'transactions',
- },
- })
- }
+ // const otherUnconfActions = getUnconfActionList(state)
+ // .filter(tx => tx.id !== action.value)
+ // const hasOtherUnconfActions = otherUnconfActions.length > 0
+
+ // if (hasOtherUnconfActions) {
+ // log.debug('reducer detected txs - rendering confTx view')
+ // return extend(appState, {
+ // transForward: false,
+ // currentView: {
+ // name: 'confTx',
+ // context: 0,
+ // },
+ // warning: null,
+ // })
+ // } else {
+ log.debug('attempting to close popup')
+ return extend(appState, {
+ // indicate notification should close
+ shouldClose: true,
+ transForward: false,
+ warning: null,
+ currentView: {
+ name: 'accountDetail',
+ context: state.metamask.selectedAddress,
+ },
+ accountDetail: {
+ subview: 'transactions',
+ },
+ })
+ // }
case actions.NEXT_TX:
return extend(appState, {
@@ -597,3 +660,7 @@ function indexForPending (state, txId) {
const index = unconfTxList.indexOf(match)
return index
}
+
+function indexForLastPending (state) {
+ return getUnconfActionList(state).length
+}
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
index 85ac3e201..fb53bbaef 100644
--- a/ui/app/reducers/metamask.js
+++ b/ui/app/reducers/metamask.js
@@ -11,6 +11,7 @@ function reduceMetamask (state, action) {
var metamaskState = extend({
isInitialized: false,
isUnlocked: false,
+ isAccountMenuOpen: false,
isMascara: window.platform instanceof MetamascaraPlatform,
rpcTarget: 'https://rawtestrpc.metamask.io/',
identities: {},
@@ -19,8 +20,23 @@ function reduceMetamask (state, action) {
lastUnreadNotice: undefined,
frequentRpcList: [],
addressBook: [],
+ selectedTokenAddress: null,
tokenExchangeRates: {},
+ tokens: [],
+ send: {
+ gasLimit: null,
+ gasPrice: null,
+ gasTotal: null,
+ tokenBalance: null,
+ from: '',
+ to: '',
+ amount: '0x0',
+ memo: '',
+ errors: {},
+ editingTransactionId: null,
+ },
coinOptions: {},
+ useBlockie: false,
}, state.metamask)
switch (action.type) {
@@ -94,6 +110,14 @@ function reduceMetamask (state, action) {
}
return newState
+ case actions.EDIT_TX:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ editingTransactionId: action.value,
+ },
+ })
+
case actions.SHOW_NEW_VAULT_SEED:
return extend(metamaskState, {
isUnlocked: true,
@@ -119,12 +143,17 @@ function reduceMetamask (state, action) {
delete newState.seedWords
return newState
+ case actions.SET_SELECTED_TOKEN:
+ return extend(metamaskState, {
+ selectedTokenAddress: action.value,
+ })
+
case actions.SAVE_ACCOUNT_LABEL:
const account = action.value.account
const name = action.value.label
- var id = {}
+ const id = {}
id[account] = extend(metamaskState.identities[account], { name })
- var identities = extend(metamaskState.identities, id)
+ const identities = extend(metamaskState.identities, id)
return extend(metamaskState, { identities })
case actions.SET_CURRENT_FIAT:
@@ -134,6 +163,139 @@ function reduceMetamask (state, action) {
conversionDate: action.value.conversionDate,
})
+ case actions.UPDATE_TOKEN_EXCHANGE_RATE:
+ const { payload: { pair, marketinfo } } = action
+ return extend(metamaskState, {
+ tokenExchangeRates: {
+ ...metamaskState.tokenExchangeRates,
+ [pair]: marketinfo,
+ },
+ })
+
+ case actions.UPDATE_TOKENS:
+ return extend(metamaskState, {
+ tokens: action.newTokens,
+ })
+
+ // metamask.send
+ case actions.UPDATE_GAS_LIMIT:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ gasLimit: action.value,
+ },
+ })
+
+ case actions.UPDATE_GAS_PRICE:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ gasPrice: action.value,
+ },
+ })
+
+ case actions.TOGGLE_ACCOUNT_MENU:
+ return extend(metamaskState, {
+ isAccountMenuOpen: !metamaskState.isAccountMenuOpen,
+ })
+
+ case actions.UPDATE_GAS_TOTAL:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ gasTotal: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_TOKEN_BALANCE:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ tokenBalance: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_FROM:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ from: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_TO:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ to: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_AMOUNT:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ amount: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_MEMO:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ memo: action.value,
+ },
+ })
+
+ case actions.UPDATE_SEND_ERRORS:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ errors: {
+ ...metamaskState.send.errors,
+ ...action.value,
+ },
+ },
+ })
+
+ case actions.UPDATE_SEND:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ ...action.value,
+ },
+ })
+
+ case actions.CLEAR_SEND:
+ return extend(metamaskState, {
+ send: {
+ gasLimit: null,
+ gasPrice: null,
+ gasTotal: null,
+ tokenBalance: null,
+ from: '',
+ to: '',
+ amount: '0x0',
+ memo: '',
+ errors: {},
+ editingTransactionId: null,
+ },
+ })
+
+ case actions.UPDATE_TRANSACTION_PARAMS:
+ const { id: txId, value } = action
+ let { selectedAddressTxList } = metamaskState
+ selectedAddressTxList = selectedAddressTxList.map(tx => {
+ if (tx.id === txId) {
+ tx.txParams = value
+ }
+ return tx
+ })
+
+ return extend(metamaskState, {
+ selectedAddressTxList,
+ })
+
case actions.PAIR_UPDATE:
const { value: { marketinfo: pairMarketInfo } } = action
return extend(metamaskState, {
@@ -144,15 +306,20 @@ function reduceMetamask (state, action) {
})
case actions.SHAPESHIFT_SUBVIEW:
- const { value: { marketinfo, coinOptions } } = action
+ const { value: { marketinfo: ssMarketInfo, coinOptions } } = action
return extend(metamaskState, {
tokenExchangeRates: {
...metamaskState.tokenExchangeRates,
- [marketinfo.pair]: marketinfo,
+ [marketinfo.pair]: ssMarketInfo,
},
coinOptions,
})
+ case actions.SET_USE_BLOCKIE:
+ return extend(metamaskState, {
+ useBlockie: action.value,
+ })
+
default:
return metamaskState
diff --git a/ui/app/selectors.js b/ui/app/selectors.js
new file mode 100644
index 000000000..a5f9a75d8
--- /dev/null
+++ b/ui/app/selectors.js
@@ -0,0 +1,160 @@
+const valuesFor = require('./util').valuesFor
+const abi = require('human-standard-token-abi')
+
+const {
+ multiplyCurrencies,
+} = require('./conversion-util')
+
+const selectors = {
+ getSelectedAddress,
+ getSelectedIdentity,
+ getSelectedAccount,
+ getSelectedToken,
+ getSelectedTokenExchangeRate,
+ getTokenExchangeRate,
+ conversionRateSelector,
+ transactionsSelector,
+ accountsWithSendEtherInfoSelector,
+ getCurrentAccountWithSendEtherInfo,
+ getGasPrice,
+ getGasLimit,
+ getAddressBook,
+ getSendFrom,
+ getCurrentCurrency,
+ getSendAmount,
+ getSelectedTokenToFiatRate,
+ getSelectedTokenContract,
+}
+
+module.exports = selectors
+
+function getSelectedAddress (state) {
+ const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0]
+
+ return selectedAddress
+}
+
+function getSelectedIdentity (state) {
+ const selectedAddress = getSelectedAddress(state)
+ const identities = state.metamask.identities
+
+ return identities[selectedAddress]
+}
+
+function getSelectedAccount (state) {
+ const accounts = state.metamask.accounts
+ const selectedAddress = getSelectedAddress(state)
+
+ return accounts[selectedAddress]
+}
+
+function getSelectedToken (state) {
+ const tokens = state.metamask.tokens || []
+ const selectedTokenAddress = state.metamask.selectedTokenAddress
+ const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0]
+
+ return selectedToken || null
+}
+
+function getSelectedTokenExchangeRate (state) {
+ const tokenExchangeRates = state.metamask.tokenExchangeRates
+ const selectedToken = getSelectedToken(state) || {}
+ const { symbol = '' } = selectedToken
+
+ const pair = `${symbol.toLowerCase()}_eth`
+ const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
+
+ return tokenExchangeRate
+}
+
+function getTokenExchangeRate (state, tokenSymbol) {
+ const pair = `${tokenSymbol.toLowerCase()}_eth`
+ const tokenExchangeRates = state.metamask.tokenExchangeRates
+ const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
+
+ return tokenExchangeRate
+}
+
+function conversionRateSelector (state) {
+ return state.metamask.conversionRate
+}
+
+function getAddressBook (state) {
+ return state.metamask.addressBook
+}
+
+function accountsWithSendEtherInfoSelector (state) {
+ const {
+ accounts,
+ identities,
+ } = state.metamask
+
+ const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
+ return Object.assign({}, account, identities[key])
+ })
+
+ return accountsWithSendEtherInfo
+}
+
+function getCurrentAccountWithSendEtherInfo (state) {
+ const currentAddress = getSelectedAddress(state)
+ const accounts = accountsWithSendEtherInfoSelector(state)
+
+ return accounts.find(({ address }) => address === currentAddress)
+}
+
+function transactionsSelector (state) {
+ const { network, selectedTokenAddress } = state.metamask
+ const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)
+ const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined
+ const transactions = state.metamask.selectedAddressTxList || []
+ const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList)
+
+ // console.log({txsToRender, selectedTokenAddress})
+ return selectedTokenAddress
+ ? txsToRender
+ .filter(({ txParams: { to } }) => to === selectedTokenAddress)
+ .sort((a, b) => b.time - a.time)
+ : txsToRender
+ .sort((a, b) => b.time - a.time)
+}
+
+function getGasPrice (state) {
+ return state.metamask.send.gasPrice
+}
+
+function getGasLimit (state) {
+ return state.metamask.send.gasLimit
+}
+
+function getSendFrom (state) {
+ return state.metamask.send.from
+}
+
+function getSendAmount (state) {
+ return state.metamask.send.amount
+}
+
+function getCurrentCurrency (state) {
+ return state.metamask.currentCurrency
+}
+
+function getSelectedTokenToFiatRate (state) {
+ const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state)
+ const conversionRate = conversionRateSelector(state)
+
+ const tokenToFiatRate = multiplyCurrencies(
+ conversionRate,
+ selectedTokenExchangeRate,
+ { toNumericBase: 'dec' }
+ )
+
+ return tokenToFiatRate
+}
+
+function getSelectedTokenContract (state) {
+ const selectedToken = getSelectedToken(state)
+ return selectedToken
+ ? global.eth.contract(abi).at(selectedToken.address)
+ : null
+}
diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js
new file mode 100644
index 000000000..788ae87b4
--- /dev/null
+++ b/ui/app/send-v2.js
@@ -0,0 +1,603 @@
+const { inherits } = require('util')
+const PersistentForm = require('../lib/persistent-form')
+const h = require('react-hyperscript')
+
+const ethUtil = require('ethereumjs-util')
+
+const Identicon = require('./components/identicon')
+const FromDropdown = require('./components/send/from-dropdown')
+const ToAutoComplete = require('./components/send/to-autocomplete')
+const CurrencyDisplay = require('./components/send/currency-display')
+const MemoTextArea = require('./components/send/memo-textarea')
+const GasFeeDisplay = require('./components/send/gas-fee-display-v2')
+
+const {
+ MIN_GAS_TOTAL,
+ MIN_GAS_PRICE_HEX,
+ MIN_GAS_LIMIT_HEX,
+} = require('./components/send/send-constants')
+
+const {
+ multiplyCurrencies,
+ conversionGreaterThan,
+ subtractCurrencies,
+} = require('./conversion-util')
+const {
+ calcTokenAmount,
+} = require('./token-util')
+const {
+ isBalanceSufficient,
+ isTokenBalanceSufficient,
+} = require('./components/send/send-utils')
+const { isValidAddress } = require('./util')
+
+module.exports = SendTransactionScreen
+
+inherits(SendTransactionScreen, PersistentForm)
+function SendTransactionScreen () {
+ PersistentForm.call(this)
+
+ this.state = {
+ fromDropdownOpen: false,
+ toDropdownOpen: false,
+ errors: {
+ to: null,
+ amount: null,
+ },
+ }
+
+ this.handleToChange = this.handleToChange.bind(this)
+ this.handleAmountChange = this.handleAmountChange.bind(this)
+ this.validateAmount = this.validateAmount.bind(this)
+}
+
+const getParamsForGasEstimate = function (selectedAddress, symbol, data) {
+ const estimatedGasParams = {
+ from: selectedAddress,
+ gas: '746a528800',
+ }
+
+ if (symbol) {
+ Object.assign(estimatedGasParams, { value: '0x0' })
+ }
+
+ if (data) {
+ Object.assign(estimatedGasParams, { data })
+ }
+
+ return estimatedGasParams
+}
+
+SendTransactionScreen.prototype.updateSendTokenBalance = function (usersToken) {
+ if (!usersToken) return
+
+ const {
+ selectedToken = {},
+ updateSendTokenBalance,
+ } = this.props
+ const { decimals } = selectedToken || {}
+ const tokenBalance = calcTokenAmount(usersToken.balance.toString(), decimals)
+
+ updateSendTokenBalance(tokenBalance)
+}
+
+SendTransactionScreen.prototype.componentWillMount = function () {
+ const {
+ updateTokenExchangeRate,
+ selectedToken = {},
+ getGasPrice,
+ estimateGas,
+ selectedAddress,
+ data,
+ updateGasTotal,
+ from,
+ tokenContract,
+ editingTransactionId,
+ gasPrice,
+ gasLimit,
+ } = this.props
+ const { symbol } = selectedToken || {}
+
+ if (symbol) {
+ updateTokenExchangeRate(symbol)
+ }
+
+ const estimateGasParams = getParamsForGasEstimate(selectedAddress, symbol, data)
+
+ const tokenBalancePromise = tokenContract && tokenContract.balanceOf(from.address)
+ let newGasTotal
+ if (!editingTransactionId) {
+ Promise
+ .all([
+ getGasPrice(),
+ estimateGas(estimateGasParams),
+ tokenBalancePromise,
+ ])
+ .then(([gasPrice, gas, usersToken]) => {
+
+ const newGasTotal = multiplyCurrencies(gas, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+ updateGasTotal(newGasTotal)
+ this.updateSendTokenBalance(usersToken)
+ })
+ } else {
+ newGasTotal = multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+ updateGasTotal(newGasTotal)
+ tokenBalancePromise && tokenBalancePromise.then(
+ usersToken => this.updateSendTokenBalance(usersToken))
+ }
+}
+
+SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) {
+ const {
+ from: { balance },
+ gasTotal,
+ tokenBalance,
+ amount,
+ selectedToken,
+ } = this.props
+ const {
+ from: { balance: prevBalance },
+ gasTotal: prevGasTotal,
+ tokenBalance: prevTokenBalance,
+ } = prevProps
+
+ const notFirstRender = [prevBalance, prevGasTotal].every(n => n !== null)
+
+ const balanceHasChanged = balance !== prevBalance
+ const gasTotalHasChange = gasTotal !== prevGasTotal
+ const tokenBalanceHasChanged = selectedToken && tokenBalance !== prevTokenBalance
+ const amountValidationChange = balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged
+
+ if (notFirstRender && amountValidationChange) {
+ this.validateAmount(amount)
+ }
+}
+
+SendTransactionScreen.prototype.renderHeaderIcon = function () {
+ const { selectedToken } = this.props
+
+ return h('div.send-v2__send-header-icon-container', [
+ selectedToken
+ ? h(Identicon, {
+ diameter: 40,
+ address: selectedToken.address,
+ })
+ : h('img.send-v2__send-header-icon', { src: '../images/eth_logo.svg' }),
+ ])
+}
+
+SendTransactionScreen.prototype.renderTitle = function () {
+ const { selectedToken } = this.props
+
+ return h('div.send-v2__title', [selectedToken ? 'Send Tokens' : 'Send Funds'])
+}
+
+SendTransactionScreen.prototype.renderCopy = function () {
+ const { selectedToken } = this.props
+
+ const tokenText = selectedToken ? 'tokens' : 'ETH'
+
+ return h('div.send-v2__form-header-copy', [
+
+ h('div.send-v2__copy', `Only send ${tokenText} to an Ethereum address.`),
+
+ h('div.send-v2__copy', 'Sending to a different crytpocurrency that is not Ethereum may result in permanent loss.'),
+
+ ])
+}
+
+SendTransactionScreen.prototype.renderHeader = function () {
+ return h('div', [
+ h('div.send-v2__header', {}, [
+
+ this.renderHeaderIcon(),
+
+ h('div.send-v2__arrow-background', [
+ h('i.fa.fa-lg.fa-arrow-circle-right.send-v2__send-arrow-icon'),
+ ]),
+
+ h('div.send-v2__header-tip'),
+
+ ]),
+
+ ])
+}
+
+SendTransactionScreen.prototype.renderErrorMessage = function (errorType) {
+ const { errors } = this.props
+ const errorMessage = errors[errorType]
+
+ return errorMessage
+ ? h('div.send-v2__error', [ errorMessage ])
+ : null
+}
+
+SendTransactionScreen.prototype.handleFromChange = async function (newFrom) {
+ const {
+ updateSendFrom,
+ tokenContract,
+ } = this.props
+
+ if (tokenContract) {
+ const usersToken = await tokenContract.balanceOf(newFrom.address)
+ this.updateSendTokenBalance(usersToken)
+ }
+ updateSendFrom(newFrom)
+}
+
+SendTransactionScreen.prototype.renderFromRow = function () {
+ const {
+ from,
+ fromAccounts,
+ conversionRate,
+ } = this.props
+
+ const { fromDropdownOpen } = this.state
+
+ return h('div.send-v2__form-row', [
+
+ h('div.send-v2__form-label', 'From:'),
+
+ h('div.send-v2__form-field', [
+ h(FromDropdown, {
+ dropdownOpen: fromDropdownOpen,
+ accounts: fromAccounts,
+ selectedAccount: from,
+ onSelect: newFrom => this.handleFromChange(newFrom),
+ openDropdown: () => this.setState({ fromDropdownOpen: true }),
+ closeDropdown: () => this.setState({ fromDropdownOpen: false }),
+ conversionRate,
+ }),
+ ]),
+
+ ])
+}
+
+SendTransactionScreen.prototype.handleToChange = function (to) {
+ const {
+ updateSendTo,
+ updateSendErrors,
+ from: {address: from},
+ } = this.props
+ let toError = null
+
+ if (!to) {
+ toError = 'Required'
+ } else if (!isValidAddress(to)) {
+ toError = 'Recipient address is invalid'
+ } else if (to === from) {
+ toError = 'From and To address cannot be the same'
+ }
+
+ updateSendTo(to)
+ updateSendErrors({ to: toError })
+}
+
+SendTransactionScreen.prototype.renderToRow = function () {
+ const { toAccounts, errors, to } = this.props
+
+ const { toDropdownOpen } = this.state
+
+ return h('div.send-v2__form-row', [
+
+ h('div.send-v2__form-label', [
+
+ 'To:',
+
+ this.renderErrorMessage('to'),
+
+ ]),
+
+ h('div.send-v2__form-field', [
+ h(ToAutoComplete, {
+ to,
+ accounts: Object.entries(toAccounts).map(([key, account]) => account),
+ dropdownOpen: toDropdownOpen,
+ openDropdown: () => this.setState({ toDropdownOpen: true }),
+ closeDropdown: () => this.setState({ toDropdownOpen: false }),
+ onChange: this.handleToChange,
+ inError: Boolean(errors.to),
+ }),
+ ]),
+
+ ])
+}
+
+SendTransactionScreen.prototype.handleAmountChange = function (value) {
+ const amount = value
+ const { updateSendAmount } = this.props
+
+ this.validateAmount(amount)
+ updateSendAmount(amount)
+}
+
+SendTransactionScreen.prototype.setAmountToMax = function () {
+ const {
+ from: { balance },
+ updateSendAmount,
+ updateSendErrors,
+ updateGasPrice,
+ updateGasLimit,
+ updateGasTotal,
+ tokenBalance,
+ selectedToken,
+ } = this.props
+ const { decimals } = selectedToken || {}
+ const multiplier = Math.pow(10, Number(decimals || 0))
+
+ const maxAmount = selectedToken
+ ? multiplyCurrencies(tokenBalance, multiplier, {toNumericBase: 'hex'})
+ : subtractCurrencies(
+ ethUtil.addHexPrefix(balance),
+ ethUtil.addHexPrefix(MIN_GAS_TOTAL),
+ { toNumericBase: 'hex' }
+ )
+
+ updateSendErrors({ amount: null })
+ if (!selectedToken) {
+ updateGasPrice(MIN_GAS_PRICE_HEX)
+ updateGasLimit(MIN_GAS_LIMIT_HEX)
+ updateGasTotal(MIN_GAS_TOTAL)
+ }
+ updateSendAmount(maxAmount)
+}
+
+SendTransactionScreen.prototype.validateAmount = function (value) {
+ const {
+ from: { balance },
+ updateSendErrors,
+ amountConversionRate,
+ conversionRate,
+ primaryCurrency,
+ selectedToken,
+ gasTotal,
+ tokenBalance,
+ } = this.props
+ const { decimals } = selectedToken || {}
+ const amount = value
+
+ let amountError = null
+ const sufficientBalance = isBalanceSufficient({
+ amount: selectedToken ? '0x0' : amount,
+ gasTotal,
+ balance,
+ primaryCurrency,
+ amountConversionRate,
+ conversionRate,
+ })
+
+ let sufficientTokens
+ if (selectedToken) {
+ sufficientTokens = isTokenBalanceSufficient({
+ tokenBalance,
+ amount,
+ decimals,
+ })
+ }
+
+ const amountLessThanZero = conversionGreaterThan(
+ { value: 0, fromNumericBase: 'dec' },
+ { value: amount, fromNumericBase: 'hex' },
+ )
+
+ if (!sufficientBalance) {
+ amountError = 'Insufficient funds.'
+ } else if (selectedToken && !sufficientTokens) {
+ amountError = 'Insufficient tokens.'
+ } else if (amountLessThanZero) {
+ amountError = 'Can not send negative amounts of ETH.'
+ }
+
+ updateSendErrors({ amount: amountError })
+}
+
+SendTransactionScreen.prototype.renderAmountRow = function () {
+ const {
+ selectedToken,
+ primaryCurrency = 'ETH',
+ convertedCurrency,
+ amountConversionRate,
+ errors,
+ amount,
+ } = this.props
+
+ return h('div.send-v2__form-row', [
+
+ h('div.send-v2__form-label', [
+ 'Amount:',
+ this.renderErrorMessage('amount'),
+ !errors.amount && h('div.send-v2__amount-max', {
+ onClick: (event) => {
+ event.preventDefault()
+ this.setAmountToMax()
+ },
+ }, [ 'Max' ]),
+ ]),
+
+ h('div.send-v2__form-field', [
+ h(CurrencyDisplay, {
+ inError: Boolean(errors.amount),
+ primaryCurrency,
+ convertedCurrency,
+ selectedToken,
+ value: amount || '0x0',
+ conversionRate: amountConversionRate,
+ handleChange: this.handleAmountChange,
+ }),
+ ]),
+
+ ])
+}
+
+SendTransactionScreen.prototype.renderGasRow = function () {
+ const {
+ conversionRate,
+ convertedCurrency,
+ showCustomizeGasModal,
+ gasTotal = MIN_GAS_TOTAL,
+ } = this.props
+
+ return h('div.send-v2__form-row', [
+
+ h('div.send-v2__form-label', 'Gas fee:'),
+
+ h('div.send-v2__form-field', [
+
+ h(GasFeeDisplay, {
+ gasTotal,
+ conversionRate,
+ convertedCurrency,
+ onClick: showCustomizeGasModal,
+ }),
+
+ h('div.send-v2__sliders-icon-container', {
+ onClick: showCustomizeGasModal,
+ }, [
+ h('i.fa.fa-sliders.send-v2__sliders-icon'),
+ ]),
+
+ ]),
+
+ ])
+}
+
+SendTransactionScreen.prototype.renderMemoRow = function () {
+ const { updateSendMemo, memo } = this.props
+
+ return h('div.send-v2__form-row', [
+
+ h('div.send-v2__form-label', 'Transaction Memo:'),
+
+ h('div.send-v2__form-field', [
+ h(MemoTextArea, {
+ memo,
+ onChange: (event) => updateSendMemo(event.target.value),
+ }),
+ ]),
+
+ ])
+}
+
+SendTransactionScreen.prototype.renderForm = function () {
+ return h('div.send-v2__form', {}, [
+
+ h('div.sendV2__form-header', [
+
+ this.renderTitle(),
+
+ this.renderCopy(),
+
+ ]),
+
+ this.renderFromRow(),
+
+ this.renderToRow(),
+
+ this.renderAmountRow(),
+
+ this.renderGasRow(),
+
+ // this.renderMemoRow(),
+
+ ])
+}
+
+SendTransactionScreen.prototype.renderFooter = function () {
+ const {
+ goHome,
+ clearSend,
+ errors: { amount: amountError, to: toError },
+ } = this.props
+
+ const noErrors = !amountError && toError === null
+ const errorClass = noErrors ? '' : '__disabled'
+
+ return h('div.send-v2__footer', [
+ h('button.send-v2__cancel-btn', {
+ onClick: () => {
+ clearSend()
+ goHome()
+ },
+ }, 'Cancel'),
+ h(`button.send-v2__next-btn${errorClass}`, {
+ onClick: event => this.onSubmit(event),
+ }, 'Next'),
+ ])
+}
+
+SendTransactionScreen.prototype.render = function () {
+ return (
+
+ h('div.send-v2__container', [
+
+ this.renderHeader(),
+
+ this.renderForm(),
+
+ this.renderFooter(),
+ ])
+
+ )
+}
+
+SendTransactionScreen.prototype.addToAddressBookIfNew = function (newAddress) {
+ const { toAccounts, addToAddressBook } = this.props
+ if (!toAccounts.find(({ address }) => newAddress === address)) {
+ // TODO: nickname, i.e. addToAddressBook(recipient, nickname)
+ addToAddressBook(newAddress)
+ }
+}
+
+SendTransactionScreen.prototype.onSubmit = function (event) {
+ event.preventDefault()
+ const {
+ from: {address: from},
+ to,
+ amount,
+ gasLimit: gas,
+ gasPrice,
+ signTokenTx,
+ signTx,
+ selectedToken,
+ editingTransactionId,
+ errors: { amount: amountError, to: toError },
+ backToConfirmScreen,
+ } = this.props
+
+ const noErrors = !amountError && toError === null
+
+ if (!noErrors) {
+ return
+ }
+
+ this.addToAddressBookIfNew(to)
+
+ if (editingTransactionId) {
+ backToConfirmScreen(editingTransactionId)
+ return
+ }
+
+ const txParams = {
+ from,
+ value: '0',
+ gas,
+ gasPrice,
+ }
+
+ if (!selectedToken) {
+ txParams.value = amount
+ txParams.to = to
+ }
+
+ selectedToken
+ ? signTokenTx(selectedToken.address, to, amount, txParams)
+ : signTx(txParams)
+}
diff --git a/ui/app/send.js b/ui/app/send.js
index e59c1130e..517b7690d 100644
--- a/ui/app/send.js
+++ b/ui/app/send.js
@@ -1,293 +1,547 @@
-const inherits = require('util').inherits
-const PersistentForm = require('../lib/persistent-form')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const Identicon = require('./components/identicon')
-const actions = require('./actions')
-const util = require('./util')
-const numericBalance = require('./util').numericBalance
-const addressSummary = require('./util').addressSummary
-const isHex = require('./util').isHex
-const EthBalance = require('./components/eth-balance')
-const EnsInput = require('./components/ens-input')
-const ethUtil = require('ethereumjs-util')
-module.exports = connect(mapStateToProps)(SendTransactionScreen)
-
-function mapStateToProps (state) {
- var result = {
- address: state.metamask.selectedAddress,
- accounts: state.metamask.accounts,
- identities: state.metamask.identities,
- warning: state.appState.warning,
- network: state.metamask.network,
- addressBook: state.metamask.addressBook,
- conversionRate: state.metamask.conversionRate,
- currentCurrency: state.metamask.currentCurrency,
- }
-
- result.error = result.warning && result.warning.split('.')[0]
-
- result.account = result.accounts[result.address]
- result.identity = result.identities[result.address]
- result.balance = result.account ? numericBalance(result.account.balance) : null
-
- return result
-}
-
-inherits(SendTransactionScreen, PersistentForm)
-function SendTransactionScreen () {
- PersistentForm.call(this)
-}
-
-SendTransactionScreen.prototype.render = function () {
- this.persistentFormParentId = 'send-tx-form'
-
- const props = this.props
- const {
- address,
- account,
- identity,
- network,
- identities,
- addressBook,
- conversionRate,
- currentCurrency,
- } = props
-
- return (
-
- h('.send-screen.flex-column.flex-grow', [
-
- //
- // Sender Profile
- //
-
- h('.account-data-subsection.flex-row.flex-grow', {
- style: {
- margin: '0 20px',
- },
- }, [
-
- // header - identicon + nav
- h('.flex-row.flex-space-between', {
- style: {
- marginTop: '15px',
- },
- }, [
- // back button
- h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
- onClick: this.back.bind(this),
- }),
-
- // large identicon
- h('.identicon-wrapper.flex-column.flex-center.select-none', [
- h(Identicon, {
- diameter: 62,
- address: address,
- }),
- ]),
-
- // invisible place holder
- h('i.fa.fa-users.fa-lg.invisible', {
- style: {
- marginTop: '28px',
- },
- }),
-
- ]),
-
- // account label
-
- h('.flex-column', {
- style: {
- marginTop: '10px',
- alignItems: 'flex-start',
- },
- }, [
- h('h2.font-medium.color-forest.flex-center', {
- style: {
- paddingTop: '8px',
- marginBottom: '8px',
- },
- }, identity && identity.name),
-
- // address and getter actions
- h('.flex-row.flex-center', {
- style: {
- marginBottom: '8px',
- },
- }, [
-
- h('div', {
- style: {
- lineHeight: '16px',
- },
- }, addressSummary(address)),
-
- ]),
-
- // balance
- h('.flex-row.flex-center', [
-
- h(EthBalance, {
- value: account && account.balance,
- conversionRate,
- currentCurrency,
- }),
-
- ]),
- ]),
- ]),
-
- //
- // Required Fields
- //
-
- h('h3.flex-center.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- marginTop: '15px',
- marginBottom: '16px',
- },
- }, [
- 'Send Transaction',
- ]),
-
- // error message
- props.error && h('span.error.flex-center', props.error),
-
- // 'to' field
- h('section.flex-row.flex-center', [
- h(EnsInput, {
- name: 'address',
- placeholder: 'Recipient Address',
- onChange: this.recipientDidChange.bind(this),
- network,
- identities,
- addressBook,
- }),
- ]),
-
- // 'amount' and send button
- h('section.flex-row.flex-center', [
-
- h('input.large-input', {
- name: 'amount',
- placeholder: 'Amount',
- type: 'number',
- style: {
- marginRight: '6px',
- },
- dataset: {
- persistentFormId: 'tx-amount',
- },
- }),
-
- h('button.primary', {
- onClick: this.onSubmit.bind(this),
- style: {
- textTransform: 'uppercase',
- },
- }, 'Next'),
-
- ]),
-
- //
- // Optional Fields
- //
- h('h3.flex-center.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- marginTop: '16px',
- marginBottom: '16px',
- },
- }, [
- 'Transaction Data (optional)',
- ]),
-
- // 'data' field
- h('section.flex-column.flex-center', [
- h('input.large-input', {
- name: 'txData',
- placeholder: '0x01234',
- style: {
- width: '100%',
- resize: 'none',
- },
- dataset: {
- persistentFormId: 'tx-data',
- },
- }),
- ]),
- ])
- )
-}
-
-SendTransactionScreen.prototype.navigateToAccounts = function (event) {
- event.stopPropagation()
- this.props.dispatch(actions.showAccountsPage())
-}
-
-SendTransactionScreen.prototype.back = function () {
- var address = this.props.address
- this.props.dispatch(actions.backToAccountDetail(address))
-}
-
-SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) {
- this.setState({
- recipient: recipient,
- nickname: nickname,
- })
-}
-
-SendTransactionScreen.prototype.onSubmit = function () {
- const state = this.state || {}
- const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '')
- const nickname = state.nickname || ' '
- const input = document.querySelector('input[name="amount"]').value
- const value = util.normalizeEthStringToWei(input)
- const txData = document.querySelector('input[name="txData"]').value
- const balance = this.props.balance
- let message
-
- if (value.gt(balance)) {
- message = 'Insufficient funds.'
- return this.props.dispatch(actions.displayWarning(message))
- }
-
- if (input < 0) {
- message = 'Can not send negative amounts of ETH.'
- return this.props.dispatch(actions.displayWarning(message))
- }
-
- if ((util.isInvalidChecksumAddress(recipient))) {
- message = 'Recipient address checksum is invalid.'
- return this.props.dispatch(actions.displayWarning(message))
- }
-
- if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) {
- message = 'Recipient address is invalid.'
- return this.props.dispatch(actions.displayWarning(message))
- }
-
- if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) {
- message = 'Transaction data must be hex string.'
- return this.props.dispatch(actions.displayWarning(message))
- }
-
- this.props.dispatch(actions.hideWarning())
-
- this.props.dispatch(actions.addToAddressBook(recipient, nickname))
-
- var txParams = {
- from: this.props.address,
- value: '0x' + value.toString(16),
- }
-
- if (recipient) txParams.to = ethUtil.addHexPrefix(recipient)
- if (txData) txParams.data = txData
-
- this.props.dispatch(actions.signTx(txParams))
-}
+// const { inherits } = require('util')
+// const PersistentForm = require('../lib/persistent-form')
+// const h = require('react-hyperscript')
+// const connect = require('react-redux').connect
+// const Identicon = require('./components/identicon')
+// const EnsInput = require('./components/ens-input')
+// const GasTooltip = require('./components/send/gas-tooltip')
+// const CurrencyToggle = require('./components/send/currency-toggle')
+// const GasFeeDisplay = require('./components/send/gas-fee-display')
+// const { getSelectedIdentity } = require('./selectors')
+
+// const {
+// showAccountsPage,
+// backToAccountDetail,
+// displayWarning,
+// hideWarning,
+// addToAddressBook,
+// signTx,
+// estimateGas,
+// getGasPrice,
+// } = require('./actions')
+// const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util')
+// const { isHex, numericBalance, isValidAddress, allNull } = require('./util')
+// const { conversionUtil, conversionGreaterThan } = require('./conversion-util')
+
+// module.exports = connect(mapStateToProps)(SendTransactionScreen)
+
+// function mapStateToProps (state) {
+// const {
+// selectedAddress: address,
+// accounts,
+// identities,
+// network,
+// addressBook,
+// conversionRate,
+// currentBlockGasLimit: blockGasLimit,
+// } = state.metamask
+// const { warning } = state.appState
+// const selectedIdentity = getSelectedIdentity(state)
+// const account = accounts[address]
+
+// return {
+// address,
+// accounts,
+// identities,
+// network,
+// addressBook,
+// conversionRate,
+// blockGasLimit,
+// warning,
+// selectedIdentity,
+// error: warning && warning.split('.')[0],
+// account,
+// identity: identities[address],
+// balance: account ? account.balance : null,
+// }
+// }
+
+// inherits(SendTransactionScreen, PersistentForm)
+// function SendTransactionScreen () {
+// PersistentForm.call(this)
+
+// // [WIP] These are the bare minimum of tx props needed to sign a transaction
+// // We will need a few more for contract-related interactions
+// this.state = {
+// newTx: {
+// from: '',
+// to: '',
+// amountToSend: '0x0',
+// gasPrice: null,
+// gas: null,
+// amount: '0x0',
+// txData: null,
+// memo: '',
+// },
+// activeCurrency: 'USD',
+// tooltipIsOpen: false,
+// errors: {},
+// isValid: false,
+// }
+
+// this.back = this.back.bind(this)
+// this.closeTooltip = this.closeTooltip.bind(this)
+// this.onSubmit = this.onSubmit.bind(this)
+// this.setActiveCurrency = this.setActiveCurrency.bind(this)
+// this.toggleTooltip = this.toggleTooltip.bind(this)
+// this.validate = this.validate.bind(this)
+// this.getAmountToSend = this.getAmountToSend.bind(this)
+// this.setErrorsFor = this.setErrorsFor.bind(this)
+// this.clearErrorsFor = this.clearErrorsFor.bind(this)
+
+// this.renderFromInput = this.renderFromInput.bind(this)
+// this.renderToInput = this.renderToInput.bind(this)
+// this.renderAmountInput = this.renderAmountInput.bind(this)
+// this.renderGasInput = this.renderGasInput.bind(this)
+// this.renderMemoInput = this.renderMemoInput.bind(this)
+// this.renderErrorMessage = this.renderErrorMessage.bind(this)
+// }
+
+// SendTransactionScreen.prototype.componentWillMount = function () {
+// const { newTx } = this.state
+// const { address } = this.props
+
+// Promise.all([
+// this.props.dispatch(getGasPrice()),
+// this.props.dispatch(estimateGas({
+// from: address,
+// gas: '746a528800',
+// })),
+// ])
+// .then(([blockGasPrice, estimatedGas]) => {
+// console.log({ blockGasPrice, estimatedGas})
+// this.setState({
+// newTx: {
+// ...newTx,
+// gasPrice: blockGasPrice,
+// gas: estimatedGas,
+// },
+// })
+// })
+// }
+
+// SendTransactionScreen.prototype.renderErrorMessage = function(errorType, warning) {
+// const { errors } = this.state
+// const errorMessage = errors[errorType];
+
+// return errorMessage || warning
+// ? h('div.send-screen-input-wrapper__error-message', [ errorMessage || warning ])
+// : null
+// }
+
+// SendTransactionScreen.prototype.renderFromInput = function (from, identities) {
+// return h('div.send-screen-input-wrapper', [
+
+// h('div', 'From:'),
+
+// h('input.large-input.send-screen-input', {
+// list: 'accounts',
+// placeholder: 'Account',
+// value: from,
+// onChange: (event) => {
+// this.setState({
+// newTx: {
+// ...this.state.newTx,
+// from: event.target.value,
+// },
+// })
+// },
+// onBlur: () => this.setErrorsFor('from'),
+// onFocus: event => {
+// this.clearErrorsFor('from')
+// this.state.newTx.from && event.target.select()
+// },
+// }),
+
+// h('datalist#accounts', [
+// Object.entries(identities).map(([key, { address, name }]) => {
+// return h('option', {
+// value: address,
+// label: name,
+// key: address,
+// })
+// }),
+// ]),
+
+// this.renderErrorMessage('from'),
+
+// ])
+// }
+
+// SendTransactionScreen.prototype.renderToInput = function (to, identities, addressBook) {
+// return h('div.send-screen-input-wrapper', [
+
+// h('div', 'To:'),
+
+// h('input.large-input.send-screen-input', {
+// name: 'address',
+// list: 'addresses',
+// placeholder: 'Address',
+// value: to,
+// onChange: (event) => {
+// this.setState({
+// newTx: {
+// ...this.state.newTx,
+// to: event.target.value,
+// },
+// })
+// },
+// onBlur: () => {
+// this.setErrorsFor('to')
+// },
+// onFocus: event => {
+// this.clearErrorsFor('to')
+// this.state.newTx.to && event.target.select()
+// },
+// }),
+
+// h('datalist#addresses', [
+// // Corresponds to the addresses owned.
+// ...Object.entries(identities).map(([key, { address, name }]) => {
+// return h('option', {
+// value: address,
+// label: name,
+// key: address,
+// })
+// }),
+// // Corresponds to previously sent-to addresses.
+// ...addressBook.map(({ address, name }) => {
+// return h('option', {
+// value: address,
+// label: name,
+// key: address,
+// })
+// }),
+// ]),
+
+// this.renderErrorMessage('to'),
+
+// ])
+// }
+
+// SendTransactionScreen.prototype.renderAmountInput = function (activeCurrency) {
+// return h('div.send-screen-input-wrapper', [
+
+// h('div.send-screen-amount-labels', [
+// h('span', 'Amount'),
+// h(CurrencyToggle, {
+// activeCurrency,
+// onClick: (newCurrency) => this.setActiveCurrency(newCurrency),
+// }), // holding on icon from design
+// ]),
+
+// h('input.large-input.send-screen-input', {
+// placeholder: `0 ${activeCurrency}`,
+// type: 'number',
+// onChange: (event) => {
+// const amountToSend = event.target.value
+// ? this.getAmountToSend(event.target.value)
+// : '0x0'
+
+// this.setState({
+// newTx: Object.assign(
+// this.state.newTx,
+// {
+// amount: event.target.value,
+// amountToSend: amountToSend,
+// }
+// ),
+// })
+// },
+// onBlur: () => {
+// this.setErrorsFor('amount')
+// },
+// onFocus: () => this.clearErrorsFor('amount'),
+// }),
+
+// this.renderErrorMessage('amount'),
+
+// ])
+// }
+
+// SendTransactionScreen.prototype.renderGasInput = function (gasPrice, gas, activeCurrency, conversionRate, blockGasLimit) {
+// return h('div.send-screen-input-wrapper', [
+// this.state.tooltipIsOpen && h(GasTooltip, {
+// className: 'send-tooltip',
+// gasPrice,
+// gasLimit: gas,
+// onClose: this.closeTooltip,
+// onFeeChange: ({gasLimit, gasPrice}) => {
+// this.setState({
+// newTx: {
+// ...this.state.newTx,
+// gas: gasLimit,
+// gasPrice,
+// },
+// })
+// },
+// }),
+
+// h('div.send-screen-gas-labels', [
+// h('span', [
+// h('i.fa.fa-bolt'),
+// 'Gas fee:',
+// ]),
+// h('span', 'What\'s this?'),
+// ]),
+
+// // TODO: handle loading time when switching to USD
+// h('div.large-input.send-screen-gas-input', {}, [
+// h(GasFeeDisplay, {
+// activeCurrency,
+// conversionRate,
+// gas,
+// gasPrice,
+// blockGasLimit,
+// }),
+// h('div.send-screen-gas-input-customize', {
+// onClick: this.toggleTooltip,
+// }, [
+// 'Customize',
+// ]),
+// ]),
+
+// ])
+// }
+
+// SendTransactionScreen.prototype.renderMemoInput = function () {
+// return h('div.send-screen-input-wrapper', [
+// h('div', 'Transaction memo (optional)'),
+// h('input.large-input.send-screen-input', {
+// onChange: () => {
+// this.setState({
+// newTx: Object.assign(
+// this.state.newTx,
+// {
+// memo: event.target.value,
+// }
+// ),
+// })
+// },
+// }),
+// ])
+// }
+
+// SendTransactionScreen.prototype.render = function () {
+// this.persistentFormParentId = 'send-tx-form'
+
+// const props = this.props
+// const {
+// warning,
+// identities,
+// addressBook,
+// conversionRate,
+// } = props
+
+// const {
+// blockGasLimit,
+// newTx,
+// activeCurrency,
+// isValid,
+// } = this.state
+// const { gas, gasPrice } = newTx
+
+// return (
+
+// h('div.send-screen-wrapper', [
+// // Main Send token Card
+// h('div.send-screen-card', [
+
+// h('img.send-eth-icon', { src: '../images/eth_logo.svg' }),
+
+// h('div.send-screen__title', 'Send'),
+
+// h('div.send-screen__subtitle', 'Send Ethereum to anyone with an Ethereum account'),
+
+// this.renderFromInput(this.state.newTx.from, identities),
+
+// this.renderToInput(this.state.newTx.to, identities, addressBook),
+
+// this.renderAmountInput(activeCurrency),
+
+// this.renderGasInput(
+// gasPrice || '0x0',
+// gas || '0x0',
+// activeCurrency,
+// conversionRate,
+// blockGasLimit
+// ),
+
+// this.renderMemoInput(),
+
+// this.renderErrorMessage(null, warning),
+
+// ]),
+
+// // Buttons underneath card
+// h('section.flex-column.flex-center', [
+// h('button.btn-secondary.send-screen__send-button', {
+// className: !isValid && 'send-screen__send-button__disabled',
+// onClick: (event) => isValid && this.onSubmit(event),
+// }, 'Next'),
+// h('button.btn-tertiary.send-screen__cancel-button', {
+// onClick: this.back,
+// }, 'Cancel'),
+// ]),
+// ])
+
+// )
+// }
+
+// SendTransactionScreen.prototype.toggleTooltip = function () {
+// this.setState({ tooltipIsOpen: !this.state.tooltipIsOpen })
+// }
+
+// SendTransactionScreen.prototype.closeTooltip = function () {
+// this.setState({ tooltipIsOpen: false })
+// }
+
+// SendTransactionScreen.prototype.setActiveCurrency = function (newCurrency) {
+// this.setState({ activeCurrency: newCurrency })
+// }
+
+// SendTransactionScreen.prototype.back = function () {
+// var address = this.props.address
+// this.props.dispatch(backToAccountDetail(address))
+// }
+
+// SendTransactionScreen.prototype.validate = function (balance, amountToSend, { to, from }) {
+// const sufficientBalance = conversionGreaterThan(
+// {
+// value: balance,
+// fromNumericBase: 'hex',
+// },
+// {
+// value: amountToSend,
+// fromNumericBase: 'hex',
+// },
+// )
+
+// const amountLessThanZero = conversionGreaterThan(
+// {
+// value: 0,
+// fromNumericBase: 'dec',
+// },
+// {
+// value: amountToSend,
+// fromNumericBase: 'hex',
+// },
+// )
+
+// const errors = {}
+
+// if (!sufficientBalance) {
+// errors.amount = 'Insufficient funds.'
+// }
+
+// if (amountLessThanZero) {
+// errors.amount = 'Can not send negative amounts of ETH.'
+// }
+
+// if (!from) {
+// errors.from = 'Required'
+// }
+
+// if (from && !isValidAddress(from)) {
+// errors.from = 'Sender address is invalid.'
+// }
+
+// if (!to) {
+// errors.to = 'Required'
+// }
+
+// if (to && !isValidAddress(to)) {
+// errors.to = 'Recipient address is invalid.'
+// }
+
+// // if (txData && !isHex(stripHexPrefix(txData))) {
+// // message = 'Transaction data must be hex string.'
+// // return this.props.dispatch(displayWarning(message))
+// // }
+
+// return {
+// isValid: allNull(errors),
+// errors,
+// }
+// }
+
+// SendTransactionScreen.prototype.getAmountToSend = function (amount) {
+// const { activeCurrency } = this.state
+// const { conversionRate } = this.props
+
+// return conversionUtil(amount, {
+// fromNumericBase: 'dec',
+// toNumericBase: 'hex',
+// fromCurrency: activeCurrency,
+// toCurrency: 'ETH',
+// toDenomination: 'WEI',
+// conversionRate,
+// invertConversionRate: activeCurrency !== 'ETH',
+// })
+// }
+
+// SendTransactionScreen.prototype.setErrorsFor = function (field) {
+// const { balance } = this.props
+// const { newTx, errors: previousErrors } = this.state
+// const { amountToSend } = newTx
+
+// const {
+// isValid,
+// errors: newErrors
+// } = this.validate(balance, amountToSend, newTx)
+
+// const nextErrors = Object.assign({}, previousErrors, {
+// [field]: newErrors[field] || null
+// })
+
+// if (!isValid) {
+// this.setState({
+// errors: nextErrors,
+// isValid,
+// })
+// }
+// }
+
+// SendTransactionScreen.prototype.clearErrorsFor = function (field) {
+// const { errors: previousErrors } = this.state
+// const nextErrors = Object.assign({}, previousErrors, {
+// [field]: null
+// })
+
+// this.setState({
+// errors: nextErrors,
+// isValid: allNull(nextErrors),
+// })
+// }
+
+// SendTransactionScreen.prototype.onSubmit = function (event) {
+// event.preventDefault()
+// const { warning, balance } = this.props
+// const state = this.state || {}
+
+// const recipient = state.newTx.to
+// const sender = state.newTx.from
+// const nickname = state.nickname || ' '
+
+// // TODO: convert this to hex when created and include it in send
+// const txData = state.newTx.memo
+
+// this.props.dispatch(hideWarning())
+
+// this.props.dispatch(addToAddressBook(recipient, nickname))
+
+// var txParams = {
+// from: this.state.newTx.from,
+// to: this.state.newTx.to,
+
+// value: this.state.newTx.amountToSend,
+
+// gas: this.state.newTx.gas,
+// gasPrice: this.state.newTx.gasPrice,
+// }
+
+// if (recipient) txParams.to = addHexPrefix(recipient)
+// if (txData) txParams.data = txData
+
+// this.props.dispatch(signTx(txParams))
+// }
diff --git a/ui/app/settings.js b/ui/app/settings.js
index 454cc95e0..caa36d2b8 100644
--- a/ui/app/settings.js
+++ b/ui/app/settings.js
@@ -1,59 +1,387 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
+const { Component } = require('react')
+const PropTypes = require('prop-types')
const h = require('react-hyperscript')
-const connect = require('react-redux').connect
+const { connect } = require('react-redux')
const actions = require('./actions')
+const infuraCurrencies = require('./infura-conversion.json')
+const validUrl = require('valid-url')
+const { exportAsFile } = require('./util')
+const TabBar = require('./components/tab-bar')
+const SimpleDropdown = require('./components/dropdowns/simple-dropdown')
+const ToggleButton = require('react-toggle-button')
-module.exports = connect(mapStateToProps)(AppSettingsPage)
+const getInfuraCurrencyOptions = () => {
+ const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
+ return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
+ })
-function mapStateToProps (state) {
- return {}
+ return sortedCurrencies.map(({ quote: { code, name } }) => {
+ return {
+ displayValue: `${code.toUpperCase()} - ${name}`,
+ key: code,
+ value: code,
+ }
+ })
}
-inherits(AppSettingsPage, Component)
-function AppSettingsPage () {
- Component.call(this)
-}
+class Settings extends Component {
+ constructor (props) {
+ super(props)
-AppSettingsPage.prototype.render = function () {
- return (
+ const { tab } = props
+ const activeTab = tab === 'info' ? 'info' : 'settings'
- h('.account-detail-section.flex-column.flex-grow', [
+ this.state = {
+ activeTab,
+ newRpc: '',
+ }
+ }
- // subtitle and nav
- h('.flex-row.flex-center', [
- h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
- onClick: this.navigateToAccounts.bind(this),
- }),
- h('h2.page-subtitle', 'Settings'),
- ]),
+ renderTabs () {
+ const { activeTab } = this.state
- h('label', {
- htmlFor: 'settings-rpc-endpoint',
- }, 'RPC Endpoint:'),
- h('input', {
- type: 'url',
- id: 'settings-rpc-endpoint',
- onKeyPress: this.onKeyPress.bind(this),
+ return h('div.settings__tabs', [
+ h(TabBar, {
+ tabs: [
+ { content: 'Settings', key: 'settings' },
+ { content: 'Info', key: 'info' },
+ ],
+ defaultTab: activeTab,
+ tabSelected: key => this.setState({ activeTab: key }),
}),
+ ])
+ }
+
+ renderBlockieOptIn () {
+ const { metamask: { useBlockie }, setUseBlockie } = this.props
+
+ return h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('span', 'Use Blockies Identicon'),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h(ToggleButton, {
+ value: useBlockie,
+ onToggle: (value) => setUseBlockie(!value),
+ activeLabel: '',
+ inactiveLabel: '',
+ }),
+ ]),
+ ]),
+ ])
+ }
+
+ renderCurrentConversion () {
+ const { metamask: { currentCurrency, conversionDate }, setCurrentCurrency } = this.props
+
+ return h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('span', 'Current Conversion'),
+ h('span.settings__content-description', `Updated ${Date(conversionDate)}`),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h(SimpleDropdown, {
+ placeholder: 'Select Currency',
+ options: getInfuraCurrencyOptions(),
+ selectedOption: currentCurrency,
+ onSelect: newCurrency => setCurrentCurrency(newCurrency),
+ }),
+ ]),
+ ]),
+ ])
+ }
+
+ renderCurrentProvider () {
+ const { metamask: { provider = {} } } = this.props
+ let title, value, color
+
+ switch (provider.type) {
+
+ case 'mainnet':
+ title = 'Current Network'
+ value = 'Main Ethereum Network'
+ color = '#038789'
+ break
+ case 'ropsten':
+ title = 'Current Network'
+ value = 'Ropsten Test Network'
+ color = '#e91550'
+ break
+
+ case 'kovan':
+ title = 'Current Network'
+ value = 'Kovan Test Network'
+ color = '#690496'
+ break
+
+ case 'rinkeby':
+ title = 'Current Network'
+ value = 'Rinkeby Test Network'
+ color = '#ebb33f'
+ break
+
+ default:
+ title = 'Current RPC'
+ value = provider.rpcTarget
+ }
+
+ return h('div.settings__content-row', [
+ h('div.settings__content-item', title),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('div.settings__provider-wrapper', [
+ h('div.settings__provider-icon', { style: { background: color } }),
+ h('div', value),
+ ]),
+ ]),
+ ]),
])
+ }
- )
+ renderNewRpcUrl () {
+ return (
+ h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('span', 'New RPC URL'),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('input.settings__input', {
+ placeholder: 'New RPC URL',
+ onChange: event => this.setState({ newRpc: event.target.value }),
+ onKeyPress: event => {
+ if (event.key === 'Enter') {
+ this.validateRpc(this.state.newRpc)
+ }
+ },
+ }),
+ h('div.settings__rpc-save-button', {
+ onClick: event => {
+ event.preventDefault()
+ this.validateRpc(this.state.newRpc)
+ },
+ }, 'Save'),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ validateRpc (newRpc) {
+ const { setRpcTarget, displayWarning } = this.props
+
+ if (validUrl.isWebUri(newRpc)) {
+ setRpcTarget(newRpc)
+ } else {
+ const appendedRpc = `http://${newRpc}`
+
+ if (validUrl.isWebUri(appendedRpc)) {
+ displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')
+ } else {
+ displayWarning('Invalid RPC URI')
+ }
+ }
+ }
+
+ renderStateLogs () {
+ return (
+ h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('div', 'State Logs'),
+ h(
+ 'div.settings__content-description',
+ 'State logs contain your public account addresses and sent transactions.'
+ ),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('button.settings__clear-button', {
+ onClick (event) {
+ exportAsFile('MetaMask State Logs', window.logState())
+ },
+ }, 'Download State Logs'),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ renderSeedWords () {
+ const { revealSeedConfirmation } = this.props
+
+ return (
+ h('div.settings__content-row', [
+ h('div.settings__content-item', 'Reveal Seed Words'),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('button.settings__clear-button.settings__clear-button--red', {
+ onClick (event) {
+ event.preventDefault()
+ revealSeedConfirmation()
+ },
+ }, 'Reveal Seed Words'),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ renderSettingsContent () {
+ const { warning } = this.props
+
+ return (
+ h('div.settings__content', [
+ warning && h('div.settings__error', warning),
+ this.renderBlockieOptIn(),
+ this.renderCurrentConversion(),
+ // this.renderCurrentProvider(),
+ this.renderNewRpcUrl(),
+ this.renderStateLogs(),
+ this.renderSeedWords(),
+ ])
+ )
+ }
+
+ renderLogo () {
+ return (
+ h('div.settings__info-logo-wrapper', [
+ h('img.settings__info-logo', { src: 'images/info-logo.png' }),
+ ])
+ )
+ }
+
+ renderInfoLinks () {
+ return (
+ h('div.settings__content-item.settings__content-item--without-height', [
+ h('div.settings__info-link-header', 'Links'),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/privacy.html',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', 'Privacy Policy'),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/terms.html',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', 'Terms of Use'),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/attributions.html',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', 'Attributions'),
+ ]),
+ ]),
+ h('hr.settings__info-separator'),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://support.metamask.io',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', 'Visit our Support Center'),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', 'Visit our web site'),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ target: '_blank',
+ href: 'mailto:help@metamask.io?subject=Feedback',
+ }, [
+ h('span.settings__info-link', 'Email us!'),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ renderInfoContent () {
+ return (
+ h('div.settings__content', [
+ h('div.settings__content-row', [
+ h('div.settings__content-item.settings__content-item--without-height', [
+ this.renderLogo(),
+ h('div.settings__info-item', [
+ h('div.settings__info-version-header', 'MetaMask Version'),
+ h('div.settings__info-version-number', '4.0.0'),
+ ]),
+ h('div.settings__info-item', [
+ h(
+ 'div.settings__info-about',
+ 'MetaMask is designed and built in California.'
+ ),
+ ]),
+ ]),
+ this.renderInfoLinks(),
+ ]),
+ ])
+ )
+ }
+
+ render () {
+ const { goHome } = this.props
+ const { activeTab } = this.state
+
+ return (
+ h('.main-container.settings', {}, [
+ h('.settings__header', [
+ h('div.settings__close-button', {
+ onClick: goHome,
+ }),
+ this.renderTabs(),
+ ]),
+
+ activeTab === 'settings'
+ ? this.renderSettingsContent()
+ : this.renderInfoContent(),
+ ])
+ )
+ }
}
-AppSettingsPage.prototype.componentDidMount = function () {
- document.querySelector('input').focus()
+Settings.propTypes = {
+ tab: PropTypes.string,
+ metamask: PropTypes.object,
+ setUseBlockie: PropTypes.func,
+ setCurrentCurrency: PropTypes.func,
+ setRpcTarget: PropTypes.func,
+ displayWarning: PropTypes.func,
+ revealSeedConfirmation: PropTypes.func,
+ warning: PropTypes.string,
+ goHome: PropTypes.func,
}
-AppSettingsPage.prototype.onKeyPress = function (event) {
- // get submit event
- if (event.key === 'Enter') {
- // this.submitPassword(event)
+const mapStateToProps = state => {
+ return {
+ metamask: state.metamask,
+ warning: state.appState.warning,
}
}
-AppSettingsPage.prototype.navigateToAccounts = function (event) {
- event.stopPropagation()
- this.props.dispatch(actions.showAccountsPage())
+const mapDispatchToProps = dispatch => {
+ return {
+ goHome: () => dispatch(actions.goHome()),
+ setCurrentCurrency: currency => dispatch(actions.setCurrentCurrency(currency)),
+ setRpcTarget: newRpc => dispatch(actions.setRpcTarget(newRpc)),
+ displayWarning: warning => dispatch(actions.displayWarning(warning)),
+ revealSeedConfirmation: () => dispatch(actions.revealSeedConfirmation()),
+ setUseBlockie: value => dispatch(actions.setUseBlockie(value)),
+ }
}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(Settings)
diff --git a/ui/app/token-tracker.js b/ui/app/token-tracker.js
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/token-tracker.js
diff --git a/ui/app/token-util.js b/ui/app/token-util.js
new file mode 100644
index 000000000..f84051ef5
--- /dev/null
+++ b/ui/app/token-util.js
@@ -0,0 +1,45 @@
+const abi = require('human-standard-token-abi')
+const Eth = require('ethjs-query')
+const EthContract = require('ethjs-contract')
+
+const tokenInfoGetter = function () {
+ if (typeof global.ethereumProvider === 'undefined') return
+
+ const eth = new Eth(global.ethereumProvider)
+ const contract = new EthContract(eth)
+ const TokenContract = contract(abi)
+
+ const tokens = {}
+
+ return async (address) => {
+ if (tokens[address]) {
+ return tokens[address]
+ }
+
+ const contract = TokenContract.at(address)
+
+ const result = await Promise.all([
+ contract.symbol(),
+ contract.decimals(),
+ ])
+
+ const [ symbol = [], decimals = [] ] = result
+
+ tokens[address] = { symbol: symbol[0], decimals: decimals[0] }
+
+ return tokens[address]
+ }
+}
+
+function calcTokenAmount (value, decimals) {
+ const multiplier = Math.pow(10, Number(decimals || 0))
+ const amount = Number(value / multiplier)
+
+ return amount
+}
+
+
+module.exports = {
+ tokenInfoGetter,
+ calcTokenAmount,
+}
diff --git a/ui/app/unlock.js b/ui/app/unlock.js
index 4180791c4..ec97b03bf 100644
--- a/ui/app/unlock.js
+++ b/ui/app/unlock.js
@@ -50,7 +50,7 @@ UnlockScreen.prototype.render = function () {
id: 'password-box',
placeholder: 'enter password',
style: {
-
+ background: 'white',
},
onKeyPress: this.onKeyPress.bind(this),
onInput: this.inputChanged.bind(this),
diff --git a/ui/app/util.js b/ui/app/util.js
index 3f8b4dcc3..82a5f9f29 100644
--- a/ui/app/util.js
+++ b/ui/app/util.js
@@ -1,4 +1,16 @@
+const abi = require('human-standard-token-abi')
const ethUtil = require('ethereumjs-util')
+const hexToBn = require('../../app/scripts/lib/hex-to-bn')
+const vreme = new (require('vreme'))()
+
+const MIN_GAS_PRICE_GWEI_BN = new ethUtil.BN(1)
+const GWEI_FACTOR = new ethUtil.BN(1e9)
+const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR)
+
+// formatData :: ( date: <Unix Timestamp> ) -> String
+function formatDate (date) {
+ return vreme.format(new Date(date), 'March 16 2014 14:30')
+}
var valueTable = {
wei: '1000000000000000000',
@@ -36,8 +48,14 @@ module.exports = {
valueTable: valueTable,
bnTable: bnTable,
isHex: isHex,
+ formatDate,
+ bnMultiplyByFraction,
+ getTxFeeBn,
+ shortenBalance,
+ getContractAtAddress,
exportAsFile: exportAsFile,
isInvalidChecksumAddress,
+ allNull,
}
function valuesFor (obj) {
@@ -224,6 +242,24 @@ function isHex (str) {
return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/))
}
+function bnMultiplyByFraction (targetBN, numerator, denominator) {
+ const numBN = new ethUtil.BN(numerator)
+ const denomBN = new ethUtil.BN(denominator)
+ return targetBN.mul(numBN).div(denomBN)
+}
+
+function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16), blockGasLimit) {
+ const gasBn = hexToBn(gas)
+ const gasPriceBn = hexToBn(gasPrice)
+ const txFeeBn = gasBn.mul(gasPriceBn)
+
+ return txFeeBn.toString(16)
+}
+
+function getContractAtAddress (tokenAddress) {
+ return global.eth.contract(abi).at(tokenAddress)
+}
+
function exportAsFile (filename, data) {
// source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
const blob = new Blob([data], {type: 'text/csv'})
@@ -238,3 +274,7 @@ function exportAsFile (filename, data) {
document.body.removeChild(elem)
}
}
+
+function allNull (obj) {
+ return Object.entries(obj).every(([key, value]) => value === null)
+}
diff --git a/ui/css.js b/ui/css.js
index 21b311c28..0d0f60806 100644
--- a/ui/css.js
+++ b/ui/css.js
@@ -4,11 +4,7 @@ const path = require('path')
module.exports = bundleCss
var cssFiles = {
- 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'),
- 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'),
- 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'),
- 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'),
- 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'),
+ 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/output/index.css'), 'utf8'),
'first-time.css': fs.readFileSync(path.join(__dirname, '../mascara/src/app/first-time/index.css'), 'utf8'),
'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'),
'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'),
diff --git a/ui/index.js b/ui/index.js
index ae05cbe67..fff677471 100644
--- a/ui/index.js
+++ b/ui/index.js
@@ -38,8 +38,11 @@ function startApp (metamaskState, accountManager, opts) {
// if unconfirmed txs, start on txConf page
const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network)
- if (unapprovedTxsAll.length > 0) {
- store.dispatch(actions.showConfTxPage())
+ const numberOfUnapprivedTx = unapprovedTxsAll.length
+ if (numberOfUnapprivedTx > 0) {
+ store.dispatch(actions.showConfTxPage({
+ id: unapprovedTxsAll[numberOfUnapprivedTx - 1].id,
+ }))
}
accountManager.on('update', function (metamaskState) {
diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js
new file mode 100644
index 000000000..037d990fa
--- /dev/null
+++ b/ui/lib/account-link.js
@@ -0,0 +1,26 @@
+module.exports = function (address, network) {
+ const net = parseInt(network)
+ let link
+ switch (net) {
+ case 1: // main net
+ link = `https://etherscan.io/address/${address}`
+ break
+ case 2: // morden test net
+ link = `https://morden.etherscan.io/address/${address}`
+ break
+ case 3: // ropsten test net
+ link = `https://ropsten.etherscan.io/address/${address}`
+ break
+ case 4: // rinkeby test net
+ link = `https://rinkeby.etherscan.io/address/${address}`
+ break
+ case 42: // kovan test net
+ link = `https://kovan.etherscan.io/address/${address}`
+ break
+ default:
+ link = ''
+ break
+ }
+
+ return link
+}
diff --git a/ui/lib/blockies.js b/ui/lib/blockies.js
new file mode 100644
index 000000000..ee5a2a5ca
--- /dev/null
+++ b/ui/lib/blockies.js
@@ -0,0 +1,364 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
+ (factory((global.blockies = {})));
+}(this, (function (exports) { 'use strict';
+
+ /**
+ * A handy class to calculate color values.
+ *
+ * @version 1.0
+ * @author Robert Eisele <robert@xarg.org>
+ * @copyright Copyright (c) 2010, Robert Eisele
+ * @link http://www.xarg.org/2010/03/generate-client-side-png-files-using-javascript/
+ * @license http://www.opensource.org/licenses/bsd-license.php BSD License
+ *
+ */
+
+
+// helper functions for that ctx
+ function write(buffer, offs) {
+ for (var i = 2; i < arguments.length; i++) {
+ for (var j = 0; j < arguments[i].length; j++) {
+ buffer[offs++] = arguments[i].charAt(j);
+ }
+ }
+ }
+
+ function byte2(w) {
+ return String.fromCharCode((w >> 8) & 255, w & 255);
+ }
+
+ function byte4(w) {
+ return String.fromCharCode((w >> 24) & 255, (w >> 16) & 255, (w >> 8) & 255, w & 255);
+ }
+
+ function byte2lsb(w) {
+ return String.fromCharCode(w & 255, (w >> 8) & 255);
+ }
+
+ var PNG = function(width,height,depth) {
+
+ this.width = width;
+ this.height = height;
+ this.depth = depth;
+
+ // pixel data and row filter identifier size
+ this.pix_size = height * (width + 1);
+
+ // deflate header, pix_size, block headers, adler32 checksum
+ this.data_size = 2 + this.pix_size + 5 * Math.floor((0xfffe + this.pix_size) / 0xffff) + 4;
+
+ // offsets and sizes of Png chunks
+ this.ihdr_offs = 0; // IHDR offset and size
+ this.ihdr_size = 4 + 4 + 13 + 4;
+ this.plte_offs = this.ihdr_offs + this.ihdr_size; // PLTE offset and size
+ this.plte_size = 4 + 4 + 3 * depth + 4;
+ this.trns_offs = this.plte_offs + this.plte_size; // tRNS offset and size
+ this.trns_size = 4 + 4 + depth + 4;
+ this.idat_offs = this.trns_offs + this.trns_size; // IDAT offset and size
+ this.idat_size = 4 + 4 + this.data_size + 4;
+ this.iend_offs = this.idat_offs + this.idat_size; // IEND offset and size
+ this.iend_size = 4 + 4 + 4;
+ this.buffer_size = this.iend_offs + this.iend_size; // total PNG size
+
+ this.buffer = new Array();
+ this.palette = new Object();
+ this.pindex = 0;
+
+ var _crc32 = new Array();
+
+ // initialize buffer with zero bytes
+ for (var i = 0; i < this.buffer_size; i++) {
+ this.buffer[i] = "\x00";
+ }
+
+ // initialize non-zero elements
+ write(this.buffer, this.ihdr_offs, byte4(this.ihdr_size - 12), 'IHDR', byte4(width), byte4(height), "\x08\x03");
+ write(this.buffer, this.plte_offs, byte4(this.plte_size - 12), 'PLTE');
+ write(this.buffer, this.trns_offs, byte4(this.trns_size - 12), 'tRNS');
+ write(this.buffer, this.idat_offs, byte4(this.idat_size - 12), 'IDAT');
+ write(this.buffer, this.iend_offs, byte4(this.iend_size - 12), 'IEND');
+
+ // initialize deflate header
+ var header = ((8 + (7 << 4)) << 8) | (3 << 6);
+ header+= 31 - (header % 31);
+
+ write(this.buffer, this.idat_offs + 8, byte2(header));
+
+ // initialize deflate block headers
+ for (var i = 0; (i << 16) - 1 < this.pix_size; i++) {
+ var size, bits;
+ if (i + 0xffff < this.pix_size) {
+ size = 0xffff;
+ bits = "\x00";
+ } else {
+ size = this.pix_size - (i << 16) - i;
+ bits = "\x01";
+ }
+ write(this.buffer, this.idat_offs + 8 + 2 + (i << 16) + (i << 2), bits, byte2lsb(size), byte2lsb(~size));
+ }
+
+ /* Create crc32 lookup table */
+ for (var i = 0; i < 256; i++) {
+ var c = i;
+ for (var j = 0; j < 8; j++) {
+ if (c & 1) {
+ c = -306674912 ^ ((c >> 1) & 0x7fffffff);
+ } else {
+ c = (c >> 1) & 0x7fffffff;
+ }
+ }
+ _crc32[i] = c;
+ }
+
+ // compute the index into a png for a given pixel
+ this.index = function(x,y) {
+ var i = y * (this.width + 1) + x + 1;
+ var j = this.idat_offs + 8 + 2 + 5 * Math.floor((i / 0xffff) + 1) + i;
+ return j;
+ };
+
+ // convert a color and build up the palette
+ this.color = function(red, green, blue, alpha) {
+
+ alpha = alpha >= 0 ? alpha : 255;
+ var color = (((((alpha << 8) | red) << 8) | green) << 8) | blue;
+
+ if (typeof this.palette[color] == "undefined") {
+ if (this.pindex == this.depth) return "\x00";
+
+ var ndx = this.plte_offs + 8 + 3 * this.pindex;
+
+ this.buffer[ndx + 0] = String.fromCharCode(red);
+ this.buffer[ndx + 1] = String.fromCharCode(green);
+ this.buffer[ndx + 2] = String.fromCharCode(blue);
+ this.buffer[this.trns_offs+8+this.pindex] = String.fromCharCode(alpha);
+
+ this.palette[color] = String.fromCharCode(this.pindex++);
+ }
+ return this.palette[color];
+ };
+
+ // output a PNG string, Base64 encoded
+ this.getBase64 = function() {
+
+ var s = this.getDump();
+
+ var ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+ var c1, c2, c3, e1, e2, e3, e4;
+ var l = s.length;
+ var i = 0;
+ var r = "";
+
+ do {
+ c1 = s.charCodeAt(i);
+ e1 = c1 >> 2;
+ c2 = s.charCodeAt(i+1);
+ e2 = ((c1 & 3) << 4) | (c2 >> 4);
+ c3 = s.charCodeAt(i+2);
+ if (l < i+2) { e3 = 64; } else { e3 = ((c2 & 0xf) << 2) | (c3 >> 6); }
+ if (l < i+3) { e4 = 64; } else { e4 = c3 & 0x3f; }
+ r+= ch.charAt(e1) + ch.charAt(e2) + ch.charAt(e3) + ch.charAt(e4);
+ } while ((i+= 3) < l);
+ return r;
+ };
+
+ // output a PNG string
+ this.getDump = function() {
+
+ // compute adler32 of output pixels + row filter bytes
+ var BASE = 65521; /* largest prime smaller than 65536 */
+ var NMAX = 5552; /* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */
+ var s1 = 1;
+ var s2 = 0;
+ var n = NMAX;
+
+ for (var y = 0; y < this.height; y++) {
+ for (var x = -1; x < this.width; x++) {
+ s1+= this.buffer[this.index(x, y)].charCodeAt(0);
+ s2+= s1;
+ if ((n-= 1) == 0) {
+ s1%= BASE;
+ s2%= BASE;
+ n = NMAX;
+ }
+ }
+ }
+ s1%= BASE;
+ s2%= BASE;
+ write(this.buffer, this.idat_offs + this.idat_size - 8, byte4((s2 << 16) | s1));
+
+ // compute crc32 of the PNG chunks
+ function crc32(png, offs, size) {
+ var crc = -1;
+ for (var i = 4; i < size-4; i += 1) {
+ crc = _crc32[(crc ^ png[offs+i].charCodeAt(0)) & 0xff] ^ ((crc >> 8) & 0x00ffffff);
+ }
+ write(png, offs+size-4, byte4(crc ^ -1));
+ }
+
+ crc32(this.buffer, this.ihdr_offs, this.ihdr_size);
+ crc32(this.buffer, this.plte_offs, this.plte_size);
+ crc32(this.buffer, this.trns_offs, this.trns_size);
+ crc32(this.buffer, this.idat_offs, this.idat_size);
+ crc32(this.buffer, this.iend_offs, this.iend_size);
+
+ // convert PNG to string
+ return "\x89PNG\r\n\x1A\n"+this.buffer.join('');
+ };
+
+ this.fillRect = function (x, y, w, h, color) {
+ for(var i = 0; i < w; i++) {
+ for (var j = 0; j < h; j++) {
+ this.buffer[this.index(x+i, y+j)] = color;
+ }
+ }
+ };
+ };
+
+// https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
+ /**
+ * Converts an HSL color value to RGB. Conversion formula
+ * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
+ * Assumes h, s, and l are contained in the set [0, 1] and
+ * returns r, g, and b in the set [0, 255].
+ *
+ * @param {number} h The hue
+ * @param {number} s The saturation
+ * @param {number} l The lightness
+ * @return {Array} The RGB representation
+ */
+
+ function hue2rgb(p, q, t) {
+ if(t < 0) t += 1;
+ if(t > 1) t -= 1;
+ if(t < 1/6) return p + (q - p) * 6 * t;
+ if(t < 1/2) return q;
+ if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+ return p;
+ }
+
+ function hsl2rgb(h, s, l){
+ var r, g, b;
+
+ if(s == 0){
+ r = g = b = l; // achromatic
+ }else{
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ var p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1/3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1/3);
+ }
+
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), 255];
+ }
+
+// The random number is a js implementation of the Xorshift PRNG
+ var randseed = new Array(4); // Xorshift: [x, y, z, w] 32 bit values
+
+ function seedrand(seed) {
+ for (var i = 0; i < randseed.length; i++) {
+ randseed[i] = 0;
+ }
+ for (var i = 0; i < seed.length; i++) {
+ randseed[i % 4] = (randseed[i % 4] << 5) - randseed[i % 4] + seed.charCodeAt(i);
+ }
+ }
+
+ function rand() {
+ // based on Java's String.hashCode(), expanded to 4 32bit values
+ var t = randseed[0] ^ (randseed[0] << 11);
+
+ randseed[0] = randseed[1];
+ randseed[1] = randseed[2];
+ randseed[2] = randseed[3];
+ randseed[3] = randseed[3] ^ (randseed[3] >> 19) ^ t ^ (t >> 8);
+
+ return (randseed[3] >>> 0) / (1 << 31 >>> 0);
+ }
+
+ function createColor() {
+ //saturation is the whole color spectrum
+ var h = Math.floor(rand() * 360);
+ //saturation goes from 40 to 100, it avoids greyish colors
+ var s = rand() * 60 + 40;
+ //lightness can be anything from 0 to 100, but probabilities are a bell curve around 50%
+ var l = (rand() + rand() + rand() + rand()) * 25;
+
+ return [h / 360,s / 100,l / 100];
+ }
+
+ function createImageData(size) {
+ var width = size; // Only support square icons for now
+ var height = size;
+
+ var dataWidth = Math.ceil(width / 2);
+ var mirrorWidth = width - dataWidth;
+
+ var data = [];
+ for (var y = 0; y < height; y++) {
+ var row = [];
+ for (var x = 0; x < dataWidth; x++) {
+ // this makes foreground and background color to have a 43% (1/2.3) probability
+ // spot color has 13% chance
+ row[x] = Math.floor(rand() * 2.3);
+ }
+ var r = row.slice(0, mirrorWidth);
+ r.reverse();
+ row = row.concat(r);
+
+ for (var i = 0; i < row.length; i++) {
+ data.push(row[i]);
+ }
+ }
+
+ return data;
+ }
+
+ function buildOpts(opts) {
+ if (!opts.seed) {
+ throw 'No seed provided'
+ }
+
+ seedrand(opts.seed);
+
+ return Object.assign({
+ size: 8,
+ scale: 16,
+ color: createColor(),
+ bgcolor: createColor(),
+ spotcolor: createColor(),
+ }, opts)
+ }
+
+ function toDataUrl(address) {
+ const opts = buildOpts({seed: address.toLowerCase()});
+
+ const imageData = createImageData(opts.size);
+ const width = Math.sqrt(imageData.length);
+
+ const p = new PNG(opts.size*opts.scale, opts.size*opts.scale, 3);
+ const bgcolor = p.color(...hsl2rgb(...opts.bgcolor));
+ const color = p.color(...hsl2rgb(...opts.color));
+ const spotcolor = p.color(...hsl2rgb(...opts.spotcolor));
+
+ for (var i = 0; i < imageData.length; i++) {
+ var row = Math.floor(i / width);
+ var col = i % width;
+ // if data is 0, leave the background
+ if (imageData[i]) {
+ // if data is 2, choose spot color, if 1 choose foreground
+ const pngColor = imageData[i] == 1 ? color : spotcolor;
+ p.fillRect(col * opts.scale, row * opts.scale, opts.scale, opts.scale, pngColor);
+ }
+ }
+ return `data:image/png;base64,${p.getBase64()}`;
+ }
+
+ exports.toDataUrl = toDataUrl;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+})));
diff --git a/ui/lib/feature-toggle-utils.js b/ui/lib/feature-toggle-utils.js
new file mode 100644
index 000000000..6d4e461ca
--- /dev/null
+++ b/ui/lib/feature-toggle-utils.js
@@ -0,0 +1,11 @@
+function checkFeatureToggle (name) {
+ const queryPairMap = window.location.search.substr(1).split('&')
+ .map(pair => pair.split('='))
+ .reduce((pairs, [key, value]) => ({...pairs, [key]: value }), {})
+ const featureToggles = queryPairMap['ft'] ? queryPairMap['ft'].split(',') : []
+ return Boolean(featureToggles.find(ft => ft === name))
+}
+
+module.exports = {
+ checkFeatureToggle,
+}
diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js
index 27a74de66..31498a3a9 100644
--- a/ui/lib/icon-factory.js
+++ b/ui/lib/icon-factory.js
@@ -53,7 +53,7 @@ function imageElFor (address) {
const path = `images/contract/${fileName}`
const img = document.createElement('img')
img.src = path
- img.style.width = '75%'
+ img.style.width = '100%'
return img
}
diff --git a/ui/lib/is-mobile-view.js b/ui/lib/is-mobile-view.js
new file mode 100644
index 000000000..78fd6cb54
--- /dev/null
+++ b/ui/lib/is-mobile-view.js
@@ -0,0 +1,5 @@
+// Checks if viewport at invoke time fits mobile dimensions
+// isMobileView :: () => Bool
+const isMobileView = () => window.matchMedia('screen and (max-width: 575px)').matches
+
+module.exports = isMobileView
diff --git a/yarn.lock b/yarn.lock
index 428a56c99..f8bd06cab 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -23,6 +23,13 @@
version "6.0.88"
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.88.tgz#f618f11a944f6a18d92b5c472028728a3e3d4b66"
+JSONStream@^0.8.4:
+ version "0.8.4"
+ resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-0.8.4.tgz#91657dfe6ff857483066132b4618b62e8f4887bd"
+ dependencies:
+ jsonparse "0.0.5"
+ through ">=2.2.7 <3"
+
JSONStream@^1.0.3:
version "1.3.1"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a"
@@ -42,6 +49,20 @@ abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
+abi-decoder@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/abi-decoder/-/abi-decoder-1.0.8.tgz#7794d864c4b3fd8bab600bdd445ceb01d333ea43"
+ dependencies:
+ babel-core "^6.23.1"
+ babel-loader "^6.3.2"
+ babel-plugin-add-module-exports "^0.2.1"
+ babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-preset-es2015 "^6.22.0"
+ chai "^3.5.0"
+ mocha "^3.2.0"
+ web3 "^0.18.4"
+ webpack "^2.2.1"
+
abstract-leveldown@2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.7.0.tgz#985052daf3d7d0ac0029dca8eb793f4cdd2a6834"
@@ -68,6 +89,12 @@ accepts@~1.3.3, accepts@~1.3.4:
mime-types "~2.1.16"
negotiator "0.6.1"
+acorn-dynamic-import@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
+ dependencies:
+ acorn "^4.0.3"
+
acorn-globals@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
@@ -104,7 +131,7 @@ after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
-ajv-keywords@^1.0.0:
+ajv-keywords@^1.0.0, ajv-keywords@^1.1.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
@@ -254,6 +281,10 @@ array-filter@~0.0.0:
version "0.0.1"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
+array-find-index@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@@ -340,7 +371,7 @@ assert-plus@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
-assert@^1.4.0:
+assert@^1.1.1, assert@^1.4.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
dependencies:
@@ -350,6 +381,10 @@ assertion-error@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
+ast-types@0.9.6:
+ version "0.9.6"
+ resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
+
astw@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/astw/-/astw-2.2.0.tgz#7bd41784d32493987aeb239b6b4e1c57a873b917"
@@ -369,18 +404,16 @@ async-each@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
-async-eventemitter@^0.2.2:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/async-eventemitter/-/async-eventemitter-0.2.3.tgz#f79f480dfda6645a97bd6142c017150d63b4e70e"
- dependencies:
- async "^2.4.0"
-
-async-eventemitter@ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c:
+async-eventemitter@^0.2.2, async-eventemitter@ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c:
version "0.2.3"
resolved "https://codeload.github.com/ahultgren/async-eventemitter/tar.gz/fa06e39e56786ba541c180061dbf2c0a5bbf951c"
dependencies:
async "^2.4.0"
+async-foreach@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
+
async-reduce@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/async-reduce/-/async-reduce-0.0.1.tgz#b236b5f376d6fae381cded9006aa7f2c73b17f31"
@@ -421,6 +454,28 @@ atob@~1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773"
+autoprefixer@^6.0.0:
+ version "6.7.7"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
+ dependencies:
+ browserslist "^1.7.6"
+ caniuse-db "^1.0.30000634"
+ normalize-range "^0.1.2"
+ num2fraction "^1.2.2"
+ postcss "^5.2.16"
+ postcss-value-parser "^3.2.3"
+
+autoprefixer@^7.0.0, autoprefixer@^7.1.2:
+ version "7.1.4"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.4.tgz#960847dbaa4016bc8e8e52ec891cbf8f1257a748"
+ dependencies:
+ browserslist "^2.4.0"
+ caniuse-lite "^1.0.30000726"
+ normalize-range "^0.1.2"
+ num2fraction "^1.2.2"
+ postcss "^6.0.11"
+ postcss-value-parser "^3.2.3"
+
await-semaphore@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.2.tgz#0ceba1bdb2cfc537496032f167bd8b7dedb97493"
@@ -449,7 +504,7 @@ babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
esutils "^2.0.2"
js-tokens "^3.0.2"
-babel-core@^6.0.14, babel-core@^6.24.1, babel-core@^6.26.0:
+babel-core@^6.0.14, babel-core@^6.23.1, babel-core@^6.24.1, babel-core@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
dependencies:
@@ -636,6 +691,15 @@ babel-helpers@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
+babel-loader@^6.3.2:
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-6.4.1.tgz#0b34112d5b0748a8dcdbf51acf6f9bd42d50b8ca"
+ dependencies:
+ find-cache-dir "^0.1.1"
+ loader-utils "^0.2.16"
+ mkdirp "^0.5.1"
+ object-assign "^4.0.1"
+
babel-messages@7.0.0-beta.0:
version "7.0.0-beta.0"
resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-7.0.0-beta.0.tgz#6df01296e49fc8fbd0637394326a167f36da817b"
@@ -646,6 +710,10 @@ babel-messages@^6.23.0:
dependencies:
babel-runtime "^6.22.0"
+babel-plugin-add-module-exports@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz#9ae9a1f4a8dc67f0cdec4f4aeda1e43a5ff65e25"
+
babel-plugin-check-es2015-constants@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
@@ -1255,6 +1323,10 @@ backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+balanced-match@^0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
+
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -1263,6 +1335,10 @@ base-x@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/base-x/-/base-x-1.1.0.tgz#42d3d717474f9ea02207f6d1aa1f426913eeb7ac"
+base62@0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/base62/-/base62-0.1.1.tgz#7b4174c2f94449753b11c2651c083da841a7b084"
+
base64-arraybuffer@0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
@@ -1322,6 +1398,18 @@ better-assert@~1.0.0:
dependencies:
callsite "1.0.0"
+big.js@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978"
+
+bignumber.js@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.0.4.tgz#7c40f5abcd2d6623ab7b99682ee7db81b11889a4"
+
+"bignumber.js@git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2":
+ version "2.0.7"
+ resolved "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2"
+
"bignumber.js@git+https://github.com/frozeman/bignumber.js-nolookahead.git":
version "2.0.7"
resolved "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934"
@@ -1382,7 +1470,7 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
-bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.4.6, bluebird@^3.5.0:
+bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.4.6, bluebird@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
@@ -1456,6 +1544,12 @@ bops@0.0.6:
base64-js "0.0.2"
to-utf8 "0.0.1"
+boron@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/boron/-/boron-0.2.3.tgz#63a1800771c0cb2b0d8f616687c62c1248cfb8a0"
+ dependencies:
+ domkit "^0.0.1"
+
brace-expansion@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
@@ -1587,7 +1681,7 @@ browserify-unibabel@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/browserify-unibabel/-/browserify-unibabel-3.0.0.tgz#5a6b8f0f704ce388d3927df47337e25830f71dda"
-browserify-zlib@~0.1.2:
+browserify-zlib@^0.1.4, browserify-zlib@~0.1.2:
version "0.1.4"
resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
dependencies:
@@ -1645,7 +1739,14 @@ browserify@^14.0.0, browserify@^14.4.0:
vm-browserify "~0.0.1"
xtend "^4.0.0"
-browserslist@^2.1.2:
+browserslist@^1.1.1, browserslist@^1.1.3, browserslist@^1.7.6:
+ version "1.7.7"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9"
+ dependencies:
+ caniuse-db "^1.0.30000639"
+ electron-to-chromium "^1.2.7"
+
+browserslist@^2.1.2, browserslist@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.4.0.tgz#693ee93d01e66468a6348da5498e011f578f87f8"
dependencies:
@@ -1681,6 +1782,14 @@ buffer-xor@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+buffer@^4.3.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
buffer@^5.0.2:
version "5.0.7"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.0.7.tgz#570a290b625cf2603290c1149223d27ccf04db97"
@@ -1738,11 +1847,18 @@ callsites@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+camelcase-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+ dependencies:
+ camelcase "^2.0.0"
+ map-obj "^1.0.0"
+
camelcase@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
-camelcase@^2.0.1:
+camelcase@^2.0.0, camelcase@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
@@ -1754,10 +1870,18 @@ camelcase@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+caniuse-db@^1.0.30000187, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
+ version "1.0.30000727"
+ resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000727.tgz#4e22593089b0f35c1b2adcfc28234493a21a4b2e"
+
caniuse-lite@^1.0.30000718:
version "1.0.30000733"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000733.tgz#ebfc48254117cc0c66197a4536cb4397a6cfbccd"
+caniuse-lite@^1.0.30000726:
+ version "1.0.30000727"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000727.tgz#20c895768398ded5f98a4beab4a76c285def41d2"
+
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@@ -1769,7 +1893,7 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
-"chai@>=1.9.2 <4.0.0":
+"chai@>=1.9.2 <4.0.0", chai@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247"
dependencies:
@@ -1812,7 +1936,7 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chalk@^2.0.0, chalk@^2.1.0:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e"
dependencies:
@@ -1861,7 +1985,7 @@ cheerio@^0.22.0:
lodash.reject "^4.4.0"
lodash.some "^4.4.0"
-chokidar@^1.0.0, chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.1:
+chokidar@^1.0.0, chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.1, chokidar@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
dependencies:
@@ -1931,6 +2055,13 @@ cliui@^3.0.3, cliui@^3.2.0:
strip-ansi "^3.0.1"
wrap-ansi "^2.0.0"
+clone-regexp@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-1.0.0.tgz#eae0a2413f55c0942f818c229fefce845d7f3b1c"
+ dependencies:
+ is-regexp "^1.0.0"
+ is-supported-regexp-flag "^1.0.0"
+
clone-stats@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
@@ -1976,6 +2107,10 @@ color-convert@^1.3.0, color-convert@^1.9.0:
dependencies:
color-name "^1.1.1"
+color-diff@^0.1.3:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/color-diff/-/color-diff-0.1.7.tgz#6db78cd9482a8e459d40821eaf4b503283dcb8e2"
+
color-name@^1.0.0, color-name@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
@@ -1994,6 +2129,21 @@ color@^0.11.1:
color-convert "^1.3.0"
color-string "^0.3.0"
+colorguard@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/colorguard/-/colorguard-1.2.0.tgz#f3facaf5caaeba4ef54653d9fb25bb73177c0d84"
+ dependencies:
+ chalk "^1.1.1"
+ color-diff "^0.1.3"
+ log-symbols "^1.0.2"
+ object-assign "^4.0.1"
+ pipetteur "^2.0.0"
+ plur "^2.0.0"
+ postcss "^5.0.4"
+ postcss-reporter "^1.2.1"
+ text-table "^0.2.0"
+ yargs "^1.2.6"
+
colors@1.0.3, colors@1.0.x:
version "1.0.3"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
@@ -2023,10 +2173,16 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
-commander@2.11.0, commander@^2.6.0, commander@~2.11.0:
+commander@2.11.0, commander@^2.5.0, commander@^2.6.0, commander@^2.9.0, commander@~2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
+commander@2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
+ dependencies:
+ graceful-readlink ">= 1.0.0"
+
commondir@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-0.0.1.tgz#89f00fdcd51b519c578733fec563e6a6da7f5be2"
@@ -2035,6 +2191,20 @@ commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+commoner@^0.10.0:
+ version "0.10.8"
+ resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5"
+ dependencies:
+ commander "^2.5.0"
+ detective "^4.3.1"
+ glob "^5.0.15"
+ graceful-fs "^4.1.2"
+ iconv-lite "^0.4.5"
+ mkdirp "^0.5.0"
+ private "^0.1.6"
+ q "^1.1.2"
+ recast "^0.11.17"
+
commonmark-react-renderer@^4.2.4:
version "4.3.3"
resolved "https://registry.yarnpkg.com/commonmark-react-renderer/-/commonmark-react-renderer-4.3.3.tgz#9c4bca138bc83287bae792ccf133738be9cbc6fa"
@@ -2127,7 +2297,7 @@ consolidate@^0.14.0:
dependencies:
bluebird "^3.1.1"
-constants-browserify@~1.0.0:
+constants-browserify@^1.0.0, constants-browserify@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@@ -2184,6 +2354,18 @@ core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+cosmiconfig@^2.1.1, cosmiconfig@^2.1.3:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892"
+ dependencies:
+ is-directory "^0.3.1"
+ js-yaml "^3.4.3"
+ minimist "^1.2.0"
+ object-assign "^4.1.0"
+ os-homedir "^1.0.1"
+ parse-json "^2.2.0"
+ require-from-string "^1.1.0"
+
coveralls@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.0.0.tgz#22ef730330538080d29b8c151dc9146afde88a99"
@@ -2229,6 +2411,13 @@ create-react-class@^15.5.2, create-react-class@^15.6.0:
loose-envify "^1.3.1"
object-assign "^4.1.1"
+cross-spawn@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
+ dependencies:
+ lru-cache "^4.0.1"
+ which "^1.2.9"
+
cross-spawn@^4:
version "4.0.2"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41"
@@ -2250,7 +2439,7 @@ cryptiles@2.x.x:
dependencies:
boom "2.x.x"
-crypto-browserify@^3.0.0:
+crypto-browserify@^3.0.0, crypto-browserify@^3.11.0:
version "3.11.1"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f"
dependencies:
@@ -2269,6 +2458,29 @@ crypto-js@^3.1.4:
version "3.1.8"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.8.tgz#715f070bf6014f2ae992a98b3929258b713f08d5"
+css-color-list@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/css-color-list/-/css-color-list-0.0.1.tgz#8718e8695ae7a2cc8787be8715f1c008a7f28b15"
+ dependencies:
+ css-color-names "0.0.1"
+
+css-color-names@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.1.tgz#5d0548fa256456ede4a9a0c2ac7ab19d3eb1ad81"
+
+css-color-names@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.3.tgz#de0cef16f4d8aa8222a320d5b6d7e9bbada7b9f6"
+
+css-rule-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/css-rule-stream/-/css-rule-stream-1.1.0.tgz#3786e7198983d965a26e31957e09078cbb7705a2"
+ dependencies:
+ css-tokenize "^1.0.1"
+ duplexer2 "0.0.2"
+ ldjson-stream "^1.2.1"
+ through2 "^0.6.3"
+
css-select@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
@@ -2278,6 +2490,13 @@ css-select@~1.2.0:
domutils "1.5.1"
nth-check "~1.0.1"
+css-tokenize@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/css-tokenize/-/css-tokenize-1.0.1.tgz#4625cb1eda21c143858b7f81d6803c1d26fc14be"
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^1.0.33"
+
css-what@2.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
@@ -2307,6 +2526,12 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
dependencies:
cssom "0.3.x"
+currently-unhandled@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+ dependencies:
+ array-find-index "^1.0.1"
+
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@@ -2377,7 +2602,7 @@ debug@2.3.3:
dependencies:
ms "0.7.2"
-debug@2.6.8, debug@^2.1.0, debug@^2.2.0, debug@^2.6.3, debug@^2.6.8:
+debug@2.6.8, debug@^2.1.0, debug@^2.2.0, debug@^2.6.0, debug@^2.6.3, debug@^2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
@@ -2395,13 +2620,13 @@ debug@3.1.0:
dependencies:
ms "2.0.0"
-debug@^3.0.1:
+debug@^3.0.0, debug@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64"
dependencies:
ms "2.0.0"
-decamelize@^1.0.0, decamelize@^1.1.1:
+decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -2569,7 +2794,7 @@ detect-node@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127"
-detective@^4.0.0:
+detective@^4.0.0, detective@^4.3.1:
version "4.5.0"
resolved "https://registry.yarnpkg.com/detective/-/detective-4.5.0.tgz#6e5a8c6b26e6c7a254b1c6b6d7490d98ec91edd1"
dependencies:
@@ -2580,6 +2805,10 @@ di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
+diff@3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
+
diff@3.3.1, diff@^3.1.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
@@ -2634,6 +2863,23 @@ doctrine@^2.0.0:
esutils "^2.0.2"
isarray "^1.0.0"
+doiuse@^2.4.1:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/doiuse/-/doiuse-2.6.0.tgz#1892d10b61a9a356addbf2b614933e81f8bb3834"
+ dependencies:
+ browserslist "^1.1.1"
+ caniuse-db "^1.0.30000187"
+ css-rule-stream "^1.1.0"
+ duplexer2 "0.0.2"
+ jsonfilter "^1.1.2"
+ ldjson-stream "^1.2.1"
+ lodash "^4.0.0"
+ multimatch "^2.0.0"
+ postcss "^5.0.8"
+ source-map "^0.4.2"
+ through2 "^0.6.3"
+ yargs "^3.5.4"
+
dom-helpers@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a"
@@ -2658,7 +2904,7 @@ dom-walk@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
-domain-browser@~1.1.0:
+domain-browser@^1.1.1, domain-browser@~1.1.0:
version "1.1.7"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
@@ -2676,6 +2922,10 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
+domkit@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/domkit/-/domkit-0.0.1.tgz#88399d586794efc1154fec6c22cfe50f19bd4dbb"
+
domutils@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
@@ -2736,11 +2986,21 @@ ecc-jsbn@~0.1.1:
dependencies:
jsbn "~0.1.0"
+editorconfig@^0.13.2:
+ version "0.13.3"
+ resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.13.3.tgz#e5219e587951d60958fd94ea9a9a008cdeff1b34"
+ dependencies:
+ bluebird "^3.0.5"
+ commander "^2.9.0"
+ lru-cache "^3.2.0"
+ semver "^5.1.0"
+ sigmund "^1.0.1"
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
-electron-to-chromium@^1.3.18:
+electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.18:
version "1.3.21"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.21.tgz#a967ebdcfe8ed0083fc244d1894022a8e8113ea2"
@@ -2756,6 +3016,10 @@ elliptic@^6.0.0, elliptic@^6.2.3:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
+emojis-list@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+
encodeurl@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
@@ -2850,6 +3114,15 @@ engine.io@1.8.3:
engine.io-parser "1.3.2"
ws "1.1.2"
+enhanced-resolve@^3.3.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.4.0"
+ object-assign "^4.0.1"
+ tapable "^0.2.7"
+
ensnare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ensnare/-/ensnare-1.0.0.tgz#72d2bf7ef48aba21f66adf29d00a0904eddb61c7"
@@ -2886,7 +3159,7 @@ enzyme@^2.8.2:
prop-types "^15.5.10"
uuid "^3.0.1"
-errno@~0.1.1:
+errno@^0.1.3, errno@~0.1.1:
version "0.1.4"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
dependencies:
@@ -3101,6 +3374,10 @@ espree@^3.5.1:
acorn "^5.1.1"
acorn-jsx "^3.0.0"
+esprima-fb@13001.1001.0-dev-harmony-fb:
+ version "13001.1001.0-dev-harmony-fb"
+ resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-13001.1001.0-dev-harmony-fb.tgz#633acdb40d9bd4db8a1c1d68c06a942959fad2b0"
+
esprima-fb@3001.1.0-dev-harmony-fb:
version "3001.1.0-dev-harmony-fb"
resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz#b77d37abcd38ea0b77426bb8bc2922ce6b426411"
@@ -3109,7 +3386,7 @@ esprima@2.7.x, esprima@^2.7.1:
version "2.7.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
-esprima@^3.1.3:
+esprima@^3.1.3, esprima@~3.1.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
@@ -3206,7 +3483,7 @@ eth-block-tracker@^2.2.0, eth-block-tracker@^2.2.2:
pify "^2.3.0"
tape "^4.6.3"
-eth-contract-metadata@^1.1.4:
+eth-contract-metadata@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/eth-contract-metadata/-/eth-contract-metadata-1.1.5.tgz#301f51b0460b8dd044997dc05870751fb7f4cfcb"
@@ -3350,7 +3627,7 @@ ethereum-ens-network-map@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.0.tgz#43cd7669ce950a789e151001118d4d65f210eeb7"
-"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git":
+ethereumjs-abi@^0.6.4, "ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git":
version "0.6.4"
resolved "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee6ded67235a98f3ef4ae2a338aee70a9f68fe20"
dependencies:
@@ -3545,7 +3822,7 @@ ethjs-util@^0.1.3:
is-hex-prefixed "1.0.0"
strip-hex-prefix "1.0.0"
-ethjs@^0.2.7:
+ethjs@^0.2.7, ethjs@^0.2.8:
version "0.2.8"
resolved "https://registry.yarnpkg.com/ethjs/-/ethjs-0.2.8.tgz#65ed276c5e58e89d51d4573585b7a16142ccf8f0"
dependencies:
@@ -3591,7 +3868,7 @@ events-to-array@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/events-to-array/-/events-to-array-1.1.2.tgz#2d41f563e1fe400ed4962fe1a4d5c6a7539df7f6"
-events@^1.1.1, events@~1.1.0:
+events@^1.0.0, events@^1.1.1, events@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
@@ -3614,6 +3891,12 @@ execa@^0.7.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
+execall@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execall/-/execall-1.0.0.tgz#73d0904e395b3cab0658b08d09ec25307f29bb73"
+ dependencies:
+ clone-regexp "^1.0.0"
+
expand-braces@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea"
@@ -4007,6 +4290,10 @@ flatten@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-0.0.1.tgz#554440766da0a0d603999f433453f6c2fc6a75c1"
+flatten@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+
flush-write-stream@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
@@ -4172,6 +4459,14 @@ functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+fuse.js@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.1.0.tgz#9062146c471552189b0f678b4f5a155731ae3b3c"
+
+gather-stream@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/gather-stream/-/gather-stream-1.0.0.tgz#b33994af457a8115700d410f317733cbe7a0904b"
+
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -4185,6 +4480,12 @@ gauge@~2.7.3:
strip-ansi "^3.0.1"
wide-align "^1.1.0"
+gaze@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105"
+ dependencies:
+ globule "^1.0.0"
+
get-caller-file@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
@@ -4197,6 +4498,14 @@ get-stdin@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-3.0.2.tgz#c1ced24b9039b38ded85bdf161e57713b6dd4abe"
+get-stdin@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+
+get-stdin@^5.0.0, get-stdin@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
+
get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -4268,7 +4577,18 @@ glob-watcher@^3.0.0:
lodash.debounce "^4.0.6"
object.defaults "^1.0.0"
-glob@7.1.2, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.0.6, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@~7.1.2:
+glob@7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.2"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.0.6, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -4331,7 +4651,7 @@ globby@^5.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
-globby@^6.1.0:
+globby@^6.0.0, globby@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
dependencies:
@@ -4341,6 +4661,18 @@ globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
+globjoin@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
+
+globule@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09"
+ dependencies:
+ glob "~7.1.1"
+ lodash "~4.17.4"
+ minimatch "~3.0.2"
+
glogg@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.0.tgz#7fe0f199f57ac906cf512feead8f90ee4a284fc5"
@@ -4351,14 +4683,32 @@ graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, gr
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+"graceful-readlink@>= 1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+
growl@1.10.3:
version "1.10.3"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f"
+growl@1.9.2:
+ version "1.9.2"
+ resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
+
growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+gulp-autoprefixer@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/gulp-autoprefixer/-/gulp-autoprefixer-4.0.0.tgz#e00a8c571b85d06516ac26341be90dfd9fc1eab0"
+ dependencies:
+ autoprefixer "^7.0.0"
+ gulp-util "^3.0.0"
+ postcss "^6.0.1"
+ through2 "^2.0.0"
+ vinyl-sourcemaps-apply "^0.2.0"
+
gulp-cli@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-1.4.0.tgz#6f5bbe2cd0bdb4849d12cf9e1246a5861f8b4f88"
@@ -4432,6 +4782,16 @@ gulp-replace@^0.6.1:
readable-stream "^2.0.1"
replacestream "^4.0.0"
+gulp-sass@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-3.1.0.tgz#53dc4b68a1f5ddfe4424ab4c247655269a8b74b7"
+ dependencies:
+ gulp-util "^3.0"
+ lodash.clonedeep "^4.3.2"
+ node-sass "^4.2.0"
+ through2 "^2.0.0"
+ vinyl-sourcemaps-apply "^0.2.0"
+
gulp-sourcemaps@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz#b86ff349d801ceb56e1d9e7dc7bbcb4b7dee600c"
@@ -4459,7 +4819,30 @@ gulp-sourcemaps@^2.6.0:
through2 "2.X"
vinyl "1.X"
-gulp-util@^3.0.0, gulp-util@^3.0.2, gulp-util@^3.0.7, gulp-util@^3.0.8, gulp-util@~3.0.0:
+gulp-stylefmt@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/gulp-stylefmt/-/gulp-stylefmt-1.1.0.tgz#7aea00a0a9bd2fbd8a2482dc01f133edca303d52"
+ dependencies:
+ gulp-util "^3.0.7"
+ postcss "^5.0.21"
+ postcss-scss "^0.4.0"
+ stylefmt "^5.0.4"
+ through2 "^2.0.1"
+
+gulp-stylelint@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/gulp-stylelint/-/gulp-stylelint-4.0.0.tgz#440fa7e6c447e92644700e1e2a06a73e6e457750"
+ dependencies:
+ chalk "^2.0.1"
+ deep-extend "^0.5.0"
+ gulp-util "^3.0.8"
+ mkdirp "^0.5.1"
+ promise "^8.0.1"
+ strip-ansi "^4.0.0"
+ stylelint "^8.0.0"
+ through2 "^2.0.3"
+
+gulp-util@^3.0, gulp-util@^3.0.0, gulp-util@^3.0.2, gulp-util@^3.0.7, gulp-util@^3.0.8, gulp-util@~3.0.0:
version "3.0.8"
resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f"
dependencies:
@@ -4694,6 +5077,10 @@ html-select@^2.3.5:
stream-splicer "^1.2.0"
through2 "^1.0.0"
+html-tags@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b"
+
html-tokenize@^1.1.1:
version "1.2.5"
resolved "https://registry.yarnpkg.com/html-tokenize/-/html-tokenize-1.2.5.tgz#7e5ba99ecb51ef906ec9a7fcdee6ca3267c7897e"
@@ -4753,6 +5140,10 @@ http-signature@~1.1.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
+https-browserify@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
+
https-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
@@ -4769,7 +5160,7 @@ iconv-lite@0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
-iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
+iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@^0.4.5, iconv-lite@~0.4.13:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
@@ -4803,7 +5194,7 @@ iframe@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/iframe/-/iframe-1.0.0.tgz#58e74822b178a0579d09cd169640fb9537470ef5"
-ignore@^3.3.3:
+ignore@^3.2.0, ignore@^3.3.3:
version "3.3.5"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6"
@@ -4823,6 +5214,16 @@ in-publish@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51"
+indent-string@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+ dependencies:
+ repeating "^2.0.0"
+
+indexes-of@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+
indexof@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
@@ -4949,6 +5350,10 @@ is-date-object@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+is-directory@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
+
is-dotfile@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
@@ -5067,6 +5472,10 @@ is-regex@^1.0.4:
dependencies:
has "^1.0.1"
+is-regexp@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
+
is-relative@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5"
@@ -5087,6 +5496,10 @@ is-subset@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+is-supported-regexp-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.0.tgz#8b520c85fae7a253382d4b02652e045576e13bb8"
+
is-symbol@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572"
@@ -5237,6 +5650,10 @@ jazzicon@^1.2.0:
mersenne-twister "^1.0.1"
raphael "^2.2.0"
+js-base64@^2.1.8, js-base64@^2.1.9:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.1.tgz#3705897c35fce0e202132630e750d8a17cd220ec"
+
js-beautify@~1.5.4:
version "1.5.10"
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.5.10.tgz#4d95371702699344a516ca26bf59f0a27bb75719"
@@ -5261,7 +5678,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-js-yaml@3.x, js-yaml@^3.2.5, js-yaml@^3.2.7, js-yaml@^3.6.1, js-yaml@^3.9.1:
+js-yaml@3.x, js-yaml@^3.2.5, js-yaml@^3.2.7, js-yaml@^3.4.3, js-yaml@^3.6.1, js-yaml@^3.9.1:
version "3.10.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
dependencies:
@@ -5325,22 +5742,26 @@ jshint-stylish@~2.2.1:
string-length "^1.0.0"
text-table "^0.2.0"
-json-rpc-engine@^3.0.1:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-3.1.0.tgz#09285363372857569d75f61df6591b1b0afb0758"
+json-loader@^0.5.4:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
+
+json-rpc-engine@3.2.0, json-rpc-engine@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-3.2.0.tgz#d34dff106c8339c337a894da801f73b1f77b1bc8"
dependencies:
async "^2.0.1"
babel-preset-env "^1.3.2"
babelify "^7.3.0"
+ json-rpc-error "^2.0.0"
-json-rpc-engine@^3.1.0, json-rpc-engine@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-3.2.0.tgz#d34dff106c8339c337a894da801f73b1f77b1bc8"
+json-rpc-engine@^3.0.1:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-3.1.0.tgz#09285363372857569d75f61df6591b1b0afb0758"
dependencies:
async "^2.0.1"
babel-preset-env "^1.3.2"
babelify "^7.3.0"
- json-rpc-error "^2.0.0"
json-rpc-engine@^3.4.0:
version "3.4.0"
@@ -5400,7 +5821,7 @@ json3@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
-json5@^0.5.1:
+json5@^0.5.0, json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
@@ -5410,10 +5831,23 @@ jsonfile@^2.1.0:
optionalDependencies:
graceful-fs "^4.1.6"
+jsonfilter@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/jsonfilter/-/jsonfilter-1.1.2.tgz#21ef7cedc75193813c75932e96a98be205ba5a11"
+ dependencies:
+ JSONStream "^0.8.4"
+ minimist "^1.1.0"
+ stream-combiner "^0.2.1"
+ through2 "^0.6.3"
+
jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+jsonparse@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-0.0.5.tgz#330542ad3f0a654665b778f3eb2d9a9fa507ac64"
+
jsonparse@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
@@ -5427,6 +5861,14 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
+jstransform@^10.1.0:
+ version "10.1.0"
+ resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-10.1.0.tgz#b4c49bf63f162c108b0348399a8737c713b0a83a"
+ dependencies:
+ base62 "0.1.1"
+ esprima-fb "13001.1001.0-dev-harmony-fb"
+ source-map "0.1.31"
+
jsx-ast-utils@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
@@ -5525,6 +5967,14 @@ klaw@^1.0.0:
optionalDependencies:
graceful-fs "^4.1.9"
+known-css-properties@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.2.0.tgz#899c94be368e55b42d7db8d5be7d73a4a4a41454"
+
+known-css-properties@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.3.0.tgz#a3d135bbfc60ee8c6eacf2f7e7e6f2d4755e49a4"
+
labeled-stream-splicer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz#a52e1d138024c00b86b1c0c91f677918b8ae0a59"
@@ -5560,6 +6010,13 @@ lcov-parse@^0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-0.0.10.tgz#1b0b8ff9ac9c7889250582b70b71315d9da6d9a3"
+ldjson-stream@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/ldjson-stream/-/ldjson-stream-1.2.1.tgz#91beceda5ac4ed2b17e649fb777e7abfa0189c2b"
+ dependencies:
+ split2 "^0.2.1"
+ through2 "^0.6.1"
+
leftpad@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/leftpad/-/leftpad-0.0.0.tgz#020c9ad0787216ba0f30d79d479b4b355d7d39c3"
@@ -5658,6 +6115,19 @@ load-json-file@^2.0.0:
pify "^2.0.0"
strip-bom "^3.0.0"
+loader-runner@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
+
+loader-utils@^0.2.16:
+ version "0.2.17"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+ object-assign "^4.0.1"
+
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -5680,6 +6150,10 @@ lodash._basecopy@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
+lodash._basecreate@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821"
+
lodash._baseflatten@^3.0.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/lodash._baseflatten/-/lodash._baseflatten-3.1.4.tgz#0770ff80131af6e34f3b511796a7ba5214e65ff7"
@@ -5751,10 +6225,18 @@ lodash.bind@^4.1.4:
version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
-lodash.clonedeep@^4.4.1:
+lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.4.1:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+lodash.create@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7"
+ dependencies:
+ lodash._baseassign "^3.0.0"
+ lodash._basecreate "^3.0.0"
+ lodash._isiterateecall "^3.0.0"
+
lodash.debounce@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-3.1.1.tgz#812211c378a94cc29d5aa4e3346cf0bfce3a7df5"
@@ -5858,6 +6340,10 @@ lodash.merge@^4.4.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
+lodash.mergewith@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
+
lodash.pick@^4.2.1:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
@@ -5915,7 +6401,7 @@ lodash@^3.8.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@~4.17.2:
+lodash@^4.0.0, lodash@^4.1.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@~4.17.2, lodash@~4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -5923,12 +6409,18 @@ log-driver@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056"
-log-symbols@^1.0.0:
+log-symbols@^1.0.0, log-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
dependencies:
chalk "^1.0.0"
+log-symbols@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.0.0.tgz#595e63be4d5c8cbf294a9e09e0d5629f5913fc0c"
+ dependencies:
+ chalk "^2.0.1"
+
log4js@^0.6.31:
version "0.6.38"
resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd"
@@ -5958,10 +6450,23 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
dependencies:
js-tokens "^3.0.0"
+loud-rejection@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+ dependencies:
+ currently-unhandled "^0.4.1"
+ signal-exit "^3.0.0"
+
lru-cache@2.2.x:
version "2.2.4"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
+lru-cache@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-3.2.0.tgz#71789b3b7f5399bec8565dda38aa30d2a097efee"
+ dependencies:
+ pseudomap "^1.0.1"
+
lru-cache@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
@@ -5993,6 +6498,10 @@ map-cache@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+map-obj@^1.0.0, map-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+
map-stream@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
@@ -6006,6 +6515,10 @@ matchdep@^1.0.0:
resolve "~1.1.6"
stack-trace "0.0.9"
+mathml-tag-names@^2.0.0, mathml-tag-names@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.0.1.tgz#8d41268168bf86d1102b98109e28e531e7a34578"
+
md5-hex@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-1.3.0.tgz#d2c4afe983c4370662179b8cad145219135046c4"
@@ -6060,10 +6573,32 @@ memoizee@^0.4.5:
next-tick "1"
timers-ext "^0.1.2"
+memory-fs@^0.4.0, memory-fs@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
memorystream@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
+meow@^3.3.0, meow@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+ dependencies:
+ camelcase-keys "^2.0.0"
+ decamelize "^1.1.2"
+ loud-rejection "^1.0.0"
+ map-obj "^1.0.1"
+ minimist "^1.1.3"
+ normalize-package-data "^2.3.4"
+ object-assign "^4.0.1"
+ read-pkg-up "^1.0.1"
+ redent "^1.0.0"
+ trim-newlines "^1.0.0"
+
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -6200,7 +6735,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
-"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
@@ -6268,6 +6803,23 @@ mocha-sinon@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mocha-sinon/-/mocha-sinon-2.0.0.tgz#723a9310e7d737d7b77c7a66821237425b032d48"
+mocha@^3.2.0:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d"
+ dependencies:
+ browser-stdout "1.3.0"
+ commander "2.9.0"
+ debug "2.6.8"
+ diff "3.2.0"
+ escape-string-regexp "1.0.5"
+ glob "7.1.1"
+ growl "1.9.2"
+ he "1.1.1"
+ json3 "3.3.2"
+ lodash.create "3.1.1"
+ mkdirp "0.5.1"
+ supports-color "3.1.2"
+
mocha@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b"
@@ -6315,6 +6867,15 @@ ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+multimatch@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b"
+ dependencies:
+ array-differ "^1.0.0"
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ minimatch "^3.0.0"
+
multipipe@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b"
@@ -6351,7 +6912,7 @@ mz@^2.6.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
-nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0:
+nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0, nan@^2.3.2:
version "2.7.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
@@ -6410,6 +6971,52 @@ node-fetch@^1.0.1, node-fetch@~1.7.1:
encoding "^0.1.11"
is-stream "^1.0.1"
+node-gyp@^3.3.1:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
+ dependencies:
+ fstream "^1.0.0"
+ glob "^7.0.3"
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ mkdirp "^0.5.0"
+ nopt "2 || 3"
+ npmlog "0 || 1 || 2 || 3 || 4"
+ osenv "0"
+ request "2"
+ rimraf "2"
+ semver "~5.3.0"
+ tar "^2.0.0"
+ which "1"
+
+node-libs-browser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646"
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.1.4"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^1.0.0"
+ https-browserify "0.0.1"
+ os-browserify "^0.2.0"
+ path-browserify "0.0.0"
+ process "^0.11.0"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.0.5"
+ stream-browserify "^2.0.1"
+ stream-http "^2.3.1"
+ string_decoder "^0.10.25"
+ timers-browserify "^2.0.2"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.10.3"
+ vm-browserify "0.0.4"
+
node-notifier@^5.0.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff"
@@ -6434,11 +7041,34 @@ node-pre-gyp@^0.6.36:
tar "^2.2.1"
tar-pack "^3.4.0"
+node-sass@^4.2.0:
+ version "4.5.3"
+ resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.3.tgz#d09c9d1179641239d1b97ffc6231fdcec53e1568"
+ dependencies:
+ async-foreach "^0.1.3"
+ chalk "^1.1.1"
+ cross-spawn "^3.0.0"
+ gaze "^1.0.0"
+ get-stdin "^4.0.1"
+ glob "^7.0.3"
+ in-publish "^2.0.0"
+ lodash.assign "^4.2.0"
+ lodash.clonedeep "^4.3.2"
+ lodash.mergewith "^4.6.0"
+ meow "^3.7.0"
+ mkdirp "^0.5.1"
+ nan "^2.3.2"
+ node-gyp "^3.3.1"
+ npmlog "^4.0.0"
+ request "^2.79.0"
+ sass-graph "^2.1.1"
+ stdout-stream "^1.4.0"
+
noop-logger@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
-nopt@3.x, nopt@~3.0.1:
+"nopt@2 || 3", nopt@3.x, nopt@~3.0.1:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
dependencies:
@@ -6451,7 +7081,7 @@ nopt@^4.0.1:
abbrev "1"
osenv "^0.1.4"
-normalize-package-data@^2.3.2:
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
version "2.4.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
dependencies:
@@ -6466,6 +7096,14 @@ normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
dependencies:
remove-trailing-separator "^1.0.1"
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+
+normalize-selector@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03"
+
now-and-later@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.0.tgz#bc61cbb456d79cb32207ce47ca05136ff2e7d6ee"
@@ -6478,7 +7116,7 @@ npm-run-path@^2.0.0:
dependencies:
path-key "^2.0.0"
-npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.0.2:
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.0.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
dependencies:
@@ -6497,6 +7135,10 @@ null-check@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd"
+num2fraction@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+
number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -6669,6 +7311,10 @@ once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.3.3, once@^1.4.0:
dependencies:
wrappy "1"
+onecolor@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.0.4.tgz#75a46f80da6c7aaa5b4daae17a47198bd9652494"
+
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
@@ -6716,6 +7362,10 @@ ordered-read-streams@^0.3.0:
is-stream "^1.0.1"
readable-stream "^2.0.1"
+os-browserify@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
+
os-browserify@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.1.2.tgz#49ca0293e0b19590a5f5de10c7f265a617d8fe54"
@@ -6742,7 +7392,7 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
-osenv@^0.1.4:
+osenv@0, osenv@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
dependencies:
@@ -6867,7 +7517,7 @@ pascalcase@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
-path-browserify@~0.0.0:
+path-browserify@0.0.0, path-browserify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
@@ -6989,6 +7639,13 @@ pinkie@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+pipetteur@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/pipetteur/-/pipetteur-2.0.3.tgz#1955760959e8d1a11cb2a50ec83eec470633e49f"
+ dependencies:
+ onecolor "^3.0.4"
+ synesthesia "^1.0.1"
+
pkg-dir@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
@@ -7007,7 +7664,7 @@ plucker@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/plucker/-/plucker-0.0.0.tgz#2ffa24e03ab2cffa4e75adc1df70f25623c45d09"
-plur@^2.1.0:
+plur@^2.0.0, plur@^2.1.0, plur@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/plur/-/plur-2.1.2.tgz#7482452c1a0f508e3e344eaec312c91c29dc655a"
dependencies:
@@ -7043,6 +7700,101 @@ post-message-stream@^3.0.0:
dependencies:
readable-stream "^2.1.4"
+postcss-less@^0.14.0:
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-0.14.0.tgz#c631b089c6cce422b9a10f3a958d2bedd3819324"
+ dependencies:
+ postcss "^5.0.21"
+
+postcss-less@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-1.1.0.tgz#bdcc76be64c4324d873fbc5cd9fa2e799e4305fa"
+ dependencies:
+ postcss "^5.2.16"
+
+postcss-media-query-parser@^0.2.0, postcss-media-query-parser@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
+
+postcss-reporter@^1.2.1, postcss-reporter@^1.3.3:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-1.4.1.tgz#c136f0a5b161915f379dd3765c61075f7e7b9af2"
+ dependencies:
+ chalk "^1.0.0"
+ lodash "^4.1.0"
+ log-symbols "^1.0.2"
+ postcss "^5.0.0"
+
+postcss-reporter@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-3.0.0.tgz#09ea0f37a444c5693878606e09b018ebeff7cf8f"
+ dependencies:
+ chalk "^1.0.0"
+ lodash "^4.1.0"
+ log-symbols "^1.0.2"
+ postcss "^5.0.0"
+
+postcss-reporter@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-5.0.0.tgz#a14177fd1342829d291653f2786efd67110332c3"
+ dependencies:
+ chalk "^2.0.1"
+ lodash "^4.17.4"
+ log-symbols "^2.0.0"
+ postcss "^6.0.8"
+
+postcss-resolve-nested-selector@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e"
+
+postcss-scss@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-0.4.1.tgz#ad771b81f0f72f5f4845d08aa60f93557653d54c"
+ dependencies:
+ postcss "^5.2.13"
+
+postcss-scss@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-1.0.2.tgz#ff45cf3354b879ee89a4eb68680f46ac9bb14f94"
+ dependencies:
+ postcss "^6.0.3"
+
+postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.1.1, postcss-selector-parser@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90"
+ dependencies:
+ flatten "^1.0.2"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss-sorting@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-2.1.0.tgz#32b1e9afa913bb225a6ad076d503d8f983bb4a82"
+ dependencies:
+ lodash "^4.17.4"
+ postcss "^5.2.17"
+
+postcss-value-parser@^3.1.1, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
+
+postcss@^5.0.0, postcss@^5.0.18, postcss@^5.0.20, postcss@^5.0.21, postcss@^5.0.4, postcss@^5.0.8, postcss@^5.2.13, postcss@^5.2.16, postcss@^5.2.17, postcss@^5.2.4, postcss@^5.2.5:
+ version "5.2.17"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b"
+ dependencies:
+ chalk "^1.1.3"
+ js-base64 "^2.1.9"
+ source-map "^0.5.6"
+ supports-color "^3.2.3"
+
+postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.3, postcss@^6.0.6, postcss@^6.0.8:
+ version "6.0.11"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.11.tgz#f48db210b1d37a7f7ab6499b7a54982997ab6f72"
+ dependencies:
+ chalk "^2.1.0"
+ source-map "^0.5.7"
+ supports-color "^4.4.0"
+
prebuild-install@^2.0.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.2.2.tgz#dd47c4d61f3754fb17bbf601759e5922e16e0671"
@@ -7082,7 +7834,7 @@ printf@^0.2.3:
version "0.2.5"
resolved "https://registry.yarnpkg.com/printf/-/printf-0.2.5.tgz#c438ca2ca33e3927671db4ab69c0e52f936a4f0f"
-private@^0.1.6, private@^0.1.7:
+private@^0.1.6, private@^0.1.7, private@~0.1.5:
version "0.1.7"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
@@ -7090,7 +7842,7 @@ process-nextick-args@^1.0.7, process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
-process@~0.11.0:
+process@^0.11.0, process@~0.11.0:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@@ -7121,6 +7873,12 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
+promise@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.1.tgz#e45d68b00a17647b6da711bf85ed6ed47208f450"
+ dependencies:
+ asap "~2.0.3"
+
prompt@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prompt/-/prompt-1.0.0.tgz#8e57123c396ab988897fb327fd3aedc3e735e4fe"
@@ -7177,7 +7935,7 @@ prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
-pseudomap@^1.0.2:
+pseudomap@^1.0.1, pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@@ -7210,7 +7968,7 @@ punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
-punycode@^1.3.2, punycode@^1.4.1:
+punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
@@ -7218,6 +7976,10 @@ punycode@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
+q@^1.1.2:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
+
qjobs@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73"
@@ -7246,7 +8008,7 @@ qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
-querystring-es3@~0.2.0:
+querystring-es3@^0.2.0, querystring-es3@~0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -7423,6 +8185,13 @@ react-testutils-additions@^15.2.0:
object-assign "3.0.0"
sizzle "2.3.3"
+react-tools@~0.13.0:
+ version "0.13.3"
+ resolved "https://registry.yarnpkg.com/react-tools/-/react-tools-0.13.3.tgz#da6ac7d4d7777a59a5e951cf46e72fd4b6b40a2c"
+ dependencies:
+ commoner "^0.10.0"
+ jstransform "^10.1.0"
+
react-tooltip-component@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/react-tooltip-component/-/react-tooltip-component-0.3.0.tgz#fb3ec78c3270fe919692bc31f1404108bcf4785e"
@@ -7437,6 +8206,17 @@ react-transition-group@^1.2.0:
prop-types "^15.5.6"
warning "^3.0.0"
+react-transition-group@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.0.tgz#793bf8cb15bfe91b3101b24bce1c1d2891659575"
+ dependencies:
+ chain-function "^1.0.0"
+ classnames "^2.2.5"
+ dom-helpers "^3.2.0"
+ loose-envify "^1.3.1"
+ prop-types "^15.5.8"
+ warning "^3.0.0"
+
react-trigger-change@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/react-trigger-change/-/react-trigger-change-1.0.2.tgz#af573398ecef2475362b84f8c08c07fea23914c3"
@@ -7451,6 +8231,19 @@ react-trigger-change@^1.0.2:
object-assign "^4.1.0"
prop-types "^15.5.10"
+reactify@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/reactify/-/reactify-1.1.1.tgz#a8f119596273c0d4bfb1abea0c14c2601ea03bba"
+ dependencies:
+ react-tools "~0.13.0"
+ through "~2.3.4"
+
+read-file-stdin@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/read-file-stdin/-/read-file-stdin-0.2.1.tgz#25eccff3a153b6809afacb23ee15387db9e0ee61"
+ dependencies:
+ gather-stream "^1.0.0"
+
read-only-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0"
@@ -7549,6 +8342,15 @@ readdirp@^2.0.0:
readable-stream "^2.0.2"
set-immediate-shim "^1.0.1"
+recast@^0.11.17:
+ version "0.11.23"
+ resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3"
+ dependencies:
+ ast-types "0.9.6"
+ esprima "~3.1.0"
+ private "~0.1.5"
+ source-map "~0.5.0"
+
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
@@ -7564,12 +8366,23 @@ recompose@^0.25.0:
hoist-non-react-statics "^1.0.0"
symbol-observable "^1.0.4"
+redent@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+ dependencies:
+ indent-string "^2.1.0"
+ strip-indent "^1.0.1"
+
redux-logger@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf"
dependencies:
deep-diff "^0.3.5"
+redux-test-utils@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/redux-test-utils/-/redux-test-utils-0.1.3.tgz#0d89100f100f86c7c7214976eaece88e7e45bf74"
+
redux-thunk@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"
@@ -7694,7 +8507,7 @@ request-promise@^4.2.1:
stealthy-require "^1.1.0"
tough-cookie ">=2.3.3"
-request@^2.67.0, request@^2.79.0, request@^2.81.0:
+request@2, request@^2.67.0, request@^2.79.0, request@^2.81.0:
version "2.81.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
dependencies:
@@ -7759,6 +8572,10 @@ resolve-from@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57"
+resolve-from@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+
resolve-url@~0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -7855,6 +8672,15 @@ sandwich-expando@^1.1.3:
react-dom "^15.0.2"
react-hyperscript "^2.4.0"
+sass-graph@^2.1.1:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
+ dependencies:
+ glob "^7.0.0"
+ lodash "^4.0.0"
+ scss-tokenizer "^0.2.3"
+ yargs "^7.0.0"
+
sax@^1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -7886,6 +8712,13 @@ scryptsy@^1.2.1:
dependencies:
pbkdf2 "^3.0.3"
+scss-tokenizer@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
+ dependencies:
+ js-base64 "^2.1.8"
+ source-map "^0.4.2"
+
secp256k1@^3.0.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.3.0.tgz#50ec9b201ba401403dd13ccbf21d31eeb3ff43cf"
@@ -7910,7 +8743,7 @@ semver-greatest-satisfied-range@^1.0.0:
dependencies:
sver-compat "^1.5.0"
-"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@~5.4.1:
+"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@~5.4.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
@@ -7918,6 +8751,10 @@ semver@~4.3.3:
version "4.3.6"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
+semver@~5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+
send@0.15.4:
version "0.15.4"
resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9"
@@ -7980,7 +8817,7 @@ set-immediate-shim@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
-setimmediate@^1.0.5:
+setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
@@ -8004,7 +8841,7 @@ sha3@^1.1.0:
dependencies:
nan "^2.0.5"
-shallow-copy@~0.0.1:
+shallow-copy@0.0.1, shallow-copy@~0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170"
@@ -8038,6 +8875,10 @@ shellwords@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+sigmund@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
+
signal-exit@^3.0.0, signal-exit@^3.0.1, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -8169,6 +9010,10 @@ solc@^0.4.2:
semver "^5.3.0"
yargs "^4.7.1"
+source-list-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
+
source-map-resolve@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.3.1.tgz#610f6122a445b8dd51535a2a71b783dfc1248761"
@@ -8188,7 +9033,13 @@ source-map-url@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9"
-source-map@0.X, "source-map@>= 0.1.2", source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3, source-map@~0.5.6:
+source-map@0.1.31:
+ version "0.1.31"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.31.tgz#9f704d0d69d9e138a81badf6ebb4fde33d151c61"
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@0.X, "source-map@>= 0.1.2", source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3, source-map@~0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -8198,7 +9049,7 @@ source-map@^0.1.38, source-map@~0.1.33:
dependencies:
amdefine ">=0.0.4"
-source-map@^0.4.4:
+source-map@^0.4.2, source-map@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
dependencies:
@@ -8243,6 +9094,16 @@ spdx-license-ids@^1.0.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+specificity@^0.3.0, specificity@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.3.1.tgz#f1b068424ce317ae07478d95de3c21cf85e8d567"
+
+split2@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/split2/-/split2-0.2.1.tgz#02ddac9adc03ec0bb78c1282ec079ca6e85ae900"
+ dependencies:
+ through2 "~0.6.1"
+
split@0.3, split@~0.3.0:
version "0.3.3"
resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f"
@@ -8301,11 +9162,21 @@ statuses@1, "statuses@>= 1.3.1 < 2", statuses@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+stdin@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/stdin/-/stdin-0.0.1.tgz#d3041981aaec3dfdbc77a1b38d6372e38f5fb71e"
+
+stdout-stream@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b"
+ dependencies:
+ readable-stream "^2.0.1"
+
stealthy-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
-stream-browserify@^2.0.0:
+stream-browserify@^2.0.0, stream-browserify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
dependencies:
@@ -8319,6 +9190,13 @@ stream-combiner2@^1.1.1:
duplexer2 "~0.1.0"
readable-stream "^2.0.2"
+stream-combiner@^0.2.1:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858"
+ dependencies:
+ duplexer "~0.1.1"
+ through "~2.3.4"
+
stream-combiner@~0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
@@ -8336,7 +9214,7 @@ stream-exhaust@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d"
-stream-http@^2.0.0:
+stream-http@^2.0.0, stream-http@^2.3.1:
version "2.7.2"
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad"
dependencies:
@@ -8401,7 +9279,7 @@ string.prototype.trim@~1.1.2:
es-abstract "^1.5.0"
function-bind "^1.0.2"
-string_decoder@~0.10.x:
+string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -8471,21 +9349,186 @@ strip-hex-prefix@1.0.0:
dependencies:
is-hex-prefixed "1.0.0"
+strip-indent@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+ dependencies:
+ get-stdin "^4.0.1"
+
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+style-search@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
+
styled_string@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/styled_string/-/styled_string-0.0.1.tgz#d22782bd81295459bc4f1df18c4bad8e94dd124a"
+stylefmt@^5.0.4:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/stylefmt/-/stylefmt-5.3.2.tgz#32013437aa54d8c5253cbc107ac914dfb5ee9eea"
+ dependencies:
+ chalk "^1.1.3"
+ css-color-list "0.0.1"
+ diff "^3.1.0"
+ editorconfig "^0.13.2"
+ globby "^6.1.0"
+ minimist "^1.2.0"
+ postcss "^5.2.5"
+ postcss-scss "^0.4.0"
+ postcss-sorting "^2.0.1"
+ postcss-value-parser "^3.3.0"
+ stdin "0.0.1"
+ stylelint "^7.5.0"
+ stylelint-order "0.4.x"
+
+stylehacks@^2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-2.3.2.tgz#64c83e0438a68c9edf449e8c552a7d9ab6009b0b"
+ dependencies:
+ browserslist "^1.1.3"
+ chalk "^1.1.1"
+ log-symbols "^1.0.2"
+ minimist "^1.2.0"
+ plur "^2.1.2"
+ postcss "^5.0.18"
+ postcss-reporter "^1.3.3"
+ postcss-selector-parser "^2.0.0"
+ read-file-stdin "^0.2.1"
+ text-table "^0.2.0"
+ write-file-stdout "0.0.2"
+
+stylelint-config-recommended@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-1.0.0.tgz#752c17fc68fa64cd5e7589e24f6e46e77e14a735"
+
+stylelint-config-standard@^17.0.0:
+ version "17.0.0"
+ resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-17.0.0.tgz#42103a090054ee2a3dde9ecaed55e5d4d9d059fc"
+ dependencies:
+ stylelint-config-recommended "^1.0.0"
+
+stylelint-order@0.4.x:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-0.4.4.tgz#db7dfca0541b5062010c7e2e21e745791fc088ac"
+ dependencies:
+ lodash "^4.17.4"
+ postcss "^5.2.16"
+ stylelint "^7.9.0"
+
+stylelint@^7.5.0, stylelint@^7.9.0:
+ version "7.13.0"
+ resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-7.13.0.tgz#111f97b6da72e775c80800d6bb6f5f869997785d"
+ dependencies:
+ autoprefixer "^6.0.0"
+ balanced-match "^0.4.0"
+ chalk "^2.0.1"
+ colorguard "^1.2.0"
+ cosmiconfig "^2.1.1"
+ debug "^2.6.0"
+ doiuse "^2.4.1"
+ execall "^1.0.0"
+ file-entry-cache "^2.0.0"
+ get-stdin "^5.0.0"
+ globby "^6.0.0"
+ globjoin "^0.1.4"
+ html-tags "^2.0.0"
+ ignore "^3.2.0"
+ imurmurhash "^0.1.4"
+ known-css-properties "^0.2.0"
+ lodash "^4.17.4"
+ log-symbols "^1.0.2"
+ mathml-tag-names "^2.0.0"
+ meow "^3.3.0"
+ micromatch "^2.3.11"
+ normalize-selector "^0.2.0"
+ pify "^2.3.0"
+ postcss "^5.0.20"
+ postcss-less "^0.14.0"
+ postcss-media-query-parser "^0.2.0"
+ postcss-reporter "^3.0.0"
+ postcss-resolve-nested-selector "^0.1.1"
+ postcss-scss "^0.4.0"
+ postcss-selector-parser "^2.1.1"
+ postcss-value-parser "^3.1.1"
+ resolve-from "^3.0.0"
+ specificity "^0.3.0"
+ string-width "^2.0.0"
+ style-search "^0.1.0"
+ stylehacks "^2.3.2"
+ sugarss "^0.2.0"
+ svg-tags "^1.0.0"
+ table "^4.0.1"
+
+stylelint@^8.0.0:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-8.1.1.tgz#9feeed699598b27427715551ed7786db341c19ad"
+ dependencies:
+ autoprefixer "^7.1.2"
+ balanced-match "^1.0.0"
+ chalk "^2.0.1"
+ cosmiconfig "^2.1.3"
+ debug "^3.0.0"
+ execall "^1.0.0"
+ file-entry-cache "^2.0.0"
+ get-stdin "^5.0.1"
+ globby "^6.1.0"
+ globjoin "^0.1.4"
+ html-tags "^2.0.0"
+ ignore "^3.3.3"
+ imurmurhash "^0.1.4"
+ known-css-properties "^0.3.0"
+ lodash "^4.17.4"
+ log-symbols "^2.0.0"
+ mathml-tag-names "^2.0.1"
+ meow "^3.7.0"
+ micromatch "^2.3.11"
+ normalize-selector "^0.2.0"
+ pify "^3.0.0"
+ postcss "^6.0.6"
+ postcss-less "^1.1.0"
+ postcss-media-query-parser "^0.2.3"
+ postcss-reporter "^5.0.0"
+ postcss-resolve-nested-selector "^0.1.1"
+ postcss-scss "^1.0.2"
+ postcss-selector-parser "^2.2.3"
+ postcss-value-parser "^3.3.0"
+ resolve-from "^3.0.0"
+ specificity "^0.3.1"
+ string-width "^2.1.0"
+ style-search "^0.1.0"
+ sugarss "^1.0.0"
+ svg-tags "^1.0.0"
+ table "^4.0.1"
+
subarg@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2"
dependencies:
minimist "^1.1.0"
-supports-color@4.4.0, supports-color@^4.0.0:
+sugarss@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-0.2.0.tgz#ac34237563327c6ff897b64742bf6aec190ad39e"
+ dependencies:
+ postcss "^5.2.4"
+
+sugarss@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-1.0.0.tgz#65e51b3958432fb70d5451a68bb33e32d0cf1ef7"
+ dependencies:
+ postcss "^6.0.0"
+
+supports-color@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
+ dependencies:
+ has-flag "^1.0.0"
+
+supports-color@4.4.0, supports-color@^4.0.0, supports-color@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
dependencies:
@@ -8499,7 +9542,7 @@ supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
-supports-color@^3.1.0, supports-color@^3.1.2:
+supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
dependencies:
@@ -8512,6 +9555,10 @@ sver-compat@^1.5.0:
es6-iterator "^2.0.1"
es6-symbol "^3.1.1"
+svg-tags@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
+
sw-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/sw-stream/-/sw-stream-2.0.0.tgz#628ebbeeb9eee0b66b03ec52fc55fcc4eeb23cf3"
@@ -8529,6 +9576,12 @@ symbol-tree@^3.2.1:
version "3.2.2"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
+synesthesia@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/synesthesia/-/synesthesia-1.0.1.tgz#5ef95ea548c0d5c6e6f9bb4b0d0731dff864a777"
+ dependencies:
+ css-color-names "0.0.3"
+
syntax-error@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.3.0.tgz#1ed9266c4d40be75dc55bf9bb1cb77062bb96ca1"
@@ -8555,6 +9608,10 @@ tap-parser@^5.1.0:
optionalDependencies:
readable-stream "^2"
+tapable@^0.2.7, tapable@~0.2.5:
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
+
tape@^4.4.0, tape@^4.5.1, tape@^4.6.0, tape@^4.6.2, tape@^4.6.3:
version "4.8.0"
resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e"
@@ -8604,7 +9661,7 @@ tar-stream@^1.1.2:
readable-stream "^2.0.0"
xtend "^4.0.0"
-tar@^2.2.1:
+tar@^2.0.0, tar@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
dependencies:
@@ -8704,7 +9761,7 @@ through2@2.X, through2@^2.0.0, through2@^2.0.1, through2@^2.0.3, through2@~2.0.0
readable-stream "^2.1.5"
xtend "~4.0.1"
-through2@^0.6.0, through2@^0.6.1, through2@^0.6.5:
+through2@^0.6.0, through2@^0.6.1, through2@^0.6.3, through2@^0.6.5, through2@~0.6.1:
version "0.6.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
dependencies:
@@ -8756,6 +9813,12 @@ timers-browserify@^1.0.1:
dependencies:
process "~0.11.0"
+timers-browserify@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6"
+ dependencies:
+ setimmediate "^1.0.4"
+
timers-ext@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.2.tgz#61cc47a76c1abd3195f14527f978d58ae94c5204"
@@ -8831,6 +9894,10 @@ traverse@~0.6.3:
version "0.6.6"
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
+trim-newlines@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+
trim-right@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
@@ -8854,7 +9921,7 @@ tryit@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
-tty-browserify@~0.0.0:
+tty-browserify@0.0.0, tty-browserify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -8908,7 +9975,7 @@ uglify-es@^3.0.15:
commander "~2.11.0"
source-map "~0.5.1"
-uglify-js@^2.6:
+uglify-js@^2.6, uglify-js@^2.8.27:
version "2.8.29"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
dependencies:
@@ -8969,7 +10036,7 @@ undertaker@^1.0.0:
object.reduce "^1.0.0"
undertaker-registry "^1.0.0"
-uniq@^1.0.0:
+uniq@^1.0.0, uniq@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
@@ -8996,7 +10063,7 @@ urix@^0.1.0, urix@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
-url@~0.11.0:
+url@^0.11.0, url@~0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
dependencies:
@@ -9022,7 +10089,7 @@ util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-util@0.10.3, util@~0.10.1:
+util@0.10.3, util@^0.10.3, util@~0.10.1:
version "0.10.3"
resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
dependencies:
@@ -9143,6 +10210,12 @@ vinyl-source-stream@^1.1.0:
through2 "^0.6.1"
vinyl "^0.4.3"
+vinyl-sourcemaps-apply@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705"
+ dependencies:
+ source-map "^0.5.1"
+
vinyl@1.X, vinyl@^1.0.0, vinyl@^1.1.0, vinyl@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884"
@@ -9166,7 +10239,7 @@ vinyl@^0.5.0:
clone-stats "^0.0.1"
replace-ext "0.0.1"
-vm-browserify@~0.0.1:
+vm-browserify@0.0.4, vm-browserify@~0.0.1:
version "0.0.4"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
dependencies:
@@ -9198,6 +10271,14 @@ watchify@^3.9.0:
through2 "^2.0.0"
xtend "^4.0.0"
+watchpack@^1.3.1:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
+ dependencies:
+ async "^2.1.2"
+ chokidar "^1.7.0"
+ graceful-fs "^4.1.2"
+
weak@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/weak/-/weak-1.0.1.tgz#ab99aab30706959aa0200cb8cf545bb9cb33b99e"
@@ -9235,6 +10316,16 @@ web3-stream-provider@^3.0.1:
dependencies:
readable-stream "^2.0.5"
+web3@^0.18.4:
+ version "0.18.4"
+ resolved "https://registry.yarnpkg.com/web3/-/web3-0.18.4.tgz#81ec1784145491f2eaa8955b31c06049e07c5e7d"
+ dependencies:
+ bignumber.js "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2"
+ crypto-js "^3.1.4"
+ utf8 "^2.1.1"
+ xhr2 "*"
+ xmlhttprequest "*"
+
web3@^0.20.1:
version "0.20.2"
resolved "https://registry.yarnpkg.com/web3/-/web3-0.20.2.tgz#c54dac5fc0e377399c04c1a6ecbb12e4513278d6"
@@ -9249,6 +10340,39 @@ webidl-conversions@^4.0.0, webidl-conversions@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+webpack-sources@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.5.3"
+
+webpack@^2.2.1:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.7.0.tgz#b2a1226804373ffd3d03ea9c6bd525067034f6b1"
+ dependencies:
+ acorn "^5.0.0"
+ acorn-dynamic-import "^2.0.0"
+ ajv "^4.7.0"
+ ajv-keywords "^1.1.1"
+ async "^2.1.2"
+ enhanced-resolve "^3.3.0"
+ interpret "^1.0.0"
+ json-loader "^0.5.4"
+ json5 "^0.5.1"
+ loader-runner "^2.3.0"
+ loader-utils "^0.2.16"
+ memory-fs "~0.4.1"
+ mkdirp "~0.5.0"
+ node-libs-browser "^2.0.0"
+ source-map "^0.5.3"
+ supports-color "^3.1.0"
+ tapable "~0.2.5"
+ uglify-js "^2.8.27"
+ watchpack "^1.3.1"
+ webpack-sources "^1.0.1"
+ yargs "^6.0.0"
+
websocket-driver@>=0.3.6:
version "0.7.0"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb"
@@ -9286,7 +10410,7 @@ which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
-which@^1.0.5, which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.4, which@^1.2.9:
+which@1, which@^1.0.5, which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.4, which@^1.2.9:
version "1.3.0"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
dependencies:
@@ -9364,6 +10488,10 @@ write-file-atomic@^1.1.4:
imurmurhash "^0.1.4"
slide "^1.1.5"
+write-file-stdout@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/write-file-stdout/-/write-file-stdout-0.0.2.tgz#c252d7c7c5b1b402897630e3453c7bfe690d9ca1"
+
write@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
@@ -9472,7 +10600,11 @@ yargs-parser@^7.0.0:
dependencies:
camelcase "^4.1.0"
-yargs@^3.28.0:
+yargs@^1.2.6:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.3.3.tgz#054de8b61f22eefdb7207059eaef9d6b83fb931a"
+
+yargs@^3.28.0, yargs@^3.5.4:
version "3.32.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"
dependencies:
@@ -9503,7 +10635,7 @@ yargs@^4.7.1:
y18n "^3.2.1"
yargs-parser "^2.4.1"
-yargs@^6.5.0:
+yargs@^6.0.0, yargs@^6.5.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
dependencies:
@@ -9521,6 +10653,24 @@ yargs@^6.5.0:
y18n "^3.2.1"
yargs-parser "^4.2.0"
+yargs@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
+ dependencies:
+ camelcase "^3.0.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^1.4.0"
+ read-pkg-up "^1.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^1.0.2"
+ which-module "^1.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^5.0.0"
+
yargs@^8.0.1:
version "8.0.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"