1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-24 09:18:09 +00:00

Merge pull request #127 from trezor/feature/app-notifications

Feature/app notifications
This commit is contained in:
Vladimir Volek 2018-10-05 15:30:42 +02:00 committed by GitHub
commit ac724210dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 467 additions and 228 deletions

View File

@ -2,6 +2,7 @@
import { push, LOCATION_CHANGE } from 'react-router-redux'; import { push, LOCATION_CHANGE } from 'react-router-redux';
import { routes } from 'support/routes'; import { routes } from 'support/routes';
import * as deviceUtils from 'utils/device';
import type { import type {
RouterLocationState, RouterLocationState,
@ -59,6 +60,11 @@ export const paramsValidation = (params: RouterLocationState): PayloadAction<boo
} }
if (!device) return false; if (!device) return false;
if (!deviceUtils.isDeviceAccessible(device)) {
// TODO: there should be no access to deep links if device has incorrect mode/firmware
// if (params.hasOwnProperty('network') || params.hasOwnProperty('account')) return false;
}
} }
// validate requested network // validate requested network
@ -177,10 +183,12 @@ const getDeviceUrl = (device: TrezorDevice | Device): PayloadAction<?string> =>
let url: ?string; let url: ?string;
if (!device.features) { if (!device.features) {
url = `/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`; url = `/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`;
} else if (device.features.bootloader_mode) { } else if (device.mode === 'bootloader') { // device in bootloader doesn't have device_id
url = `/device/${device.path}/bootloader`; url = `/device/${device.path}/bootloader`;
} else if (!device.features.initialized) { } else if (device.mode === 'initialize') {
url = `/device/${device.features.device_id}/initialize`; url = `/device/${device.features.device_id}/initialize`;
} else if (device.firmware === 'required') {
url = `/device/${device.features.device_id}/firmware-update`;
} else if (typeof device.instance === 'number') { } else if (typeof device.instance === 'number') {
url = `/device/${device.features.device_id}:${device.instance}`; url = `/device/${device.features.device_id}:${device.instance}`;
} else { } else {
@ -303,6 +311,24 @@ export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => (dispat
} }
}; };
/*
* Go to UpdateBridge page
*/
export const gotoBridgeUpdate = (): ThunkAction => (dispatch: Dispatch): void => {
dispatch(goto('/bridge'));
};
/*
* Go to UpdateFirmware page
* Called from App notification
*/
export const gotoFirmwareUpdate = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const { selectedDevice } = getState().wallet;
if (!selectedDevice || !selectedDevice.features) return;
const devUrl: string = `${selectedDevice.features.device_id}${selectedDevice.instance ? `:${selectedDevice.instance}` : ''}`;
dispatch(goto(`/device/${devUrl}/firmware-update`));
};
/* /*
* Try to redirect to initial url * Try to redirect to initial url
*/ */

View File

