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... --> |
|||
|
|||
## Any background context and/or relevant tickets/issues you want to provide with? |
|||
### Type |
|||
|
|||
<!-- e.g. GitHub issue #45 --> |
|||
|
|||
## Short description on what this PR suppose to do? |
|||
<!-- e.g. Bug Fix, Feature, Code Quality Improvement, UI Polish... --> |
|||
|
|||
<!-- 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. |
|||
RangeError: '{{message}}' |
|||
BtcUnmatchedApp: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.' |
|||
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}}' |
|||
LedgerAPIErrorWithMessage: '{{message}}' |
|||
TransportStatusError: '{{message}}' |
|||
TimeoutError: 'The request timed out.' |
|||
FeeEstimationFailed: 'Fee estimation error. Try again or set a custom fee (status: {{status}})' |
|||
NotEnoughBalance: 'Insufficient funds to proceed.' |
|||
BtcUnmatchedApp: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.' |
|||
WrongAppOpened: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.' |
|||
WrongDeviceForAccount: 'Use the device associated with the account ‘{{accountName}}’.' |
|||
LedgerAPINotAvailable: 'Ledger API not available for {{currencyName}}.' |
|||
generic: Oops, an unknown error occurred. Please try again or contact Ledger Support. |
|||
HardResetFail: Reset failed. Please try again. |
|||
LatestMCUInstalledError: MCU on device already up to date. |
|||
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.' |
|||
NoAddressesFound: 'No accounts were found.' |
|||
NotEnoughBalance: 'Insufficient funds to proceed.' |
|||
RangeError: '{{message}}' |
|||
TimeoutError: 'The request timed out.' |
|||
TransportStatusError: '{{message}}' |
|||
UserRefusedOnDevice: Transaction refused on device. |
|||
WebsocketConnectionError: Oops, device connection failed. Please try again. [web-err] |
|||
WebsocketConnectionFailed: Oops, device connection failed. Please try again. [web-fail] |
|||
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. |
|||
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 |
|||
WrongAppOpened: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.' |
|||
WrongDeviceForAccount: 'Use the device associated with the account ‘{{accountName}}’.' |
|||
|
Loading…
Reference in new issue