diff --git a/src/components/DeviceConnect/index.js b/src/components/DeviceConnect/index.js index 13b7d5d7..c693625f 100644 --- a/src/components/DeviceConnect/index.js +++ b/src/components/DeviceConnect/index.js @@ -17,6 +17,7 @@ import IconExclamationCircle from 'icons/ExclamationCircle' import IconInfoCircle from 'icons/InfoCircle' import IconLoader from 'icons/Loader' import IconUsb from 'icons/Usb' +import IconHome from 'icons/Home' import * as IconDevice from 'icons/device' @@ -33,12 +34,14 @@ const Step = styled(Box).attrs({ ? 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, @@ -138,6 +141,8 @@ StepCheck.defaultProps = { type Props = { accountName: null | string, appOpened: null | 'success' | 'fail', + genuineCheckStatus: null | 'success' | 'fail', + withGenuineCheck: boolean, currency: CryptoCurrency, devices: Device[], deviceSelected: ?Device, @@ -161,6 +166,7 @@ class DeviceConnect extends PureComponent { devices: [], deviceSelected: null, onChangeDevice: noop, + withGenuineCheck: false, } componentDidMount() { @@ -171,18 +177,17 @@ class DeviceConnect extends PureComponent { emitChangeDevice(nextProps) } - getAppState() { - const { appOpened } = this.props - - return { - success: appOpened === 'success', - fail: appOpened === 'fail', - } - } + getStepState = stepStatus => ({ + success: stepStatus === 'success', + fail: stepStatus === 'fail', + }) render() { const { deviceSelected, + genuineCheckStatus, + withGenuineCheck, + appOpened, errorMessage, accountName, currency, @@ -191,7 +196,8 @@ class DeviceConnect extends PureComponent { devices, } = this.props - const appState = this.getAppState() + const appState = this.getStepState(appOpened) + const genuineCheckState = this.getStepState(genuineCheckStatus) const hasDevice = devices.length > 0 const hasMultipleDevices = devices.length > 1 @@ -242,24 +248,66 @@ class DeviceConnect extends PureComponent { )} + - - - - - - - - - {'Open '} - {currency.name} - {' App on your device'} - - - - + {currency ? ( + + + + + + + + + {'Open '} + {currency.name} + {' App on your device'} + + + + + ) : ( + + + + + + + + + {'Go to the '} + {'dashboard'} + {' on your device'} + + + + + )} + {/* GENUINE CHECK */} + {/* ------------- */} + + {withGenuineCheck && ( + + + + + + + + + + {'Confirm '} + {'authentication'} + {' on your device'} + + + + + + )} + {appState.fail ? ( diff --git a/src/components/EnsureDeviceApp/index.js b/src/components/EnsureDeviceApp/index.js index 0a5addea..f765f392 100644 --- a/src/components/EnsureDeviceApp/index.js +++ b/src/components/EnsureDeviceApp/index.js @@ -1,5 +1,4 @@ // @flow -import invariant from 'invariant' import { PureComponent } from 'react' import { connect } from 'react-redux' import { standardDerivation } from 'helpers/derivations' @@ -14,8 +13,10 @@ import getAddress from 'commands/getAddress' type OwnProps = { currency: ?CryptoCurrency, deviceSelected: ?Device, + withGenuineCheck: boolean, account: ?Account, onStatusChange?: (DeviceStatus, AppStatus, ?string) => void, + onGenuineCheck: (isGenuine: boolean) => void, // TODO prefer children function render?: ({ appStatus: AppStatus, @@ -87,19 +88,21 @@ class EnsureDeviceApp extends PureComponent { componentWillUnmount() { clearTimeout(this._timeout) + this._unmounted = true } checkAppOpened = async () => { - const { deviceSelected, account, currency } = this.props + const { deviceSelected, account, currency, withGenuineCheck } = this.props + const { appStatus } = this.state if (!deviceSelected) { return } - let options + let appOptions if (account) { - options = { + appOptions = { devicePath: deviceSelected.path, currencyId: account.currency.id, path: account.path, @@ -107,21 +110,31 @@ class EnsureDeviceApp extends PureComponent { segwit: account.path.startsWith("49'"), // TODO: store segwit info in account } } else if (currency) { - options = { + appOptions = { devicePath: deviceSelected.path, currencyId: currency.id, path: standardDerivation({ currency, x: 0, segwit: false }), } - } else { - throw new Error('either currency or account is required') } try { - const { address } = await getAddress.send(options).toPromise() - if (account && account.address !== address) { - throw new Error('Account address is different than device address') + if (appOptions) { + const { address } = await getAddress.send(appOptions).toPromise() + 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') + + if (withGenuineCheck && appStatus !== 'success') { + this.handleGenuineCheck() + } } catch (e) { this.handleStatusChange(this.state.deviceStatus, 'fail', e.message) } @@ -130,27 +143,42 @@ class EnsureDeviceApp extends PureComponent { } _timeout: * + _unmounted = false handleStatusChange = (deviceStatus, appStatus, errorMessage = null) => { const { onStatusChange } = this.props clearTimeout(this._timeout) - this.setState({ deviceStatus, appStatus, errorMessage }) - onStatusChange && onStatusChange(deviceStatus, appStatus, errorMessage) + if (!this._unmounted) { + 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() { const { currency, account, devices, deviceSelected, render } = this.props - const { appStatus, deviceStatus, errorMessage } = this.state + const { appStatus, deviceStatus, genuineCheckStatus, errorMessage } = this.state 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 - invariant(cur, 'currency is either provided or taken from account') + return render({ appStatus, currency: cur, devices, deviceSelected: deviceStatus === 'connected' ? deviceSelected : null, deviceStatus, + genuineCheckStatus, errorMessage, }) } @@ -160,3 +188,7 @@ class EnsureDeviceApp extends PureComponent { } export default connect(mapStateToProps)(EnsureDeviceApp) + +async function sleep(s) { + return new Promise(resolve => setTimeout(resolve, s * 1e3)) +} diff --git a/src/components/GenuineCheckModal/index.js b/src/components/GenuineCheckModal/index.js new file mode 100644 index 00000000..b80fe0d9 --- /dev/null +++ b/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 { + 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 ( + + {t('genuinecheck:modal.title')} + + { + console.log(`status changed to ${status}`) + }} + render={({ appStatus, genuineCheckStatus, deviceSelected, errorMessage }) => ( + + )} + /> + + + ) + } + + render() { + const { ...props } = this.props + return this.renderBody({ onClose })} /> + } +} + +export default compose(connect(mapStateToProps), translate())(GenuineCheck) diff --git a/src/components/Onboarding/steps/GenuineCheck.js b/src/components/Onboarding/steps/GenuineCheck.js index f2ca39e2..907470d6 100644 --- a/src/components/Onboarding/steps/GenuineCheck.js +++ b/src/components/Onboarding/steps/GenuineCheck.js @@ -12,8 +12,11 @@ import { setGenuineCheckFail } from 'reducers/onboarding' import Box, { Card } from 'components/base/Box' import Button from 'components/base/Button' import RadioGroup from 'components/base/RadioGroup' +import GenuineCheckModal from 'components/GenuineCheckModal' + import IconLedgerNanoError from 'icons/onboarding/LedgerNanoError' import IconLedgerBlueError from 'icons/onboarding/LedgerBlueError' +import IconCheck from 'icons/Check' import { Title, Description, IconOptionRow } from '../helperComponents' @@ -27,6 +30,8 @@ type State = { phraseStepPass: boolean | null, cachedPinStepButton: string, cachedPhraseStepButton: string, + isGenuineCheckModalOpened: boolean, + isDeviceGenuine: boolean, } class GenuineCheck extends PureComponent { @@ -35,6 +40,8 @@ class GenuineCheck extends PureComponent { phraseStepPass: null, cachedPinStepButton: '', cachedPhraseStepButton: '', + isGenuineCheckModalOpened: false, + isDeviceGenuine: false, } getButtonLabel() { @@ -66,12 +73,23 @@ class GenuineCheck extends PureComponent { } } + 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 = () => { this.props.setGenuineCheckFail(false) } + contactSupport = () => { console.log('contact support coming later') } + renderGenuineFail = () => ( { render() { 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) { return this.renderGenuineFail() @@ -138,8 +163,20 @@ class GenuineCheck extends PureComponent { {t('onboarding:genuineCheck.steps.step3.desc')} - @@ -153,6 +190,11 @@ class GenuineCheck extends PureComponent { nextStep={nextStep} prevStep={prevStep} /> + ) } diff --git a/src/icons/Home.js b/src/icons/Home.js new file mode 100644 index 00000000..72768838 --- /dev/null +++ b/src/icons/Home.js @@ -0,0 +1,13 @@ +// @flow + +import React from 'react' + +const path = ( + +) + +export default ({ size, ...p }: { size: number }) => ( + + {path} + +) diff --git a/static/i18n/en/genuinecheck.yml b/static/i18n/en/genuinecheck.yml new file mode 100644 index 00000000..b6ecfcf5 --- /dev/null +++ b/static/i18n/en/genuinecheck.yml @@ -0,0 +1,2 @@ +modal: + title: Genuine check, bro diff --git a/static/i18n/en/onboarding.yml b/static/i18n/en/onboarding.yml index b4fabb67..ea52a172 100644 --- a/static/i18n/en/onboarding.yml +++ b/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 buttons: genuineCheck: Genuine check + tryAgain: Check again contactSupport: Contact Support errorPage: ledgerNano: