From 4b10d545bde9cde3422a78cc280b16b6123ce89e Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 24 Jun 2018 19:59:41 +0200 Subject: [PATCH 01/33] Create DeviceInteraction component --- .../DeviceInteractionStep.js | 223 ++++++++++++++++++ .../DeviceInteraction/components.js | 153 ++++++++++++ src/components/DeviceInteraction/index.js | 120 ++++++++++ src/components/DeviceInteraction/stories.js | 81 +++++++ 4 files changed, 577 insertions(+) create mode 100644 src/components/DeviceInteraction/DeviceInteractionStep.js create mode 100644 src/components/DeviceInteraction/components.js create mode 100644 src/components/DeviceInteraction/index.js create mode 100644 src/components/DeviceInteraction/stories.js diff --git a/src/components/DeviceInteraction/DeviceInteractionStep.js b/src/components/DeviceInteraction/DeviceInteractionStep.js new file mode 100644 index 00000000..a1ff3a53 --- /dev/null +++ b/src/components/DeviceInteraction/DeviceInteractionStep.js @@ -0,0 +1,223 @@ +// @flow + +import React, { PureComponent } from 'react' + +import Box from 'components/base/Box' +import { delay } from 'helpers/promise' + +import { + DeviceInteractionStepContainer, + SpinnerContainer, + IconContainer, + SuccessContainer, + ErrorDescContainer, + ErrorContainer, +} from './components' + +export type Step = { + id: string, + title?: React$Node | (Object => React$Node), + desc?: React$Node, + icon: React$Node, + run?: Object => Promise | { promise: Promise, unsubscribe: void => any }, + render?: ( + { onSuccess: Object => any, onFail: Error => void, onRetry: void => void }, + any, + ) => React$Node, + minMs?: number, +} + +type Status = 'idle' | 'running' + +type Props = { + isFirst: boolean, + isLast: boolean, + isActive: boolean, + isPrecedentActive: boolean, + isError: boolean, + isSuccess: boolean, + isPassed: boolean, + step: Step, + error: ?Error, + onSuccess: (any, Step) => any, + onFail: (Error, Step) => any, + onRetry: void => any, + data: any, +} + +class DeviceInteractionStep extends PureComponent< + Props, + { + status: Status, + }, +> { + static defaultProps = { + data: {}, + } + + constructor(props: Props) { + super(props) + const { isFirst } = this.props + if (isFirst) { + // cf: __IS_MOUNTED__THX_FOR_REMOVING_COMPONENTWILLMOUNT__ + this.state.status = 'running' + + this.run() + } + } + + state = { + status: 'idle', + } + + componentDidMount() { + this.__IS_MOUNTED__THX_FOR_REMOVING_COMPONENTWILLMOUNT__ = true + } + + componentDidUpdate(prevProps: Props) { + const { isActive, error } = this.props + const { status } = this.state + + const didActivated = isActive && !prevProps.isActive + const didDeactivated = !isActive && prevProps.isActive + const stillActivated = isActive && prevProps.isActive + const didResetError = !error && !!prevProps.error + + 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 + } + + __IS_MOUNTED__THX_FOR_REMOVING_COMPONENTWILLMOUNT__ = false + _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 + + if (this.__IS_MOUNTED__THX_FOR_REMOVING_COMPONENTWILLMOUNT__) { + 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, + isPrecedentActive, + isSuccess, + isError, + isPassed, + step, + error, + onRetry, + 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 ( + + {step.icon} + + {title && ( + + {title} + + )} + {step.desc && step.desc} + {CustomRender && ( + + )} + {isError && error && } + + +
+ + + +
+
+ ) + } +} + +export default DeviceInteractionStep diff --git a/src/components/DeviceInteraction/components.js b/src/components/DeviceInteraction/components.js new file mode 100644 index 00000000..bd673d9b --- /dev/null +++ b/src/components/DeviceInteraction/components.js @@ -0,0 +1,153 @@ +// @flow + +import React from 'react' +import styled from 'styled-components' +import Tooltip from 'components/base/Tooltip' + +import { radii, colors } from 'styles/theme' +import { rgba } from 'styles/helpers' + +import Box from 'components/base/Box' +import Spinner from 'components/base/Spinner' +import IconCheck from 'icons/Check' +import IconCross from 'icons/Cross' +import IconRecover from 'icons/Recover' + +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.isSuccess ? 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'}; + + &:after { + content: ''; + position: absolute; + left: -2px; + top: 0; + bottom: 0; + width: 2px; + box-shadow: ${p => + p.isActive && !p.isSuccess + ? `${p.theme.colors[p.isError ? 'alertRed' : 'wallet']} 2px 0 0` + : 'none'}; + } +` + +export const IconContainer = ({ children }: { children: any }) => ( + + {children} + +) + +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 }) => ( + + + +) + +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 }) => ( + + + +) + +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 }) => ( + + + +) + +const ErrorRetryContainer = styled(Box).attrs({ + grow: 1, + color: 'alertRed', + cursor: 'pointer', + p: 1, + align: 'center', + justify: 'center', + overflow: 'hidden', +})` + &:hover { + background-color: ${() => rgba(colors.alertRed, 0.1)}; + } + &:active { + background-color: ${() => rgba(colors.alertRed, 0.15)}; + } +` + +export const ErrorDescContainer = ({ + error, + onRetry, + ...p +}: { + error: Error, + onRetry: void => void, +}) => ( + + + + {error.message || 'Failed'} + + 'Retry'} style={{ display: 'flex', alignItems: 'center' }}> + + + + + + +) diff --git a/src/components/DeviceInteraction/index.js b/src/components/DeviceInteraction/index.js new file mode 100644 index 00000000..2375ebbf --- /dev/null +++ b/src/components/DeviceInteraction/index.js @@ -0,0 +1,120 @@ +// @flow + +import React, { PureComponent } from 'react' +import styled from 'styled-components' + +import { delay } from 'helpers/promise' + +import Box from 'components/base/Box' +import DeviceInteractionStep from './DeviceInteractionStep' + +import type { Step } from './DeviceInteractionStep' + +const INITIAL_STATE = { + stepIndex: 0, + isSuccess: false, + showSuccess: false, + error: null, + data: {}, +} + +class DeviceInteraction extends PureComponent< + { + steps: Step[], + onSuccess?: any => void, + onFail?: any => void, + renderSuccess?: any => any, + waitBeforeSuccess?: number, + }, + { + stepIndex: number, + isSuccess: boolean, + // used to be able to display the last check for a small amount of time + showSuccess: boolean, + error: ?Error, + data: Object, + }, +> { + 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, showSuccess: !waitBeforeSuccess }) + if (waitBeforeSuccess) { + await delay(waitBeforeSuccess) + if (this._unmounted) return + onSuccess && onSuccess(data) + this.setState({ showSuccess: true }) + } + } 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, renderSuccess, waitBeforeSuccess: _waitBeforeSuccess, ...props } = this.props + const { stepIndex, error, isSuccess, data, showSuccess } = this.state + + return ( + + {isSuccess && showSuccess && renderSuccess + ? renderSuccess(data) + : steps.map((step, i) => { + const isError = !!error && i === stepIndex + return ( + + ) + })} + + ) + } +} + +const DeviceInteractionContainer = styled(Box).attrs({})`` + +export default DeviceInteraction diff --git a/src/components/DeviceInteraction/stories.js b/src/components/DeviceInteraction/stories.js new file mode 100644 index 00000000..e111336b --- /dev/null +++ b/src/components/DeviceInteraction/stories.js @@ -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', () => ) + +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 = + +class Wrapper extends React.Component { + _ref = null + handleReset = () => this._ref && this._ref.reset() + render() { + return ( + + + (this._ref = n)} + steps={[ + { + id: 'deviceConnect', + title: 'Connect your device', + icon: , + desc: 'If you dont connect your device, we wont be able to read on it', + render: ({ onSuccess, onFail }) => ( + + + + + + + + ), + }, + { + 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')} + /> + + ) + } +} From d4283dd8c731df8839420c4c3435cd355ad71422 Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 24 Jun 2018 20:04:46 +0200 Subject: [PATCH 02/33] Create GenuineCheck component --- src/components/GenuineCheck.js | 148 +++++++++++++++++++++++++++++++++ src/config/constants.js | 1 + src/helpers/promise.js | 24 ++++++ static/i18n/en/errors.yml | 2 + 4 files changed, 175 insertions(+) create mode 100644 src/components/GenuineCheck.js diff --git a/src/components/GenuineCheck.js b/src/components/GenuineCheck.js new file mode 100644 index 00000000..56442003 --- /dev/null +++ b/src/components/GenuineCheck.js @@ -0,0 +1,148 @@ +// @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 = +const homeIcon = +const eyeIcon = + +const mapStateToProps = state => ({ + device: getCurrentDevice(state), +}) + +const Bold = props => + +class GenuineCheck extends PureComponent { + 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, + infos, + }: { + device: Device, + infos: DeviceInfo, + }) => { + const res = await getIsGenuine + .send({ + devicePath: device.path, + deviceInfo: infos, + }) + .pipe(timeout(GENUINE_TIMEOUT)) + .toPromise() + const isGenuine = res === '0000' + if (!isGenuine) { + return Promise.reject(new Error('Device not genuine')) // TODO: use custom error class + } + return Promise.resolve(true) + } + + handleFail = (err: Error) => { + const { onFail, onUnavailable } = this.props + if (err instanceof DeviceNotGenuineError) { + onFail(err) + } else { + onUnavailable(err) + } + } + + render() { + const { onSuccess } = this.props + const steps = [ + { + id: 'device', + title: ( + + {'Connect and unlock your '} + {'Ledger device'} + + ), + icon: usbIcon, + run: this.connectInteractionHandler, + }, + { + id: 'infos', + title: ( + + {'Navigate to the '} + {'dashboard'} + {' on your device'} + + ), + icon: homeIcon, + run: this.checkDashboardInteractionHandler, + }, + { + id: 'isGenuine', + title: ( + + {'Allow the '} + {'Ledger Manager'} + {' on your device'} + + ), + icon: eyeIcon, + run: this.checkGenuineInteractionHandler, + }, + ] + + return ( + + ) + } +} + +export default compose( + translate(), + connect(mapStateToProps), +)(GenuineCheck) diff --git a/src/config/constants.js b/src/config/constants.js index 587769dc..1efa4dbb 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -26,6 +26,7 @@ export const LISTEN_DEVICES_POLLING_INTERVAL = intFromEnv('LISTEN_DEVICES_POLLIN export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 1) export const SYNC_BOOT_DELAY = 2 * 1000 export const SYNC_ALL_INTERVAL = 120 * 1000 +export const DEVICE_INFOS_TIMEOUT = intFromEnv('DEVICE_INFOS_TIMEOUT', 5 * 1000) export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 120 * 1000) export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 30 * 1000) export const OUTDATED_CONSIDERED_DELAY = intFromEnv('OUTDATED_CONSIDERED_DELAY', 5 * 60 * 1000) diff --git a/src/helpers/promise.js b/src/helpers/promise.js index 09c2828b..7142c7ff 100644 --- a/src/helpers/promise.js +++ b/src/helpers/promise.js @@ -31,3 +31,27 @@ export function retry(f: () => Promise, options?: $Shape) export function idleCallback() { return new Promise(resolve => window.requestIdleCallback(resolve)) } + +export function createCancelablePolling( + job: any => Promise, + { pollingInterval = 500 }: { pollingInterval: number } = {}, +) { + let isUnsub = false + const unsubscribe = () => (isUnsub = true) + const getUnsub = () => isUnsub + const promise = new Promise(resolve => { + async function poll() { + try { + const res = await job() + if (getUnsub()) return + resolve(res) + } catch (err) { + await delay(pollingInterval) + if (getUnsub()) return + poll() + } + } + poll() + }) + return { unsubscribe, promise } +} diff --git a/static/i18n/en/errors.yml b/static/i18n/en/errors.yml index 640ff96d..8a6c1085 100644 --- a/static/i18n/en/errors.yml +++ b/static/i18n/en/errors.yml @@ -29,3 +29,5 @@ ManagerDeviceLocked: Device is locked ManagerAppAlreadyInstalled: App is already installed ManagerAppRelyOnBTC: You must install Bitcoin application first ManagerUninstallBTCDep: You must uninstall other altcoins first + +DeviceNotGenuine: Device is not genuine From 905c5998e664c156e6d33fe064bb868cf4fd0813 Mon Sep 17 00:00:00 2001 From: meriadec Date: Mon, 25 Jun 2018 16:50:41 +0200 Subject: [PATCH 03/33] Create EnsureDeviceAppInteraction --- src/components/EnsureDeviceAppInteraction.js | 118 +++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/components/EnsureDeviceAppInteraction.js diff --git a/src/components/EnsureDeviceAppInteraction.js b/src/components/EnsureDeviceAppInteraction.js new file mode 100644 index 00000000..fcc79996 --- /dev/null +++ b/src/components/EnsureDeviceAppInteraction.js @@ -0,0 +1,118 @@ +// @flow + +import React, { Component } from 'react' +import invariant from 'invariant' +import { connect } from 'react-redux' +import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types' +import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' + +import logger from 'logger' +import { createCancelablePolling } from 'helpers/promise' +import { standardDerivation } from 'helpers/derivations' +import { isSegwitAccount } from 'helpers/bip32' +import DeviceInteraction from 'components/DeviceInteraction' +import getAddress from 'commands/getAddress' +import IconUsb from 'icons/Usb' + +import type { Device } from 'types/common' + +import { createCustomErrorClass } from 'helpers/errors' +import { getCurrentDevice } from 'reducers/devices' + +export const WrongAppOpened = createCustomErrorClass('WrongAppOpened') +export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount') + +const usbIcon = + +const mapStateToProps = state => ({ + device: getCurrentDevice(state), +}) + +class EnsureDeviceAppInteraction extends Component<{ + device: ?Device, + account?: ?Account, + currency?: ?CryptoCurrency, +}> { + connectInteractionHandler = () => + createCancelablePolling(() => { + if (!this.props.device) return Promise.reject() + return Promise.resolve(this.props.device) + }) + + openAppInteractionHandler = ({ device }) => + createCancelablePolling(async () => { + const { account, currency } = this.props + const cur = account ? account.currency : currency + invariant(cur, 'No currency given') + const { address } = await getAddress + .send({ + devicePath: device.path, + currencyId: cur.id, + path: account + ? account.freshAddressPath + : standardDerivation({ currency: cur, segwit: false, x: 0 }), + segwit: account ? isSegwitAccount(account) : false, + }) + .toPromise() + .catch(e => { + if ( + e && + (e.name === 'TransportStatusError' || + // we don't want these error to appear (caused by usb disconnect..) + e.message === 'could not read from HID device' || + e.message === 'Cannot write to HID device') + ) { + throw new WrongAppOpened(`WrongAppOpened ${cur.id}`, { currencyName: cur.name }) + } + throw e + }) + + if (account) { + const { freshAddress } = account + if (account && freshAddress !== address) { + logger.warn({ freshAddress, address }) + throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, { + accountName: account.name, + }) + } + } + return address + }) + + renderOpenAppTitle = ({ device }) => { + const { account, currency } = this.props + const cur = account ? account.currency : currency + invariant(cur, 'No currency given') + return `Open the ${cur.name} app on your ${device ? `${device.product} ` : 'device'}` + } + + render() { + const { account, currency, ...props } = this.props + const cur = account ? account.currency : currency + const Icon = cur ? getCryptoCurrencyIcon(cur) : null + return ( + : null, + run: this.openAppInteractionHandler, + }, + ]} + {...props} + /> + ) + } +} + +export default connect(mapStateToProps)(EnsureDeviceAppInteraction) From 423794294977680346946968729d3db48804f09d Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 24 Jun 2018 20:19:37 +0200 Subject: [PATCH 04/33] Use GenuineCheck in GenuineCheckModal --- src/components/GenuineCheckModal.js | 37 ++++++++ src/components/GenuineCheckModal/index.js | 90 ------------------- .../Onboarding/steps/GenuineCheck/index.js | 6 +- 3 files changed, 40 insertions(+), 93 deletions(-) create mode 100644 src/components/GenuineCheckModal.js delete mode 100644 src/components/GenuineCheckModal/index.js diff --git a/src/components/GenuineCheckModal.js b/src/components/GenuineCheckModal.js new file mode 100644 index 00000000..d36c46d7 --- /dev/null +++ b/src/components/GenuineCheckModal.js @@ -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 { + renderBody = ({ onClose }) => { + const { t, onSuccess, onFail, onUnavailable } = this.props + return ( + + {t('app:genuinecheck.modal.title')} + + + + + ) + } + + render() { + const { t, ...props } = this.props + return + } +} + +export default translate()(GenuineCheckModal) diff --git a/src/components/GenuineCheckModal/index.js b/src/components/GenuineCheckModal/index.js deleted file mode 100644 index 8afe0b3b..00000000 --- a/src/components/GenuineCheckModal/index.js +++ /dev/null @@ -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 { - 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 ( - - {t('app:genuinecheck.modal.title')} - - ( - - - - - )} - /> - - - ) - } - - render() { - const { ...props } = this.props - return this.renderBody({ onClose })} /> - } -} - -export default translate()(GenuineCheck) diff --git a/src/components/Onboarding/steps/GenuineCheck/index.js b/src/components/Onboarding/steps/GenuineCheck/index.js index d00ce863..2516d65c 100644 --- a/src/components/Onboarding/steps/GenuineCheck/index.js +++ b/src/components/Onboarding/steps/GenuineCheck/index.js @@ -263,9 +263,9 @@ class GenuineCheck extends PureComponent { ) From 255ebd7d4b24d342691fec808a60851ce6679870 Mon Sep 17 00:00:00 2001 From: meriadec Date: Mon, 25 Jun 2018 16:50:54 +0200 Subject: [PATCH 05/33] Use EnsureDeviceAppInteraction in StepConnectDevice --- src/components/modals/StepConnectDevice.js | 45 ++++++++-------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/src/components/modals/StepConnectDevice.js b/src/components/modals/StepConnectDevice.js index ef9b7142..c6ed57ff 100644 --- a/src/components/modals/StepConnectDevice.js +++ b/src/components/modals/StepConnectDevice.js @@ -5,40 +5,29 @@ import React from 'react' import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types' import type { Device } from 'types/common' -import DeviceConnect from 'components/DeviceConnect' -import EnsureDeviceApp from 'components/EnsureDeviceApp' +import EnsureDeviceAppInteraction from 'components/EnsureDeviceAppInteraction' type Props = { account?: ?Account, currency?: ?CryptoCurrency, - deviceSelected?: ?Device, onChangeDevice?: Device => void, - onStatusChange: string => void, + onStatusChange: (string, string) => void, } -const StepConnectDevice = ({ - account, - currency, - deviceSelected, - onChangeDevice, - onStatusChange, -}: Props) => ( - ( - - )} - /> -) +const StepConnectDevice = ({ account, currency, onChangeDevice, onStatusChange }: Props) => + account || currency ? ( + { + // TODO: remove those non-nense callbacks + if (onChangeDevice) { + onChangeDevice(device) + } + onStatusChange('success', 'success') + }} + /> + ) : null export default StepConnectDevice From bd864ce279b263f081f420013a79591635bae79a Mon Sep 17 00:00:00 2001 From: meriadec Date: Tue, 26 Jun 2018 14:04:10 +0200 Subject: [PATCH 06/33] Add `onStepChange`, `disabledSteps` and other stuff on Stepper --- src/components/base/Stepper/index.js | 48 ++++++++++++++++++++++---- src/components/base/Stepper/stories.js | 2 +- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/components/base/Stepper/index.js b/src/components/base/Stepper/index.js index 038aef23..8daf4acb 100644 --- a/src/components/base/Stepper/index.js +++ b/src/components/base/Stepper/index.js @@ -12,9 +12,12 @@ import Breadcrumb from 'components/Breadcrumb' type Props = { t: T, title: string, + steps: Step[], initialStepId: string, onClose: void => void, - steps: Step[], + onStepChange?: Step => void, + disabledSteps?: number[], + errorSteps?: number[], children: any, } @@ -23,7 +26,8 @@ export type Step = { label: string, component: StepProps => React$Node, footer: StepProps => React$Node, - preventClose?: boolean, + shouldRenderFooter?: StepProps => boolean, + shouldPreventClose?: boolean | (StepProps => boolean), onBack?: StepProps => void, } @@ -41,10 +45,20 @@ class Stepper extends PureComponent { stepId: this.props.initialStepId, } - transitionTo = stepId => this.setState({ stepId }) + transitionTo = stepId => { + const { onStepChange, steps } = this.props + this.setState({ stepId }) + if (onStepChange) { + const stepIndex = steps.findIndex(s => s.id === stepId) + const step = steps[stepIndex] + if (step) { + onStepChange(step) + } + } + } render() { - const { t, steps, title, onClose, children, ...props } = this.props + const { t, steps, title, onClose, disabledSteps, errorSteps, children, ...props } = this.props const { stepId } = this.state const stepIndex = steps.findIndex(s => s.id === stepId) @@ -52,7 +66,13 @@ class Stepper extends PureComponent { invariant(step, `Stepper: step ${stepId} doesn't exists`) - const { component: StepComponent, footer: StepFooter, onBack, preventClose } = step + const { + component: StepComponent, + footer: StepFooter, + onBack, + shouldPreventClose, + shouldRenderFooter, + } = step const stepProps: StepProps = { t, @@ -60,15 +80,29 @@ class Stepper extends PureComponent { ...props, } + const renderFooter = + !!StepFooter && (shouldRenderFooter === undefined || shouldRenderFooter(stepProps)) + + const preventClose = + typeof shouldPreventClose === 'function' + ? shouldPreventClose(stepProps) + : !!shouldPreventClose + return ( onBack(stepProps) : undefined}>{title} - + {children} - {StepFooter && ( + {renderFooter && ( diff --git a/src/components/base/Stepper/stories.js b/src/components/base/Stepper/stories.js index 4cb41487..b03a0be9 100644 --- a/src/components/base/Stepper/stories.js +++ b/src/components/base/Stepper/stories.js @@ -27,7 +27,7 @@ const steps: Step[] = [ { id: 'second', label: 'second step', - preventClose: true, + shouldPreventClose: true, onBack: ({ transitionTo }: StepProps) => transitionTo('first'), component: () =>
second step (you cant close on this one)
, footer: ({ transitionTo }: StepProps) => ( From 1ddbeccce6115b8bcf443c6f80bfad102af6c18a Mon Sep 17 00:00:00 2001 From: meriadec Date: Tue, 26 Jun 2018 14:04:44 +0200 Subject: [PATCH 07/33] Refactor and clean ReceiveModal to use Stepper --- .../modals/Receive/01-step-account.js | 23 - .../modals/Receive/03-step-confirm-address.js | 57 --- .../modals/Receive/04-step-receive-funds.js | 46 -- src/components/modals/Receive/index.js | 449 ++++++------------ .../modals/Receive/steps/01-step-account.js | 27 ++ .../Receive/steps/02-step-connect-device.js | 42 ++ .../Receive/steps/03-step-confirm-address.js | 96 ++++ .../Receive/steps/04-step-receive-funds.js | 66 +++ 8 files changed, 373 insertions(+), 433 deletions(-) delete mode 100644 src/components/modals/Receive/01-step-account.js delete mode 100644 src/components/modals/Receive/03-step-confirm-address.js delete mode 100644 src/components/modals/Receive/04-step-receive-funds.js create mode 100644 src/components/modals/Receive/steps/01-step-account.js create mode 100644 src/components/modals/Receive/steps/02-step-connect-device.js create mode 100644 src/components/modals/Receive/steps/03-step-confirm-address.js create mode 100644 src/components/modals/Receive/steps/04-step-receive-funds.js diff --git a/src/components/modals/Receive/01-step-account.js b/src/components/modals/Receive/01-step-account.js deleted file mode 100644 index 97d0cc6d..00000000 --- a/src/components/modals/Receive/01-step-account.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow - -import React from 'react' - -import type { Account } from '@ledgerhq/live-common/lib/types' -import type { T } from 'types/common' - -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) => ( - - - - -) diff --git a/src/components/modals/Receive/03-step-confirm-address.js b/src/components/modals/Receive/03-step-confirm-address.js deleted file mode 100644 index 960ea747..00000000 --- a/src/components/modals/Receive/03-step-confirm-address.js +++ /dev/null @@ -1,57 +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 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) => ( - - {props.addressVerified === false ? ( - - {props.t('app:receive.steps.confirmAddress.error.title')} - {props.t('app:receive.steps.confirmAddress.error.text')} - - - ) : ( - - {props.t('app:receive.steps.confirmAddress.action')} - {props.t('app:receive.steps.confirmAddress.text')} - {props.account && } - {props.device && - props.account && } - - )} - -) diff --git a/src/components/modals/Receive/04-step-receive-funds.js b/src/components/modals/Receive/04-step-receive-funds.js deleted file mode 100644 index b93bc732..00000000 --- a/src/components/modals/Receive/04-step-receive-funds.js +++ /dev/null @@ -1,46 +0,0 @@ -// @flow - -import React from 'react' - -import type { Account } from '@ledgerhq/live-common/lib/types' -import type { T } from 'types/common' - -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) => ( - - - - - - {props.account && ( - - )} - -) diff --git a/src/components/modals/Receive/index.js b/src/components/modals/Receive/index.js index d038c7ca..79028623 100644 --- a/src/components/modals/Receive/index.js +++ b/src/components/modals/Receive/index.js @@ -1,192 +1,111 @@ // @flow -import React, { Fragment, PureComponent } from 'react' +import React, { PureComponent } from 'react' import { compose } from 'redux' import { connect } from 'react-redux' import { translate } from 'react-i18next' import { createStructuredSelector } from 'reselect' -import { accountsSelector } from 'reducers/accounts' +import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' import type { Account } from '@ledgerhq/live-common/lib/types' -import type { T, Device } from 'types/common' import { MODAL_RECEIVE } from 'config/constants' -import { isSegwitAccount } from 'helpers/bip32' +import type { T, Device } from 'types/common' +import type { StepProps as DefaultStepProps } from 'components/base/Stepper' -import getAddress from 'commands/getAddress' -import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' -import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount' +import { getCurrentDevice } from 'reducers/devices' +import { accountsSelector } from 'reducers/accounts' +import { closeModal } from 'reducers/modals' -import Box from 'components/base/Box' -import Breadcrumb from 'components/Breadcrumb' -import Button from 'components/base/Button' -import Modal, { ModalBody, ModalTitle, ModalContent, ModalFooter } from 'components/base/Modal' -import StepConnectDevice from 'components/modals/StepConnectDevice' -import { WrongDeviceForAccount } from 'components/EnsureDeviceApp' +import Modal from 'components/base/Modal' +import Stepper from 'components/base/Stepper' -import StepAccount from './01-step-account' -import StepConfirmAddress from './03-step-confirm-address' -import StepReceiveFunds from './04-step-receive-funds' +import StepAccount, { StepAccountFooter } from './steps/01-step-account' +import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device' +import StepConfirmAddress, { StepConfirmAddressFooter } from './steps/03-step-confirm-address' +import StepReceiveFunds, { StepReceiveFundsFooter } from './steps/04-step-receive-funds' type Props = { t: T, + device: ?Device, accounts: Account[], + closeModal: string => void, } type State = { - account: Account | null, - addressVerified: null | boolean, - amount: string | number, - appStatus: null | string, - deviceSelected: Device | null, - stepIndex: number, - stepsDisabled: Array, - stepsErrors: Array, + stepId: string, + account: ?Account, + isAppOpened: boolean, + isAddressVerified: ?boolean, + disabledSteps: number[], + errorSteps: number[], } -const GET_STEPS = t => [ - { label: t('app:receive.steps.chooseAccount.title'), Comp: StepAccount }, - { label: t('app:receive.steps.connectDevice.title'), Comp: StepConnectDevice }, - { label: t('app:receive.steps.confirmAddress.title'), Comp: StepConfirmAddress }, - { label: t('app:receive.steps.receiveFunds.title'), Comp: StepReceiveFunds }, -] - -const INITIAL_STATE = { - account: null, - addressVerified: null, - amount: '', - appStatus: null, - deviceSelected: null, - stepIndex: 0, - stepsDisabled: [], - stepsErrors: [], - // FIXME the two above can be derivated from other info (if we keep error etc) - // we can get rid of it after a big refactoring (see how done in Send) +export type StepProps = DefaultStepProps & { + device: ?Device, + account: ?Account, + closeModal: void => void, + isAppOpened: boolean, + isAddressVerified: ?boolean, + onSkipConfirm: void => void, + onResetSkip: void => void, + onChangeAccount: (?Account) => void, + onChangeAppOpened: boolean => void, + onChangeAddressVerified: boolean => void, } +const createSteps = ({ t }: { t: T }) => [ + { + id: 'account', + label: t('app:receive.steps.chooseAccount.title'), + component: StepAccount, + footer: StepAccountFooter, + }, + { + id: 'device', + label: t('app:receive.steps.connectDevice.title'), + component: StepConnectDevice, + footer: StepConnectDeviceFooter, + onBack: ({ transitionTo }: StepProps) => transitionTo('account'), + }, + { + id: 'confirm', + label: t('app:receive.steps.confirmAddress.title'), + component: StepConfirmAddress, + footer: StepConfirmAddressFooter, + shouldRenderFooter: ({ isAddressVerified }: StepProps) => isAddressVerified === false, + shouldPreventClose: ({ isAddressVerified }: StepProps) => isAddressVerified === null, + }, + { + id: 'receive', + label: t('app:receive.steps.receiveFunds.title'), + component: StepReceiveFunds, + footer: StepReceiveFundsFooter, + }, +] + const mapStateToProps = createStructuredSelector({ + device: getCurrentDevice, accounts: accountsSelector, }) -class ReceiveModal extends PureComponent { - state = INITIAL_STATE - - _steps = GET_STEPS(this.props.t) - - canNext = () => { - const { account, stepIndex } = this.state - - if (stepIndex === 0) { - return account !== null - } - - if (stepIndex === 1) { - const { deviceSelected, appStatus } = this.state - return deviceSelected !== null && appStatus === 'success' - } - - return false - } - - canClose = () => { - const { stepIndex, addressVerified } = this.state - - if (stepIndex === 2) { - return addressVerified === false - } - - return true - } - - canPrev = () => { - const { addressVerified, stepIndex } = this.state - - if (stepIndex === 1) { - return true - } - - if (stepIndex === 2) { - return addressVerified === false - } - - if (stepIndex === 3) { - return true - } - - return false - } - - handleReset = () => this.setState(INITIAL_STATE) - - handleNextStep = () => { - const { stepIndex } = this.state - if (stepIndex >= this._steps.length - 1) { - return - } - this.setState({ stepIndex: stepIndex + 1 }) - - // TODO: do that better - if (stepIndex === 1) { - this.verifyAddress() - } - } - - handlePrevStep = () => { - const { stepIndex } = this.state - - let newStepIndex - - switch (stepIndex) { - default: - case 1: - newStepIndex = 0 - break - - case 2: - case 3: - newStepIndex = 1 - break - } - - this.setState({ - addressVerified: null, - appStatus: null, - deviceSelected: null, - stepIndex: newStepIndex, - stepsDisabled: [], - stepsErrors: [], - }) - } - - handleChangeDevice = d => this.setState({ deviceSelected: d }) - - handleChangeAccount = account => this.setState({ account }) - - handleChangeStatus = (deviceStatus, appStatus) => this.setState({ appStatus }) - - handleCheckAddress = isVerified => { - this.setState({ - addressVerified: isVerified, - stepsErrors: isVerified === false ? [2] : [], - }) - - if (isVerified === true) { - this.handleNextStep() - } - } - - handleRetryCheckAddress = () => { - this.setState({ - addressVerified: null, - stepsErrors: [], - }) +const mapDispatchToProps = { + closeModal, +} - // TODO: do that better - this.verifyAddress() - } +const INITIAL_STATE = { + stepId: 'account', + account: null, + isAppOpened: false, + isAddressVerified: null, + disabledSteps: [], + errorSteps: [], +} - handleChangeAmount = amount => this.setState({ amount }) +class ReceiveModal extends PureComponent { + state = INITIAL_STATE + STEPS = createSteps({ t: this.props.t }) handleBeforeOpenModal = ({ data }) => { const { account } = this.state @@ -194,174 +113,87 @@ class ReceiveModal extends PureComponent { if (!account) { if (data && data.account) { - this.setState({ - account: data.account, - stepIndex: 1, - }) + this.setState({ account: data.account }) } else { - this.setState({ - account: accounts[0], - }) + this.setState({ account: accounts[0] }) } } } - handleSkipStep = () => - this.setState({ - addressVerified: false, - stepsErrors: [], - stepsDisabled: [1, 2], - stepIndex: this._steps.length - 1, // last step - }) - - verifyAddress = async () => { - const { account, deviceSelected: device } = this.state - try { - if (account && device) { - const { address } = await getAddress - .send({ - currencyId: account.currency.id, - devicePath: device.path, - path: account.freshAddressPath, - segwit: isSegwitAccount(account), - verify: true, - }) - .toPromise() - - if (address !== account.freshAddress) { - throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, { - accountName: account.name, - }) - } - - this.handleCheckAddress(true) - } else { - this.handleCheckAddress(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) => { + if (isAddressVerified) { + this.setState({ isAddressVerified }) + } else { + const confirmStepIndex = this.STEPS.findIndex(step => step.id === 'confirm') + if (confirmStepIndex > -1) { + this.setState({ + isAddressVerified, + errorSteps: [confirmStepIndex], + }) } - } catch (err) { - this.handleCheckAddress(false) } } - renderStep = () => { - const { account, amount, addressVerified, deviceSelected, stepIndex } = this.state - const { t } = this.props - const step = this._steps[stepIndex] - if (!step) { - return null - } - const { Comp } = step - - const props = (predicate, props) => (predicate ? props : {}) - - const stepProps = { - t, - account, - ...props(stepIndex === 0, { - onChangeAccount: this.handleChangeAccount, - }), - ...props(stepIndex === 1, { - accountName: account ? account.name : undefined, - deviceSelected, - onChangeDevice: this.handleChangeDevice, - onStatusChange: this.handleChangeStatus, - }), - ...props(stepIndex === 2, { - addressVerified, - onCheck: this.handleCheckAddress, - device: deviceSelected, - }), - ...props(stepIndex === 3, { - addressVerified, - amount, - onChangeAmount: this.handleChangeAmount, - onVerify: this.handlePrevStep, - }), + 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] }) } - - return - } - - renderButton = () => { - const { t } = this.props - const { stepIndex, addressVerified } = this.state - - let onClick - let props - - switch (stepIndex) { - case 2: - props = { - primary: true, - onClick: this.handleRetryCheckAddress, - children: t('app:common.retry'), - } - break - default: - onClick = this.handleNextStep - props = { - primary: true, - disabled: !this.canNext(), - onClick, - children: t('app:common.next'), - } - } - - return ( - - {stepIndex === 1 && ( - - )} - {stepIndex === 2 && - addressVerified === false && ( - - )} - + ) +} diff --git a/src/components/modals/Receive/steps/02-step-connect-device.js b/src/components/modals/Receive/steps/02-step-connect-device.js new file mode 100644 index 00000000..c0c4af39 --- /dev/null +++ b/src/components/modals/Receive/steps/02-step-connect-device.js @@ -0,0 +1,42 @@ +// @flow + +import React from 'react' + +import Box from 'components/base/Box' +import Button from 'components/base/Button' +import EnsureDeviceAppInteraction from 'components/EnsureDeviceAppInteraction' + +import type { StepProps } from '../index' + +export default function StepConnectDevice({ account, onChangeAppOpened }: StepProps) { + return ( + onChangeAppOpened(true)} + /> + ) +} + +export function StepConnectDeviceFooter({ + t, + transitionTo, + isAppOpened, + onSkipConfirm, +}: StepProps) { + return ( + + + + + ) +} diff --git a/src/components/modals/Receive/steps/03-step-confirm-address.js b/src/components/modals/Receive/steps/03-step-confirm-address.js new file mode 100644 index 00000000..e9876cdd --- /dev/null +++ b/src/components/modals/Receive/steps/03-step-confirm-address.js @@ -0,0 +1,96 @@ +// @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 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 { + 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 ( + + {isAddressVerified === false ? ( + + {t('app:receive.steps.confirmAddress.error.title')} + {t('app:receive.steps.confirmAddress.error.text')} + + + ) : ( + + {t('app:receive.steps.confirmAddress.action')} + {t('app:receive.steps.confirmAddress.text')} + + + + )} + + ) + } +} + +export function StepConfirmAddressFooter({ t }: StepProps) { + // This will be displayed only if user rejected address + return +} + +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; +` diff --git a/src/components/modals/Receive/steps/04-step-receive-funds.js b/src/components/modals/Receive/steps/04-step-receive-funds.js new file mode 100644 index 00000000..f68b41e5 --- /dev/null +++ b/src/components/modals/Receive/steps/04-step-receive-funds.js @@ -0,0 +1,66 @@ +// @flow + +import invariant from 'invariant' +import React, { PureComponent } from 'react' + +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 { + 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 ( + + + + + + + + ) + } +} + +export function StepReceiveFundsFooter({ t, closeModal }: StepProps) { + return ( + + ) +} From b6b5943e330fcb04488276fa2447415e9151535e Mon Sep 17 00:00:00 2001 From: meriadec Date: Tue, 26 Jun 2018 15:20:01 +0200 Subject: [PATCH 08/33] Wording on EnsureDeviceAppInteraction --- src/components/EnsureDeviceAppInteraction.js | 27 +++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/components/EnsureDeviceAppInteraction.js b/src/components/EnsureDeviceAppInteraction.js index fcc79996..32bd1e7b 100644 --- a/src/components/EnsureDeviceAppInteraction.js +++ b/src/components/EnsureDeviceAppInteraction.js @@ -3,15 +3,19 @@ import React, { Component } from 'react' import invariant from 'invariant' import { connect } from 'react-redux' +import { Trans } from 'react-i18next' import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types' import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' import logger from 'logger' +import getAddress from 'commands/getAddress' import { createCancelablePolling } from 'helpers/promise' import { standardDerivation } from 'helpers/derivations' import { isSegwitAccount } from 'helpers/bip32' + import DeviceInteraction from 'components/DeviceInteraction' -import getAddress from 'commands/getAddress' +import Text from 'components/base/Text' + import IconUsb from 'icons/Usb' import type { Device } from 'types/common' @@ -23,6 +27,7 @@ export const WrongAppOpened = createCustomErrorClass('WrongAppOpened') export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount') const usbIcon = +const Bold = props => const mapStateToProps = state => ({ device: getCurrentDevice(state), @@ -79,11 +84,17 @@ class EnsureDeviceAppInteraction extends Component<{ return address }) - renderOpenAppTitle = ({ device }) => { + renderOpenAppTitle = () => { const { account, currency } = this.props const cur = account ? account.currency : currency invariant(cur, 'No currency given') - return `Open the ${cur.name} app on your ${device ? `${device.product} ` : 'device'}` + return ( + + {'Open the '} + {cur.name} + {' app on your device'} + + ) } render() { @@ -95,16 +106,18 @@ class EnsureDeviceAppInteraction extends Component<{ steps={[ { id: 'device', - title: 'Connect your device', + title: ( + + {'Connect and unlock your '} + {'Ledger device'} + + ), icon: usbIcon, - desc: 'Because it is required', - minMs: 300, run: this.connectInteractionHandler, }, { id: 'address', title: this.renderOpenAppTitle, - desc: 'To be able to retriev your Bitcoins', icon: Icon ? : null, run: this.openAppInteractionHandler, }, From 554461ad2b657f6d2432982983b51eeba580bfaa Mon Sep 17 00:00:00 2001 From: meriadec Date: Tue, 26 Jun 2018 15:26:53 +0200 Subject: [PATCH 09/33] Rename EnsureDeviceAppInteraction -> EnsureDeviceApp, and get rid of the old one --- src/components/EnsureDeviceApp.js | 319 ++++++------------ src/components/EnsureDeviceAppInteraction.js | 131 ------- .../Receive/steps/02-step-connect-device.js | 4 +- src/components/modals/StepConnectDevice.js | 4 +- 4 files changed, 110 insertions(+), 348 deletions(-) delete mode 100644 src/components/EnsureDeviceAppInteraction.js diff --git a/src/components/EnsureDeviceApp.js b/src/components/EnsureDeviceApp.js index 5759e6c8..0c160e68 100644 --- a/src/components/EnsureDeviceApp.js +++ b/src/components/EnsureDeviceApp.js @@ -1,238 +1,131 @@ // @flow -import { PureComponent } from 'react' -import { connect } from 'react-redux' -import logger from 'logger' -import invariant from 'invariant' -import { isSegwitAccount } from 'helpers/bip32' +import React, { Component } from 'react' +import invariant from 'invariant' +import { connect } from 'react-redux' +import { Trans } from 'react-i18next' import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types' -import type { Device } from 'types/common' +import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' -import { getDevices } from 'reducers/devices' -import type { State as StoreState } from 'reducers/index' +import logger from 'logger' import getAddress from 'commands/getAddress' +import { createCancelablePolling } from 'helpers/promise' import { standardDerivation } from 'helpers/derivations' -import isDashboardOpen from 'commands/isDashboardOpen' -import { createCustomErrorClass } from 'helpers/errors' - -import { CHECK_APP_INTERVAL_WHEN_VALID, CHECK_APP_INTERVAL_WHEN_INVALID } from 'config/constants' - -export const WrongAppOpened = createCustomErrorClass('WrongAppOpened') -export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount') +import { isSegwitAccount } from 'helpers/bip32' -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, - genuineCheckStatus: GenuineCheckStatus, - currency: ?CryptoCurrency, - devices: Device[], - deviceSelected: ?Device, - deviceStatus: DeviceStatus, - error: ?Error, - }) => React$Node, -} +import DeviceInteraction from 'components/DeviceInteraction' +import Text from 'components/base/Text' -type Props = OwnProps & { - devices: Device[], -} +import IconUsb from 'icons/Usb' -type DeviceStatus = 'unconnected' | 'connected' +import type { Device } from 'types/common' -type AppStatus = 'success' | 'fail' | 'progress' +import { createCustomErrorClass } from 'helpers/errors' +import { getCurrentDevice } from 'reducers/devices' -type GenuineCheckStatus = 'success' | 'fail' | 'progress' +export const WrongAppOpened = createCustomErrorClass('WrongAppOpened') +export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount') -type State = { - deviceStatus: DeviceStatus, - appStatus: AppStatus, - error: ?Error, - genuineCheckStatus: GenuineCheckStatus, -} +const usbIcon = +const Bold = props => -const mapStateToProps = (state: StoreState) => ({ - devices: getDevices(state), +const mapStateToProps = state => ({ + device: getCurrentDevice(state), }) -// TODO we want to split into and -// and minimize the current codebase AF -class EnsureDeviceApp extends PureComponent { - state = { - appStatus: 'progress', - deviceStatus: this.props.deviceSelected ? 'connected' : 'unconnected', - error: null, - genuineCheckStatus: 'progress', - } - - componentDidMount() { - if (this.props.deviceSelected !== null) { - this.checkAppOpened() - } - } - - componentWillReceiveProps(nextProps) { - const { deviceStatus } = this.state - const { deviceSelected, devices } = this.props - const { devices: nextDevices, deviceSelected: nextDeviceSelected } = nextProps - - if (deviceStatus === 'unconnected' && !deviceSelected && nextDeviceSelected) { - this.handleStatusChange('connected', 'progress') - } - - if (deviceStatus !== 'unconnected' && devices !== nextDevices) { - const isConnected = nextDevices.find(d => d === nextDeviceSelected) - if (!isConnected) { - this.handleStatusChange('unconnected', 'progress') - } - } - } - - componentDidUpdate(prevProps) { - const { deviceSelected } = this.props - const { deviceSelected: prevDeviceSelected } = prevProps - - if (prevDeviceSelected !== deviceSelected) { - this.handleStatusChange('connected', 'progress') - // TODO: refacto to more generic/global way - clearTimeout(this._timeout) - this._timeout = setTimeout(this.checkAppOpened, 250) - } - } - - componentWillUnmount() { - clearTimeout(this._timeout) - this._unmounted = true - } - - checkAppOpened = async () => { - const { deviceSelected, account, currency, withGenuineCheck } = this.props - const { appStatus } = this.state - - if (!deviceSelected) { - return - } - - let isSuccess = true - - try { - if (account || currency) { - const cur = account ? account.currency : currency - invariant(cur, 'currency is available') - const { address } = await getAddress - .send({ - devicePath: deviceSelected.path, - currencyId: cur.id, - path: account - ? account.freshAddressPath - : standardDerivation({ currency: cur, segwit: false, x: 0 }), - segwit: account ? isSegwitAccount(account) : false, - }) - .toPromise() - .catch(e => { - if ( - e && - (e.name === 'TransportStatusError' || - // we don't want these error to appear (caused by usb disconnect..) - e.message === 'could not read from HID device' || - e.message === 'Cannot write to HID device') - ) { - logger.log(e) - throw new WrongAppOpened(`WrongAppOpened ${cur.id}`, { currencyName: cur.name }) - } - throw e - }) - - if (account) { - const { freshAddress } = account - if (account && freshAddress !== address) { - logger.warn({ freshAddress, address }) - throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, { - accountName: account.name, - }) +class EnsureDeviceApp extends Component<{ + device: ?Device, + account?: ?Account, + currency?: ?CryptoCurrency, +}> { + connectInteractionHandler = () => + createCancelablePolling(() => { + if (!this.props.device) return Promise.reject() + return Promise.resolve(this.props.device) + }) + + openAppInteractionHandler = ({ device }) => + createCancelablePolling(async () => { + const { account, currency } = this.props + const cur = account ? account.currency : currency + invariant(cur, 'No currency given') + const { address } = await getAddress + .send({ + devicePath: device.path, + currencyId: cur.id, + path: account + ? account.freshAddressPath + : standardDerivation({ currency: cur, segwit: false, x: 0 }), + segwit: account ? isSegwitAccount(account) : false, + }) + .toPromise() + .catch(e => { + if ( + e && + (e.name === 'TransportStatusError' || + // we don't want these error to appear (caused by usb disconnect..) + e.message === 'could not read from HID device' || + e.message === 'Cannot write to HID device') + ) { + throw new WrongAppOpened(`WrongAppOpened ${cur.id}`, { currencyName: cur.name }) } + throw e + }) + + if (account) { + const { freshAddress } = account + if (account && freshAddress !== address) { + logger.warn({ freshAddress, address }) + throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, { + accountName: account.name, + }) } - } else { - logger.warn('EnsureDeviceApp for using dashboard is DEPRECATED !!!') - // TODO: FIXME REMOVE THIS ! should use EnsureDashboard dedicated component. - const isDashboard = isDashboardOpen.send({ devicePath: deviceSelected.path }).toPromise() - - if (!isDashboard) { - throw new Error(`dashboard is not opened`) - } - } - - this.handleStatusChange(this.state.deviceStatus, 'success') - - if (withGenuineCheck && appStatus !== 'success') { - this.handleGenuineCheck() } - } catch (e) { - this.handleStatusChange(this.state.deviceStatus, 'fail', e) - isSuccess = false - } - - // TODO: refacto to more generic/global way - if (!this._unmounted) { - this._timeout = setTimeout( - this.checkAppOpened, - isSuccess ? CHECK_APP_INTERVAL_WHEN_VALID : CHECK_APP_INTERVAL_WHEN_INVALID, - ) - } - } - - _timeout: * - _unmounted = false - - handleStatusChange = (deviceStatus, appStatus, error = null) => { - const { onStatusChange } = this.props - clearTimeout(this._timeout) - if (!this._unmounted) { - this.setState({ deviceStatus, appStatus, error }) - onStatusChange && onStatusChange(deviceStatus, appStatus, error) - } - } - - handleGenuineCheck = async () => { - // TODO: do a *real* genuine check - await sleep(1) - if (!this._unmounted) { - this.setState({ genuineCheckStatus: 'success' }) - this.props.onGenuineCheck && this.props.onGenuineCheck(true) - } + return address + }) + + renderOpenAppTitle = () => { + const { account, currency } = this.props + const cur = account ? account.currency : currency + invariant(cur, 'No currency given') + return ( + + {'Open the '} + {cur.name} + {' app on your device'} + + ) } render() { - const { currency, account, devices, deviceSelected, render } = this.props - const { appStatus, deviceStatus, genuineCheckStatus, error } = 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 - - return render({ - appStatus, - currency: cur, - devices, - deviceSelected: deviceStatus === 'connected' ? deviceSelected : null, - deviceStatus, - genuineCheckStatus, - error, - }) - } - - return null + const { account, currency, ...props } = this.props + const cur = account ? account.currency : currency + const Icon = cur ? getCryptoCurrencyIcon(cur) : null + return ( + + {'Connect and unlock your '} + {'Ledger device'} + + ), + icon: usbIcon, + run: this.connectInteractionHandler, + }, + { + id: 'address', + title: this.renderOpenAppTitle, + icon: Icon ? : null, + run: this.openAppInteractionHandler, + }, + ]} + {...props} + /> + ) } } export default connect(mapStateToProps)(EnsureDeviceApp) - -async function sleep(s) { - return new Promise(resolve => setTimeout(resolve, s * 1e3)) -} diff --git a/src/components/EnsureDeviceAppInteraction.js b/src/components/EnsureDeviceAppInteraction.js deleted file mode 100644 index 32bd1e7b..00000000 --- a/src/components/EnsureDeviceAppInteraction.js +++ /dev/null @@ -1,131 +0,0 @@ -// @flow - -import React, { Component } from 'react' -import invariant from 'invariant' -import { connect } from 'react-redux' -import { Trans } from 'react-i18next' -import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types' -import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' - -import logger from 'logger' -import getAddress from 'commands/getAddress' -import { createCancelablePolling } from 'helpers/promise' -import { standardDerivation } from 'helpers/derivations' -import { isSegwitAccount } from 'helpers/bip32' - -import DeviceInteraction from 'components/DeviceInteraction' -import Text from 'components/base/Text' - -import IconUsb from 'icons/Usb' - -import type { Device } from 'types/common' - -import { createCustomErrorClass } from 'helpers/errors' -import { getCurrentDevice } from 'reducers/devices' - -export const WrongAppOpened = createCustomErrorClass('WrongAppOpened') -export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount') - -const usbIcon = -const Bold = props => - -const mapStateToProps = state => ({ - device: getCurrentDevice(state), -}) - -class EnsureDeviceAppInteraction extends Component<{ - device: ?Device, - account?: ?Account, - currency?: ?CryptoCurrency, -}> { - connectInteractionHandler = () => - createCancelablePolling(() => { - if (!this.props.device) return Promise.reject() - return Promise.resolve(this.props.device) - }) - - openAppInteractionHandler = ({ device }) => - createCancelablePolling(async () => { - const { account, currency } = this.props - const cur = account ? account.currency : currency - invariant(cur, 'No currency given') - const { address } = await getAddress - .send({ - devicePath: device.path, - currencyId: cur.id, - path: account - ? account.freshAddressPath - : standardDerivation({ currency: cur, segwit: false, x: 0 }), - segwit: account ? isSegwitAccount(account) : false, - }) - .toPromise() - .catch(e => { - if ( - e && - (e.name === 'TransportStatusError' || - // we don't want these error to appear (caused by usb disconnect..) - e.message === 'could not read from HID device' || - e.message === 'Cannot write to HID device') - ) { - throw new WrongAppOpened(`WrongAppOpened ${cur.id}`, { currencyName: cur.name }) - } - throw e - }) - - if (account) { - const { freshAddress } = account - if (account && freshAddress !== address) { - logger.warn({ freshAddress, address }) - throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, { - accountName: account.name, - }) - } - } - return address - }) - - renderOpenAppTitle = () => { - const { account, currency } = this.props - const cur = account ? account.currency : currency - invariant(cur, 'No currency given') - return ( - - {'Open the '} - {cur.name} - {' app on your device'} - - ) - } - - render() { - const { account, currency, ...props } = this.props - const cur = account ? account.currency : currency - const Icon = cur ? getCryptoCurrencyIcon(cur) : null - return ( - - {'Connect and unlock your '} - {'Ledger device'} - - ), - icon: usbIcon, - run: this.connectInteractionHandler, - }, - { - id: 'address', - title: this.renderOpenAppTitle, - icon: Icon ? : null, - run: this.openAppInteractionHandler, - }, - ]} - {...props} - /> - ) - } -} - -export default connect(mapStateToProps)(EnsureDeviceAppInteraction) diff --git a/src/components/modals/Receive/steps/02-step-connect-device.js b/src/components/modals/Receive/steps/02-step-connect-device.js index c0c4af39..539d6b99 100644 --- a/src/components/modals/Receive/steps/02-step-connect-device.js +++ b/src/components/modals/Receive/steps/02-step-connect-device.js @@ -4,13 +4,13 @@ import React from 'react' import Box from 'components/base/Box' import Button from 'components/base/Button' -import EnsureDeviceAppInteraction from 'components/EnsureDeviceAppInteraction' +import EnsureDeviceApp from 'components/EnsureDeviceApp' import type { StepProps } from '../index' export default function StepConnectDevice({ account, onChangeAppOpened }: StepProps) { return ( - onChangeAppOpened(true)} diff --git a/src/components/modals/StepConnectDevice.js b/src/components/modals/StepConnectDevice.js index c6ed57ff..11906e6a 100644 --- a/src/components/modals/StepConnectDevice.js +++ b/src/components/modals/StepConnectDevice.js @@ -5,7 +5,7 @@ import React from 'react' import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types' import type { Device } from 'types/common' -import EnsureDeviceAppInteraction from 'components/EnsureDeviceAppInteraction' +import EnsureDeviceApp from 'components/EnsureDeviceApp' type Props = { account?: ?Account, @@ -16,7 +16,7 @@ type Props = { const StepConnectDevice = ({ account, currency, onChangeDevice, onStatusChange }: Props) => account || currency ? ( - Date: Wed, 27 Jun 2018 09:43:28 +0200 Subject: [PATCH 10/33] Simplify first job running on DeviceInteractionStep --- .../DeviceInteractionStep.js | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/components/DeviceInteraction/DeviceInteractionStep.js b/src/components/DeviceInteraction/DeviceInteractionStep.js index a1ff3a53..e462ebab 100644 --- a/src/components/DeviceInteraction/DeviceInteractionStep.js +++ b/src/components/DeviceInteraction/DeviceInteractionStep.js @@ -55,23 +55,14 @@ class DeviceInteractionStep extends PureComponent< data: {}, } - constructor(props: Props) { - super(props) - const { isFirst } = this.props - if (isFirst) { - // cf: __IS_MOUNTED__THX_FOR_REMOVING_COMPONENTWILLMOUNT__ - this.state.status = 'running' - - this.run() - } - } - state = { - status: 'idle', + status: this.props.isFirst ? 'running' : 'idle', } componentDidMount() { - this.__IS_MOUNTED__THX_FOR_REMOVING_COMPONENTWILLMOUNT__ = true + if (this.props.isFirst) { + this.run() + } } componentDidUpdate(prevProps: Props) { @@ -103,7 +94,6 @@ class DeviceInteractionStep extends PureComponent< this._unmounted = true } - __IS_MOUNTED__THX_FOR_REMOVING_COMPONENTWILLMOUNT__ = false _unsubscribe = null _unmounted = false @@ -122,8 +112,9 @@ class DeviceInteractionStep extends PureComponent< run = async () => { const { step, data } = this.props + const { status } = this.state - if (this.__IS_MOUNTED__THX_FOR_REMOVING_COMPONENTWILLMOUNT__) { + if (status !== 'running') { this.setState({ status: 'running' }) } From 51afb159b0737b1ac3b6739d94e37ae044be93eb Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 12:32:55 +0200 Subject: [PATCH 11/33] Refacto ManagerPage to use GenuineCheck component --- src/components/GenuineCheck.js | 11 ++- .../ManagerPage/ManagerGenuineCheck.js | 53 ++++++++++++++ src/components/ManagerPage/index.js | 70 +++++++++---------- 3 files changed, 90 insertions(+), 44 deletions(-) create mode 100644 src/components/ManagerPage/ManagerGenuineCheck.js diff --git a/src/components/GenuineCheck.js b/src/components/GenuineCheck.js index 56442003..44a307cf 100644 --- a/src/components/GenuineCheck.js +++ b/src/components/GenuineCheck.js @@ -63,16 +63,13 @@ class GenuineCheck extends PureComponent { checkGenuineInteractionHandler = async ({ device, - infos, + deviceInfo, }: { device: Device, - infos: DeviceInfo, + deviceInfo: DeviceInfo, }) => { const res = await getIsGenuine - .send({ - devicePath: device.path, - deviceInfo: infos, - }) + .send({ devicePath: device.path, deviceInfo }) .pipe(timeout(GENUINE_TIMEOUT)) .toPromise() const isGenuine = res === '0000' @@ -106,7 +103,7 @@ class GenuineCheck extends PureComponent { run: this.connectInteractionHandler, }, { - id: 'infos', + id: 'deviceInfo', title: ( {'Navigate to the '} diff --git a/src/components/ManagerPage/ManagerGenuineCheck.js b/src/components/ManagerPage/ManagerGenuineCheck.js new file mode 100644 index 00000000..d637db48 --- /dev/null +++ b/src/components/ManagerPage/ManagerGenuineCheck.js @@ -0,0 +1,53 @@ +// @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 { + render() { + const { t, onSuccess } = this.props + return ( + + + connect your device + + {t('app:manager.device.title')} + + + {t('app:manager.device.desc')} + + + + { + console.log(`fail`) + }} + onUnavailable={() => { + console.log(`unavailable`) + }} + /> + + ) + } +} + +export default translate()(ManagerGenuineCheck) diff --git a/src/components/ManagerPage/index.js b/src/components/ManagerPage/index.js index 49a7312b..a8e3ca65 100644 --- a/src/components/ManagerPage/index.js +++ b/src/components/ManagerPage/index.js @@ -1,52 +1,48 @@ // @flow -/* eslint-disable react/jsx-no-literals */ // FIXME: remove import React, { PureComponent } from 'react' +import invariant from 'invariant' import type { Device } from 'types/common' import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' -import Workflow from 'components/Workflow' -import WorkflowWithIcon from 'components/Workflow/WorkflowWithIcon' import Dashboard from './Dashboard' -import FlashMcu from './FlashMcu' +// import FlashMcu from './FlashMcu' -type Error = { - message: string, - stack: string, +import ManagerGenuineCheck from './ManagerGenuineCheck' + +type Props = {} + +type State = { + isGenuine: ?boolean, + device: ?Device, + deviceInfo: ?DeviceInfo, } -class ManagerPage extends PureComponent<*, *> { +class ManagerPage extends PureComponent { + state = { + isGenuine: null, + } + + handleSuccessGenuine = ({ device, deviceInfo }) => { + this.setState({ isGenuine: true, device, deviceInfo }) + } + render() { - return ( - ( -

