Browse Source

Implement new flow of receive

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

80
src/components/CurrentAddress/index.js

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

3
src/components/DeviceConfirm/index.js

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

2
src/components/modals/OperationDetails.js

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

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

@ -40,7 +40,6 @@ type State = {
isAppOpened: boolean, isAppOpened: boolean,
isAddressVerified: ?boolean, isAddressVerified: ?boolean,
disabledSteps: number[], disabledSteps: number[],
errorSteps: number[],
verifyAddressError: ?Error, verifyAddressError: ?Error,
} }
@ -76,15 +75,16 @@ const createSteps = ({ t }: { t: T }) => [
{ {
id: 'confirm', id: 'confirm',
label: t('app:receive.steps.confirmAddress.title'), label: t('app:receive.steps.confirmAddress.title'),
component: StepConfirmAddress,
footer: StepConfirmAddressFooter, footer: StepConfirmAddressFooter,
component: StepConfirmAddress,
onBack: ({ transitionTo }: StepProps) => transitionTo('device'),
shouldRenderFooter: ({ isAddressVerified }: StepProps) => isAddressVerified === false, shouldRenderFooter: ({ isAddressVerified }: StepProps) => isAddressVerified === false,
shouldPreventClose: ({ isAddressVerified }: StepProps) => isAddressVerified === null,
}, },
{ {
id: 'receive', id: 'receive',
label: t('app:receive.steps.receiveFunds.title'), label: t('app:receive.steps.receiveFunds.title'),
component: StepReceiveFunds, component: StepReceiveFunds,
shouldPreventClose: ({ isAddressVerified }: StepProps) => isAddressVerified === null,
}, },
] ]
@ -103,7 +103,6 @@ const INITIAL_STATE = {
isAppOpened: false, isAppOpened: false,
isAddressVerified: null, isAddressVerified: null,
disabledSteps: [], disabledSteps: [],
errorSteps: [],
verifyAddressError: null, 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 }) handleReset = () => this.setState({ ...INITIAL_STATE })
handleCloseModal = () => this.props.closeModal(MODAL_RECEIVE) handleCloseModal = () => this.props.closeModal(MODAL_RECEIVE)
handleStepChange = step => this.setState({ stepId: step.id }) handleStepChange = step => this.setState({ stepId: step.id })
handleChangeAccount = (account: ?Account) => this.setState({ account }) handleChangeAccount = (account: ?Account) => this.setState({ account })
handleChangeAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened }) handleChangeAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened })
handleChangeAddressVerified = (isAddressVerified: boolean, err: ?Error) => { handleChangeAddressVerified = (isAddressVerified: boolean, err: ?Error) => {
if (isAddressVerified) {
this.setState({ isAddressVerified, verifyAddressError: err }) 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],
})
}
}
} }
handleResetSkip = () => this.setState({ disabledSteps: [] }) handleResetSkip = () => this.setState({ disabledSteps: [] })
handleSkipConfirm = () => { handleSkipConfirm = () => {
const connectStepIndex = this.STEPS.findIndex(step => step.id === 'device') const connectStepIndex = this.STEPS.findIndex(step => step.id === 'device')
const confirmStepIndex = this.STEPS.findIndex(step => step.id === 'confirm') const confirmStepIndex = this.STEPS.findIndex(step => step.id === 'confirm')
if (confirmStepIndex > -1 && connectStepIndex > -1) { 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, isAppOpened,
isAddressVerified, isAddressVerified,
disabledSteps, disabledSteps,
errorSteps,
verifyAddressError, verifyAddressError,
} = this.state } = this.state
@ -183,6 +184,10 @@ class ReceiveModal extends PureComponent<Props, State> {
onChangeAddressVerified: this.handleChangeAddressVerified, onChangeAddressVerified: this.handleChangeAddressVerified,
} }
const errorSteps = verifyAddressError
? [verifyAddressError.name === 'UserRefusedAddress' ? 2 : 3]
: []
const isModalLocked = stepId === 'confirm' && isAddressVerified === null const isModalLocked = stepId === 'confirm' && isAddressVerified === null
return ( 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 styled from 'styled-components'
import React, { Fragment, PureComponent } from 'react' import React, { Fragment, PureComponent } from 'react'
import getAddress from 'commands/getAddress'
import { isSegwitAccount } from 'helpers/bip32'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import DeviceConfirm from 'components/DeviceConfirm' import DeviceConfirm from 'components/DeviceConfirm'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount'
import { WrongDeviceForAccount } from 'components/EnsureDeviceApp'
import type { StepProps } from '../index' import type { StepProps } from '../index'
import TranslatedError from '../../../TranslatedError' import TranslatedError from '../../../TranslatedError'
export default class StepConfirmAddress extends PureComponent<StepProps> { 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() { render() {
const { t, device, account, isAddressVerified, verifyAddressError } = this.props const { t, device, account, isAddressVerified, verifyAddressError, transitionTo } = this.props
invariant(account, 'No account given') invariant(account, 'No account given')
invariant(device, 'No device given') invariant(device, 'No device given')
return ( return (
@ -68,9 +32,13 @@ export default class StepConfirmAddress extends PureComponent<StepProps> {
) : ( ) : (
<Fragment> <Fragment>
<Title>{t('app:receive.steps.confirmAddress.action')}</Title> <Title>{t('app:receive.steps.confirmAddress.action')}</Title>
<Text>{t('app:receive.steps.confirmAddress.text')}</Text> <Text>
<CurrentAddressForAccount account={account} /> {t('app:receive.steps.confirmAddress.text', { currencyName: account.currency.name })}
<DeviceConfirm mb={2} mt={-1} error={isAddressVerified === false} /> </Text>
<Button mt={4} mb={2} primary onClick={() => transitionTo('receive')}>
{t('app:buttons.displayAddressOnDevice')}
</Button>
<DeviceConfirm withoutPushDisplay error={isAddressVerified === false} />
</Fragment> </Fragment>
)} )}
</Container> </Container>
@ -101,7 +69,8 @@ const Container = styled(Box).attrs({
alignItems: 'center', alignItems: 'center',
fontSize: 4, fontSize: 4,
color: 'dark', color: 'dark',
px: 7, px: 5,
mb: 2,
})`` })``
const Title = styled(Box).attrs({ const Title = styled(Box).attrs({

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

@ -4,24 +4,50 @@ import invariant from 'invariant'
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import TrackPage from 'analytics/TrackPage' import TrackPage from 'analytics/TrackPage'
import getAddress from 'commands/getAddress'
import { isSegwitAccount } from 'helpers/bip32'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Label from 'components/base/Label'
import CurrentAddressForAccount from 'components/CurrentAddressForAccount' 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 = { export default class StepReceiveFunds extends PureComponent<StepProps> {
amount: number, componentDidMount() {
if (this.props.isAddressVerified === null) {
this.confirmAddress()
}
} }
export default class StepReceiveFunds extends PureComponent<StepProps, State> { confirmAddress = async () => {
state = { const { account, device, onChangeAddressVerified, transitionTo } = this.props
amount: 0, 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)
this.props.transitionTo('confirm')
}
} }
handleChangeAmount = (amount: number) => this.setState({ amount })
handleGoPrev = () => { 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.onChangeAddressVerified(null)
this.props.onChangeAppOpened(false) this.props.onChangeAppOpened(false)
this.props.onResetSkip() this.props.onResetSkip()
@ -29,30 +55,18 @@ export default class StepReceiveFunds extends PureComponent<StepProps, State> {
} }
render() { render() {
const { t, account, isAddressVerified } = this.props const { account, isAddressVerified } = this.props
const { amount } = this.state
invariant(account, 'No account given') invariant(account, 'No account given')
return ( return (
<Box flow={5}> <Box flow={5}>
<TrackPage category="Receive" name="Step4" /> <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 <CurrentAddressForAccount
account={account} account={account}
addressVerified={isAddressVerified === true} isAddressVerified={isAddressVerified}
amount={amount}
onVerify={this.handleGoPrev} onVerify={this.handleGoPrev}
withBadge withBadge
withFooter withFooter
withQRCode withQRCode
withVerify={isAddressVerified !== true}
/> />
</Box> </Box>
) )

9
static/i18n/en/app.yml

@ -36,6 +36,7 @@ common:
reverify: Re-verify reverify: Re-verify
verify: Verify verify: Verify
copy: Copy copy: Copy
copyAddress: Copy address
copied: Copied! copied: Copied!
addressCopied: Address copied! addressCopied: Address copied!
lockScreen: lockScreen:
@ -54,6 +55,8 @@ common:
error: error:
load: Unable to load load: Unable to load
noResults: No results noResults: No results
buttons:
displayAddressOnDevice: Display address on device
operation: operation:
type: type:
IN: Received IN: Received
@ -122,7 +125,9 @@ dashboard:
currentAddress: currentAddress:
title: Current address title: Current address
for: Address for account <1><0>{{accountName}}</0></1> 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: deviceConnect:
step1: step1:
choose: "We detected {{count}} connected devices, please select one:" choose: "We detected {{count}} connected devices, please select one:"
@ -250,7 +255,7 @@ receive:
confirmAddress: confirmAddress:
title: Verification title: Verification
action: Confirm address on device 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 support: Contact us
receiveFunds: receiveFunds:
title: Receive title: Receive

Loading…
Cancel
Save