1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-12-28 18:08:08 +00:00

Merge pull request #174 from trezor/feature/modal-context

Feature/modal context
This commit is contained in:
Vladimir Volek 2018-10-11 15:57:05 +02:00 committed by GitHub
commit 8b332b4763
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 588 additions and 336 deletions

View File

@ -13,8 +13,9 @@ import type { State } from 'reducers/ModalReducer';
export type ModalAction = { export type ModalAction = {
type: typeof MODAL.CLOSE type: typeof MODAL.CLOSE
} | { } | {
type: typeof MODAL.REMEMBER, type: typeof MODAL.OPEN_EXTERNAL_WALLET,
device: TrezorDevice id: string,
url: string,
}; };
export const onPinSubmit = (value: string): Action => { 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 => ({ export const onRememberDevice = (device: TrezorDevice): Action => ({
type: CONNECT.REMEMBER, type: CONNECT.REMEMBER,
device, device,
@ -77,9 +71,9 @@ export const onRememberRequest = (prevState: State): ThunkAction => (dispatch: D
const state: State = getState().modal; const state: State = getState().modal;
// handle case where forget modal is already opened // handle case where forget modal is already opened
// TODO: 2 modals at once (two devices disconnected in the same time) // 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) // forget current (new)
if (state.opened) { if (state.context === MODAL.CONTEXT_DEVICE) {
dispatch({ dispatch({
type: CONNECT.FORGET, type: CONNECT.FORGET,
device: state.device, device: state.device,
@ -98,7 +92,7 @@ export const onDeviceConnect = (device: Device): ThunkAction => (dispatch: Dispa
// interrupt process of remembering device (force forget) // interrupt process of remembering device (force forget)
// TODO: the same for disconnect more than 1 device at once // TODO: the same for disconnect more than 1 device at once
const { modal } = getState(); 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) { if (device.features && modal.device && modal.device.features && modal.device.features.device_id === device.features.device_id) {
dispatch({ dispatch({
type: MODAL.CLOSE, 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 { export default {
onPinSubmit, onPinSubmit,
onPassphraseSubmit, onPassphraseSubmit,
@ -134,4 +138,5 @@ export default {
onCancel, onCancel,
onDuplicateDevice, onDuplicateDevice,
onWalletTypeRequest, onWalletTypeRequest,
gotoExternalWallet,
}; };

View File

@ -1,6 +1,7 @@
/* @flow */ /* @flow */
import { push, LOCATION_CHANGE } from 'react-router-redux'; import { push, LOCATION_CHANGE } from 'react-router-redux';
import { CONTEXT_NONE } from 'actions/constants/modal';
import { routes } from 'support/routes'; import { routes } from 'support/routes';
import * as deviceUtils from 'utils/device'; import * as deviceUtils from 'utils/device';
@ -138,7 +139,7 @@ export const getValidUrl = (action: RouterAction): PayloadAction<string> => (dis
// Modal is opened // Modal is opened
// redirect to previous url // redirect to previous url
if (getState().modal.opened) { if (getState().modal.context !== CONTEXT_NONE) {
// Corner case: modal is opened and currentParams are still valid // 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 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 // example 2 (invalid blocking): url changes while passphrase modal opened because device disconnect

View File

@ -2,6 +2,7 @@
import TrezorConnect, { import TrezorConnect, {
DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT, DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT,
} from 'trezor-connect'; } from 'trezor-connect';
import { CONTEXT_NONE } from 'actions/constants/modal';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
import { getDuplicateInstanceNumber } from 'reducers/utils'; import { getDuplicateInstanceNumber } from 'reducers/utils';
@ -236,8 +237,8 @@ export const deviceDisconnect = (device: Device): AsyncAction => async (dispatch
if (device.features) { 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); 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) { if (instances.length > 0) {
const isSelected = deviceUtils.isSelectedDevice(getState().wallet.selectedDevice, instances[0]); const isSelected = deviceUtils.isSelectedDevice(getState().wallet.selectedDevice, device);
if (!isSelected && getState().modal.opened) { if (!isSelected && getState().modal.context !== CONTEXT_NONE) {
dispatch({ dispatch({
type: CONNECT.FORGET_SILENT, type: CONNECT.FORGET_SILENT,
device: instances[0], device: instances[0],

View File

@ -1,17 +1,7 @@
/* @flow */ /* @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 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';

View File

@ -62,7 +62,7 @@ class Link extends PureComponent {
<A <A
className={this.props.className} className={this.props.className}
href={this.props.href} href={this.props.href}
target="_blank" target={this.props.target || '_blank'}
rel="noreferrer noopener" rel="noreferrer noopener"
onClick={this.props.onClick} onClick={this.props.onClick}
isGreen={this.props.isGreen} isGreen={this.props.isGreen}
@ -84,6 +84,7 @@ Link.propTypes = {
]).isRequired, ]).isRequired,
className: PropTypes.string, className: PropTypes.string,
href: PropTypes.string, href: PropTypes.string,
target: PropTypes.string,
to: PropTypes.string, to: PropTypes.string,
onClick: PropTypes.func, onClick: PropTypes.func,
isGreen: PropTypes.bool, isGreen: PropTypes.bool,

View File

@ -0,0 +1,55 @@
/* @flow */
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import ModalActions from 'actions/ModalActions';
import ReceiveActions from 'actions/ReceiveActions';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype';
import Modal from './index';
type OwnProps = { }
type StateProps = {
modal: $ElementType<State, 'modal'>,
accounts: $ElementType<State, 'accounts'>,
devices: $ElementType<State, 'devices'>,
connect: $ElementType<State, 'connect'>,
selectedAccount: $ElementType<State, 'selectedAccount'>,
sendForm: $ElementType<State, 'sendForm'>,
receive: $ElementType<State, 'receive'>,
localStorage: $ElementType<State, 'localStorage'>,
wallet: $ElementType<State, 'wallet'>,
}
type DispatchProps = {
modalActions: typeof ModalActions,
receiveActions: typeof ReceiveActions,
}
export type Props = StateProps & DispatchProps;
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (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, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
modalActions: bindActionCreators(ModalActions, dispatch),
receiveActions: bindActionCreators(ReceiveActions, dispatch),
});
// export default connect(mapStateToProps, mapDispatchToProps)(Modal);
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Modal),
);

View File

@ -1,13 +1,16 @@
/* @flow */ /* @flow */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import H3 from 'components/Heading';
import colors from 'config/colors'; import colors from 'config/colors';
import P from 'components/Paragraph';
import { FONT_SIZE } from 'config/variables'; 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` const Wrapper = styled.div`
width: 390px; width: 390px;
@ -49,4 +52,8 @@ const ConfirmAddress = (props: Props) => {
); );
}; };
ConfirmAddress.propTypes = {
selectedAccount: PropTypes.object.isRequired,
};
export default ConfirmAddress; export default ConfirmAddress;

View File

@ -1,15 +1,24 @@
/* @flow */ /* @flow */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components'; 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 icons from 'config/icons';
import { H3 } from 'components/Heading'; import colors from 'config/colors';
import { LINE_HEIGHT } from 'config/variables'; 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<BaseProps, 'sendForm'>;
}
const Wrapper = styled.div` const Wrapper = styled.div`
width: 390px; width: 390px;
@ -39,9 +48,6 @@ const Label = styled.div`
`; `;
const ConfirmSignTx = (props: Props) => { const ConfirmSignTx = (props: Props) => {
if (!props.modal.opened) return null;
const { device } = props.modal;
const { const {
amount, amount,
address, address,
@ -53,7 +59,7 @@ const ConfirmSignTx = (props: Props) => {
<Wrapper> <Wrapper>
<Header> <Header>
<Icon icon={icons.T1} size={60} color={colors.TEXT_SECONDARY} /> <Icon icon={icons.T1} size={60} color={colors.TEXT_SECONDARY} />
<H3>Confirm transaction on { device.label } device</H3> <H3>Confirm transaction on { props.device.label } device</H3>
<P isSmaller>Details are shown on display</P> <P isSmaller>Details are shown on display</P>
</Header> </Header>
<Content> <Content>
@ -68,4 +74,9 @@ const ConfirmSignTx = (props: Props) => {
); );
}; };
ConfirmSignTx.propTypes = {
device: PropTypes.object.isRequired,
sendForm: PropTypes.object.isRequired,
};
export default ConfirmSignTx; export default ConfirmSignTx;

View File

@ -1,15 +1,27 @@
/* @flow */ /* @flow */
import React, { PureComponent } from 'react'; 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 { H2 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import styled from 'styled-components';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import colors from 'config/colors';
import icons from 'config/icons';
import Button from 'components/Button'; import Button from 'components/Button';
import Link from 'components/Link'; 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<BaseProps, 'selectedAccount'>, 'account'>;
showAddress: $ElementType<$ElementType<BaseProps, 'receiveActions'>, 'showAddress'>;
showUnverifiedAddress: $ElementType<$ElementType<BaseProps, 'receiveActions'>, 'showUnverifiedAddress'>;
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>;
}
const StyledLink = styled(Link)` const StyledLink = styled(Link)`
position: absolute; position: absolute;
@ -56,29 +68,20 @@ class ConfirmUnverifiedAddress extends PureComponent<Props> {
} }
verifyAddress() { verifyAddress() {
if (!this.props.modal.opened) return; const { account, onCancel, showAddress } = this.props;
const { account } = this.props.selectedAccount;
if (!account) return; if (!account) return;
this.props.modalActions.onCancel(); onCancel();
this.props.receiveActions.showAddress(account.addressPath); showAddress(account.addressPath);
} }
showUnverifiedAddress() { showUnverifiedAddress() {
if (!this.props.modal.opened) return; const { onCancel, showUnverifiedAddress } = this.props;
onCancel();
this.props.modalActions.onCancel(); showUnverifiedAddress();
this.props.receiveActions.showUnverifiedAddress();
} }
render() { render() {
if (!this.props.modal.opened) return null; const { device, account, onCancel } = this.props;
const {
device,
} = this.props.modal;
const {
onCancel,
} = this.props.modalActions;
let deviceStatus: string; let deviceStatus: string;
let claim: string; let claim: string;
@ -101,7 +104,7 @@ class ConfirmUnverifiedAddress extends PureComponent<Props> {
<H2>{ deviceStatus }</H2> <H2>{ deviceStatus }</H2>
<StyledP isSmaller>To prevent phishing attacks, you should verify the address on your TREZOR first. { claim } to continue with the verification process.</StyledP> <StyledP isSmaller>To prevent phishing attacks, you should verify the address on your TREZOR first. { claim } to continue with the verification process.</StyledP>
<Row> <Row>
<StyledButton onClick={() => (!this.props.selectedAccount.account ? this.verifyAddress() : 'false')}>Try again</StyledButton> <StyledButton onClick={() => (!account ? this.verifyAddress() : 'false')}>Try again</StyledButton>
<StyledButton isWhite onClick={() => this.showUnverifiedAddress()}>Show unverified address</StyledButton> <StyledButton isWhite onClick={() => this.showUnverifiedAddress()}>Show unverified address</StyledButton>
</Row> </Row>
</Wrapper> </Wrapper>
@ -109,4 +112,12 @@ class ConfirmUnverifiedAddress extends PureComponent<Props> {
} }
} }
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; export default ConfirmUnverifiedAddress;

View File

@ -1,18 +1,30 @@
/* @flow */ /* @flow */
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components'; 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 { H3 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import Button from 'components/Button'; import Button from 'components/Button';
import Input from 'components/inputs/Input'; import Input from 'components/inputs/Input';
import { getDuplicateInstanceNumber } from 'reducers/utils';
import { FONT_SIZE } from 'config/variables';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import icons from 'config/icons';
import colors from 'config/colors';
import Link from 'components/Link'; 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<BaseProps, 'devices'>;
onDuplicateDevice: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onDuplicateDevice'>;
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>;
}
type State = { type State = {
defaultName: string; defaultName: string;
@ -62,17 +74,14 @@ const ErrorMessage = styled.div`
width: 100%; width: 100%;
`; `;
export default class DuplicateDevice extends PureComponent<Props, State> { class DuplicateDevice extends PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const device = props.modal.opened ? props.modal.device : null; const instance = getDuplicateInstanceNumber(props.devices, props.device);
if (!device) return;
const instance = getDuplicateInstanceNumber(props.devices, device);
this.state = { this.state = {
defaultName: `${device.label} (${instance.toString()})`, defaultName: `${props.device.label} (${instance.toString()})`,
instance, instance,
instanceName: null, instanceName: null,
isUsed: false, isUsed: false,
@ -114,16 +123,12 @@ export default class DuplicateDevice extends PureComponent<Props, State> {
keyboardHandler: (event: KeyboardEvent) => void; keyboardHandler: (event: KeyboardEvent) => void;
submit() { submit() {
if (!this.props.modal.opened) return;
const extended: Object = { instanceName: this.state.instanceName, instance: this.state.instance }; 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() { render() {
if (!this.props.modal.opened) return null; const { device, onCancel } = this.props;
const { device } = this.props.modal;
const { onCancel } = this.props.modalActions;
const { const {
defaultName, defaultName,
instanceName, instanceName,
@ -167,4 +172,13 @@ export default class DuplicateDevice extends PureComponent<Props, State> {
</Wrapper> </Wrapper>
); );
} }
} }
DuplicateDevice.propTypes = {
device: PropTypes.object.isRequired,
devices: PropTypes.array.isRequired,
onDuplicateDevice: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};
export default DuplicateDevice;

View File

@ -1,12 +1,21 @@
/* @flow */ /* @flow */
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import { H3 } from 'components/Heading'; import { H3 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import Button from 'components/Button'; 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<BaseProps, 'modalActions'>, 'onForgetSingleDevice'>;
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>;
}
const Wrapper = styled.div` const Wrapper = styled.div`
width: 360px; width: 360px;
@ -47,26 +56,27 @@ class ForgetDevice extends PureComponent<Props> {
keyboardHandler: (event: KeyboardEvent) => void; keyboardHandler: (event: KeyboardEvent) => void;
forget() { forget() {
if (this.props.modal.opened) { this.props.onForgetSingleDevice(this.props.device);
this.props.modalActions.onForgetSingleDevice(this.props.modal.device);
}
} }
render() { render() {
if (!this.props.modal.opened) return null;
const { device } = this.props.modal;
const { onCancel } = this.props.modalActions;
return ( return (
<Wrapper> <Wrapper>
<H3>Forget { device.instanceLabel }?</H3> <H3>Forget { this.props.device.instanceLabel }?</H3>
<StyledP isSmaller>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.</StyledP> <StyledP isSmaller>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.</StyledP>
<Row> <Row>
<StyledButton onClick={() => this.forget()}>Forget</StyledButton> <StyledButton onClick={() => this.forget()}>Forget</StyledButton>
<StyledButton isWhite onClick={onCancel}>Don&apos;t forget</StyledButton> <StyledButton isWhite onClick={this.props.onCancel}>Don&apos;t forget</StyledButton>
</Row> </Row>
</Wrapper> </Wrapper>
); );
} }
} }
ForgetDevice.propTypes = {
device: PropTypes.object.isRequired,
onForgetSingleDevice: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};
export default ForgetDevice; export default ForgetDevice;

View File

@ -1,12 +1,22 @@
/* @flow */ /* @flow */
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import { H3 } from 'components/Heading'; import { H3 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import Loader from 'components/Loader'; import Loader from 'components/Loader';
import Button from 'components/Button'; 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<TrezorDevice>;
onRememberDevice: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onRememberDevice'>;
onForgetDevice: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onForgetDevice'>;
}
type State = { type State = {
countdown: number; countdown: number;
@ -47,7 +57,7 @@ const StyledLoader = styled(Loader)`
left: 200px; left: 200px;
`; `;
export default class RememberDevice extends PureComponent<Props, State> { class RememberDevice extends PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -62,9 +72,7 @@ export default class RememberDevice extends PureComponent<Props, State> {
// TODO: possible race condition, // TODO: possible race condition,
// device could be already connected but it didn't emit Device.CONNECT event yet // device could be already connected but it didn't emit Device.CONNECT event yet
window.clearInterval(this.state.ticker); window.clearInterval(this.state.ticker);
if (this.props.modal.opened) { this.props.onForgetDevice(this.props.device);
this.props.modalActions.onForgetDevice(this.props.modal.device);
}
} else { } else {
this.setState(previousState => ({ this.setState(previousState => ({
countdown: previousState.countdown - 1, countdown: previousState.countdown - 1,
@ -98,15 +106,11 @@ export default class RememberDevice extends PureComponent<Props, State> {
keyboardHandler: (event: KeyboardEvent) => void; keyboardHandler: (event: KeyboardEvent) => void;
forget() { forget() {
if (this.props.modal.opened) { this.props.onForgetDevice(this.props.device);
this.props.modalActions.onForgetDevice(this.props.modal.device);
}
} }
render() { render() {
if (!this.props.modal.opened) return null; const { device, instances, onRememberDevice } = this.props;
const { device, instances } = this.props.modal;
const { onRememberDevice } = this.props.modalActions;
let { label } = device; let { label } = device;
const devicePlural: string = instances && instances.length > 1 ? 'devices or to remember them' : 'device or to remember it'; 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<Props, State> {
</Wrapper> </Wrapper>
); );
} }
} }
RememberDevice.propTypes = {
device: PropTypes.object.isRequired,
instances: PropTypes.array.isRequired,
onRememberDevice: PropTypes.func.isRequired,
onForgetDevice: PropTypes.func.isRequired,
};
export default RememberDevice;

View File

@ -1,18 +1,28 @@
/* @flow */ /* @flow */
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import icons from 'config/icons';
import colors from 'config/colors';
import { H3 } from 'components/Heading'; import { H3 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import Button from 'components/Button'; import Button from 'components/Button';
import Tooltip from 'components/Tooltip'; import Tooltip from 'components/Tooltip';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import Link from 'components/Link'; import Link from 'components/Link';
import colors from 'config/colors';
import icons from 'config/icons';
import WalletTypeIcon from 'components/images/WalletType'; 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<BaseProps, 'modalActions'>, 'onWalletTypeRequest'>;
onCancel: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onCancel'>;
}
const Wrapper = styled.div` const Wrapper = styled.div`
width: 360px; width: 360px;
@ -88,15 +98,11 @@ class WalletType extends PureComponent<Props> {
keyboardHandler: (event: KeyboardEvent) => void; keyboardHandler: (event: KeyboardEvent) => void;
changeType(hidden: boolean, state: ?string) { changeType(hidden: boolean, state: ?string) {
const { modal } = this.props; this.props.onWalletTypeRequest(this.props.device, hidden, state);
if (!modal.opened) return;
this.props.modalActions.onWalletTypeRequest(modal.device, hidden, state);
} }
render() { render() {
if (!this.props.modal.opened) return null; const { device, onCancel } = this.props;
const { device } = this.props.modal;
const { onCancel } = this.props.modalActions;
return ( return (
<Wrapper> <Wrapper>
@ -109,7 +115,7 @@ class WalletType extends PureComponent<Props> {
/> />
</StyledLink> </StyledLink>
)} )}
<StyledHeading>Change wallet type for { device.instanceLabel }</StyledHeading> <StyledHeading>{ device.state ? 'Change' : 'Select' } wallet type for { device.instanceLabel }</StyledHeading>
<Content isTop> <Content isTop>
<Header> <Header>
<WalletTypeIcon type="standard" size={32} color={colors.TEXT_PRIMARY} /> <WalletTypeIcon type="standard" size={32} color={colors.TEXT_PRIMARY} />
@ -147,4 +153,10 @@ class WalletType extends PureComponent<Props> {
} }
} }
WalletType.propTypes = {
device: PropTypes.object.isRequired,
onWalletTypeRequest: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};
export default WalletType; export default WalletType;

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -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<BaseProps, 'modalActions'>, '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) => (
<Wrapper>
<StyledLink onClick={props.onCancel}>
<Icon
size={20}
color={colors.TEXT_SECONDARY}
icon={icons.CLOSE}
/>
</StyledLink>
<H3>NEM Wallet</H3>
<P isSmaller>We have partnered up with the NEM Foundation to provide you with a full-fledged NEM Wallet.</P>
<H4>Make sure you download the Universal Client for TREZOR support.</H4>
<Img src={NemImage} />
<Link href={coins.find(i => i.id === 'xem').url}>
<StyledButton>Go to nem.io</StyledButton>
</Link>
</Wrapper>
);
NemWallet.propTypes = {
onCancel: PropTypes.func.isRequired,
};
export default NemWallet;

View File

@ -1,64 +1,39 @@
/* @flow */ /* @flow */
import * as React from 'react'; 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 { CSSTransition } from 'react-transition-group';
import styled from 'styled-components';
import colors from 'config/colors';
import { UI } from 'trezor-connect'; import { UI } from 'trezor-connect';
import * as MODAL from 'actions/constants/modal';
import ModalActions from 'actions/ModalActions';
import ReceiveActions from 'actions/ReceiveActions';
import * as RECEIVE from 'actions/constants/receive'; import * as RECEIVE from 'actions/constants/receive';
import * as CONNECT from 'actions/constants/TrezorConnect'; 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 Pin from 'components/modals/pin/Pin';
import InvalidPin from 'components/modals/pin/Invalid'; import InvalidPin from 'components/modals/pin/Invalid';
import Passphrase from 'components/modals/passphrase/Passphrase'; import Passphrase from 'components/modals/passphrase/Passphrase';
import PassphraseType from 'components/modals/passphrase/Type'; import PassphraseType from 'components/modals/passphrase/Type';
import ConfirmSignTx from 'components/modals/confirm/SignTx'; import ConfirmSignTx from 'components/modals/confirm/SignTx';
import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress'; import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress';
import ForgetDevice from 'components/modals/device/Forget'; import ForgetDevice from 'components/modals/device/Forget';
import RememberDevice from 'components/modals/device/Remember'; import RememberDevice from 'components/modals/device/Remember';
import DuplicateDevice from 'components/modals/device/Duplicate'; import DuplicateDevice from 'components/modals/device/Duplicate';
import WalletType from 'components/modals/device/WalletType'; import WalletType from 'components/modals/device/WalletType';
type OwnProps = { } // external context
import NemWallet from 'components/modals/external/NemWallet';
type StateProps = { import type { Props } from './Container';
modal: $ElementType<State, 'modal'>,
accounts: $ElementType<State, 'accounts'>,
devices: $ElementType<State, 'devices'>,
connect: $ElementType<State, 'connect'>,
selectedAccount: $ElementType<State, 'selectedAccount'>,
sendForm: $ElementType<State, 'sendForm'>,
receive: $ElementType<State, 'receive'>,
localStorage: $ElementType<State, 'localStorage'>,
wallet: $ElementType<State, 'wallet'>,
}
type DispatchProps = {
modalActions: typeof ModalActions,
receiveActions: typeof ReceiveActions,
}
export type Props = StateProps & DispatchProps;
const Fade = (props: { children: React.Node}) => ( const Fade = (props: { children: React.Node}) => (
<CSSTransition <CSSTransition
{...props} {...props}
timeout={1000} timeout={1000}
classNames="fade" classNames="fade"
> >{ props.children }
{ props.children }
</CSSTransition> </CSSTransition>
); );
@ -85,87 +60,124 @@ const ModalWindow = styled.div`
text-align: center; text-align: center;
`; `;
class Modal extends React.PureComponent<Props> { // get modal component with device context
render() { const getDeviceContextModal = (props: Props) => {
if (!this.props.modal.opened) return null; const { modal, modalActions } = props;
if (modal.context !== MODAL.CONTEXT_DEVICE) return null;
const { opened, windowType } = this.props.modal; switch (modal.windowType) {
let component = null; case UI.REQUEST_PIN:
switch (windowType) { return (
case UI.REQUEST_PIN: <Pin
component = (<Pin {...this.props} />); device={modal.device}
break; onPinSubmit={modalActions.onPinSubmit}
case UI.INVALID_PIN: />);
component = (<InvalidPin {...this.props} />);
break;
case UI.REQUEST_PASSPHRASE:
component = (<Passphrase {...this.props} />);
break;
case 'ButtonRequest_SignTx':
component = (<ConfirmSignTx {...this.props} />);
break;
case 'ButtonRequest_PassphraseType':
component = (<PassphraseType {...this.props} />);
break;
case RECEIVE.REQUEST_UNVERIFIED:
component = (<ConfirmUnverifiedAddress {...this.props} />);
break;
case CONNECT.REMEMBER_REQUEST: case UI.INVALID_PIN:
component = (<RememberDevice {...this.props} />); return <InvalidPin device={modal.device} />;
break;
case CONNECT.FORGET_REQUEST: case UI.REQUEST_PASSPHRASE:
component = (<ForgetDevice {...this.props} />); return (
break; <Passphrase
device={modal.device}
selectedDevice={props.wallet.selectedDevice}
onPassphraseSubmit={modalActions.onPassphraseSubmit}
/>);
case CONNECT.TRY_TO_DUPLICATE: case 'ButtonRequest_PassphraseType':
component = (<DuplicateDevice {...this.props} />); return <PassphraseType device={modal.device} />;
break;
case CONNECT.REQUEST_WALLET_TYPE: case 'ButtonRequest_SignTx':
component = (<WalletType {...this.props} />); return <ConfirmSignTx device={modal.device} sendForm={props.sendForm} />;
break;
default: case RECEIVE.REQUEST_UNVERIFIED:
component = null; return (
} <ConfirmUnverifiedAddress
device={modal.device}
account={props.selectedAccount.account}
onCancel={modalActions.onCancel}
showAddress={props.receiveActions.showAddress}
showUnverifiedAddress={props.receiveActions.showUnverifiedAddress}
/>);
let ch = null; case CONNECT.REMEMBER_REQUEST:
if (opened) { return (
ch = ( <RememberDevice
<Fade key="1"> device={modal.device}
<ModalContainer> instances={modal.instances}
<ModalWindow> onRememberDevice={modalActions.onRememberDevice}
{ component } onForgetDevice={modalActions.onForgetDevice}
</ModalWindow> />);
</ModalContainer>
</Fade>
);
}
return ch; case CONNECT.FORGET_REQUEST:
return (
<ForgetDevice
device={modal.device}
onForgetSingleDevice={modalActions.onForgetSingleDevice}
onCancel={modalActions.onCancel}
/>);
case CONNECT.TRY_TO_DUPLICATE:
return (
<DuplicateDevice
device={modal.device}
devices={props.devices}
onDuplicateDevice={modalActions.onDuplicateDevice}
onCancel={modalActions.onCancel}
/>);
case CONNECT.REQUEST_WALLET_TYPE:
return (
<WalletType
device={modal.device}
onWalletTypeRequest={modalActions.onWalletTypeRequest}
onCancel={modalActions.onCancel}
/>);
default:
return null;
} }
} };
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({ // get modal component with external context
modal: state.modal, const getExternalContextModal = (props: Props) => {
accounts: state.accounts, const { modal, modalActions } = props;
devices: state.devices, if (modal.context !== MODAL.CONTEXT_EXTERNAL_WALLET) return null;
connect: state.connect,
selectedAccount: state.selectedAccount,
sendForm: state.sendForm,
receive: state.receive,
localStorage: state.localStorage,
wallet: state.wallet,
});
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({ switch (modal.windowType) {
modalActions: bindActionCreators(ModalActions, dispatch), case 'xem':
receiveActions: bindActionCreators(ReceiveActions, dispatch), return (<NemWallet onCancel={modalActions.onCancel} />);
}); default:
return null;
}
};
// export default connect(mapStateToProps, mapDispatchToProps)(Modal); // modal container component
export default withRouter( const Modal = (props: Props) => {
connect(mapStateToProps, mapDispatchToProps)(Modal), 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 (
<Fade key="modal-fade">
<ModalContainer>
<ModalWindow>
{ component }
</ModalWindow>
</ModalContainer>
</Fade>
);
};
export default Modal;

View File

@ -1,15 +1,25 @@
/* @flow */ /* @flow */
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import { FONT_SIZE, TRANSITION } from 'config/variables'; import { FONT_SIZE, TRANSITION } from 'config/variables';
import { H2 } from 'components/Heading'; import { H2 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import Checkbox from 'components/Checkbox'; import Checkbox from 'components/Checkbox';
import Button from 'components/Button'; import Button from 'components/Button';
import Input from 'components/inputs/Input'; 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<BaseProps, 'modalActions'>, 'onPassphraseSubmit'>;
}
type State = { type State = {
deviceLabel: string, deviceLabel: string,
@ -79,15 +89,10 @@ const LinkButton = styled(Button)`
class Passphrase extends PureComponent<Props, State> { class Passphrase extends PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const { device, selectedDevice } = props;
const device = props.modal.opened ? props.modal.device : null;
if (!device) {
return;
}
// Check if this device is already known // Check if this device is already known
// if device is already known then only one input is presented // if device is already known then only one input is presented
const { selectedDevice } = props.wallet;
let deviceLabel = device.label; let deviceLabel = device.label;
let shouldShowSingleInput = false; let shouldShowSingleInput = false;
if (selectedDevice && selectedDevice.path === device.path) { if (selectedDevice && selectedDevice.path === device.path) {
@ -105,8 +110,6 @@ class Passphrase extends PureComponent<Props, State> {
}; };
} }
state: State;
componentDidMount() { componentDidMount() {
this.passphraseInput.focus(); this.passphraseInput.focus();
@ -180,7 +183,7 @@ class Passphrase extends PureComponent<Props, State> {
} }
submitPassphrase(shouldLeavePassphraseBlank: boolean = false) { submitPassphrase(shouldLeavePassphraseBlank: boolean = false) {
const { onPassphraseSubmit } = this.props.modalActions; const { onPassphraseSubmit } = this.props;
const passphrase = this.state.passphraseInputValue; const passphrase = this.state.passphraseInputValue;
// Reset state so same passphrase isn't filled when the modal will be visible again // Reset state so same passphrase isn't filled when the modal will be visible again
@ -197,7 +200,6 @@ class Passphrase extends PureComponent<Props, State> {
handleKeyPress(event: KeyboardEvent) { handleKeyPress(event: KeyboardEvent) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
console.warn('ENTER', this.state);
if (this.state.doPassphraseInputsMatch) { if (this.state.doPassphraseInputsMatch) {
this.submitPassphrase(); this.submitPassphrase();
} }
@ -205,10 +207,6 @@ class Passphrase extends PureComponent<Props, State> {
} }
render() { render() {
if (!this.props.modal.opened) {
return null;
}
return ( return (
<Wrapper> <Wrapper>
<H2>Enter {this.state.deviceLabel} passphrase</H2> <H2>Enter {this.state.deviceLabel} passphrase</H2>
@ -277,4 +275,10 @@ class Passphrase extends PureComponent<Props, State> {
} }
} }
Passphrase.propTypes = {
device: PropTypes.object.isRequired,
selectedDevice: PropTypes.object.isRequired,
onPassphraseSubmit: PropTypes.func.isRequired,
};
export default Passphrase; export default Passphrase;

View File

@ -1,14 +1,21 @@
/* @flow */ /* @flow */
import React from 'react'; import React from 'react';
import Icon from 'components/Icon'; import PropTypes from 'prop-types';
import colors from 'config/colors';
import icons from 'config/icons';
import styled from 'styled-components'; 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 { H3 } from 'components/Heading';
import P from 'components/Paragraph'; 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` const Wrapper = styled.div`
width: 360px; width: 360px;
@ -17,19 +24,18 @@ const Wrapper = styled.div`
const Header = styled.div``; const Header = styled.div``;
const Confirmation = (props: Props) => { const PassphraseType = (props: Props) => (
if (!props.modal.opened) return null; <Wrapper>
const { device } = props.modal; <Header>
<Icon icon={icons.T1} size={60} color={colors.TEXT_SECONDARY} />
<H3>Complete the action on { props.device.label } device</H3>
<P isSmaller>If you enter a wrong passphrase, you will not unlock the desired hidden wallet.</P>
</Header>
</Wrapper>
);
return ( PassphraseType.propTypes = {
<Wrapper> device: PropTypes.object.isRequired,
<Header>
<Icon icon={icons.T1} size={60} color={colors.TEXT_SECONDARY} />
<H3>Complete the action on { device.label } device</H3>
<P isSmaller>If you enter a wrong passphrase, you will not unlock the desired hidden wallet.</P>
</Header>
</Wrapper>
);
}; };
export default Confirmation; export default PassphraseType;

View File

@ -1,26 +1,31 @@
/* @flow */ /* @flow */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import { H3 } from 'components/Heading'; import { H3 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import type { Props } from '../../index'; import type { TrezorDevice } from 'flowtype';
type Props = {
device: TrezorDevice;
}
const Wrapper = styled.div` const Wrapper = styled.div`
padding: 24px 48px; padding: 24px 48px;
`; `;
const InvalidPin = (props: Props) => { const InvalidPin = (props: Props) => (
if (!props.modal.opened) return null; <Wrapper>
<H3>Entered PIN for { props.device.label } is not correct</H3>
<P isSmaller>Retrying...</P>
</Wrapper>
);
const { device } = props.modal; InvalidPin.propTypes = {
return ( device: PropTypes.object.isRequired,
<Wrapper>
<H3>Entered PIN for { device.label } is not correct</H3>
<P isSmaller>Retrying...</P>
</Wrapper>
);
}; };
export default InvalidPin; export default InvalidPin;

View File

@ -1,14 +1,24 @@
/* @flow */ /* @flow */
import P from 'components/Paragraph';
import { H2 } from 'components/Heading';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import Link from 'components/Link'; import PropTypes from 'prop-types';
import styled from 'styled-components'; 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 Button from 'components/Button';
import type { TrezorDevice } from 'flowtype';
import PinButton from './components/Button'; import PinButton from './components/Button';
import PinInput from './components/Input'; import PinInput from './components/Input';
import type { Props } from '../../index';
import type { Props as BaseProps } from '../../Container';
type Props = {
device: TrezorDevice;
onPinSubmit: $ElementType<$ElementType<BaseProps, 'modalActions'>, 'onPinSubmit'>;
}
type State = { type State = {
pin: string; pin: string;
@ -74,7 +84,7 @@ class Pin extends PureComponent<Props, State> {
} }
keyboardHandler(event: KeyboardEvent): void { keyboardHandler(event: KeyboardEvent): void {
const { onPinSubmit } = this.props.modalActions; const { onPinSubmit } = this.props;
const { pin } = this.state; const { pin } = this.state;
event.preventDefault(); event.preventDefault();
@ -132,9 +142,7 @@ class Pin extends PureComponent<Props, State> {
keyboardHandler: (event: KeyboardEvent) => void; keyboardHandler: (event: KeyboardEvent) => void;
render() { render() {
if (!this.props.modal.opened) return null; const { device, onPinSubmit } = this.props;
const { onPinSubmit } = this.props.modalActions;
const { device } = this.props.modal;
const { pin } = this.state; const { pin } = this.state;
return ( return (
<Wrapper> <Wrapper>
@ -173,4 +181,9 @@ class Pin extends PureComponent<Props, State> {
} }
} }
Pin.propTypes = {
device: PropTypes.object.isRequired,
onPinSubmit: PropTypes.func.isRequired,
};
export default Pin; export default Pin;

View File

@ -48,5 +48,6 @@ export default [
id: 'xem', id: 'xem',
coinName: 'NEM', coinName: 'NEM',
url: 'https://nem.io/downloads/', url: 'https://nem.io/downloads/',
external: true,
}, },
]; ];

View File

@ -9,92 +9,79 @@ import * as CONNECT from 'actions/constants/TrezorConnect';
import type { Action, TrezorDevice } from 'flowtype'; import type { Action, TrezorDevice } from 'flowtype';
export type State = { export type State = {
opened: false; context: typeof MODAL.CONTEXT_NONE;
} | { } | {
opened: true; context: typeof MODAL.CONTEXT_DEVICE,
device: TrezorDevice; device: TrezorDevice;
instances?: Array<TrezorDevice>; instances?: Array<TrezorDevice>;
windowType?: string; windowType?: string;
} | {
context: typeof MODAL.CONTEXT_EXTERNAL_WALLET,
windowType?: string;
} }
const initialState: State = { const initialState: State = {
opened: false, context: MODAL.CONTEXT_NONE,
// instances: null,
// windowType: null
}; };
export default function modal(state: State = initialState, action: Action): State { export default function modal(state: State = initialState, action: Action): State {
switch (action.type) { switch (action.type) {
case RECEIVE.REQUEST_UNVERIFIED: case RECEIVE.REQUEST_UNVERIFIED:
return { case CONNECT.FORGET_REQUEST:
opened: true, case CONNECT.TRY_TO_DUPLICATE:
device: action.device,
windowType: action.type,
};
case CONNECT.REQUEST_WALLET_TYPE: case CONNECT.REQUEST_WALLET_TYPE:
return { return {
opened: true, context: MODAL.CONTEXT_DEVICE,
device: action.device, device: action.device,
windowType: action.type, windowType: action.type,
}; };
case CONNECT.REMEMBER_REQUEST: case CONNECT.REMEMBER_REQUEST:
return { return {
opened: true, context: MODAL.CONTEXT_DEVICE,
device: action.device, device: action.device,
instances: action.instances, instances: action.instances,
windowType: action.type, 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: 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 initialState;
} }
return state; 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: 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 initialState;
} }
return state; 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.REQUEST_PIN:
case UI.INVALID_PIN: case UI.INVALID_PIN:
case UI.REQUEST_PASSPHRASE: case UI.REQUEST_PASSPHRASE:
return { return {
opened: true, context: MODAL.CONTEXT_DEVICE,
device: action.payload.device, device: action.payload.device,
windowType: action.type, windowType: action.type,
}; };
case UI.REQUEST_BUTTON: case UI.REQUEST_BUTTON:
return { return {
opened: true, context: MODAL.CONTEXT_DEVICE,
device: action.payload.device, device: action.payload.device,
windowType: action.payload.code, windowType: action.payload.code,
}; };
@ -106,6 +93,12 @@ export default function modal(state: State = initialState, action: Action): Stat
case CONNECT.REMEMBER: case CONNECT.REMEMBER:
return initialState; return initialState;
case MODAL.OPEN_EXTERNAL_WALLET:
return {
context: MODAL.CONTEXT_EXTERNAL_WALLET,
windowType: action.id,
};
default: default:
return state; return state;
} }

View File

@ -2,6 +2,7 @@
import colors from 'config/colors'; import colors from 'config/colors';
import type { Device } from 'trezor-connect';
import type { import type {
TrezorDevice, TrezorDevice,
State, State,
@ -91,7 +92,7 @@ export const isDeviceAccessible = (device: ?TrezorDevice): boolean => {
return device.mode === 'normal' && device.firmware !== 'required'; 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 => { export const getVersion = (device: TrezorDevice): string => {
let version; let version;

View File

@ -7,6 +7,7 @@ import { withRouter } from 'react-router-dom';
import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as TrezorConnectActions from 'actions/TrezorConnectActions';
import * as DiscoveryActions from 'actions/DiscoveryActions'; import * as DiscoveryActions from 'actions/DiscoveryActions';
import * as RouterActions from 'actions/RouterActions'; import * as RouterActions from 'actions/RouterActions';
import * as ModalActions from 'actions/ModalActions';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype'; import type { State, Dispatch } from 'flowtype';
@ -38,6 +39,7 @@ const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps>
duplicateDevice: bindActionCreators(TrezorConnectActions.duplicateDevice, dispatch), duplicateDevice: bindActionCreators(TrezorConnectActions.duplicateDevice, dispatch),
gotoDeviceSettings: bindActionCreators(RouterActions.gotoDeviceSettings, dispatch), gotoDeviceSettings: bindActionCreators(RouterActions.gotoDeviceSettings, dispatch),
onSelectDevice: bindActionCreators(RouterActions.selectDevice, dispatch), onSelectDevice: bindActionCreators(RouterActions.selectDevice, dispatch),
gotoExternalWallet: bindActionCreators(ModalActions.gotoExternalWallet, dispatch),
}); });
export default withRouter( export default withRouter(

View File

@ -1,3 +1,5 @@
/* @flow */
import styled from 'styled-components'; import styled from 'styled-components';
import coins from 'constants/coins'; import coins from 'constants/coins';
import colors from 'config/colors'; import colors from 'config/colors';
@ -5,12 +7,19 @@ import ICONS from 'config/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import Link from 'components/Link';
import Divider from '../Divider'; import Divider from '../Divider';
import RowCoin from '../RowCoin'; import RowCoin from '../RowCoin';
import type { Props } from '../common';
const Wrapper = styled.div``; const Wrapper = styled.div``;
class CoinMenu extends PureComponent { const ExternalWallet = styled.div`
cursor: pointer;
`;
class CoinMenu extends PureComponent<Props> {
getBaseUrl() { getBaseUrl() {
const { selectedDevice } = this.props.wallet; const { selectedDevice } = this.props.wallet;
let baseUrl = ''; let baseUrl = '';
@ -24,6 +33,27 @@ class CoinMenu extends PureComponent {
return baseUrl; return baseUrl;
} }
getOtherCoins() {
return coins.map((coin) => {
const row = (
<RowCoin
coin={{
name: coin.coinName,
id: coin.id,
}}
iconRight={{
type: ICONS.SKIP,
color: colors.TEXT_SECONDARY,
size: 27,
}}
/>
);
if (coin.external) return <ExternalWallet key={coin.id} onClick={() => this.props.gotoExternalWallet(coin.id, coin.url)}>{row}</ExternalWallet>;
return <Link key={coin.id} href={coin.url} target="_top">{row}</Link>;
});
}
render() { render() {
const { config } = this.props.localStorage; const { config } = this.props.localStorage;
return ( return (
@ -46,31 +76,16 @@ class CoinMenu extends PureComponent {
textRight="(You will be redirected)" textRight="(You will be redirected)"
hasBorder hasBorder
/> />
{coins.map(coin => ( {this.getOtherCoins()}
<a key={coin.id} href={coin.url}>
<RowCoin
coin={{
name: coin.coinName,
id: coin.id,
}}
iconRight={{
type: ICONS.SKIP,
color: colors.TEXT_SECONDARY,
size: 27,
}}
/>
</a>
))}
</Wrapper> </Wrapper>
); );
} }
} }
CoinMenu.propTypes = { CoinMenu.propTypes = {
config: PropTypes.object, localStorage: PropTypes.object.isRequired,
wallet: PropTypes.object, wallet: PropTypes.object.isRequired,
selectedDevice: PropTypes.object, gotoExternalWallet: PropTypes.func.isRequired,
localStorage: PropTypes.object,
}; };
export default CoinMenu; export default CoinMenu;

View File

@ -2,6 +2,7 @@
import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as TrezorConnectActions from 'actions/TrezorConnectActions';
import * as DiscoveryActions from 'actions/DiscoveryActions'; import * as DiscoveryActions from 'actions/DiscoveryActions';
import * as RouterActions from 'actions/RouterActions'; import * as RouterActions from 'actions/RouterActions';
import * as ModalActions from 'actions/ModalActions';
import { toggleDeviceDropdown } from 'actions/WalletActions'; import { toggleDeviceDropdown } from 'actions/WalletActions';
import type { State } from 'flowtype'; import type { State } from 'flowtype';
@ -25,6 +26,7 @@ export type DispatchProps = {
duplicateDevice: typeof TrezorConnectActions.duplicateDevice, duplicateDevice: typeof TrezorConnectActions.duplicateDevice,
gotoDeviceSettings: typeof RouterActions.gotoDeviceSettings, gotoDeviceSettings: typeof RouterActions.gotoDeviceSettings,
onSelectDevice: typeof RouterActions.selectDevice, onSelectDevice: typeof RouterActions.selectDevice,
gotoExternalWallet: typeof ModalActions.gotoExternalWallet,
} }
export type Props = StateProps & DispatchProps; export type Props = StateProps & DispatchProps;

View File

@ -11,7 +11,7 @@ import type { State } from 'flowtype';
import Header from 'components/Header'; import Header from 'components/Header';
import Footer from 'components/Footer'; import Footer from 'components/Footer';
import ModalContainer from 'components/modals'; import ModalContainer from 'components/modals/Container';
import AppNotifications from 'components/notifications/App'; import AppNotifications from 'components/notifications/App';
import ContextNotifications from 'components/notifications/Context'; import ContextNotifications from 'components/notifications/Context';

View File

@ -148,14 +148,13 @@ const AccountReceive = (props: Props) => {
addressUnverified, addressUnverified,
} = props.receive; } = 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)}...`; let address = `${account.address.substring(0, 20)}...`;
if (addressVerified if (addressVerified || addressUnverified || isAddressVerifying) {
|| addressUnverified
|| (props.modal.opened && props.modal.windowType === 'ButtonRequest_Address')) {
({ address } = account); ({ address } = account);
} }
const isAddressVerifying = props.modal.opened && props.modal.windowType === 'ButtonRequest_Address';
const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified;
return ( return (
<Content> <Content>