UPDATE FINAL FIRMARE (TEMPLATE + ACTION WIP) {deviceInfo.isOSU}

- )} - renderMcuUpdate={(device: Device, deviceInfo: DeviceInfo) => ( - - )} - renderDashboard={(device: Device, deviceInfo: DeviceInfo) => ( - - )} - renderDefault={( - device: ?Device, - deviceInfo: ?DeviceInfo, - isGenuine: ?boolean, - errors: { - dashboardError: ?Error, - genuineError: ?Error, - }, - ) => ( - - )} - /> - ) + const { isGenuine, device, deviceInfo } = this.state + + if (!isGenuine) { + return + } + + invariant(device, 'Inexistant device considered genuine') + invariant(deviceInfo, 'Inexistant device infos for genuine device') + + // TODO + // renderFinalUpdate + // renderMcuUpdate + + return } } From f91e67ca2941523303ec90c91539164677decd1e Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 12:33:17 +0200 Subject: [PATCH 12/33] Cache device genuinity in GenuineCheck component --- src/components/GenuineCheck.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/GenuineCheck.js b/src/components/GenuineCheck.js index 44a307cf..3528b50e 100644 --- a/src/components/GenuineCheck.js +++ b/src/components/GenuineCheck.js @@ -45,6 +45,14 @@ const mapStateToProps = state => ({ const 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 { connectInteractionHandler = () => createCancelablePolling(() => { @@ -68,6 +76,9 @@ class GenuineCheck extends PureComponent { device: Device, deviceInfo: DeviceInfo, }) => { + if (getDeviceGenuinity(device) === true) { + return true + } const res = await getIsGenuine .send({ devicePath: device.path, deviceInfo }) .pipe(timeout(GENUINE_TIMEOUT)) @@ -76,6 +87,7 @@ class GenuineCheck extends PureComponent { if (!isGenuine) { return Promise.reject(new Error('Device not genuine')) // TODO: use custom error class } + setDeviceGenuinity(device, true) return Promise.resolve(true) } From 929f620bbff771aa2f981a1a9297f524e6038179 Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 12:45:38 +0200 Subject: [PATCH 13/33] Grey border for passed steps, and remove weird left border --- src/components/DeviceInteraction/components.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/components/DeviceInteraction/components.js b/src/components/DeviceInteraction/components.js index bd673d9b..884649ad 100644 --- a/src/components/DeviceInteraction/components.js +++ b/src/components/DeviceInteraction/components.js @@ -26,7 +26,7 @@ export const DeviceInteractionStepContainer = styled(Box).attrs({ min-height: 80px; border: 1px solid ${p => p.theme.colors.fog}; border-color: ${p => - p.isError ? p.theme.colors.alertRed : p.isActive || p.isSuccess ? p.theme.colors.wallet : ''}; + p.isError ? p.theme.colors.alertRed : p.isActive ? 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)}; @@ -39,19 +39,6 @@ export const DeviceInteractionStepContainer = styled(Box).attrs({ ${rgba(p.isError ? p.theme.colors.alertRed : p.theme.colors.wallet, 0.2)} 0 0 3px 2px ` : 'none'}; - - &:after { - content: ''; - position: absolute; - left: -2px; - top: 0; - bottom: 0; - width: 2px; - box-shadow: ${p => - p.isActive && !p.isSuccess - ? `${p.theme.colors[p.isError ? 'alertRed' : 'wallet']} 2px 0 0` - : 'none'}; - } ` export const IconContainer = ({ children }: { children: any }) => ( From 56b14106b6a973a13bc1c0432b1f135eb1034ad0 Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 13:33:23 +0200 Subject: [PATCH 14/33] Change error behavior of DeviceInteractionStep --- .../DeviceInteractionStep.js | 22 +--- .../DeviceInteraction/components.js | 5 +- src/components/DeviceInteraction/index.js | 103 +++++++++--------- src/components/GenuineCheck.js | 3 +- .../ManagerPage/ManagerGenuineCheck.js | 13 +-- src/components/ManagerPage/index.js | 4 +- 6 files changed, 69 insertions(+), 81 deletions(-) diff --git a/src/components/DeviceInteraction/DeviceInteractionStep.js b/src/components/DeviceInteraction/DeviceInteractionStep.js index e462ebab..5fe9a077 100644 --- a/src/components/DeviceInteraction/DeviceInteractionStep.js +++ b/src/components/DeviceInteraction/DeviceInteractionStep.js @@ -10,7 +10,6 @@ import { SpinnerContainer, IconContainer, SuccessContainer, - ErrorDescContainer, ErrorContainer, } from './components' @@ -20,10 +19,7 @@ export type Step = { desc?: React$Node, icon: React$Node, run?: Object => Promise | { promise: Promise, unsubscribe: void => any }, - render?: ( - { onSuccess: Object => any, onFail: Error => void, onRetry: void => void }, - any, - ) => React$Node, + render?: ({ onSuccess: Object => any, onFail: Error => void }, any) => React$Node, minMs?: number, } @@ -38,10 +34,8 @@ type Props = { isSuccess: boolean, isPassed: boolean, step: Step, - error: ?Error, onSuccess: (any, Step) => any, onFail: (Error, Step) => any, - onRetry: void => any, data: any, } @@ -66,13 +60,13 @@ class DeviceInteractionStep extends PureComponent< } componentDidUpdate(prevProps: Props) { - const { isActive, error } = this.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 = !error && !!prevProps.error + const didResetError = !isError && !!prevProps.isError if (didActivated && status !== 'running') { this.run() @@ -163,8 +157,6 @@ class DeviceInteractionStep extends PureComponent< isError, isPassed, step, - error, - onRetry, data, } = this.props @@ -191,14 +183,8 @@ class DeviceInteractionStep extends PureComponent< )} {step.desc && step.desc} {CustomRender && ( - + )} - {isError && error && }
diff --git a/src/components/DeviceInteraction/components.js b/src/components/DeviceInteraction/components.js index 884649ad..dfce363d 100644 --- a/src/components/DeviceInteraction/components.js +++ b/src/components/DeviceInteraction/components.js @@ -92,6 +92,8 @@ export const ErrorContainer = ({ isVisible }: { isVisible: boolean }) => ( ) const ErrorRetryContainer = styled(Box).attrs({ + bg: rgba(colors.alertRed, 0.1), + borderRadius: 1, grow: 1, color: 'alertRed', cursor: 'pointer', @@ -118,6 +120,7 @@ export const ErrorDescContainer = ({ }) => ( - + {error.message || 'Failed'} diff --git a/src/components/DeviceInteraction/index.js b/src/components/DeviceInteraction/index.js index 2375ebbf..1759c41d 100644 --- a/src/components/DeviceInteraction/index.js +++ b/src/components/DeviceInteraction/index.js @@ -1,40 +1,43 @@ // @flow import React, { PureComponent } from 'react' -import styled from 'styled-components' import { delay } from 'helpers/promise' import Box from 'components/base/Box' +import Space from 'components/base/Space' + import DeviceInteractionStep from './DeviceInteractionStep' import type { Step } from './DeviceInteractionStep' +import { ErrorDescContainer } from './components' + +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, - showSuccess: false, error: null, data: {}, } -class DeviceInteraction extends PureComponent< - { - steps: Step[], - onSuccess?: any => void, - onFail?: any => void, - renderSuccess?: any => any, - waitBeforeSuccess?: number, - }, - { - stepIndex: number, - isSuccess: boolean, - // used to be able to display the last check for a small amount of time - showSuccess: boolean, - error: ?Error, - data: Object, - }, -> { +class DeviceInteraction extends PureComponent { state = INITIAL_STATE componentWillUnmount() { @@ -58,12 +61,11 @@ class DeviceInteraction extends PureComponent< if (!waitBeforeSuccess) { onSuccess && onSuccess(data) } - this.setState({ isSuccess: true, data, showSuccess: !waitBeforeSuccess }) + this.setState({ isSuccess: true, data }) if (waitBeforeSuccess) { await delay(waitBeforeSuccess) if (this._unmounted) return onSuccess && onSuccess(data) - this.setState({ showSuccess: true }) } } else { this.setState({ stepIndex: stepIndex + 1, data }) @@ -82,39 +84,40 @@ class DeviceInteraction extends PureComponent< } render() { - const { steps, renderSuccess, waitBeforeSuccess: _waitBeforeSuccess, ...props } = this.props - const { stepIndex, error, isSuccess, data, showSuccess } = this.state + const { steps, shouldRenderRetry, ...props } = this.props + const { stepIndex, error, isSuccess, data } = this.state return ( - - {isSuccess && showSuccess && renderSuccess - ? renderSuccess(data) - : steps.map((step, i) => { - const isError = !!error && i === stepIndex - return ( - - ) - })} - + + {steps.map((step, i) => { + const isError = !!error && i === stepIndex + return ( + + ) + })} + {error && + shouldRenderRetry && ( + + + + + )} + ) } } -const DeviceInteractionContainer = styled(Box).attrs({})`` - export default DeviceInteraction diff --git a/src/components/GenuineCheck.js b/src/components/GenuineCheck.js index 3528b50e..d673f5d7 100644 --- a/src/components/GenuineCheck.js +++ b/src/components/GenuineCheck.js @@ -101,7 +101,7 @@ class GenuineCheck extends PureComponent { } render() { - const { onSuccess } = this.props + const { onSuccess, ...props } = this.props const steps = [ { id: 'device', @@ -146,6 +146,7 @@ class GenuineCheck extends PureComponent { steps={steps} onSuccess={onSuccess} onFail={this.handleFail} + {...props} /> ) } diff --git a/src/components/ManagerPage/ManagerGenuineCheck.js b/src/components/ManagerPage/ManagerGenuineCheck.js index d637db48..27d34841 100644 --- a/src/components/ManagerPage/ManagerGenuineCheck.js +++ b/src/components/ManagerPage/ManagerGenuineCheck.js @@ -21,7 +21,8 @@ class ManagerGenuineCheck extends PureComponent { render() { const { t, onSuccess } = this.props return ( - + + { - { - console.log(`fail`) - }} - onUnavailable={() => { - console.log(`unavailable`) - }} - /> + ) } diff --git a/src/components/ManagerPage/index.js b/src/components/ManagerPage/index.js index a8e3ca65..634dce65 100644 --- a/src/components/ManagerPage/index.js +++ b/src/components/ManagerPage/index.js @@ -22,9 +22,11 @@ type State = { class ManagerPage extends PureComponent { state = { isGenuine: null, + device: null, + deviceInfo: null, } - handleSuccessGenuine = ({ device, deviceInfo }) => { + handleSuccessGenuine = ({ device, deviceInfo }: { device: Device, deviceInfo: DeviceInfo }) => { this.setState({ isGenuine: true, device, deviceInfo }) } From 52f18e07597a4dcfbc94213e980f3405d5dd93ec Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 13:49:50 +0200 Subject: [PATCH 15/33] Move EnsureDevice out of its folder --- src/components/{Workflow => }/EnsureDevice.js | 0 src/components/ManagerPage/FirmwareUpdate.js | 2 +- src/components/ManagerPage/index.js | 3 ++- src/components/Workflow/index.js | 2 +- src/components/modals/Debug.js | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) rename src/components/{Workflow => }/EnsureDevice.js (100%) diff --git a/src/components/Workflow/EnsureDevice.js b/src/components/EnsureDevice.js similarity index 100% rename from src/components/Workflow/EnsureDevice.js rename to src/components/EnsureDevice.js diff --git a/src/components/ManagerPage/FirmwareUpdate.js b/src/components/ManagerPage/FirmwareUpdate.js index e6b80de1..4cb3dc9c 100644 --- a/src/components/ManagerPage/FirmwareUpdate.js +++ b/src/components/ManagerPage/FirmwareUpdate.js @@ -25,7 +25,7 @@ import Button from 'components/base/Button' import NanoS from 'icons/device/NanoS' import CheckFull from 'icons/CheckFull' -import { PreventDeviceChangeRecheck } from '../Workflow/EnsureDevice' +import { PreventDeviceChangeRecheck } from 'components/EnsureDevice' import UpdateFirmwareButton from './UpdateFirmwareButton' let CACHED_LATEST_FIRMWARE = null diff --git a/src/components/ManagerPage/index.js b/src/components/ManagerPage/index.js index 634dce65..56035d87 100644 --- a/src/components/ManagerPage/index.js +++ b/src/components/ManagerPage/index.js @@ -26,7 +26,8 @@ class ManagerPage extends PureComponent { deviceInfo: null, } - handleSuccessGenuine = ({ device, deviceInfo }: { device: Device, deviceInfo: DeviceInfo }) => { + // prettier-ignore + handleSuccessGenuine = ({ device, deviceInfo }: { device: Device, deviceInfo: DeviceInfo }) => { // eslint-disable-line react/no-unused-prop-types this.setState({ isGenuine: true, device, deviceInfo }) } diff --git a/src/components/Workflow/index.js b/src/components/Workflow/index.js index 728ff9ec..a5cd2101 100644 --- a/src/components/Workflow/index.js +++ b/src/components/Workflow/index.js @@ -6,7 +6,7 @@ import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' import type { Node } from 'react' import type { Device } from 'types/common' -import EnsureDevice from './EnsureDevice' +import EnsureDevice from 'components/EnsureDevice' import EnsureDashboard from './EnsureDashboard' import EnsureGenuine from './EnsureGenuine' diff --git a/src/components/modals/Debug.js b/src/components/modals/Debug.js index a18abcb0..22968053 100644 --- a/src/components/modals/Debug.js +++ b/src/components/modals/Debug.js @@ -7,7 +7,7 @@ import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Moda import Button from 'components/base/Button' import Box from 'components/base/Box' import Input from 'components/base/Input' -import EnsureDevice from 'components/Workflow/EnsureDevice' +import EnsureDevice from 'components/EnsureDevice' import { getDerivations } from 'helpers/derivations' import getAddress from 'commands/getAddress' import testInterval from 'commands/testInterval' From 2adbed70e1fd03511c509b08241a05686f9a0c2a Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 13:51:23 +0200 Subject: [PATCH 16/33] Remove Workflow --- src/components/Workflow/EnsureDashboard.js | 81 -------- src/components/Workflow/EnsureGenuine.js | 88 --------- src/components/Workflow/WorkflowDefault.js | 171 ----------------- src/components/Workflow/WorkflowWithIcon.js | 194 -------------------- src/components/Workflow/index.js | 93 ---------- 5 files changed, 627 deletions(-) delete mode 100644 src/components/Workflow/EnsureDashboard.js delete mode 100644 src/components/Workflow/EnsureGenuine.js delete mode 100644 src/components/Workflow/WorkflowDefault.js delete mode 100644 src/components/Workflow/WorkflowWithIcon.js delete mode 100644 src/components/Workflow/index.js diff --git a/src/components/Workflow/EnsureDashboard.js b/src/components/Workflow/EnsureDashboard.js deleted file mode 100644 index bc762558..00000000 --- a/src/components/Workflow/EnsureDashboard.js +++ /dev/null @@ -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 { - 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 diff --git a/src/components/Workflow/EnsureGenuine.js b/src/components/Workflow/EnsureGenuine.js deleted file mode 100644 index 3be90a83..00000000 --- a/src/components/Workflow/EnsureGenuine.js +++ /dev/null @@ -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 { - 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 diff --git a/src/components/Workflow/WorkflowDefault.js b/src/components/Workflow/WorkflowDefault.js deleted file mode 100644 index 664fedd8..00000000 --- a/src/components/Workflow/WorkflowDefault.js +++ /dev/null @@ -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 }) => ( - - {checked ? ( - - - - ) : hasErrors ? ( - - - - ) : ( - - )} - -) - -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) => ( - - - - - - - - - Connect and unlock your Ledger device - - - - - - - - - - - - - - - - {'Navigate to the '} - {'dashboard'} - {' on your device'} - - - - - - - {/* GENUINE CHECK */} - {/* ------------- */} - - - - - - - - - - - {'Allow the '} - {'Ledger Manager'} - {' on your device'} - - - - - - -) - -export default translate()(WorkflowDefault) diff --git a/src/components/Workflow/WorkflowWithIcon.js b/src/components/Workflow/WorkflowWithIcon.js deleted file mode 100644 index 6e0fdd9a..00000000 --- a/src/components/Workflow/WorkflowWithIcon.js +++ /dev/null @@ -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 }) => ( - - {checked ? ( - - - - ) : hasErrors ? ( - - - - ) : ( - - )} - -) - -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) => ( - - - connect your device - - {t('app:manager.device.title')} - - - {t('app:manager.device.desc')} - - - - {/* DEVICE CHECK */} - - - - - - - - {'Connect and unlock your '} - Ledger device - - - - - - - {/* DASHBOARD CHECK */} - - - - - - - - - - {'Navigate to the '} - {'dashboard'} - {' on your device'} - - - - - - - {/* GENUINE CHECK */} - - - - - - - - - - {'Allow the '} - {'Ledger Manager'} - {' on your device'} - - - - - - - -) - -export default translate()(WorkflowWithIcon) diff --git a/src/components/Workflow/index.js b/src/components/Workflow/index.js deleted file mode 100644 index a5cd2101..00000000 --- a/src/components/Workflow/index.js +++ /dev/null @@ -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 'components/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 { - render() { - const { - renderDashboard, - renderFinalUpdate, - renderMcuUpdate, - renderError, - renderDefault, - onGenuineCheck, - } = this.props - return ( - - {(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 ( - - {(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, - }) - }} - - ) - }} - - )} - - ) - } -} - -export default Workflow From 7a19d8b2640d65144f2cf2dcd16a06cbd672b868 Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 14:21:09 +0200 Subject: [PATCH 17/33] Ability to make an interaction polling throw on specific errors --- src/components/EnsureDeviceApp.js | 75 ++++++++++++------------ src/helpers/getAddressForCurrency/btc.js | 2 +- src/helpers/promise.js | 13 +++- 3 files changed, 51 insertions(+), 39 deletions(-) diff --git a/src/components/EnsureDeviceApp.js b/src/components/EnsureDeviceApp.js index 0c160e68..7685c77c 100644 --- a/src/components/EnsureDeviceApp.js +++ b/src/components/EnsureDeviceApp.js @@ -12,6 +12,7 @@ import getAddress from 'commands/getAddress' import { createCancelablePolling } from 'helpers/promise' import { standardDerivation } from 'helpers/derivations' import { isSegwitAccount } from 'helpers/bip32' +import { BtcUnmatchedApp } from 'helpers/getAddressForCurrency/btc' import DeviceInteraction from 'components/DeviceInteraction' import Text from 'components/base/Text' @@ -45,44 +46,31 @@ class EnsureDeviceApp extends Component<{ }) openAppInteractionHandler = ({ device }) => - createCancelablePolling(async () => { - const { account, currency } = this.props - const cur = account ? account.currency : currency - invariant(cur, 'No currency given') - const { address } = await getAddress - .send({ - devicePath: device.path, - currencyId: cur.id, - path: account - ? account.freshAddressPath - : standardDerivation({ currency: cur, segwit: false, x: 0 }), - segwit: account ? isSegwitAccount(account) : false, - }) - .toPromise() - .catch(e => { - if ( - e && - (e.name === 'TransportStatusError' || - // we don't want these error to appear (caused by usb disconnect..) - e.message === 'could not read from HID device' || - e.message === 'Cannot write to HID device') - ) { - throw new WrongAppOpened(`WrongAppOpened ${cur.id}`, { currencyName: cur.name }) + createCancelablePolling( + async () => { + const { account, currency: _currency } = this.props + const currency = account ? account.currency : _currency + invariant(currency, 'No currency given') + const address = await getAddressFromAccountOrCurrency(device, account, currency) + if (account) { + const { freshAddress } = account + if (account && freshAddress !== address) { + logger.warn({ freshAddress, address }) + throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, { + accountName: account.name, + }) } - throw e - }) - - if (account) { - const { freshAddress } = account - if (account && freshAddress !== address) { - logger.warn({ freshAddress, address }) - throw new WrongDeviceForAccount(`WrongDeviceForAccount ${account.name}`, { - accountName: account.name, - }) } - } - return address - }) + return address + }, + { + shouldThrow: (err: Error) => { + const isWrongApp = err instanceof BtcUnmatchedApp + const isWrongDevice = err instanceof WrongDeviceForAccount + return isWrongApp || isWrongDevice + }, + }, + ) renderOpenAppTitle = () => { const { account, currency } = this.props @@ -103,6 +91,7 @@ class EnsureDeviceApp extends Component<{ const Icon = cur ? getCryptoCurrencyIcon(cur) : null return ( , diff --git a/src/helpers/promise.js b/src/helpers/promise.js index 7142c7ff..2918c5f7 100644 --- a/src/helpers/promise.js +++ b/src/helpers/promise.js @@ -32,20 +32,29 @@ export function idleCallback() { return new Promise(resolve => window.requestIdleCallback(resolve)) } +type CancellablePollingOpts = { + pollingInterval?: number, + shouldThrow?: Error => boolean, +} + export function createCancelablePolling( job: any => Promise, - { pollingInterval = 500 }: { pollingInterval: number } = {}, + { pollingInterval = 500, shouldThrow }: CancellablePollingOpts = {}, ) { let isUnsub = false const unsubscribe = () => (isUnsub = true) const getUnsub = () => isUnsub - const promise = new Promise(resolve => { + const promise = new Promise((resolve, reject) => { async function poll() { try { const res = await job() if (getUnsub()) return resolve(res) } catch (err) { + if (shouldThrow && shouldThrow(err)) { + reject(err) + return + } await delay(pollingInterval) if (getUnsub()) return poll() From e07e9686e573863b746f01668abce711ded23af0 Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 14:34:16 +0200 Subject: [PATCH 18/33] Use TranslatedError on DeviceInteraction --- src/components/DeviceInteraction/components.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/DeviceInteraction/components.js b/src/components/DeviceInteraction/components.js index dfce363d..32a83123 100644 --- a/src/components/DeviceInteraction/components.js +++ b/src/components/DeviceInteraction/components.js @@ -7,6 +7,7 @@ import Tooltip from 'components/base/Tooltip' import { radii, colors } from 'styles/theme' import { rgba } from 'styles/helpers' +import TranslatedError from 'components/TranslatedError' import Box from 'components/base/Box' import Spinner from 'components/base/Spinner' import IconCheck from 'icons/Check' @@ -131,7 +132,7 @@ export const ErrorDescContainer = ({ > - {error.message || 'Failed'} + 'Retry'} style={{ display: 'flex', alignItems: 'center' }}> From 32c691d3c25c7bf2251b11be9b529e80ac858b3d Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 14:50:28 +0200 Subject: [PATCH 19/33] Cleaner design for error desc on device interaction --- .../DeviceInteraction/components.js | 52 ++++--------------- src/components/DeviceInteraction/index.js | 12 +---- 2 files changed, 12 insertions(+), 52 deletions(-) diff --git a/src/components/DeviceInteraction/components.js b/src/components/DeviceInteraction/components.js index 32a83123..a6cb01ae 100644 --- a/src/components/DeviceInteraction/components.js +++ b/src/components/DeviceInteraction/components.js @@ -2,17 +2,17 @@ import React from 'react' import styled from 'styled-components' -import Tooltip from 'components/base/Tooltip' -import { radii, colors } from 'styles/theme' +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 IconRecover from 'icons/Recover' +import IconExclamationCircle from 'icons/ExclamationCircle' export const DeviceInteractionStepContainer = styled(Box).attrs({ horizontal: true, @@ -92,25 +92,6 @@ export const ErrorContainer = ({ isVisible }: { isVisible: boolean }) => ( ) -const ErrorRetryContainer = styled(Box).attrs({ - bg: rgba(colors.alertRed, 0.1), - borderRadius: 1, - grow: 1, - color: 'alertRed', - cursor: 'pointer', - p: 1, - align: 'center', - justify: 'center', - overflow: 'hidden', -})` - &:hover { - background-color: ${() => rgba(colors.alertRed, 0.1)}; - } - &:active { - background-color: ${() => rgba(colors.alertRed, 0.15)}; - } -` - export const ErrorDescContainer = ({ error, onRetry, @@ -119,26 +100,13 @@ export const ErrorDescContainer = ({ error: Error, onRetry: void => void, }) => ( - - - - - - 'Retry'} style={{ display: 'flex', alignItems: 'center' }}> - - - - + + + + + + {'Retry'} + ) diff --git a/src/components/DeviceInteraction/index.js b/src/components/DeviceInteraction/index.js index 1759c41d..5542aaaa 100644 --- a/src/components/DeviceInteraction/index.js +++ b/src/components/DeviceInteraction/index.js @@ -5,14 +5,11 @@ import React, { PureComponent } from 'react' import { delay } from 'helpers/promise' import Box from 'components/base/Box' -import Space from 'components/base/Space' - import DeviceInteractionStep from './DeviceInteractionStep' +import { ErrorDescContainer } from './components' import type { Step } from './DeviceInteractionStep' -import { ErrorDescContainer } from './components' - type Props = { steps: Step[], onSuccess?: any => void, @@ -109,12 +106,7 @@ class DeviceInteraction extends PureComponent { ) })} {error && - shouldRenderRetry && ( - - - - - )} + shouldRenderRetry && } ) } From a6cc0cf78c09920c8ef23d82bf1462546e99a1ca Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 15:22:47 +0200 Subject: [PATCH 20/33] Remove unused code --- .../modals/Receive/01-step-account.js | 25 -------- .../modals/Receive/02-step-connect-device.js | 16 ----- .../modals/Receive/03-step-confirm-address.js | 59 ------------------- .../modals/Receive/04-step-receive-funds.js | 48 --------------- 4 files changed, 148 deletions(-) delete mode 100644 src/components/modals/Receive/01-step-account.js delete mode 100644 src/components/modals/Receive/02-step-connect-device.js delete mode 100644 src/components/modals/Receive/03-step-confirm-address.js delete mode 100644 src/components/modals/Receive/04-step-receive-funds.js diff --git a/src/components/modals/Receive/01-step-account.js b/src/components/modals/Receive/01-step-account.js deleted file mode 100644 index 8e8a6de9..00000000 --- a/src/components/modals/Receive/01-step-account.js +++ /dev/null @@ -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) => ( - - - - - -) diff --git a/src/components/modals/Receive/02-step-connect-device.js b/src/components/modals/Receive/02-step-connect-device.js deleted file mode 100644 index 82e61aeb..00000000 --- a/src/components/modals/Receive/02-step-connect-device.js +++ /dev/null @@ -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 ( - - - - - ) - } -} - -export default ReceiveStepConnectDevice diff --git a/src/components/modals/Receive/03-step-confirm-address.js b/src/components/modals/Receive/03-step-confirm-address.js deleted file mode 100644 index 27e859ab..00000000 --- a/src/components/modals/Receive/03-step-confirm-address.js +++ /dev/null @@ -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) => ( - - - {props.addressVerified === false ? ( - - {props.t('app:receive.steps.confirmAddress.error.title')} - {props.t('app:receive.steps.confirmAddress.error.text')} - - - ) : ( - - {props.t('app:receive.steps.confirmAddress.action')} - {props.t('app:receive.steps.confirmAddress.text')} - {props.account && } - {props.device && - props.account && } - - )} - -) diff --git a/src/components/modals/Receive/04-step-receive-funds.js b/src/components/modals/Receive/04-step-receive-funds.js deleted file mode 100644 index 8dcceb2d..00000000 --- a/src/components/modals/Receive/04-step-receive-funds.js +++ /dev/null @@ -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) => ( - - - - - - - {props.account && ( - - )} - -) From f69853a4c02229db236255b208cc53776af60a76 Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 15:32:04 +0200 Subject: [PATCH 21/33] Handle retry on Receive modal --- src/components/modals/Receive/index.js | 3 +++ .../Receive/steps/03-step-confirm-address.js | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/modals/Receive/index.js b/src/components/modals/Receive/index.js index 5be3fd1c..d1b4914b 100644 --- a/src/components/modals/Receive/index.js +++ b/src/components/modals/Receive/index.js @@ -49,6 +49,7 @@ export type StepProps = DefaultStepProps & { closeModal: void => void, isAppOpened: boolean, isAddressVerified: ?boolean, + onRetry: void => void, onSkipConfirm: void => void, onResetSkip: void => void, onChangeAccount: (?Account) => void, @@ -121,6 +122,7 @@ class ReceiveModal extends PureComponent { } } + handleRetry = () => this.setState({ isAddressVerified: null, errorSteps: [] }) handleReset = () => this.setState({ ...INITIAL_STATE }) handleCloseModal = () => this.props.closeModal(MODAL_RECEIVE) handleStepChange = step => this.setState({ stepId: step.id }) @@ -166,6 +168,7 @@ class ReceiveModal extends PureComponent { isAppOpened, isAddressVerified, closeModal: this.handleCloseModal, + onRetry: this.handleRetry, onSkipConfirm: this.handleSkipConfirm, onResetSkip: this.handleResetSkip, onChangeAccount: this.handleChangeAccount, diff --git a/src/components/modals/Receive/steps/03-step-confirm-address.js b/src/components/modals/Receive/steps/03-step-confirm-address.js index 7f7b9187..debdc74a 100644 --- a/src/components/modals/Receive/steps/03-step-confirm-address.js +++ b/src/components/modals/Receive/steps/03-step-confirm-address.js @@ -73,9 +73,23 @@ export default class StepConfirmAddress extends PureComponent { } } -export function StepConfirmAddressFooter({ t }: StepProps) { +export function StepConfirmAddressFooter({ t, transitionTo, onRetry }: StepProps) { // This will be displayed only if user rejected address - return + return ( + + + + + ) } const Container = styled(Box).attrs({ From 45c1691bd1f177d4db96e3f4aad7dce411089cb3 Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 15:45:46 +0200 Subject: [PATCH 22/33] Remove highlight color of DeviceInteraction when finished --- src/components/DeviceInteraction/DeviceInteractionStep.js | 3 +++ src/components/DeviceInteraction/components.js | 2 +- src/components/DeviceInteraction/index.js | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/DeviceInteraction/DeviceInteractionStep.js b/src/components/DeviceInteraction/DeviceInteractionStep.js index 5fe9a077..9b77fb4a 100644 --- a/src/components/DeviceInteraction/DeviceInteractionStep.js +++ b/src/components/DeviceInteraction/DeviceInteractionStep.js @@ -29,6 +29,7 @@ type Props = { isFirst: boolean, isLast: boolean, isActive: boolean, + isFinished: boolean, isPrecedentActive: boolean, isError: boolean, isSuccess: boolean, @@ -152,6 +153,7 @@ class DeviceInteractionStep extends PureComponent< isFirst, isLast, isActive, + isFinished, isPrecedentActive, isSuccess, isError, @@ -169,6 +171,7 @@ class DeviceInteractionStep extends PureComponent< p.theme.colors.fog}; border-color: ${p => - p.isError ? p.theme.colors.alertRed : p.isActive ? p.theme.colors.wallet : ''}; + 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)}; diff --git a/src/components/DeviceInteraction/index.js b/src/components/DeviceInteraction/index.js index 5542aaaa..b11e038b 100644 --- a/src/components/DeviceInteraction/index.js +++ b/src/components/DeviceInteraction/index.js @@ -99,6 +99,7 @@ class DeviceInteraction extends PureComponent { isActive={i === stepIndex} isPassed={i < stepIndex} isSuccess={i < stepIndex || (i === stepIndex && isSuccess)} + isFinished={isSuccess} onSuccess={this.handleSuccess} onFail={this.handleFail} data={data} From fe3d14311764e377de8bf60feffb75cc04830798 Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 15:48:11 +0200 Subject: [PATCH 23/33] Correct reset state when retrying verify address --- src/components/modals/Receive/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modals/Receive/index.js b/src/components/modals/Receive/index.js index d1b4914b..d5cb47a8 100644 --- a/src/components/modals/Receive/index.js +++ b/src/components/modals/Receive/index.js @@ -122,7 +122,7 @@ class ReceiveModal extends PureComponent { } } - handleRetry = () => this.setState({ isAddressVerified: null, errorSteps: [] }) + handleRetry = () => this.setState({ isAddressVerified: null, isAppOpened: false, errorSteps: [] }) handleReset = () => this.setState({ ...INITIAL_STATE }) handleCloseModal = () => this.props.closeModal(MODAL_RECEIVE) handleStepChange = step => this.setState({ stepId: step.id }) From df77a8bc3c8f39d4bd5ddba4d1cc07202af9a913 Mon Sep 17 00:00:00 2001 From: Anastasia Poupeney Date: Wed, 27 Jun 2018 15:53:36 +0200 Subject: [PATCH 24/33] shorten the title for PR template --- PULL_REQUEST_TEMPLATE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index f97490fb..adf8ebb2 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,16 +1,16 @@ -## What is the type of this PR? +## Type -## Any background context and/or relevant tickets/issues you want to provide with? +## Context -## Short description on what this PR suppose to do? +## Description -## Any special conditions required for testing? +## Testing From 48862c1efd82dfd6bdc5c8b242c83c8c1e6c4915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 27 Jun 2018 16:32:01 +0200 Subject: [PATCH 25/33] Update PULL_REQUEST_TEMPLATE.md --- PULL_REQUEST_TEMPLATE.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index adf8ebb2..e020462d 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,17 +1,17 @@ -## Type +### Type -## Context +### Context -## Description +### Description -## Testing + - +### Testing -## Screenshots (if appropriate) + From c83beeb782bbfe0c8582798394a30c414b332882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 27 Jun 2018 16:34:50 +0200 Subject: [PATCH 26/33] Bugfix account.unit to refresh account page when you change it this also start splitting a bit the AccountPage component as CalculateBalance will memoize things, we want inner part to be potentially re-render if they depend on other things, we do this by connecting them to redux. redux idea is to connect leafs instead of connecting the whole tree when possible, so this fit this paradigm to me. --- .../AccountBalanceSummaryHeader.js | 125 ++++++++++++++ .../AccountPage/AccountHeaderActions.js | 100 +++++++++++ src/components/AccountPage/index.js | 160 +++--------------- src/components/BalanceSummary/index.js | 1 + src/components/CalculateBalance.js | 10 +- 5 files changed, 259 insertions(+), 137 deletions(-) create mode 100644 src/components/AccountPage/AccountBalanceSummaryHeader.js create mode 100644 src/components/AccountPage/AccountHeaderActions.js diff --git a/src/components/AccountPage/AccountBalanceSummaryHeader.js b/src/components/AccountPage/AccountBalanceSummaryHeader.js new file mode 100644 index 00000000..b396e801 --- /dev/null +++ b/src/components/AccountPage/AccountBalanceSummaryHeader.js @@ -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 { + handleChangeSelectedTime = item => { + this.props.saveSettings({ selectedTimeRange: item.key }) + } + + render() { + const { + account, + t, + counterValue, + selectedTimeRange, + isAvailable, + totalBalance, + sinceBalance, + refBalance, + } = this.props + + return ( + + + + + + + + + + + + + + + ) + } +} + +export default compose( + connect( + mapStateToProps, + mapDispatchToProps, + ), + translate(), // FIXME t() is not even needed directly here. should be underlying component responsability to inject it +)(AccountBalanceSummaryHeader) diff --git a/src/components/AccountPage/AccountHeaderActions.js b/src/components/AccountPage/AccountHeaderActions.js new file mode 100644 index 00000000..eb60494b --- /dev/null +++ b/src/components/AccountPage/AccountHeaderActions.js @@ -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 { + render() { + const { account, openModal, t } = this.props + return ( + + {account.operations.length > 0 && ( + + + + + + )} + t('app:account.settings.title')}> + openModal(MODAL_SETTINGS_ACCOUNT, { account })}> + + + + + + + ) + } +} + +export default compose( + connect( + mapStateToProps, + mapDispatchToProps, + ), + translate(), +)(AccountHeaderActions) diff --git a/src/components/AccountPage/index.js b/src/components/AccountPage/index.js index 730ba135..397c6f4e 100644 --- a/src/components/AccountPage/index.js +++ b/src/components/AccountPage/index.js @@ -5,19 +5,8 @@ import { compose } from 'redux' import { connect } from 'react-redux' import { translate } from 'react-i18next' import { Redirect } from 'react-router' -import styled from 'styled-components' import type { Currency, Account } from '@ledgerhq/live-common/lib/types' -import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount' -import Tooltip from 'components/base/Tooltip' -import TrackPage from 'analytics/TrackPage' - -import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants' - import type { T } from 'types/common' - -import { rgba } from 'styles/helpers' - -import { saveSettings } from 'actions/settings' import { accountSelector } from 'reducers/accounts' import { counterValueCurrencySelector, @@ -26,47 +15,19 @@ import { timeRangeDaysByKey, } from 'reducers/settings' import type { TimeRange } from 'reducers/settings' -import { openModal } from 'reducers/modals' - -import IconAccountSettings from 'icons/AccountSettings' -import IconReceive from 'icons/Receive' -import IconSend from 'icons/Send' +import TrackPage from 'analytics/TrackPage' +import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount' import BalanceSummary from 'components/BalanceSummary' -import { - BalanceTotal, - BalanceSinceDiff, - BalanceSincePercent, -} from 'components/BalanceSummary/BalanceInfos' -import Box, { Tabbable } from 'components/base/Box' -import Button from 'components/base/Button' -import FormattedVal from 'components/base/FormattedVal' -import PillsDaysCount from 'components/PillsDaysCount' +import Box from 'components/base/Box' import OperationsList from 'components/OperationsList' import StickyBackToTop from 'components/StickyBackToTop' import AccountHeader from './AccountHeader' +import AccountHeaderActions from './AccountHeaderActions' +import AccountBalanceSummaryHeader from './AccountBalanceSummaryHeader' import EmptyStateAccount from './EmptyStateAccount' -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 = (state, props) => ({ account: accountSelector(state, { accountId: props.match.params.id }), counterValue: counterValueCurrencySelector(state), @@ -74,74 +35,54 @@ const mapStateToProps = (state, props) => ({ selectedTimeRange: selectedTimeRangeSelector(state), }) -const mapDispatchToProps = { - openModal, - saveSettings, -} +const mapDispatchToProps = null type Props = { counterValue: Currency, t: T, account?: Account, - openModal: Function, - saveSettings: ({ selectedTimeRange: TimeRange }) => *, selectedTimeRange: TimeRange, } class AccountPage extends PureComponent { - handleChangeSelectedTime = item => { - this.props.saveSettings({ selectedTimeRange: item.key }) + renderBalanceSummaryHeader = ({ isAvailable, totalBalance, sinceBalance, refBalance }) => { + const { account } = this.props + if (!account) return null + return ( + + ) } - _cacheBalance = null - render() { - const { account, openModal, t, counterValue, selectedTimeRange } = this.props + const { account, t, counterValue, selectedTimeRange } = this.props const daysCount = timeRangeDaysByKey[selectedTimeRange] - // Don't even throw if we jumped in wrong account route if (!account) { return } return ( - // Force re-render account page, for avoid animation + // `key` forces re-render account page when going an another account (skip animations) + + - - {account.operations.length > 0 && ( - - - - - - )} - t('app:account.settings.title')}> - openModal(MODAL_SETTINGS_ACCOUNT, { account })}> - - - - - - + + {account.operations.length > 0 ? ( @@ -152,59 +93,12 @@ class AccountPage extends PureComponent { counterValue={counterValue} daysCount={daysCount} selectedTimeRange={selectedTimeRange} - renderHeader={({ isAvailable, totalBalance, sinceBalance, refBalance }) => ( - - - - - - - - - - - - - - - )} + renderHeader={this.renderBalanceSummaryHeader} /> + + ) : ( diff --git a/src/components/BalanceSummary/index.js b/src/components/BalanceSummary/index.js index a517c3ad..e0b9c02e 100644 --- a/src/components/BalanceSummary/index.js +++ b/src/components/BalanceSummary/index.js @@ -35,6 +35,7 @@ const BalanceSummary = ({ selectedTimeRange, }: Props) => { const account = accounts.length === 1 ? accounts[0] : undefined + // FIXME This nesting 😱 return ( diff --git a/src/components/CalculateBalance.js b/src/components/CalculateBalance.js index d94f0b6e..532372e8 100644 --- a/src/components/CalculateBalance.js +++ b/src/components/CalculateBalance.js @@ -32,6 +32,7 @@ type Props = OwnProps & { balanceStart: number, balanceEnd: number, isAvailable: boolean, + hash: string, } const mapStateToProps = (state: State, props: OwnProps) => { @@ -71,19 +72,20 @@ const mapStateToProps = (state: State, props: OwnProps) => { ({ ...item, originalValue: originalValues[i] || 0 }), ) + const balanceEnd = balanceHistory[balanceHistory.length - 1].value + return { isAvailable, balanceHistory, balanceStart: balanceHistory[0].value, - balanceEnd: balanceHistory[balanceHistory.length - 1].value, + balanceEnd, + hash: `${balanceHistory.length}_${balanceEnd}`, } } -const hash = ({ balanceHistory, balanceEnd }) => `${balanceHistory.length}_${balanceEnd}` - class CalculateBalance extends Component { shouldComponentUpdate(nextProps) { - return hash(nextProps) !== hash(this.props) + return nextProps.hash !== this.props.hash } render() { const { children } = this.props From 77f30df8c1d7f2fe342e5d1b011fed4dd962d839 Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 16:38:39 +0200 Subject: [PATCH 27/33] Cursor `not-allowed` on disabled sidebar items --- src/components/base/SideBar/SideBarListItem.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/base/SideBar/SideBarListItem.js b/src/components/base/SideBar/SideBarListItem.js index 671dee6f..d241fa3c 100644 --- a/src/components/base/SideBar/SideBarListItem.js +++ b/src/components/base/SideBar/SideBarListItem.js @@ -33,7 +33,7 @@ class SideBarListItem extends PureComponent { {!!Icon && } @@ -62,8 +62,7 @@ const Container = styled(Tabbable).attrs({ px: 3, py: 2, })` - cursor: ${p => (p.disabled || p.isActive ? 'default' : 'pointer')}; - pointer-events: ${p => (p.isDisabled ? 'none' : 'auto')}; + cursor: ${p => (p.disabled ? 'not-allowed' : p.isActive ? 'default' : 'pointer')}; color: ${p => (p.isActive ? p.theme.colors.dark : p.theme.colors.smoke)}; background: ${p => (p.isActive ? p.theme.colors.lightGrey : '')}; opacity: ${p => (p.disabled ? 0.5 : 1)}; From d5abb2d4eaa5862603f987d00d89e36d2de8dd8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 27 Jun 2018 16:40:55 +0200 Subject: [PATCH 28/33] Update PULL_REQUEST_TEMPLATE.md --- PULL_REQUEST_TEMPLATE.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index e020462d..9afea908 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,17 +1,13 @@ + + ### Type ### Context - - -### Description - - - - + -### Testing +### Testing / Affected part - + From 71ce2b4599426a322d96a72c5c864a1b89354af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 27 Jun 2018 16:48:50 +0200 Subject: [PATCH 29/33] Update PULL_REQUEST_TEMPLATE.md --- PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 9afea908..0914791c 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -8,6 +8,6 @@ -### Testing / Affected part +### Parts of the app affected / Test plan From badf77f360141af05aa62401e7e7c3246cea743d Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 17:07:34 +0200 Subject: [PATCH 30/33] Give correct props to AppsList --- src/components/ManagerPage/Dashboard.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/ManagerPage/Dashboard.js b/src/components/ManagerPage/Dashboard.js index 51410c31..78f8e383 100644 --- a/src/components/ManagerPage/Dashboard.js +++ b/src/components/ManagerPage/Dashboard.js @@ -31,12 +31,7 @@ const Dashboard = ({ device, deviceInfo, t }: Props) => ( - + ) From a8f75afcb7d90543dd8dfd5a4b71e35fad0ceb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 27 Jun 2018 17:48:45 +0200 Subject: [PATCH 31/33] Add disconnected device event. also sort errors by alphabetic --- src/helpers/deviceAccess.js | 13 +++++++++- static/i18n/en/errors.yml | 47 ++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/helpers/deviceAccess.js b/src/helpers/deviceAccess.js index c380fe0c..f6fbc478 100644 --- a/src/helpers/deviceAccess.js +++ b/src/helpers/deviceAccess.js @@ -4,6 +4,7 @@ import type Transport from '@ledgerhq/hw-transport' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' import { DEBUG_DEVICE } from 'config/constants' import { retry } from './promise' +import { createCustomErrorClass } from './errors' // all open to device must use openDevice so we can prevent race conditions // and guarantee we do one device access at a time. It also will handle the .close() @@ -13,6 +14,16 @@ type WithDevice = (devicePath: string) => (job: (Transport<*>) => Promise) const semaphorePerDevice = {} +const DisconnectedDevice = createCustomErrorClass('DisconnectedDevice') + +const remapError = (p: Promise): Promise => + p.catch(e => { + if (e && e.message && e.message.indexOf('HID') >= 0) { + throw new DisconnectedDevice(e.message) + } + throw e + }) + export const withDevice: WithDevice = devicePath => { const sem = semaphorePerDevice[devicePath] || (semaphorePerDevice[devicePath] = createSemaphore(1)) @@ -23,7 +34,7 @@ export const withDevice: WithDevice = devicePath => { if (DEBUG_DEVICE) t.setDebugMode(true) try { - const res = await job(t) + const res = await remapError(job(t)) // $FlowFixMe return res } finally { diff --git a/static/i18n/en/errors.yml b/static/i18n/en/errors.yml index dc07a20a..01564ee9 100644 --- a/static/i18n/en/errors.yml +++ b/static/i18n/en/errors.yml @@ -1,33 +1,32 @@ -generic: Oops, an unknown error occurred. Please try again or contact Ledger Support. -RangeError: '{{message}}' -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}}.' -LedgerAPIError: 'Ledger API error. Try again. (HTTP {{status}})' -NetworkDown: 'Your internet connection seems down.' -NoAddressesFound: 'No accounts were found.' -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] +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. -LatestMCUInstalledError: MCU on device already up to date. +DisconnectedDevice: 'The device was disconnected.' +Error: '{{message}}' +FeeEstimationFailed: 'Fee estimation error. Try again or set a custom fee (status: {{status}})' +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. -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: 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 - -DeviceNotGenuine: Device is not genuine +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] +WrongAppOpened: 'Open the ‘{{currencyName}}’ app on your Ledger device to proceed.' +WrongDeviceForAccount: 'Use the device associated with the account ‘{{accountName}}’.' From a4050d8103c0cafd9c0befa4248057c9b4ecda9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 27 Jun 2018 18:23:16 +0200 Subject: [PATCH 32/33] Fix confettis to go under the ledger logo. fix flow --- src/components/ConfettiParty/Confetti.js | 3 +- src/components/ConfettiParty/index.js | 66 +++++++++++++-- src/components/Onboarding/helperComponents.js | 2 +- src/components/Onboarding/steps/Finish.js | 84 ++++++++++++------- 4 files changed, 114 insertions(+), 41 deletions(-) diff --git a/src/components/ConfettiParty/Confetti.js b/src/components/ConfettiParty/Confetti.js index 478d26ad..0d8e8028 100644 --- a/src/components/ConfettiParty/Confetti.js +++ b/src/components/ConfettiParty/Confetti.js @@ -1,3 +1,4 @@ +// @flow import React, { PureComponent } from 'react' import Animated from 'animated/lib/targets/react-dom' @@ -13,7 +14,7 @@ class Confetti extends PureComponent< delta: [number, number], }, { - value: *, + progress: Animated.Value, }, > { state = { diff --git a/src/components/ConfettiParty/index.js b/src/components/ConfettiParty/index.js index ae6dbeb8..31b46d8b 100644 --- a/src/components/ConfettiParty/index.js +++ b/src/components/ConfettiParty/index.js @@ -1,3 +1,5 @@ +// @flow + import React, { PureComponent } from 'react' import { i } from 'helpers/staticPath' import Confetti from './Confetti' @@ -9,22 +11,68 @@ const shapes = [ i('confetti-shapes/4.svg'), ] -class ConfettiParty extends PureComponent<{}> { - state = { - confettis: Array(64) - .fill(null) - .map((_, i) => ({ - id: i, +let id = 1 + +const nextConfetti = (mode: ?string) => + mode === 'emit' + ? { + id: id++, shape: shapes[Math.floor(shapes.length * Math.random())], initialRotation: 360 * Math.random(), - initialYPercent: -0.2 + 0.1 * Math.random(), + initialYPercent: -0.05, + initialXPercent: + 0.5 + 0.5 * Math.cos(Date.now() / 1000) * (0.5 + 0.5 * Math.sin(Date.now() / 6000)), + initialScale: 1, + rotations: 4 * Math.random() - 2, + delta: [(Math.random() - 0.5) * 200, 600 + 200 * Math.random()], + duration: 10000, + } + : { + id: id++, + shape: shapes[Math.floor(shapes.length * Math.random())], + initialRotation: 360 * Math.random(), + initialYPercent: -0.15 * Math.random(), initialXPercent: 0.2 + 0.6 * Math.random(), initialScale: 1, - rotations: 4 + 4 * Math.random(), + rotations: 8 * Math.random() - 4, delta: [(Math.random() - 0.5) * 600, 300 + 300 * Math.random()], duration: 6000 + 5000 * Math.random(), - })), + } + +class ConfettiParty extends PureComponent<{ emit: boolean }, { confettis: Array }> { + state = { + // $FlowFixMe + confettis: Array(64) + .fill(null) + .map(nextConfetti), + } + + componentDidMount() { + this.setEmit(this.props.emit) + } + + componentDidUpdate(prevProps: *) { + if (this.props.emit !== prevProps.emit) { + this.setEmit(this.props.emit) + } + } + + componentWillUnmount() { + this.setEmit(false) + } + + setEmit(on: boolean) { + if (on) { + this.interval = setInterval(() => { + this.setState(({ confettis }) => ({ + confettis: confettis.slice(confettis.length > 200 ? 1 : 0).concat(nextConfetti('emit')), + })) + }, 40) + } else { + clearInterval(this.interval) + } } + interval: * render() { const { confettis } = this.state diff --git a/src/components/Onboarding/helperComponents.js b/src/components/Onboarding/helperComponents.js index a45ca150..d0e02d79 100644 --- a/src/components/Onboarding/helperComponents.js +++ b/src/components/Onboarding/helperComponents.js @@ -61,10 +61,10 @@ export const LiveLogoContainer = styled(Box).attrs({ alignItems: 'center', justifyContent: 'center', })` + background-color: white; box-shadow: 0 2px 24px 0 #00000014; width: ${p => (p.width ? p.width : 80)} height: ${p => (p.height ? p.height : 80)} - ` // INSTRUCTION LIST diff --git a/src/components/Onboarding/steps/Finish.js b/src/components/Onboarding/steps/Finish.js index 2fb41697..05ba2f96 100644 --- a/src/components/Onboarding/steps/Finish.js +++ b/src/components/Onboarding/steps/Finish.js @@ -1,6 +1,6 @@ // @flow -import React from 'react' +import React, { Component } from 'react' import { shell } from 'electron' import styled from 'styled-components' import { i } from 'helpers/staticPath' @@ -48,39 +48,63 @@ const socialMedia = [ }, ] -export default (props: StepProps) => { - const { finish, t } = props - return ( - - - - - - - } - /> - - +export default class Finish extends Component { + state = { emit: false } + onMouseUp = () => this.setState({ emit: false }) + onMouseDown = () => { + this.setState({ emit: true }) + } + onMouseLeave = () => { + this.setState({ emit: false }) + } + render() { + const { finish, t } = this.props + const { emit } = this.state + return ( + + + + + + + + } + /> + + + - - - {t('onboarding:finish.title')} - {t('onboarding:finish.desc')} - - - - - - {socialMedia.map(socMed => )} + + {t('onboarding:finish.title')} + {t('onboarding:finish.desc')} + + + + + + {socialMedia.map(socMed => )} + - - ) + ) + } } type SocMed = { From 39eb6d4e33e17011a733281da1bcd23182aa025b Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 27 Jun 2018 18:26:08 +0200 Subject: [PATCH 33/33] Update README.md and add all-os-compatible hard reset --- README.md | 10 ++++++---- package.json | 2 +- scripts/reset-files.sh | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 scripts/reset-files.sh diff --git a/README.md b/README.md index e0a9745b..03d83eef 100644 --- a/README.md +++ b/README.md @@ -120,12 +120,14 @@ yarn flow # launch flow yarn test # launch unit tests ``` -### Programmaically reset hard the app - -Stop the app and to clean accounts, settings, etc, run +### Programmatically reset app files ```bash -rm -rf ~/Library/Application\ Support/Electron/ +# clear the dev electron user data directory +# it remove sqlite db, accounts, settings +# useful to start from a fresh state + +yarn reset-files ``` ## File structure diff --git a/package.json b/package.json index afa5a012..c41bca4b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "release": "bash ./scripts/release.sh", "start": "bash ./scripts/start.sh", "storybook": "NODE_ENV=development STORYBOOK_ENV=1 start-storybook -s ./static -p 4444", - "trans": "node scripts/trans" + "reset-files": "bash ./scripts/reset-files.sh" }, "electronWebpack": { "title": true, diff --git a/scripts/reset-files.sh b/scripts/reset-files.sh new file mode 100644 index 00000000..11820bc7 --- /dev/null +++ b/scripts/reset-files.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +echo "> Getting user data folder..." + +TMP_FILE=`mktemp` +cat < $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