meriadec
7 years ago
51 changed files with 1836 additions and 1683 deletions
@ -1,17 +1,13 @@ |
|||||
## What is the type of this PR? |
<!-- Description of what the PR does go here... screenshot might be good if appropriate --> |
||||
|
|
||||
<!-- e.g. Bug Fix, Feature, Code Quality Improvement, UI Polish... --> |
### Type |
||||
|
|
||||
## Any background context and/or relevant tickets/issues you want to provide with? |
|
||||
|
|
||||
<!-- e.g. GitHub issue #45 --> |
<!-- e.g. Bug Fix, Feature, Code Quality Improvement, UI Polish... --> |
||||
|
|
||||
## Short description on what this PR suppose to do? |
|
||||
|
|
||||
<!-- e.g. Adding genuine check to the onboarding --> |
### Context |
||||
|
|
||||
## Any special conditions required for testing? |
<!-- e.g. GitHub issue #45 / contextual discussion --> |
||||
|
|
||||
<!-- e.g. Clear db, add special env variable.. --> |
### Parts of the app affected / Test plan |
||||
|
|
||||
## Screenshots (if appropriate) |
<!-- Which part of the app is affected? What to do to test it, any special thing to do? --> |
||||
|
@ -0,0 +1,22 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
echo "> Getting user data folder..." |
||||
|
|
||||
|
TMP_FILE=`mktemp` |
||||
|
cat <<EOF > $TMP_FILE |
||||
|
const { app } = require('electron') |
||||
|
console.log(app.getPath('userData')) |
||||
|
EOF |
||||
|
USER_DATA_FOLDER=`timeout --preserve-status 0.5 electron $TMP_FILE || echo` # echo used to ensure status 0 |
||||
|
rm $TMP_FILE |
||||
|
|
||||
|
read -p "> Remove folder \"$USER_DATA_FOLDER\"? (y/n) " -n 1 -r |
||||
|
echo |
||||
|
if [[ $REPLY == "y" ]] |
||||
|
then |
||||
|
rm -rf "$USER_DATA_FOLDER" |
||||
|
else |
||||
|
echo "> Nothing done. Bye" |
||||
|
fi |
@ -0,0 +1,125 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import { createStructuredSelector } from 'reselect' |
||||
|
import { compose } from 'redux' |
||||
|
import { connect } from 'react-redux' |
||||
|
import { translate } from 'react-i18next' |
||||
|
import type { Currency, Account } from '@ledgerhq/live-common/lib/types' |
||||
|
|
||||
|
import type { T } from 'types/common' |
||||
|
|
||||
|
import { saveSettings } from 'actions/settings' |
||||
|
import { accountSelector } from 'reducers/accounts' |
||||
|
import { counterValueCurrencySelector, selectedTimeRangeSelector } from 'reducers/settings' |
||||
|
import type { TimeRange } from 'reducers/settings' |
||||
|
|
||||
|
import { |
||||
|
BalanceTotal, |
||||
|
BalanceSinceDiff, |
||||
|
BalanceSincePercent, |
||||
|
} from 'components/BalanceSummary/BalanceInfos' |
||||
|
import Box from 'components/base/Box' |
||||
|
import FormattedVal from 'components/base/FormattedVal' |
||||
|
import PillsDaysCount from 'components/PillsDaysCount' |
||||
|
|
||||
|
type OwnProps = { |
||||
|
isAvailable: boolean, |
||||
|
totalBalance: number, |
||||
|
sinceBalance: number, |
||||
|
refBalance: number, |
||||
|
accountId: string, // eslint-disable-line
|
||||
|
} |
||||
|
|
||||
|
type Props = OwnProps & { |
||||
|
counterValue: Currency, |
||||
|
t: T, |
||||
|
account: Account, |
||||
|
saveSettings: ({ selectedTimeRange: TimeRange }) => *, |
||||
|
selectedTimeRange: TimeRange, |
||||
|
} |
||||
|
|
||||
|
const mapStateToProps = createStructuredSelector({ |
||||
|
account: accountSelector, |
||||
|
counterValue: counterValueCurrencySelector, |
||||
|
selectedTimeRange: selectedTimeRangeSelector, |
||||
|
}) |
||||
|
|
||||
|
const mapDispatchToProps = { |
||||
|
saveSettings, |
||||
|
} |
||||
|
|
||||
|
class AccountBalanceSummaryHeader extends PureComponent<Props> { |
||||
|
handleChangeSelectedTime = item => { |
||||
|
this.props.saveSettings({ selectedTimeRange: item.key }) |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { |
||||
|
account, |
||||
|
t, |
||||
|
counterValue, |
||||
|
selectedTimeRange, |
||||
|
isAvailable, |
||||
|
totalBalance, |
||||
|
sinceBalance, |
||||
|
refBalance, |
||||
|
} = this.props |
||||
|
|
||||
|
return ( |
||||
|
<Box flow={4} mb={2}> |
||||
|
<Box horizontal> |
||||
|
<BalanceTotal |
||||
|
showCryptoEvenIfNotAvailable |
||||
|
isAvailable={isAvailable} |
||||
|
totalBalance={account.balance} |
||||
|
unit={account.unit} |
||||
|
> |
||||
|
<FormattedVal |
||||
|
animateTicker |
||||
|
disableRounding |
||||
|
alwaysShowSign={false} |
||||
|
color="warmGrey" |
||||
|
unit={counterValue.units[0]} |
||||
|
fontSize={6} |
||||
|
showCode |
||||
|
val={totalBalance} |
||||
|
/> |
||||
|
</BalanceTotal> |
||||
|
<Box> |
||||
|
<PillsDaysCount selected={selectedTimeRange} onChange={this.handleChangeSelectedTime} /> |
||||
|
</Box> |
||||
|
</Box> |
||||
|
<Box horizontal justifyContent="center" flow={7}> |
||||
|
<BalanceSincePercent |
||||
|
isAvailable={isAvailable} |
||||
|
t={t} |
||||
|
alignItems="center" |
||||
|
totalBalance={totalBalance} |
||||
|
sinceBalance={sinceBalance} |
||||
|
refBalance={refBalance} |
||||
|
since={selectedTimeRange} |
||||
|
/> |
||||
|
<BalanceSinceDiff |
||||
|
isAvailable={isAvailable} |
||||
|
t={t} |
||||
|
counterValue={counterValue} |
||||
|
alignItems="center" |
||||
|
totalBalance={totalBalance} |
||||
|
sinceBalance={sinceBalance} |
||||
|
refBalance={refBalance} |
||||
|
since={selectedTimeRange} |
||||
|
/> |
||||
|
</Box> |
||||
|
</Box> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default compose( |
||||
|
connect( |
||||
|
mapStateToProps, |
||||
|
mapDispatchToProps, |
||||
|
), |
||||
|
translate(), // FIXME t() is not even needed directly here. should be underlying component responsability to inject it
|
||||
|
)(AccountBalanceSummaryHeader) |
@ -0,0 +1,100 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent, Fragment } from 'react' |
||||
|
import { compose } from 'redux' |
||||
|
import { connect } from 'react-redux' |
||||
|
import { translate } from 'react-i18next' |
||||
|
import styled from 'styled-components' |
||||
|
import type { Account } from '@ledgerhq/live-common/lib/types' |
||||
|
import Tooltip from 'components/base/Tooltip' |
||||
|
|
||||
|
import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants' |
||||
|
|
||||
|
import type { T } from 'types/common' |
||||
|
|
||||
|
import { rgba } from 'styles/helpers' |
||||
|
|
||||
|
import { openModal } from 'reducers/modals' |
||||
|
|
||||
|
import IconAccountSettings from 'icons/AccountSettings' |
||||
|
import IconReceive from 'icons/Receive' |
||||
|
import IconSend from 'icons/Send' |
||||
|
|
||||
|
import Box, { Tabbable } from 'components/base/Box' |
||||
|
import Button from 'components/base/Button' |
||||
|
|
||||
|
const ButtonSettings = styled(Tabbable).attrs({ |
||||
|
cursor: 'pointer', |
||||
|
align: 'center', |
||||
|
justify: 'center', |
||||
|
borderRadius: 1, |
||||
|
})` |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
|
||||
|
&:hover { |
||||
|
color: ${p => (p.disabled ? '' : p.theme.colors.dark)}; |
||||
|
background: ${p => (p.disabled ? '' : rgba(p.theme.colors.fog, 0.2))}; |
||||
|
} |
||||
|
|
||||
|
&:active { |
||||
|
background: ${p => (p.disabled ? '' : rgba(p.theme.colors.fog, 0.3))}; |
||||
|
} |
||||
|
` |
||||
|
|
||||
|
const mapStateToProps = null |
||||
|
|
||||
|
const mapDispatchToProps = { |
||||
|
openModal, |
||||
|
} |
||||
|
|
||||
|
type OwnProps = { |
||||
|
account: Account, |
||||
|
} |
||||
|
|
||||
|
type Props = OwnProps & { |
||||
|
t: T, |
||||
|
openModal: Function, |
||||
|
} |
||||
|
|
||||
|
class AccountHeaderActions extends PureComponent<Props> { |
||||
|
render() { |
||||
|
const { account, openModal, t } = this.props |
||||
|
return ( |
||||
|
<Box horizontal alignItems="center" justifyContent="flex-end" flow={2}> |
||||
|
{account.operations.length > 0 && ( |
||||
|
<Fragment> |
||||
|
<Button small primary onClick={() => openModal(MODAL_SEND, { account })}> |
||||
|
<Box horizontal flow={1} alignItems="center"> |
||||
|
<IconSend size={12} /> |
||||
|
<Box>{t('app:send.title')}</Box> |
||||
|
</Box> |
||||
|
</Button> |
||||
|
|
||||
|
<Button small primary onClick={() => openModal(MODAL_RECEIVE, { account })}> |
||||
|
<Box horizontal flow={1} alignItems="center"> |
||||
|
<IconReceive size={12} /> |
||||
|
<Box>{t('app:receive.title')}</Box> |
||||
|
</Box> |
||||
|
</Button> |
||||
|
</Fragment> |
||||
|
)} |
||||
|
<Tooltip render={() => t('app:account.settings.title')}> |
||||
|
<ButtonSettings onClick={() => openModal(MODAL_SETTINGS_ACCOUNT, { account })}> |
||||
|
<Box justifyContent="center"> |
||||
|
<IconAccountSettings size={16} /> |
||||
|
</Box> |
||||
|
</ButtonSettings> |
||||
|
</Tooltip> |
||||
|
</Box> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default compose( |
||||
|
connect( |
||||
|
mapStateToProps, |
||||
|
mapDispatchToProps, |
||||
|
), |
||||
|
translate(), |
||||
|
)(AccountHeaderActions) |
@ -0,0 +1,203 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
|
||||
|
import Box from 'components/base/Box' |
||||
|
import { delay } from 'helpers/promise' |
||||
|
|
||||
|
import { |
||||
|
DeviceInteractionStepContainer, |
||||
|
SpinnerContainer, |
||||
|
IconContainer, |
||||
|
SuccessContainer, |
||||
|
ErrorContainer, |
||||
|
} from './components' |
||||
|
|
||||
|
export type Step = { |
||||
|
id: string, |
||||
|
title?: React$Node | (Object => React$Node), |
||||
|
desc?: React$Node, |
||||
|
icon: React$Node, |
||||
|
run?: Object => Promise<any> | { promise: Promise<any>, unsubscribe: void => any }, |
||||
|
render?: ({ onSuccess: Object => any, onFail: Error => void }, any) => React$Node, |
||||
|
minMs?: number, |
||||
|
} |
||||
|
|
||||
|
type Status = 'idle' | 'running' |
||||
|
|
||||
|
type Props = { |
||||
|
isFirst: boolean, |
||||
|
isLast: boolean, |
||||
|
isActive: boolean, |
||||
|
isFinished: boolean, |
||||
|
isPrecedentActive: boolean, |
||||
|
isError: boolean, |
||||
|
isSuccess: boolean, |
||||
|
isPassed: boolean, |
||||
|
step: Step, |
||||
|
onSuccess: (any, Step) => any, |
||||
|
onFail: (Error, Step) => any, |
||||
|
data: any, |
||||
|
} |
||||
|
|
||||
|
class DeviceInteractionStep extends PureComponent< |
||||
|
Props, |
||||
|
{ |
||||
|
status: Status, |
||||
|
}, |
||||
|
> { |
||||
|
static defaultProps = { |
||||
|
data: {}, |
||||
|
} |
||||
|
|
||||
|
state = { |
||||
|
status: this.props.isFirst ? 'running' : 'idle', |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
if (this.props.isFirst) { |
||||
|
this.run() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate(prevProps: Props) { |
||||
|
const { isActive, isError } = this.props |
||||
|
const { status } = this.state |
||||
|
|
||||
|
const didActivated = isActive && !prevProps.isActive |
||||
|
const didDeactivated = !isActive && prevProps.isActive |
||||
|
const stillActivated = isActive && prevProps.isActive |
||||
|
const didResetError = !isError && !!prevProps.isError |
||||
|
|
||||
|
if (didActivated && status !== 'running') { |
||||
|
this.run() |
||||
|
} |
||||
|
|
||||
|
if (didResetError && stillActivated) { |
||||
|
this.run() |
||||
|
} |
||||
|
|
||||
|
if (didDeactivated && status === 'running') { |
||||
|
this.cancel() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
componentWillUnmount() { |
||||
|
if (this._unsubscribe) { |
||||
|
this._unsubscribe() |
||||
|
} |
||||
|
this._unmounted = true |
||||
|
} |
||||
|
|
||||
|
_unsubscribe = null |
||||
|
_unmounted = false |
||||
|
|
||||
|
handleSuccess = (res: any) => { |
||||
|
const { onSuccess, step, isError } = this.props |
||||
|
if (isError) return |
||||
|
this.setState({ status: 'idle' }) |
||||
|
onSuccess(res, step) |
||||
|
} |
||||
|
|
||||
|
handleFail = (e: Error) => { |
||||
|
const { onFail, step } = this.props |
||||
|
this.setState({ status: 'idle' }) |
||||
|
onFail(e, step) |
||||
|
} |
||||
|
|
||||
|
run = async () => { |
||||
|
const { step, data } = this.props |
||||
|
const { status } = this.state |
||||
|
|
||||
|
if (status !== 'running') { |
||||
|
this.setState({ status: 'running' }) |
||||
|
} |
||||
|
|
||||
|
if (!step.run) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const d1 = Date.now() |
||||
|
|
||||
|
// $FlowFixMe JUST TESTED THE `run` 6 LINES BEFORE!!!
|
||||
|
const res = (await step.run(data)) || {} |
||||
|
if (this._unmounted) return |
||||
|
|
||||
|
if (step.minMs) { |
||||
|
const d2 = Date.now() |
||||
|
// $FlowFixMe SAME THING, JUST TESTED THE MINMS KEY, BUT EH
|
||||
|
if (d2 - d1 < step.minMs) { |
||||
|
// $FlowFixMe nice type checking
|
||||
|
await delay(step.minMs - (d2 - d1)) |
||||
|
if (this._unmounted) return |
||||
|
} |
||||
|
} |
||||
|
if (res.promise) { |
||||
|
this._unsubscribe = res.unsubscribe |
||||
|
const realRes = await res.promise |
||||
|
if (this._unmounted) return |
||||
|
this.handleSuccess(realRes) |
||||
|
} else { |
||||
|
this.handleSuccess(res) |
||||
|
} |
||||
|
} catch (e) { |
||||
|
this.handleFail(e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cancel = () => this.setState({ status: 'idle' }) |
||||
|
|
||||
|
render() { |
||||
|
const { |
||||
|
isFirst, |
||||
|
isLast, |
||||
|
isActive, |
||||
|
isFinished, |
||||
|
isPrecedentActive, |
||||
|
isSuccess, |
||||
|
isError, |
||||
|
isPassed, |
||||
|
step, |
||||
|
data, |
||||
|
} = this.props |
||||
|
|
||||
|
const { status } = this.state |
||||
|
const title = typeof step.title === 'function' ? step.title(data) : step.title |
||||
|
const { render: CustomRender } = step |
||||
|
const isRunning = status === 'running' |
||||
|
|
||||
|
return ( |
||||
|
<DeviceInteractionStepContainer |
||||
|
isFirst={isFirst} |
||||
|
isLast={isLast} |
||||
|
isFinished={isFinished} |
||||
|
isSuccess={isSuccess} |
||||
|
isActive={isActive} |
||||
|
isPrecedentActive={isPrecedentActive} |
||||
|
isError={isError} |
||||
|
> |
||||
|
<IconContainer>{step.icon}</IconContainer> |
||||
|
<Box py={4} justify="center" grow shrink> |
||||
|
{title && ( |
||||
|
<Box color={isActive && !isSuccess ? 'dark' : ''} ff="Open Sans|SemiBold"> |
||||
|
{title} |
||||
|
</Box> |
||||
|
)} |
||||
|
{step.desc && step.desc} |
||||
|
{CustomRender && ( |
||||
|
<CustomRender onSuccess={this.handleSuccess} onFail={this.handleFail} data={data} /> |
||||
|
)} |
||||
|
</Box> |
||||
|
|
||||
|
<div style={{ width: 70, position: 'relative', overflow: 'hidden', pointerEvents: 'none' }}> |
||||
|
<SpinnerContainer isVisible={isRunning} isPassed={isPassed} isError={isError} /> |
||||
|
<ErrorContainer isVisible={isError} /> |
||||
|
<SuccessContainer isVisible={isSuccess} /> |
||||
|
</div> |
||||
|
</DeviceInteractionStepContainer> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default DeviceInteractionStep |
@ -0,0 +1,112 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React from 'react' |
||||
|
import styled from 'styled-components' |
||||
|
|
||||
|
import { radii } from 'styles/theme' |
||||
|
import { rgba } from 'styles/helpers' |
||||
|
|
||||
|
import TranslatedError from 'components/TranslatedError' |
||||
|
import Box from 'components/base/Box' |
||||
|
import FakeLink from 'components/base/FakeLink' |
||||
|
import Spinner from 'components/base/Spinner' |
||||
|
import IconCheck from 'icons/Check' |
||||
|
import IconCross from 'icons/Cross' |
||||
|
import IconExclamationCircle from 'icons/ExclamationCircle' |
||||
|
|
||||
|
export const DeviceInteractionStepContainer = styled(Box).attrs({ |
||||
|
horizontal: true, |
||||
|
ff: 'Open Sans', |
||||
|
fontSize: 3, |
||||
|
bg: 'white', |
||||
|
color: 'graphite', |
||||
|
})` |
||||
|
position: relative; |
||||
|
z-index: ${p => (p.isActive ? 1 : '')}; |
||||
|
max-width: 500px; |
||||
|
min-height: 80px; |
||||
|
border: 1px solid ${p => p.theme.colors.fog}; |
||||
|
border-color: ${p => |
||||
|
p.isError ? p.theme.colors.alertRed : p.isActive && !p.isFinished ? p.theme.colors.wallet : ''}; |
||||
|
border-top-color: ${p => (p.isFirst || p.isActive ? '' : 'transparent')}; |
||||
|
border-bottom-color: ${p => (p.isPrecedentActive ? 'transparent' : '')}; |
||||
|
border-bottom-left-radius: ${p => (p.isLast ? `${radii[1]}px` : 0)}; |
||||
|
border-bottom-right-radius: ${p => (p.isLast ? `${radii[1]}px` : 0)}; |
||||
|
border-top-left-radius: ${p => (p.isFirst ? `${radii[1]}px` : 0)}; |
||||
|
border-top-right-radius: ${p => (p.isFirst ? `${radii[1]}px` : 0)}; |
||||
|
box-shadow: ${p => |
||||
|
p.isActive && !p.isSuccess |
||||
|
? ` |
||||
|
${rgba(p.isError ? p.theme.colors.alertRed : p.theme.colors.wallet, 0.2)} 0 0 3px 2px |
||||
|
` |
||||
|
: 'none'}; |
||||
|
` |
||||
|
|
||||
|
export const IconContainer = ({ children }: { children: any }) => ( |
||||
|
<Box align="center" justify="center" style={{ width: 70 }}> |
||||
|
{children} |
||||
|
</Box> |
||||
|
) |
||||
|
|
||||
|
const SpinnerContainerWrapper = styled.div` |
||||
|
color: ${p => p.theme.colors.grey}; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
transition: 350ms cubic-bezier(0.62, 0.28, 0.39, 0.94); |
||||
|
transition-property: transform opacity; |
||||
|
opacity: ${p => (p.isVisible ? 1 : 0)}; |
||||
|
transform: translate3d(0, ${p => (!p.isVisible ? -40 : 0)}px, 0); |
||||
|
` |
||||
|
|
||||
|
export const SpinnerContainer = ({ isVisible }: { isVisible: boolean }) => ( |
||||
|
<SpinnerContainerWrapper isVisible={isVisible}> |
||||
|
<Spinner size={16} /> |
||||
|
</SpinnerContainerWrapper> |
||||
|
) |
||||
|
|
||||
|
const SuccessContainerWrapper = styled(SpinnerContainerWrapper)` |
||||
|
color: ${p => p.theme.colors.wallet}; |
||||
|
transform: translate3d(0, ${p => (!p.isVisible ? 40 : 0)}px, 0); |
||||
|
` |
||||
|
|
||||
|
export const SuccessContainer = ({ isVisible }: { isVisible: boolean }) => ( |
||||
|
<SuccessContainerWrapper isVisible={isVisible}> |
||||
|
<IconCheck size={16} /> |
||||
|
</SuccessContainerWrapper> |
||||
|
) |
||||
|
|
||||
|
const ErrorContainerWrapper = styled(SpinnerContainerWrapper)` |
||||
|
color: ${p => p.theme.colors.alertRed}; |
||||
|
transform: translate3d(0, ${p => (!p.isVisible ? 40 : 0)}px, 0); |
||||
|
` |
||||
|
|
||||
|
export const ErrorContainer = ({ isVisible }: { isVisible: boolean }) => ( |
||||
|
<ErrorContainerWrapper isVisible={isVisible}> |
||||
|
<IconCross size={16} /> |
||||
|
</ErrorContainerWrapper> |
||||
|
) |
||||
|
|
||||
|
export const ErrorDescContainer = ({ |
||||
|
error, |
||||
|
onRetry, |
||||
|
...p |
||||
|
}: { |
||||
|
error: Error, |
||||
|
onRetry: void => void, |
||||
|
}) => ( |
||||
|
<Box horizontal fontSize={3} color="alertRed" align="center" cursor="text" {...p}> |
||||
|
<IconExclamationCircle size={16} /> |
||||
|
<Box ml={1}> |
||||
|
<TranslatedError error={error} /> |
||||
|
</Box> |
||||
|
<FakeLink ml={1} underline color="alertRed" onClick={onRetry}> |
||||
|
{'Retry'} |
||||
|
</FakeLink> |
||||
|
</Box> |
||||
|
) |
@ -0,0 +1,116 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
|
||||
|
import { delay } from 'helpers/promise' |
||||
|
|
||||
|
import Box from 'components/base/Box' |
||||
|
import DeviceInteractionStep from './DeviceInteractionStep' |
||||
|
import { ErrorDescContainer } from './components' |
||||
|
|
||||
|
import type { Step } from './DeviceInteractionStep' |
||||
|
|
||||
|
type Props = { |
||||
|
steps: Step[], |
||||
|
onSuccess?: any => void, |
||||
|
onFail?: any => void, |
||||
|
waitBeforeSuccess?: number, |
||||
|
|
||||
|
// when true and there is an error, display the error + retry button
|
||||
|
shouldRenderRetry?: boolean, |
||||
|
} |
||||
|
|
||||
|
type State = { |
||||
|
stepIndex: number, |
||||
|
isSuccess: boolean, |
||||
|
error: ?Error, |
||||
|
data: Object, |
||||
|
} |
||||
|
|
||||
|
const INITIAL_STATE = { |
||||
|
stepIndex: 0, |
||||
|
isSuccess: false, |
||||
|
error: null, |
||||
|
data: {}, |
||||
|
} |
||||
|
|
||||
|
class DeviceInteraction extends PureComponent<Props, State> { |
||||
|
state = INITIAL_STATE |
||||
|
|
||||
|
componentWillUnmount() { |
||||
|
this._unmounted = true |
||||
|
} |
||||
|
|
||||
|
_unmounted = false |
||||
|
|
||||
|
reset = () => this.setState(INITIAL_STATE) |
||||
|
|
||||
|
handleSuccess = async (res: any, step: Step) => { |
||||
|
const { onSuccess, steps, waitBeforeSuccess } = this.props |
||||
|
const { stepIndex, data: prevData } = this.state |
||||
|
const isCurrentStep = step.id === steps[stepIndex].id |
||||
|
if (!isCurrentStep) { |
||||
|
return |
||||
|
} |
||||
|
const data = { ...prevData, [step.id]: res || true } |
||||
|
const isLast = stepIndex === steps.length - 1 |
||||
|
if (isLast) { |
||||
|
if (!waitBeforeSuccess) { |
||||
|
onSuccess && onSuccess(data) |
||||
|
} |
||||
|
this.setState({ isSuccess: true, data }) |
||||
|
if (waitBeforeSuccess) { |
||||
|
await delay(waitBeforeSuccess) |
||||
|
if (this._unmounted) return |
||||
|
onSuccess && onSuccess(data) |
||||
|
} |
||||
|
} else { |
||||
|
this.setState({ stepIndex: stepIndex + 1, data }) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
handleFail = (error: Error, step: Step) => { |
||||
|
const { steps, onFail } = this.props |
||||
|
const { stepIndex } = this.state |
||||
|
const isCurrentStep = step === steps[stepIndex] |
||||
|
if (!isCurrentStep) { |
||||
|
return |
||||
|
} |
||||
|
this.setState({ error }) |
||||
|
onFail && onFail(error) |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { steps, shouldRenderRetry, ...props } = this.props |
||||
|
const { stepIndex, error, isSuccess, data } = this.state |
||||
|
|
||||
|
return ( |
||||
|
<Box {...props}> |
||||
|
{steps.map((step, i) => { |
||||
|
const isError = !!error && i === stepIndex |
||||
|
return ( |
||||
|
<DeviceInteractionStep |
||||
|
key={step.id} |
||||
|
step={step} |
||||
|
isError={isError} |
||||
|
isFirst={i === 0} |
||||
|
isLast={i === steps.length - 1} |
||||
|
isPrecedentActive={i === stepIndex - 1} |
||||
|
isActive={i === stepIndex} |
||||
|
isPassed={i < stepIndex} |
||||
|
isSuccess={i < stepIndex || (i === stepIndex && isSuccess)} |
||||
|
isFinished={isSuccess} |
||||
|
onSuccess={this.handleSuccess} |
||||
|
onFail={this.handleFail} |
||||
|
data={data} |
||||
|
/> |
||||
|
) |
||||
|
})} |
||||
|
{error && |
||||
|
shouldRenderRetry && <ErrorDescContainer error={error} onRetry={this.reset} mt={2} />} |
||||
|
</Box> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default DeviceInteraction |
@ -0,0 +1,81 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { Fragment } from 'react' |
||||
|
import styled from 'styled-components' |
||||
|
import { storiesOf } from '@storybook/react' |
||||
|
import { action } from '@storybook/addon-actions' |
||||
|
|
||||
|
import DeviceInteraction from 'components/DeviceInteraction' |
||||
|
import Box from 'components/base/Box' |
||||
|
import Button from 'components/base/Button' |
||||
|
|
||||
|
import IconUsb from 'icons/Usb' |
||||
|
|
||||
|
const stories = storiesOf('Components', module) |
||||
|
|
||||
|
stories.add('DeviceInteraction', () => <Wrapper />) |
||||
|
|
||||
|
const MockIcon = styled.div` |
||||
|
width: ${p => p.size}px; |
||||
|
height: ${p => p.size}px; |
||||
|
background: ${p => p.theme.colors.lightFog}; |
||||
|
border-radius: 50%; |
||||
|
` |
||||
|
|
||||
|
const mockIcon = <MockIcon size={36} /> |
||||
|
|
||||
|
class Wrapper extends React.Component<any> { |
||||
|
_ref = null |
||||
|
handleReset = () => this._ref && this._ref.reset() |
||||
|
render() { |
||||
|
return ( |
||||
|
<Fragment> |
||||
|
<button style={{ marginBottom: 40 }} onClick={this.handleReset}> |
||||
|
{'reset'} |
||||
|
</button> |
||||
|
<DeviceInteraction |
||||
|
ref={n => (this._ref = n)} |
||||
|
steps={[ |
||||
|
{ |
||||
|
id: 'deviceConnect', |
||||
|
title: 'Connect your device', |
||||
|
icon: <IconUsb size={36} />, |
||||
|
desc: 'If you dont connect your device, we wont be able to read on it', |
||||
|
render: ({ onSuccess, onFail }) => ( |
||||
|
<Box p={2} bg="lightGrey" mt={2} borderRadius={1}> |
||||
|
<Box horizontal flow={2}> |
||||
|
<Button small primary onClick={() => onSuccess({ name: 'Nano S' })}> |
||||
|
{'Nano S'} |
||||
|
</Button> |
||||
|
<Button small primary onClick={() => onSuccess({ name: 'Blue' })}> |
||||
|
{'Blue'} |
||||
|
</Button> |
||||
|
<Button small danger onClick={onFail}> |
||||
|
{'make it fail'} |
||||
|
</Button> |
||||
|
</Box> |
||||
|
</Box> |
||||
|
), |
||||
|
}, |
||||
|
{ |
||||
|
id: 'deviceOpen', |
||||
|
title: ({ deviceConnect: device }) => |
||||
|
`Open the Bitcoin application on your ${device ? `${device.name} ` : ''}device`, |
||||
|
desc: 'To be able to retriev your Bitcoins', |
||||
|
icon: mockIcon, |
||||
|
run: () => new Promise(resolve => setTimeout(resolve, 1 * 1000)), |
||||
|
}, |
||||
|
{ |
||||
|
id: 'check', |
||||
|
title: 'Checking if all is alright...', |
||||
|
desc: 'This should take only 1 second...', |
||||
|
icon: mockIcon, |
||||
|
run: () => new Promise(resolve => setTimeout(resolve, 1 * 1000)), |
||||
|
}, |
||||
|
]} |
||||
|
onSuccess={action('onSuccess')} |
||||
|
/> |
||||
|
</Fragment> |
||||
|
) |
||||
|
} |
||||
|
} |
@ -0,0 +1,158 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import { timeout } from 'rxjs/operators/timeout' |
||||
|
import { connect } from 'react-redux' |
||||
|
import { compose } from 'redux' |
||||
|
import { translate, Trans } from 'react-i18next' |
||||
|
|
||||
|
import type { T, Device } from 'types/common' |
||||
|
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' |
||||
|
|
||||
|
import { GENUINE_TIMEOUT, DEVICE_INFOS_TIMEOUT } from 'config/constants' |
||||
|
|
||||
|
import { createCancelablePolling } from 'helpers/promise' |
||||
|
import { getCurrentDevice } from 'reducers/devices' |
||||
|
import { createCustomErrorClass } from 'helpers/errors' |
||||
|
|
||||
|
import getDeviceInfo from 'commands/getDeviceInfo' |
||||
|
import getIsGenuine from 'commands/getIsGenuine' |
||||
|
|
||||
|
import DeviceInteraction from 'components/DeviceInteraction' |
||||
|
import Text from 'components/base/Text' |
||||
|
|
||||
|
import IconUsb from 'icons/Usb' |
||||
|
import IconHome from 'icons/Home' |
||||
|
import IconEye from 'icons/Eye' |
||||
|
|
||||
|
const DeviceNotGenuineError = createCustomErrorClass('DeviceNotGenuine') |
||||
|
|
||||
|
type Props = { |
||||
|
t: T, |
||||
|
onSuccess: void => void, |
||||
|
onFail: Error => void, |
||||
|
onUnavailable: Error => void, |
||||
|
device: ?Device, |
||||
|
} |
||||
|
|
||||
|
const usbIcon = <IconUsb size={36} /> |
||||
|
const homeIcon = <IconHome size={24} /> |
||||
|
const eyeIcon = <IconEye size={24} /> |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
device: getCurrentDevice(state), |
||||
|
}) |
||||
|
|
||||
|
const Bold = props => <Text ff="Open Sans|Bold" {...props} /> |
||||
|
|
||||
|
// to speed up genuine check, cache result by device id
|
||||
|
const GENUINITY_CACHE = {} |
||||
|
const getDeviceId = (device: Device) => device.path |
||||
|
const setDeviceGenuinity = (device: Device, isGenuine: boolean) => |
||||
|
(GENUINITY_CACHE[getDeviceId(device)] = isGenuine) |
||||
|
const getDeviceGenuinity = (device: Device): ?boolean => |
||||
|
GENUINITY_CACHE[getDeviceId(device)] || null |
||||
|
|
||||
|
class GenuineCheck extends PureComponent<Props> { |
||||
|
connectInteractionHandler = () => |
||||
|
createCancelablePolling(() => { |
||||
|
const { device } = this.props |
||||
|
if (!device) return Promise.reject() |
||||
|
return Promise.resolve(device) |
||||
|
}) |
||||
|
|
||||
|
checkDashboardInteractionHandler = ({ device }: { device: Device }) => |
||||
|
createCancelablePolling(() => |
||||
|
getDeviceInfo |
||||
|
.send({ devicePath: device.path }) |
||||
|
.pipe(timeout(DEVICE_INFOS_TIMEOUT)) |
||||
|
.toPromise(), |
||||
|
) |
||||
|
|
||||
|
checkGenuineInteractionHandler = async ({ |
||||
|
device, |
||||
|
deviceInfo, |
||||
|
}: { |
||||
|
device: Device, |
||||
|
deviceInfo: DeviceInfo, |
||||
|
}) => { |
||||
|
if (getDeviceGenuinity(device) === true) { |
||||
|
return true |
||||
|
} |
||||
|
const res = await getIsGenuine |
||||
|
.send({ devicePath: device.path, deviceInfo }) |
||||
|
.pipe(timeout(GENUINE_TIMEOUT)) |
||||
|
.toPromise() |
||||
|
const isGenuine = res === '0000' |
||||
|
if (!isGenuine) { |
||||
|
return Promise.reject(new Error('Device not genuine')) // TODO: use custom error class
|
||||
|
} |
||||
|
setDeviceGenuinity(device, true) |
||||
|
return Promise.resolve(true) |
||||
|
} |
||||
|
|
||||
|
handleFail = (err: Error) => { |
||||
|
const { onFail, onUnavailable } = this.props |
||||
|
if (err instanceof DeviceNotGenuineError) { |
||||
|
onFail(err) |
||||
|
} else { |
||||
|
onUnavailable(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { onSuccess, ...props } = this.props |
||||
|
const steps = [ |
||||
|
{ |
||||
|
id: 'device', |
||||
|
title: ( |
||||
|
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div"> |
||||
|
{'Connect and unlock your '} |
||||
|
<Bold>{'Ledger device'}</Bold> |
||||
|
</Trans> |
||||
|
), |
||||
|
icon: usbIcon, |
||||
|
run: this.connectInteractionHandler, |
||||
|
}, |
||||
|
{ |
||||
|
id: 'deviceInfo', |
||||
|
title: ( |
||||
|
<Trans i18nKey="deviceConnect:dashboard.open" parent="div"> |
||||
|
{'Navigate to the '} |
||||
|
<Bold>{'dashboard'}</Bold> |
||||
|
{' on your device'} |
||||
|
</Trans> |
||||
|
), |
||||
|
icon: homeIcon, |
||||
|
run: this.checkDashboardInteractionHandler, |
||||
|
}, |
||||
|
{ |
||||
|
id: 'isGenuine', |
||||
|
title: ( |
||||
|
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div"> |
||||
|
{'Allow '} |
||||
|
<Bold>{'Ledger Manager'}</Bold> |
||||
|
{' on your device'} |
||||
|
</Trans> |
||||
|
), |
||||
|
icon: eyeIcon, |
||||
|
run: this.checkGenuineInteractionHandler, |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
return ( |
||||
|
<DeviceInteraction |
||||
|
waitBeforeSuccess={500} |
||||
|
steps={steps} |
||||
|
onSuccess={onSuccess} |
||||
|
onFail={this.handleFail} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default compose( |
||||
|
translate(), |
||||
|
connect(mapStateToProps), |
||||
|
)(GenuineCheck) |
@ -0,0 +1,37 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import { translate } from 'react-i18next' |
||||
|
|
||||
|
import type { T } from 'types/common' |
||||
|
|
||||
|
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal' |
||||
|
import GenuineCheck from 'components/GenuineCheck' |
||||
|
|
||||
|
type Props = { |
||||
|
t: T, |
||||
|
onSuccess: void => void, |
||||
|
onFail: void => void, |
||||
|
onUnavailable: Error => void, |
||||
|
} |
||||
|
|
||||
|
class GenuineCheckModal extends PureComponent<Props> { |
||||
|
renderBody = ({ onClose }) => { |
||||
|
const { t, onSuccess, onFail, onUnavailable } = this.props |
||||
|
return ( |
||||
|
<ModalBody onClose={onClose}> |
||||
|
<ModalTitle>{t('app:genuinecheck.modal.title')}</ModalTitle> |
||||
|
<ModalContent> |
||||
|
<GenuineCheck onSuccess={onSuccess} onFail={onFail} onUnavailable={onUnavailable} /> |
||||
|
</ModalContent> |
||||
|
</ModalBody> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { t, ...props } = this.props |
||||
|
return <Modal {...props} render={this.renderBody} /> |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default translate()(GenuineCheckModal) |
@ -1,90 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import React, { PureComponent, Fragment } from 'react' |
|
||||
import { translate } from 'react-i18next' |
|
||||
|
|
||||
import type { T } from 'types/common' |
|
||||
|
|
||||
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal' |
|
||||
import Workflow from 'components/Workflow' |
|
||||
import WorkflowDefault from 'components/Workflow/WorkflowDefault' |
|
||||
|
|
||||
type Props = { |
|
||||
t: T, |
|
||||
onGenuineCheckPass: () => void, |
|
||||
onGenuineCheckFailed: () => void, |
|
||||
onGenuineCheckUnavailable: Error => void, |
|
||||
} |
|
||||
|
|
||||
type State = {} |
|
||||
|
|
||||
class GenuineCheckStatus extends PureComponent<*> { |
|
||||
componentDidUpdate() { |
|
||||
this.sideEffect() |
|
||||
} |
|
||||
sideEffect() { |
|
||||
const { |
|
||||
isGenuine, |
|
||||
error, |
|
||||
onGenuineCheckPass, |
|
||||
onGenuineCheckFailed, |
|
||||
onGenuineCheckUnavailable, |
|
||||
} = this.props |
|
||||
if (isGenuine !== null) { |
|
||||
if (isGenuine) { |
|
||||
onGenuineCheckPass() |
|
||||
} else { |
|
||||
onGenuineCheckFailed() |
|
||||
} |
|
||||
} else if (error) { |
|
||||
onGenuineCheckUnavailable(error) |
|
||||
} |
|
||||
} |
|
||||
render() { |
|
||||
return null |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/* eslint-disable react/no-multi-comp */ |
|
||||
class GenuineCheck extends PureComponent<Props, State> { |
|
||||
renderBody = ({ onClose }) => { |
|
||||
const { t, onGenuineCheckPass, onGenuineCheckFailed, onGenuineCheckUnavailable } = this.props |
|
||||
|
|
||||
// TODO: use the real devices list. for now we force choosing only
|
|
||||
// the current device because we don't handle multi device in MVP
|
|
||||
|
|
||||
return ( |
|
||||
<ModalBody onClose={onClose}> |
|
||||
<ModalTitle>{t('app:genuinecheck.modal.title')}</ModalTitle> |
|
||||
<ModalContent> |
|
||||
<Workflow |
|
||||
renderDefault={(device, deviceInfo, isGenuine, errors) => ( |
|
||||
<Fragment> |
|
||||
<GenuineCheckStatus |
|
||||
isGenuine={isGenuine} |
|
||||
error={errors.genuineError} |
|
||||
onGenuineCheckPass={onGenuineCheckPass} |
|
||||
onGenuineCheckFailed={onGenuineCheckFailed} |
|
||||
onGenuineCheckUnavailable={onGenuineCheckUnavailable} |
|
||||
/> |
|
||||
<WorkflowDefault |
|
||||
device={device} |
|
||||
deviceInfo={deviceInfo} |
|
||||
isGenuine={isGenuine} |
|
||||
errors={errors} // TODO: FIX ERRORS
|
|
||||
/> |
|
||||
</Fragment> |
|
||||
)} |
|
||||
/> |
|
||||
</ModalContent> |
|
||||
</ModalBody> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { ...props } = this.props |
|
||||
return <Modal {...props} render={({ onClose }) => this.renderBody({ onClose })} /> |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default translate()(GenuineCheck) |
|
@ -0,0 +1,46 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import { translate } from 'react-i18next' |
||||
|
|
||||
|
import type { T } from 'types/common' |
||||
|
|
||||
|
import { i } from 'helpers/staticPath' |
||||
|
|
||||
|
import GenuineCheck from 'components/GenuineCheck' |
||||
|
import Box from 'components/base/Box' |
||||
|
import Space from 'components/base/Space' |
||||
|
import Text from 'components/base/Text' |
||||
|
|
||||
|
type Props = { |
||||
|
t: T, |
||||
|
onSuccess: void => void, |
||||
|
} |
||||
|
|
||||
|
class ManagerGenuineCheck extends PureComponent<Props> { |
||||
|
render() { |
||||
|
const { t, onSuccess } = this.props |
||||
|
return ( |
||||
|
<Box align="center"> |
||||
|
<Space of={60} /> |
||||
|
<Box align="center" style={{ maxWidth: 460 }}> |
||||
|
<img |
||||
|
src={i('logos/connectDevice.png')} |
||||
|
alt="connect your device" |
||||
|
style={{ marginBottom: 30, maxWidth: 362, width: '100%' }} |
||||
|
/> |
||||
|
<Text ff="Museo Sans|Regular" fontSize={7} color="black" style={{ marginBottom: 10 }}> |
||||
|
{t('app:manager.device.title')} |
||||
|
</Text> |
||||
|
<Text ff="Museo Sans|Light" fontSize={5} color="grey" align="center"> |
||||
|
{t('app:manager.device.desc')} |
||||
|
</Text> |
||||
|
</Box> |
||||
|
<Space of={40} /> |
||||
|
<GenuineCheck shouldRenderRetry onSuccess={onSuccess} /> |
||||
|
</Box> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default translate()(ManagerGenuineCheck) |
@ -1,81 +0,0 @@ |
|||||
// @flow
|
|
||||
import { PureComponent } from 'react' |
|
||||
import isEqual from 'lodash/isEqual' |
|
||||
|
|
||||
import type { Node } from 'react' |
|
||||
import type { Device } from 'types/common' |
|
||||
|
|
||||
import getDeviceInfo from 'commands/getDeviceInfo' |
|
||||
|
|
||||
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' |
|
||||
|
|
||||
type Error = { |
|
||||
message: string, |
|
||||
stack: string, |
|
||||
} |
|
||||
|
|
||||
type Props = { |
|
||||
device: ?Device, |
|
||||
children: (deviceInfo: ?DeviceInfo, error: ?Error) => Node, |
|
||||
} |
|
||||
|
|
||||
type State = { |
|
||||
deviceInfo: ?DeviceInfo, |
|
||||
error: ?Error, |
|
||||
} |
|
||||
|
|
||||
class EnsureDashboard extends PureComponent<Props, State> { |
|
||||
static defaultProps = { |
|
||||
children: null, |
|
||||
device: null, |
|
||||
} |
|
||||
|
|
||||
state = { |
|
||||
deviceInfo: null, |
|
||||
error: null, |
|
||||
} |
|
||||
|
|
||||
componentDidMount() { |
|
||||
this.checkForDashboard() |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate({ device }: Props) { |
|
||||
if (this.props.device !== device && this.props.device) { |
|
||||
this.checkForDashboard() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
componentWillUnmount() { |
|
||||
this._unmounting = true |
|
||||
} |
|
||||
|
|
||||
_checking = false |
|
||||
_unmounting = false |
|
||||
|
|
||||
checkForDashboard = async () => { |
|
||||
const { device } = this.props |
|
||||
if (device && !this._checking) { |
|
||||
this._checking = true |
|
||||
try { |
|
||||
const deviceInfo = await getDeviceInfo.send({ devicePath: device.path }).toPromise() |
|
||||
if (!isEqual(this.state.deviceInfo, deviceInfo) || this.state.error) { |
|
||||
!this._unmounting && this.setState({ deviceInfo, error: null }) |
|
||||
} |
|
||||
} catch (err) { |
|
||||
if (!isEqual(err, this.state.error)) { |
|
||||
!this._unmounting && this.setState({ error: err, deviceInfo: null }) |
|
||||
} |
|
||||
} |
|
||||
this._checking = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { deviceInfo, error } = this.state |
|
||||
const { children } = this.props |
|
||||
|
|
||||
return children(deviceInfo, error) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default EnsureDashboard |
|
@ -1,88 +0,0 @@ |
|||||
// @flow
|
|
||||
import { timeout } from 'rxjs/operators/timeout' |
|
||||
import { PureComponent } from 'react' |
|
||||
import isEqual from 'lodash/isEqual' |
|
||||
|
|
||||
import { GENUINE_TIMEOUT } from 'config/constants' |
|
||||
import type { Device } from 'types/common' |
|
||||
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' |
|
||||
|
|
||||
import getIsGenuine from 'commands/getIsGenuine' |
|
||||
|
|
||||
type Error = { |
|
||||
message: string, |
|
||||
stack: string, |
|
||||
} |
|
||||
|
|
||||
type Props = { |
|
||||
device: ?Device, |
|
||||
deviceInfo: ?DeviceInfo, |
|
||||
children: (isGenuine: ?boolean, error: ?Error) => *, |
|
||||
} |
|
||||
|
|
||||
type State = { |
|
||||
genuine: ?boolean, |
|
||||
error: ?Error, |
|
||||
} |
|
||||
|
|
||||
class EnsureGenuine extends PureComponent<Props, State> { |
|
||||
static defaultProps = { |
|
||||
children: () => null, |
|
||||
firmwareInfo: null, |
|
||||
} |
|
||||
|
|
||||
state = { |
|
||||
error: null, |
|
||||
genuine: null, |
|
||||
} |
|
||||
|
|
||||
componentDidMount() { |
|
||||
this.checkIsGenuine() |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate() { |
|
||||
this.checkIsGenuine() |
|
||||
} |
|
||||
|
|
||||
componentWillUnmount() { |
|
||||
this._unmounting = true |
|
||||
} |
|
||||
|
|
||||
_checking = false |
|
||||
_unmounting = false |
|
||||
|
|
||||
async checkIsGenuine() { |
|
||||
const { device, deviceInfo } = this.props |
|
||||
if (device && deviceInfo && !this._checking) { |
|
||||
this._checking = true |
|
||||
try { |
|
||||
const res = await getIsGenuine |
|
||||
.send({ |
|
||||
devicePath: device.path, |
|
||||
deviceInfo, |
|
||||
}) |
|
||||
.pipe(timeout(GENUINE_TIMEOUT)) |
|
||||
.toPromise() |
|
||||
if (this._unmounting) return |
|
||||
const isGenuine = res === '0000' |
|
||||
if (!this.state.genuine || this.state.error) { |
|
||||
this.setState({ genuine: isGenuine, error: null }) |
|
||||
} |
|
||||
} catch (err) { |
|
||||
if (!isEqual(this.state.error, err)) { |
|
||||
this.setState({ genuine: null, error: err }) |
|
||||
} |
|
||||
} |
|
||||
this._checking = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { error, genuine } = this.state |
|
||||
const { children } = this.props |
|
||||
|
|
||||
return children(genuine, error) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default EnsureGenuine |
|
@ -1,171 +0,0 @@ |
|||||
// @flow
|
|
||||
/* eslint-disable react/jsx-no-literals */ |
|
||||
|
|
||||
import React from 'react' |
|
||||
import { Trans, translate } from 'react-i18next' |
|
||||
import styled from 'styled-components' |
|
||||
import isNull from 'lodash/isNull' |
|
||||
import type { Device } from 'types/common' |
|
||||
|
|
||||
import Box from 'components/base/Box' |
|
||||
import Spinner from 'components/base/Spinner' |
|
||||
|
|
||||
import IconCheck from 'icons/Check' |
|
||||
import IconExclamationCircle from 'icons/ExclamationCircle' |
|
||||
import IconUsb from 'icons/Usb' |
|
||||
import IconHome from 'icons/Home' |
|
||||
|
|
||||
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 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 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> |
|
||||
) : ( |
|
||||
<Spinner size={16} /> |
|
||||
)} |
|
||||
</Box> |
|
||||
) |
|
||||
|
|
||||
StepCheck.defaultProps = { |
|
||||
hasErrors: false, |
|
||||
} |
|
||||
|
|
||||
type DeviceInfo = { |
|
||||
targetId: number | string, |
|
||||
version: string, |
|
||||
final: boolean, |
|
||||
mcu: boolean, |
|
||||
} |
|
||||
|
|
||||
type Error = { |
|
||||
message: string, |
|
||||
stack: string, |
|
||||
} |
|
||||
|
|
||||
type Props = { |
|
||||
// t: T,
|
|
||||
device: ?Device, |
|
||||
deviceInfo: ?DeviceInfo, |
|
||||
errors: { |
|
||||
dashboardError: ?Error, |
|
||||
genuineError: ?Error, |
|
||||
}, |
|
||||
isGenuine: boolean, |
|
||||
} |
|
||||
|
|
||||
const WorkflowDefault = ({ device, deviceInfo, errors, isGenuine }: Props) => ( |
|
||||
<Box flow={4} ff="Open Sans"> |
|
||||
<Step validated={!!device}> |
|
||||
<StepContent> |
|
||||
<StepIcon> |
|
||||
<IconUsb size={36} /> |
|
||||
</StepIcon> |
|
||||
<Box grow shrink> |
|
||||
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div"> |
|
||||
Connect and unlock your <strong>Ledger device</strong> <strong /> |
|
||||
</Trans> |
|
||||
</Box> |
|
||||
<StepCheck checked={!!device} /> |
|
||||
</StepContent> |
|
||||
</Step> |
|
||||
|
|
||||
<Step validated={!!device && !!deviceInfo} hasErrors={!!device && !!errors.dashboardError}> |
|
||||
<StepContent> |
|
||||
<StepIcon> |
|
||||
<WrapperIconCurrency> |
|
||||
<IconHome size={12} /> |
|
||||
</WrapperIconCurrency> |
|
||||
</StepIcon> |
|
||||
<Box grow shrink> |
|
||||
<Trans i18nKey="deviceConnect:dashboard.open" parent="div"> |
|
||||
{'Navigate to the '} |
|
||||
<strong>{'dashboard'}</strong> |
|
||||
{' on your device'} |
|
||||
</Trans> |
|
||||
</Box> |
|
||||
<StepCheck |
|
||||
checked={!!device && !!deviceInfo} |
|
||||
hasErrors={!!device && !!errors.dashboardError} |
|
||||
/> |
|
||||
</StepContent> |
|
||||
</Step> |
|
||||
|
|
||||
{/* GENUINE CHECK */} |
|
||||
{/* ------------- */} |
|
||||
|
|
||||
<Step |
|
||||
validated={(!!device && !isNull(isGenuine) && isGenuine && !errors.genuineError) || undefined} |
|
||||
hasErrors={(!!device && !isNull(isGenuine) && !isGenuine) || errors.genuineError || undefined} |
|
||||
> |
|
||||
<StepContent> |
|
||||
<StepIcon> |
|
||||
<WrapperIconCurrency> |
|
||||
<IconCheck size={12} /> |
|
||||
</WrapperIconCurrency> |
|
||||
</StepIcon> |
|
||||
<Box grow shrink> |
|
||||
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div"> |
|
||||
{'Allow the '} |
|
||||
<strong>{'Ledger Manager'}</strong> |
|
||||
{' on your device'} |
|
||||
</Trans> |
|
||||
</Box> |
|
||||
<StepCheck |
|
||||
checked={!!device && !isNull(isGenuine) && isGenuine} |
|
||||
hasErrors={(!!device && !isNull(isGenuine) && !isGenuine) || undefined} |
|
||||
/> |
|
||||
</StepContent> |
|
||||
</Step> |
|
||||
</Box> |
|
||||
) |
|
||||
|
|
||||
export default translate()(WorkflowDefault) |
|
@ -1,194 +0,0 @@ |
|||||
// @flow
|
|
||||
/* eslint-disable react/jsx-no-literals */ // FIXME
|
|
||||
|
|
||||
import React from 'react' |
|
||||
import { Trans, translate } from 'react-i18next' |
|
||||
import styled from 'styled-components' |
|
||||
import isNull from 'lodash/isNull' |
|
||||
import type { Device, T } from 'types/common' |
|
||||
|
|
||||
import { i } from 'helpers/staticPath' |
|
||||
|
|
||||
import Box from 'components/base/Box' |
|
||||
import Text from 'components/base/Text' |
|
||||
import Spinner from 'components/base/Spinner' |
|
||||
|
|
||||
import IconCheck from 'icons/Check' |
|
||||
import IconExclamationCircle from 'icons/ExclamationCircle' |
|
||||
import IconUsb from 'icons/Usb' |
|
||||
import IconHome from 'icons/Home' |
|
||||
|
|
||||
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 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 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> |
|
||||
) : ( |
|
||||
<Spinner size={16} /> |
|
||||
)} |
|
||||
</Box> |
|
||||
) |
|
||||
|
|
||||
StepCheck.defaultProps = { |
|
||||
hasErrors: false, |
|
||||
} |
|
||||
|
|
||||
type DeviceInfo = { |
|
||||
targetId: number | string, |
|
||||
version: string, |
|
||||
final: boolean, |
|
||||
mcu: boolean, |
|
||||
} |
|
||||
|
|
||||
type Error = { |
|
||||
message: string, |
|
||||
stack: string, |
|
||||
} |
|
||||
|
|
||||
type Props = { |
|
||||
t: T, |
|
||||
device: ?Device, |
|
||||
deviceInfo: ?DeviceInfo, |
|
||||
errors: { |
|
||||
dashboardError: ?Error, |
|
||||
genuineError: ?Error, |
|
||||
}, |
|
||||
isGenuine: boolean, |
|
||||
} |
|
||||
|
|
||||
const WorkflowWithIcon = ({ device, deviceInfo, errors, isGenuine, t }: Props) => ( |
|
||||
<Box align="center" justify="center" sticky> |
|
||||
<Box align="center" style={{ maxWidth: 460, padding: '0 10px' }}> |
|
||||
<img |
|
||||
src={i('logos/connectDevice.png')} |
|
||||
alt="connect your device" |
|
||||
style={{ marginBottom: 30, maxWidth: 362, width: '100%' }} |
|
||||
/> |
|
||||
<Text ff="Museo Sans|Regular" fontSize={7} color="black" style={{ marginBottom: 10 }}> |
|
||||
{t('app:manager.device.title')} |
|
||||
</Text> |
|
||||
<Text ff="Museo Sans|Light" fontSize={5} color="grey" align="center"> |
|
||||
{t('app:manager.device.desc')} |
|
||||
</Text> |
|
||||
</Box> |
|
||||
<Box flow={4} style={{ maxWidth: 460, padding: '60px 10px 0' }} ff="Open Sans|Regular"> |
|
||||
{/* DEVICE CHECK */} |
|
||||
<Step validated={!!device}> |
|
||||
<StepContent> |
|
||||
<StepIcon> |
|
||||
<IconUsb size={36} /> |
|
||||
</StepIcon> |
|
||||
<Box grow shrink> |
|
||||
<Trans i18nKey="deviceConnect:step1.connect" parent="div"> |
|
||||
{'Connect and unlock your '} |
|
||||
<strong>Ledger device</strong> |
|
||||
</Trans> |
|
||||
</Box> |
|
||||
<StepCheck checked={!!device} /> |
|
||||
</StepContent> |
|
||||
</Step> |
|
||||
|
|
||||
{/* DASHBOARD CHECK */} |
|
||||
<Step validated={!!device && !!deviceInfo} hasErrors={!!device && !!errors.dashboardError}> |
|
||||
<StepContent> |
|
||||
<StepIcon> |
|
||||
<WrapperIconCurrency> |
|
||||
<IconHome size={12} /> |
|
||||
</WrapperIconCurrency> |
|
||||
</StepIcon> |
|
||||
<Box grow shrink> |
|
||||
<Trans i18nKey="deviceConnect:dashboard.open" parent="div"> |
|
||||
{'Navigate to the '} |
|
||||
<strong>{'dashboard'}</strong> |
|
||||
{' on your device'} |
|
||||
</Trans> |
|
||||
</Box> |
|
||||
<StepCheck |
|
||||
checked={!!device && !!deviceInfo} |
|
||||
hasErrors={!!device && !!errors.dashboardError} |
|
||||
/> |
|
||||
</StepContent> |
|
||||
</Step> |
|
||||
|
|
||||
{/* GENUINE CHECK */} |
|
||||
<Step |
|
||||
validated={ |
|
||||
(!!device && !isNull(isGenuine) && isGenuine && !errors.genuineError) || undefined |
|
||||
} |
|
||||
hasErrors={ |
|
||||
(!!device && !isNull(isGenuine) && !isGenuine) || errors.genuineError || undefined |
|
||||
} |
|
||||
> |
|
||||
<StepContent> |
|
||||
<StepIcon> |
|
||||
<WrapperIconCurrency> |
|
||||
<IconCheck size={12} /> |
|
||||
</WrapperIconCurrency> |
|
||||
</StepIcon> |
|
||||
<Box grow shrink> |
|
||||
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div"> |
|
||||
{'Allow '} |
|
||||
<strong>{'Ledger Manager'}</strong> |
|
||||
{' on your device'} |
|
||||
</Trans> |
|
||||
</Box> |
|
||||
<StepCheck |
|
||||
checked={(!!device && !isNull(isGenuine) && isGenuine) || undefined} |
|
||||
hasErrors={(!!device && !isNull(isGenuine) && !isGenuine) || undefined} |
|
||||
/> |
|
||||
</StepContent> |
|
||||
</Step> |
|
||||
</Box> |
|
||||
</Box> |
|
||||
) |
|
||||
|
|
||||
export default translate()(WorkflowWithIcon) |
|
@ -1,93 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import React, { PureComponent } from 'react' |
|
||||
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' |
|
||||
|
|
||||
import type { Node } from 'react' |
|
||||
import type { Device } from 'types/common' |
|
||||
|
|
||||
import EnsureDevice from './EnsureDevice' |
|
||||
import EnsureDashboard from './EnsureDashboard' |
|
||||
import EnsureGenuine from './EnsureGenuine' |
|
||||
|
|
||||
type Error = { |
|
||||
message: string, |
|
||||
stack: string, |
|
||||
} |
|
||||
|
|
||||
type Props = { |
|
||||
renderDefault: ( |
|
||||
device: ?Device, |
|
||||
deviceInfo: ?DeviceInfo, |
|
||||
isGenuine: ?boolean, |
|
||||
error: { |
|
||||
dashboardError: ?Error, |
|
||||
genuineError: ?Error, |
|
||||
}, |
|
||||
) => Node, |
|
||||
renderMcuUpdate?: (device: Device, deviceInfo: DeviceInfo) => Node, |
|
||||
renderFinalUpdate?: (device: Device, deviceInfo: DeviceInfo) => Node, |
|
||||
renderDashboard?: (device: Device, deviceInfo: DeviceInfo, isGenuine: boolean) => Node, |
|
||||
onGenuineCheck?: (isGenuine: boolean) => void, |
|
||||
renderError?: (dashboardError: ?Error, genuineError: ?Error) => Node, |
|
||||
} |
|
||||
type State = {} |
|
||||
|
|
||||
// In future, move to meri's approach; this code is way too much specific
|
|
||||
class Workflow extends PureComponent<Props, State> { |
|
||||
render() { |
|
||||
const { |
|
||||
renderDashboard, |
|
||||
renderFinalUpdate, |
|
||||
renderMcuUpdate, |
|
||||
renderError, |
|
||||
renderDefault, |
|
||||
onGenuineCheck, |
|
||||
} = this.props |
|
||||
return ( |
|
||||
<EnsureDevice> |
|
||||
{(device: Device) => ( |
|
||||
<EnsureDashboard device={device}> |
|
||||
{(deviceInfo: ?DeviceInfo, dashboardError: ?Error) => { |
|
||||
if (deviceInfo && deviceInfo.isBootloader && renderMcuUpdate) { |
|
||||
return renderMcuUpdate(device, deviceInfo) |
|
||||
} |
|
||||
|
|
||||
if (deviceInfo && deviceInfo.isOSU && renderFinalUpdate) { |
|
||||
return renderFinalUpdate(device, deviceInfo) |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<EnsureGenuine device={device} deviceInfo={deviceInfo}> |
|
||||
{(isGenuine: ?boolean, genuineError: ?Error) => { |
|
||||
if (dashboardError || genuineError) { |
|
||||
return renderError |
|
||||
? renderError(dashboardError, genuineError) |
|
||||
: renderDefault(device, deviceInfo, isGenuine, { |
|
||||
genuineError, |
|
||||
dashboardError, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
if (isGenuine && deviceInfo && device && !dashboardError && !genuineError) { |
|
||||
if (onGenuineCheck) onGenuineCheck(isGenuine) |
|
||||
|
|
||||
if (renderDashboard) return renderDashboard(device, deviceInfo, isGenuine) |
|
||||
} |
|
||||
|
|
||||
return renderDefault(device, deviceInfo, isGenuine, { |
|
||||
genuineError, |
|
||||
dashboardError, |
|
||||
}) |
|
||||
}} |
|
||||
</EnsureGenuine> |
|
||||
) |
|
||||
}} |
|
||||
</EnsureDashboard> |
|
||||
)} |
|
||||
</EnsureDevice> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default Workflow |
|
@ -1,25 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import React from 'react' |
|
||||
|
|
||||
import type { Account } from '@ledgerhq/live-common/lib/types' |
|
||||
import type { T } from 'types/common' |
|
||||
|
|
||||
import TrackPage from 'analytics/TrackPage' |
|
||||
import Box from 'components/base/Box' |
|
||||
import Label from 'components/base/Label' |
|
||||
import SelectAccount from 'components/SelectAccount' |
|
||||
|
|
||||
type Props = { |
|
||||
account: ?Account, |
|
||||
onChangeAccount: Function, |
|
||||
t: T, |
|
||||
} |
|
||||
|
|
||||
export default (props: Props) => ( |
|
||||
<Box flow={1}> |
|
||||
<TrackPage category="Receive" name="Step1" /> |
|
||||
<Label>{props.t('app:receive.steps.chooseAccount.label')}</Label> |
|
||||
<SelectAccount autoFocus onChange={props.onChangeAccount} value={props.account} /> |
|
||||
</Box> |
|
||||
) |
|
@ -1,16 +0,0 @@ |
|||||
import React, { Component, Fragment } from 'react' |
|
||||
import TrackPage from 'analytics/TrackPage' |
|
||||
import StepConnectDevice from '../StepConnectDevice' |
|
||||
|
|
||||
class ReceiveStepConnectDevice extends Component<*> { |
|
||||
render() { |
|
||||
return ( |
|
||||
<Fragment> |
|
||||
<TrackPage category="Receive" name="Step2" /> |
|
||||
<StepConnectDevice {...this.props} /> |
|
||||
</Fragment> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ReceiveStepConnectDevice |
|
@ -1,59 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import React, { Fragment } from 'react' |
|
||||
import styled from 'styled-components' |
|
||||
|
|
||||
import type { Account } from '@ledgerhq/live-common/lib/types' |
|
||||
import type { Device, T } from 'types/common' |
|
||||
|
|
||||
import TrackPage from 'analytics/TrackPage' |
|
||||
import Box from 'components/base/Box' |
|
||||
import CurrentAddressForAccount from 'components/CurrentAddressForAccount' |
|
||||
import DeviceConfirm from 'components/DeviceConfirm' |
|
||||
|
|
||||
const Container = styled(Box).attrs({ |
|
||||
alignItems: 'center', |
|
||||
fontSize: 4, |
|
||||
color: 'dark', |
|
||||
px: 7, |
|
||||
})`` |
|
||||
|
|
||||
const Title = styled(Box).attrs({ |
|
||||
ff: 'Museo Sans|Regular', |
|
||||
fontSize: 6, |
|
||||
mb: 1, |
|
||||
})`` |
|
||||
|
|
||||
const Text = styled(Box).attrs({ |
|
||||
color: 'smoke', |
|
||||
})` |
|
||||
text-align: center; |
|
||||
` |
|
||||
|
|
||||
type Props = { |
|
||||
account: ?Account, |
|
||||
addressVerified: ?boolean, |
|
||||
device: ?Device, |
|
||||
t: T, |
|
||||
} |
|
||||
|
|
||||
export default (props: Props) => ( |
|
||||
<Container> |
|
||||
<TrackPage category="Receive" name="Step3" /> |
|
||||
{props.addressVerified === false ? ( |
|
||||
<Fragment> |
|
||||
<Title>{props.t('app:receive.steps.confirmAddress.error.title')}</Title> |
|
||||
<Text mb={5}>{props.t('app:receive.steps.confirmAddress.error.text')}</Text> |
|
||||
<DeviceConfirm error /> |
|
||||
</Fragment> |
|
||||
) : ( |
|
||||
<Fragment> |
|
||||
<Title>{props.t('app:receive.steps.confirmAddress.action')}</Title> |
|
||||
<Text>{props.t('app:receive.steps.confirmAddress.text')}</Text> |
|
||||
{props.account && <CurrentAddressForAccount account={props.account} />} |
|
||||
{props.device && |
|
||||
props.account && <DeviceConfirm mb={2} mt={-1} error={props.addressVerified === false} />} |
|
||||
</Fragment> |
|
||||
)} |
|
||||
</Container> |
|
||||
) |
|
@ -1,48 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import React from 'react' |
|
||||
|
|
||||
import type { Account } from '@ledgerhq/live-common/lib/types' |
|
||||
import type { T } from 'types/common' |
|
||||
|
|
||||
import TrackPage from 'analytics/TrackPage' |
|
||||
import Box from 'components/base/Box' |
|
||||
import CurrentAddressForAccount from 'components/CurrentAddressForAccount' |
|
||||
import Label from 'components/base/Label' |
|
||||
import RequestAmount from 'components/RequestAmount' |
|
||||
|
|
||||
type Props = { |
|
||||
account: ?Account, |
|
||||
addressVerified: ?boolean, |
|
||||
amount: string | number, |
|
||||
onChangeAmount: Function, |
|
||||
onVerify: Function, |
|
||||
t: T, |
|
||||
} |
|
||||
|
|
||||
export default (props: Props) => ( |
|
||||
<Box flow={5}> |
|
||||
<TrackPage category="Receive" name="Step4" /> |
|
||||
<Box flow={1}> |
|
||||
<Label>{props.t('app:receive.steps.receiveFunds.label')}</Label> |
|
||||
<RequestAmount |
|
||||
account={props.account} |
|
||||
onChange={props.onChangeAmount} |
|
||||
value={props.amount} |
|
||||
withMax={false} |
|
||||
/> |
|
||||
</Box> |
|
||||
{props.account && ( |
|
||||
<CurrentAddressForAccount |
|
||||
account={props.account} |
|
||||
addressVerified={props.addressVerified} |
|
||||
amount={props.amount} |
|
||||
onVerify={props.onVerify} |
|
||||
withBadge |
|
||||
withFooter |
|
||||
withQRCode |
|
||||
withVerify={props.addressVerified === false} |
|
||||
/> |
|
||||
)} |
|
||||
</Box> |
|
||||
) |
|
@ -0,0 +1,29 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React from 'react' |
||||
|
|
||||
|
import TrackPage from 'analytics/TrackPage' |
||||
|
import Box from 'components/base/Box' |
||||
|
import Label from 'components/base/Label' |
||||
|
import Button from 'components/base/Button' |
||||
|
import SelectAccount from 'components/SelectAccount' |
||||
|
|
||||
|
import type { StepProps } from '../index' |
||||
|
|
||||
|
export default function StepAccount({ t, account, onChangeAccount }: StepProps) { |
||||
|
return ( |
||||
|
<Box flow={1}> |
||||
|
<TrackPage category="Receive" name="Step1" /> |
||||
|
<Label>{t('app:receive.steps.chooseAccount.label')}</Label> |
||||
|
<SelectAccount autoFocus onChange={onChangeAccount} value={account} /> |
||||
|
</Box> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export function StepAccountFooter({ t, transitionTo, account }: StepProps) { |
||||
|
return ( |
||||
|
<Button disabled={!account} primary onClick={() => transitionTo('device')}> |
||||
|
{t('app:common.next')} |
||||
|
</Button> |
||||
|
) |
||||
|
} |
@ -0,0 +1,42 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React from 'react' |
||||
|
|
||||
|
import Box from 'components/base/Box' |
||||
|
import Button from 'components/base/Button' |
||||
|
import EnsureDeviceApp from 'components/EnsureDeviceApp' |
||||
|
|
||||
|
import type { StepProps } from '../index' |
||||
|
|
||||
|
export default function StepConnectDevice({ account, onChangeAppOpened }: StepProps) { |
||||
|
return ( |
||||
|
<EnsureDeviceApp |
||||
|
account={account} |
||||
|
waitBeforeSuccess={200} |
||||
|
onSuccess={() => onChangeAppOpened(true)} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export function StepConnectDeviceFooter({ |
||||
|
t, |
||||
|
transitionTo, |
||||
|
isAppOpened, |
||||
|
onSkipConfirm, |
||||
|
}: StepProps) { |
||||
|
return ( |
||||
|
<Box horizontal flow={2}> |
||||
|
<Button |
||||
|
onClick={() => { |
||||
|
onSkipConfirm() |
||||
|
transitionTo('receive') |
||||
|
}} |
||||
|
> |
||||
|
{t('app:receive.steps.connectDevice.withoutDevice')} |
||||
|
</Button> |
||||
|
<Button disabled={!isAppOpened} primary onClick={() => transitionTo('confirm')}> |
||||
|
{t('app:common.next')} |
||||
|
</Button> |
||||
|
</Box> |
||||
|
) |
||||
|
} |
@ -0,0 +1,112 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import invariant from 'invariant' |
||||
|
import styled from 'styled-components' |
||||
|
import React, { Fragment, PureComponent } from 'react' |
||||
|
|
||||
|
import getAddress from 'commands/getAddress' |
||||
|
import { isSegwitAccount } from 'helpers/bip32' |
||||
|
|
||||
|
import TrackPage from 'analytics/TrackPage' |
||||
|
import Box from 'components/base/Box' |
||||
|
import Button from 'components/base/Button' |
||||
|
import DeviceConfirm from 'components/DeviceConfirm' |
||||
|
import CurrentAddressForAccount from 'components/CurrentAddressForAccount' |
||||
|
import { WrongDeviceForAccount } from 'components/EnsureDeviceApp' |
||||
|
|
||||
|
import type { StepProps } from '../index' |
||||
|
|
||||
|
export default class StepConfirmAddress extends PureComponent<StepProps> { |
||||
|
componentDidMount() { |
||||
|
this.confirmAddress() |
||||
|
} |
||||
|
|
||||
|
confirmAddress = async () => { |
||||
|
const { account, device, onChangeAddressVerified, transitionTo } = this.props |
||||
|
invariant(account, 'No account given') |
||||
|
invariant(device, 'No device given') |
||||
|
try { |
||||
|
const params = { |
||||
|
currencyId: account.currency.id, |
||||
|
devicePath: device.path, |
||||
|
path: account.freshAddressPath, |
||||
|
segwit: isSegwitAccount(account), |
||||
|
verify: true, |
||||
|
} |
||||
|
const { address } = await getAddress.send(params).toPromise() |
||||
|
|
||||
|
if (address !== account.freshAddress) { |
||||
|
throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, { |
||||
|
accountName: account.name, |
||||
|
}) |
||||
|
} |
||||
|
onChangeAddressVerified(true) |
||||
|
transitionTo('receive') |
||||
|
} catch (err) { |
||||
|
onChangeAddressVerified(false) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { t, device, account, isAddressVerified } = this.props |
||||
|
invariant(account, 'No account given') |
||||
|
invariant(device, 'No device given') |
||||
|
return ( |
||||
|
<Container> |
||||
|
<TrackPage category="Receive" name="Step3" /> |
||||
|
{isAddressVerified === false ? ( |
||||
|
<Fragment> |
||||
|
<Title>{t('app:receive.steps.confirmAddress.error.title')}</Title> |
||||
|
<Text mb={5}>{t('app:receive.steps.confirmAddress.error.text')}</Text> |
||||
|
<DeviceConfirm error /> |
||||
|
</Fragment> |
||||
|
) : ( |
||||
|
<Fragment> |
||||
|
<Title>{t('app:receive.steps.confirmAddress.action')}</Title> |
||||
|
<Text>{t('app:receive.steps.confirmAddress.text')}</Text> |
||||
|
<CurrentAddressForAccount account={account} /> |
||||
|
<DeviceConfirm mb={2} mt={-1} error={isAddressVerified === false} /> |
||||
|
</Fragment> |
||||
|
)} |
||||
|
</Container> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function StepConfirmAddressFooter({ t, transitionTo, onRetry }: StepProps) { |
||||
|
// This will be displayed only if user rejected address
|
||||
|
return ( |
||||
|
<Fragment> |
||||
|
<Button>{t('app:receive.steps.confirmAddress.support')}</Button> |
||||
|
<Button |
||||
|
ml={2} |
||||
|
primary |
||||
|
onClick={() => { |
||||
|
onRetry() |
||||
|
transitionTo('device') |
||||
|
}} |
||||
|
> |
||||
|
{t('app:common.retry')} |
||||
|
</Button> |
||||
|
</Fragment> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const Container = styled(Box).attrs({ |
||||
|
alignItems: 'center', |
||||
|
fontSize: 4, |
||||
|
color: 'dark', |
||||
|
px: 7, |
||||
|
})`` |
||||
|
|
||||
|
const Title = styled(Box).attrs({ |
||||
|
ff: 'Museo Sans|Regular', |
||||
|
fontSize: 6, |
||||
|
mb: 1, |
||||
|
})`` |
||||
|
|
||||
|
const Text = styled(Box).attrs({ |
||||
|
color: 'smoke', |
||||
|
})` |
||||
|
text-align: center; |
||||
|
` |
@ -0,0 +1,68 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import invariant from 'invariant' |
||||
|
import React, { PureComponent } from 'react' |
||||
|
|
||||
|
import TrackPage from 'analytics/TrackPage' |
||||
|
import Button from 'components/base/Button' |
||||
|
import Box from 'components/base/Box' |
||||
|
import Label from 'components/base/Label' |
||||
|
import CurrentAddressForAccount from 'components/CurrentAddressForAccount' |
||||
|
import RequestAmount from 'components/RequestAmount' |
||||
|
|
||||
|
import type { StepProps } from '../index' |
||||
|
|
||||
|
type State = { |
||||
|
amount: number, |
||||
|
} |
||||
|
|
||||
|
export default class StepReceiveFunds extends PureComponent<StepProps, State> { |
||||
|
state = { |
||||
|
amount: 0, |
||||
|
} |
||||
|
|
||||
|
handleChangeAmount = (amount: number) => this.setState({ amount }) |
||||
|
handleGoPrev = () => { |
||||
|
this.props.onChangeAppOpened(false) |
||||
|
this.props.onResetSkip() |
||||
|
this.props.transitionTo('device') |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { t, account, isAddressVerified } = this.props |
||||
|
const { amount } = this.state |
||||
|
invariant(account, 'No account given') |
||||
|
return ( |
||||
|
<Box flow={5}> |
||||
|
<TrackPage category="Receive" name="Step4" /> |
||||
|
<Box flow={1}> |
||||
|
<Label>{t('app:receive.steps.receiveFunds.label')}</Label> |
||||
|
<RequestAmount |
||||
|
account={account} |
||||
|
onChange={this.handleChangeAmount} |
||||
|
value={amount} |
||||
|
withMax={false} |
||||
|
/> |
||||
|
</Box> |
||||
|
<CurrentAddressForAccount |
||||
|
account={account} |
||||
|
addressVerified={isAddressVerified === true} |
||||
|
amount={amount} |
||||
|
onVerify={this.handleGoPrev} |
||||
|
withBadge |
||||
|
withFooter |
||||
|
withQRCode |
||||
|
withVerify={isAddressVerified !== true} |
||||
|
/> |
||||
|
</Box> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function StepReceiveFundsFooter({ t, closeModal }: StepProps) { |
||||
|
return ( |
||||
|
<Button primary onClick={closeModal}> |
||||
|
{t('app:common.close')} |
||||
|
</Button> |
||||
|
) |
||||
|
} |
@ -1,30 +1,32 @@ |
|||||
generic: Oops, an unknown error occurred. Please try again or contact Ledger Support. |
BtcUnmatchedApp: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.' |
||||
RangeError: '{{message}}' |
DeviceNotGenuine: Device is not genuine |
||||
|
DeviceSocketFail: Oops, device connection failed. Please try again. [device-fail] |
||||
|
DeviceSocketNoBulkStatus: Oops, device connection failed. Please try again [bulk]. |
||||
|
DeviceSocketNoHandler: Oops, device connection failed (handler {{query}}). Please try again. |
||||
|
DisconnectedDevice: 'The device was disconnected.' |
||||
Error: '{{message}}' |
Error: '{{message}}' |
||||
LedgerAPIErrorWithMessage: '{{message}}' |
|
||||
TransportStatusError: '{{message}}' |
|
||||
TimeoutError: 'The request timed out.' |
|
||||
FeeEstimationFailed: 'Fee estimation error. Try again or set a custom fee (status: {{status}})' |
FeeEstimationFailed: 'Fee estimation error. Try again or set a custom fee (status: {{status}})' |
||||
NotEnoughBalance: 'Insufficient funds to proceed.' |
generic: Oops, an unknown error occurred. Please try again or contact Ledger Support. |
||||
BtcUnmatchedApp: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.' |
HardResetFail: Reset failed. Please try again. |
||||
WrongAppOpened: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.' |
LatestMCUInstalledError: MCU on device already up to date. |
||||
WrongDeviceForAccount: 'Use the device associated with the account ‘{{accountName}}’.' |
|
||||
LedgerAPINotAvailable: 'Ledger API not available for {{currencyName}}.' |
|
||||
LedgerAPIError: 'Ledger API error. Try again. (HTTP {{status}})' |
LedgerAPIError: 'Ledger API error. Try again. (HTTP {{status}})' |
||||
|
LedgerAPIErrorWithMessage: '{{message}}' |
||||
|
LedgerAPINotAvailable: 'Ledger API not available for {{currencyName}}.' |
||||
|
ManagerAPIsFail: Services are unavailable. Please try again. |
||||
|
ManagerAppAlreadyInstalled: App is already installed |
||||
|
ManagerAppRelyOnBTC: You must install Bitcoin application first |
||||
|
ManagerDeviceLocked: Device is locked |
||||
|
ManagerNotEnoughSpace: Not enough storage on device. Uninstall some apps and try again. |
||||
|
ManagerUnexpected: Unexpected error occurred ({{msg}}). Please try again. |
||||
|
ManagerUninstallBTCDep: You must uninstall other altcoins first |
||||
NetworkDown: 'Your internet connection seems down.' |
NetworkDown: 'Your internet connection seems down.' |
||||
NoAddressesFound: 'No accounts were found.' |
NoAddressesFound: 'No accounts were found.' |
||||
|
NotEnoughBalance: 'Insufficient funds to proceed.' |
||||
|
RangeError: '{{message}}' |
||||
|
TimeoutError: 'The request timed out.' |
||||
|
TransportStatusError: '{{message}}' |
||||
UserRefusedOnDevice: Transaction refused on device. |
UserRefusedOnDevice: Transaction refused on device. |
||||
WebsocketConnectionError: Oops, device connection failed. Please try again. [web-err] |
WebsocketConnectionError: Oops, device connection failed. Please try again. [web-err] |
||||
WebsocketConnectionFailed: Oops, device connection failed. Please try again. [web-fail] |
WebsocketConnectionFailed: Oops, device connection failed. Please try again. [web-fail] |
||||
DeviceSocketFail: Oops, device connection failed. Please try again. [device-fail] |
WrongAppOpened: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.' |
||||
DeviceSocketNoBulkStatus: Oops, device connection failed. Please try again [bulk]. |
WrongDeviceForAccount: 'Use the device associated with the account ‘{{accountName}}’.' |
||||
DeviceSocketNoHandler: Oops, device connection failed (handler {{query}}). Please try again. |
|
||||
LatestMCUInstalledError: MCU on device already up to date. |
|
||||
HardResetFail: Reset failed. Please try again. |
|
||||
ManagerAPIsFail: Services are unavailable. Please try again. |
|
||||
ManagerUnexpected: Unexpected error occurred ({{msg}}). Please try again. |
|
||||
ManagerNotEnoughSpace: Not enough storage on device. Uninstall some apps and try again. |
|
||||
ManagerDeviceLocked: Device is locked |
|
||||
ManagerAppAlreadyInstalled: App is already installed |
|
||||
ManagerAppRelyOnBTC: First install the Bitcoin app |
|
||||
ManagerUninstallBTCDep: Other apps depend on Bitcoin, uninstall those first |
|
||||
|
Loading…
Reference in new issue