Browse Source

Skeleton for genuine check in onboarding

master
meriadec 7 years ago
parent
commit
13f8e69034
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 96
      src/components/DeviceConnect/index.js
  2. 60
      src/components/EnsureDeviceApp/index.js
  3. 71
      src/components/GenuineCheckModal/index.js
  4. 48
      src/components/Onboarding/steps/GenuineCheck.js
  5. 13
      src/icons/Home.js
  6. 2
      static/i18n/en/genuinecheck.yml
  7. 1
      static/i18n/en/onboarding.yml

96
src/components/DeviceConnect/index.js

@ -17,6 +17,7 @@ import IconExclamationCircle from 'icons/ExclamationCircle'
import IconInfoCircle from 'icons/InfoCircle' import IconInfoCircle from 'icons/InfoCircle'
import IconLoader from 'icons/Loader' import IconLoader from 'icons/Loader'
import IconUsb from 'icons/Usb' import IconUsb from 'icons/Usb'
import IconHome from 'icons/Home'
import * as IconDevice from 'icons/device' import * as IconDevice from 'icons/device'
@ -33,12 +34,14 @@ const Step = styled(Box).attrs({
? p.theme.colors.alertRed ? p.theme.colors.alertRed
: p.theme.colors.fog}; : p.theme.colors.fog};
` `
const StepIcon = styled(Box).attrs({ const StepIcon = styled(Box).attrs({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
})` })`
width: 64px; width: 64px;
` `
const StepContent = styled(Box).attrs({ const StepContent = styled(Box).attrs({
color: 'dark', color: 'dark',
horizontal: true, horizontal: true,
@ -138,6 +141,8 @@ StepCheck.defaultProps = {
type Props = { type Props = {
accountName: null | string, accountName: null | string,
appOpened: null | 'success' | 'fail', appOpened: null | 'success' | 'fail',
genuineCheckStatus: null | 'success' | 'fail',
withGenuineCheck: boolean,
currency: CryptoCurrency, currency: CryptoCurrency,
devices: Device[], devices: Device[],
deviceSelected: ?Device, deviceSelected: ?Device,
@ -161,6 +166,7 @@ class DeviceConnect extends PureComponent<Props> {
devices: [], devices: [],
deviceSelected: null, deviceSelected: null,
onChangeDevice: noop, onChangeDevice: noop,
withGenuineCheck: false,
} }
componentDidMount() { componentDidMount() {
@ -171,18 +177,17 @@ class DeviceConnect extends PureComponent<Props> {
emitChangeDevice(nextProps) emitChangeDevice(nextProps)
} }
getAppState() { getStepState = stepStatus => ({
const { appOpened } = this.props success: stepStatus === 'success',
fail: stepStatus === 'fail',
return { })
success: appOpened === 'success',
fail: appOpened === 'fail',
}
}
render() { render() {
const { const {
deviceSelected, deviceSelected,
genuineCheckStatus,
withGenuineCheck,
appOpened,
errorMessage, errorMessage,
accountName, accountName,
currency, currency,
@ -191,7 +196,8 @@ class DeviceConnect extends PureComponent<Props> {
devices, devices,
} = this.props } = this.props
const appState = this.getAppState() const appState = this.getStepState(appOpened)
const genuineCheckState = this.getStepState(genuineCheckStatus)
const hasDevice = devices.length > 0 const hasDevice = devices.length > 0
const hasMultipleDevices = devices.length > 1 const hasMultipleDevices = devices.length > 1
@ -242,24 +248,66 @@ class DeviceConnect extends PureComponent<Props> {
</ListDevices> </ListDevices>
)} )}
</Step> </Step>
<Step validated={appState.success} hasErrors={appState.fail}> <Step validated={appState.success} hasErrors={appState.fail}>
<StepContent> {currency ? (
<StepIcon> <StepContent>
<WrapperIconCurrency> <StepIcon>
<CryptoCurrencyIcon currency={currency} size={12} /> <WrapperIconCurrency>
</WrapperIconCurrency> <CryptoCurrencyIcon currency={currency} size={12} />
</StepIcon> </WrapperIconCurrency>
<Box grow shrink> </StepIcon>
<Trans i18nKey="deviceConnect:step2.open" parent="div"> <Box grow shrink>
{'Open '} <Trans i18nKey="deviceConnect:step2.open" parent="div">
<strong>{currency.name}</strong> {'Open '}
{' App on your device'} <strong>{currency.name}</strong>
</Trans> {' App on your device'}
</Box> </Trans>
<StepCheck checked={appState.success} hasErrors={appState.fail} /> </Box>
</StepContent> <StepCheck checked={appState.success} hasErrors={appState.fail} />
</StepContent>
) : (
<StepContent>
<StepIcon>
<WrapperIconCurrency>
<IconHome size={12} />
</WrapperIconCurrency>
</StepIcon>
<Box grow shrink>
<Trans i18nKey="deviceConnect:dashboard.open" parent="div">
{'Go to the '}
<strong>{'dashboard'}</strong>
{' on your device'}
</Trans>
</Box>
<StepCheck checked={appState.success} hasErrors={appState.fail} />
</StepContent>
)}
</Step> </Step>
{/* GENUINE CHECK */}
{/* ------------- */}
{withGenuineCheck && (
<Step validated={genuineCheckState.success} hasErrors={genuineCheckState.fail}>
<StepContent>
<StepIcon>
<WrapperIconCurrency>
<IconCheck size={12} />
</WrapperIconCurrency>
</StepIcon>
<Box grow shrink>
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div">
{'Confirm '}
<strong>{'authentication'}</strong>
{' on your device'}
</Trans>
</Box>
<StepCheck checked={genuineCheckState.success} hasErrors={genuineCheckState.fail} />
</StepContent>
</Step>
)}
{appState.fail ? ( {appState.fail ? (
<Info hasErrors> <Info hasErrors>
<Box> <Box>

60
src/components/EnsureDeviceApp/index.js

@ -1,5 +1,4 @@
// @flow // @flow
import invariant from 'invariant'
import { PureComponent } from 'react' import { PureComponent } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { standardDerivation } from 'helpers/derivations' import { standardDerivation } from 'helpers/derivations'
@ -14,8 +13,10 @@ import getAddress from 'commands/getAddress'
type OwnProps = { type OwnProps = {
currency: ?CryptoCurrency, currency: ?CryptoCurrency,
deviceSelected: ?Device, deviceSelected: ?Device,
withGenuineCheck: boolean,
account: ?Account, account: ?Account,
onStatusChange?: (DeviceStatus, AppStatus, ?string) => void, onStatusChange?: (DeviceStatus, AppStatus, ?string) => void,
onGenuineCheck: (isGenuine: boolean) => void,
// TODO prefer children function // TODO prefer children function
render?: ({ render?: ({
appStatus: AppStatus, appStatus: AppStatus,
@ -87,19 +88,21 @@ class EnsureDeviceApp extends PureComponent<Props, State> {
componentWillUnmount() { componentWillUnmount() {
clearTimeout(this._timeout) clearTimeout(this._timeout)
this._unmounted = true
} }
checkAppOpened = async () => { checkAppOpened = async () => {
const { deviceSelected, account, currency } = this.props const { deviceSelected, account, currency, withGenuineCheck } = this.props
const { appStatus } = this.state
if (!deviceSelected) { if (!deviceSelected) {
return return
} }
let options let appOptions
if (account) { if (account) {
options = { appOptions = {
devicePath: deviceSelected.path, devicePath: deviceSelected.path,
currencyId: account.currency.id, currencyId: account.currency.id,
path: account.path, path: account.path,
@ -107,21 +110,31 @@ class EnsureDeviceApp extends PureComponent<Props, State> {
segwit: account.path.startsWith("49'"), // TODO: store segwit info in account segwit: account.path.startsWith("49'"), // TODO: store segwit info in account
} }
} else if (currency) { } else if (currency) {
options = { appOptions = {
devicePath: deviceSelected.path, devicePath: deviceSelected.path,
currencyId: currency.id, currencyId: currency.id,
path: standardDerivation({ currency, x: 0, segwit: false }), path: standardDerivation({ currency, x: 0, segwit: false }),
} }
} else {
throw new Error('either currency or account is required')
} }
try { try {
const { address } = await getAddress.send(options).toPromise() if (appOptions) {
if (account && account.address !== address) { const { address } = await getAddress.send(appOptions).toPromise()
throw new Error('Account address is different than device address') if (account && account.address !== address) {
throw new Error('Account address is different than device address')
}
} else {
// TODO: real check if user is on the device dashboard
if (!deviceSelected) {
throw new Error('No device')
}
await sleep(1)
} }
this.handleStatusChange(this.state.deviceStatus, 'success') this.handleStatusChange(this.state.deviceStatus, 'success')
if (withGenuineCheck && appStatus !== 'success') {
this.handleGenuineCheck()
}
} catch (e) { } catch (e) {
this.handleStatusChange(this.state.deviceStatus, 'fail', e.message) this.handleStatusChange(this.state.deviceStatus, 'fail', e.message)
} }
@ -130,27 +143,42 @@ class EnsureDeviceApp extends PureComponent<Props, State> {
} }
_timeout: * _timeout: *
_unmounted = false
handleStatusChange = (deviceStatus, appStatus, errorMessage = null) => { handleStatusChange = (deviceStatus, appStatus, errorMessage = null) => {
const { onStatusChange } = this.props const { onStatusChange } = this.props
clearTimeout(this._timeout) clearTimeout(this._timeout)
this.setState({ deviceStatus, appStatus, errorMessage }) if (!this._unmounted) {
onStatusChange && onStatusChange(deviceStatus, appStatus, errorMessage) this.setState({ deviceStatus, appStatus, errorMessage })
onStatusChange && onStatusChange(deviceStatus, appStatus, errorMessage)
}
}
handleGenuineCheck = async () => {
// TODO: do a *real* genuine check
await sleep(1)
if (!this._unmounted) {
this.setState({ genuineCheckStatus: 'success' })
this.props.onGenuineCheck(true)
}
} }
render() { render() {
const { currency, account, devices, deviceSelected, render } = this.props const { currency, account, devices, deviceSelected, render } = this.props
const { appStatus, deviceStatus, errorMessage } = this.state const { appStatus, deviceStatus, genuineCheckStatus, errorMessage } = this.state
if (render) { if (render) {
// if cur is not provided, we assume we want to check if user is on
// the dashboard
const cur = account ? account.currency : currency const cur = account ? account.currency : currency
invariant(cur, 'currency is either provided or taken from account')
return render({ return render({
appStatus, appStatus,
currency: cur, currency: cur,
devices, devices,
deviceSelected: deviceStatus === 'connected' ? deviceSelected : null, deviceSelected: deviceStatus === 'connected' ? deviceSelected : null,
deviceStatus, deviceStatus,
genuineCheckStatus,
errorMessage, errorMessage,
}) })
} }
@ -160,3 +188,7 @@ class EnsureDeviceApp extends PureComponent<Props, State> {
} }
export default connect(mapStateToProps)(EnsureDeviceApp) export default connect(mapStateToProps)(EnsureDeviceApp)
async function sleep(s) {
return new Promise(resolve => setTimeout(resolve, s * 1e3))
}

71
src/components/GenuineCheckModal/index.js

@ -0,0 +1,71 @@
// @flow
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import type { T, Device } from 'types/common'
import { getCurrentDevice } from 'reducers/devices'
import DeviceConnect from 'components/DeviceConnect'
import EnsureDeviceApp from 'components/EnsureDeviceApp'
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal'
const mapStateToProps = state => ({
currentDevice: getCurrentDevice(state),
})
type Props = {
t: T,
currentDevice: ?Device,
onGenuineCheck: (isGenuine: boolean) => void,
}
type State = {}
class GenuineCheck extends PureComponent<Props, State> {
renderBody = ({ onClose }) => {
const { t, currentDevice, onGenuineCheck } = 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
const reducedDevicesList = currentDevice ? [currentDevice] : []
return (
<ModalBody onClose={onClose}>
<ModalTitle>{t('genuinecheck:modal.title')}</ModalTitle>
<ModalContent>
<EnsureDeviceApp
deviceSelected={currentDevice}
withGenuineCheck
onGenuineCheck={onGenuineCheck}
onStatusChange={status => {
console.log(`status changed to ${status}`)
}}
render={({ appStatus, genuineCheckStatus, deviceSelected, errorMessage }) => (
<DeviceConnect
appOpened={
appStatus === 'success' ? 'success' : appStatus === 'fail' ? 'fail' : null
}
withGenuineCheck
genuineCheckStatus={genuineCheckStatus}
devices={reducedDevicesList}
deviceSelected={deviceSelected}
errorMessage={errorMessage}
/>
)}
/>
</ModalContent>
</ModalBody>
)
}
render() {
const { ...props } = this.props
return <Modal {...props} render={({ onClose }) => this.renderBody({ onClose })} />
}
}
export default compose(connect(mapStateToProps), translate())(GenuineCheck)

48
src/components/Onboarding/steps/GenuineCheck.js

@ -12,8 +12,11 @@ import { setGenuineCheckFail } from 'reducers/onboarding'
import Box, { Card } from 'components/base/Box' import Box, { Card } from 'components/base/Box'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import RadioGroup from 'components/base/RadioGroup' import RadioGroup from 'components/base/RadioGroup'
import GenuineCheckModal from 'components/GenuineCheckModal'
import IconLedgerNanoError from 'icons/onboarding/LedgerNanoError' import IconLedgerNanoError from 'icons/onboarding/LedgerNanoError'
import IconLedgerBlueError from 'icons/onboarding/LedgerBlueError' import IconLedgerBlueError from 'icons/onboarding/LedgerBlueError'
import IconCheck from 'icons/Check'
import { Title, Description, IconOptionRow } from '../helperComponents' import { Title, Description, IconOptionRow } from '../helperComponents'
@ -27,6 +30,8 @@ type State = {
phraseStepPass: boolean | null, phraseStepPass: boolean | null,
cachedPinStepButton: string, cachedPinStepButton: string,
cachedPhraseStepButton: string, cachedPhraseStepButton: string,
isGenuineCheckModalOpened: boolean,
isDeviceGenuine: boolean,
} }
class GenuineCheck extends PureComponent<StepProps, State> { class GenuineCheck extends PureComponent<StepProps, State> {
@ -35,6 +40,8 @@ class GenuineCheck extends PureComponent<StepProps, State> {
phraseStepPass: null, phraseStepPass: null,
cachedPinStepButton: '', cachedPinStepButton: '',
cachedPhraseStepButton: '', cachedPhraseStepButton: '',
isGenuineCheckModalOpened: false,
isDeviceGenuine: false,
} }
getButtonLabel() { getButtonLabel() {
@ -66,12 +73,23 @@ class GenuineCheck extends PureComponent<StepProps, State> {
} }
} }
handleOpenGenuineCheckModal = () => this.setState({ isGenuineCheckModalOpened: true })
handleCloseGenuineCheckModal = () => this.setState({ isGenuineCheckModalOpened: false })
handleGenuineCheck = async isGenuine => {
await new Promise(r => setTimeout(r, 1e3)) // let's wait a bit before closing modal
this.handleCloseGenuineCheckModal()
this.setState({ isDeviceGenuine: isGenuine })
}
redoGenuineCheck = () => { redoGenuineCheck = () => {
this.props.setGenuineCheckFail(false) this.props.setGenuineCheckFail(false)
} }
contactSupport = () => { contactSupport = () => {
console.log('contact support coming later') console.log('contact support coming later')
} }
renderGenuineFail = () => ( renderGenuineFail = () => (
<GenuineCheckFail <GenuineCheckFail
redoGenuineCheck={this.redoGenuineCheck} redoGenuineCheck={this.redoGenuineCheck}
@ -83,7 +101,14 @@ class GenuineCheck extends PureComponent<StepProps, State> {
render() { render() {
const { nextStep, prevStep, t, onboarding } = this.props const { nextStep, prevStep, t, onboarding } = this.props
const { pinStepPass, phraseStepPass, cachedPinStepButton, cachedPhraseStepButton } = this.state const {
pinStepPass,
phraseStepPass,
cachedPinStepButton,
cachedPhraseStepButton,
isGenuineCheckModalOpened,
isDeviceGenuine,
} = this.state
if (onboarding.isGenuineFail) { if (onboarding.isGenuineFail) {
return this.renderGenuineFail() return this.renderGenuineFail()
@ -138,8 +163,20 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<CardDescription>{t('onboarding:genuineCheck.steps.step3.desc')}</CardDescription> <CardDescription>{t('onboarding:genuineCheck.steps.step3.desc')}</CardDescription>
</Box> </Box>
<Box justify="center" horizontal mx={5}> <Box justify="center" horizontal mx={5}>
<Button big primary disabled={!phraseStepPass}> <Button
{t('onboarding:genuineCheck.buttons.genuineCheck')} big
primary
disabled={!phraseStepPass}
onClick={this.handleOpenGenuineCheckModal}
>
{isDeviceGenuine ? (
<Box horizontal align="center" flow={1}>
<IconCheck size={16} />
<span>{t('onboarding:genuineCheck.buttons.tryAgain')}</span>
</Box>
) : (
t('onboarding:genuineCheck.buttons.genuineCheck')
)}
</Button> </Button>
</Box> </Box>
</CardWrapper> </CardWrapper>
@ -153,6 +190,11 @@ class GenuineCheck extends PureComponent<StepProps, State> {
nextStep={nextStep} nextStep={nextStep}
prevStep={prevStep} prevStep={prevStep}
/> />
<GenuineCheckModal
isOpened={isGenuineCheckModalOpened}
onClose={this.handleCloseGenuineCheckModal}
onGenuineCheck={this.handleGenuineCheck}
/>
</Box> </Box>
) )
} }

13
src/icons/Home.js

@ -0,0 +1,13 @@
// @flow
import React from 'react'
const path = (
<path d="m8.261 2.252a1.11 1.11 0 0 0-1.408 0l-6.772 5.545a0.224 0.224 0 0 0-0.03 0.313l0.563 0.69a0.224 0.224 0 0 0 0.314 0.03l0.408-0.333v5.502c0 0.245 0.2 0.445 0.445 0.445h4.667c0.122 0 0.222-0.1 0.222-0.222v-3.556h1.778v3.556c0 0.122 0.1 0.222 0.222 0.222h4.666c0.245 0 0.445-0.2 0.445-0.445v-5.505l0.408 0.333a0.224 0.224 0 0 0 0.314-0.03l0.564-0.69a0.227 0.227 0 0 0-0.036-0.31l-6.771-5.545zm4.184 10.858h-2.667v-3.555c0-0.122-0.1-0.222-0.222-0.222h-4c-0.122 0-0.222 0.1-0.222 0.222v3.555h-2.667v-5.708l4.747-3.889c0.08-0.066 0.2-0.066 0.28 0l4.748 3.89v5.707z"/>
)
export default ({ size, ...p }: { size: number }) => (
<svg viewBox="0 0 15.111 12.444" height={size} width={size} {...p}>
{path}
</svg>
)

2
static/i18n/en/genuinecheck.yml

@ -0,0 +1,2 @@
modal:
title: Genuine check, bro

1
static/i18n/en/onboarding.yml

@ -67,6 +67,7 @@ genuineCheck:
desc: This is a long text, please replace it with the final wording once it’s done. Lorem ipsum dolor amet ledger lorem dolor desc: This is a long text, please replace it with the final wording once it’s done. Lorem ipsum dolor amet ledger lorem dolor
buttons: buttons:
genuineCheck: Genuine check genuineCheck: Genuine check
tryAgain: Check again
contactSupport: Contact Support contactSupport: Contact Support
errorPage: errorPage:
ledgerNano: ledgerNano:

Loading…
Cancel
Save