diff --git a/src/actions/AccountsActions.js b/src/actions/AccountsActions.js index ee59d816..267cdb41 100644 --- a/src/actions/AccountsActions.js +++ b/src/actions/AccountsActions.js @@ -1,8 +1,13 @@ /* @flow */ import * as ACCOUNT from 'actions/constants/account'; -import type { Action } from 'flowtype'; +import * as NOTIFICATION from 'actions/constants/notification'; +import type { Action, TrezorDevice, Network } from 'flowtype'; import type { Account, State } from 'reducers/AccountsReducer'; +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 AccountAction = | { @@ -18,3 +23,103 @@ export const update = (account: Account): Action => ({ type: ACCOUNT.UPDATE, payload: account, }); + +export const importAddress = ( + address: string, + network: Network, + device: TrezorDevice +): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { + if (!device) return; + + let payload; + const index = getState().accounts.filter( + a => a.imported === true && a.network === network.shortcut && 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: network.type, + nonce: account.nonce, + }; + } else if (network.type === 'ripple') { + const response = await TrezorConnect.rippleGetAccountInfo({ + account: { + descriptor: address, + }, + coin: network.shortcut, + }); + console.log(response); + + // 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: network.type, + sequence: account.sequence, + reserve: toDecimalAmount(account.reserve, network.decimals), + }; + } + dispatch({ + type: ACCOUNT.CREATE, + payload, + }); + dispatch(LocalStorageActions.setImportedAccount(payload)); + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'success', + title: 'The account has been successfully imported', + cancelable: true, + }, + }); + } catch (error) { + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Import account error', + message: error.message, + cancelable: true, + }, + }); + } +}; 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/LocalStorageActions.js b/src/actions/LocalStorageActions.js index baf0a171..d4e92466 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,20 @@ 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: ?string = JSON.parse(storageUtils.get(TYPE, KEY_IMPORTED_ACCOUNTS)); + 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 importedAccounts; +}; diff --git a/src/components/notifications/Context/components/Static/index.js b/src/components/notifications/Context/components/Static/index.js index 55c71577..ee2a2806 100644 --- a/src/components/notifications/Context/components/Static/index.js +++ b/src/components/notifications/Context/components/Static/index.js @@ -4,20 +4,24 @@ 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 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) => { const { selectedAccount } = props; const { account } = selectedAccount; const { location } = props.router; const notifications: Array = []; - 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 +55,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/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/utils/index.js b/src/reducers/utils/index.js index 473852d0..6528a7b1 100644 --- a/src/reducers/utils/index.js +++ b/src/reducers/utils/index.js @@ -83,10 +83,24 @@ 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); + + if (isImported) { + return state.accounts.find( + a => + a.imported === true && + a.deviceState === device.state && + a.index === index && + a.network === locationState.network + ); + } return state.accounts.find( a => + a.imported === false && a.deviceState === device.state && a.index === index && a.network === locationState.network 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/LeftNavigation/components/AccountMenu/index.js b/src/views/Wallet/components/LeftNavigation/components/AccountMenu/index.js index b680de05..c54342a3 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,21 @@ const DiscoveryLoadingText = styled.span` margin-left: 14px; `; +const Col = styled.div` + display: flex; + flex: 1; + flex-direction: column; +`; + +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; @@ -105,7 +118,12 @@ const AccountMenu = (props: Props) => { 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,37 +143,38 @@ const AccountMenu = (props: Props) => { } } - const urlAccountIndex = parseInt(props.router.location.state.account, 10); return ( - + - - - {balance && !props.wallet.hideBalance && ( - - {balance} - {fiatRates && ( - - )} - - )} - {!balance && ( - - - - )} + + + + {balance && !props.wallet.hideBalance && ( + + {balance} + {fiatRates && ( + + )} + + )} + {!balance && ( + + + + )} + + {account.imported && watch-only} diff --git a/src/views/Landing/views/Import/Container.js b/src/views/Wallet/views/Import/Container.js similarity index 69% rename from src/views/Landing/views/Import/Container.js rename to src/views/Wallet/views/Import/Container.js index 5991a397..347d9a82 100644 --- a/src/views/Landing/views/Import/Container.js +++ b/src/views/Wallet/views/Import/Container.js @@ -2,19 +2,20 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import * as RouterActions from 'actions/RouterActions'; +import * as AccountsAction from 'actions/AccountsActions'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; -import type { State, Dispatch } from 'flowtype'; +import type { Device, State, Dispatch } from 'flowtype'; import ImportView from './index'; export type StateProps = { transport: $ElementType<$ElementType, 'transport'>, + device: ?Device, children?: React.Node, }; type DispatchProps = { - selectFirstAvailableDevice: typeof RouterActions.selectFirstAvailableDevice, + importAddress: typeof AccountsAction.importAddress, }; type OwnProps = {}; @@ -24,16 +25,14 @@ export type Props = StateProps & DispatchProps; const mapStateToProps: MapStateToProps = ( state: State ): StateProps => ({ - transport: state.connect.transport, + config: state.localStorage.config, + device: state.wallet.selectedDevice, }); const mapDispatchToProps: MapDispatchToProps = ( dispatch: Dispatch ): DispatchProps => ({ - selectFirstAvailableDevice: bindActionCreators( - RouterActions.selectFirstAvailableDevice, - dispatch - ), + importAddress: bindActionCreators(AccountsAction.importAddress, dispatch), }); export default connect( diff --git a/src/views/Wallet/views/Import/index.js b/src/views/Wallet/views/Import/index.js new file mode 100644 index 00000000..6f821153 --- /dev/null +++ b/src/views/Wallet/views/Import/index.js @@ -0,0 +1,99 @@ +/* @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 ( + // + + + + a.shortcut > b.shortcut) + .map(net => ({ + label: net.shortcut, + value: net, + }))} + onChange={option => setSelectedNetwork(option)} + /> + + + + setAddress(e.target.value)} + type="text" + /> + + + + + + + + + + + + + + // + ); +}; +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 = () => ( - + +