diff --git a/src/actions/ModalActions.js b/src/actions/ModalActions.js index 0ca8f9fa..a9def9fe 100644 --- a/src/actions/ModalActions.js +++ b/src/actions/ModalActions.js @@ -13,8 +13,9 @@ import type { State } from 'reducers/ModalReducer'; export type ModalAction = { type: typeof MODAL.CLOSE } | { - type: typeof MODAL.REMEMBER, - device: TrezorDevice + type: typeof MODAL.OPEN_EXTERNAL_WALLET, + id: string, + url: string, }; export const onPinSubmit = (value: string): Action => { @@ -38,13 +39,6 @@ export const onPassphraseSubmit = (passphrase: string): AsyncAction => async (di }); }; -// export const askForRemember = (device: TrezorDevice): Action => { -// return { -// type: MODAL.REMEMBER, -// device -// } -// } - export const onRememberDevice = (device: TrezorDevice): Action => ({ type: CONNECT.REMEMBER, device, @@ -77,9 +71,9 @@ export const onRememberRequest = (prevState: State): ThunkAction => (dispatch: D const state: State = getState().modal; // handle case where forget modal is already opened // TODO: 2 modals at once (two devices disconnected in the same time) - if (prevState.opened && prevState.windowType === CONNECT.REMEMBER_REQUEST) { + if (prevState.context === MODAL.CONTEXT_DEVICE && prevState.windowType === CONNECT.REMEMBER_REQUEST) { // forget current (new) - if (state.opened) { + if (state.context === MODAL.CONTEXT_DEVICE) { dispatch({ type: CONNECT.FORGET, device: state.device, @@ -98,7 +92,7 @@ export const onDeviceConnect = (device: Device): ThunkAction => (dispatch: Dispa // interrupt process of remembering device (force forget) // TODO: the same for disconnect more than 1 device at once const { modal } = getState(); - if (modal.opened && modal.windowType === CONNECT.REMEMBER_REQUEST) { + if (modal.context === MODAL.CONTEXT_DEVICE && modal.windowType === CONNECT.REMEMBER_REQUEST) { if (device.features && modal.device && modal.device.features && modal.device.features.device_id === device.features.device_id) { dispatch({ type: MODAL.CLOSE, @@ -124,6 +118,16 @@ export const onWalletTypeRequest = (device: TrezorDevice, hidden: boolean, state }); }; +export const gotoExternalWallet = (id: string, url: string): ThunkAction => (dispatch: Dispatch): void => { + console.warn('OPEN', id, url); + dispatch({ + type: MODAL.OPEN_EXTERNAL_WALLET, + id, + url, + }); +}; + + export default { onPinSubmit, onPassphraseSubmit, @@ -134,4 +138,5 @@ export default { onCancel, onDuplicateDevice, onWalletTypeRequest, + gotoExternalWallet, }; \ No newline at end of file diff --git a/src/actions/RouterActions.js b/src/actions/RouterActions.js index e47a72d8..b764dcb9 100644 --- a/src/actions/RouterActions.js +++ b/src/actions/RouterActions.js @@ -1,6 +1,7 @@ /* @flow */ import { push, LOCATION_CHANGE } from 'react-router-redux'; +import { CONTEXT_NONE } from 'actions/constants/modal'; import { routes } from 'support/routes'; import * as deviceUtils from 'utils/device'; @@ -138,7 +139,7 @@ export const getValidUrl = (action: RouterAction): PayloadAction => (dis // Modal is opened // redirect to previous url - if (getState().modal.opened) { + if (getState().modal.context !== CONTEXT_NONE) { // Corner case: modal is opened and currentParams are still valid // example 1 (valid blocking): url changed while passphrase modal opened but device is still connected (we want user to finish this action) // example 2 (invalid blocking): url changes while passphrase modal opened because device disconnect diff --git a/src/actions/TrezorConnectActions.js b/src/actions/TrezorConnectActions.js index d25770c1..85d03cf3 100644 --- a/src/actions/TrezorConnectActions.js +++ b/src/actions/TrezorConnectActions.js @@ -2,6 +2,7 @@ import TrezorConnect, { DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT, } from 'trezor-connect'; +import { CONTEXT_NONE } from 'actions/constants/modal'; import * as CONNECT from 'actions/constants/TrezorConnect'; import * as NOTIFICATION from 'actions/constants/notification'; import { getDuplicateInstanceNumber } from 'reducers/utils'; @@ -236,8 +237,8 @@ export const deviceDisconnect = (device: Device): AsyncAction => async (dispatch if (device.features) { const instances = getState().devices.filter(d => d.features && device.features && d.state && !d.remember && d.features.device_id === device.features.device_id); if (instances.length > 0) { - const isSelected = deviceUtils.isSelectedDevice(getState().wallet.selectedDevice, instances[0]); - if (!isSelected && getState().modal.opened) { + const isSelected = deviceUtils.isSelectedDevice(getState().wallet.selectedDevice, device); + if (!isSelected && getState().modal.context !== CONTEXT_NONE) { dispatch({ type: CONNECT.FORGET_SILENT, device: instances[0], diff --git a/src/actions/constants/modal.js b/src/actions/constants/modal.js index 917b2029..6e53d156 100644 --- a/src/actions/constants/modal.js +++ b/src/actions/constants/modal.js @@ -1,17 +1,7 @@ /* @flow */ - -export const ON_PASSPHRASE_CHANGE: 'action__on_passphrase_change' = 'action__on_passphrase_change'; -export const ON_PASSPHRASE_SHOW: 'action__on_passphrase_show' = 'action__on_passphrase_show'; -export const ON_PASSPHRASE_HIDE: 'action__on_passphrase_hide' = 'action__on_passphrase_hide'; -export const ON_PASSPHRASE_SAVE: 'action__on_passphrase_save' = 'action__on_passphrase_save'; -export const ON_PASSPHRASE_FORGET: 'action__on_passphrase_forget' = 'action__on_passphrase_forget'; -export const ON_PASSPHRASE_FOCUS: 'action__on_passphrase_focus' = 'action__on_passphrase_focus'; -export const ON_PASSPHRASE_BLUR: 'action__on_passphrase_blur' = 'action__on_passphrase_blur'; -export const ON_PASSPHRASE_SUBMIT: 'action__on_passphrase_submit' = 'action__on_passphrase_submit'; - -export const FORGET: 'modal__forget' = 'modal__forget'; -export const REMEMBER: 'modal__remember' = 'modal__remember'; -export const ON_FORGET: 'modal__on_forget' = 'modal__on_forget'; -export const ON_REMEMBER: 'modal__on_remember' = 'modal__on_remember'; export const CLOSE: 'modal__close' = 'modal__close'; +export const OPEN_EXTERNAL_WALLET: 'modal__external_wallet' = 'modal__external_wallet'; +export const CONTEXT_NONE: 'modal_ctx_none' = 'modal_ctx_none'; +export const CONTEXT_DEVICE: 'modal_ctx_device' = 'modal_ctx_device'; +export const CONTEXT_EXTERNAL_WALLET: 'modal_ctx_external-wallet' = 'modal_ctx_external-wallet'; diff --git a/src/components/Link/index.js b/src/components/Link/index.js index d95d6f81..80962b9c 100644 --- a/src/components/Link/index.js +++ b/src/components/Link/index.js @@ -62,7 +62,7 @@ class Link extends PureComponent { , + accounts: $ElementType, + devices: $ElementType, + connect: $ElementType, + selectedAccount: $ElementType, + sendForm: $ElementType, + receive: $ElementType, + localStorage: $ElementType, + wallet: $ElementType, +} + +type DispatchProps = { + modalActions: typeof ModalActions, + receiveActions: typeof ReceiveActions, +} + +export type Props = StateProps & DispatchProps; + +const mapStateToProps: MapStateToProps = (state: State): StateProps => ({ + modal: state.modal, + accounts: state.accounts, + devices: state.devices, + connect: state.connect, + selectedAccount: state.selectedAccount, + sendForm: state.sendForm, + receive: state.receive, + localStorage: state.localStorage, + wallet: state.wallet, +}); + +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + modalActions: bindActionCreators(ModalActions, dispatch), + receiveActions: bindActionCreators(ReceiveActions, dispatch), +}); + +// export default connect(mapStateToProps, mapDispatchToProps)(Modal); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(Modal), +); \ No newline at end of file diff --git a/src/components/modals/confirm/Address/index.js b/src/components/modals/confirm/Address/index.js index b3b4958f..9581561d 100644 --- a/src/components/modals/confirm/Address/index.js +++ b/src/components/modals/confirm/Address/index.js @@ -1,13 +1,16 @@ /* @flow */ import React from 'react'; +import PropTypes from 'prop-types'; import styled from 'styled-components'; -import H3 from 'components/Heading'; + import colors from 'config/colors'; -import P from 'components/Paragraph'; import { FONT_SIZE } from 'config/variables'; -import type { Props } from '../../index'; +import H3 from 'components/Heading'; +import P from 'components/Paragraph'; + +import type { Props } from '../../Container'; const Wrapper = styled.div` width: 390px; @@ -49,4 +52,8 @@ const ConfirmAddress = (props: Props) => { ); }; +ConfirmAddress.propTypes = { + selectedAccount: PropTypes.object.isRequired, +}; + export default ConfirmAddress; \ No newline at end of file diff --git a/src/components/modals/confirm/SignTx/index.js b/src/components/modals/confirm/SignTx/index.js index 3b71afa3..4668979f 100644 --- a/src/components/modals/confirm/SignTx/index.js +++ b/src/components/modals/confirm/SignTx/index.js @@ -1,15 +1,24 @@ /* @flow */ import React from 'react'; +import PropTypes from 'prop-types'; import styled from 'styled-components'; -import colors from 'config/colors'; -import P from 'components/Paragraph'; -import Icon from 'components/Icon'; + import icons from 'config/icons'; -import { H3 } from 'components/Heading'; +import colors from 'config/colors'; import { LINE_HEIGHT } from 'config/variables'; -import type { Props } from '../../index'; +import P from 'components/Paragraph'; +import Icon from 'components/Icon'; +import { H3 } from 'components/Heading'; + +import type { TrezorDevice } from 'flowtype'; +import type { Props as BaseProps } from '../../Container'; + +type Props = { + device: TrezorDevice; + sendForm: $ElementType; +} const Wrapper = styled.div` width: 390px; @@ -39,9 +48,6 @@ const Label = styled.div` `; const ConfirmSignTx = (props: Props) => { - if (!props.modal.opened) return null; - const { device } = props.modal; - const { amount, address, @@ -53,7 +59,7 @@ const ConfirmSignTx = (props: Props) => {
-

