From d6f3a183a916fbd9717610a3a0478270d7e4a130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Mon, 25 Jun 2018 22:02:39 +0200 Subject: [PATCH 1/2] Add 'Outdated' concept Outdated rarely happen: it's only if you have not yet pulled for a time and you had no network condition. because otherwise, our pull interval is always smaller than the time we consider 'outdated' (set at block time + 5mn to be safe) In case an error occur during the sync, we won't show it until one of the account is also outdated, which i think will lighten a bit the cases we have errors. we can decide later to be smarter and more verbose (like being able to know which account have a problem, which one is not sync etc..) but we'll need to have a design for it --- src/components/TopBar/ActivityIndicator.js | 78 ++++++++-------------- src/config/constants.js | 2 + src/reducers/accounts.js | 16 ++++- static/i18n/en/app.yml | 1 + 4 files changed, 46 insertions(+), 51 deletions(-) diff --git a/src/components/TopBar/ActivityIndicator.js b/src/components/TopBar/ActivityIndicator.js index ee37de73..ca1b7e53 100644 --- a/src/components/TopBar/ActivityIndicator.js +++ b/src/components/TopBar/ActivityIndicator.js @@ -10,6 +10,7 @@ import type { T } from 'types/common' import type { AsyncState } from 'reducers/bridgeSync' import { globalSyncStateSelector } from 'reducers/bridgeSync' +import { isUpToDateSelector } from 'reducers/accounts' import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext' import CounterValues from 'helpers/countervalues' @@ -24,78 +25,45 @@ import ItemContainer from './ItemContainer' const mapStateToProps = createStructuredSelector({ globalSyncState: globalSyncStateSelector, + isUpToDate: isUpToDateSelector, }) type Props = { - // FIXME: eslint should see that it is used in static method - isGlobalSyncStatePending: boolean, // eslint-disable-line react/no-unused-prop-types - error: ?Error, isPending: boolean, isError: boolean, + isUpToDate: boolean, t: T, cvPoll: *, setSyncBehavior: *, } -type State = { - hasClicked: boolean, - isGlobalSyncStatePending: boolean, - isFirstSync: boolean, -} - -class ActivityIndicatorInner extends PureComponent { - state = { - hasClicked: false, - isFirstSync: true, - - // FIXME: eslint should see that it is used in static method - isGlobalSyncStatePending: false, // eslint-disable-line react/no-unused-state - } - - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - const nextState = { - ...prevState, - isGlobalSyncStatePending: nextProps.isGlobalSyncStatePending, - } - - if (prevState.isGlobalSyncStatePending && !nextProps.isGlobalSyncStatePending) { - nextState.isFirstSync = false - nextState.hasClicked = false - } - - return nextState - } - +class ActivityIndicatorInner extends PureComponent { onClick = () => { this.props.cvPoll() this.props.setSyncBehavior({ type: 'SYNC_ALL_ACCOUNTS', priority: 5 }) } - handleRefresh = () => { - this.setState({ hasClicked: true }) - this.onClick() - } - render() { - const { isPending, isError, error, t } = this.props - const { hasClicked, isFirstSync } = this.state - const isDisabled = isError || (isPending && (isFirstSync || hasClicked)) - const isRotating = isPending && (hasClicked || isFirstSync) + const { isUpToDate, isPending, isError, error, t } = this.props + const isDisabled = isError || isPending + const isRotating = isPending const content = ( - + {isError ? ( ) : isRotating ? ( - ) : ( + ) : isUpToDate ? ( + ) : ( + )} { ml={2} cursor="pointer" style={{ textDecoration: 'underline', pointerEvents: 'all' }} - onClick={this.handleRefresh} + onClick={this.onClick} > {t('app:common.sync.refresh')} - ) : ( + ) : isUpToDate ? ( t('app:common.sync.upToDate') + ) : ( + t('app:common.sync.outdated') )} ) - if (error) { + if (isError && error) { return ( { } } -const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState, t: T }) => ( +const ActivityIndicator = ({ + globalSyncState, + t, + isUpToDate, +}: { + globalSyncState: AsyncState, + t: T, + isUpToDate: boolean, +}) => ( {setSyncBehavior => ( @@ -156,9 +134,9 @@ const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState return ( state.accounts +export const isUpToDateSelector = createSelector(accountsSelector, accounts => + accounts.every(a => { + const { lastSyncDate } = a + const { blockAvgTime } = a.currency + if (!blockAvgTime) return true + const outdated = + Date.now() - (lastSyncDate || 0) > blockAvgTime * 1000 + OUTDATED_CONSIDERED_DELAY + if (outdated && DEBUG_SYNC) { + logger.log('account not up to date', a) + } + return !outdated + }), +) + export const hasAccountsSelector = createSelector(accountsSelector, accounts => accounts.length > 0) export const currenciesSelector = createSelector(accountsSelector, accounts => diff --git a/static/i18n/en/app.yml b/static/i18n/en/app.yml index 7a1e2256..7da263a7 100644 --- a/static/i18n/en/app.yml +++ b/static/i18n/en/app.yml @@ -40,6 +40,7 @@ common: sync: syncing: Synchronizing... upToDate: Up to date + outdated: Outdated error: Synchronization error refresh: Refresh ago: Synced {{time}} From 7f4d833ce98d11d0d6392e7a0c5724c401ff8fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Tue, 26 Jun 2018 10:14:50 +0200 Subject: [PATCH 2/2] serialize error that go through libcore as well this help for translated errors --- src/helpers/init-libcore.js | 3 ++- src/helpers/libcore.js | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/helpers/init-libcore.js b/src/helpers/init-libcore.js index f1df2ee0..70e3e632 100644 --- a/src/helpers/init-libcore.js +++ b/src/helpers/init-libcore.js @@ -3,6 +3,7 @@ import logger from 'logger' import invariant from 'invariant' import network from 'api/network' +import { serializeError } from './errors' const lib = require('@ledgerhq/ledger-core') @@ -96,7 +97,7 @@ const NJSHttpClient = new lib.NJSHttpClient({ r.complete(urlConnection, null) } catch (err) { const urlConnection = createHttpConnection(res, err.message) - r.complete(urlConnection, { code: 0, message: err.message }) + r.complete(urlConnection, { code: 0, message: JSON.stringify(serializeError(err)) }) } }, }) diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index e42bf774..7628c4c8 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -11,8 +11,7 @@ import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgerc import { isSegwitAccount } from 'helpers/bip32' import * as accountIdHelper from 'helpers/accountId' -import { createCustomErrorClass } from './errors' - +import { createCustomErrorClass, deserializeError } from './errors' import { getAccountPlaceholderName, getNewAccountPlaceholderName } from './accountName' const NoAddressesFound = createCustomErrorClass('NoAddressesFound') @@ -158,7 +157,11 @@ const coreSyncAccount = (core, account) => (payload && payload.getString('EV_SYNC_ERROR_MESSAGE')) || 'Sync failed' ).replace(' (EC_PRIV_KEY_INVALID_FORMAT)', '') - reject(new Error(message)) + try { + reject(deserializeError(JSON.parse(message))) + } catch (e) { + reject(message) + } return } if (