From e80d0a68a9a34b68b817417138131520efe85ab2 Mon Sep 17 00:00:00 2001 From: William O'Beirne Date: Tue, 19 Sep 2017 20:47:08 -0400 Subject: [PATCH 01/22] All v3 Nodes & Networks (#202) * Add all v3s nodes, create node libs for etherscan and infura. * Add all network configs for alternatives. * Color and animate nav border on network selection. * Address PR comments. * Persist network selection to local storage. * Modifiy TransactionSucceeded to link to network-specific explorer. * - Reload on Node Change to reset state. Should be refactored in the future so that we are not forcing clients to reload. --- .../TransactionSucceeded.js | 13 +- .../Header/components/Navigation.jsx | 14 +- .../Header/components/Navigation.scss | 1 + common/components/Header/index.jsx | 7 +- common/config/contracts/exp.json | 1 + common/config/contracts/rsk.json | 1 + common/config/contracts/ubq.json | 1 + common/config/data.js | 164 ++++++- common/config/tokens/etc.json | 8 + common/config/tokens/eth.js | 439 ------------------ common/config/tokens/eth.json | 437 +++++++++++++++++ common/config/tokens/exp.json | 1 + common/config/tokens/kovan.json | 14 + common/config/tokens/rinkeby.json | 1 + common/config/tokens/ropsten.json | 1 + common/config/tokens/rsk.json | 1 + common/config/tokens/ubq.json | 14 + common/libs/nodes/etherscan/client.js | 29 ++ common/libs/nodes/etherscan/index.js | 14 + common/libs/nodes/etherscan/requests.js | 67 +++ common/libs/nodes/etherscan/types.js | 42 ++ common/libs/nodes/index.js | 2 + common/libs/nodes/infura/client.js | 9 + common/libs/nodes/infura/index.js | 11 + common/libs/nodes/rpc/client.js | 100 +--- common/libs/nodes/rpc/index.js | 97 ++-- common/libs/nodes/rpc/requests.js | 62 +++ common/libs/nodes/rpc/types.js | 2 - common/sagas/config.js | 5 +- common/sagas/wallet.js | 12 +- common/store.js | 1 + package-lock.json | 18 +- 32 files changed, 993 insertions(+), 596 deletions(-) create mode 100644 common/config/contracts/exp.json create mode 100644 common/config/contracts/rsk.json create mode 100644 common/config/contracts/ubq.json create mode 100644 common/config/tokens/etc.json delete mode 100644 common/config/tokens/eth.js create mode 100644 common/config/tokens/eth.json create mode 100644 common/config/tokens/exp.json create mode 100644 common/config/tokens/kovan.json create mode 100644 common/config/tokens/rinkeby.json create mode 100644 common/config/tokens/ropsten.json create mode 100644 common/config/tokens/rsk.json create mode 100644 common/config/tokens/ubq.json create mode 100644 common/libs/nodes/etherscan/client.js create mode 100644 common/libs/nodes/etherscan/index.js create mode 100644 common/libs/nodes/etherscan/requests.js create mode 100644 common/libs/nodes/etherscan/types.js create mode 100644 common/libs/nodes/infura/client.js create mode 100644 common/libs/nodes/infura/index.js create mode 100644 common/libs/nodes/rpc/requests.js diff --git a/common/components/ExtendedNotifications/TransactionSucceeded.js b/common/components/ExtendedNotifications/TransactionSucceeded.js index 5801e8d0..41cb83fb 100644 --- a/common/components/ExtendedNotifications/TransactionSucceeded.js +++ b/common/components/ExtendedNotifications/TransactionSucceeded.js @@ -1,13 +1,18 @@ import React from 'react'; -import { ETHTxExplorer } from 'config/data'; +import type { BlockExplorerConfig } from 'config.data'; import translate from 'translations'; + export type TransactionSucceededProps = { - txHash: string + txHash: string, + blockExplorer: BlockExplorerConfig }; -const TransactionSucceeded = ({ txHash }: TransactionSucceededProps) => { +const TransactionSucceeded = ({ + txHash, + blockExplorer +}: TransactionSucceededProps) => { // const checkTxLink = `https://www.myetherwallet.com?txHash=${txHash}/#check-tx-status`; - const txHashLink = ETHTxExplorer(txHash); + const txHashLink = blockExplorer.tx(txHash); return (
diff --git a/common/components/Header/components/Navigation.jsx b/common/components/Header/components/Navigation.jsx index f3bcadf8..bcf9eb88 100644 --- a/common/components/Header/components/Navigation.jsx +++ b/common/components/Header/components/Navigation.jsx @@ -39,7 +39,7 @@ const tabs = [ } ]; -export default class TabsOptions extends Component { +export default class Navigation extends Component { constructor(props) { super(props); this.state = { @@ -49,7 +49,8 @@ export default class TabsOptions extends Component { } static propTypes = { - location: PropTypes.object + location: PropTypes.object, + color: PropTypes.string }; scrollLeft() {} @@ -57,12 +58,19 @@ export default class TabsOptions extends Component { scrollRight() {} render() { - const { location } = this.props; + const { location, color } = this.props; + const borderStyle = {}; + + if (color) { + borderStyle.borderTopColor = color; + } + return ( ); } diff --git a/common/components/Header/components/NavigationLink.jsx b/common/components/Header/components/NavigationLink.jsx deleted file mode 100644 index f2ba519b..00000000 --- a/common/components/Header/components/NavigationLink.jsx +++ /dev/null @@ -1,51 +0,0 @@ -// @flow -import React from 'react'; -import classnames from 'classnames'; -import translate from 'translations'; -import { Link } from 'react-router'; -import './NavigationLink.scss'; - -type Props = { - link: { - name: string, - to?: string, - external?: boolean - }, - location: Object -}; - -export default class NavigationLink extends React.Component { - props: Props; - - render() { - const { link, location } = this.props; - const linkClasses = classnames({ - 'NavigationLink-link': true, - 'is-disabled': !link.to, - 'is-active': - location.pathname === link.to || - location.pathname.substring(1) === link.to - }); - // $FlowFixMe flow is wrong, this isn't an element - const linkLabel = `nav item: ${translate(link.name, true)}`; - - const linkEl = link.external - ? - {translate(link.name)} - - : - {translate(link.name)} - ; - - return ( -
  • - {linkEl} -
  • - ); - } -} diff --git a/common/components/Header/components/NavigationLink.tsx b/common/components/Header/components/NavigationLink.tsx new file mode 100644 index 00000000..322c49e5 --- /dev/null +++ b/common/components/Header/components/NavigationLink.tsx @@ -0,0 +1,49 @@ +import classnames from 'classnames'; +import React from 'react'; +import { Link } from 'react-router'; +import translate, { translateRaw } from 'translations'; +import './NavigationLink.scss'; + +interface Props { + link: { + name: string; + to?: string; + external?: boolean; + }; + location: any; +} + +export default class NavigationLink extends React.Component { + public render() { + const { link, location } = this.props; + const linkClasses = classnames({ + 'NavigationLink-link': true, + 'is-disabled': !link.to, + 'is-active': + location.pathname === link.to || + location.pathname.substring(1) === link.to + }); + const linkLabel = `nav item: ${translateRaw(link.name)}`; + + const linkEl = link.external ? ( + + {translate(link.name)} + + ) : ( + + {translate(link.name)} + + ); + + return
  • {linkEl}
  • ; + } +} diff --git a/common/components/Header/index.jsx b/common/components/Header/index.tsx similarity index 70% rename from common/components/Header/index.jsx rename to common/components/Header/index.tsx index 908e6e0c..c549f889 100644 --- a/common/components/Header/index.jsx +++ b/common/components/Header/index.tsx @@ -1,49 +1,53 @@ -// @flow -import React, { Component } from 'react'; -import Navigation from './components/Navigation'; -import GasPriceDropdown from './components/GasPriceDropdown'; -import { Link } from 'react-router'; -import { Dropdown } from 'components/ui'; -import { - languages, - NODES, - NETWORKS, - VERSION, - ANNOUNCEMENT_TYPE, - ANNOUNCEMENT_MESSAGE -} from '../../config/data'; +import { TChangeGasPrice, TChangeLanguage, TChangeNode } from 'actions/config'; import logo from 'assets/images/logo-myetherwallet.svg'; +import { Dropdown } from 'components/ui'; +import React, { Component } from 'react'; +import { Link } from 'react-router'; +import { + ANNOUNCEMENT_MESSAGE, + ANNOUNCEMENT_TYPE, + languages, + NETWORKS, + NODES, + VERSION +} from '../../config/data'; +import GasPriceDropdown from './components/GasPriceDropdown'; +import Navigation from './components/Navigation'; import './index.scss'; -export default class Header extends Component { - props: { - location: {}, - languageSelection: string, - nodeSelection: string, - gasPriceGwei: number, +interface Props { + location: {}; + languageSelection: string; + nodeSelection: string; + gasPriceGwei: number; - changeLanguage: (sign: string) => any, - changeNode: (key: string) => any, - changeGasPrice: (price: number) => any - }; + changeLanguage: TChangeLanguage; + changeNode: TChangeNode; + changeGasPrice: TChangeGasPrice; +} - render() { +export default class Header extends Component { + public render() { const { languageSelection, changeNode, nodeSelection } = this.props; const selectedLanguage = languages.find(l => l.sign === languageSelection) || languages[0]; const selectedNode = NODES[nodeSelection]; const selectedNetwork = NETWORKS[selectedNode.network]; - + const LanguageDropDown = Dropdown as new () => Dropdown< + typeof selectedLanguage + >; + const NodeDropDown = Dropdown as new () => Dropdown; return (
    - {ANNOUNCEMENT_MESSAGE && + {ANNOUNCEMENT_MESSAGE && (
    } + /> + )}
    @@ -71,10 +75,10 @@ export default class Header extends Component { onChange={this.props.changeGasPrice} /> - o.name} + formatTitle={this.extractName} value={selectedLanguage} extra={[
  • , @@ -87,20 +91,14 @@ export default class Header extends Component { onChange={this.changeLanguage} /> - [ - NODES[o].network, - ' ', - - ({NODES[o].service}) - - ]} + formatTitle={this.nodeNetworkAndService} value={nodeSelection} extra={
  • - {}}>Add Custom Node + Add Custom Node
  • } onChange={changeNode} @@ -111,13 +109,23 @@ export default class Header extends Component {
    ); } - changeLanguage = (value: { sign: string }) => { + public changeLanguage = (value: { sign: string }) => { this.props.changeLanguage(value.sign); }; + + private extractName(): (option: { sign: string; name: string }) => string { + return name; + } + + private nodeNetworkAndService = (option: string) => [ + NODES[option].network, + ' ', + ({NODES[option].service}) + ]; } diff --git a/common/components/PaperWallet/index.jsx b/common/components/PaperWallet/index.tsx similarity index 89% rename from common/components/PaperWallet/index.jsx rename to common/components/PaperWallet/index.tsx index 840b5b4c..1bef1481 100644 --- a/common/components/PaperWallet/index.jsx +++ b/common/components/PaperWallet/index.tsx @@ -1,16 +1,15 @@ -// @flow +import { Identicon, QRCode } from 'components/ui'; +import PrivKeyWallet from 'libs/wallet/privkey'; import React from 'react'; -import { QRCode, Identicon } from 'components/ui'; -import type PrivKeyWallet from 'libs/wallet/privkey'; import ethLogo from 'assets/images/logo-ethereum-1.png'; -import sidebarImg from 'assets/images/print-sidebar.png'; import notesBg from 'assets/images/notes-bg.png'; +import sidebarImg from 'assets/images/print-sidebar.png'; const walletWidth = 680; const walletHeight = 280; -const styles = { +const styles: any = { container: { position: 'relative', margin: '0 auto', @@ -91,22 +90,26 @@ const styles = { } }; -type Props = { - wallet: PrivKeyWallet -}; +interface Props { + wallet: PrivKeyWallet; +} -export default class PaperWallet extends React.Component { - props: Props; - state = { address: '' }; +interface State { + address: string; +} +export default class PaperWallet extends React.Component { + public state = { address: '' }; - componentDidMount() { - if (!this.props.wallet) return; + public componentDidMount() { + if (!this.props.wallet) { + return; + } this.props.wallet.getAddress().then(addr => { this.setState({ address: addr }); }); } - render() { + public render() { const privateKey = this.props.wallet.getPrivateKey(); return ( diff --git a/common/components/PrintableWallet/index.jsx b/common/components/PrintableWallet/index.tsx similarity index 74% rename from common/components/PrintableWallet/index.jsx rename to common/components/PrintableWallet/index.tsx index d14f9d5f..205d56b1 100644 --- a/common/components/PrintableWallet/index.jsx +++ b/common/components/PrintableWallet/index.tsx @@ -1,22 +1,15 @@ -// @flow +import { PaperWallet } from 'components'; +import PrivKeyWallet from 'libs/wallet/privkey'; import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import translate from 'translations'; import printElement from 'utils/printElement'; -import { PaperWallet } from 'components'; -import type PrivKeyWallet from 'libs/wallet/privkey'; -type Props = { - wallet: PrivKeyWallet -}; +interface Props { + wallet: PrivKeyWallet; +} -export default class PrintableWallet extends Component { - props: Props; - static propTypes = { - wallet: PropTypes.object.isRequired - }; - - print = () => { +export default class PrintableWallet extends Component { + public print = () => { printElement(, { popupFeatures: { scrollbars: 'no' @@ -36,7 +29,7 @@ export default class PrintableWallet extends Component { }); }; - render() { + public render() { return (
    diff --git a/common/components/Root/index.jsx b/common/components/Root/index.tsx similarity index 68% rename from common/components/Root/index.jsx rename to common/components/Root/index.tsx index a8a24b11..1bfaaa8b 100644 --- a/common/components/Root/index.jsx +++ b/common/components/Root/index.tsx @@ -1,16 +1,16 @@ import React, { Component } from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; -import PropTypes from 'prop-types'; -export default class Root extends Component { - static propTypes = { - store: PropTypes.object, - history: PropTypes.object, - routes: PropTypes.func - }; +// TODO: fix this +interface Props { + store: any; + history: any; + routes(): null; +} - render() { +export default class Root extends Component { + public render() { const { store, history, routes } = this.props; // key={Math.random()} = hack for HMR from https://github.com/webpack/webpack-dev-server/issues/395 return ( diff --git a/common/components/Translate.jsx b/common/components/Translate.tsx similarity index 90% rename from common/components/Translate.jsx rename to common/components/Translate.tsx index d192983a..c1a2e383 100644 --- a/common/components/Translate.jsx +++ b/common/components/Translate.tsx @@ -1,11 +1,10 @@ -// @flow import React from 'react'; import Markdown from 'react-markdown'; import { translateRaw } from 'translations'; -type Props = { - translationKey: string -}; +interface Props { + translationKey: string; +} const Translate = ({ translationKey }: Props) => { const source = translateRaw(translationKey); diff --git a/common/components/WalletDecrypt/DeterministicWalletsModal.jsx b/common/components/WalletDecrypt/DeterministicWalletsModal.tsx similarity index 70% rename from common/components/WalletDecrypt/DeterministicWalletsModal.jsx rename to common/components/WalletDecrypt/DeterministicWalletsModal.tsx index 032441ff..40e953d1 100644 --- a/common/components/WalletDecrypt/DeterministicWalletsModal.jsx +++ b/common/components/WalletDecrypt/DeterministicWalletsModal.tsx @@ -1,60 +1,59 @@ -// @flow -import './DeterministicWalletsModal.scss'; -import React from 'react'; -import { connect } from 'react-redux'; -import Modal from 'components/ui/Modal'; import { - getDeterministicWallets, - setDesiredToken -} from 'actions/deterministicWallets'; -import { getNetworkConfig } from 'selectors/config'; -import { getTokens } from 'selectors/wallet'; -import { isValidPath } from 'libs/validators'; -import type { DeterministicWalletData, - GetDeterministicWalletsArgs, + getDeterministicWallets, GetDeterministicWalletsAction, + GetDeterministicWalletsArgs, + setDesiredToken, SetDesiredTokenAction } from 'actions/deterministicWallets'; -import type { NetworkConfig, Token } from 'config/data'; +import Modal, { IButton } from 'components/ui/Modal'; +import { NetworkConfig, Token } from 'config/data'; +import { isValidPath } from 'libs/validators'; +import React from 'react'; +import { connect } from 'react-redux'; +import { getNetworkConfig } from 'selectors/config'; +import { getTokens, MergedToken } from 'selectors/wallet'; +import './DeterministicWalletsModal.scss'; const WALLETS_PER_PAGE = 5; -type Props = { +interface Props { + // Passed props + isOpen?: boolean; + walletType?: string; + dPath: string; + dPaths: { label: string; value: string }[]; + publicKey?: string; + chainCode?: string; + seed?: string; + // Redux state - wallets: DeterministicWalletData[], - desiredToken: string, - network: NetworkConfig, - tokens: Token[], + wallets: DeterministicWalletData[]; + desiredToken: string; + network: NetworkConfig; + tokens: MergedToken[]; // Redux actions - getDeterministicWallets: GetDeterministicWalletsArgs => GetDeterministicWalletsAction, - setDesiredToken: (tkn: ?string) => SetDesiredTokenAction, + getDeterministicWallets( + args: GetDeterministicWalletsArgs + ): GetDeterministicWalletsAction; + setDesiredToken(tkn: string | undefined): SetDesiredTokenAction; - // Passed props - isOpen?: boolean, - walletType: ?string, - dPath: string, - dPaths: { label: string, value: string }[], - publicKey: ?string, - chainCode: ?string, - seed: ?string, - onCancel: () => void, - onConfirmAddress: (string, number) => void, - onPathChange: string => void -}; + onCancel(): void; + onConfirmAddress(address: string, addressIndex: number): void; + onPathChange(path: string): void; +} -type State = { - selectedAddress: string, - selectedAddrIndex: number, - isCustomPath: boolean, - customPath: string, - page: number -}; +interface State { + selectedAddress: string; + selectedAddrIndex: number; + isCustomPath: boolean; + customPath: string; + page: number; +} -class DeterministicWalletsModal extends React.Component { - props: Props; - state: State = { +class DeterministicWalletsModal extends React.Component { + public state = { selectedAddress: '', selectedAddrIndex: 0, isCustomPath: false, @@ -62,11 +61,11 @@ class DeterministicWalletsModal extends React.Component { page: 0 }; - componentDidMount() { - this._getAddresses(); + public componentDidMount() { + this.getAddresses(); } - componentWillReceiveProps(nextProps) { + public componentWillReceiveProps(nextProps) { const { publicKey, chainCode, seed, dPath } = this.props; if ( nextProps.publicKey !== publicKey || @@ -74,11 +73,125 @@ class DeterministicWalletsModal extends React.Component { nextProps.dPath !== dPath || nextProps.seed !== seed ) { - this._getAddresses(nextProps); + this.getAddresses(nextProps); } } - _getAddresses(props: Props = this.props) { + public render() { + const { + wallets, + desiredToken, + network, + tokens, + dPath, + dPaths, + onCancel, + walletType + } = this.props; + const { selectedAddress, isCustomPath, customPath, page } = this.state; + const validPathClass = isValidPath(customPath) ? 'is-valid' : 'is-invalid'; + + const buttons: IButton[] = [ + { + text: 'Unlock this Address', + type: 'primary', + onClick: this.handleConfirmAddress, + disabled: !selectedAddress + }, + { + text: 'Cancel', + type: 'default', + onClick: onCancel + } + ]; + + return ( + +
    +
    + Addresses for + + {isCustomPath && + } +
    + +
    + + + + + + + + + + + + {wallets.map(wallet => this.renderWalletRow(wallet))} + +
    #Address + {network.unit} + + + More
    + +
    + + +
    +
    +
    +
    + ); + } + + private getAddresses(props: Props = this.props) { const { dPath, publicKey, chainCode, seed } = props; if (dPath && ((publicKey && chainCode) || seed) && isValidPath(dPath)) { @@ -93,8 +206,8 @@ class DeterministicWalletsModal extends React.Component { } } - _handleChangePath = (ev: SyntheticInputEvent) => { - const { value } = ev.target; + private handleChangePath = (ev: React.SyntheticEvent) => { + const { value } = ev.target as HTMLSelectElement; if (value === 'custom') { this.setState({ isCustomPath: true }); @@ -106,21 +219,29 @@ class DeterministicWalletsModal extends React.Component { } }; - _handleChangeCustomPath = (ev: SyntheticInputEvent) => { - this.setState({ customPath: ev.target.value }); + private handleChangeCustomPath = ( + ev: React.SyntheticEvent + ) => { + this.setState({ customPath: (ev.target as HTMLInputElement).value }); }; - _handleSubmitCustomPath = (ev: SyntheticInputEvent) => { + private handleSubmitCustomPath = ( + ev: React.SyntheticEvent + ) => { ev.preventDefault(); - if (!isValidPath(this.state.customPath)) return; + if (!isValidPath(this.state.customPath)) { + return; + } this.props.onPathChange(this.state.customPath); }; - _handleChangeToken = (ev: SyntheticInputEvent) => { - this.props.setDesiredToken(ev.target.value || null); + private handleChangeToken = (ev: React.SyntheticEvent) => { + this.props.setDesiredToken( + (ev.target as HTMLSelectElement).value || undefined + ); }; - _handleConfirmAddress = () => { + private handleConfirmAddress = () => { if (this.state.selectedAddress) { this.props.onConfirmAddress( this.state.selectedAddress, @@ -129,22 +250,22 @@ class DeterministicWalletsModal extends React.Component { } }; - _selectAddress(selectedAddress, selectedAddrIndex) { + private selectAddress(selectedAddress, selectedAddrIndex) { this.setState({ selectedAddress, selectedAddrIndex }); } - _nextPage = () => { - this.setState({ page: this.state.page + 1 }, this._getAddresses); + private nextPage = () => { + this.setState({ page: this.state.page + 1 }, this.getAddresses); }; - _prevPage = () => { + private prevPage = () => { this.setState( { page: Math.max(this.state.page - 1, 0) }, - this._getAddresses + this.getAddresses ); }; - _renderWalletRow(wallet) { + private renderWalletRow(wallet) { const { desiredToken, network } = this.props; const { selectedAddress } = this.state; @@ -157,7 +278,7 @@ class DeterministicWalletsModal extends React.Component { return ( {wallet.index + 1} @@ -188,120 +309,6 @@ class DeterministicWalletsModal extends React.Component { ); } - - render() { - const { - wallets, - desiredToken, - network, - tokens, - dPath, - dPaths, - onCancel, - walletType - } = this.props; - const { selectedAddress, isCustomPath, customPath, page } = this.state; - const validPathClass = isValidPath(customPath) ? 'is-valid' : 'is-invalid'; - - const buttons = [ - { - text: 'Unlock this Address', - type: 'primary', - onClick: this._handleConfirmAddress, - disabled: !selectedAddress - }, - { - text: 'Cancel', - type: 'default', - onClick: onCancel - } - ]; - - return ( - -
    -
    - Addresses for - - {isCustomPath && - } -
    - -
    - - - - - - - - - - - - {wallets.map(wallet => this._renderWalletRow(wallet))} - -
    #Address - {network.unit} - - - More
    - -
    - - -
    -
    -
    -
    - ); - } } function mapStateToProps(state) { diff --git a/common/components/WalletDecrypt/Keystore.jsx b/common/components/WalletDecrypt/Keystore.tsx similarity index 80% rename from common/components/WalletDecrypt/Keystore.jsx rename to common/components/WalletDecrypt/Keystore.tsx index 729c815c..aaa07112 100644 --- a/common/components/WalletDecrypt/Keystore.jsx +++ b/common/components/WalletDecrypt/Keystore.tsx @@ -1,33 +1,33 @@ +import { isKeystorePassRequired } from 'libs/keystore'; import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; -import { isKeystorePassRequired } from 'libs/keystore'; -export type KeystoreValue = { - file: string, - password: string, - valid: boolean -}; +export interface KeystoreValue { + file: string; + password: string; + valid: boolean; +} function isPassRequired(file: string): boolean { let passReq = false; try { passReq = isKeystorePassRequired(file); } catch (e) { - //TODO: communicate invalid file to user + // TODO: communicate invalid file to user } return passReq; } export default class KeystoreDecrypt extends Component { - props: { - value: KeystoreValue, - onChange: (value: KeystoreValue) => void, - onUnlock: () => void + public props: { + value: KeystoreValue; + onChange(value: KeystoreValue): void; + onUnlock(): void; }; - render() { + public render() { const { file, password } = this.props.value; - let passReq = isPassRequired(file); + const passReq = isPassRequired(file); return (
    @@ -47,7 +47,7 @@ export default class KeystoreDecrypt extends Component { {translate('ADD_Radio_2_short')} @@ -74,7 +74,7 @@ export default class KeystoreDecrypt extends Component { ); } - onKeyDown = (e: SyntheticKeyboardEvent) => { + public onKeyDown = (e: any) => { if (e.keyCode === 13) { e.preventDefault(); e.stopPropagation(); @@ -82,7 +82,7 @@ export default class KeystoreDecrypt extends Component { } }; - onPasswordChange = (e: SyntheticInputEvent) => { + public onPasswordChange = (e: any) => { const valid = this.props.value.file.length && e.target.value.length; this.props.onChange({ ...this.props.value, @@ -91,13 +91,14 @@ export default class KeystoreDecrypt extends Component { }); }; - handleFileSelection = (e: SyntheticInputEvent) => { + public handleFileSelection = (e: any) => { const fileReader = new FileReader(); - const inputFile = e.target.files[0]; + const target = e.target; + const inputFile = target.files[0]; fileReader.onload = () => { const keystore = fileReader.result; - let passReq = isPassRequired(keystore); + const passReq = isPassRequired(keystore); this.props.onChange({ ...this.props.value, diff --git a/common/components/WalletDecrypt/LedgerNano.jsx b/common/components/WalletDecrypt/LedgerNano.tsx similarity index 82% rename from common/components/WalletDecrypt/LedgerNano.jsx rename to common/components/WalletDecrypt/LedgerNano.tsx index 735ee37b..b17fa0db 100644 --- a/common/components/WalletDecrypt/LedgerNano.jsx +++ b/common/components/WalletDecrypt/LedgerNano.tsx @@ -2,11 +2,13 @@ import React, { Component } from 'react'; import translate from 'translations'; export default class LedgerNanoSDecrypt extends Component { - render() { + public render() { return (
    -

    {translate('ADD_Radio_2_alt')}

    +

    + {translate('ADD_Radio_2_alt')} +

    @@ -14,7 +16,7 @@ export default class LedgerNanoSDecrypt extends Component { {translate('ADD_Radio_2_short')} diff --git a/common/components/WalletDecrypt/Mnemonic.jsx b/common/components/WalletDecrypt/Mnemonic.tsx similarity index 67% rename from common/components/WalletDecrypt/Mnemonic.jsx rename to common/components/WalletDecrypt/Mnemonic.tsx index 98cc58ce..c170d0dd 100644 --- a/common/components/WalletDecrypt/Mnemonic.jsx +++ b/common/components/WalletDecrypt/Mnemonic.tsx @@ -1,29 +1,30 @@ +import { mnemonicToSeed, validateMnemonic } from 'bip39'; +import DPATHS from 'config/dpaths'; import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; -import { validateMnemonic, mnemonicToSeed } from 'bip39'; - import DeterministicWalletsModal from './DeterministicWalletsModal'; -import DPATHS from 'config/dpaths.js'; const DEFAULT_PATH = DPATHS.MNEMONIC[0].value; -type State = { - phrase: string, - pass: string, - seed: string, - dPath: string -}; +interface Props { + onUnlock(param: any): void; +} +interface State { + phrase: string; + pass: string; + seed: string; + dPath: string; +} -export default class MnemonicDecrypt extends Component { - props: { onUnlock: any => void }; - state: State = { +export default class MnemonicDecrypt extends Component { + public state: State = { phrase: '', pass: '', seed: '', dPath: DEFAULT_PATH }; - render() { + public render() { const { phrase, seed, dPath, pass } = this.state; const isValidMnemonic = validateMnemonic(phrase); @@ -42,7 +43,7 @@ export default class MnemonicDecrypt extends Component { value={phrase} onChange={this.onMnemonicChange} placeholder={translateRaw('x_Mnemonic')} - rows="4" + rows={4} />
    @@ -72,45 +73,47 @@ export default class MnemonicDecrypt extends Component { seed={seed} dPath={dPath} dPaths={DPATHS.MNEMONIC} - onCancel={this._handleCancel} - onConfirmAddress={this._handleUnlock} - onPathChange={this._handlePathChange} + onCancel={this.handleCancel} + onConfirmAddress={this.handleUnlock} + onPathChange={this.handlePathChange} walletType={translateRaw('x_Mnemonic')} />
    ); } - onPasswordChange = (e: SyntheticInputEvent) => { - this.setState({ pass: e.target.value }); + public onPasswordChange = (e: React.SyntheticEvent) => { + this.setState({ pass: (e.target as HTMLInputElement).value }); }; - onMnemonicChange = (e: SyntheticInputEvent) => { - this.setState({ phrase: e.target.value }); + public onMnemonicChange = (e: React.SyntheticEvent) => { + this.setState({ phrase: (e.target as HTMLTextAreaElement).value }); }; - onDWModalOpen = (e: SyntheticInputEvent) => { + public onDWModalOpen = (e: React.SyntheticEvent) => { const { phrase, pass } = this.state; - if (!validateMnemonic(phrase)) return; + if (!validateMnemonic(phrase)) { + return; + } try { - let seed = mnemonicToSeed(phrase.trim(), pass).toString('hex'); + const seed = mnemonicToSeed(phrase.trim(), pass).toString('hex'); this.setState({ seed }); } catch (err) { console.log(err); } }; - _handleCancel = () => { + private handleCancel = () => { this.setState({ seed: '' }); }; - _handlePathChange = (dPath: string) => { + private handlePathChange = (dPath: string) => { this.setState({ dPath }); }; - _handleUnlock = (address, index) => { + private handleUnlock = (address, index) => { const { phrase, pass, dPath } = this.state; this.props.onUnlock({ diff --git a/common/components/WalletDecrypt/PrivateKey.jsx b/common/components/WalletDecrypt/PrivateKey.tsx similarity index 76% rename from common/components/WalletDecrypt/PrivateKey.jsx rename to common/components/WalletDecrypt/PrivateKey.tsx index 101e03bf..0e564144 100644 --- a/common/components/WalletDecrypt/PrivateKey.jsx +++ b/common/components/WalletDecrypt/PrivateKey.tsx @@ -1,13 +1,12 @@ -// @flow +import { isValidEncryptedPrivKey, isValidPrivKey } from 'libs/validators'; import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; -import { isValidPrivKey, isValidEncryptedPrivKey } from 'libs/validators'; -export type PrivateKeyValue = { - key: string, - password: string, - valid: boolean -}; +export interface PrivateKeyValue { + key: string; + password: string; + valid: boolean; +} function fixPkey(key) { if (key.indexOf('0x') === 0) { @@ -16,14 +15,14 @@ function fixPkey(key) { return key; } -type validated = { - fixedPkey: string, - isValidPkey: boolean, - isPassRequired: boolean, - valid: boolean -}; +interface Validated { + fixedPkey: string; + isValidPkey: boolean; + isPassRequired: boolean; + valid: boolean; +} -function validatePkeyAndPass(pkey: string, pass: string): validated { +function validatePkeyAndPass(pkey: string, pass: string): Validated { const fixedPkey = fixPkey(pkey); const validPkey = isValidPrivKey(fixedPkey); const validEncPkey = isValidEncryptedPrivKey(fixedPkey); @@ -46,13 +45,13 @@ function validatePkeyAndPass(pkey: string, pass: string): validated { } export default class PrivateKeyDecrypt extends Component { - props: { - value: PrivateKeyValue, - onChange: (value: PrivateKeyValue) => void, - onUnlock: () => void + public props: { + value: PrivateKeyValue; + onChange(value: PrivateKeyValue): void; + onUnlock(): void; }; - render() { + public render() { const { key, password } = this.props.value; const { isValidPkey, isPassRequired } = validatePkeyAndPass(key, password); @@ -72,7 +71,7 @@ export default class PrivateKeyDecrypt extends Component { onChange={this.onPkeyChange} onKeyDown={this.onKeyDown} placeholder={translateRaw('x_PrivKey2')} - rows="4" + rows={4} />
    {isValidPkey && @@ -97,17 +96,17 @@ export default class PrivateKeyDecrypt extends Component { ); } - onPkeyChange = (e: SyntheticInputEvent) => { - const pkey = e.target.value; + public onPkeyChange = (e: React.SyntheticEvent) => { + const pkey = (e.target as HTMLInputElement).value; const pass = this.props.value.password; const { fixedPkey, valid } = validatePkeyAndPass(pkey, pass); this.props.onChange({ ...this.props.value, key: fixedPkey, valid }); }; - onPasswordChange = (e: SyntheticInputEvent) => { + public onPasswordChange = (e: React.SyntheticEvent) => { const pkey = this.props.value.key; - const pass = e.target.value; + const pass = (e.target as HTMLInputElement).value; const { valid } = validatePkeyAndPass(pkey, pass); this.props.onChange({ @@ -117,7 +116,7 @@ export default class PrivateKeyDecrypt extends Component { }); }; - onKeyDown = (e: SyntheticKeyboardEvent) => { + public onKeyDown = (e: any) => { if (e.keyCode === 13) { e.preventDefault(); e.stopPropagation(); diff --git a/common/components/WalletDecrypt/Trezor.jsx b/common/components/WalletDecrypt/Trezor.tsx similarity index 70% rename from common/components/WalletDecrypt/Trezor.jsx rename to common/components/WalletDecrypt/Trezor.tsx index dba45ddc..cd29ebef 100644 --- a/common/components/WalletDecrypt/Trezor.jsx +++ b/common/components/WalletDecrypt/Trezor.tsx @@ -1,24 +1,25 @@ -// @flow -import './Trezor.scss'; +import DPATHS from 'config/dpaths'; +import TrezorWallet from 'libs/wallet/trezor'; import React, { Component } from 'react'; -import translate from 'translations'; +import translate, { translateRaw } from 'translations'; import TrezorConnect from 'vendor/trezor-connect'; import DeterministicWalletsModal from './DeterministicWalletsModal'; -import TrezorWallet from 'libs/wallet/trezor'; -import DPATHS from 'config/dpaths.js'; +import './Trezor.scss'; const DEFAULT_PATH = DPATHS.TREZOR[0].value; -type State = { - publicKey: string, - chainCode: string, - dPath: string, - error: ?string, - isLoading: boolean -}; +interface Props { + onUnlock(param: any): void; +} +interface State { + publicKey: string; + chainCode: string; + dPath: string; + error: string | null; + isLoading: boolean; +} -export default class TrezorDecrypt extends Component { - props: { onUnlock: any => void }; - state: State = { +export default class TrezorDecrypt extends Component { + public state: State = { publicKey: '', chainCode: '', dPath: DEFAULT_PATH, @@ -26,50 +27,7 @@ export default class TrezorDecrypt extends Component { isLoading: false }; - _handlePathChange = (dPath: string) => { - this._handleConnect(dPath); - }; - - _handleConnect = (dPath: string = this.state.dPath) => { - this.setState({ - isLoading: true, - error: null - }); - - TrezorConnect.getXPubKey( - dPath, - res => { - if (res.success) { - this.setState({ - dPath, - publicKey: res.publicKey, - chainCode: res.chainCode, - isLoading: false - }); - } else { - this.setState({ - error: res.error, - isLoading: false - }); - } - }, - '1.5.2' - ); - }; - - _handleCancel = () => { - this.setState({ - publicKey: '', - chainCode: '', - dPath: DEFAULT_PATH - }); - }; - - _handleUnlock = (address: string, index: number) => { - this.props.onUnlock(new TrezorWallet(address, this.state.dPath, index)); - }; - - render() { + public render() { const { dPath, publicKey, chainCode, error, isLoading } = this.state; const showErr = error ? 'is-showing' : ''; @@ -77,7 +35,7 @@ export default class TrezorDecrypt extends Component {
    ); } + + private handlePathChange = (dPath: string) => { + this.handleConnect(dPath); + }; + + private handleConnect = (dPath: string = this.state.dPath): void => { + this.setState({ + isLoading: true, + error: null + }); + + // TODO: type vendor file + (TrezorConnect as any).getXPubKey( + dPath, + res => { + if (res.success) { + this.setState({ + dPath, + publicKey: res.publicKey, + chainCode: res.chainCode, + isLoading: false + }); + } else { + this.setState({ + error: res.error, + isLoading: false + }); + } + }, + '1.5.2' + ); + }; + + private handleCancel = () => { + this.setState({ + publicKey: '', + chainCode: '', + dPath: DEFAULT_PATH + }); + }; + + private handleUnlock = (address: string, index: number) => { + this.props.onUnlock(new TrezorWallet(address, this.state.dPath, index)); + }; + + private handleNullConnect(): void { + return this.handleConnect(); + } } diff --git a/common/components/WalletDecrypt/ViewOnly.jsx b/common/components/WalletDecrypt/ViewOnly.tsx similarity index 82% rename from common/components/WalletDecrypt/ViewOnly.jsx rename to common/components/WalletDecrypt/ViewOnly.tsx index 5c593e1c..d19dfa0a 100644 --- a/common/components/WalletDecrypt/ViewOnly.jsx +++ b/common/components/WalletDecrypt/ViewOnly.tsx @@ -2,11 +2,13 @@ import React, { Component } from 'react'; import translate from 'translations'; export default class ViewOnlyDecrypt extends Component { - render() { + public render() { return (
    -

    {translate('ADD_Radio_2_alt')}

    +

    + {translate('ADD_Radio_2_alt')} +

    @@ -14,7 +16,7 @@ export default class ViewOnlyDecrypt extends Component { {translate('ADD_Radio_2_short')} diff --git a/common/components/WalletDecrypt/index.jsx b/common/components/WalletDecrypt/index.tsx similarity index 76% rename from common/components/WalletDecrypt/index.jsx rename to common/components/WalletDecrypt/index.tsx index 4935b81f..15ff4b4f 100644 --- a/common/components/WalletDecrypt/index.jsx +++ b/common/components/WalletDecrypt/index.tsx @@ -1,22 +1,24 @@ -// @flow +import { + setWallet, + unlockKeystore, + UnlockKeystoreAction, + unlockMnemonic, + UnlockMnemonicAction, + unlockPrivateKey, + UnlockPrivateKeyAction +} from 'actions/wallet'; +import isEmpty from 'lodash/isEmpty'; +import map from 'lodash/map'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; import translate from 'translations'; import KeystoreDecrypt from './Keystore'; -import PrivateKeyDecrypt from './PrivateKey'; -import type { PrivateKeyValue } from './PrivateKey'; -import MnemonicDecrypt from './Mnemonic'; import LedgerNanoSDecrypt from './LedgerNano'; +import MnemonicDecrypt from './Mnemonic'; +import PrivateKeyDecrypt, { PrivateKeyValue } from './PrivateKey'; import TrezorDecrypt from './Trezor'; import ViewOnlyDecrypt from './ViewOnly'; -import map from 'lodash/map'; -import { - unlockPrivateKey, - unlockKeystore, - unlockMnemonic, - setWallet -} from 'actions/wallet'; -import { connect } from 'react-redux'; -import isEmpty from 'lodash/isEmpty'; const WALLETS = { 'keystore-file': { @@ -26,7 +28,8 @@ const WALLETS = { file: '', password: '' }, - unlock: unlockKeystore + unlock: unlockKeystore, + disabled: false }, 'private-key': { lid: 'x_PrivKey2', @@ -35,13 +38,15 @@ const WALLETS = { key: '', password: '' }, - unlock: unlockPrivateKey + unlock: unlockPrivateKey, + disabled: false }, 'mnemonic-phrase': { lid: 'x_Mnemonic', component: MnemonicDecrypt, initialParams: {}, - unlock: unlockMnemonic + unlock: unlockMnemonic, + disabled: false }, 'ledger-nano-s': { lid: 'x_Ledger', @@ -52,7 +57,8 @@ const WALLETS = { lid: 'x_Trezor', component: TrezorDecrypt, initialParams: {}, - unlock: setWallet + unlock: setWallet, + disabled: false }, 'view-only': { lid: 'View with Address Only', @@ -63,22 +69,25 @@ const WALLETS = { type UnlockParams = {} | PrivateKeyValue; -type State = { - selectedWalletKey: string, - value: UnlockParams -}; +interface Props { + // FIXME + dispatch: Dispatch< + UnlockKeystoreAction | UnlockMnemonicAction | UnlockPrivateKeyAction + >; +} -export class WalletDecrypt extends Component { - props: { - // FIXME - dispatch: (action: any) => void - }; - state: State = { +interface State { + selectedWalletKey: string; + value: UnlockParams; +} + +export class WalletDecrypt extends Component { + public state: State = { selectedWalletKey: 'keystore-file', value: WALLETS['keystore-file'].initialParams }; - getDecryptionComponent() { + public getDecryptionComponent() { const { selectedWalletKey, value } = this.state; const selectedWallet = WALLETS[selectedWalletKey]; @@ -95,7 +104,7 @@ export class WalletDecrypt extends Component { ); } - buildWalletOptions() { + public buildWalletOptions() { return map(WALLETS, (wallet, key) => { const isSelected = this.state.selectedWalletKey === key; @@ -119,20 +128,22 @@ export class WalletDecrypt extends Component { }); } - handleDecryptionChoiceChange = (event: SyntheticInputEvent) => { - const wallet = WALLETS[event.target.value]; + public handleDecryptionChoiceChange = ( + event: React.SyntheticEvent + ) => { + const wallet = WALLETS[(event.target as HTMLInputElement).value]; if (!wallet) { return; } this.setState({ - selectedWalletKey: event.target.value, + selectedWalletKey: (event.target as HTMLInputElement).value, value: wallet.initialParams }); }; - render() { + public render() { const decryptionComponent = this.getDecryptionComponent(); return ( @@ -146,14 +157,14 @@ export class WalletDecrypt extends Component {
    {decryptionComponent} - {!!this.state.value.valid && + {!!(this.state.value as PrivateKeyValue).valid &&

    {translate('ADD_Label_6')}

    { + public onChange = (value: UnlockParams) => { this.setState({ value }); }; - onUnlock = (payload: any) => { + public onUnlock = (payload: any) => { // some components (TrezorDecrypt) don't take an onChange prop, and thus this.state.value will remain unpopulated. // in this case, we can expect the payload to contain the unlocked wallet info. const unlockValue = diff --git a/common/components/index.js b/common/components/index.ts similarity index 96% rename from common/components/index.js rename to common/components/index.ts index b703b27b..ff1aa68b 100644 --- a/common/components/index.js +++ b/common/components/index.ts @@ -1,5 +1,3 @@ -// @flow - export { default as Header } from './Header'; export { default as Footer } from './Footer'; export { default as Root } from './Root'; diff --git a/common/components/ui/Dropdown.jsx b/common/components/ui/Dropdown.tsx similarity index 73% rename from common/components/ui/Dropdown.jsx rename to common/components/ui/Dropdown.tsx index e7948c58..18cc22d1 100644 --- a/common/components/ui/Dropdown.jsx +++ b/common/components/ui/Dropdown.tsx @@ -1,38 +1,31 @@ -// @flow import React, { Component } from 'react'; -type Props = { - value: T, - options: T[], - ariaLabel: string, - formatTitle: (option: T) => any, - extra?: any, - onChange: (value: T) => void -}; +interface Props { + value: T; + options: T[]; + ariaLabel: string; + extra?: any; + formatTitle(option: T): any; + onChange(value: T): void; +} -type State = { - expanded: boolean -}; +interface State { + expanded: boolean; +} -export default class DropdownComponent extends Component< - void, - Props, - State -> { - props: Props; - - state = { +export default class DropdownComponent extends Component, State> { + public state = { expanded: false }; - render() { + public render() { const { options, value, ariaLabel, extra } = this.props; const { expanded } = this.state; return (
    - {content || children} {/* Keep content for short-hand text insertion */} - ; - -export default NewTabLink; diff --git a/common/components/ui/NewTabLink.tsx b/common/components/ui/NewTabLink.tsx new file mode 100644 index 00000000..520986fe --- /dev/null +++ b/common/components/ui/NewTabLink.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +interface AAttributes { + charset?: string; + coords?: string; + download?: string; + href: string; + hreflang?: string; + media?: string; + name?: string; + rel?: + | 'alternate' + | 'author' + | 'bookmark' + | 'external' + | 'help' + | 'license' + | 'next' + | 'nofollow' + | 'noreferrer' + | 'noopener' + | 'prev' + | 'search' + | 'tag'; + rev?: string; + shape?: 'default' | 'rect' | 'circle' | 'poly'; + target?: '_blank' | '_parent' | '_self' | '_top'; + type?: string; +} + +interface NewTabLinkProps extends AAttributes { + content?: React.ReactElement | string; + children?: React.ReactElement | string; +} + +const NewTabLink = ({ content, children, ...rest }: NewTabLinkProps) => + + {content || children} {/* Keep content for short-hand text insertion */} + ; + +export default NewTabLink; diff --git a/common/components/ui/QRCode.jsx b/common/components/ui/QRCode.tsx similarity index 62% rename from common/components/ui/QRCode.jsx rename to common/components/ui/QRCode.tsx index 9f90609a..043f4669 100644 --- a/common/components/ui/QRCode.jsx +++ b/common/components/ui/QRCode.tsx @@ -1,59 +1,36 @@ -// @flow -import React from 'react'; import QRCodeLib from 'qrcode'; +import React from 'react'; // FIXME should store limited amount if history // data -> qr cache const cache: { [key: string]: string } = {}; -type Props = { - data: string -}; +interface Props { + data: string; +} -type State = { - qr?: string -}; +interface State { + qr?: string; +} -export default class QRCode extends React.Component { - props: Props; - state: State = {}; - - componentWillMount() { +export default class QRCode extends React.Component { + public state: State = {} + public componentWillMount() { + console.error(this.props.data) // Start generating QR codes immediately - this._generateQrCode(this.props.data); + this.generateQrCode(this.props.data); } - componentWillReceiveProps(nextProps: Props) { + public componentWillReceiveProps(nextProps: Props) { + console.error(this.props.data) + // Regenerate QR codes if props change if (nextProps.data !== this.props.data) { - this._generateQrCode(nextProps.data); + this.generateQrCode(nextProps.data); } } - _generateQrCode(value: string) { - if (cache[value]) { - this.setState({ qr: cache[value] }); - return; - } - QRCodeLib.toDataURL( - value, - { - color: { - dark: '#000', - light: '#fff' - }, - margin: 0, - errorCorrectionLevel: 'H' - }, - (err, qr) => { - if (err) return; - cache[value] = qr; - this.setState({ qr }); - } - ); - } - - render() { + public render() { const { qr } = this.state; if (!qr) { return null; @@ -68,4 +45,30 @@ export default class QRCode extends React.Component { /> ); } + + private generateQrCode(value: string) { + if (cache[value]) { + this.setState({ qr: cache[value] }); + return; + } + console.error(value, 'Value') + QRCodeLib.toDataURL( + value, + { + color: { + dark: '#000', + light: '#fff' + }, + margin: 0, + errorCorrectionLevel: 'H' + }, + (err, qr) => { + if (err) { + return; + } + cache[value] = qr; + this.setState({ qr }); + } + ); + } } diff --git a/common/components/ui/SimpleButton.jsx b/common/components/ui/SimpleButton.tsx similarity index 66% rename from common/components/ui/SimpleButton.jsx rename to common/components/ui/SimpleButton.tsx index 08daa490..9d6c00b4 100644 --- a/common/components/ui/SimpleButton.jsx +++ b/common/components/ui/SimpleButton.tsx @@ -1,12 +1,9 @@ -// @flow import React, { Component } from 'react'; -import type { Element } from 'react'; +import Spinner from './Spinner'; const DEFAULT_BUTTON_TYPE = 'primary'; const DEFAULT_BUTTON_SIZE = 'lg'; -import Spinner from './Spinner'; - type ButtonType = | 'default' | 'primary' @@ -16,26 +13,24 @@ type ButtonType = | 'danger'; type ButtonSize = 'lg' | 'sm' | 'xs'; -type Props = { - onClick: () => any, - text: Element<*> | string, - loading?: boolean, - disabled?: boolean, - loadingText?: string, - size?: ButtonSize, - type?: ButtonType -}; +interface Props { + text: React.ReactElement | string; + loading?: boolean; + disabled?: boolean; + loadingText?: string; + size?: ButtonSize; + type?: ButtonType; + onClick(): any; +} -export default class SimpleButton extends Component { - props: Props; - - computedClass = () => { +export default class SimpleButton extends Component { + public computedClass = () => { return `btn btn-${this.props.size || DEFAULT_BUTTON_TYPE} btn-${this.props .type || DEFAULT_BUTTON_SIZE}`; }; - render() { - let { loading, disabled, loadingText, text, onClick } = this.props; + public render() { + const { loading, disabled, loadingText, text, onClick } = this.props; return (
    diff --git a/common/components/ui/SimpleDropDown.jsx b/common/components/ui/SimpleDropDown.tsx similarity index 71% rename from common/components/ui/SimpleDropDown.jsx rename to common/components/ui/SimpleDropDown.tsx index fa951cb4..90793f8d 100644 --- a/common/components/ui/SimpleDropDown.jsx +++ b/common/components/ui/SimpleDropDown.tsx @@ -1,33 +1,32 @@ -// @flow import React, { Component } from 'react'; -type Props = { - value?: string, - options: string[], - onChange: (value: string) => void -}; +interface Props { + value?: string; + options: string[]; + onChange(value: string): void; +} -export default class SimpleDropDown extends Component { - props: Props; - state: { - expanded: boolean - } = { +interface State { + expanded: boolean; +} +export default class SimpleDropDown extends Component { + public state = { expanded: false }; - toggleExpanded = () => { + public toggleExpanded = () => { this.setState(state => { return { expanded: !state.expanded }; }); }; - onClick = (event: SyntheticInputEvent) => { - const value = event.target.getAttribute('data-value') || ''; + public onClick = (event: React.SyntheticEvent) => { + const value = (event.target as HTMLAnchorElement).getAttribute('data-value') || ''; this.props.onChange(value); this.setState({ expanded: false }); }; - render() { + public render() { const { options, value } = this.props; const { expanded } = this.state; return ( diff --git a/common/components/ui/SimpleSelect.jsx b/common/components/ui/SimpleSelect.tsx similarity index 66% rename from common/components/ui/SimpleSelect.jsx rename to common/components/ui/SimpleSelect.tsx index 49cca406..c9b57c78 100644 --- a/common/components/ui/SimpleSelect.jsx +++ b/common/components/ui/SimpleSelect.tsx @@ -1,16 +1,13 @@ -// @flow import React, { Component } from 'react'; -type Props = { - value?: string, - options: string[], - onChange: (event: SyntheticInputEvent) => void -}; +interface Props { + value?: string; + options: string[]; + onChange(event: React.SyntheticEvent): void; +} -export default class SimpleSelect extends Component { - props: Props; - - render() { +export default class SimpleSelect extends Component { + public render() { return (