Browse Source

Merge pull request #1615 from meriadec/ll-status

Implement CurrenciesStatusBanner to provide a way to inform infra pbs
gre-patch-1
Gaëtan Renaudeau 6 years ago
committed by GitHub
parent
commit
058cd5ce8c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .eslintrc
  2. 5
      src/actions/settings.js
  3. 9
      src/bridge/BridgeSyncContext.js
  4. 181
      src/components/CurrenciesStatusBanner.js
  5. 2
      src/components/TopBar/index.js
  6. 1
      src/config/constants.js
  7. 4
      src/config/urls.js
  8. 58
      src/reducers/currenciesStatus.js
  9. 4
      src/reducers/index.js
  10. 12
      src/reducers/settings.js
  11. 1
      static/i18n/en/app.json

1
.eslintrc

@ -60,6 +60,7 @@
"react/jsx-no-literals": [1, {"noStrings": false}],
"react/prefer-stateless-function": 0,
"react/require-default-props": 0,
"react/no-multi-comp": 0,
"react/sort-comp": [1, {
order: [
'static-methods',

5
src/actions/settings.js

@ -45,3 +45,8 @@ export const setExchangePairsAction: SetExchangePairs = pairs => ({
type: 'SETTINGS_SET_PAIRS',
pairs,
})
export const dismissBanner = (bannerId: string) => ({
type: 'SETTINGS_DISMISS_BANNER',
payload: bannerId,
})

9
src/bridge/BridgeSyncContext.js

@ -16,7 +16,9 @@ import { setAccountSyncState } from 'actions/bridgeSync'
import { bridgeSyncSelector, syncStateLocalSelector } from 'reducers/bridgeSync'
import type { BridgeSyncState } from 'reducers/bridgeSync'
import { accountsSelector, isUpToDateSelector } from 'reducers/accounts'
import { currenciesStatusSelector, getIsCurrencyDown } from 'reducers/currenciesStatus'
import { SYNC_MAX_CONCURRENT, SYNC_TIMEOUT } from 'config/constants'
import type { CurrencyStatus } from 'reducers/currenciesStatus'
import { getBridgeForCurrency } from '.'
type BridgeSyncProviderProps = {
@ -29,6 +31,7 @@ type BridgeSyncProviderOwnProps = BridgeSyncProviderProps & {
isUpToDate: boolean,
updateAccountWithUpdater: (string, (Account) => Account) => void,
setAccountSyncState: (string, AsyncState) => *,
currenciesStatus: CurrencyStatus[],
}
type AsyncState = {
@ -48,6 +51,7 @@ export type Sync = (action: BehaviorAction) => void
const BridgeSyncContext = React.createContext((_: BehaviorAction) => {})
const mapStateToProps = createStructuredSelector({
currenciesStatus: currenciesStatusSelector,
accounts: accountsSelector,
bridgeSync: bridgeSyncSelector,
isUpToDate: isUpToDateSelector,
@ -73,6 +77,11 @@ class Provider extends Component<BridgeSyncProviderOwnProps, Sync> {
return
}
if (getIsCurrencyDown(this.props.currenciesStatus, account.currency)) {
next()
return
}
const bridge = getBridgeForCurrency(account.currency)
this.props.setAccountSyncState(accountId, { pending: true, error: null })

181
src/components/CurrenciesStatusBanner.js

@ -0,0 +1,181 @@
// @flow
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { createStructuredSelector } from 'reselect'
import styled from 'styled-components'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { colors } from 'styles/theme'
import { openURL } from 'helpers/linking'
import { CHECK_CUR_STATUS_INTERVAL } from 'config/constants'
import IconCross from 'icons/Cross'
import IconTriangleWarning from 'icons/TriangleWarning'
import IconChevronRight from 'icons/ChevronRight'
import { dismissedBannersSelector } from 'reducers/settings'
import { currenciesStatusSelector, fetchCurrenciesStatus } from 'reducers/currenciesStatus'
import { currenciesSelector } from 'reducers/accounts'
import { dismissBanner } from 'actions/settings'
import type { CurrencyStatus } from 'reducers/currenciesStatus'
import Box from 'components/base/Box'
const mapStateToProps = createStructuredSelector({
dismissedBanners: dismissedBannersSelector,
accountsCurrencies: currenciesSelector,
currenciesStatus: currenciesStatusSelector,
})
const mapDispatchToProps = {
dismissBanner,
fetchCurrenciesStatus,
}
const getItemKey = (item: CurrencyStatus) => `${item.id}_${item.nonce}`
const CloseIconContainer = styled.div`
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-bottom-left-radius: 4px;
opacity: 0.5;
&:hover {
opacity: 1;
}
`
const CloseIcon = (props: *) => (
<CloseIconContainer {...props}>
<IconCross size={16} color="white" />
</CloseIconContainer>
)
type Props = {
accountsCurrencies: Currency[],
dismissedBanners: string[],
dismissBanner: string => void,
currenciesStatus: CurrencyStatus[],
fetchCurrenciesStatus: () => Promise<void>,
t: *,
}
class CurrenciesStatusBanner extends PureComponent<Props> {
componentDidMount() {
this.pollStatus()
}
componentWillUnmount() {
this.unmounted = true
if (this.timeout) {
clearTimeout(this.timeout)
}
}
unmounted = false
timeout: *
pollStatus = async () => {
await this.props.fetchCurrenciesStatus()
if (this.unmounted) return
this.timeout = setTimeout(this.pollStatus, CHECK_CUR_STATUS_INTERVAL)
}
dismiss = item => this.props.dismissBanner(getItemKey(item))
render() {
const { dismissedBanners, accountsCurrencies, currenciesStatus, t } = this.props
const filtered = currenciesStatus.filter(
item =>
accountsCurrencies.find(cur => cur.id === item.id) &&
dismissedBanners.indexOf(getItemKey(item)) === -1,
)
if (!filtered.length) return null
return (
<Box flow={2} style={styles.container}>
{filtered.map(r => <BannerItem key={r.id} t={t} item={r} onItemDismiss={this.dismiss} />)}
</Box>
)
}
}
class BannerItem extends PureComponent<{
item: CurrencyStatus,
onItemDismiss: CurrencyStatus => void,
t: *,
}> {
onLinkClick = () => openURL(this.props.item.link)
dismiss = () => this.props.onItemDismiss(this.props.item)
render() {
const { item, t } = this.props
return (
<Box relative key={item.id} style={styles.banner}>
<CloseIcon onClick={this.dismiss} />
<Box horizontal flow={2}>
<IconTriangleWarning height={16} width={16} color="white" />
<Box shrink ff="Open Sans|SemiBold">
{item.message}
</Box>
</Box>
{item.link && <BannerItemLink t={t} onClick={this.onLinkClick} />}
</Box>
)
}
}
const UnderlinedLink = styled.span`
border-bottom: 1px solid transparent;
&:hover {
border-bottom-color: white;
}
`
const BannerItemLink = ({ t, onClick }: { t: *, onClick: void => * }) => (
<Box
mt={2}
ml={4}
flow={1}
horizontal
align="center"
cursor="pointer"
onClick={onClick}
color="white"
>
<IconChevronRight size={16} color="white" />
<UnderlinedLink>{t('common.learnMore')}</UnderlinedLink>
</Box>
)
const styles = {
container: {
position: 'fixed',
left: 32,
bottom: 32,
},
banner: {
background: colors.alertRed,
overflow: 'hidden',
borderRadius: 4,
fontSize: 13,
padding: 14,
color: 'white',
fontWeight: 'bold',
paddingRight: 50,
width: 350,
},
}
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
translate(),
)(CurrenciesStatusBanner)

2
src/components/TopBar/index.js

@ -21,6 +21,7 @@ import IconSettings from 'icons/Settings'
import Box from 'components/base/Box'
import GlobalSearch from 'components/GlobalSearch'
import Tooltip from 'components/base/Tooltip'
import CurrenciesStatusBanner from 'components/CurrenciesStatusBanner'
import ActivityIndicator from './ActivityIndicator'
import ItemContainer from './ItemContainer'
@ -101,6 +102,7 @@ class TopBar extends PureComponent<Props> {
<Inner>
<Box grow horizontal>
<GlobalSearch t={t} isHidden />
<CurrenciesStatusBanner />
{hasAccounts && (
<Fragment>
<ActivityIndicator />

1
src/config/constants.js

@ -32,6 +32,7 @@ export const MIN_HEIGHT = intFromEnv('LEDGER_MIN_HEIGHT', 700)
export const CHECK_APP_INTERVAL_WHEN_INVALID = 600
export const CHECK_APP_INTERVAL_WHEN_VALID = 1200
export const CHECK_UPDATE_DELAY = 5000
export const CHECK_CUR_STATUS_INTERVAL = intFromEnv('CHECK_CUR_STATUS_INTERVAL', 60 * 60 * 1000)
export const DEVICE_INFOS_TIMEOUT = intFromEnv('DEVICE_INFOS_TIMEOUT', 5 * 1000)
export const GENUINE_CACHE_DELAY = intFromEnv('GENUINE_CACHE_DELAY', 1000)
export const GENUINE_TIMEOUT = intFromEnv('GENUINE_TIMEOUT', 120 * 1000)

4
src/config/urls.js

@ -36,4 +36,8 @@ export const urls = {
errors: {
CantOpenDevice: 'https://support.ledgerwallet.com/hc/en-us/articles/115005165269',
},
// Currencies status
currenciesStatus:
'https://s3-eu-west-1.amazonaws.com/ledger-ledgerlive-resources-prod/public_resources/currencies.json',
}

58
src/reducers/currenciesStatus.js

@ -0,0 +1,58 @@
// @flow
import { handleActions, createAction } from 'redux-actions'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import network from 'api/network'
import { urls } from 'config/urls'
import logger from 'logger'
import type { State } from './index'
export type CurrencyStatus = {
id: string, // the currency id
status: 'KO' | 'OK',
message: string,
link: string,
nonce: number,
}
export type CurrenciesStatusState = CurrencyStatus[]
const state: CurrenciesStatusState = []
const handlers = {
CURRENCIES_STATUS_SET: (
state: CurrenciesStatusState,
{ payload }: { payload: CurrenciesStatusState },
) => payload,
}
// Actions
const setCurrenciesStatus = createAction('CURRENCIES_STATUS_SET')
export const fetchCurrenciesStatus = () => async (dispatch: *) => {
try {
const { data } = await network({
method: 'GET',
url: process.env.LL_STATUS_ENDPOINT || urls.currenciesStatus,
})
dispatch(setCurrenciesStatus(data))
} catch (err) {
logger.error(err)
}
}
// Selectors
export const currenciesStatusSelector = (state: State) => state.currenciesStatus
// It's not a *real* selector, but it's better than having this logic inside component
export const getIsCurrencyDown = (currenciesStatus: CurrenciesStatusState, currency: Currency) => {
const item = currenciesStatus.find(c => c.id === currency.id)
return !!item && item.status === 'KO'
}
// Exporting reducer
export default handleActions(handlers, state)

4
src/reducers/index.js

@ -9,6 +9,7 @@ import type { CounterValuesState } from '@ledgerhq/live-common/lib/countervalues
import CounterValues from 'helpers/countervalues'
import accounts from './accounts'
import application from './application'
import currenciesStatus from './currenciesStatus'
import devices from './devices'
import modals from './modals'
import settings from './settings'
@ -24,11 +25,13 @@ import type { SettingsState } from './settings'
import type { UpdateState } from './update'
import type { OnboardingState } from './onboarding'
import type { BridgeSyncState } from './bridgeSync'
import type { CurrenciesStatusState } from './currenciesStatus'
export type State = {
accounts: AccountsState,
application: ApplicationState,
countervalues: CounterValuesState,
currenciesStatus: CurrenciesStatusState,
devices: DevicesState,
modals: ModalsState,
router: LocationShape,
@ -42,6 +45,7 @@ export default combineReducers({
accounts,
application,
countervalues: CounterValues.reducer,
currenciesStatus,
devices,
modals,
router,

12
src/reducers/settings.js

@ -47,6 +47,7 @@ export type SettingsState = {
shareAnalytics: boolean,
sentryLogs: boolean,
lastUsedVersion: string,
dismissedBanners: string[],
}
const defaultsForCurrency: CryptoCurrency => CurrencySettings = crypto => {
@ -73,6 +74,7 @@ const INITIAL_STATE: SettingsState = {
shareAnalytics: true,
sentryLogs: true,
lastUsedVersion: __APP_VERSION__,
dismissedBanners: [],
}
function asCryptoCurrency(c: Currency): ?CryptoCurrency {
@ -126,6 +128,14 @@ const handlers: Object = {
...settings,
loaded: true,
}),
SETTINGS_DISMISS_BANNER: (state: SettingsState, { payload: bannerId }) => ({
...state,
dismissedBanners: [...state.dismissedBanners, bannerId],
}),
CLEAN_ACCOUNTS_CACHE: (state: SettingsState) => ({
...state,
dismissedBanners: [],
}),
}
// TODO refactor selectors to *Selector naming convention
@ -224,6 +234,8 @@ export const selectedTimeRangeSelector = (state: State) => state.settings.select
export const hasCompletedOnboardingSelector = (state: State) =>
state.settings.hasCompletedOnboarding
export const dismissedBannersSelector = (state: State) => state.settings.dismissedBanners || []
export const exportSettingsSelector = createSelector(
counterValueCurrencySelector,
counterValueExchangeSelector,

1
static/i18n/en/app.json

@ -11,6 +11,7 @@
"continue": "Continue",
"learnMore": "Learn more",
"help": "Help",
"dismiss": "Dismiss",
"skipThisStep": "Skip this step",
"needHelp": "Need help?",
"areYouSure": "Are you sure?",

Loading…
Cancel
Save