Browse Source

Implement CurrenciesStatusBanner to provide a way to inform infra problems

gre-patch-1
meriadec 6 years ago
parent
commit
5de9d3e817
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 1
      .eslintrc
  2. 5
      src/actions/settings.js
  3. 209
      src/components/CurrenciesStatusBanner.js
  4. 2
      src/components/TopBar/index.js
  5. 1
      src/config/constants.js
  6. 3
      src/config/urls.js
  7. 12
      src/reducers/settings.js
  8. 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,
})

209
src/components/CurrenciesStatusBanner.js

@ -0,0 +1,209 @@
// @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 logger from 'logger'
import { colors } from 'styles/theme'
import { openURL } from 'helpers/linking'
import { urls } from 'config/urls'
import { CHECK_CUR_STATUS_INTERVAL } from 'config/constants'
import network from 'api/network'
import IconCross from 'icons/Cross'
import IconTriangleWarning from 'icons/TriangleWarning'
import IconChevronRight from 'icons/ChevronRight'
import { dismissedBannersSelector } from 'reducers/settings'
import { currenciesSelector } from 'reducers/accounts'
import { dismissBanner } from 'actions/settings'
import Box from 'components/base/Box'
type ResultItem = {
id: string,
status: string,
message: string,
link: string,
nonce: number,
}
const mapStateToProps = createStructuredSelector({
dismissedBanners: dismissedBannersSelector,
accountsCurrencies: currenciesSelector,
})
const mapDispatchToProps = {
dismissBanner,
}
const getItemKey = (item: ResultItem) => `${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,
t: *,
}
type State = {
result: ResultItem[],
}
class CurrenciesStatusBanner extends PureComponent<Props, State> {
state = {
result: [],
}
componentDidMount() {
this.pollStatus()
}
componentWillUnmount() {
this.unmounted = true
if (this.timeout) {
clearTimeout(this.timeout)
}
}
unmounted = false
timeout: *
pollStatus = () => {
this.fetchStatus()
this.timeout = setTimeout(this.pollStatus, CHECK_CUR_STATUS_INTERVAL)
}
fetchStatus = async () => {
try {
const baseUrl = process.env.LL_STATUS_API || urls.currenciesStatus
const { data } = await network({
method: 'GET',
url: `${baseUrl}/currencies-status`,
})
if (this.unmounted) return
this.setState({ result: data })
} catch (err) {
logger.error(err)
}
}
dismiss = item => this.props.dismissBanner(getItemKey(item))
render() {
const { dismissedBanners, accountsCurrencies, t } = this.props
const { result } = this.state
if (!result) return null
const filtered = result.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: ResultItem,
onItemDismiss: ResultItem => 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)

3
src/config/urls.js

@ -36,4 +36,7 @@ export const urls = {
errors: {
CantOpenDevice: 'https://support.ledgerwallet.com/hc/en-us/articles/115005165269',
},
// Currencies status
currenciesStatus: 'https://i-dont-know-which-url.ledger.com/currencies-status',
}

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