diff --git a/CHANGELOG.md b/CHANGELOG.md index 07b0ed68..7cb10b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ __added__ - Fiat currency switcher - Application settings - Button to copy log to clipboard +- Import tool (for support) - Prettier __updated__ diff --git a/src/actions/DiscoveryActions.js b/src/actions/DiscoveryActions.js index 22ca82a0..57e97251 100644 --- a/src/actions/DiscoveryActions.js +++ b/src/actions/DiscoveryActions.js @@ -16,6 +16,7 @@ import type { Account, } from 'flowtype'; import type { Discovery, State } from 'reducers/DiscoveryReducer'; +import * as LocalStorageActions from 'actions/LocalStorageActions'; import * as BlockchainActions from './BlockchainActions'; import * as EthereumDiscoveryActions from './ethereum/DiscoveryActions'; import * as RippleDiscoveryActions from './ripple/DiscoveryActions'; @@ -120,6 +121,7 @@ const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean) } if (!discoveryProcess) { + dispatch(addImportedAccounts()); dispatch(begin(device, network)); } else if (discoveryProcess.completed && !ignoreCompleted) { dispatch({ @@ -380,3 +382,17 @@ export const addAccount = (): ThunkAction => (dispatch: Dispatch, getState: GetS if (!selected) return; dispatch(start(selected, getState().router.location.state.network, true)); }; + +export const addImportedAccounts = (): ThunkAction => (dispatch: Dispatch): void => { + // get imported accounts from local storage + const importedAccounts = LocalStorageActions.getImportedAccounts(); + if (importedAccounts) { + // create each account + importedAccounts.forEach(account => { + dispatch({ + type: ACCOUNT.CREATE, + payload: account, + }); + }); + } +}; diff --git a/src/actions/ImportAccountActions.js b/src/actions/ImportAccountActions.js new file mode 100644 index 00000000..ff6c24fe --- /dev/null +++ b/src/actions/ImportAccountActions.js @@ -0,0 +1,153 @@ +/* @flow */ + +import * as ACCOUNT from 'actions/constants/account'; +import * as IMPORT from 'actions/constants/importAccount'; +import * as NOTIFICATION from 'actions/constants/notification'; +import type { AsyncAction, TrezorDevice, Network, Dispatch, GetState } from 'flowtype'; +import * as BlockchainActions from 'actions/ethereum/BlockchainActions'; +import * as LocalStorageActions from 'actions/LocalStorageActions'; +import TrezorConnect from 'trezor-connect'; +import { toDecimalAmount } from 'utils/formatUtils'; + +export type ImportAccountAction = + | { + type: typeof IMPORT.START, + } + | { + type: typeof IMPORT.SUCCESS, + } + | { + type: typeof IMPORT.FAIL, + error: ?string, + }; + +export const importAddress = ( + address: string, + network: Network, + device: ?TrezorDevice +): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { + if (!device) return; + + dispatch({ + type: IMPORT.START, + }); + + let payload; + const index = getState().accounts.filter( + a => + a.imported === true && + a.network === network.shortcut && + device && + a.deviceState === device.state + ).length; + + try { + if (network.type === 'ethereum') { + const account = await dispatch( + BlockchainActions.discoverAccount(device, address, network.shortcut) + ); + + const empty = account.nonce <= 0 && account.balance === '0'; + payload = { + imported: true, + index, + network: network.shortcut, + deviceID: device.features ? device.features.device_id : '0', + deviceState: device.state || '0', + accountPath: account.path || [], + descriptor: account.descriptor, + + balance: account.balance, + availableBalance: account.balance, + block: account.block, + transactions: account.transactions, + empty, + + networkType: 'ethereum', + nonce: account.nonce, + }; + dispatch({ + type: ACCOUNT.CREATE, + payload, + }); + dispatch({ + type: IMPORT.SUCCESS, + }); + dispatch(LocalStorageActions.setImportedAccount(payload)); + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'success', + title: 'The account has been successfully imported', + cancelable: true, + }, + }); + } else if (network.type === 'ripple') { + const response = await TrezorConnect.rippleGetAccountInfo({ + account: { + descriptor: address, + }, + coin: network.shortcut, + }); + + // handle TREZOR response error + if (!response.success) { + throw new Error(response.payload.error); + } + + const account = response.payload; + const empty = account.sequence <= 0 && account.balance === '0'; + + payload = { + imported: true, + index, + network: network.shortcut, + deviceID: device.features ? device.features.device_id : '0', + deviceState: device.state || '0', + accountPath: account.path || [], + descriptor: account.descriptor, + + balance: toDecimalAmount(account.balance, network.decimals), + availableBalance: toDecimalAmount(account.availableBalance, network.decimals), + block: account.block, + transactions: account.transactions, + empty, + + networkType: 'ripple', + sequence: account.sequence, + reserve: toDecimalAmount(account.reserve, network.decimals), + }; + dispatch({ + type: ACCOUNT.CREATE, + payload, + }); + dispatch({ + type: IMPORT.SUCCESS, + }); + dispatch(LocalStorageActions.setImportedAccount(payload)); + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'success', + title: 'The account has been successfully imported', + cancelable: true, + }, + }); + } + } catch (error) { + dispatch({ + type: IMPORT.FAIL, + error: error.message, + }); + + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Import account error', + message: error.message, + cancelable: true, + }, + }); + } +}; diff --git a/src/actions/LocalStorageActions.js b/src/actions/LocalStorageActions.js index baf0a171..2a2a2736 100644 --- a/src/actions/LocalStorageActions.js +++ b/src/actions/LocalStorageActions.js @@ -52,6 +52,7 @@ const { STORAGE_PATH } = storageUtils; const KEY_VERSION: string = `${STORAGE_PATH}version`; const KEY_DEVICES: string = `${STORAGE_PATH}devices`; const KEY_ACCOUNTS: string = `${STORAGE_PATH}accounts`; +const KEY_IMPORTED_ACCOUNTS: string = `${STORAGE_PATH}importedAccounts`; const KEY_DISCOVERY: string = `${STORAGE_PATH}discovery`; const KEY_TOKENS: string = `${STORAGE_PATH}tokens`; const KEY_PENDING: string = `${STORAGE_PATH}pending`; @@ -321,3 +322,36 @@ export const setLocalCurrency = (): ThunkAction => ( const { localCurrency } = getState().wallet; storageUtils.set(TYPE, KEY_LOCAL_CURRENCY, JSON.stringify(localCurrency)); }; + +export const setImportedAccount = (account: Account): ThunkAction => (): void => { + const prevImportedAccounts: ?Array = getImportedAccounts(); + let importedAccounts = [account]; + if (prevImportedAccounts) { + importedAccounts = importedAccounts.concat(prevImportedAccounts); + } + storageUtils.set(TYPE, KEY_IMPORTED_ACCOUNTS, JSON.stringify(importedAccounts)); +}; + +export const getImportedAccounts = (): ?Array => { + const importedAccounts: ?string = storageUtils.get(TYPE, KEY_IMPORTED_ACCOUNTS); + if (importedAccounts) { + return JSON.parse(importedAccounts); + } + return null; +}; + +export const removeImportedAccounts = (device: TrezorDevice): ThunkAction => ( + dispatch: Dispatch +): void => { + const importedAccounts: ?Array = getImportedAccounts(); + if (!importedAccounts) return; + + const deviceId = device.features ? device.features.device_id : null; + const filteredImportedAccounts = importedAccounts.filter( + account => account.deviceID !== deviceId + ); + storageUtils.remove(TYPE, KEY_IMPORTED_ACCOUNTS); + filteredImportedAccounts.forEach(account => { + dispatch(setImportedAccount(account)); + }); +}; diff --git a/src/actions/constants/importAccount.js b/src/actions/constants/importAccount.js new file mode 100644 index 00000000..ac5fd498 --- /dev/null +++ b/src/actions/constants/importAccount.js @@ -0,0 +1,5 @@ +/* @flow */ + +export const START: 'import__account__start' = 'import__account__start'; +export const SUCCESS: 'import__account__success' = 'import__account__success'; +export const FAIL: 'import__account__fail' = 'import__account__fail'; diff --git a/src/components/Transaction/index.js b/src/components/Transaction/index.js index b36f2b52..2e035b2a 100644 --- a/src/components/Transaction/index.js +++ b/src/components/Transaction/index.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; - +import { SCREEN_SIZE } from 'config/variables'; import { Link, colors } from 'trezor-ui-components'; import type { Transaction, Network } from 'flowtype'; @@ -17,14 +17,19 @@ const Wrapper = styled.div` padding: 14px 0; display: flex; flex-direction: row; + word-break: break-all; &:last-child { border-bottom: 0px; } + + @media screen and (max-width: ${SCREEN_SIZE.SM}) { + flex-direction: column; + } `; const Addresses = styled.div` - flex: 1; + flex: 1 1 auto; `; const Address = styled.div` @@ -43,9 +48,16 @@ const Date = styled(Link)` line-height: 18px; padding-right: 8px; border-bottom: 0px; + flex: 0 1 auto; + word-break: normal; +`; + +const TransactionHash = styled(Date)` + word-break: break-all; `; const Value = styled.div` + flex: 1 1 auto; padding-left: 8px; white-space: nowrap; text-align: right; @@ -100,9 +112,9 @@ const TransactionItem = ({ tx, network }: Props) => {
{addr}
))} {!tx.blockHeight && ( - + Transaction hash: {tx.hash} - + )} diff --git a/src/components/notifications/Context/components/Static/index.js b/src/components/notifications/Context/components/Static/index.js index cb7e5491..d14c5aa0 100644 --- a/src/components/notifications/Context/components/Static/index.js +++ b/src/components/notifications/Context/components/Static/index.js @@ -4,20 +4,25 @@ import * as React from 'react'; import { Notification, Link } from 'trezor-ui-components'; import Bignumber from 'bignumber.js'; import { FormattedMessage } from 'react-intl'; +import { withRouter } from 'react-router-dom'; +import type { ContextRouter } from 'react-router'; + import l10nCommonMessages from 'views/common.messages'; +import { matchPath } from 'react-router'; +import { getPattern } from 'support/routes'; import l10nMessages from './index.messages'; import type { Props } from '../../index'; -export default (props: Props) => { +export default withRouter((props: {| ...Props, ...ContextRouter |}) => { const { selectedAccount } = props; const { account } = selectedAccount; const { location } = props.router; const notifications = []; - if (!location || !selectedAccount || !account) return null; + if (!location) return null; // Ripple minimum reserve notification - if (account.networkType === 'ripple') { + if (selectedAccount && account && account.networkType === 'ripple') { const { reserve, balance } = account; const bigBalance = new Bignumber(balance); const bigReserve = new Bignumber(reserve); @@ -51,5 +56,28 @@ export default (props: Props) => { } } + // Import tool notification + if (matchPath(location.pathname, { path: getPattern('wallet-import') })) { + notifications.push( + + ); + } + + if (account && account.imported) { + notifications.push( + + ); + } + return {notifications}; -}; +}); diff --git a/src/config/variables.js b/src/config/variables.js index 2bea148b..80eb0c5e 100644 --- a/src/config/variables.js +++ b/src/config/variables.js @@ -39,6 +39,7 @@ export const FONT_SIZE = { H3: '1rem', H4: '0.8571rem', COUNTER: '0.7857rem', + BADGE: '0.7857rem', }; export const FONT_WEIGHT = { diff --git a/src/flowtype/index.js b/src/flowtype/index.js index be8638cf..f7b26df4 100644 --- a/src/flowtype/index.js +++ b/src/flowtype/index.js @@ -32,6 +32,7 @@ import type { TokenAction } from 'actions/TokenActions'; import type { TrezorConnectAction } from 'actions/TrezorConnectActions'; import type { WalletAction } from 'actions/WalletActions'; import type { Web3Action } from 'actions/Web3Actions'; +import type { ImportAccountAction } from 'actions/ImportAccountActions'; import type { FiatRateAction } from 'services/CoingeckoService'; // this service has no action file, all is written inside one file import type { @@ -142,7 +143,8 @@ export type Action = | TrezorConnectAction | WalletAction | Web3Action - | FiatRateAction; + | FiatRateAction + | ImportAccountAction; export type State = ReducersState; diff --git a/src/reducers/AccountsReducer.js b/src/reducers/AccountsReducer.js index e84452c3..3fe36f19 100644 --- a/src/reducers/AccountsReducer.js +++ b/src/reducers/AccountsReducer.js @@ -75,6 +75,11 @@ const createAccount = (state: State, account: Account): State => { } const newState: State = [...state]; newState.push(account); + + // sort the accounts array so the imported accounts always come before discovered accounts + if (account.imported) { + newState.sort((a, b) => Number(b.imported) - Number(a.imported) || a.index - b.index); + } return newState; }; diff --git a/src/reducers/DiscoveryReducer.js b/src/reducers/DiscoveryReducer.js index 7dca1bc5..30b42061 100644 --- a/src/reducers/DiscoveryReducer.js +++ b/src/reducers/DiscoveryReducer.js @@ -95,7 +95,11 @@ const complete = (state: State, action: DiscoveryCompleteAction): State => { const accountCreate = (state: State, account: Account): State => { const index: number = findIndex(state, account.network, account.deviceState); const newState: State = [...state]; - newState[index].accountIndex++; + // do not increment index when adding imported account + // imported accounts should not interfere with the index used in discovery proccess. + if (!account.imported) { + newState[index].accountIndex++; + } return newState; }; diff --git a/src/reducers/ImportAccountReducer.js b/src/reducers/ImportAccountReducer.js new file mode 100644 index 00000000..7ecd5e59 --- /dev/null +++ b/src/reducers/ImportAccountReducer.js @@ -0,0 +1,43 @@ +/* @flow */ + +import * as IMPORT from 'actions/constants/importAccount'; + +import type { Action } from 'flowtype'; + +export type ImportState = { + loading: boolean, + error: ?string, +}; + +export const initialState: ImportState = { + loading: false, + error: null, +}; + +export default (state: ImportState = initialState, action: Action): ImportState => { + switch (action.type) { + case IMPORT.START: + return { + ...state, + loading: true, + error: null, + }; + + case IMPORT.SUCCESS: + return { + ...state, + loading: false, + error: null, + }; + + case IMPORT.FAIL: + return { + ...state, + loading: false, + error: action.error, + }; + + default: + return state; + } +}; diff --git a/src/reducers/index.js b/src/reducers/index.js index 4738bfb5..6af93617 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -11,6 +11,7 @@ import notifications from 'reducers/NotificationReducer'; import modal from 'reducers/ModalReducer'; import web3 from 'reducers/Web3Reducer'; import accounts from 'reducers/AccountsReducer'; +import importAccount from 'reducers/ImportAccountReducer'; import selectedAccount from 'reducers/SelectedAccountReducer'; import sendFormEthereum from 'reducers/SendFormEthereumReducer'; import sendFormRipple from 'reducers/SendFormRippleReducer'; @@ -32,6 +33,7 @@ const reducers = { notifications, modal, web3, + importAccount, accounts, selectedAccount, sendFormEthereum, diff --git a/src/reducers/utils/index.js b/src/reducers/utils/index.js index 473852d0..41422f4c 100644 --- a/src/reducers/utils/index.js +++ b/src/reducers/utils/index.js @@ -83,10 +83,15 @@ export const getSelectedAccount = (state: State): ?Account => { const locationState = state.router.location.state; if (!device || !locationState.network || !locationState.account) return null; - const index: number = parseInt(locationState.account, 10); + // imported account index has 'i' prefix + const isImported = /^i\d+$/i.test(locationState.account); + const index: number = isImported + ? parseInt(locationState.account.substr(1), 10) + : parseInt(locationState.account, 10); return state.accounts.find( a => + a.imported === isImported && a.deviceState === device.state && a.index === index && a.network === locationState.network diff --git a/src/services/LocalStorageService.js b/src/services/LocalStorageService.js index c5d9daf6..018941ba 100644 --- a/src/services/LocalStorageService.js +++ b/src/services/LocalStorageService.js @@ -58,6 +58,10 @@ const LocalStorageService: Middleware = (api: MiddlewareAPI) => (next: Middlewar case CONNECT.FORGET: case CONNECT.FORGET_SINGLE: case CONNECT.FORGET_SILENT: + api.dispatch(LocalStorageActions.save()); + api.dispatch(LocalStorageActions.removeImportedAccounts(action.device)); + break; + case CONNECT.RECEIVE_WALLET_TYPE: case DEVICE.CHANGED: case DEVICE.DISCONNECT: diff --git a/src/services/LogService.js b/src/services/LogService.js index ad797e3c..ee9d7d31 100644 --- a/src/services/LogService.js +++ b/src/services/LogService.js @@ -2,6 +2,7 @@ import * as LogActions from 'actions/LogActions'; import { TRANSPORT, DEVICE } from 'trezor-connect'; import * as DISCOVERY from 'actions/constants/discovery'; +import * as ACCOUNT from 'actions/constants/account'; import type { Middleware, MiddlewareAPI, MiddlewareDispatch, Action } from 'flowtype'; @@ -10,6 +11,7 @@ const actions: Array = [ DEVICE.CONNECT, DEVICE.DISCONNECT, DISCOVERY.START, + ACCOUNT.CREATE, ]; /** @@ -40,6 +42,9 @@ const LogService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch case DISCOVERY.START: api.dispatch(LogActions.add('Discovery started', action)); break; + case ACCOUNT.CREATE: + api.dispatch(LogActions.add('Account created', action)); + break; default: break; } diff --git a/src/support/routes.js b/src/support/routes.js index abc46b11..77f84172 100644 --- a/src/support/routes.js +++ b/src/support/routes.js @@ -23,9 +23,9 @@ export const routes: Array = [ fields: ['bridge'], }, { - name: 'landing-import', - pattern: '/import', - fields: ['import'], + name: 'wallet-import', + pattern: '/device/:device/import', + fields: ['device', 'import'], }, { name: 'wallet-settings', diff --git a/src/views/Landing/views/Import/index.js b/src/views/Landing/views/Import/index.js deleted file mode 100644 index 3b689ed0..00000000 --- a/src/views/Landing/views/Import/index.js +++ /dev/null @@ -1,28 +0,0 @@ -/* @flow */ - -import React from 'react'; -import styled from 'styled-components'; -import { Button, Link, Icon, H5, icons, colors } from 'trezor-ui-components'; -import LandingWrapper from 'views/Landing/components/LandingWrapper'; - -const Wrapper = styled.div` - width: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -`; - -const Import = () => ( - - - -
Import tool is under construction
- - - -
-
-); - -export default Import; diff --git a/src/views/Wallet/components/Content/index.js b/src/views/Wallet/components/Content/index.js index 4682ffe9..c214cff2 100644 --- a/src/views/Wallet/components/Content/index.js +++ b/src/views/Wallet/components/Content/index.js @@ -40,11 +40,14 @@ const Loading = styled.div` flex-direction: column; `; +const LoaderWrapper = styled.div` + margin-right: 10px; +`; + const Title = styled(H4)` font-size: ${FONT_SIZE.BIGGER}; font-weight: ${FONT_WEIGHT.NORMAL}; color: ${props => (props.type === 'progress' ? colors.TEXT_SECONDARY : '')}; - margin-left: 10px; text-align: center; padding: 0; `; @@ -80,7 +83,11 @@ const Content = ({ className, children, isLoading = false, loader, exceptionPage {isLoading && loader && ( - {loader.type === 'progress' && } + {loader.type === 'progress' && ( + + + + )} {loader.title || ( <FormattedMessage {...l10nMessages.TR_INITIALIZING_ACCOUNTS} /> diff --git a/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js b/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js index b680de05..0c441fb3 100644 --- a/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js +++ b/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js @@ -28,8 +28,6 @@ const Text = styled.span` const RowAccountWrapper = styled.div` width: 100%; display: flex; - flex-direction: column; - align-items: flex-start; padding: ${LEFT_NAVIGATION_ROW.PADDING}; font-size: ${FONT_SIZE.BASE}; color: ${colors.TEXT_PRIMARY}; @@ -86,6 +84,25 @@ const DiscoveryLoadingText = styled.span` margin-left: 14px; `; +const Col = styled.div` + display: flex; + flex: 1; + flex-direction: column; +`; + +const RightCol = styled(Col)` + justify-content: center; +`; + +const Badge = styled.div` + padding: 4px 8px; + background: lightslategray; + color: white; + font-size: ${FONT_SIZE.BADGE}; + border-radius: 3px; + align-self: flex-end; +`; + // TODO: Refactorize deviceStatus & selectedAccounts const AccountMenu = (props: Props) => { const selected = props.wallet.selectedDevice; @@ -102,10 +119,18 @@ const AccountMenu = (props: Props) => { if (!selected || !network) return null; const deviceAccounts: Accounts = findDeviceAccounts(accounts, selected, location.state.network); + const discoveryAccounts: Accounts = deviceAccounts.filter( + account => account.imported === false + ); const selectedAccounts = deviceAccounts.map((account, i) => { // const url: string = `${baseUrl}/network/${location.state.network}/account/${i}`; - const url: string = location.pathname.replace(/account+\/([0-9]*)/, `account/${i}`); + let url: string; + if (account.imported) { + url = location.pathname.replace(/account+\/(i?[0-9]*)/, `account/i${account.index}`); + } else { + url = location.pathname.replace(/account+\/(i?[0-9]*)/, `account/${account.index}`); + } let balance: ?string = null; const fiatRates = props.fiat.find(f => f.network === network.shortcut); @@ -125,36 +150,41 @@ const AccountMenu = (props: Props) => { } } - const urlAccountIndex = parseInt(props.router.location.state.account, 10); return ( - <NavLink to={url} key={account.index}> + <NavLink to={url} key={url}> <Row column> - <RowAccountWrapper - isSelected={urlAccountIndex === account.index} - borderTop={account.index === 0} - > - <FormattedMessage - {...l10nCommonMessages.TR_ACCOUNT_HASH} - values={{ number: account.index + 1 }} - /> - {balance && !props.wallet.hideBalance && ( - <Text> - {balance} - {fiatRates && ( - <FormattedNumber - currency={localCurrency} - value={fiat} - minimumFractionDigits={2} - // eslint-disable-next-line react/style-prop-object - style="currency" - /> - )} - </Text> - )} - {!balance && ( - <Text> - <FormattedMessage {...l10nMessages.TR_LOADING_DOT_DOT_DOT} /> - </Text> + <RowAccountWrapper isSelected={location.pathname === url} borderTop={i === 0}> + <Col> + <FormattedMessage + {...(account.imported + ? l10nCommonMessages.TR_IMPORTED_ACCOUNT_HASH + : l10nCommonMessages.TR_ACCOUNT_HASH)} + values={{ number: account.index + 1 }} + /> + {balance && !props.wallet.hideBalance && ( + <Text> + {balance} + {fiatRates && ( + <FormattedNumber + currency={localCurrency} + value={fiat} + minimumFractionDigits={2} + // eslint-disable-next-line react/style-prop-object + style="currency" + /> + )} + </Text> + )} + {!balance && ( + <Text> + <FormattedMessage {...l10nMessages.TR_LOADING_DOT_DOT_DOT} /> + </Text> + )} + </Col> + {account.imported && ( + <RightCol> + <Badge>watch-only</Badge> + </RightCol> )} </RowAccountWrapper> </Row> @@ -168,7 +198,7 @@ const AccountMenu = (props: Props) => { ); if (discovery && discovery.completed) { - const lastAccount = deviceAccounts[deviceAccounts.length - 1]; + const lastAccount = discoveryAccounts[discoveryAccounts.length - 1]; if (!selected.connected) { discoveryStatus = ( <Tooltip diff --git a/src/views/Wallet/components/TopNavigationAccount/index.js b/src/views/Wallet/components/TopNavigationAccount/index.js index 748a610a..6e5a4869 100644 --- a/src/views/Wallet/components/TopNavigationAccount/index.js +++ b/src/views/Wallet/components/TopNavigationAccount/index.js @@ -95,9 +95,11 @@ class TopNavigationAccount extends React.PureComponent<Props, LocalState> { render() { const { state, pathname } = this.props.router.location; if (!state) return null; - const { network } = this.props.selectedAccount; + const { network, account } = this.props.selectedAccount; if (!network) return null; + const isAccountImported = account && account.imported; + const basePath = `/device/${state.device}/network/${state.network}/account/${ state.account }`; @@ -113,7 +115,7 @@ class TopNavigationAccount extends React.PureComponent<Props, LocalState> { <StyledNavLink to={`${basePath}/send`}> <FormattedMessage {...l10nMessages.TR_NAV_SEND} /> </StyledNavLink> - {network.type === 'ethereum' && ( + {network.type === 'ethereum' && !isAccountImported && ( <StyledNavLink to={`${basePath}/signverify`}> <FormattedMessage {...l10nMessages.TR_NAV_SIGN_AND_VERIFY} /> </StyledNavLink> diff --git a/src/views/Wallet/views/Account/Receive/ethereum/index.js b/src/views/Wallet/views/Account/Receive/ethereum/index.js index 406820db..f48436e1 100644 --- a/src/views/Wallet/views/Account/Receive/ethereum/index.js +++ b/src/views/Wallet/views/Account/Receive/ethereum/index.js @@ -95,10 +95,11 @@ const AccountReceive = (props: Props) => { const isAddressVerifying = props.modal.context === CONTEXT_DEVICE && props.modal.windowType === 'ButtonRequest_Address'; - const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified; + const isAddressHidden = + !isAddressVerifying && !addressVerified && !addressUnverified && !account.imported; let address = `${account.descriptor.substring(0, 20)}...`; - if (addressVerified || addressUnverified || isAddressVerifying) { + if (addressVerified || addressUnverified || isAddressVerifying || account.imported) { address = account.descriptor; } @@ -166,7 +167,7 @@ const AccountReceive = (props: Props) => { ) } /> - {!(addressVerified || addressUnverified) && ( + {!(addressVerified || addressUnverified) && !account.imported && ( <ShowAddressButton icon={ICONS.EYE} onClick={() => props.showAddress(account.accountPath)} @@ -176,20 +177,21 @@ const AccountReceive = (props: Props) => { </ShowAddressButton> )} </Row> - {(addressVerified || addressUnverified) && !isAddressVerifying && ( - <QrWrapper> - <Label> - <FormattedMessage {...l10nReceiveMessages.TR_QR_CODE} /> - </Label> - <StyledQRCode - bgColor="#FFFFFF" - fgColor="#000000" - level="Q" - style={{ width: 150 }} - value={account.descriptor} - /> - </QrWrapper> - )} + {((addressVerified || addressUnverified) && !isAddressVerifying) || + (account.imported && ( + <QrWrapper> + <Label> + <FormattedMessage {...l10nReceiveMessages.TR_QR_CODE} /> + </Label> + <StyledQRCode + bgColor="#FFFFFF" + fgColor="#000000" + level="Q" + style={{ width: 150 }} + value={account.descriptor} + /> + </QrWrapper> + ))} </AddressWrapper> </React.Fragment> </Content> diff --git a/src/views/Wallet/views/Account/Receive/ripple/index.js b/src/views/Wallet/views/Account/Receive/ripple/index.js index 93480906..f1b0e8c5 100644 --- a/src/views/Wallet/views/Account/Receive/ripple/index.js +++ b/src/views/Wallet/views/Account/Receive/ripple/index.js @@ -101,10 +101,11 @@ const AccountReceive = (props: Props) => { const isAddressVerifying = props.modal.context === CONTEXT_DEVICE && props.modal.windowType === 'ButtonRequest_Address'; - const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified; + const isAddressHidden = + !isAddressVerifying && !addressVerified && !addressUnverified && !account.imported; let address = `${account.descriptor.substring(0, 20)}...`; - if (addressVerified || addressUnverified || isAddressVerifying) { + if (addressVerified || addressUnverified || isAddressVerifying || account.imported) { address = account.descriptor; } @@ -172,7 +173,7 @@ const AccountReceive = (props: Props) => { ) } /> - {!(addressVerified || addressUnverified) && ( + {!(addressVerified || addressUnverified) && !account.imported && ( <ShowAddressButton onClick={() => props.showAddress(account.accountPath)} isDisabled={device.connected && !discovery.completed} @@ -182,20 +183,21 @@ const AccountReceive = (props: Props) => { </ShowAddressButton> )} </Row> - {(addressVerified || addressUnverified) && !isAddressVerifying && ( - <QrWrapper> - <Label> - <FormattedMessage {...l10nReceiveMessages.TR_QR_CODE} /> - </Label> - <StyledQRCode - bgColor="#FFFFFF" - fgColor="#000000" - level="Q" - style={{ width: 150 }} - value={account.descriptor} - /> - </QrWrapper> - )} + {((addressVerified || addressUnverified) && !isAddressVerifying) || + (account.imported && ( + <QrWrapper> + <Label> + <FormattedMessage {...l10nReceiveMessages.TR_QR_CODE} /> + </Label> + <StyledQRCode + bgColor="#FFFFFF" + fgColor="#000000" + level="Q" + style={{ width: 150 }} + value={account.descriptor} + /> + </QrWrapper> + ))} </AddressWrapper> </React.Fragment> </Content> diff --git a/src/views/Wallet/views/Account/Send/components/PendingTransactions/index.js b/src/views/Wallet/views/Account/Send/components/PendingTransactions/index.js index 080a61c8..6d0ab40b 100644 --- a/src/views/Wallet/views/Account/Send/components/PendingTransactions/index.js +++ b/src/views/Wallet/views/Account/Send/components/PendingTransactions/index.js @@ -1,7 +1,7 @@ /* @flow */ import React from 'react'; import styled from 'styled-components'; -import { colors, H5 } from 'trezor-ui-components'; +import { colors, H5, P } from 'trezor-ui-components'; import Transaction from 'components/Transaction'; import type { Network } from 'reducers/LocalStorageReducer'; @@ -18,6 +18,8 @@ const Wrapper = styled.div` border-top: 1px solid ${colors.DIVIDER}; `; +const NoTransactions = styled(P)``; + const PendingTransactions = (props: Props) => { // const pending = props.pending.filter(tx => !tx.rejected).concat(testData); const pending = props.pending.filter(tx => !tx.rejected); @@ -25,6 +27,9 @@ const PendingTransactions = (props: Props) => { return ( <Wrapper> <H5>Pending transactions</H5> + {pending.length === 0 && ( + <NoTransactions>There are no pending transactions</NoTransactions> + )} {pending.map(tx => ( <Transaction key={tx.hash} network={props.network} tx={tx} /> ))} diff --git a/src/views/Wallet/views/Account/Send/ethereum/index.js b/src/views/Wallet/views/Account/Send/ethereum/index.js index 73fd4000..fe6ebed5 100644 --- a/src/views/Wallet/views/Account/Send/ethereum/index.js +++ b/src/views/Wallet/views/Account/Send/ethereum/index.js @@ -312,7 +312,9 @@ const AccountSend = (props: Props) => { total === '0' || amount.length === 0 || address.length === 0 || - sending; + sending || + account.imported; + let amountText = ''; if (networkSymbol !== currency && amount.length > 0 && !errors.amount) { amountText = `${amount} ${currency.toUpperCase()}`; @@ -516,13 +518,14 @@ const AccountSend = (props: Props) => { </AdvancedForm> )} - {props.selectedAccount.pending.length > 0 && ( - <PendingTransactions - pending={props.selectedAccount.pending} - tokens={props.selectedAccount.tokens} - network={network} - /> - )} + {props.selectedAccount.pending.length > 0 || + (account.imported && ( + <PendingTransactions + pending={props.selectedAccount.pending} + tokens={props.selectedAccount.tokens} + network={network} + /> + ))} </Content> ); }; diff --git a/src/views/Wallet/views/Account/Send/ripple/index.js b/src/views/Wallet/views/Account/Send/ripple/index.js index d3e144d0..c2c2b949 100644 --- a/src/views/Wallet/views/Account/Send/ripple/index.js +++ b/src/views/Wallet/views/Account/Send/ripple/index.js @@ -279,7 +279,9 @@ const AccountSend = (props: Props) => { total === '0' || amount.length === 0 || address.length === 0 || - sending; + sending || + account.imported; + let sendButtonText = <FormattedMessage {...l10nSendMessages.TR_SEND} values={{ amount: '' }} />; if (total !== '0') { sendButtonText = ( @@ -481,13 +483,14 @@ const AccountSend = (props: Props) => { </AdvancedForm> )} - {props.selectedAccount.pending.length > 0 && ( - <PendingTransactions - pending={props.selectedAccount.pending} - tokens={props.selectedAccount.tokens} - network={network} - /> - )} + {props.selectedAccount.pending.length > 0 || + (account.imported && ( + <PendingTransactions + pending={props.selectedAccount.pending} + tokens={props.selectedAccount.tokens} + network={network} + /> + ))} </Content> ); }; diff --git a/src/views/Wallet/views/Account/Summary/ethereum/index.js b/src/views/Wallet/views/Account/Summary/ethereum/index.js index 2788da89..6d1b0d4a 100644 --- a/src/views/Wallet/views/Account/Summary/ethereum/index.js +++ b/src/views/Wallet/views/Account/Summary/ethereum/index.js @@ -94,7 +94,9 @@ const AccountSummary = (props: Props) => { <StyledCoinLogo height={23} network={account.network} /> <AccountTitle> <FormattedMessage - {...l10nCommonMessages.TR_ACCOUNT_HASH} + {...(account.imported + ? l10nCommonMessages.TR_IMPORTED_ACCOUNT_HASH + : l10nCommonMessages.TR_ACCOUNT_HASH)} values={{ number: parseInt(account.index, 10) + 1 }} /> </AccountTitle> diff --git a/src/views/Wallet/views/Account/Summary/ripple/index.js b/src/views/Wallet/views/Account/Summary/ripple/index.js index 3d42bea0..0c9dde3b 100644 --- a/src/views/Wallet/views/Account/Summary/ripple/index.js +++ b/src/views/Wallet/views/Account/Summary/ripple/index.js @@ -44,6 +44,10 @@ const StyledCoinLogo = styled(CoinLogo)` margin-right: 10px; `; +const StyledLink = styled(Link)` + font-size: ${FONT_SIZE.SMALL}; +`; + const AccountSummary = (props: Props) => { const device = props.wallet.selectedDevice; const { account, network, pending, shouldRender } = props.selectedAccount; @@ -69,17 +73,19 @@ const AccountSummary = (props: Props) => { <StyledCoinLogo height={23} network={account.network} /> <AccountTitle> <FormattedMessage - {...l10nCommonMessages.TR_ACCOUNT_HASH} + {...(account.imported + ? l10nCommonMessages.TR_IMPORTED_ACCOUNT_HASH + : l10nCommonMessages.TR_ACCOUNT_HASH)} values={{ number: parseInt(account.index, 10) + 1 }} /> </AccountTitle> </AccountName> {!account.empty && ( - <Link href={explorerLink} isGray> + <StyledLink href={explorerLink} isGray> <FormattedMessage {...l10nSummaryMessages.TR_SEE_FULL_TRANSACTION_HISTORY} /> - </Link> + </StyledLink> )} </AccountHeading> <AccountBalance diff --git a/src/views/Landing/views/Import/Container.js b/src/views/Wallet/views/Import/Container.js similarity index 55% rename from src/views/Landing/views/Import/Container.js rename to src/views/Wallet/views/Import/Container.js index 26ced7de..e0ea276f 100644 --- a/src/views/Landing/views/Import/Container.js +++ b/src/views/Wallet/views/Import/Container.js @@ -2,33 +2,33 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import * as RouterActions from 'actions/RouterActions'; - -import type { State, Dispatch } from 'flowtype'; +import * as ImportAccountActions from 'actions/ImportAccountActions'; +import type { State, Dispatch, TrezorDevice, Config } from 'flowtype'; import ImportView from './index'; type OwnProps = {| children?: React.Node, |}; export type StateProps = {| - transport: $ElementType<$ElementType<State, 'connect'>, 'transport'>, + device: ?TrezorDevice, + config: Config, + importAccount: $ElementType<State, 'importAccount'>, |}; type DispatchProps = {| - selectFirstAvailableDevice: typeof RouterActions.selectFirstAvailableDevice, + importAddress: typeof ImportAccountActions.importAddress, |}; export type Props = {| ...OwnProps, ...StateProps, ...DispatchProps |}; const mapStateToProps = (state: State): StateProps => ({ - transport: state.connect.transport, + config: state.localStorage.config, + device: state.wallet.selectedDevice, + importAccount: state.importAccount, }); const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - selectFirstAvailableDevice: bindActionCreators( - RouterActions.selectFirstAvailableDevice, - dispatch - ), + importAddress: bindActionCreators(ImportAccountActions.importAddress, dispatch), }); export default connect<Props, OwnProps, StateProps, DispatchProps, State, Dispatch>( diff --git a/src/views/Wallet/views/Import/index.js b/src/views/Wallet/views/Import/index.js new file mode 100644 index 00000000..f58a8518 --- /dev/null +++ b/src/views/Wallet/views/Import/index.js @@ -0,0 +1,105 @@ +/* @flow */ + +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { FormattedMessage } from 'react-intl'; +import { Select, Button, Input, Link, colors } from 'trezor-ui-components'; +import l10nCommonMessages from 'views/common.messages'; +import type { Props } from './Container'; + +const Wrapper = styled.div` + text-align: left; + flex-direction: column; + display: flex; + padding: 24px; + min-width: 300px; +`; + +const StyledSelect = styled(Select)` + min-width: 100px; +`; + +const InputRow = styled.div` + margin-bottom: 16px; +`; + +const Label = styled.div` + color: ${colors.TEXT_SECONDARY}; + padding-bottom: 10px; +`; + +const ButtonActions = styled.div` + display: flex; + flex-direction: row; + + justify-content: flex-end; +`; + +const ButtonWrapper = styled.div` + & + & { + margin-left: 10px; + } +`; + +const Import = (props: Props) => { + const [selectedNetwork, setSelectedNetwork] = useState(null); + const [address, setAddress] = useState(''); + + const { networks } = props.config; + return ( + // <LandingWrapper> + <Wrapper> + <InputRow> + <Label>Select network</Label> + <StyledSelect + value={selectedNetwork} + options={networks + .sort((a, b) => a.shortcut.localeCompare(b.shortcut)) + .map(net => ({ + label: net.shortcut, + value: net, + }))} + onChange={option => setSelectedNetwork(option)} + /> + </InputRow> + + <InputRow> + <Input + topLabel="Address" + name="cryptoAddress" + value={address} + onChange={e => setAddress(e.target.value)} + type="text" + /> + </InputRow> + <ButtonActions> + <ButtonWrapper> + <Link to="/"> + <Button isWhite> + <FormattedMessage {...l10nCommonMessages.TR_CLOSE} /> + </Button> + </Link> + </ButtonWrapper> + + <ButtonWrapper> + <Button + isDisabled={ + !selectedNetwork || address === '' || props.importAccount.loading + } + onClick={() => + props.importAddress( + address, + (selectedNetwork || {}).value, + props.device + ) + } + > + Import + </Button> + </ButtonWrapper> + </ButtonActions> + </Wrapper> + // </LandingWrapper> + ); +}; +export default Import; diff --git a/src/views/common.messages.js b/src/views/common.messages.js index 0d0c9f5b..43047d19 100644 --- a/src/views/common.messages.js +++ b/src/views/common.messages.js @@ -16,6 +16,11 @@ const definedMessages: Messages = defineMessages({ defaultMessage: 'Account #{number}', description: 'Used in auto-generated account label', }, + TR_IMPORTED_ACCOUNT_HASH: { + id: 'TR_IMPORTED_ACCOUNT_HASH', + defaultMessage: 'Imported account #{number}', + description: 'Used in auto-generated label for imported accounts', + }, TR_CLEAR: { id: 'TR_CLEAR', defaultMessage: 'Clear', diff --git a/src/views/index.js b/src/views/index.js index be84722a..ecb5b90b 100644 --- a/src/views/index.js +++ b/src/views/index.js @@ -14,7 +14,6 @@ import { getPattern } from 'support/routes'; // landing views import RootView from 'views/Landing/views/Root/Container'; import InstallBridge from 'views/Landing/views/InstallBridge/Container'; -import ImportView from 'views/Landing/views/Import/Container'; // wallet views import WalletContainer from 'views/Wallet'; @@ -23,6 +22,7 @@ import AccountSend from 'views/Wallet/views/Account/Send'; import AccountReceive from 'views/Wallet/views/Account/Receive'; import AccountSignVerify from 'views/Wallet/views/Account/SignVerify/Container'; +import WalletImport from 'views/Wallet/views/Import/Container'; import WalletDashboard from 'views/Wallet/views/Dashboard/Container'; import WalletDeviceSettings from 'views/Wallet/views/DeviceSettings'; import WalletSettings from 'views/Wallet/views/WalletSettings/Container'; @@ -44,11 +44,16 @@ const App = () => ( <Route exact path={getPattern('landing-home')} component={RootView} /> <Route exact path={getPattern('landing-version')} component={Version} /> <Route exact path={getPattern('landing-bridge')} component={InstallBridge} /> - <Route exact path={getPattern('landing-import')} component={ImportView} /> <Route> <ErrorBoundary> <ImagesPreloader /> <WalletContainer> + <Route + exact + path={getPattern('wallet-import')} + component={WalletImport} + /> + <Route exact path={getPattern('wallet-settings')}