diff --git a/.travis.yml b/.travis.yml index e0c9554a..8da34db3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,8 @@ before_install: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start +install: + - npm install --silent notifications: email: @@ -16,4 +18,6 @@ notifications: script: - npm run test - npm run tslint -- tsc --noEmit +- npm run tscheck +- npm run freezer +- npm run freezer:validate diff --git a/README.md b/README.md index d0fe51a7..90841522 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,36 @@ When working on a module that has styling in Less, try to do the following: * Convert as many `
` tags or ` `s to margins * Ensure that there has been little to no deviation from screenshot - +#### Adding Icon-fonts +1. Download chosen icon-font +1. Declare css font-family: + ``` + @font-face { + font-family: 'social-media'; + src: url('../assets/fonts/social-media.eot'); + src: url('../assets/fonts/social-media.eot') format('embedded-opentype'), + url('../assets/fonts/social-media.woff2') format('woff2'), + url('../assets/fonts/social-media.woff') format('woff'), + url('../assets/fonts/social-media.ttf') format('truetype'), + url('../assets/fonts/social-media.svg') format('svg'); + font-weight: normal; + font-style: normal; + } + ``` +1. Create classes for each icon using their unicode character + ``` + .sm-logo-facebook:before { + content: '\ea02'; + } + ``` + * [How to get unicode icon values?](https://stackoverflow.com/questions/27247145/get-the-unicode-icon-value-from-a-custom-font) +1. Write some markup: + ``` + + + Hello World + + ``` ## Thanks & Support diff --git a/common/Root.tsx b/common/Root.tsx index c0c139c8..a531d9a4 100644 --- a/common/Root.tsx +++ b/common/Root.tsx @@ -11,6 +11,7 @@ import Swap from 'containers/Tabs/Swap'; import ViewWallet from 'containers/Tabs/ViewWallet'; import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage'; import BroadcastTx from 'containers/Tabs/BroadcastTx'; +import RestoreKeystore from 'containers/Tabs/RestoreKeystore'; // TODO: fix this interface Props { @@ -33,12 +34,12 @@ export default class Root extends Component { + - diff --git a/common/actions/config/actionCreators.ts b/common/actions/config/actionCreators.ts index 8b189127..dcfea5c4 100644 --- a/common/actions/config/actionCreators.ts +++ b/common/actions/config/actionCreators.ts @@ -1,6 +1,6 @@ import * as interfaces from './actionTypes'; import { TypeKeys } from './constants'; -import { NodeConfig, CustomNodeConfig } from 'config/data'; +import { NodeConfig, CustomNodeConfig, CustomNetworkConfig } from 'config/data'; export type TForceOfflineConfig = typeof forceOfflineConfig; export function forceOfflineConfig(): interfaces.ForceOfflineAction { @@ -80,6 +80,26 @@ export function removeCustomNode( }; } +export type TAddCustomNetwork = typeof addCustomNetwork; +export function addCustomNetwork( + payload: CustomNetworkConfig +): interfaces.AddCustomNetworkAction { + return { + type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK, + payload + }; +} + +export type TRemoveCustomNetwork = typeof removeCustomNetwork; +export function removeCustomNetwork( + payload: CustomNetworkConfig +): interfaces.RemoveCustomNetworkAction { + return { + type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK, + payload + }; +} + export type TSetLatestBlock = typeof setLatestBlock; export function setLatestBlock( payload: string diff --git a/common/actions/config/actionTypes.ts b/common/actions/config/actionTypes.ts index e8b27135..7e72703d 100644 --- a/common/actions/config/actionTypes.ts +++ b/common/actions/config/actionTypes.ts @@ -1,5 +1,5 @@ import { TypeKeys } from './constants'; -import { CustomNodeConfig, NodeConfig } from 'config/data'; +import { NodeConfig, CustomNodeConfig, CustomNetworkConfig } from 'config/data'; /*** Toggle Offline ***/ export interface ToggleOfflineAction { @@ -56,6 +56,18 @@ export interface RemoveCustomNodeAction { payload: CustomNodeConfig; } +/*** Add Custom Network ***/ +export interface AddCustomNetworkAction { + type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK; + payload: CustomNetworkConfig; +} + +/*** Remove Custom Network ***/ +export interface RemoveCustomNetworkAction { + type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK; + payload: CustomNetworkConfig; +} + /*** Set Latest Block ***/ export interface SetLatestBlockAction { type: TypeKeys.CONFIG_SET_LATEST_BLOCK; @@ -78,5 +90,7 @@ export type ConfigAction = | ChangeNodeIntentAction | AddCustomNodeAction | RemoveCustomNodeAction + | AddCustomNetworkAction + | RemoveCustomNetworkAction | SetLatestBlockAction | Web3UnsetNodeAction; diff --git a/common/actions/config/constants.ts b/common/actions/config/constants.ts index d11471ac..519f2ba6 100644 --- a/common/actions/config/constants.ts +++ b/common/actions/config/constants.ts @@ -8,6 +8,8 @@ export enum TypeKeys { CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS', CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE', CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE', + CONFIG_ADD_CUSTOM_NETWORK = 'CONFIG_ADD_CUSTOM_NETWORK', + CONFIG_REMOVE_CUSTOM_NETWORK = 'CONFIG_REMOVE_CUSTOM_NETWORK', CONFIG_SET_LATEST_BLOCK = 'CONFIG_SET_LATEST_BLOCK', CONFIG_NODE_WEB3_UNSET = 'CONFIG_NODE_WEB3_UNSET' } diff --git a/common/actions/notifications/actionCreators.ts b/common/actions/notifications/actionCreators.ts index 75c0a735..bfda0fa3 100644 --- a/common/actions/notifications/actionCreators.ts +++ b/common/actions/notifications/actionCreators.ts @@ -6,7 +6,7 @@ export type TShowNotification = typeof showNotification; export function showNotification( level: types.NOTIFICATION_LEVEL = 'info', msg: ReactElement | string, - duration?: number | types.INFINITY + duration?: number ): types.ShowNotificationAction { return { type: TypeKeys.SHOW_NOTIFICATION, diff --git a/common/actions/notifications/actionTypes.ts b/common/actions/notifications/actionTypes.ts index 16de73a2..f6b70117 100644 --- a/common/actions/notifications/actionTypes.ts +++ b/common/actions/notifications/actionTypes.ts @@ -2,13 +2,12 @@ import { ReactElement } from 'react'; import { TypeKeys } from './constants'; /*** Shared types ***/ export type NOTIFICATION_LEVEL = 'danger' | 'warning' | 'success' | 'info'; -export type INFINITY = 'infinity'; export interface Notification { level: NOTIFICATION_LEVEL; msg: ReactElement | string; id: number; - duration?: number | INFINITY; + duration?: number; } /*** Close notification ***/ diff --git a/common/actions/rates/actionPayloads.ts b/common/actions/rates/actionPayloads.ts index 9256049a..7ba9d9cf 100644 --- a/common/actions/rates/actionPayloads.ts +++ b/common/actions/rates/actionPayloads.ts @@ -26,6 +26,11 @@ export const fetchRates = (symbols: string[] = []): Promise => fetch(CCRates(symbols)) .then(response => handleJSONResponse(response, ERROR_MESSAGE)) .then(rates => { + // API errors come as 200s, so check the json for error + if (rates.Response && rates.Response === 'Error') { + throw new Error('Failed to fetch rates'); + } + // All currencies are in ETH right now. We'll do token -> eth -> value to // do it all in one request // to their respective rates via ETH. diff --git a/common/actions/swap/actionTypes.ts b/common/actions/swap/actionTypes.ts index 75ef19f4..6f81de04 100644 --- a/common/actions/swap/actionTypes.ts +++ b/common/actions/swap/actionTypes.ts @@ -69,14 +69,14 @@ export interface BityOrderCreateRequestedSwapAction { }; } -interface BityOrderInput { +export interface BityOrderInput { amount: string; currency: string; reference: string; status: string; } -interface BityOrderOutput { +export interface BityOrderOutput { amount: string; currency: string; reference: string; diff --git a/common/actions/wallet/actionCreators.ts b/common/actions/wallet/actionCreators.ts index 8acf32dc..f69305f3 100644 --- a/common/actions/wallet/actionCreators.ts +++ b/common/actions/wallet/actionCreators.ts @@ -71,7 +71,10 @@ export function setBalanceRejected(): types.SetBalanceRejectedAction { export type TSetTokenBalances = typeof setTokenBalances; export function setTokenBalances(payload: { - [key: string]: TokenValue; + [key: string]: { + balance: TokenValue; + error: string | null; + }; }): types.SetTokenBalancesAction { return { type: TypeKeys.WALLET_SET_TOKEN_BALANCES, diff --git a/common/actions/wallet/actionTypes.ts b/common/actions/wallet/actionTypes.ts index 1eb64b50..e11aab99 100644 --- a/common/actions/wallet/actionTypes.ts +++ b/common/actions/wallet/actionTypes.ts @@ -48,7 +48,10 @@ export interface SetBalanceRejectedAction { export interface SetTokenBalancesAction { type: TypeKeys.WALLET_SET_TOKEN_BALANCES; payload: { - [key: string]: TokenValue; + [key: string]: { + balance: TokenValue; + error: string | null; + }; }; } diff --git a/common/api/bity.ts b/common/api/bity.ts index dc66c24d..21989135 100644 --- a/common/api/bity.ts +++ b/common/api/bity.ts @@ -3,8 +3,8 @@ import { checkHttpStatus, parseJSON } from './utils'; export function getAllRates() { const mappedRates = {}; - return _getAllRates().then((bityRates) => { - bityRates.objects.forEach((each) => { + return _getAllRates().then(bityRates => { + bityRates.objects.forEach(each => { const pairName = each.pair; mappedRates[pairName] = parseFloat(each.rate_we_sell); }); @@ -26,7 +26,7 @@ export function postOrder( mode, pair }), - headers: bityConfig.postConfig.headers + headers: new Headers(bityConfig.postConfig.headers) }) .then(checkHttpStatus) .then(parseJSON); @@ -38,7 +38,7 @@ export function getOrderStatus(orderId: string) { body: JSON.stringify({ orderid: orderId }), - headers: bityConfig.postConfig.headers + headers: new Headers(bityConfig.postConfig.headers) }) .then(checkHttpStatus) .then(parseJSON); @@ -48,4 +48,4 @@ function _getAllRates() { return fetch(`${bityConfig.bityURL}/v1/rate2/`) .then(checkHttpStatus) .then(parseJSON); -} \ No newline at end of file +} diff --git a/common/assets/fonts/social-media.eot b/common/assets/fonts/social-media.eot new file mode 100644 index 00000000..b17b3ff3 Binary files /dev/null and b/common/assets/fonts/social-media.eot differ diff --git a/common/assets/fonts/social-media.svg b/common/assets/fonts/social-media.svg new file mode 100644 index 00000000..a903a42f --- /dev/null +++ b/common/assets/fonts/social-media.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/common/assets/fonts/social-media.ttf b/common/assets/fonts/social-media.ttf new file mode 100644 index 00000000..ea56ac6c Binary files /dev/null and b/common/assets/fonts/social-media.ttf differ diff --git a/common/assets/fonts/social-media.woff b/common/assets/fonts/social-media.woff new file mode 100644 index 00000000..d4d0e518 Binary files /dev/null and b/common/assets/fonts/social-media.woff differ diff --git a/common/assets/fonts/social-media.woff2 b/common/assets/fonts/social-media.woff2 new file mode 100644 index 00000000..f2acafc8 Binary files /dev/null and b/common/assets/fonts/social-media.woff2 differ diff --git a/common/assets/images/icon-help-3.svg b/common/assets/images/icon-help-3.svg new file mode 100644 index 00000000..7def12b7 --- /dev/null +++ b/common/assets/images/icon-help-3.svg @@ -0,0 +1 @@ + diff --git a/common/assets/styles/etherwallet-custom.less b/common/assets/styles/etherwallet-custom.less index 4ab73d56..0e10a4de 100755 --- a/common/assets/styles/etherwallet-custom.less +++ b/common/assets/styles/etherwallet-custom.less @@ -135,52 +135,6 @@ textarea { } } -.account-help-icon { - h3, - h4, - h5, - h6, - img { - display: inline-block; - } - img:hover + .account-help-text { - display: block; - } - img { - margin-left: -30px; - margin-right: 3px; - } -} - -.account-help-text { - background: white; - border-radius: @border-radius; - border: 1px solid #cdcdcd; - box-shadow: 0 0 @space-sm rgba(100, 100, 100, .2); - display: none; - font-size: @font-size-xs; - font-weight: 400; - padding: @space-xs; - position: absolute; - width: 18rem; - z-index: 999; - ul& { - padding-left: 1.6rem; - } - li { - font-size: @font-size-xs; - font-weight: 400; - } -} - -@media screen and (max-width: 767px) { - .account-help-icon li, - .account-help-icon img, - .account-help-icon p { - display: none; - } -} - // monospace things .mono, .form-control, @@ -283,7 +237,7 @@ input[type="password"] + .eye { .m-addresses td:first-child { max-width: 50px; - min-widht: 50px; + min-width: 50px; } .m-addresses td:last-child { @@ -444,4 +398,4 @@ label small { .ens-response { color: @gray; -} +} \ No newline at end of file diff --git a/common/assets/styles/etherwallet-fonts.less b/common/assets/styles/etherwallet-fonts.less index 43481768..dae83ee5 100755 --- a/common/assets/styles/etherwallet-fonts.less +++ b/common/assets/styles/etherwallet-fonts.less @@ -1,7 +1,7 @@ @font-face { font-family: 'Lato'; src: url('../fonts/Lato-Light.woff') format('woff'), - url('../fonts/Lato-Light.ttf') format('truetype'); + url('../fonts/Lato-Light.ttf') format('truetype'); font-style: normal; font-weight: 300; } @@ -9,7 +9,7 @@ @font-face { font-family: 'Lato'; src: url('../fonts/Lato-Regular.woff') format('woff'), - url('../fonts/Lato-Regular.ttf') format('truetype'); + url('../fonts/Lato-Regular.ttf') format('truetype'); font-style: normal; font-weight: 400; } @@ -17,8 +17,8 @@ @font-face { font-family: 'Lato'; src: url('../fonts/Lato-Bold.woff') format('woff'), - url('../fonts/Lato-Bold.ttf') format('truetype'); + url('../fonts/Lato-Bold.ttf') format('truetype'); font-style: normal; font-weight: 700; -} +} \ No newline at end of file diff --git a/common/components/BalanceSidebar/EquivalentValues.tsx b/common/components/BalanceSidebar/EquivalentValues.tsx index 7fda3f81..40162333 100644 --- a/common/components/BalanceSidebar/EquivalentValues.tsx +++ b/common/components/BalanceSidebar/EquivalentValues.tsx @@ -87,6 +87,8 @@ export default class EquivalentValues extends React.Component { }); } else if (ratesError) { valuesEl =
{ratesError}
; + } else if (tokenBalances && tokenBalances.length === 0) { + valuesEl =
No tokens found!
; } else { valuesEl = (
diff --git a/common/components/Footer/index.scss b/common/components/Footer/index.scss index 0f8bd4a7..866f031e 100644 --- a/common/components/Footer/index.scss +++ b/common/components/Footer/index.scss @@ -8,7 +8,7 @@ padding-bottom: $space-sm; display: flex; flex-direction: column; - justify-content: space-between; + justify-content: space-around; text-align: center; align-items: center; @@ -18,26 +18,74 @@ align-items: flex-start; } + & > div { + padding: 8px 16px; + } + + &-social-media-wrap { + margin-top: 32px; + & .Footer-social-media-link { + transition: opacity 0.3s; + color: white; + margin: 1rem; + margin-left: 0; + &:hover { + opacity: 0.8; + color: white; + } + &:focus { + opacity: 0.8; + color: white; + } + } + } + + &-affiliate-wrap { + & .Footer-affiliate-tag { + background-color: #0e97c0; + display: inline-block; + padding: 4px 12px; + border-radius: 30px; + margin: 0rem 0.5rem 0.5rem 0px; + transition: color 0.3s, background-color 0.3s; + &:hover { + background-color: white; + color: #0e97c0; + } + & a { + transition: color 0s; + color: inherit; + &:hover { + color: inherit; + } + } + } + } + &-column { padding: 1rem 2rem; } &-about { - max-width: 22rem; - &-logo { width: 100%; height: auto; max-width: 20rem; } + &-text { + max-width: 50ch; + } } + &-about, + &-links, &-info { - max-width: 34rem; - } - - &-links { - max-width: 28rem; + & > a { + display: block; + font-size: 0.9rem; + margin-top: 0.5rem; + margin-bottom: 0.5rem; + } } &-modal-button { @@ -58,10 +106,14 @@ margin: $space-xs 0 $space-sm; } - a { - color: #4ac8ed; + a, + .Footer-modal-button { + color: #7fe5ff; + font-weight: 400; + transition: color 0.3s; &:hover, &:focus { + opacity: 1; color: darken(#4ac8ed, 5%); } } @@ -81,7 +133,7 @@ ul { list-style: none; - padding-left: $space-sm; + padding-left: 0; margin: 0 0 $space-xs 0; } @@ -101,4 +153,4 @@ .Modal { color: #000; -} \ No newline at end of file +} diff --git a/common/components/Footer/index.tsx b/common/components/Footer/index.tsx index e8eb76c4..9ebd4c97 100644 --- a/common/components/Footer/index.tsx +++ b/common/components/Footer/index.tsx @@ -1,100 +1,105 @@ import logo from 'assets/images/logo-myetherwallet.svg'; -import { bityReferralURL, donationAddressMap } from 'config/data'; +import { + bityReferralURL, + ledgerReferralURL, + trezorReferralURL, + bitboxReferralURL, + donationAddressMap +} from 'config/data'; import React from 'react'; import translate from 'translations'; import './index.scss'; import PreFooter from './PreFooter'; import Modal, { IButton } from 'components/ui/Modal'; +import { NewTabLink } from 'components/ui'; -const LINKS_LEFT = [ +const AffiliateTag = ({ link, text }) => { + return ( +
  • + {text} +
  • + ); +}; + +const SocialMediaLink = ({ link, text }) => { + return ( + + + + ); +}; + +const SOCIAL_MEDIA: Link[] = [ { - text: 'Knowledge Base', - href: 'https://myetherwallet.groovehq.com/help_center' + link: 'https://myetherwallet.herokuapp.com/', + text: 'slack' }, + { - text: 'Helpers & ENS Debugging', - href: 'https://www.myetherwallet.com/helpers.html' + link: 'https://www.reddit.com/r/MyEtherWallet/', + text: 'reddit' }, + { - text: 'Sign Message', - href: 'https://www.myetherwallet.com/signmsg.html' + link: 'https://twitter.com/myetherwallet', + text: 'twitter' + }, + + { + link: 'https://www.facebook.com/MyEtherWallet', + text: 'facebook' + }, + + { + link: 'https://medium.com/@myetherwallet', + text: 'medium' + }, + + { + link: 'https://www.linkedin.com/company/myetherwallet/', + text: 'linkedin' + }, + + { + link: 'https://github.com/MyEtherWallet', + text: 'github' } ]; -const LINKS_SUPPORT = [ +const PRODUCT_INFO: Link[] = [ { - href: bityReferralURL, - text: 'Swap ETH/BTC/EUR/CHF via Bity.com' - }, - { - href: 'https://www.ledgerwallet.com/r/fa4b?path=/products/', - text: 'Buy a Ledger Nano S' - }, - { - href: 'https://trezor.io/?a=myetherwallet.com', - text: 'Buy a TREZOR' - }, - { - href: 'https://digitalbitbox.com/?ref=mew', - text: 'Buy a Digital Bitbox' - } -]; - -const LINKS_RIGHT = [ - { - href: 'https://www.MyEtherWallet.com', - text: 'MyEtherWallet.com' - }, - { - href: 'https://github.com/MyEtherWallet/MyEtherWallet', + link: 'https://github.com/MyEtherWallet/MyEtherWallet', text: 'Github: Current Site' }, { - href: 'https://github.com/MyEtherWallet', + link: 'https://github.com/MyEtherWallet', text: 'Github: MEW Org' }, { - href: 'https://github.com/MyEtherWallet/MyEtherWallet/releases/latest', + link: 'https://github.com/MyEtherWallet/MyEtherWallet/releases/latest', text: 'Github: Latest Release' }, + { - href: + link: 'https://chrome.google.com/webstore/detail/myetherwallet-cx/nlbmnnijcnlegkjjpcfjclmcfggfefdm?hl=en', - text: 'MyEtherWallet CX' + text: 'MyEtherWallet Extension' }, { - href: + link: 'https://chrome.google.com/webstore/detail/etheraddresslookup/pdknmigbbbhmllnmgdfalmedcmcefdfn', - text: 'Anti-Phishing CX' + text: 'Anti - Phishing Extension' } ]; -const LINKS_SOCIAL = [ - { - href: 'https://myetherwallet.herokuapp.com/', - text: 'Slack' - }, - { - href: 'https://www.reddit.com/r/MyEtherWallet/', - text: 'Reddit' - }, - { - href: 'https://twitter.com/myetherwallet', - text: 'Twitter' - }, - { - href: 'https://www.facebook.com/MyEtherWallet/', - text: 'Facebook' - }, - { - href: 'https://medium.com/@myetherwallet', - text: 'Medium' - } -]; +interface Link { + link: string; + text: string; +} interface Props { latestBlock: string; -}; +} interface State { isOpen: boolean; @@ -122,9 +127,9 @@ export default class Footer extends React.Component {
    -
    +
    -

    - {translate('FOOTER_1')} - {translate('FOOTER_1b')} -

    - - {LINKS_LEFT.map(link => { - return ( -

    - - {link.text} - -

    - ); - })} +

    {translate('FOOTER_1')}

    + + Knowledge Base + + + Helpers & ENS Debugging +
    -
    +
    You can support us by supporting our blockchain-family.
    -

    Consider using our affiliate links to...

    -
      - {LINKS_SUPPORT.map(link => { - return ( -
    • - - {link.text} - -
    • - ); - })} +

      Consider using our affiliate links to

      +
        + +
      +

      Buy a

      +
        + + +
      -
      {translate('FOOTER_2')}
      • - {' '} - ETH: {donationAddressMap.ETH} + ETH: mewtopia.eth{' '} + + + {donationAddressMap.ETH} + +
      • {' '} @@ -258,29 +264,22 @@ export default class Footer extends React.Component {
    -
    - {LINKS_RIGHT.map(link => { - return ( -

    - - {link.text} - -

    - ); - })} -

    - {LINKS_SOCIAL.map((link, i) => { - return ( - - - {link.text} - - {i !== LINKS_SOCIAL.length - 1 && ' · '} - - ); - })} -

    -

    Latest Block#: {this.props.latestBlock}

    +
    + {PRODUCT_INFO.map((productInfoItem, idx) => ( + + {productInfoItem.text} + + ))} + +
    + {SOCIAL_MEDIA.map((socialMediaItem, idx) => ( + + ))} +
    diff --git a/common/components/Header/components/CustomNodeModal.tsx b/common/components/Header/components/CustomNodeModal.tsx index d4b63800..ac58e895 100644 --- a/common/components/Header/components/CustomNodeModal.tsx +++ b/common/components/Header/components/CustomNodeModal.tsx @@ -2,9 +2,12 @@ import React from 'react'; import classnames from 'classnames'; import Modal, { IButton } from 'components/ui/Modal'; import translate from 'translations'; -import { NETWORKS, CustomNodeConfig } from 'config/data'; +import { NETWORKS, CustomNodeConfig, CustomNetworkConfig } from 'config/data'; +import { makeCustomNodeId } from 'utils/node'; +import { makeCustomNetworkId } from 'utils/network'; const NETWORK_KEYS = Object.keys(NETWORKS); +const CUSTOM = 'custom'; interface Input { name: string; @@ -13,7 +16,10 @@ interface Input { } interface Props { + customNodes: CustomNodeConfig[]; + customNetworks: CustomNetworkConfig[]; handleAddCustomNode(node: CustomNodeConfig): void; + handleAddCustomNetwork(node: CustomNetworkConfig): void; handleClose(): void; } @@ -22,6 +28,9 @@ interface State { url: string; port: string; network: string; + customNetworkName: string; + customNetworkUnit: string; + customNetworkChainId: string; hasAuth: boolean; username: string; password: string; @@ -33,25 +42,34 @@ export default class CustomNodeModal extends React.Component { url: '', port: '', network: NETWORK_KEYS[0], + customNetworkName: '', + customNetworkUnit: '', + customNetworkChainId: '', hasAuth: false, username: '', - password: '', + password: '' }; public render() { - const { handleClose } = this.props; + const { customNetworks, handleClose } = this.props; + const { network } = this.state; const isHttps = window.location.protocol.includes('https'); const invalids = this.getInvalids(); - const buttons: IButton[] = [{ - type: 'primary', - text: translate('NODE_CTA'), - onClick: this.saveAndAdd, - disabled: !!Object.keys(invalids).length, - }, { - text: translate('x_Cancel'), - onClick: handleClose - }]; + const buttons: IButton[] = [ + { + type: 'primary', + text: translate('NODE_CTA'), + onClick: this.saveAndAdd, + disabled: !!Object.keys(invalids).length + }, + { + text: translate('x_Cancel'), + onClick: handleClose + } + ]; + + const conflictedNode = this.getConflictedNode(); return ( { handleClose={handleClose} >
    - {isHttps && -
    + {isHttps && ( +
    {translate('NODE_Warning')}
    - } + )} + + {conflictedNode && ( +
    + You already have a node called '{conflictedNode.name}' that + matches this one, saving this will overwrite it +
    + )}
    - {this.renderInput({ - name: 'name', - placeholder: 'My Node', - }, invalids)} + {this.renderInput( + { + name: 'name', + placeholder: 'My Node' + }, + invalids + )}
    + {network === CUSTOM && ( +
    +
    + + {this.renderInput( + { + name: 'customNetworkName', + placeholder: 'My Custom Network' + }, + invalids + )} +
    +
    + + {this.renderInput( + { + name: 'customNetworkUnit', + placeholder: 'ETH' + }, + invalids + )} +
    +
    + + {this.renderInput( + { + name: 'customNetworkChainId', + placeholder: 'e.g. 1' + }, + invalids + )} +
    +
    + )} + +
    +
    - {this.renderInput({ - name: 'url', - placeholder: 'http://127.0.0.1/', - }, invalids)} + {this.renderInput( + { + name: 'url', + placeholder: 'http://127.0.0.1/' + }, + invalids + )}
    - {this.renderInput({ - name: 'port', - placeholder: '8545', - type: 'number', - }, invalids)} + {this.renderInput( + { + name: 'port', + placeholder: '8545', + type: 'number' + }, + invalids + )}
    +
    - {this.state.hasAuth && + {this.state.hasAuth && (
    - + {this.renderInput({ name: 'username' }, invalids)}
    - - {this.renderInput({ - name: 'password', - type: 'password', - }, invalids)} + + {this.renderInput( + { + name: 'password', + type: 'password' + }, + invalids + )}
    - } + )}
    @@ -145,15 +230,17 @@ export default class CustomNodeModal extends React.Component { } private renderInput(input: Input, invalids: { [key: string]: boolean }) { - return ; + return ( + + ); } private getInvalids(): { [key: string]: boolean } { @@ -163,12 +250,16 @@ export default class CustomNodeModal extends React.Component { hasAuth, username, password, + network, + customNetworkName, + customNetworkUnit, + customNetworkChainId } = this.state; - const required = ["name", "url", "port", "network"]; + const required = ['name', 'url', 'port', 'network']; const invalids: { [key: string]: boolean } = {}; // Required fields - required.forEach((field) => { + required.forEach(field => { if (!this.state[field]) { invalids[field] = true; } @@ -195,12 +286,67 @@ export default class CustomNodeModal extends React.Component { } } + // If they have a custom network, make sure info is provided + if (network === CUSTOM) { + if (!customNetworkName) { + invalids.customNetworkName = true; + } + if (!customNetworkUnit) { + invalids.customNetworkUnit = true; + } + + // Numeric chain ID (if provided) + const iChainId = parseInt(customNetworkChainId, 10); + if (!iChainId || iChainId < 0) { + invalids.customNetworkChainId = true; + } + } + return invalids; } - private handleChange = (ev: React.FormEvent< - HTMLInputElement | HTMLSelectElement - >) => { + private makeCustomNetworkConfigFromState(): CustomNetworkConfig { + return { + name: this.state.customNetworkName, + unit: this.state.customNetworkUnit, + chainId: this.state.customNetworkChainId + ? parseInt(this.state.customNetworkChainId, 10) + : 0 + }; + } + + private makeCustomNodeConfigFromState(): CustomNodeConfig { + const { network } = this.state; + const node: CustomNodeConfig = { + name: this.state.name.trim(), + url: this.state.url.trim(), + port: parseInt(this.state.port, 10), + network: + network === CUSTOM + ? makeCustomNetworkId(this.makeCustomNetworkConfigFromState()) + : network + }; + + if (this.state.hasAuth) { + node.auth = { + username: this.state.username, + password: this.state.password + }; + } + + return node; + } + + private getConflictedNode(): CustomNodeConfig | undefined { + const { customNodes } = this.props; + const config = this.makeCustomNodeConfigFromState(); + const thisId = makeCustomNodeId(config); + return customNodes.find(conf => makeCustomNodeId(conf) === thisId); + } + + private handleChange = ( + ev: React.FormEvent + ) => { const { name, value } = ev.currentTarget; this.setState({ [name as any]: value }); }; @@ -211,18 +357,11 @@ export default class CustomNodeModal extends React.Component { }; private saveAndAdd = () => { - const node: CustomNodeConfig = { - name: this.state.name.trim(), - url: this.state.url.trim(), - port: parseInt(this.state.port, 10), - network: this.state.network, - }; + const node = this.makeCustomNodeConfigFromState(); - if (this.state.hasAuth) { - node.auth = { - username: this.state.username, - password: this.state.password, - }; + if (this.state.network === CUSTOM) { + const network = this.makeCustomNetworkConfigFromState(); + this.props.handleAddCustomNetwork(network); } this.props.handleAddCustomNode(node); diff --git a/common/components/Header/components/Navigation.tsx b/common/components/Header/components/Navigation.tsx index facdd691..8a37f04e 100644 --- a/common/components/Header/components/Navigation.tsx +++ b/common/components/Header/components/Navigation.tsx @@ -8,6 +8,7 @@ const tabs = [ name: 'NAV_GenerateWallet', to: '/' }, + { name: 'NAV_SendEther', to: 'send-transaction' @@ -36,6 +37,10 @@ const tabs = [ name: 'Broadcast Transaction', to: 'pushTx' }, + { + name: 'NAV_Utilities', + to: 'utilities' + }, { name: 'NAV_Help', to: 'https://myetherwallet.groovehq.com/help_center', diff --git a/common/components/Header/index.tsx b/common/components/Header/index.tsx index 6e381db4..111a8a04 100644 --- a/common/components/Header/index.tsx +++ b/common/components/Header/index.tsx @@ -3,7 +3,8 @@ import { TChangeLanguage, TChangeNodeIntent, TAddCustomNode, - TRemoveCustomNode + TRemoveCustomNode, + TAddCustomNetwork } from 'actions/config'; import logo from 'assets/images/logo-myetherwallet.svg'; import { Dropdown, ColorDropdown } from 'components/ui'; @@ -14,17 +15,18 @@ import { ANNOUNCEMENT_MESSAGE, ANNOUNCEMENT_TYPE, languages, - NETWORKS, NODES, VERSION, NodeConfig, - CustomNodeConfig -} from '../../config/data'; + CustomNodeConfig, + CustomNetworkConfig +} from 'config/data'; import GasPriceDropdown from './components/GasPriceDropdown'; import Navigation from './components/Navigation'; import CustomNodeModal from './components/CustomNodeModal'; import { getKeyByValue } from 'utils/helpers'; import { makeCustomNodeId } from 'utils/node'; +import { getNetworkConfigFromId } from 'utils/network'; import './index.scss'; interface Props { @@ -34,12 +36,14 @@ interface Props { isChangingNode: boolean; gasPriceGwei: number; customNodes: CustomNodeConfig[]; + customNetworks: CustomNetworkConfig[]; changeLanguage: TChangeLanguage; changeNodeIntent: TChangeNodeIntent; changeGasPrice: TChangeGasPrice; addCustomNode: TAddCustomNode; removeCustomNode: TRemoveCustomNode; + addCustomNetwork: TAddCustomNetwork; } interface State { @@ -58,40 +62,47 @@ export default class Header extends Component { node, nodeSelection, isChangingNode, - customNodes + customNodes, + customNetworks } = this.props; const { isAddingCustomNode } = this.state; const selectedLanguage = languageSelection; - const selectedNetwork = NETWORKS[node.network]; + const selectedNetwork = getNetworkConfigFromId( + node.network, + customNetworks + ); const LanguageDropDown = Dropdown as new () => Dropdown< typeof selectedLanguage >; const nodeOptions = Object.keys(NODES) .map(key => { + const n = NODES[key]; + const network = getNetworkConfigFromId(n.network, customNetworks); return { value: key, name: ( - {NODES[key].network} ({NODES[key].service}) + {network && network.name} ({n.service}) ), - color: NETWORKS[NODES[key].network].color, - hidden: NODES[key].hidden + color: network && network.color, + hidden: n.hidden }; }) .concat( - customNodes.map(customNode => { + customNodes.map(cn => { + const network = getNetworkConfigFromId(cn.network, customNetworks); return { - value: makeCustomNodeId(customNode), + value: makeCustomNodeId(cn), name: ( - {customNode.network} - {customNode.name} (custom) + {network && network.name} - {cn.name} (custom) ), - color: '#000', + color: network && network.color, hidden: false, - onRemove: () => this.props.removeCustomNode(customNode) + onRemove: () => this.props.removeCustomNode(cn) }; }) ); @@ -161,8 +172,8 @@ export default class Header extends Component { > { - + {isAddingCustomNode && ( )} diff --git a/common/components/PrintableWallet/index.tsx b/common/components/PrintableWallet/index.tsx index bbabe064..c6d38252 100644 --- a/common/components/PrintableWallet/index.tsx +++ b/common/components/PrintableWallet/index.tsx @@ -3,6 +3,7 @@ import { IFullWallet } from 'ethereumjs-wallet'; import React from 'react'; import translate from 'translations'; import printElement from 'utils/printElement'; +import { stripHexPrefix } from 'libs/values'; const print = (address: string, privateKey: string) => () => address && @@ -27,7 +28,7 @@ const print = (address: string, privateKey: string) => () => const PrintableWallet: React.SFC<{ wallet: IFullWallet }> = ({ wallet }) => { const address = wallet.getAddressString(); - const privateKey = wallet.getPrivateKeyString(); + const privateKey = stripHexPrefix(wallet.getPrivateKeyString()); if (!address || !privateKey) { return null; diff --git a/common/components/WalletDecrypt/PrivateKey.tsx b/common/components/WalletDecrypt/PrivateKey.tsx index 0e564144..96af3673 100644 --- a/common/components/WalletDecrypt/PrivateKey.tsx +++ b/common/components/WalletDecrypt/PrivateKey.tsx @@ -1,4 +1,5 @@ import { isValidEncryptedPrivKey, isValidPrivKey } from 'libs/validators'; +import { stripHexPrefix } from 'libs/values'; import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; @@ -8,13 +9,6 @@ export interface PrivateKeyValue { valid: boolean; } -function fixPkey(key) { - if (key.indexOf('0x') === 0) { - return key.slice(2); - } - return key; -} - interface Validated { fixedPkey: string; isValidPkey: boolean; @@ -23,7 +17,7 @@ interface Validated { } function validatePkeyAndPass(pkey: string, pass: string): Validated { - const fixedPkey = fixPkey(pkey); + const fixedPkey = stripHexPrefix(pkey); const validPkey = isValidPrivKey(fixedPkey); const validEncPkey = isValidEncryptedPrivKey(fixedPkey); const isValidPkey = validPkey || validEncPkey; @@ -58,15 +52,13 @@ export default class PrivateKeyDecrypt extends Component { return (
    -

    - {translate('ADD_Radio_3')} -

    +

    {translate('ADD_Radio_3')}