@ -154,10 +154,10 @@ export const postInit = (): ThunkAction => (dispatch: Dispatch): void => {
export const getSelectedDeviceState = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const getSelectedDeviceState = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const selected = getState().wallet.selectedDevice; const selected = getState().wallet.selectedDevice;
if (selected if (!selected) return;
&& selected.connected const isDeviceReady = selected.connected && selected.features && !selected.state && selected.mode === 'normal' && selected.firmware !== 'required';
&& (selected.features && !selected.features.bootloader_mode && selected.features.initialized) if (!isDeviceReady) return;
&& !selected.state) {
const response = await TrezorConnect.getDeviceState({ const response = await TrezorConnect.getDeviceState({
device: { device: {
path: selected.path, path: selected.path,
@ -197,7 +197,7 @@ export const getSelectedDeviceState = (): AsyncAction => async (dispatch: Dispat
}, },
}); });
} }
}
}; };
export const deviceDisconnect = (device: Device): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { export const deviceDisconnect = (device: Device): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {

View File

@ -5,6 +5,7 @@ import { DEVICE } from 'trezor-connect';
import * as CONNECT from 'actions/constants/TrezorConnect'; import * as CONNECT from 'actions/constants/TrezorConnect';
import * as WALLET from 'actions/constants/wallet'; import * as WALLET from 'actions/constants/wallet';
import * as reducerUtils from 'reducers/utils'; import * as reducerUtils from 'reducers/utils';
import * as deviceUtils from 'utils/device';
import type { import type {
Device, Device,
@ -104,7 +105,7 @@ export const observe = (prevState: State, action: Action): PayloadAction<boolean
// handle devices state change (from trezor-connect events or location change) // handle devices state change (from trezor-connect events or location change)
if (locationChanged || selectedDeviceChanged) { if (locationChanged || selectedDeviceChanged) {
if (device && reducerUtils.isSelectedDevice(state.wallet.selectedDevice, device)) { if (device && deviceUtils.isSelectedDevice(state.wallet.selectedDevice, device)) {
dispatch({ dispatch({
type: WALLET.UPDATE_SELECTED_DEVICE, type: WALLET.UPDATE_SELECTED_DEVICE,
device, device,

View File

@ -98,7 +98,7 @@ const DeviceHeader = ({
device, device,
isHoverable = true, isHoverable = true,
onClickWrapper, onClickWrapper,
isBootloader = false, isAccessible = true,
disabled = false, disabled = false,
isSelected = false, isSelected = false,
}) => { }) => {
@ -123,7 +123,7 @@ const DeviceHeader = ({
<Status>{getStatusName(status)}</Status> <Status>{getStatusName(status)}</Status>
</LabelWrapper> </LabelWrapper>
<IconWrapper> <IconWrapper>
{icon && !disabled && !isBootloader && icon} {icon && !disabled && isAccessible && icon}
</IconWrapper> </IconWrapper>
</ClickWrapper> </ClickWrapper>
</Wrapper> </Wrapper>
@ -131,7 +131,7 @@ const DeviceHeader = ({
}; };
DeviceHeader.propTypes = { DeviceHeader.propTypes = {
isBootloader: PropTypes.bool, isAccessible: PropTypes.bool,
device: PropTypes.object, device: PropTypes.object,
icon: PropTypes.element, icon: PropTypes.element,
isHoverable: PropTypes.bool, isHoverable: PropTypes.bool,

View File

@ -0,0 +1,11 @@
/* @flow */
import * as React from 'react';
import { Notification } from 'components/Notification';
import type { Props } from '../../index';
export default (props: Props) => {
const { online } = props.wallet;
if (online) return null;
return (<Notification type="error" title="Wallet is offline" />);
};

View File

@ -0,0 +1,23 @@
/* @flow */
import * as React from 'react';
import { Notification } from 'components/Notification';
import type { Props } from '../../index';
export default (props: Props) => {
if (props.connect.transport && props.connect.transport.outdated) {
return (
<Notification
type="warning"
title="New Trezor Bridge is available"
actions={
[{
label: 'Update',
callback: props.routerActions.gotoBridgeUpdate,
}]
}
/>
);
}
return null;
};

View File

@ -0,0 +1,23 @@
/* @flow */
import * as React from 'react';
import { Notification } from 'components/Notification';
import type { Props } from '../../index';
export default (props: Props) => {
const { selectedDevice } = props.wallet;
const outdated = selectedDevice && selectedDevice.features && selectedDevice.firmware === 'outdated';
if (!outdated) return null;
return (
<Notification
type="warning"
title="Firmware update"
actions={
[{
label: 'Update',
callback: props.routerActions.gotoFirmwareUpdate,
}]
}
/>
);
};

View File

@ -0,0 +1,49 @@
/* @flow */
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype';
import * as NotificationActions from 'actions/NotificationActions';
import * as RouterActions from 'actions/RouterActions';
import OnlineStatus from './components/OnlineStatus';
import UpdateBridge from './components/UpdateBridge';
import UpdateFirmware from './components/UpdateFirmware';
export type StateProps = {
connect: $ElementType<State, 'connect'>;
wallet: $ElementType<State, 'wallet'>;
children?: React.Node;
}
export type DispatchProps = {
close: typeof NotificationActions.close;
routerActions: typeof RouterActions;
}
export type Props = StateProps & DispatchProps;
type OwnProps = {};
const Notifications = (props: Props) => (
<React.Fragment>
<OnlineStatus {...props} />
<UpdateBridge {...props} />
<UpdateFirmware {...props} />
</React.Fragment>
);
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
connect: state.connect,
wallet: state.wallet,
});
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
close: bindActionCreators(NotificationActions.close, dispatch),
routerActions: bindActionCreators(RouterActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Notifications);

View File

@ -1,8 +0,0 @@
export default [
{ id: 'Windows', value: 'trezor-bridge-2.0.11-win32-install.exe', label: 'Windows' },
{ id: 'macOS', value: 'trezor-bridge-2.0.11.pkg', label: 'macOS' },
{ id: 'Linux', value: 'trezor-bridge_2.0.11_amd64.deb', label: 'Linux 64-bit (deb)' },
{ id: 'Linux-rpm', value: 'trezor-bridge_2.0.11_amd64.rpm', label: 'Linux 64-bit (rpm)' },
{ id: '01', value: 'trezor-bridge_2.0.11_amd32.deb', label: 'Linux 32-bit (deb)' },
{ id: '02', value: 'trezor-bridge_2.0.11_amd32.rpm', label: 'Linux 32-bit (rpm)' },
];

View File

@ -38,6 +38,7 @@ import type {
Features, Features,
DeviceStatus, DeviceStatus,
DeviceFirmwareStatus, DeviceFirmwareStatus,
DeviceMode,
DeviceMessageType, DeviceMessageType,
TransportMessageType, TransportMessageType,
BlockchainMessageType, BlockchainMessageType,
@ -53,6 +54,7 @@ export type AcquiredDevice = $Exact<{
+features: Features, +features: Features,
+firmware: DeviceFirmwareStatus, +firmware: DeviceFirmwareStatus,
status: DeviceStatus, status: DeviceStatus,
+mode: DeviceMode,
state: ?string, state: ?string,
remember: boolean; // device should be remembered remember: boolean; // device should be remembered

View File

@ -9,12 +9,24 @@ export type SelectedDevice = {
instance: ?number; instance: ?number;
} }
export type LatestBridge = {
version: Array<number>;
directory: string;
packages: Array<{ name: string; url: string; signature?: string; preferred: boolean; }>;
changelog: Array<string>;
}
export type State = { export type State = {
initialized: boolean; initialized: boolean;
error: ?string; error: ?string;
transport: ?{ transport: {
type: string; type: string;
version: string; version: string;
outdated: boolean;
bridge: LatestBridge;
} | {
type: null,
bridge: LatestBridge;
}; };
// browserState: { // browserState: {
// name: string; // name: string;
@ -30,7 +42,15 @@ export type State = {
const initialState: State = { const initialState: State = {
initialized: false, initialized: false,
error: null, error: null,
transport: null, transport: {
type: null,
bridge: {
version: [],
directory: '',
packages: [],
changelog: [],
},
},
browserState: {}, browserState: {},
acquiringDevice: false, acquiringDevice: false,
}; };
@ -63,9 +83,12 @@ export default function connect(state: State = initialState, action: Action): St
case TRANSPORT.ERROR: case TRANSPORT.ERROR:
return { return {
...state, ...state,
// error: action.payload, // message is wrapped in "device" field. It's dispatched from TrezorConnect.on(DEVICE_EVENT...) in TrezorConnectService // error: action.payload.error, // message is wrapped in "device" field. It's dispatched from TrezorConnect.on(DEVICE_EVENT...) in TrezorConnectService
error: 'Transport is missing', error: 'Transport is missing',
transport: null, transport: {
type: null,
bridge: action.payload.bridge,
},
}; };
case CONNECT.START_ACQUIRING: case CONNECT.START_ACQUIRING:

View File

@ -21,7 +21,7 @@ export const getSelectedDevice = (state: State): ?TrezorDevice => {
return state.devices.find((d) => { return state.devices.find((d) => {
if (!d.features && d.path === locationState.device) { if (!d.features && d.path === locationState.device) {
return true; return true;
} if (d.features && d.features.bootloader_mode && d.path === locationState.device) { } if (d.mode === 'bootloader' && d.path === locationState.device) {
return true; return true;
} if (d.features && d.features.device_id === locationState.device && d.instance === instance) { } if (d.features && d.features.device_id === locationState.device && d.instance === instance) {
return true; return true;
@ -30,9 +30,6 @@ export const getSelectedDevice = (state: State): ?TrezorDevice => {
}); });
}; };
//
export const isSelectedDevice = (current: ?TrezorDevice, device: ?TrezorDevice): boolean => !!((current && device && (current.path === device.path && current.instance === device.instance)));
// find device by id and state // find device by id and state
export const findDevice = (devices: Array<TrezorDevice>, deviceId: string, deviceState: string /*, instance: ?number*/): ?TrezorDevice => devices.find((d) => { export const findDevice = (devices: Array<TrezorDevice>, deviceId: string, deviceState: string /*, instance: ?number*/): ?TrezorDevice => devices.find((d) => {
// TODO: && (instance && d.instance === instance) // TODO: && (instance && d.instance === instance)

View File

@ -1,64 +1,99 @@
/* @flow */
import colors from 'config/colors'; import colors from 'config/colors';
const getStatus = (device) => { import type {
let status = 'connected'; TrezorDevice,
if (device.features && device.features.bootloader_mode) { State,
status = 'connected-bootloader'; } from 'flowtype';
} else if (!device.connected) {
status = 'disconnected'; type Transport = $ElementType<$ElementType<State, 'connect'>, 'transport'>;
} else if (!device.available) {
status = 'unavailable'; export const getStatus = (device: TrezorDevice): string => {
} else if (device.type === 'acquired') { if (!device.connected) {
return 'disconnected';
}
if (device.type === 'acquired') {
if (device.mode === 'bootloader') {
return 'bootloader';
}
if (device.mode === 'initialize') {
return 'initialize';
}
if (device.firmware === 'required') {
return 'firmware-required';
}
if (device.status === 'occupied') { if (device.status === 'occupied') {
status = 'used-in-other-window'; return 'used-in-other-window';
} }
} else if (device.type === 'unacquired') { if (device.status === 'used') {
status = 'unacquired'; return 'used-in-other-window';
} }
if (device.firmware === 'outdated') {
return status; return 'firmware-recommended';
}
return 'connected';
}
if (!device.available) { // deprecated
return 'unavailable';
}
if (device.type === 'unacquired') {
return 'unacquired';
}
if (device.type === 'unreadable') {
return 'unreadable';
}
return 'unknown';
}; };
const getStatusName = (deviceStatus) => { export const getStatusName = (deviceStatus: string): string => {
let statusName;
switch (deviceStatus) { switch (deviceStatus) {
case 'used-in-other-window':
statusName = 'Used in other window';
break;
case 'connected': case 'connected':
statusName = 'Connected'; return 'Connected';
break;
case 'connected-bootloader':
statusName = 'Connected (bootloader mode)';
break;
case 'disconnected': case 'disconnected':
statusName = 'Disconnected'; return 'Disconnected';
break; case 'bootloader':
return 'Connected (bootloader mode)';
case 'initialize':
return 'Connected (not initialized)';
case 'firmware-required':
return 'Connected (update required)';
case 'firmware-recommended':
return 'Connected (update recommended)';
case 'used-in-other-window':
return 'Used in other window';
case 'unacquired': case 'unacquired':
statusName = 'Used in other window'; return 'Used in other window';
break;
case 'unavailable': case 'unavailable':
statusName = 'Unavailable'; return 'Unavailable';
break; case 'unreadable':
return 'Unreadable';
default: default:
statusName = 'Status unknown'; return 'Status unknown';
} }
return statusName;
}; };
const isWebUSB = transport => !!((transport && transport.version.indexOf('webusb') >= 0)); export const isWebUSB = (transport: Transport) => !!((transport.type && transport.version.indexOf('webusb') >= 0));
const isDisabled = (selectedDevice, devices, transport) => { export const isDisabled = (selectedDevice: TrezorDevice, devices: Array<TrezorDevice>, transport: Transport) => {
if (isWebUSB(transport)) return false; // always enabled if webusb if (isWebUSB(transport)) return false; // always enabled if webusb
if (devices.length < 1) return true; // no devices if (devices.length < 1) return true; // no devices
if (devices.length === 1) { if (devices.length === 1) {
if (!selectedDevice.features) return true; // unacquired, unreadable if (!selectedDevice.features) return true; // unacquired, unreadable
if (selectedDevice.features.bootloader_mode || !selectedDevice.features.initialized) return true; // bootlader, not initialized if (selectedDevice.mode !== 'normal') return true; // bootloader, not initialized
if (selectedDevice.firmware === 'required') return true; // bootloader, not initialized
} }
return false; // default return false; // default
}; };
const getVersion = (device) => { export const isDeviceAccessible = (device: ?TrezorDevice): boolean => {
if (!device || !device.features) return false;
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 getVersion = (device: TrezorDevice): string => {
let version; let version;
if (device.features && device.features.major_version > 1) { if (device.features && device.features.major_version > 1) {
version = 'T'; version = 'T';
@ -68,38 +103,23 @@ const getVersion = (device) => {
return version; return version;
}; };
const getStatusColor = (deviceStatus) => { export const getStatusColor = (deviceStatus: string): string => {
let color;
switch (deviceStatus) { switch (deviceStatus) {
case 'used-in-other-window':
color = colors.WARNING_PRIMARY;
break;
case 'connected': case 'connected':
color = colors.GREEN_PRIMARY; return colors.GREEN_PRIMARY;
break;
case 'connected-bootloader':
color = colors.WARNING_PRIMARY;
break;
case 'unacquired':
color = colors.WARNING_PRIMARY;
break;
case 'disconnected': case 'disconnected':
color = colors.ERROR_PRIMARY; return colors.ERROR_PRIMARY;
break; case 'bootloader':
case 'initialize':
case 'firmware-recommended':
case 'used-in-other-window':
case 'unacquired':
return colors.WARNING_PRIMARY;
case 'firmware-required':
case 'unavailable': case 'unavailable':
color = colors.ERROR_PRIMARY; case 'unreadable':
break; return colors.ERROR_PRIMARY;
default: default:
color = colors.TEXT_PRIMARY; return colors.TEXT_PRIMARY;
} }
return color;
};
export {
isWebUSB,
getStatus,
isDisabled,
getStatusName,
getVersion,
getStatusColor,
}; };

View File

@ -1,10 +1,13 @@
/* @flow */ /* @flow */
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as RouterActions from 'actions/RouterActions';
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';
import LandingPage from './index'; import LandingPage from './index';
export type StateProps = { export type StateProps = {
localStorage: $ElementType<State, 'localStorage'>, localStorage: $ElementType<State, 'localStorage'>,
modal: $ElementType<State, 'modal'>, modal: $ElementType<State, 'modal'>,
@ -34,8 +37,8 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
devices: state.devices, devices: state.devices,
}); });
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (/* dispatch: Dispatch */): DispatchProps => ({ const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
selectFirstAvailableDevice: bindActionCreators(RouterActions.selectFirstAvailableDevice, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(LandingPage); export default connect(mapStateToProps, mapDispatchToProps)(LandingPage);

View File

@ -4,7 +4,6 @@ import React, { Component } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables'; import { FONT_SIZE, FONT_WEIGHT } from 'config/variables';
import installers from 'constants/bridge';
import { Select } from 'components/Select'; import { Select } from 'components/Select';
import Link from 'components/Link'; import Link from 'components/Link';
import Button from 'components/Button'; import Button from 'components/Button';
@ -13,24 +12,25 @@ import P from 'components/Paragraph';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import ICONS from 'config/icons'; import ICONS from 'config/icons';
import type { State as TrezorConnectState } from 'reducers/TrezorConnectReducer';
type InstallTarget = { type InstallTarget = {
id: string;
value: string; value: string;
label: string; label: string;
signature: ?string;
preferred: boolean;
} }
type State = { type State = {
version: string; currentVersion: string;
target: ?InstallTarget; latestVersion: string;
url: string; installers: Array<InstallTarget>;
target: InstallTarget;
uri: string;
} }
// import type { Props } from './index';
type Props = { type Props = {
browserState: { transport: $ElementType<TrezorConnectState, 'transport'>;
osname: string,
};
} }
const InstallBridgeWrapper = styled.div` const InstallBridgeWrapper = styled.div`
@ -85,21 +85,21 @@ export default class InstallBridge extends Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const currentTarget: ?InstallTarget = installers.find(i => i.id === props.browserState.osname); const installers = props.transport.bridge.packages.map(p => ({
this.state = { label: p.name,
version: '2.0.12', value: p.url,
url: 'https://wallet.trezor.io/data/bridge/2.0.12/', signature: p.signature,
target: currentTarget, preferred: p.preferred,
}; }));
}
componentWillUpdate() { const currentTarget: ?InstallTarget = installers.find(i => i.preferred === true);
if (this.props.browserState.osname && !this.state.target) { this.state = {
const currentTarget: ?InstallTarget = installers.find(i => i.id === this.props.browserState.osname); currentVersion: props.transport.type ? `Your version ${props.transport.version}` : 'Not installed',
this.setState({ latestVersion: props.transport.bridge.version.join('.'),
target: currentTarget, installers,
}); target: currentTarget || installers[0],
} uri: 'https://wallet.trezor.io/data/',
};
} }
onChange(value: InstallTarget) { onChange(value: InstallTarget) {
@ -109,23 +109,26 @@ export default class InstallBridge extends Component<Props, State> {
} }
render() { render() {
if (!this.state.target) { const { target } = this.state;
if (!target) {
return <Loader text="Loading" size={100} />; return <Loader text="Loading" size={100} />;
} }
const { label } = this.state.target;
const url = `${this.state.url}${this.state.target.value}`;
const changelog = this.props.transport.bridge.changelog.map(entry => (
<li key={entry}>{entry}</li>
));
const url = `${this.state.uri}${target.value}`;
return ( return (
<InstallBridgeWrapper> <InstallBridgeWrapper>
<TitleHeader>TREZOR Bridge.<BridgeVersion>Version {this.state.version}</BridgeVersion></TitleHeader> <TitleHeader>TREZOR Bridge.<BridgeVersion>{this.state.currentVersion}</BridgeVersion></TitleHeader>
<P>New communication tool to facilitate the connection between your TREZOR and your internet browser.</P> <P>New communication tool to facilitate the connection between your TREZOR and your internet browser.</P>
<DownloadBridgeWrapper> <DownloadBridgeWrapper>
<SelectWrapper <SelectWrapper
isSearchable={false} isSearchable={false}
isClearable={false} isClearable={false}
value={this.state.target} value={target}
onChange={val => this.onChange(val)} onChange={v => this.onChange(v)}
options={installers} options={this.state.installers}
/> />
<Link href={url}> <Link href={url}>
<DownloadBridgeButton> <DownloadBridgeButton>
@ -134,12 +137,24 @@ export default class InstallBridge extends Component<Props, State> {
color={colors.WHITE} color={colors.WHITE}
size={30} size={30}
/> />
Download for {label} Download latest Bridge {this.state.latestVersion}
</DownloadBridgeButton> </DownloadBridgeButton>
</Link> </Link>
</DownloadBridgeWrapper> </DownloadBridgeWrapper>
{target.signature && (
<P> <P>
<LearnMoreText>Learn more about latest version in</LearnMoreText> <Link
href={this.state.uri + target.signature}
target="_blank"
rel="noreferrer noopener"
isGreen
>Check PGP signature
</Link>
</P>
)}
<P>
{ changelog }
<LearnMoreText>Learn more about latest versions in</LearnMoreText>
<Link <Link
href="https://github.com/trezor/trezord-go/blob/master/CHANGELOG.md" href="https://github.com/trezor/trezord-go/blob/master/CHANGELOG.md"
target="_blank" target="_blank"
@ -148,6 +163,16 @@ export default class InstallBridge extends Component<Props, State> {
>Changelog >Changelog
</Link> </Link>
</P> </P>
{this.props.transport.type && (
<React.Fragment>
<P>
No, i dont want to upgrade Bridge now,
</P>
<P>
Take me <Link href="#/">back to the wallet</Link>
</P>
</React.Fragment>
)}
</InstallBridgeWrapper> </InstallBridgeWrapper>
); );
} }

View File

@ -103,7 +103,7 @@ export default (props: Props) => {
<Log /> <Log />
<LandingContent> <LandingContent>
{shouldShowUnsupportedBrowser && <BrowserNotSupported />} {shouldShowUnsupportedBrowser && <BrowserNotSupported />}
{shouldShowInstallBridge && <InstallBridge browserState={browserState} />} {shouldShowInstallBridge && <InstallBridge transport={transport} />}
{(shouldShowConnectDevice || shouldShowDisconnectDevice) && ( {(shouldShowConnectDevice || shouldShowDisconnectDevice) && (
<div> <div>

View File

@ -1,24 +1,33 @@
/* @flow */
import React, { Component } from 'react'; import React, { Component } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import DeviceHeader from 'components/DeviceHeader'; import DeviceHeader from 'components/DeviceHeader';
import * as deviceUtils from 'utils/device';
import icons from 'config/icons'; import icons from 'config/icons';
import colors from 'config/colors'; import colors from 'config/colors';
import { withRouter } from 'react-router-dom';
import type { TrezorDevice } from 'flowtype';
import type { Props as CommonProps } from '../../../common';
const Wrapper = styled.div``; const Wrapper = styled.div``;
const IconClick = styled.div``; const IconClick = styled.div``;
class DeviceList extends Component { type Props = {
sortByInstance(a, b) { devices: $ElementType<CommonProps, 'devices'>;
selectedDevice: $ElementType<$ElementType<CommonProps, 'wallet'>, 'selectedDevice'>;
onSelectDevice: $ElementType<CommonProps, 'onSelectDevice'>;
forgetDevice: $ElementType<CommonProps, 'forgetDevice'>;
};
class DeviceList extends Component<Props> {
sortByInstance(a: TrezorDevice, b: TrezorDevice) {
if (!a.instance || !b.instance) return -1; if (!a.instance || !b.instance) return -1;
return a.instance > b.instance ? 1 : -1; return a.instance > b.instance ? 1 : -1;
} }
redirectToBootloader(selectedDevice) {
this.props.history.push(`/device/${selectedDevice.features.device_id}/bootloader`);
}
render() { render() {
const { const {
devices, selectedDevice, onSelectDevice, forgetDevice, devices, selectedDevice, onSelectDevice, forgetDevice,
@ -28,16 +37,11 @@ class DeviceList extends Component {
{devices {devices
.sort(this.sortByInstance) .sort(this.sortByInstance)
.map(device => ( .map(device => (
device !== selectedDevice && ( !deviceUtils.isSelectedDevice(selectedDevice, device) && (
<DeviceHeader <DeviceHeader
key={device.state || device.path} key={device.state || device.path}
isBootloader={device.features && device.features.bootloader_mode} isAccessible={deviceUtils.isDeviceAccessible(device)}
onClickWrapper={() => { onClickWrapper={() => {
if (device.features) {
if (device.features.bootloader_mode) {
this.redirectToBootloader(selectedDevice);
}
}
onSelectDevice(device); onSelectDevice(device);
}} }}
onClickIcon={() => forgetDevice(device)} onClickIcon={() => forgetDevice(device)}
@ -69,4 +73,4 @@ class DeviceList extends Component {
} }
} }
export default withRouter(DeviceList); export default DeviceList;

View File

@ -42,8 +42,8 @@ class MenuItems extends Component {
} }
showDeviceMenu() { showDeviceMenu() {
const device = this.props.device; const { device } = this.props;
return device && device.features && !device.features.bootloader_mode && device.features.initialized; return device && device.mode === 'normal';
} }
showClone() { showClone() {

View File

@ -4,7 +4,7 @@ import styled from 'styled-components';
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import type { TrezorDevice } from 'flowtype'; import type { TrezorDevice } from 'flowtype';
import Button from 'components/Button'; import Button from 'components/Button';
import { isWebUSB } from 'utils/device'; import * as deviceUtils from 'utils/device';
import MenuItems from './components/MenuItems'; import MenuItems from './components/MenuItems';
import DeviceList from './components/DeviceList'; import DeviceList from './components/DeviceList';
@ -28,10 +28,6 @@ type DeviceMenuItem = {
} }
class DeviceMenu extends Component<Props> { class DeviceMenu extends Component<Props> {
mouseDownHandler: (event: MouseEvent) => void;
blurHandler: (event: FocusEvent) => void;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.mouseDownHandler = this.mouseDownHandler.bind(this); this.mouseDownHandler = this.mouseDownHandler.bind(this);
@ -42,12 +38,33 @@ class DeviceMenu extends Component<Props> {
window.addEventListener('mousedown', this.mouseDownHandler, false); window.addEventListener('mousedown', this.mouseDownHandler, false);
// window.addEventListener('blur', this.blurHandler, false); // window.addEventListener('blur', this.blurHandler, false);
const { transport } = this.props.connect; const { transport } = this.props.connect;
if (transport && transport.version.indexOf('webusb') >= 0) TrezorConnect.renderWebUSBButton(); if (transport.type && transport.version.indexOf('webusb') >= 0) TrezorConnect.renderWebUSBButton();
} }
componentDidUpdate() { componentDidUpdate() {
const { transport } = this.props.connect; const { transport } = this.props.connect;
if (isWebUSB(transport)) TrezorConnect.renderWebUSBButton(); if (deviceUtils.isWebUSB(transport)) TrezorConnect.renderWebUSBButton();
}
componentWillUnmount(): void {
window.removeEventListener('mousedown', this.mouseDownHandler, false);
}
onDeviceMenuClick(item: DeviceMenuItem, device: TrezorDevice): void {
if (item.type === 'reload') {
this.props.acquireDevice();
} else if (item.type === 'forget') {
this.props.forgetDevice(device);
} else if (item.type === 'clone') {
this.props.duplicateDevice(device);
} else if (item.type === 'settings') {
this.props.toggleDeviceDropdown(false);
this.props.gotoDeviceSettings(device);
}
}
blurHandler(): void {
this.props.toggleDeviceDropdown(false);
} }
mouseDownHandler(event: MouseEvent): void { mouseDownHandler(event: MouseEvent): void {
@ -67,34 +84,16 @@ class DeviceMenu extends Component<Props> {
} }
} }
blurHandler(): void { mouseDownHandler: (event: MouseEvent) => void;
this.props.toggleDeviceDropdown(false);
}
onDeviceMenuClick(item: DeviceMenuItem, device: TrezorDevice): void { blurHandler: (event: FocusEvent) => void;
if (item.type === 'reload') {
this.props.acquireDevice();
} else if (item.type === 'forget') {
this.props.forgetDevice(device);
} else if (item.type === 'clone') {
this.props.duplicateDevice(device);
} else if (item.type === 'settings') {
this.props.toggleDeviceDropdown(false);
this.props.gotoDeviceSettings(device);
}
}
componentWillUnmount(): void {
window.removeEventListener('mousedown', this.mouseDownHandler, false);
}
showDivider() { showDivider() {
return this.props.devices.length > 1; return this.props.devices.length > 1;
} }
showMenuItems() { showMenuItems() {
const { selectedDevice } = this.props.wallet; return deviceUtils.isDeviceAccessible(this.props.wallet.selectedDevice);
return selectedDevice && selectedDevice.features;
} }
render() { render() {
@ -113,7 +112,7 @@ class DeviceMenu extends Component<Props> {
forgetDevice={forgetDevice} forgetDevice={forgetDevice}
/> />
<ButtonWrapper> <ButtonWrapper>
{isWebUSB(transport) && ( {deviceUtils.isWebUSB(transport) && (
<StyledButton isWebUsb>Check for devices</StyledButton> <StyledButton isWebUsb>Check for devices</StyledButton>
)} )}
</ButtonWrapper> </ButtonWrapper>

View File

@ -1,3 +1,5 @@
/* @flow */
import * as React from 'react'; import * as React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import colors from 'config/colors'; import colors from 'config/colors';
@ -6,10 +8,13 @@ import icons from 'config/icons';
import { TransitionGroup, CSSTransition } from 'react-transition-group'; import { TransitionGroup, CSSTransition } from 'react-transition-group';
import styled from 'styled-components'; import styled from 'styled-components';
import DeviceHeader from 'components/DeviceHeader'; import DeviceHeader from 'components/DeviceHeader';
import * as deviceUtils from 'utils/device';
import AccountMenu from './components/AccountMenu'; import AccountMenu from './components/AccountMenu';
import CoinMenu from './components/CoinMenu'; import CoinMenu from './components/CoinMenu';
import DeviceMenu from './components/DeviceMenu'; import DeviceMenu from './components/DeviceMenu';
import StickyContainer from './components/StickyContainer'; import StickyContainer from './components/StickyContainer';
import type { Props } from './components/common'; import type { Props } from './components/common';
const Header = styled(DeviceHeader)` const Header = styled(DeviceHeader)`
@ -105,6 +110,7 @@ const TransitionMenu = (props: TransitionMenuProps): React$Element<TransitionGro
type State = { type State = {
animationType: ?string; animationType: ?string;
shouldRenderDeviceSelection: boolean; shouldRenderDeviceSelection: boolean;
clicked: boolean;
} }
class LeftNavigation extends React.PureComponent<Props, State> { class LeftNavigation extends React.PureComponent<Props, State> {
@ -119,11 +125,11 @@ class LeftNavigation extends React.PureComponent<Props, State> {
}; };
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props) {
const { dropdownOpened, selectedDevice } = nextProps.wallet; const { dropdownOpened, selectedDevice } = nextProps.wallet;
const hasNetwork = nextProps.location.state && nextProps.location.state.network; const { location } = nextProps.router;
const hasFeatures = selectedDevice && selectedDevice.features; const hasNetwork = location && location.state.network;
const deviceReady = hasFeatures && !selectedDevice.features.bootloader_mode && selectedDevice.features.initialized; const deviceReady = selectedDevice && selectedDevice.features && selectedDevice.mode === 'normal';
if (dropdownOpened) { if (dropdownOpened) {
this.setState({ shouldRenderDeviceSelection: true }); this.setState({ shouldRenderDeviceSelection: true });
} else if (hasNetwork) { } else if (hasNetwork) {
@ -176,22 +182,23 @@ class LeftNavigation extends React.PureComponent<Props, State> {
); );
} }
const isDeviceInBootloader = this.props.wallet.selectedDevice.features && this.props.wallet.selectedDevice.features.bootloader_mode; const { selectedDevice } = props.wallet;
const isDeviceAccessible = deviceUtils.isDeviceAccessible(selectedDevice);
return ( return (
<StickyContainer <StickyContainer
location={this.props.location.pathname} location={props.router.location.pathname}
deviceSelection={this.props.wallet.dropdownOpened} deviceSelection={this.props.wallet.dropdownOpened}
> >
<Header <Header
isSelected isSelected
isHoverable={false} isHoverable={false}
onClickWrapper={() => { onClickWrapper={() => {
if (!isDeviceInBootloader || this.props.devices.length > 1) { if (isDeviceAccessible || this.props.devices.length > 1) {
this.handleOpen(); this.handleOpen();
} }
}} }}
device={this.props.wallet.selectedDevice} device={this.props.wallet.selectedDevice}
disabled={isDeviceInBootloader && this.props.devices.length === 1} disabled={!isDeviceAccessible && this.props.devices.length === 1}
isOpen={this.props.wallet.dropdownOpened} isOpen={this.props.wallet.dropdownOpened}
icon={( icon={(
<React.Fragment> <React.Fragment>
@ -211,7 +218,7 @@ class LeftNavigation extends React.PureComponent<Props, State> {
/> />
<Body> <Body>
{this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />} {this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />}
{!isDeviceInBootloader && menu} {isDeviceAccessible && menu}
</Body> </Body>
<Footer key="sticky-footer"> <Footer key="sticky-footer">
<Help> <Help>
@ -241,8 +248,12 @@ LeftNavigation.propTypes = {
pending: PropTypes.array, pending: PropTypes.array,
toggleDeviceDropdown: PropTypes.func, toggleDeviceDropdown: PropTypes.func,
selectedDevice: PropTypes.object, addAccount: PropTypes.func,
acquireDevice: PropTypes.func,
forgetDevice: PropTypes.func,
duplicateDevice: PropTypes.func,
gotoDeviceSettings: PropTypes.func,
onSelectDevice: PropTypes.func,
}; };
export default LeftNavigation; export default LeftNavigation;

View File

@ -12,6 +12,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';
import AppNotifications from 'components/notifications/App';
import ContextNotifications from 'components/notifications/Context'; import ContextNotifications from 'components/notifications/Context';
import Log from 'components/Log'; import Log from 'components/Log';
@ -84,6 +85,7 @@ const Body = styled.div`
const Wallet = (props: WalletContainerProps) => ( const Wallet = (props: WalletContainerProps) => (
<AppWrapper> <AppWrapper>
<Header /> <Header />
<AppNotifications />
<WalletWrapper> <WalletWrapper>
{props.wallet.selectedDevice && <LeftNavigation />} {props.wallet.selectedDevice && <LeftNavigation />}
<MainContent> <MainContent>

View File

@ -1,13 +1,32 @@
/* @flow */
import React from 'react'; import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { H1 } from 'components/Heading'; import { H1 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import colors from 'config/colors'; import colors from 'config/colors';
import Link from 'components/Link'; import Link from 'components/Link';
import Button from 'components/Button'; import Button from 'components/Button';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { FONT_SIZE } from 'config/variables'; import { FONT_SIZE } from 'config/variables';
import * as deviceUtils from 'utils/device';
import * as RouterActions from 'actions/RouterActions';
import type {
TrezorDevice,
State,
Dispatch,
} from 'flowtype';
type Props = {
device: ?TrezorDevice;
cancel: typeof RouterActions.selectFirstAvailableDevice,
}
const Wrapper = styled.section` const Wrapper = styled.section`
display: flex; display: flex;
@ -31,7 +50,7 @@ const StyledP = styled(P)`
padding: 0 0 15px 0; padding: 0 0 15px 0;
`; `;
const FirmwareUpdate = () => ( const FirmwareUpdate = (props: Props) => (
<Wrapper> <Wrapper>
<Image> <Image>
<svg width="181px" height="134px" viewBox="0 0 181 134" version="1.1" xmlns="http://www.w3.org/2000/svg"> <svg width="181px" height="134px" viewBox="0 0 181 134" version="1.1" xmlns="http://www.w3.org/2000/svg">
@ -114,10 +133,19 @@ const FirmwareUpdate = () => (
<Link href="https://wallet.trezor.io" target="_blank"> <Link href="https://wallet.trezor.io" target="_blank">
<Button>Take me to the old wallet</Button> <Button>Take me to the old wallet</Button>
</Link> </Link>
{deviceUtils.isDeviceAccessible(props.device) && (
<StyledNavLink to="/"> <StyledNavLink to="/">
Ill do that later. Ill do that later.
</StyledNavLink> </StyledNavLink>
)}
</Wrapper> </Wrapper>
); );
export default connect(null, null)(FirmwareUpdate); export default connect(
(state: State) => ({
device: state.wallet.selectedDevice,
}),
(dispatch: Dispatch) => ({
cancel: bindActionCreators(RouterActions.selectFirstAvailableDevice, dispatch),
}),
)(FirmwareUpdate);