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?",