Loëck Vézien
7 years ago
31 changed files with 1093 additions and 591 deletions
@ -0,0 +1,271 @@ |
|||
// @flow
|
|||
|
|||
import React, { PureComponent } from 'react' |
|||
import styled from 'styled-components' |
|||
import { Trans, translate } from 'react-i18next' |
|||
import { getCurrencyByCoinType } from '@ledgerhq/currencies' |
|||
import { getIconByCoinType } from '@ledgerhq/currencies/react' |
|||
|
|||
import type { T, Device, Devices } from 'types/common' |
|||
|
|||
import noop from 'lodash/noop' |
|||
|
|||
import Box from 'components/base/Box' |
|||
|
|||
import IconCheck from 'icons/Check' |
|||
import IconExclamationCircle from 'icons/ExclamationCircle' |
|||
import IconInfoCircle from 'icons/InfoCircle' |
|||
import IconLoader from 'icons/Loader' |
|||
import IconUsb from 'icons/Usb' |
|||
|
|||
import * as IconDevice from 'icons/device' |
|||
|
|||
const Step = styled(Box).attrs({ |
|||
borderRadius: 1, |
|||
justifyContent: 'center', |
|||
fontSize: 4, |
|||
})` |
|||
border: 1px solid |
|||
${p => |
|||
p.validated |
|||
? p.theme.colors.wallet |
|||
: p.hasErrors ? p.theme.colors.alertRed : p.theme.colors.fog}; |
|||
` |
|||
const StepIcon = styled(Box).attrs({ |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
})` |
|||
width: 64px; |
|||
` |
|||
const StepContent = styled(Box).attrs({ |
|||
color: 'dark', |
|||
horizontal: true, |
|||
alignItems: 'center', |
|||
})` |
|||
height: 60px; |
|||
line-height: 1.2; |
|||
|
|||
strong { |
|||
font-weight: 600; |
|||
} |
|||
` |
|||
|
|||
const ListDevices = styled(Box).attrs({ |
|||
p: 3, |
|||
pt: 1, |
|||
flow: 2, |
|||
})`` |
|||
|
|||
const DeviceItem = styled(Box).attrs({ |
|||
bg: 'lightGrey', |
|||
borderRadius: 1, |
|||
alignItems: 'center', |
|||
color: 'dark', |
|||
ff: 'Open Sans|SemiBold', |
|||
fontSize: 4, |
|||
horizontal: true, |
|||
pr: 3, |
|||
pl: 0, |
|||
})` |
|||
cursor: pointer; |
|||
height: 54px; |
|||
` |
|||
const DeviceIcon = styled(Box).attrs({ |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
color: 'graphite', |
|||
})` |
|||
width: 55px; |
|||
` |
|||
const DeviceSelected = styled(Box).attrs({ |
|||
alignItems: 'center', |
|||
bg: p => (p.selected ? 'wallet' : 'white'), |
|||
color: 'white', |
|||
justifyContent: 'center', |
|||
})` |
|||
border-radius: 50%; |
|||
border: 1px solid ${p => (p.selected ? p.theme.colors.wallet : p.theme.colors.fog)}; |
|||
height: 18px; |
|||
width: 18px; |
|||
` |
|||
|
|||
const WrapperIconCurrency = styled(Box).attrs({ |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
})` |
|||
border: 1px solid ${p => p.theme.colors[p.color]}; |
|||
border-radius: 8px; |
|||
height: 24px; |
|||
width: 24px; |
|||
` |
|||
|
|||
const Info = styled(Box).attrs({ |
|||
alignItems: 'center', |
|||
color: p => (p.hasErrors ? 'alertRed' : 'grey'), |
|||
flow: 2, |
|||
fontSize: 3, |
|||
horizontal: true, |
|||
ml: 1, |
|||
pt: 1, |
|||
})` |
|||
strong { |
|||
font-weight: 600; |
|||
} |
|||
` |
|||
|
|||
const StepCheck = ({ checked, hasErrors }: { checked: boolean, hasErrors?: boolean }) => ( |
|||
<Box pr={5}> |
|||
{checked ? ( |
|||
<Box color="wallet"> |
|||
<IconCheck size={16} /> |
|||
</Box> |
|||
) : hasErrors ? ( |
|||
<Box color="alertRed"> |
|||
<IconExclamationCircle size={16} /> |
|||
</Box> |
|||
) : ( |
|||
<IconLoader size={16} /> |
|||
)} |
|||
</Box> |
|||
) |
|||
|
|||
StepCheck.defaultProps = { |
|||
hasErrors: false, |
|||
} |
|||
|
|||
type Props = { |
|||
accountName: null | string, |
|||
appOpened: null | 'success' | 'fail', |
|||
coinType: number, |
|||
devices: Devices, |
|||
deviceSelected: Device | null, |
|||
onChangeDevice: Function, |
|||
t: T, |
|||
} |
|||
|
|||
const emitChangeDevice = props => { |
|||
const { onChangeDevice, deviceSelected, devices } = props |
|||
|
|||
if (deviceSelected === null && devices.length > 0) { |
|||
onChangeDevice(devices[0]) |
|||
} |
|||
} |
|||
|
|||
class DeviceConnect extends PureComponent<Props> { |
|||
static defaultProps = { |
|||
accountName: null, |
|||
appOpened: null, |
|||
devices: [], |
|||
deviceSelected: null, |
|||
onChangeDevice: noop, |
|||
} |
|||
|
|||
componentDidMount() { |
|||
emitChangeDevice(this.props) |
|||
} |
|||
|
|||
componentWillReceiveProps(nextProps) { |
|||
emitChangeDevice(nextProps) |
|||
} |
|||
|
|||
getAppState() { |
|||
const { appOpened } = this.props |
|||
|
|||
return { |
|||
success: appOpened === 'success', |
|||
fail: appOpened === 'fail', |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
const { deviceSelected, accountName, coinType, t, onChangeDevice, devices } = this.props |
|||
|
|||
const appState = this.getAppState() |
|||
|
|||
const hasDevice = devices.length > 0 |
|||
const hasMultipleDevices = devices.length > 1 |
|||
|
|||
const { name: appName } = getCurrencyByCoinType(coinType) |
|||
const IconCurrency = getIconByCoinType(coinType) |
|||
|
|||
return ( |
|||
<Box flow={4}> |
|||
<Step validated={hasDevice}> |
|||
<StepContent> |
|||
<StepIcon> |
|||
<IconUsb size={36} /> |
|||
</StepIcon> |
|||
<Box grow shrink> |
|||
<Trans i18nKey="deviceConnect:step1.connect" parent="div"> |
|||
Connect your <strong>Ledger device</strong> to your computer and enter your{' '} |
|||
<strong>PIN code</strong> on your device |
|||
</Trans> |
|||
</Box> |
|||
<StepCheck checked={hasDevice} /> |
|||
</StepContent> |
|||
{hasMultipleDevices && ( |
|||
<ListDevices> |
|||
<Box color="graphite" fontSize={3}> |
|||
{t('deviceConnect:step1.choose', { devicesCount: devices.length })} |
|||
</Box> |
|||
<Box flow={2}> |
|||
{devices.map((d, i) => { |
|||
const Icon = IconDevice[d.product.replace(/\s/g, '')] |
|||
return ( |
|||
<DeviceItem |
|||
key={i} // eslint-disable-line react/no-array-index-key
|
|||
onClick={() => onChangeDevice(d)} |
|||
> |
|||
<DeviceIcon> |
|||
<Icon size={28} /> |
|||
</DeviceIcon> |
|||
<Box grow noShrink> |
|||
{`${d.manufacturer} ${d.product}`} |
|||
</Box> |
|||
<Box> |
|||
<DeviceSelected selected={d === deviceSelected}> |
|||
<IconCheck size={10} /> |
|||
</DeviceSelected> |
|||
</Box> |
|||
</DeviceItem> |
|||
) |
|||
})} |
|||
</Box> |
|||
</ListDevices> |
|||
)} |
|||
</Step> |
|||
<Step validated={appState.success} hasErrors={appState.fail}> |
|||
<StepContent> |
|||
<StepIcon> |
|||
<WrapperIconCurrency> |
|||
<IconCurrency size={12} /> |
|||
</WrapperIconCurrency> |
|||
</StepIcon> |
|||
<Box grow shrink> |
|||
<Trans i18nKey="deviceConnect:step2.open" parent="div"> |
|||
{/* $FlowFixMe */} |
|||
Open <strong>{{ appName }} App</strong> on your device |
|||
</Trans> |
|||
</Box> |
|||
<StepCheck checked={appState.success} hasErrors={appState.fail} /> |
|||
</StepContent> |
|||
</Step> |
|||
{accountName !== null && ( |
|||
<Info hasErrors={appState.fail}> |
|||
<Box> |
|||
<IconInfoCircle size={12} /> |
|||
</Box> |
|||
<Box> |
|||
<Trans i18nKey="deviceConnect:info" parent="div"> |
|||
{/* $FlowFixMe */} |
|||
You must use the device associated to the account <strong>{{ accountName }}</strong> |
|||
</Trans> |
|||
</Box> |
|||
</Info> |
|||
)} |
|||
</Box> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default translate()(DeviceConnect) |
@ -0,0 +1,45 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
import { storiesOf } from '@storybook/react' |
|||
import { select, text } from '@storybook/addon-knobs' |
|||
import { action } from '@storybook/addon-actions' |
|||
|
|||
import DeviceConnect from 'components/DeviceConnect' |
|||
|
|||
const stories = storiesOf('Components', module) |
|||
|
|||
const devices = [ |
|||
{ |
|||
manufacturer: 'Ledger', |
|||
product: 'Nano S', |
|||
vendorId: '1', |
|||
productId: '11', |
|||
path: '111', |
|||
}, |
|||
{ |
|||
manufacturer: 'Ledger', |
|||
product: 'Blue', |
|||
vendorId: '2', |
|||
productId: '22', |
|||
path: '222', |
|||
}, |
|||
{ |
|||
manufacturer: 'Ledger', |
|||
product: 'Nano S', |
|||
vendorId: '3', |
|||
productId: '33', |
|||
path: '333', |
|||
}, |
|||
] |
|||
|
|||
stories.add('DeviceConnect', () => ( |
|||
<DeviceConnect |
|||
accountName={text('accountName', 'Test Account')} |
|||
coinType={select('coinType', [0, 1, 145, 156, 2, 3, 5], 0)} |
|||
appOpened={select('appOpened', ['', 'success', 'fail'], '')} |
|||
devices={devices.slice(0, select('devices', [0, 1, 2, 3], 0))} |
|||
deviceSelected={devices[select('deviceSelected', ['', 0, 1, 2], '')] || null} |
|||
onChangeDevice={action('onChangeDevice')} |
|||
/> |
|||
)) |
@ -0,0 +1,152 @@ |
|||
// @flow
|
|||
|
|||
import { PureComponent } from 'react' |
|||
import { connect } from 'react-redux' |
|||
import { ipcRenderer } from 'electron' |
|||
|
|||
import type { Account } from '@ledgerhq/wallet-common/lib/types' |
|||
import type { Device, Devices } from 'types/common' |
|||
|
|||
import { sendEvent } from 'renderer/events' |
|||
import { getDevices } from 'reducers/devices' |
|||
|
|||
const mapStateToProps = state => ({ |
|||
devices: getDevices(state), |
|||
}) |
|||
|
|||
type DeviceStatus = |
|||
| 'unconnected' |
|||
| 'connected' |
|||
| 'appOpened.success' |
|||
| 'appOpened.fail' |
|||
| 'appOpened.progress' |
|||
|
|||
type Props = { |
|||
coinType: number, |
|||
devices: Devices, |
|||
deviceSelected: Device | null, |
|||
account?: Account, |
|||
onStatusChange?: DeviceStatus => void, |
|||
render?: Function, |
|||
} |
|||
|
|||
type State = { |
|||
status: DeviceStatus, |
|||
} |
|||
|
|||
class DeviceMonit extends PureComponent<Props, State> { |
|||
state = { |
|||
status: this.props.deviceSelected ? 'connected' : 'unconnected', |
|||
} |
|||
|
|||
componentDidMount() { |
|||
ipcRenderer.on('msg', this.handleMsgEvent) |
|||
if (this.props.deviceSelected !== null) { |
|||
this.checkAppOpened() |
|||
} |
|||
} |
|||
|
|||
componentWillReceiveProps(nextProps) { |
|||
const { status } = this.state |
|||
const { deviceSelected, devices } = this.props |
|||
const { devices: nextDevices, deviceSelected: nextDeviceSelected } = nextProps |
|||
|
|||
if (status === 'unconnected' && !deviceSelected && nextDeviceSelected) { |
|||
this.handleStatusChange('connected') |
|||
} |
|||
|
|||
if (status !== 'unconnected' && devices !== nextDevices) { |
|||
const isConnected = nextDevices.find(d => d === nextDeviceSelected) |
|||
if (!isConnected) { |
|||
this.handleStatusChange('unconnected') |
|||
clearTimeout(this._timeout) |
|||
} |
|||
} |
|||
} |
|||
|
|||
componentDidUpdate(prevProps) { |
|||
const { deviceSelected } = this.props |
|||
const { deviceSelected: prevDeviceSelected } = prevProps |
|||
|
|||
if (prevDeviceSelected !== deviceSelected) { |
|||
this.handleStatusChange('appOpened.progress') |
|||
this._timeout = setTimeout(this.checkAppOpened, 250) |
|||
} |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
ipcRenderer.removeListener('msg', this.handleMsgEvent) |
|||
clearTimeout(this._timeout) |
|||
} |
|||
|
|||
checkAppOpened = () => { |
|||
const { deviceSelected, account, coinType } = this.props |
|||
|
|||
if (deviceSelected === null) { |
|||
return |
|||
} |
|||
|
|||
let options = null |
|||
|
|||
if (account && account.currency) { |
|||
options = { |
|||
accountPath: account.path, |
|||
accountAddress: account.address, |
|||
} |
|||
} |
|||
|
|||
if (coinType) { |
|||
options = { |
|||
coinType, |
|||
} |
|||
} |
|||
|
|||
sendEvent('usb', 'wallet.checkIfAppOpened', { |
|||
devicePath: deviceSelected.path, |
|||
...options, |
|||
}) |
|||
} |
|||
|
|||
_timeout: any = null |
|||
|
|||
handleStatusChange = status => { |
|||
const { onStatusChange } = this.props |
|||
this.setState({ status }) |
|||
onStatusChange && onStatusChange(status) |
|||
} |
|||
|
|||
handleMsgEvent = (e, { type, data }) => { |
|||
const { deviceSelected } = this.props |
|||
|
|||
if (deviceSelected === null) { |
|||
return |
|||
} |
|||
|
|||
if (type === 'wallet.checkIfAppOpened.success' && deviceSelected.path === data.devicePath) { |
|||
clearTimeout(this._timeout) |
|||
this.handleStatusChange('appOpened.success') |
|||
} |
|||
|
|||
if (type === 'wallet.checkIfAppOpened.fail' && deviceSelected.path === data.devicePath) { |
|||
this.handleStatusChange('appOpened.fail') |
|||
this._timeout = setTimeout(this.checkAppOpened, 1e3) |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
const { status } = this.state |
|||
const { devices, deviceSelected, render } = this.props |
|||
|
|||
if (render) { |
|||
return render({ |
|||
status, |
|||
devices, |
|||
deviceSelected: status === 'connected' ? deviceSelected : null, |
|||
}) |
|||
} |
|||
|
|||
return null |
|||
} |
|||
} |
|||
|
|||
export default connect(mapStateToProps)(DeviceMonit) |
@ -0,0 +1,41 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
|
|||
import { listCurrencies } from '@ledgerhq/currencies' |
|||
|
|||
import type { Currency } from '@ledgerhq/currencies/lib/types' |
|||
import type { T } from 'types/common' |
|||
|
|||
import get from 'lodash/get' |
|||
|
|||
import Box from 'components/base/Box' |
|||
import Label from 'components/base/Label' |
|||
import Select from 'components/base/Select' |
|||
|
|||
const currencies = listCurrencies().map(currency => ({ |
|||
key: currency.coinType, |
|||
name: currency.name, |
|||
data: currency, |
|||
})) |
|||
|
|||
type Props = { |
|||
onChangeCurrency: Function, |
|||
currency: Currency | null, |
|||
t: T, |
|||
} |
|||
|
|||
export default (props: Props) => ( |
|||
<Box flow={1}> |
|||
<Label>{props.t('common:currency')}</Label> |
|||
<Select |
|||
placeholder={props.t('common:chooseWalletPlaceholder')} |
|||
onChange={item => props.onChangeCurrency(item.data)} |
|||
renderSelected={item => item.name} |
|||
items={currencies} |
|||
value={ |
|||
props.currency ? currencies.find(c => c.key === get(props, 'currency.coinType')) : null |
|||
} |
|||
/> |
|||
</Box> |
|||
) |
@ -0,0 +1,36 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
|
|||
import type { Currency } from '@ledgerhq/currencies/lib/types' |
|||
|
|||
import type { Device } from 'types/common' |
|||
|
|||
import DeviceConnect from 'components/DeviceConnect' |
|||
import DeviceMonit from 'components/DeviceMonitNew' |
|||
|
|||
type Props = { |
|||
currency: Currency | null, |
|||
deviceSelected: Device | null, |
|||
onChangeDevice: Function, |
|||
onStatusChange: Function, |
|||
} |
|||
|
|||
export default (props: Props) => ( |
|||
<DeviceMonit |
|||
coinType={props.currency && props.currency.coinType} |
|||
deviceSelected={props.deviceSelected} |
|||
onStatusChange={props.onStatusChange} |
|||
render={({ status, devices, deviceSelected }) => ( |
|||
<DeviceConnect |
|||
coinType={props.currency && props.currency.coinType} |
|||
appOpened={ |
|||
status === 'appOpened.success' ? 'success' : status === 'appOpened.fail' ? 'fail' : null |
|||
} |
|||
devices={devices} |
|||
deviceSelected={deviceSelected} |
|||
onChangeDevice={props.onChangeDevice} |
|||
/> |
|||
)} |
|||
/> |
|||
) |
@ -0,0 +1,100 @@ |
|||
// @flow
|
|||
|
|||
import React, { Fragment } from 'react' |
|||
import styled from 'styled-components' |
|||
import { getDefaultUnitByCoinType } from '@ledgerhq/currencies' |
|||
|
|||
import type { Currency } from '@ledgerhq/currencies/lib/types' |
|||
import type { Account } from '@ledgerhq/wallet-common/lib/types' |
|||
|
|||
import Box from 'components/base/Box' |
|||
|
|||
import AccountCard from 'components/DashboardPage/AccountCard' |
|||
|
|||
const AccountsContainer = styled(Box).attrs({ |
|||
horizontal: true, |
|||
m: -2, |
|||
})` |
|||
flex-wrap: wrap; |
|||
` |
|||
const AccountItemWrapper = styled(Box).attrs({ |
|||
p: 2, |
|||
})` |
|||
width: 50%; |
|||
` |
|||
const AccountItem = styled(AccountCard)` |
|||
${p => p.selected && `box-shadow: inset 0 0 0 1px ${p.theme.colors.wallet};`}; |
|||
` |
|||
|
|||
type Props = { |
|||
accountsImport: Object, |
|||
archivedAccounts: Account[], |
|||
currency: Currency | null, |
|||
importProgress: boolean, |
|||
onSelectAccount?: Function, |
|||
selectedAccounts?: Array<number>, |
|||
} |
|||
|
|||
function StepImport(props: Props) { |
|||
const hasAccountsImports = Object.keys(props.accountsImport).length > 0 |
|||
const unit = props.currency !== null && getDefaultUnitByCoinType(props.currency.coinType) |
|||
return ( |
|||
<Box> |
|||
{props.importProgress ? ( |
|||
<Box alignItems="center">In progress...</Box> |
|||
) : ( |
|||
hasAccountsImports && <Box mb={-2}>Accounts</Box> |
|||
)} |
|||
{hasAccountsImports && ( |
|||
<AccountsContainer pt={5}> |
|||
{Object.keys(props.accountsImport).map(k => { |
|||
const a = props.accountsImport[k] |
|||
return ( |
|||
<AccountItemWrapper key={a.id}> |
|||
<AccountItem |
|||
selected={props.selectedAccounts && props.selectedAccounts.includes(a.id)} |
|||
onClick={props.onSelectAccount && props.onSelectAccount(a.id)} |
|||
account={{ |
|||
...a, |
|||
coinType: props.currency && props.currency.coinType, |
|||
name: `Account ${a.accountIndex}`, |
|||
currency: props.currency, |
|||
unit, |
|||
}} |
|||
counterValue="USD" |
|||
daysCount={365} |
|||
/> |
|||
</AccountItemWrapper> |
|||
) |
|||
})} |
|||
</AccountsContainer> |
|||
)} |
|||
{!props.importProgress && |
|||
props.archivedAccounts.length > 0 && ( |
|||
<Fragment> |
|||
<Box pb={3}>Archived accounts</Box> |
|||
<AccountsContainer> |
|||
{props.archivedAccounts.map(a => ( |
|||
<AccountItemWrapper key={a.id}> |
|||
<AccountItem |
|||
selected={props.selectedAccounts && props.selectedAccounts.includes(a.id)} |
|||
onClick={props.onSelectAccount && props.onSelectAccount(a.id)} |
|||
account={a} |
|||
counterValue="USD" |
|||
daysCount={365} |
|||
/> |
|||
</AccountItemWrapper> |
|||
))} |
|||
</AccountsContainer> |
|||
</Fragment> |
|||
)} |
|||
</Box> |
|||
) |
|||
} |
|||
|
|||
StepImport.defaultProps = { |
|||
onSelectAccount: undefined, |
|||
selectedAccounts: [], |
|||
} |
|||
|
|||
export default StepImport |
@ -1,69 +0,0 @@ |
|||
// @flow
|
|||
|
|||
import React, { PureComponent } from 'react' |
|||
|
|||
import Box from 'components/base/Box' |
|||
import Button from 'components/base/Button' |
|||
import Input from 'components/base/Input' |
|||
import Label from 'components/base/Label' |
|||
|
|||
type Props = { |
|||
account: Object, |
|||
onAddAccount: Function, |
|||
} |
|||
|
|||
type State = { |
|||
accountName: string, |
|||
} |
|||
|
|||
class CreateAccount extends PureComponent<Props, State> { |
|||
state = { |
|||
accountName: '', |
|||
} |
|||
|
|||
handleCreateAccount = (e: SyntheticEvent<HTMLFormElement>) => { |
|||
e.preventDefault() |
|||
|
|||
const { accountName } = this.state |
|||
const { onAddAccount, account } = this.props |
|||
|
|||
if (accountName.trim() === '') { |
|||
return |
|||
} |
|||
|
|||
onAddAccount({ |
|||
...account, |
|||
name: accountName, |
|||
}) |
|||
} |
|||
|
|||
handleChangeInput = (value: string) => |
|||
this.setState({ |
|||
accountName: value, |
|||
}) |
|||
|
|||
render() { |
|||
const { accountName } = this.state |
|||
|
|||
return ( |
|||
<Box> |
|||
<Box>Create Account</Box> |
|||
<form onSubmit={this.handleCreateAccount}> |
|||
<Box flow={3}> |
|||
<Box flow={1}> |
|||
<Label>Account name</Label> |
|||
<Input value={accountName} onChange={this.handleChangeInput} /> |
|||
</Box> |
|||
<Box horizontal justifyContent="flex-end"> |
|||
<Button primary type="submit"> |
|||
Create |
|||
</Button> |
|||
</Box> |
|||
</Box> |
|||
</form> |
|||
</Box> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default CreateAccount |
@ -1,131 +0,0 @@ |
|||
// @flow
|
|||
|
|||
import React, { PureComponent } from 'react' |
|||
import { translate } from 'react-i18next' |
|||
import type { Account } from '@ledgerhq/wallet-common/lib/types' |
|||
|
|||
import type { T } from 'types/common' |
|||
|
|||
import Box from 'components/base/Box' |
|||
import Button from 'components/base/Button' |
|||
import CheckBox from 'components/base/CheckBox' |
|||
import FormattedVal from 'components/base/FormattedVal' |
|||
import Input from 'components/base/Input' |
|||
|
|||
type Props = { |
|||
t: T, |
|||
accounts: Account[], |
|||
onImportAccounts: Function, |
|||
} |
|||
|
|||
type State = { |
|||
accountsSelected: Array<string>, |
|||
accountsName: Object, |
|||
} |
|||
|
|||
class ImportAccounts extends PureComponent<Props, State> { |
|||
state = { |
|||
accountsSelected: [], |
|||
accountsName: this.props.accounts.reduce((result, value, index) => { |
|||
result[value.id] = { |
|||
placeholder: this.props.t(`addAccount:import.placeholder`, { |
|||
index: index + 1, |
|||
}), |
|||
} |
|||
|
|||
return result |
|||
}, {}), |
|||
} |
|||
|
|||
handleSelectAccount = (id: string, selected: boolean) => () => |
|||
this.setState(prev => ({ |
|||
accountsSelected: selected |
|||
? prev.accountsSelected.filter(v => v !== id) |
|||
: [...prev.accountsSelected, id], |
|||
})) |
|||
|
|||
handleChangeInput = (id: string) => (value: string) => |
|||
this.setState(prev => ({ |
|||
accountsName: { |
|||
...prev.accountsName, |
|||
[id]: { |
|||
...prev.accountsName[id], |
|||
value, |
|||
}, |
|||
}, |
|||
})) |
|||
|
|||
handleImportAccounts = (e: SyntheticEvent<HTMLFormElement>) => { |
|||
e.preventDefault() |
|||
|
|||
const { accounts, onImportAccounts } = this.props |
|||
const { accountsSelected, accountsName } = this.state |
|||
|
|||
const importAccounts = accountsSelected.map(id => ({ |
|||
...accounts.find(a => a.id === id), |
|||
name: accountsName[id].value || accountsName[id].placeholder, |
|||
})) |
|||
|
|||
onImportAccounts(importAccounts) |
|||
} |
|||
|
|||
render() { |
|||
const { accounts } = this.props |
|||
const { accountsSelected, accountsName } = this.state |
|||
|
|||
const canImportAccounts = accountsSelected.length > 0 |
|||
|
|||
return ( |
|||
<Box> |
|||
<Box>Import Accounts</Box> |
|||
<form onSubmit={this.handleImportAccounts}> |
|||
<Box flow={3}> |
|||
{accounts.map(account => { |
|||
const selected = accountsSelected.includes(account.id) |
|||
const accountName = accountsName[account.id] |
|||
return ( |
|||
<Box key={account.id} horizontal flow={10}> |
|||
<Box> |
|||
<CheckBox |
|||
isChecked={selected} |
|||
onChange={this.handleSelectAccount(account.id, selected)} |
|||
/> |
|||
</Box> |
|||
<Box grow> |
|||
<Box> |
|||
<Input |
|||
type="text" |
|||
disabled={!selected} |
|||
placeholder={accountName.placeholder} |
|||
value={accountName.value || ''} |
|||
onChange={this.handleChangeInput(account.id)} |
|||
/> |
|||
</Box> |
|||
<Box> |
|||
Balance:{' '} |
|||
<FormattedVal |
|||
alwaysShowSign={false} |
|||
color="dark" |
|||
unit={account.unit} |
|||
showCode |
|||
val={account.balance} |
|||
/> |
|||
</Box> |
|||
<Box>Operations: {account.operations.length}</Box> |
|||
</Box> |
|||
</Box> |
|||
) |
|||
})} |
|||
<Box horizontal justifyContent="flex-end"> |
|||
<Button primary disabled={!canImportAccounts} type="submit"> |
|||
Import |
|||
</Button> |
|||
</Box> |
|||
</Box> |
|||
</form> |
|||
</Box> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default translate()(ImportAccounts) |
@ -1,39 +0,0 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
import styled from 'styled-components' |
|||
import type { Account } from '@ledgerhq/wallet-common/lib/types' |
|||
|
|||
import Box from 'components/base/Box' |
|||
import Button from 'components/base/Button' |
|||
import Text from 'components/base/Text' |
|||
|
|||
const Container = styled(Box)` |
|||
border: 1px solid ${p => p.theme.colors.alertRed}; |
|||
` |
|||
|
|||
type Props = { |
|||
archivedAccounts: Account[], |
|||
updateAccount: Function, |
|||
} |
|||
|
|||
function RestoreAccounts(props: Props) { |
|||
const { archivedAccounts, updateAccount } = props |
|||
return ( |
|||
<Container> |
|||
<Text fontSize={3} fontWeight="bold"> |
|||
{'Restore account'} |
|||
</Text> |
|||
{archivedAccounts.map(account => ( |
|||
<Box key={account.id} horizontal flow={2} alignItems="center"> |
|||
<Text>{account.name}</Text> |
|||
<Button primary onClick={() => updateAccount({ ...account, archived: false })}> |
|||
{'restore'} |
|||
</Button> |
|||
</Box> |
|||
))} |
|||
</Container> |
|||
) |
|||
} |
|||
|
|||
export default RestoreAccounts |
@ -0,0 +1,12 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
|
|||
export default ({ size, ...p }: { size: number }) => ( |
|||
<svg viewBox="0 0 16 16" height={size} width={size} {...p}> |
|||
<path |
|||
fill="currentColor" |
|||
d="M8 .25C3.72009375.25.25 3.72134375.25 8c0 4.2811563 3.47009375 7.75 7.75 7.75 4.2799062 0 7.75-3.4688437 7.75-7.75C15.75 3.72134375 12.2799062.25 8 .25zm0 14c-3.454125 0-6.25-2.7947187-6.25-6.25 0-3.45296875 2.796-6.25 6.25-6.25 3.4528437 0 6.25 2.79596875 6.25 6.25 0 3.4540625-2.7947187 6.25-6.25 6.25zM9.3125 11c0 .7237187-.58878125 1.3125-1.3125 1.3125S6.6875 11.7237187 6.6875 11 7.27628125 9.6875 8 9.6875 9.3125 10.2762813 9.3125 11zM6.7696875 4.39371875l.2125 4.25C6.99215625 8.8433125 7.15690625 9 7.35671875 9h1.2865625c.1998125 0 .3645625-.1566875.37453125-.35628125l.2125-4.25C9.24103125 4.17953125 9.07025 4 8.85578125 4h-1.7115625c-.21446875 0-.38525.17953125-.37453125.39371875z" |
|||
/> |
|||
</svg> |
|||
) |
@ -0,0 +1,9 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
|
|||
export default ({ size, ...p }: { size: number }) => ( |
|||
<svg viewBox="0 0 36 36" height={size} width={size} {...p}> |
|||
<path d="M16.735 2.8c0-.69864021.5663598-1.265 1.265-1.265.6986402 0 1.265.56635979 1.265 1.265v6.4c0 .69864021-.5663598 1.265-1.265 1.265-.6986402 0-1.265-.56635979-1.265-1.265V2.8zm11.118533 3.55748685c.4940132-.49401323 1.2949669-.49401323 1.7889802 0 .4940132.49401323.4940132 1.29496693 0 1.78898016l-4.5254834 4.52548339c-.4940133.4940132-1.294967.4940132-1.7889802 0-.4940132-.4940132-.4940132-1.2949669 0-1.7889802l4.5254834-4.52548335zM33.2 16.735c.6986402 0 1.265.5663598 1.265 1.265 0 .6986402-.5663598 1.265-1.265 1.265h-6.4c-.6986402 0-1.265-.5663598-1.265-1.265 0-.6986402.5663598-1.265 1.265-1.265h6.4zm-3.5574868 11.118533c.4940132.4940132.4940132 1.2949669 0 1.7889802-.4940133.4940132-1.294967.4940132-1.7889802 0l-4.5254834-4.5254834c-.4940132-.4940133-.4940132-1.294967 0-1.7889802.4940132-.4940132 1.2949669-.4940132 1.7889802 0l4.5254834 4.5254834zM19.265 33.2c0 .6986402-.5663598 1.265-1.265 1.265-.6986402 0-1.265-.5663598-1.265-1.265v-6.4c0-.6986402.5663598-1.265 1.265-1.265.6986402 0 1.265.5663598 1.265 1.265v6.4zM8.14646701 29.6425132c-.49401323.4940132-1.29496693.4940132-1.78898016 0-.49401323-.4940133-.49401323-1.294967 0-1.7889802l4.52548335-4.5254834c.4940133-.4940132 1.294967-.4940132 1.7889802 0 .4940132.4940132.4940132 1.2949669 0 1.7889802l-4.52548339 4.5254834zM2.8 19.265c-.69864021 0-1.265-.5663598-1.265-1.265 0-.6986402.56635979-1.265 1.265-1.265h6.4c.69864021 0 1.265.5663598 1.265 1.265 0 .6986402-.56635979 1.265-1.265 1.265H2.8zM6.35748685 8.14646701c-.49401323-.49401323-.49401323-1.29496693 0-1.78898016.49401323-.49401323 1.29496693-.49401323 1.78898016 0l4.52548339 4.52548335c.4940132.4940133.4940132 1.294967 0 1.7889802-.4940132.4940132-1.2949669.4940132-1.7889802 0L6.35748685 8.14646701z" /> |
|||
</svg> |
|||
) |
@ -0,0 +1,14 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
|
|||
export default ({ size, ...p }: { size: number }) => ( |
|||
<svg viewBox="0 0 36 36" height={size} width={size} {...p}> |
|||
<g fill="currentColor"> |
|||
<path d="M13.62711864 10.13333333h8.77966106c.346348 0 .6271186-.2760911.6271186-.61666666v-7.4c0-.3405756-.2807706-.61666667-.6271186-.61666667h-8.77966106C13.28077058 1.5 13 1.77609107 13 2.11666667v7.4c0 .34057556.28077058.61666666.62711864.61666666zm.62711865-7.4H21.779661V8.9h-7.52542371V2.73333333z" /> |
|||
<path d="M23.4922034 23.0333333H12.3050339l-.05079661-12.79999997H23.5423729l-.0501695 12.79999997zm1.3044068-.0666667V10.23333333C24.7966102 9.5517411 24.2355175 9 23.5423729 9H12.25423729C11.56159999 9 11 9.55185858 11 10.23333333V22.9666666c0 .7179702.58202986 1.3000001 1.30000007 1.3000001H23.4966101c.7179702 0 1.3000001-.5820299 1.3000001-1.3000001z" /> |
|||
<path d="M19.71696115 24.23333333V26.7h-3.76271186v-2.46666667h3.76271186zm0 3.7c.69314464 0 1.25423729-.5517411 1.25423729-1.23333333v-3.08333333c0-.34057557-.28077061-.61666667-.62711864-.61666667h-5.01694916c-.34634803 0-.62711864.2760911-.62711864.61666667V26.7c0 .68147476.56159999 1.23333333 1.25423729 1.23333333h3.76271186z" /> |
|||
<path d="M16.52711233 34.8666667h2.50847458c.34634803 0 .62711864-.2760911.62711864-.6166667v-6.63333333c0-.34057557-.28077061-.61666667-.62711864-.61666667h-2.50847458c-.34634803 0-.62711864.2760911-.62711864.61666667V34.25c0 .3405756.28077061.6166667.62711864.6166667zm.62711865-6.63333337h1.25423729v5.39999997h-1.25423729v-5.39999997zM15.399994 4.61666667v2.46666666c0 .3405756.28077061.61666667.62711864.61666667.34634804 0 .62711865-.27609107.62711865-.61666667V4.61666667c0-.3405756-.28077061-.61666667-.62711865-.61666667-.34634803 0-.62711864.27609107-.62711864.61666667zm4 0v2.46666666c0 .3405756.28077061.61666667.62711864.61666667.34634804 0 .62711865-.27609107.62711865-.61666667V4.61666667c0-.3405756-.28077061-.61666667-.62711865-.61666667-.34634803 0-.62711864.27609107-.62711864.61666667z" /> |
|||
</g> |
|||
</svg> |
|||
) |
@ -0,0 +1,12 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
|
|||
export default ({ size, ...p }: { size: number }) => ( |
|||
<svg viewBox="0 0 36 36" height={size} width={size} {...p}> |
|||
<path |
|||
fill="currentColor" |
|||
d="M8.78055577 5C8.34946672 5 8 5.34946672 8 5.78055577V30.2194442C8 30.6505333 8.34946672 31 8.78055577 31H27.2194442C27.6505333 31 28 30.6505333 28 30.2194442V5.78055577C28 5.34946672 27.6505333 5 27.2194442 5H8.78055577zm0-2H27.2194442C28.7551028 3 30 4.24489722 30 5.78055577V30.2194442C30 31.7551028 28.7551028 33 27.2194442 33H8.78055577C7.24489722 33 6 31.7551028 6 30.2194442V5.78055577C6 4.24489722 7.24489722 3 8.78055577 3zm3.33166653 5h11.7755554C24.5020411 8 25 8.49795889 25 9.11222231V26.8877777C25 27.5020411 24.5020411 28 23.8877777 28H12.1122223C11.4979589 28 11 27.5020411 11 26.8877777V9.11222231C11 8.49795889 11.4979589 8 12.1122223 8zM12.5 9.5v17h11v-17h-11z" |
|||
/> |
|||
</svg> |
|||
) |
@ -0,0 +1,12 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
|
|||
export default ({ size, ...p }: { size: number }) => ( |
|||
<svg viewBox="0 0 9 34" height={size} width={size} {...p}> |
|||
<path |
|||
fill="currentColor" |
|||
d="M8 17.451c-.51562-.68629-1.2037-1.2356-2-1.5835V1.9995H2v13.868c-.79625.3479-1.4844.89718-2 1.5835V1.44C0 .64471.64471 0 1.44 0h5.12C7.35529 0 8 .64471 8 1.44v.75355h.40706c.32747 0 .59294.26547.59294.59294v1.0077c0 .32747-.26547.59294-.59294.59294H8v6.5806h.40706c.32747 0 .59294.26547.59294.59294v1.0077c0 .32747-.26547.59294-.59294.59294H8v4.2898zm-4 6.4844c-.55228 0-1-.44772-1-1s.44772-1 1-1 1 .44772 1 1-.44772 1-1 1zm-2 8.0645h4v-11.548c0-1.1046-.89543-2-2-2s-2 .89543-2 2v11.548zm2-15.548c2.2091 0 4 1.7909 4 4v12.363c0 .65494-.53094 1.1859-1.1859 1.1859H1.1859C.53096 34.0008 0 33.46986 0 32.8149v-12.363c0-2.2091 1.7909-4 4-4z" |
|||
/> |
|||
</svg> |
|||
) |
@ -0,0 +1,2 @@ |
|||
export Blue from './Blue' |
|||
export NanoS from './NanoS' |
@ -1,3 +1,14 @@ |
|||
title: Add account |
|||
import: |
|||
placeholder: 'Account {{index}}' |
|||
|
|||
steps: |
|||
currency: |
|||
title: Informations |
|||
connectDevice: |
|||
title: Connect Device |
|||
importProgress: |
|||
title: In Progress |
|||
importAccounts: |
|||
title: Import Accounts |
|||
cta_0: Import Account |
|||
cta_1: Import {{count}} Account |
|||
cta_2: Import {{count}} Accounts |
|||
|
@ -0,0 +1,8 @@ |
|||
step1: |
|||
connect: Connect your <0>Ledger device</0> to your computer and enter your <1>PIN code</1> on your device |
|||
choose: We detected {{devicesCount}} devices connected, please select one: |
|||
|
|||
step2: |
|||
open: Open <0>{{appName}}</0> App on your device |
|||
|
|||
info: You must use the device associated to the account <0>{{accountName}}</0> |
@ -1,12 +1,16 @@ |
|||
const babelConfig = require('../babel.config') |
|||
|
|||
const { NODE_ENV } = process.env |
|||
|
|||
module.exports = [ |
|||
{ |
|||
test: /\.js$/, |
|||
loader: 'babel-loader', |
|||
options: { |
|||
babelrc: false, |
|||
cacheDirectory: NODE_ENV === 'development', |
|||
...babelConfig(), |
|||
}, |
|||
exclude: /node_modules/, |
|||
}, |
|||
] |
|||
|
Loading…
Reference in new issue