Confirm transaction on { device.label } device

+

Confirm transaction on { props.device.label } device

Details are shown on display

@@ -68,4 +74,9 @@ const ConfirmSignTx = (props: Props) => { ); }; +ConfirmSignTx.propTypes = { + device: PropTypes.object.isRequired, + sendForm: PropTypes.object.isRequired, +}; + export default ConfirmSignTx; \ No newline at end of file diff --git a/src/components/modals/confirm/UnverifiedAddress/index.js b/src/components/modals/confirm/UnverifiedAddress/index.js index 1fae5a98..992d3325 100644 --- a/src/components/modals/confirm/UnverifiedAddress/index.js +++ b/src/components/modals/confirm/UnverifiedAddress/index.js @@ -1,15 +1,27 @@ /* @flow */ import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +import icons from 'config/icons'; +import colors from 'config/colors'; + import { H2 } from 'components/Heading'; import P from 'components/Paragraph'; -import styled from 'styled-components'; import Icon from 'components/Icon'; -import colors from 'config/colors'; -import icons from 'config/icons'; import Button from 'components/Button'; import Link from 'components/Link'; -import type { Props } from '../../index'; +import type { TrezorDevice } from 'flowtype'; +import type { Props as BaseProps } from '../../Container'; + +type Props = { + device: TrezorDevice; + account: $ElementType<$ElementType, 'account'>; + showAddress: $ElementType<$ElementType, 'showAddress'>; + showUnverifiedAddress: $ElementType<$ElementType, 'showUnverifiedAddress'>; + onCancel: $ElementType<$ElementType, 'onCancel'>; +} const StyledLink = styled(Link)` position: absolute; @@ -56,29 +68,20 @@ class ConfirmUnverifiedAddress extends PureComponent { } verifyAddress() { - if (!this.props.modal.opened) return; - const { account } = this.props.selectedAccount; + const { account, onCancel, showAddress } = this.props; if (!account) return; - this.props.modalActions.onCancel(); - this.props.receiveActions.showAddress(account.addressPath); + onCancel(); + showAddress(account.addressPath); } showUnverifiedAddress() { - if (!this.props.modal.opened) return; - - this.props.modalActions.onCancel(); - this.props.receiveActions.showUnverifiedAddress(); + const { onCancel, showUnverifiedAddress } = this.props; + onCancel(); + showUnverifiedAddress(); } render() { - if (!this.props.modal.opened) return null; - const { - device, - } = this.props.modal; - - const { - onCancel, - } = this.props.modalActions; + const { device, account, onCancel } = this.props; let deviceStatus: string; let claim: string; @@ -101,7 +104,7 @@ class ConfirmUnverifiedAddress extends PureComponent {

{ deviceStatus }

To prevent phishing attacks, you should verify the address on your TREZOR first. { claim } to continue with the verification process. - (!this.props.selectedAccount.account ? this.verifyAddress() : 'false')}>Try again + (!account ? this.verifyAddress() : 'false')}>Try again this.showUnverifiedAddress()}>Show unverified address
@@ -109,4 +112,12 @@ class ConfirmUnverifiedAddress extends PureComponent { } } +ConfirmUnverifiedAddress.propTypes = { + device: PropTypes.object.isRequired, + account: PropTypes.object.isRequired, + showAddress: PropTypes.func.isRequired, + showUnverifiedAddress: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +}; + export default ConfirmUnverifiedAddress; \ No newline at end of file diff --git a/src/components/modals/device/Duplicate/index.js b/src/components/modals/device/Duplicate/index.js index 6633a9be..03e5e007 100644 --- a/src/components/modals/device/Duplicate/index.js +++ b/src/components/modals/device/Duplicate/index.js @@ -1,18 +1,30 @@ /* @flow */ import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; import styled from 'styled-components'; + +import icons from 'config/icons'; +import colors from 'config/colors'; +import { FONT_SIZE } from 'config/variables'; + import { H3 } from 'components/Heading'; import P from 'components/Paragraph'; import Button from 'components/Button'; import Input from 'components/inputs/Input'; -import { getDuplicateInstanceNumber } from 'reducers/utils'; -import { FONT_SIZE } from 'config/variables'; import Icon from 'components/Icon'; -import icons from 'config/icons'; -import colors from 'config/colors'; import Link from 'components/Link'; -import type { Props } from 'components/modals/index'; +import { getDuplicateInstanceNumber } from 'reducers/utils'; + +import type { TrezorDevice } from 'flowtype'; +import type { Props as BaseProps } from '../../Container'; + +type Props = { + device: TrezorDevice; + devices: $ElementType; + onDuplicateDevice: $ElementType<$ElementType, 'onDuplicateDevice'>; + onCancel: $ElementType<$ElementType, 'onCancel'>; +} type State = { defaultName: string; @@ -62,17 +74,14 @@ const ErrorMessage = styled.div` width: 100%; `; -export default class DuplicateDevice extends PureComponent { +class DuplicateDevice extends PureComponent { constructor(props: Props) { super(props); - const device = props.modal.opened ? props.modal.device : null; - if (!device) return; - - const instance = getDuplicateInstanceNumber(props.devices, device); + const instance = getDuplicateInstanceNumber(props.devices, props.device); this.state = { - defaultName: `${device.label} (${instance.toString()})`, + defaultName: `${props.device.label} (${instance.toString()})`, instance, instanceName: null, isUsed: false, @@ -114,16 +123,12 @@ export default class DuplicateDevice extends PureComponent { keyboardHandler: (event: KeyboardEvent) => void; submit() { - if (!this.props.modal.opened) return; const extended: Object = { instanceName: this.state.instanceName, instance: this.state.instance }; - this.props.modalActions.onDuplicateDevice({ ...this.props.modal.device, ...extended }); + this.props.onDuplicateDevice({ ...this.props.device, ...extended }); } render() { - if (!this.props.modal.opened) return null; - - const { device } = this.props.modal; - const { onCancel } = this.props.modalActions; + const { device, onCancel } = this.props; const { defaultName, instanceName, @@ -167,4 +172,13 @@ export default class DuplicateDevice extends PureComponent { ); } -} \ No newline at end of file +} + +DuplicateDevice.propTypes = { + device: PropTypes.object.isRequired, + devices: PropTypes.array.isRequired, + onDuplicateDevice: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +}; + +export default DuplicateDevice; \ No newline at end of file diff --git a/src/components/modals/device/Forget/index.js b/src/components/modals/device/Forget/index.js index 9da00a51..d20702ca 100644 --- a/src/components/modals/device/Forget/index.js +++ b/src/components/modals/device/Forget/index.js @@ -1,12 +1,21 @@ /* @flow */ import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; import styled from 'styled-components'; + import { H3 } from 'components/Heading'; import P from 'components/Paragraph'; import Button from 'components/Button'; -import type { Props } from '../../index'; +import type { TrezorDevice } from 'flowtype'; +import type { Props as BaseProps } from '../../Container'; + +type Props = { + device: TrezorDevice; + onForgetSingleDevice: $ElementType<$ElementType, 'onForgetSingleDevice'>; + onCancel: $ElementType<$ElementType, 'onCancel'>; +} const Wrapper = styled.div` width: 360px; @@ -47,26 +56,27 @@ class ForgetDevice extends PureComponent { keyboardHandler: (event: KeyboardEvent) => void; forget() { - if (this.props.modal.opened) { - this.props.modalActions.onForgetSingleDevice(this.props.modal.device); - } + this.props.onForgetSingleDevice(this.props.device); } render() { - if (!this.props.modal.opened) return null; - const { device } = this.props.modal; - const { onCancel } = this.props.modalActions; return ( -

Forget { device.instanceLabel }?

+

Forget { this.props.device.instanceLabel }?

Forgetting only removes the device from the list on the left, your coins are still safe and you can access them by reconnecting your TREZOR again. this.forget()}>Forget - Don't forget + Don't forget
); } } +ForgetDevice.propTypes = { + device: PropTypes.object.isRequired, + onForgetSingleDevice: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +}; + export default ForgetDevice; \ No newline at end of file diff --git a/src/components/modals/device/Remember/index.js b/src/components/modals/device/Remember/index.js index c4dc2c23..937e04bc 100644 --- a/src/components/modals/device/Remember/index.js +++ b/src/components/modals/device/Remember/index.js @@ -1,12 +1,22 @@ /* @flow */ import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; import styled from 'styled-components'; + import { H3 } from 'components/Heading'; import P from 'components/Paragraph'; import Loader from 'components/Loader'; import Button from 'components/Button'; -import type { Props } from '../../index'; +import type { TrezorDevice } from 'flowtype'; +import type { Props as BaseProps } from '../../Container'; + +type Props = { + device: TrezorDevice; + instances: ?Array; + onRememberDevice: $ElementType<$ElementType, 'onRememberDevice'>; + onForgetDevice: $ElementType<$ElementType, 'onForgetDevice'>; +} type State = { countdown: number; @@ -47,7 +57,7 @@ const StyledLoader = styled(Loader)` left: 200px; `; -export default class RememberDevice extends PureComponent { +class RememberDevice extends PureComponent { constructor(props: Props) { super(props); @@ -62,9 +72,7 @@ export default class RememberDevice extends PureComponent { // TODO: possible race condition, // device could be already connected but it didn't emit Device.CONNECT event yet window.clearInterval(this.state.ticker); - if (this.props.modal.opened) { - this.props.modalActions.onForgetDevice(this.props.modal.device); - } + this.props.onForgetDevice(this.props.device); } else { this.setState(previousState => ({ countdown: previousState.countdown - 1, @@ -98,15 +106,11 @@ export default class RememberDevice extends PureComponent { keyboardHandler: (event: KeyboardEvent) => void; forget() { - if (this.props.modal.opened) { - this.props.modalActions.onForgetDevice(this.props.modal.device); - } + this.props.onForgetDevice(this.props.device); } render() { - if (!this.props.modal.opened) return null; - const { device, instances } = this.props.modal; - const { onRememberDevice } = this.props.modalActions; + const { device, instances, onRememberDevice } = this.props; let { label } = device; const devicePlural: string = instances && instances.length > 1 ? 'devices or to remember them' : 'device or to remember it'; @@ -144,4 +148,13 @@ export default class RememberDevice extends PureComponent { ); } -} \ No newline at end of file +} + +RememberDevice.propTypes = { + device: PropTypes.object.isRequired, + instances: PropTypes.array.isRequired, + onRememberDevice: PropTypes.func.isRequired, + onForgetDevice: PropTypes.func.isRequired, +}; + +export default RememberDevice; \ No newline at end of file diff --git a/src/components/modals/device/WalletType/index.js b/src/components/modals/device/WalletType/index.js index fb22b318..eb11b8a9 100644 --- a/src/components/modals/device/WalletType/index.js +++ b/src/components/modals/device/WalletType/index.js @@ -1,18 +1,28 @@ /* @flow */ import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; import styled, { css } from 'styled-components'; + +import icons from 'config/icons'; +import colors from 'config/colors'; + import { H3 } from 'components/Heading'; import P from 'components/Paragraph'; import Button from 'components/Button'; import Tooltip from 'components/Tooltip'; import Icon from 'components/Icon'; import Link from 'components/Link'; -import colors from 'config/colors'; -import icons from 'config/icons'; import WalletTypeIcon from 'components/images/WalletType'; -import type { Props } from 'components/modals/index'; +import type { TrezorDevice } from 'flowtype'; +import type { Props as BaseProps } from '../../Container'; + +type Props = { + device: TrezorDevice; + onWalletTypeRequest: $ElementType<$ElementType, 'onWalletTypeRequest'>; + onCancel: $ElementType<$ElementType, 'onCancel'>; +} const Wrapper = styled.div` width: 360px; @@ -88,15 +98,11 @@ class WalletType extends PureComponent { keyboardHandler: (event: KeyboardEvent) => void; changeType(hidden: boolean, state: ?string) { - const { modal } = this.props; - if (!modal.opened) return; - this.props.modalActions.onWalletTypeRequest(modal.device, hidden, state); + this.props.onWalletTypeRequest(this.props.device, hidden, state); } render() { - if (!this.props.modal.opened) return null; - const { device } = this.props.modal; - const { onCancel } = this.props.modalActions; + const { device, onCancel } = this.props; return ( @@ -109,7 +115,7 @@ class WalletType extends PureComponent { /> )} - Change wallet type for { device.instanceLabel } + { device.state ? 'Change' : 'Select' } wallet type for { device.instanceLabel }
@@ -147,4 +153,10 @@ class WalletType extends PureComponent { } } +WalletType.propTypes = { + device: PropTypes.object.isRequired, + onWalletTypeRequest: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +}; + export default WalletType; \ No newline at end of file diff --git a/src/components/modals/external/NemWallet/images/nem-download.png b/src/components/modals/external/NemWallet/images/nem-download.png new file mode 100644 index 00000000..c7551c31 Binary files /dev/null and b/src/components/modals/external/NemWallet/images/nem-download.png differ diff --git a/src/components/modals/external/NemWallet/index.js b/src/components/modals/external/NemWallet/index.js new file mode 100644 index 00000000..468fe3a0 --- /dev/null +++ b/src/components/modals/external/NemWallet/index.js @@ -0,0 +1,68 @@ +/* @flow */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import colors from 'config/colors'; +import icons from 'config/icons'; +import Icon from 'components/Icon'; +import Link from 'components/Link'; +import Button from 'components/Button'; +import { H3, H4 } from 'components/Heading'; +import P from 'components/Paragraph'; +import coins from 'constants/coins'; + +import NemImage from './images/nem-download.png'; +import type { Props as BaseProps } from '../../Container'; + +type Props = { + onCancel: $ElementType<$ElementType, 'onCancel'>; +} + +const Wrapper = styled.div` + width: 100%; + max-width: 620px; + padding: 24px 48px; +`; + +const StyledButton = styled(Button)` + margin: 0 0 10px 0; + width: 100%; +`; + +const StyledLink = styled(Link)` + position: absolute; + right: 15px; + top: 10px; +`; + +const Img = styled.img` + display: block; + width: 100%; + height: auto; +`; + +const NemWallet = (props: Props) => ( + + + + +

NEM Wallet

+

We have partnered up with the NEM Foundation to provide you with a full-fledged NEM Wallet.

+

Make sure you download the Universal Client for TREZOR support.

+ + i.id === 'xem').url}> + Go to nem.io + +
+); + +NemWallet.propTypes = { + onCancel: PropTypes.func.isRequired, +}; + +export default NemWallet; \ No newline at end of file diff --git a/src/components/modals/index.js b/src/components/modals/index.js index 9a4645cc..54e37ae8 100644 --- a/src/components/modals/index.js +++ b/src/components/modals/index.js @@ -1,64 +1,39 @@ /* @flow */ + import * as React from 'react'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; -import styled from 'styled-components'; -import colors from 'config/colors'; import { CSSTransition } from 'react-transition-group'; +import styled from 'styled-components'; +import colors from 'config/colors'; + import { UI } from 'trezor-connect'; - -import ModalActions from 'actions/ModalActions'; -import ReceiveActions from 'actions/ReceiveActions'; - +import * as MODAL from 'actions/constants/modal'; import * as RECEIVE from 'actions/constants/receive'; import * as CONNECT from 'actions/constants/TrezorConnect'; -import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; -import type { State, Dispatch } from 'flowtype'; +// device context import Pin from 'components/modals/pin/Pin'; import InvalidPin from 'components/modals/pin/Invalid'; - import Passphrase from 'components/modals/passphrase/Passphrase'; import PassphraseType from 'components/modals/passphrase/Type'; - import ConfirmSignTx from 'components/modals/confirm/SignTx'; import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress'; - import ForgetDevice from 'components/modals/device/Forget'; import RememberDevice from 'components/modals/device/Remember'; import DuplicateDevice from 'components/modals/device/Duplicate'; import WalletType from 'components/modals/device/WalletType'; -type OwnProps = { } +// external context +import NemWallet from 'components/modals/external/NemWallet'; -type StateProps = { - modal: $ElementType, - accounts: $ElementType, - devices: $ElementType, - connect: $ElementType, - selectedAccount: $ElementType, - sendForm: $ElementType, - receive: $ElementType, - localStorage: $ElementType, - wallet: $ElementType, -} - -type DispatchProps = { - modalActions: typeof ModalActions, - receiveActions: typeof ReceiveActions, -} - -export type Props = StateProps & DispatchProps; +import type { Props } from './Container'; const Fade = (props: { children: React.Node}) => ( - { props.children } + >{ props.children } ); @@ -85,87 +60,124 @@ const ModalWindow = styled.div` text-align: center; `; -class Modal extends React.PureComponent { - render() { - if (!this.props.modal.opened) return null; +// get modal component with device context +const getDeviceContextModal = (props: Props) => { + const { modal, modalActions } = props; + if (modal.context !== MODAL.CONTEXT_DEVICE) return null; - const { opened, windowType } = this.props.modal; - let component = null; - switch (windowType) { - case UI.REQUEST_PIN: - component = (); - break; - case UI.INVALID_PIN: - component = (); - break; - case UI.REQUEST_PASSPHRASE: - component = (); - break; - case 'ButtonRequest_SignTx': - component = (); - break; - case 'ButtonRequest_PassphraseType': - component = (); - break; - case RECEIVE.REQUEST_UNVERIFIED: - component = (); - break; + switch (modal.windowType) { + case UI.REQUEST_PIN: + return ( + ); - case CONNECT.REMEMBER_REQUEST: - component = (); - break; + case UI.INVALID_PIN: + return ; - case CONNECT.FORGET_REQUEST: - component = (); - break; + case UI.REQUEST_PASSPHRASE: + return ( + ); - case CONNECT.TRY_TO_DUPLICATE: - component = (); - break; + case 'ButtonRequest_PassphraseType': + return ; - case CONNECT.REQUEST_WALLET_TYPE: - component = (); - break; + case 'ButtonRequest_SignTx': + return ; - default: - component = null; - } + case RECEIVE.REQUEST_UNVERIFIED: + return ( + ); - let ch = null; - if (opened) { - ch = ( - - - - { component } - - - - ); - } + case CONNECT.REMEMBER_REQUEST: + return ( + ); - return ch; + case CONNECT.FORGET_REQUEST: + return ( + ); + + case CONNECT.TRY_TO_DUPLICATE: + return ( + ); + + case CONNECT.REQUEST_WALLET_TYPE: + return ( + ); + + default: + return null; } -} +}; -const mapStateToProps: MapStateToProps = (state: State): StateProps => ({ - modal: state.modal, - accounts: state.accounts, - devices: state.devices, - connect: state.connect, - selectedAccount: state.selectedAccount, - sendForm: state.sendForm, - receive: state.receive, - localStorage: state.localStorage, - wallet: state.wallet, -}); +// get modal component with external context +const getExternalContextModal = (props: Props) => { + const { modal, modalActions } = props; + if (modal.context !== MODAL.CONTEXT_EXTERNAL_WALLET) return null; -const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - modalActions: bindActionCreators(ModalActions, dispatch), - receiveActions: bindActionCreators(ReceiveActions, dispatch), -}); + switch (modal.windowType) { + case 'xem': + return (); + default: + return null; + } +}; -// export default connect(mapStateToProps, mapDispatchToProps)(Modal); -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(Modal), -); +// modal container component +const Modal = (props: Props) => { + const { modal } = props; + if (modal.context === MODAL.CONTEXT_NONE) return null; + + let component = null; + switch (modal.context) { + case MODAL.CONTEXT_DEVICE: + component = getDeviceContextModal(props); + break; + case MODAL.CONTEXT_EXTERNAL_WALLET: + component = getExternalContextModal(props); + break; + default: + break; + } + + return ( + + + + { component } + + + + ); +}; + +export default Modal; diff --git a/src/components/modals/passphrase/Passphrase/index.js b/src/components/modals/passphrase/Passphrase/index.js index 9850cbd9..79137762 100644 --- a/src/components/modals/passphrase/Passphrase/index.js +++ b/src/components/modals/passphrase/Passphrase/index.js @@ -1,15 +1,25 @@ /* @flow */ import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; import styled from 'styled-components'; + import colors from 'config/colors'; import { FONT_SIZE, TRANSITION } from 'config/variables'; + import { H2 } from 'components/Heading'; import P from 'components/Paragraph'; import Checkbox from 'components/Checkbox'; import Button from 'components/Button'; import Input from 'components/inputs/Input'; -import type { Props } from '../../index'; +import type { TrezorDevice } from 'flowtype'; +import type { Props as BaseProps } from '../../Container'; + +type Props = { + device: TrezorDevice; + selectedDevice: ?TrezorDevice; + onPassphraseSubmit: $ElementType<$ElementType, 'onPassphraseSubmit'>; +} type State = { deviceLabel: string, @@ -79,15 +89,10 @@ const LinkButton = styled(Button)` class Passphrase extends PureComponent { constructor(props: Props) { super(props); - - const device = props.modal.opened ? props.modal.device : null; - if (!device) { - return; - } + const { device, selectedDevice } = props; // Check if this device is already known // if device is already known then only one input is presented - const { selectedDevice } = props.wallet; let deviceLabel = device.label; let shouldShowSingleInput = false; if (selectedDevice && selectedDevice.path === device.path) { @@ -105,8 +110,6 @@ class Passphrase extends PureComponent { }; } - state: State; - componentDidMount() { this.passphraseInput.focus(); @@ -180,7 +183,7 @@ class Passphrase extends PureComponent { } submitPassphrase(shouldLeavePassphraseBlank: boolean = false) { - const { onPassphraseSubmit } = this.props.modalActions; + const { onPassphraseSubmit } = this.props; const passphrase = this.state.passphraseInputValue; // Reset state so same passphrase isn't filled when the modal will be visible again @@ -197,7 +200,6 @@ class Passphrase extends PureComponent { handleKeyPress(event: KeyboardEvent) { if (event.key === 'Enter') { event.preventDefault(); - console.warn('ENTER', this.state); if (this.state.doPassphraseInputsMatch) { this.submitPassphrase(); } @@ -205,10 +207,6 @@ class Passphrase extends PureComponent { } render() { - if (!this.props.modal.opened) { - return null; - } - return (

Enter {this.state.deviceLabel} passphrase

@@ -277,4 +275,10 @@ class Passphrase extends PureComponent { } } +Passphrase.propTypes = { + device: PropTypes.object.isRequired, + selectedDevice: PropTypes.object.isRequired, + onPassphraseSubmit: PropTypes.func.isRequired, +}; + export default Passphrase; \ No newline at end of file diff --git a/src/components/modals/passphrase/Type/index.js b/src/components/modals/passphrase/Type/index.js index c42cf3af..ce659800 100644 --- a/src/components/modals/passphrase/Type/index.js +++ b/src/components/modals/passphrase/Type/index.js @@ -1,14 +1,21 @@ /* @flow */ import React from 'react'; -import Icon from 'components/Icon'; -import colors from 'config/colors'; -import icons from 'config/icons'; +import PropTypes from 'prop-types'; import styled from 'styled-components'; + +import icons from 'config/icons'; +import colors from 'config/colors'; + +import Icon from 'components/Icon'; import { H3 } from 'components/Heading'; import P from 'components/Paragraph'; -import type { Props } from 'components/modals/index'; +import type { TrezorDevice } from 'flowtype'; + +type Props = { + device: TrezorDevice; +} const Wrapper = styled.div` width: 360px; @@ -17,19 +24,18 @@ const Wrapper = styled.div` const Header = styled.div``; -const Confirmation = (props: Props) => { - if (!props.modal.opened) return null; - const { device } = props.modal; +const PassphraseType = (props: Props) => ( + +
+ +

Complete the action on { props.device.label } device

+

If you enter a wrong passphrase, you will not unlock the desired hidden wallet.

+
+
+); - return ( - -
- -

Complete the action on { device.label } device

-

If you enter a wrong passphrase, you will not unlock the desired hidden wallet.

-
-
- ); +PassphraseType.propTypes = { + device: PropTypes.object.isRequired, }; -export default Confirmation; \ No newline at end of file +export default PassphraseType; \ No newline at end of file diff --git a/src/components/modals/pin/Invalid/index.js b/src/components/modals/pin/Invalid/index.js index 239b21ce..e69d7554 100644 --- a/src/components/modals/pin/Invalid/index.js +++ b/src/components/modals/pin/Invalid/index.js @@ -1,26 +1,31 @@ /* @flow */ import React from 'react'; +import PropTypes from 'prop-types'; import styled from 'styled-components'; + import { H3 } from 'components/Heading'; import P from 'components/Paragraph'; -import type { Props } from '../../index'; +import type { TrezorDevice } from 'flowtype'; + +type Props = { + device: TrezorDevice; +} const Wrapper = styled.div` padding: 24px 48px; `; -const InvalidPin = (props: Props) => { - if (!props.modal.opened) return null; +const InvalidPin = (props: Props) => ( + +

Entered PIN for { props.device.label } is not correct

+

Retrying...

+
+); - const { device } = props.modal; - return ( - -

Entered PIN for { device.label } is not correct

-

Retrying...

-
- ); +InvalidPin.propTypes = { + device: PropTypes.object.isRequired, }; export default InvalidPin; \ No newline at end of file diff --git a/src/components/modals/pin/Pin/index.js b/src/components/modals/pin/Pin/index.js index fa1fc805..7a8eed98 100644 --- a/src/components/modals/pin/Pin/index.js +++ b/src/components/modals/pin/Pin/index.js @@ -1,14 +1,24 @@ /* @flow */ -import P from 'components/Paragraph'; -import { H2 } from 'components/Heading'; import React, { PureComponent } from 'react'; -import Link from 'components/Link'; +import PropTypes from 'prop-types'; import styled from 'styled-components'; +import P from 'components/Paragraph'; +import { H2 } from 'components/Heading'; +import Link from 'components/Link'; import Button from 'components/Button'; + +import type { TrezorDevice } from 'flowtype'; + import PinButton from './components/Button'; import PinInput from './components/Input'; -import type { Props } from '../../index'; + +import type { Props as BaseProps } from '../../Container'; + +type Props = { + device: TrezorDevice; + onPinSubmit: $ElementType<$ElementType, 'onPinSubmit'>; +} type State = { pin: string; @@ -74,7 +84,7 @@ class Pin extends PureComponent { } keyboardHandler(event: KeyboardEvent): void { - const { onPinSubmit } = this.props.modalActions; + const { onPinSubmit } = this.props; const { pin } = this.state; event.preventDefault(); @@ -132,9 +142,7 @@ class Pin extends PureComponent { keyboardHandler: (event: KeyboardEvent) => void; render() { - if (!this.props.modal.opened) return null; - const { onPinSubmit } = this.props.modalActions; - const { device } = this.props.modal; + const { device, onPinSubmit } = this.props; const { pin } = this.state; return ( @@ -173,4 +181,9 @@ class Pin extends PureComponent { } } +Pin.propTypes = { + device: PropTypes.object.isRequired, + onPinSubmit: PropTypes.func.isRequired, +}; + export default Pin; \ No newline at end of file diff --git a/src/constants/coins.js b/src/constants/coins.js index d6a46972..71d4f2e2 100644 --- a/src/constants/coins.js +++ b/src/constants/coins.js @@ -48,5 +48,6 @@ export default [ id: 'xem', coinName: 'NEM', url: 'https://nem.io/downloads/', + external: true, }, ]; \ No newline at end of file diff --git a/src/reducers/ModalReducer.js b/src/reducers/ModalReducer.js index 8cfde96d..a425676a 100644 --- a/src/reducers/ModalReducer.js +++ b/src/reducers/ModalReducer.js @@ -9,92 +9,79 @@ import * as CONNECT from 'actions/constants/TrezorConnect'; import type { Action, TrezorDevice } from 'flowtype'; export type State = { - opened: false; + context: typeof MODAL.CONTEXT_NONE; } | { - opened: true; + context: typeof MODAL.CONTEXT_DEVICE, device: TrezorDevice; instances?: Array; windowType?: string; +} | { + context: typeof MODAL.CONTEXT_EXTERNAL_WALLET, + windowType?: string; } const initialState: State = { - opened: false, - // instances: null, - // windowType: null + context: MODAL.CONTEXT_NONE, }; export default function modal(state: State = initialState, action: Action): State { switch (action.type) { case RECEIVE.REQUEST_UNVERIFIED: - return { - opened: true, - device: action.device, - windowType: action.type, - }; - + case CONNECT.FORGET_REQUEST: + case CONNECT.TRY_TO_DUPLICATE: case CONNECT.REQUEST_WALLET_TYPE: return { - opened: true, + context: MODAL.CONTEXT_DEVICE, device: action.device, windowType: action.type, }; case CONNECT.REMEMBER_REQUEST: return { - opened: true, + context: MODAL.CONTEXT_DEVICE, device: action.device, instances: action.instances, windowType: action.type, }; - case CONNECT.FORGET_REQUEST: - return { - opened: true, - device: action.device, - windowType: action.type, - }; - - case CONNECT.TRY_TO_DUPLICATE: - return { - opened: true, - device: action.device, - windowType: action.type, - }; + // device acquired + // close modal case DEVICE.CHANGED: - if (state.opened && action.device.path === state.device.path && action.device.status === 'occupied') { + if (state.context === MODAL.CONTEXT_DEVICE && action.device.path === state.device.path && action.device.status === 'occupied') { return initialState; } - return state; + // device connected + // close modal if modal context is not 'device' + case DEVICE.CONNECT: + case DEVICE.CONNECT_UNACQUIRED: + if (state.context !== MODAL.CONTEXT_DEVICE) { + return initialState; + } + return state; + + // device with context assigned to modal was disconnected + // close modal case DEVICE.DISCONNECT: - if (state.opened && action.device.path === state.device.path) { + if (state.context === MODAL.CONTEXT_DEVICE && action.device.path === state.device.path) { return initialState; } return state; - // case DEVICE.CONNECT : - // case DEVICE.CONNECT_UNACQUIRED : - // if (state.opened && state.windowType === CONNECT.TRY_TO_FORGET) { - // return { - // ...initialState, - // passphraseCached: state.passphraseCached - // } - // } - // return state; case UI.REQUEST_PIN: case UI.INVALID_PIN: case UI.REQUEST_PASSPHRASE: return { - opened: true, + context: MODAL.CONTEXT_DEVICE, device: action.payload.device, windowType: action.type, }; case UI.REQUEST_BUTTON: return { - opened: true, + context: MODAL.CONTEXT_DEVICE, device: action.payload.device, windowType: action.payload.code, }; @@ -106,6 +93,12 @@ export default function modal(state: State = initialState, action: Action): Stat case CONNECT.REMEMBER: return initialState; + case MODAL.OPEN_EXTERNAL_WALLET: + return { + context: MODAL.CONTEXT_EXTERNAL_WALLET, + windowType: action.id, + }; + default: return state; } diff --git a/src/utils/device.js b/src/utils/device.js index 94924325..8bf3a368 100644 --- a/src/utils/device.js +++ b/src/utils/device.js @@ -2,6 +2,7 @@ import colors from 'config/colors'; +import type { Device } from 'trezor-connect'; import type { TrezorDevice, State, @@ -91,7 +92,7 @@ export const isDeviceAccessible = (device: ?TrezorDevice): boolean => { return device.mode === 'normal' && device.firmware !== 'required'; }; -export const isSelectedDevice = (current: ?TrezorDevice, device: ?TrezorDevice): boolean => !!((current && device && (current.path === device.path && current.instance === device.instance))); +export const isSelectedDevice = (selected: ?TrezorDevice, device: ?(TrezorDevice | Device)): boolean => !!((selected && device && (selected.path === device.path && (device.ts && selected.instance === device.instance)))); export const getVersion = (device: TrezorDevice): string => { let version; diff --git a/src/views/Wallet/components/LeftNavigation/Container.js b/src/views/Wallet/components/LeftNavigation/Container.js index d3c1c18a..f88e87ae 100644 --- a/src/views/Wallet/components/LeftNavigation/Container.js +++ b/src/views/Wallet/components/LeftNavigation/Container.js @@ -7,6 +7,7 @@ import { withRouter } from 'react-router-dom'; import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as DiscoveryActions from 'actions/DiscoveryActions'; import * as RouterActions from 'actions/RouterActions'; +import * as ModalActions from 'actions/ModalActions'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; import type { State, Dispatch } from 'flowtype'; @@ -38,6 +39,7 @@ const mapDispatchToProps: MapDispatchToProps duplicateDevice: bindActionCreators(TrezorConnectActions.duplicateDevice, dispatch), gotoDeviceSettings: bindActionCreators(RouterActions.gotoDeviceSettings, dispatch), onSelectDevice: bindActionCreators(RouterActions.selectDevice, dispatch), + gotoExternalWallet: bindActionCreators(ModalActions.gotoExternalWallet, dispatch), }); export default withRouter( diff --git a/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.js b/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.js index d22b3cd5..8391bc17 100644 --- a/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.js +++ b/src/views/Wallet/components/LeftNavigation/components/CoinMenu/index.js @@ -1,3 +1,5 @@ +/* @flow */ + import styled from 'styled-components'; import coins from 'constants/coins'; import colors from 'config/colors'; @@ -5,12 +7,19 @@ import ICONS from 'config/icons'; import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import { NavLink } from 'react-router-dom'; +import Link from 'components/Link'; import Divider from '../Divider'; import RowCoin from '../RowCoin'; +import type { Props } from '../common'; + const Wrapper = styled.div``; -class CoinMenu extends PureComponent { +const ExternalWallet = styled.div` + cursor: pointer; +`; + +class CoinMenu extends PureComponent { getBaseUrl() { const { selectedDevice } = this.props.wallet; let baseUrl = ''; @@ -24,6 +33,27 @@ class CoinMenu extends PureComponent { return baseUrl; } + getOtherCoins() { + return coins.map((coin) => { + const row = ( + + ); + + if (coin.external) return this.props.gotoExternalWallet(coin.id, coin.url)}>{row}; + return {row}; + }); + } + render() { const { config } = this.props.localStorage; return ( @@ -46,31 +76,16 @@ class CoinMenu extends PureComponent { textRight="(You will be redirected)" hasBorder /> - {coins.map(coin => ( -
- - - ))} + {this.getOtherCoins()} ); } } CoinMenu.propTypes = { - config: PropTypes.object, - wallet: PropTypes.object, - selectedDevice: PropTypes.object, - localStorage: PropTypes.object, + localStorage: PropTypes.object.isRequired, + wallet: PropTypes.object.isRequired, + gotoExternalWallet: PropTypes.func.isRequired, }; export default CoinMenu; diff --git a/src/views/Wallet/components/LeftNavigation/components/common.js b/src/views/Wallet/components/LeftNavigation/components/common.js index c4c45ff4..8a6b9c0e 100644 --- a/src/views/Wallet/components/LeftNavigation/components/common.js +++ b/src/views/Wallet/components/LeftNavigation/components/common.js @@ -2,6 +2,7 @@ import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as DiscoveryActions from 'actions/DiscoveryActions'; import * as RouterActions from 'actions/RouterActions'; +import * as ModalActions from 'actions/ModalActions'; import { toggleDeviceDropdown } from 'actions/WalletActions'; import type { State } from 'flowtype'; @@ -25,6 +26,7 @@ export type DispatchProps = { duplicateDevice: typeof TrezorConnectActions.duplicateDevice, gotoDeviceSettings: typeof RouterActions.gotoDeviceSettings, onSelectDevice: typeof RouterActions.selectDevice, + gotoExternalWallet: typeof ModalActions.gotoExternalWallet, } export type Props = StateProps & DispatchProps; \ No newline at end of file diff --git a/src/views/Wallet/index.js b/src/views/Wallet/index.js index 48eb27aa..78679df7 100644 --- a/src/views/Wallet/index.js +++ b/src/views/Wallet/index.js @@ -11,7 +11,7 @@ import type { State } from 'flowtype'; import Header from 'components/Header'; import Footer from 'components/Footer'; -import ModalContainer from 'components/modals'; +import ModalContainer from 'components/modals/Container'; import AppNotifications from 'components/notifications/App'; import ContextNotifications from 'components/notifications/Context'; diff --git a/src/views/Wallet/views/Account/Receive/index.js b/src/views/Wallet/views/Account/Receive/index.js index 80237ef3..99830694 100644 --- a/src/views/Wallet/views/Account/Receive/index.js +++ b/src/views/Wallet/views/Account/Receive/index.js @@ -148,14 +148,13 @@ const AccountReceive = (props: Props) => { addressUnverified, } = props.receive; + const isAddressVerifying = props.modal.context === 'device' && props.modal.windowType === 'ButtonRequest_Address'; + const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified; + let address = `${account.address.substring(0, 20)}...`; - if (addressVerified - || addressUnverified - || (props.modal.opened && props.modal.windowType === 'ButtonRequest_Address')) { + if (addressVerified || addressUnverified || isAddressVerifying) { ({ address } = account); } - const isAddressVerifying = props.modal.opened && props.modal.windowType === 'ButtonRequest_Address'; - const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified; return (