Gaëtan Renaudeau
6 years ago
committed by
GitHub
11 changed files with 278 additions and 0 deletions
@ -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) |
@ -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) |
Loading…
Reference in new issue