Browse Source

Implement new flow of receive

master
Gaëtan Renaudeau 7 years ago
parent
commit
3bd6937e9c
  1. 140
      src/components/CurrentAddress/index.js
  2. 3
      src/components/DeviceConfirm/index.js
  3. 2
      src/components/modals/OperationDetails.js
  4. 47
      src/components/modals/Receive/index.js
  5. 51
      src/components/modals/Receive/steps/03-step-confirm-address.js
  6. 62
      src/components/modals/Receive/steps/04-step-receive-funds.js
  7. 9
      static/i18n/en/app.yml

140
src/components/CurrentAddress/index.js

@ -18,18 +18,17 @@ import QRCode from 'components/base/QRCode'
import IconRecheck from 'icons/Recover'
import IconCopy from 'icons/Copy'
import IconInfoCircle from 'icons/InfoCircle'
import IconShield from 'icons/Shield'
const Container = styled(Box).attrs({
borderRadius: 1,
alignItems: 'center',
bg: p =>
p.withQRCode ? (p.notValid ? rgba(p.theme.colors.alertRed, 0.02) : 'lightGrey') : 'transparent',
py: 4,
px: 5,
bg: p => (p.isAddressVerified === false ? rgba(p.theme.colors.alertRed, 0.02) : 'lightGrey'),
p: 6,
pb: 4,
})`
border: ${p => (p.notValid ? `1px dashed ${rgba(p.theme.colors.alertRed, 0.5)}` : 'none')};
border: ${p =>
p.isAddressVerified === false ? `1px dashed ${rgba(p.theme.colors.alertRed, 0.5)}` : 'none'};
`
const Address = styled(Box).attrs({
@ -46,6 +45,8 @@ const Address = styled(Box).attrs({
border: ${p => `1px dashed ${p.theme.colors.fog}`};
cursor: text;
user-select: text;
text-align: center;
min-width: 320px;
`
const CopyFeedback = styled(Box).attrs({
@ -66,7 +67,6 @@ const Label = styled(Box).attrs({
strong {
color: ${p => p.theme.colors.dark};
font-weight: 600;
border-bottom: 1px dashed ${p => p.theme.colors.dark};
}
`
@ -127,31 +127,17 @@ const FooterButton = ({
type Props = {
account: Account,
address: string,
amount?: number,
addressVerified?: boolean,
isAddressVerified?: boolean,
onCopy: () => void,
onPrint: () => void,
onShare: () => void,
onVerify: () => void,
t: T,
withBadge: boolean,
withFooter: boolean,
withQRCode: boolean,
withVerify: boolean,
}
class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
static defaultProps = {
addressVerified: null,
amount: null,
onCopy: noop,
onPrint: noop,
onShare: noop,
onVerify: noop,
withBadge: false,
withFooter: false,
withQRCode: false,
withVerify: false,
}
state = {
@ -164,38 +150,28 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
const {
account: { name: accountName, currency },
address,
addressVerified,
amount,
onCopy,
onPrint,
onShare,
onVerify,
isAddressVerified,
t,
withBadge,
withFooter,
withQRCode,
withVerify,
...props
} = this.props
const { copyFeedback } = this.state
const currencyName = currency.name
const notValid = addressVerified === false
const { copyFeedback } = this.state
return (
<Container withQRCode={withQRCode} notValid={notValid} {...props}>
{withQRCode && (
<Box mb={4}>
<QRCode
size={120}
data={encodeURIScheme({
address,
amount,
currency,
})}
/>
</Box>
)}
<Container isAddressVerified={isAddressVerified} {...props}>
<Box mb={4}>
<QRCode
size={120}
data={encodeURIScheme({
address,
currency,
})}
/>
</Box>
<Label>
<Box>
{accountName ? (
@ -207,48 +183,56 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
t('app:currentAddress.title')
)}
</Box>
<IconInfoCircle size={12} />
</Label>
<Address withQRCode={withQRCode} notValid={notValid}>
<Address>
{copyFeedback && <CopyFeedback>{t('app:common.addressCopied')}</CopyFeedback>}
{address}
</Address>
{withBadge && (
<Box horizontal flow={2} mt={2} alignItems="center">
<Box color={notValid ? 'alertRed' : 'wallet'}>
<IconShield height={32} width={28} />
</Box>
<Box shrink fontSize={12} color={notValid ? 'alertRed' : 'dark'} ff="Open Sans">
{t('app:currentAddress.message')}
</Box>
<Box horizontal flow={2} mt={2} alignItems="center" style={{ maxWidth: 320 }}>
<Box color={isAddressVerified === false ? 'alertRed' : 'wallet'}>
<IconShield height={32} width={28} />
</Box>
)}
{withFooter && (
<Footer>
<Box
shrink
fontSize={12}
color={isAddressVerified === false ? 'alertRed' : 'dark'}
ff="Open Sans"
>
{isAddressVerified === null
? t('app:currentAddress.messageIfUnverified', { currencyName })
: isAddressVerified
? t('app:currentAddress.messageIfAccepted', { currencyName })
: t('app:currentAddress.messageIfSkipped', { currencyName })}
</Box>
</Box>
<Footer>
{isAddressVerified !== null ? (
<FooterButton
icon={<IconRecheck size={16} />}
label={notValid ? t('app:common.verify') : t('app:common.reverify')}
label={
isAddressVerified === false ? t('app:common.verify') : t('app:common.reverify')
}
onClick={onVerify}
/>
<CopyToClipboard
data={address}
render={copy => (
<FooterButton
icon={<IconCopy size={16} />}
label={t('app:common.copy')}
onClick={() => {
this.setState({ copyFeedback: true })
setTimeout(() => {
if (this._isUnmounted) return
this.setState({ copyFeedback: false })
}, 1e3)
copy()
}}
/>
)}
/>
</Footer>
)}
) : null}
<CopyToClipboard
data={address}
render={copy => (
<FooterButton
icon={<IconCopy size={16} />}
label={t('app:common.copyAddress')}
onClick={() => {
this.setState({ copyFeedback: true })
setTimeout(() => {
if (this._isUnmounted) return
this.setState({ copyFeedback: false })
}, 1e3)
copy()
}}
/>
)}
/>
</Footer>
</Container>
)
}

3
src/components/DeviceConfirm/index.js

@ -75,6 +75,7 @@ const PushButton = styled(Box)`
type Props = {
error?: boolean,
withoutPushDisplay?: boolean,
}
const SVG = (
@ -165,7 +166,7 @@ const SVG = (
const DeviceConfirm = (props: Props) => (
<Wrapper {...props}>
{!props.error ? <PushButton /> : null}
{!props.error && !props.withoutPushDisplay ? <PushButton /> : null}
<Check error={props.error} />
{SVG}
</Wrapper>

2
src/components/modals/OperationDetails.js

@ -44,7 +44,7 @@ const OpDetailsTitle = styled(Box).attrs({
letter-spacing: 2px;
`
const GradientHover = styled(Box).attrs({
export const GradientHover = styled(Box).attrs({
align: 'center',
color: 'wallet',
})`

47
src/components/modals/Receive/index.js

@ -40,7 +40,6 @@ type State = {
isAppOpened: boolean,
isAddressVerified: ?boolean,
disabledSteps: number[],
errorSteps: number[],
verifyAddressError: ?Error,
}
@ -76,15 +75,16 @@ const createSteps = ({ t }: { t: T }) => [
{
id: 'confirm',
label: t('app:receive.steps.confirmAddress.title'),
component: StepConfirmAddress,
footer: StepConfirmAddressFooter,
component: StepConfirmAddress,
onBack: ({ transitionTo }: StepProps) => transitionTo('device'),
shouldRenderFooter: ({ isAddressVerified }: StepProps) => isAddressVerified === false,
shouldPreventClose: ({ isAddressVerified }: StepProps) => isAddressVerified === null,
},
{
id: 'receive',
label: t('app:receive.steps.receiveFunds.title'),
component: StepReceiveFunds,
shouldPreventClose: ({ isAddressVerified }: StepProps) => isAddressVerified === null,
},
]
@ -103,7 +103,6 @@ const INITIAL_STATE = {
isAppOpened: false,
isAddressVerified: null,
disabledSteps: [],
errorSteps: [],
verifyAddressError: null,
}
@ -124,35 +123,38 @@ class ReceiveModal extends PureComponent<Props, State> {
}
}
handleRetry = () => this.setState({ isAddressVerified: null, isAppOpened: false, errorSteps: [] })
handleRetry = () =>
this.setState({
verifyAddressError: null,
isAddressVerified: null,
isAppOpened: false,
})
handleReset = () => this.setState({ ...INITIAL_STATE })
handleCloseModal = () => this.props.closeModal(MODAL_RECEIVE)
handleStepChange = step => this.setState({ stepId: step.id })
handleChangeAccount = (account: ?Account) => this.setState({ account })
handleChangeAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened })
handleChangeAddressVerified = (isAddressVerified: boolean, err: ?Error) => {
if (isAddressVerified) {
this.setState({ isAddressVerified, verifyAddressError: err })
} else if (isAddressVerified === null) {
this.setState({ isAddressVerified: null, errorSteps: [], verifyAddressError: err })
} else {
const confirmStepIndex = this.STEPS.findIndex(step => step.id === 'confirm')
if (confirmStepIndex > -1) {
this.setState({
isAddressVerified,
verifyAddressError: err,
errorSteps: [confirmStepIndex],
})
}
}
this.setState({ isAddressVerified, verifyAddressError: err })
}
handleResetSkip = () => this.setState({ disabledSteps: [] })
handleSkipConfirm = () => {
const connectStepIndex = this.STEPS.findIndex(step => step.id === 'device')
const confirmStepIndex = this.STEPS.findIndex(step => step.id === 'confirm')
if (confirmStepIndex > -1 && connectStepIndex > -1) {
this.setState({ disabledSteps: [connectStepIndex, confirmStepIndex] })
this.setState({
isAddressVerified: false,
verifyAddressError: null,
disabledSteps: [connectStepIndex, confirmStepIndex],
})
}
}
@ -164,7 +166,6 @@ class ReceiveModal extends PureComponent<Props, State> {
isAppOpened,
isAddressVerified,
disabledSteps,
errorSteps,
verifyAddressError,
} = this.state
@ -183,6 +184,10 @@ class ReceiveModal extends PureComponent<Props, State> {
onChangeAddressVerified: this.handleChangeAddressVerified,
}
const errorSteps = verifyAddressError
? [verifyAddressError.name === 'UserRefusedAddress' ? 2 : 3]
: []
const isModalLocked = stepId === 'confirm' && isAddressVerified === null
return (

51
src/components/modals/Receive/steps/03-step-confirm-address.js

@ -4,52 +4,16 @@ 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'
import TranslatedError from '../../../TranslatedError'
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, err)
}
}
render() {
const { t, device, account, isAddressVerified, verifyAddressError } = this.props
const { t, device, account, isAddressVerified, verifyAddressError, transitionTo } = this.props
invariant(account, 'No account given')
invariant(device, 'No device given')
return (
@ -68,9 +32,13 @@ export default class StepConfirmAddress extends PureComponent<StepProps> {
) : (
<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} />
<Text>
{t('app:receive.steps.confirmAddress.text', { currencyName: account.currency.name })}
</Text>
<Button mt={4} mb={2} primary onClick={() => transitionTo('receive')}>
{t('app:buttons.displayAddressOnDevice')}
</Button>
<DeviceConfirm withoutPushDisplay error={isAddressVerified === false} />
</Fragment>
)}
</Container>
@ -101,7 +69,8 @@ const Container = styled(Box).attrs({
alignItems: 'center',
fontSize: 4,
color: 'dark',
px: 7,
px: 5,
mb: 2,
})``
const Title = styled(Box).attrs({

62
src/components/modals/Receive/steps/04-step-receive-funds.js

@ -4,24 +4,50 @@ import invariant from 'invariant'
import React, { PureComponent } from 'react'
import TrackPage from 'analytics/TrackPage'
import getAddress from 'commands/getAddress'
import { isSegwitAccount } from 'helpers/bip32'
import Box from 'components/base/Box'
import Label from 'components/base/Label'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import RequestAmount from 'components/RequestAmount'
import { WrongDeviceForAccount } from 'components/EnsureDeviceApp'
import type { StepProps } from '../index'
import type { StepProps } from '..'
type State = {
amount: number,
}
export default class StepReceiveFunds extends PureComponent<StepProps> {
componentDidMount() {
if (this.props.isAddressVerified === null) {
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()
export default class StepReceiveFunds extends PureComponent<StepProps, State> {
state = {
amount: 0,
if (address !== account.freshAddress) {
throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, {
accountName: account.name,
})
}
onChangeAddressVerified(true)
transitionTo('receive')
} catch (err) {
onChangeAddressVerified(false, err)
this.props.transitionTo('confirm')
}
}
handleChangeAmount = (amount: number) => this.setState({ amount })
handleGoPrev = () => {
// FIXME this is not a good practice at all. it triggers tons of setState. these are even concurrent setState potentially in future React :o
this.props.onChangeAddressVerified(null)
this.props.onChangeAppOpened(false)
this.props.onResetSkip()
@ -29,30 +55,18 @@ export default class StepReceiveFunds extends PureComponent<StepProps, State> {
}
render() {
const { t, account, isAddressVerified } = this.props
const { amount } = this.state
const { account, isAddressVerified } = this.props
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}
isAddressVerified={isAddressVerified}
onVerify={this.handleGoPrev}
withBadge
withFooter
withQRCode
withVerify={isAddressVerified !== true}
/>
</Box>
)

9
static/i18n/en/app.yml

@ -36,6 +36,7 @@ common:
reverify: Re-verify
verify: Verify
copy: Copy
copyAddress: Copy address
copied: Copied!
addressCopied: Address copied!
lockScreen:
@ -54,6 +55,8 @@ common:
error:
load: Unable to load
noResults: No results
buttons:
displayAddressOnDevice: Display address on device
operation:
type:
IN: Received
@ -122,7 +125,9 @@ dashboard:
currentAddress:
title: Current address
for: Address for account <1><0>{{accountName}}</0></1>
message: Your receive address has not been confirmed on your Ledger device. Please verify the address for optimal security.
messageIfUnverified: Please verify the address for optimal security.
messageIfAccepted: You can now use your {{currencyName}} address if it matches the one displayed on your Ledger device.
messageIfSkipped: Your receive address has not been confirmed on your Ledger device. Please verify your {{currencyName}} address for optimal security.
deviceConnect:
step1:
choose: "We detected {{count}} connected devices, please select one:"
@ -250,7 +255,7 @@ receive:
confirmAddress:
title: Verification
action: Confirm address on device
text: Verify that the address below matches the address displayed on your device
text: To receive cryptoassets, confirm the address on your device. Click the button below to reveal your {{currencyName}} address.
support: Contact us
receiveFunds:
title: Receive

Loading…
Cancel
Save