diff --git a/src/components/modals/AddAccounts/index.js b/src/components/modals/AddAccounts/index.js index 239b0d13..3d56f246 100644 --- a/src/components/modals/AddAccounts/index.js +++ b/src/components/modals/AddAccounts/index.js @@ -1,6 +1,5 @@ // @flow -import invariant from 'invariant' import React, { PureComponent } from 'react' import { compose } from 'redux' import { connect } from 'react-redux' @@ -13,59 +12,66 @@ import type { Currency, Account } from '@ledgerhq/live-common/lib/types' import { MODAL_ADD_ACCOUNTS } from 'config/constants' import type { T, Device } from 'types/common' +import type { StepProps as DefaultStepProps, Step } from 'components/base/Stepper' +import { idleCallback } from 'helpers/promise' import { getCurrentDevice } from 'reducers/devices' import { accountsSelector } from 'reducers/accounts' import { addAccount } from 'actions/accounts' import { closeModal } from 'reducers/modals' -import Modal, { ModalContent, ModalTitle, ModalFooter, ModalBody } from 'components/base/Modal' -import Box from 'components/base/Box' -import Breadcrumb from 'components/Breadcrumb' +import Modal from 'components/base/Modal' +import Stepper from 'components/base/Stepper' import StepChooseCurrency, { StepChooseCurrencyFooter } from './steps/01-step-choose-currency' import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device' import StepImport, { StepImportFooter } from './steps/03-step-import' import StepFinish from './steps/04-step-finish' -const createSteps = ({ t }: { t: T }) => [ - { - id: 'chooseCurrency', - label: t('app:addAccounts.breadcrumb.informations'), - component: StepChooseCurrency, - footer: StepChooseCurrencyFooter, - onBack: null, - hideFooter: false, - }, - { - id: 'connectDevice', - label: t('app:addAccounts.breadcrumb.connectDevice'), - component: StepConnectDevice, - footer: StepConnectDeviceFooter, - onBack: ({ transitionTo }: StepProps) => transitionTo('chooseCurrency'), - hideFooter: false, - }, - { - id: 'import', - label: t('app:addAccounts.breadcrumb.import'), - component: StepImport, - footer: StepImportFooter, - onBack: ({ transitionTo }: StepProps) => transitionTo('chooseCurrency'), - hideFooter: false, - }, - { - id: 'finish', - label: t('app:addAccounts.breadcrumb.finish'), - component: StepFinish, - footer: null, - onBack: null, - hideFooter: true, - }, -] +const createSteps = ({ t }: { t: T }) => { + const onBack = ({ transitionTo, resetScanState }: StepProps) => { + resetScanState() + transitionTo('chooseCurrency') + } + return [ + { + id: 'chooseCurrency', + label: t('app:addAccounts.breadcrumb.informations'), + component: StepChooseCurrency, + footer: StepChooseCurrencyFooter, + onBack: null, + hideFooter: false, + }, + { + id: 'connectDevice', + label: t('app:addAccounts.breadcrumb.connectDevice'), + component: StepConnectDevice, + footer: StepConnectDeviceFooter, + onBack, + hideFooter: false, + }, + { + id: 'import', + label: t('app:addAccounts.breadcrumb.import'), + component: StepImport, + footer: StepImportFooter, + onBack, + hideFooter: false, + }, + { + id: 'finish', + label: t('app:addAccounts.breadcrumb.finish'), + component: StepFinish, + footer: null, + onBack: null, + hideFooter: true, + }, + ] +} type Props = { t: T, - currentDevice: ?Device, + device: ?Device, existingAccounts: Account[], closeModal: string => void, addAccount: Account => void, @@ -75,37 +81,39 @@ type StepId = 'chooseCurrency' | 'connectDevice' | 'import' | 'finish' type ScanStatus = 'idle' | 'scanning' | 'error' | 'finished' type State = { - stepId: StepId, + // TODO: I'm sure there will be always StepId and ScanStatus given, + // but I struggle making flow understand it. So I put string as fallback + stepId: StepId | string, + scanStatus: ScanStatus | string, + isAppOpened: boolean, currency: ?Currency, - - // scan process scannedAccounts: Account[], checkedAccountsIds: string[], - scanStatus: ScanStatus, err: ?Error, } -export type StepProps = { +export type StepProps = DefaultStepProps & { t: T, currency: ?Currency, - currentDevice: ?Device, + device: ?Device, isAppOpened: boolean, - transitionTo: StepId => void, - setState: any => void, - onClickAdd: void => Promise, - onCloseModal: void => void, - - // scan process scannedAccounts: Account[], existingAccounts: Account[], checkedAccountsIds: string[], scanStatus: ScanStatus, err: ?Error, + onClickAdd: void => Promise, + onCloseModal: void => void, + resetScanState: void => void, + setCurrency: (?Currency) => void, + setAppOpened: boolean => void, + setScanStatus: (ScanStatus, ?Error) => string, + setScannedAccounts: ({ scannedAccounts?: Account[], checkedAccountsIds?: string[] }) => void, } const mapStateToProps = createStructuredSelector({ - currentDevice: getCurrentDevice, + device: getCurrentDevice, existingAccounts: accountsSelector, }) @@ -126,18 +134,7 @@ const INITIAL_STATE = { class AddAccounts extends PureComponent { state = INITIAL_STATE - STEPS = createSteps({ - t: this.props.t, - }) - - transitionTo = stepId => { - const { currency } = this.state - let nextState = { stepId } - if (stepId === 'chooseCurrency') { - nextState = { ...INITIAL_STATE, currency } - } - this.setState(nextState) - } + STEPS = createSteps({ t: this.props.t }) handleClickAdd = async () => { const { addAccount } = this.props @@ -151,16 +148,43 @@ class AddAccounts extends PureComponent { await idleCallback() addAccount(accountsToAdd[i]) } - this.transitionTo('finish') } - handleCloseModal = () => { - const { closeModal } = this.props - closeModal(MODAL_ADD_ACCOUNTS) + handleCloseModal = () => this.props.closeModal(MODAL_ADD_ACCOUNTS) + handleStepChange = (step: Step) => this.setState({ stepId: step.id }) + + handleSetCurrency = (currency: ?Currency) => this.setState({ currency }) + + handleSetScanStatus = (scanStatus: string, err: ?Error = null) => { + this.setState({ scanStatus, err }) + } + + handleSetScannedAccounts = ({ + checkedAccountsIds, + scannedAccounts, + }: { + checkedAccountsIds: string[], + scannedAccounts: Account[], + }) => { + this.setState({ + ...(checkedAccountsIds ? { checkedAccountsIds } : {}), + ...(scannedAccounts ? { scannedAccounts } : {}), + }) + } + + handleResetScanState = () => { + this.setState({ + scanStatus: 'idle', + err: null, + scannedAccounts: [], + checkedAccountsIds: [], + }) } + handleSetAppOpened = (isAppOpened: boolean) => this.setState({ isAppOpened }) + render() { - const { t, currentDevice, existingAccounts } = this.props + const { t, device, existingAccounts } = this.props const { stepId, currency, @@ -171,17 +195,9 @@ class AddAccounts extends PureComponent { err, } = this.state - const stepIndex = this.STEPS.findIndex(s => s.id === stepId) - const step = this.STEPS[stepIndex] - - invariant(step, `AddAccountsModal: step ${stepId} doesn't exists`) - - const { component: StepComponent, footer: StepFooter, hideFooter, onBack } = step - - const stepProps: StepProps = { - t, + const addtionnalProps = { currency, - currentDevice, + device, existingAccounts, scannedAccounts, checkedAccountsIds, @@ -190,8 +206,11 @@ class AddAccounts extends PureComponent { isAppOpened, onClickAdd: this.handleClickAdd, onCloseModal: this.handleCloseModal, - transitionTo: this.transitionTo, - setState: (...args) => this.setState(...args), + setScanStatus: this.handleSetScanStatus, + setCurrency: this.handleSetCurrency, + setScannedAccounts: this.handleSetScannedAccounts, + resetScanState: this.handleResetScanState, + setAppOpened: this.handleSetAppOpened, } return ( @@ -200,21 +219,16 @@ class AddAccounts extends PureComponent { refocusWhenChange={stepId} onHide={() => this.setState({ ...INITIAL_STATE })} render={({ onClose }) => ( - + - onBack(stepProps) : void 0}> - {t('app:addAccounts.title')} - - - - - - {!hideFooter && ( - - {StepFooter ? : } - - )} - + )} /> ) @@ -228,7 +242,3 @@ export default compose( ), translate(), )(AddAccounts) - -function idleCallback() { - return new Promise(resolve => window.requestIdleCallback(resolve)) -} diff --git a/src/components/modals/AddAccounts/steps/01-step-choose-currency.js b/src/components/modals/AddAccounts/steps/01-step-choose-currency.js index 9c1639ef..1cbf30d0 100644 --- a/src/components/modals/AddAccounts/steps/01-step-choose-currency.js +++ b/src/components/modals/AddAccounts/steps/01-step-choose-currency.js @@ -1,7 +1,6 @@ // @flow import React, { Fragment } from 'react' -import isArray from 'lodash/isArray' import SelectCurrency from 'components/SelectCurrency' import Button from 'components/base/Button' @@ -9,15 +8,11 @@ import CurrencyBadge from 'components/base/CurrencyBadge' import type { StepProps } from '../index' -function StepChooseCurrency({ currency, setState }: StepProps) { +function StepChooseCurrency({ currency, setCurrency }: StepProps) { return ( { - setState({ - currency: isArray(currency) && currency.length === 0 ? null : currency, - }) - }} + onChange={setCurrency} value={currency} /> ) diff --git a/src/components/modals/AddAccounts/steps/02-step-connect-device.js b/src/components/modals/AddAccounts/steps/02-step-connect-device.js index 60244dd3..f3815588 100644 --- a/src/components/modals/AddAccounts/steps/02-step-connect-device.js +++ b/src/components/modals/AddAccounts/steps/02-step-connect-device.js @@ -11,7 +11,7 @@ import { CurrencyCircleIcon } from 'components/base/CurrencyBadge' import type { StepProps } from '../index' -function StepConnectDevice({ t, currency, currentDevice, setState }: StepProps) { +function StepConnectDevice({ t, currency, device, setAppOpened }: StepProps) { invariant(currency, 'No currency given') return ( @@ -30,11 +30,11 @@ function StepConnectDevice({ t, currency, currentDevice, setState }: StepProps) { if (appStatus === 'success') { - setState({ isAppOpened: true }) + setAppOpened(true) } }} /> diff --git a/src/components/modals/AddAccounts/steps/03-step-import.js b/src/components/modals/AddAccounts/steps/03-step-import.js index 21ad19ea..e12e337f 100644 --- a/src/components/modals/AddAccounts/steps/03-step-import.js +++ b/src/components/modals/AddAccounts/steps/03-step-import.js @@ -18,19 +18,23 @@ import type { StepProps } from '../index' class StepImport extends PureComponent { componentDidMount() { - this.props.setState({ scanStatus: 'scanning' }) + this.props.setScanStatus('scanning') } componentDidUpdate(prevProps: StepProps) { - // handle case when we click on stop sync - if (prevProps.scanStatus !== 'finished' && this.props.scanStatus === 'finished') { - this.unsub() - } + const didStartScan = prevProps.scanStatus !== 'scanning' && this.props.scanStatus === 'scanning' + const didFinishScan = + prevProps.scanStatus !== 'finished' && this.props.scanStatus === 'finished' // handle case when we click on retry sync - if (prevProps.scanStatus !== 'scanning' && this.props.scanStatus === 'scanning') { + if (didStartScan) { this.startScanAccountsDevice() } + + // handle case when we click on stop sync + if (didFinishScan) { + this.unsub() + } } componentWillUnmount() { @@ -63,15 +67,15 @@ class StepImport extends PureComponent { startScanAccountsDevice() { this.unsub() - const { currency, currentDevice, setState } = this.props + const { currency, device, setScanStatus, setScannedAccounts } = this.props try { invariant(currency, 'No currency to scan') - invariant(currentDevice, 'No device') + invariant(device, 'No device') const bridge = getBridgeForCurrency(currency) // TODO: use the real device - const devicePath = currentDevice.path + const devicePath = device.path this.scanSubscription = bridge.scanAccountsOnDevice(currency, devicePath).subscribe({ next: account => { @@ -80,7 +84,7 @@ class StepImport extends PureComponent { const hasAlreadyBeenImported = !!existingAccounts.find(a => account.id === a.id) const isNewAccount = account.operations.length === 0 if (!hasAlreadyBeenScanned) { - setState({ + setScannedAccounts({ scannedAccounts: [...scannedAccounts, this.translateName(account)], checkedAccountsIds: !hasAlreadyBeenImported && !isNewAccount @@ -89,43 +93,33 @@ class StepImport extends PureComponent { }) } }, - complete: () => setState({ scanStatus: 'finished' }), - error: err => setState({ scanStatus: 'error', err }), + complete: () => setScanStatus('finished'), + error: err => setScanStatus('error', err), }) } catch (err) { - setState({ scanStatus: 'error', err }) + setScanStatus('error', err) } } handleRetry = () => { this.unsub() - this.handleResetState() + this.props.resetScanState() this.startScanAccountsDevice() } - handleResetState = () => { - const { setState } = this.props - setState({ - scanStatus: 'idle', - err: null, - scannedAccounts: [], - checkedAccountsIds: [], - }) - } - handleToggleAccount = (account: Account) => { - const { checkedAccountsIds, setState } = this.props + const { checkedAccountsIds, setScannedAccounts } = this.props const isChecked = checkedAccountsIds.find(id => id === account.id) !== undefined if (isChecked) { - setState({ checkedAccountsIds: checkedAccountsIds.filter(id => id !== account.id) }) + setScannedAccounts({ checkedAccountsIds: checkedAccountsIds.filter(id => id !== account.id) }) } else { - setState({ checkedAccountsIds: [...checkedAccountsIds, account.id] }) + setScannedAccounts({ checkedAccountsIds: [...checkedAccountsIds, account.id] }) } } handleUpdateAccount = (updatedAccount: Account) => { - const { scannedAccounts, setState } = this.props - setState({ + const { scannedAccounts, setScannedAccounts } = this.props + setScannedAccounts({ scannedAccounts: scannedAccounts.map(account => { if (account.id !== updatedAccount.id) { return account @@ -136,19 +130,26 @@ class StepImport extends PureComponent { } handleSelectAll = () => { - const { scannedAccounts, setState } = this.props - setState({ + const { scannedAccounts, setScannedAccounts } = this.props + setScannedAccounts({ checkedAccountsIds: scannedAccounts.filter(a => a.operations.length > 0).map(a => a.id), }) } - handleUnselectAll = () => this.props.setState({ checkedAccountsIds: [] }) + handleUnselectAll = () => this.props.setScannedAccounts({ checkedAccountsIds: [] }) renderError() { const { err, t } = this.props invariant(err, 'Trying to render inexisting error') return ( - + {t('app:addAccounts.somethingWentWrong')} @@ -236,7 +237,8 @@ class StepImport extends PureComponent { export default StepImport export const StepImportFooter = ({ - setState, + transitionTo, + setScanStatus, scanStatus, onClickAdd, onCloseModal, @@ -274,18 +276,23 @@ export const StepImportFooter = ({ : t('app:common.close') const willClose = !willCreateAccount && !willAddAccounts - const onClick = willClose ? onCloseModal : onClickAdd + const onClick = willClose + ? onCloseModal + : async () => { + await onClickAdd() + transitionTo('finish') + } return ( {currency && } {scanStatus === 'error' && ( - )} {scanStatus === 'scanning' && ( - )} diff --git a/src/helpers/promise.js b/src/helpers/promise.js index 8f9f6a1a..09c2828b 100644 --- a/src/helpers/promise.js +++ b/src/helpers/promise.js @@ -27,3 +27,7 @@ export function retry(f: () => Promise, options?: $Shape) }) } } + +export function idleCallback() { + return new Promise(resolve => window.requestIdleCallback(resolve)) +}