diff --git a/.eslintrc b/.eslintrc
index a5c7283d..e22c8543 100644
--- a/.eslintrc
+++ b/.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',
diff --git a/src/actions/settings.js b/src/actions/settings.js
index 60251280..afd189d4 100644
--- a/src/actions/settings.js
+++ b/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,
+})
diff --git a/src/components/CurrenciesStatusBanner.js b/src/components/CurrenciesStatusBanner.js
new file mode 100644
index 00000000..656786f2
--- /dev/null
+++ b/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: *) => (
+
+
+
+)
+
+type Props = {
+ accountsCurrencies: Currency[],
+ dismissedBanners: string[],
+ dismissBanner: string => void,
+ t: *,
+}
+
+type State = {
+ result: ResultItem[],
+}
+
+class CurrenciesStatusBanner extends PureComponent {
+ 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 (
+
+ {filtered.map(r => )}
+
+ )
+ }
+}
+
+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 (
+
+
+
+
+
+ {item.message}
+
+
+ {item.link && }
+
+ )
+ }
+}
+
+const UnderlinedLink = styled.span`
+ border-bottom: 1px solid transparent;
+ &:hover {
+ border-bottom-color: white;
+ }
+`
+
+const BannerItemLink = ({ t, onClick }: { t: *, onClick: void => * }) => (
+
+
+ {t('common.learnMore')}
+
+)
+
+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)
diff --git a/src/components/TopBar/index.js b/src/components/TopBar/index.js
index 3378772f..b18914c5 100644
--- a/src/components/TopBar/index.js
+++ b/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 {
+
{hasAccounts && (
diff --git a/src/config/constants.js b/src/config/constants.js
index 06ad1b7d..ac43b517 100644
--- a/src/config/constants.js
+++ b/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)
diff --git a/src/config/urls.js b/src/config/urls.js
index 60c2fc42..27e1489f 100644
--- a/src/config/urls.js
+++ b/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',
}
diff --git a/src/reducers/settings.js b/src/reducers/settings.js
index a62f81a8..4ef84c24 100644
--- a/src/reducers/settings.js
+++ b/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,
diff --git a/static/i18n/en/app.json b/static/i18n/en/app.json
index f6ddfe72..d6dc3f3b 100644
--- a/static/i18n/en/app.json
+++ b/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?",