Merge pull request #63 from andrerfneves/feature/catch-error-middleware

Feature/catch error middleware
This commit is contained in:
André Neves 2019-02-04 22:19:44 -05:00 committed by GitHub
commit 07a9e817ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 289 additions and 115 deletions

View File

@ -0,0 +1,94 @@
// @flow
import React, { PureComponent } from 'react';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
import { TextComponent } from './text';
import { Button } from './button';
import ErrorIcon from '../assets/images/error_icon.png';
const ModalWrapper = styled.div`
width: 100vw;
height: 100vh;
position: fixed;
display: flex;
align-items: center;
justify-content: center;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
`;
const ChildrenWrapper = styled.div`
width: 350px;
background-color: ${props => props.theme.colors.background};
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 6px;
box-shadow: 0px 0px 30px 0px black;
position: relative;
z-index: 90;
min-height: 400px;
`;
const Message = styled(TextComponent)`
margin: 15px 0;
`;
const ErrorImage = styled.img`
width: 35px;
height: 35px;
margin-bottom: 15px;
`;
type Props = {
isVisible: boolean,
message: string,
onRequestClose: () => void,
};
const modalRoot = document.getElementById('modal-root');
export class ErrorModalComponent extends PureComponent<Props> {
element = document.createElement('div');
componentDidMount() {
const { isVisible } = this.props;
if (isVisible) {
if (modalRoot) modalRoot.appendChild(this.element);
}
}
componentDidUpdate = (prevProps: Props) => {
const { isVisible } = this.props;
if (!prevProps.isVisible && isVisible) {
if (modalRoot) modalRoot.appendChild(this.element);
}
if (prevProps.isVisible && !isVisible) {
if (modalRoot) modalRoot.removeChild(this.element);
}
};
render() {
const { isVisible, message, onRequestClose } = this.props;
return isVisible
? createPortal(
<ModalWrapper id='error-modal-portal-wrapper'>
<ChildrenWrapper>
<ErrorImage src={ErrorIcon} alt='Error Icon' />
<Message value={message} />
<Button label='Ok!' onClick={onRequestClose} />
</ChildrenWrapper>
</ModalWrapper>,
this.element,
)
: null;
}
}

View File

@ -1,7 +1,9 @@
// @flow // @flow
import React from 'react'; import React, { type Element } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ErrorModalComponent } from './error-modal';
const Layout = styled.div` const Layout = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -14,12 +16,25 @@ const Layout = styled.div`
`; `;
type Props = { type Props = {
chidren: any, // eslint-disable-line children: Element<*>,
closeErrorModal: () => void,
isErrorModalVisible: boolean,
error: string,
}; };
export const LayoutComponent = (props: Props) => { export const LayoutComponent = (props: Props) => {
// $FlowFixMe const {
const { children } = props; // eslint-disable-line children, error, isErrorModalVisible, closeErrorModal,
} = props;
return <Layout id='layout'>{children}</Layout>; return (
<Layout id='layout'>
{children}
<ErrorModalComponent
message={error}
isVisible={isErrorModalVisible}
onRequestClose={closeErrorModal}
/>
</Layout>
);
}; };

View File

@ -1,9 +1,12 @@
// @flow // @flow
import React from 'react'; import React, { Component } from 'react';
import styled, { keyframes } from 'styled-components'; import styled, { keyframes } from 'styled-components';
import eres from 'eres';
import { TextComponent } from './text'; import { TextComponent } from './text';
import rpc from '../../services/api';
import readyIcon from '../assets/images/green_check.png'; import readyIcon from '../assets/images/green_check.png';
import syncIcon from '../assets/images/sync_icon.png'; import syncIcon from '../assets/images/sync_icon.png';
import errorIcon from '../assets/images/error_icon.png'; import errorIcon from '../assets/images/error_icon.png';
@ -42,39 +45,70 @@ const StatusPillLabel = styled(TextComponent)`
user-select: none; user-select: none;
`; `;
type Props = { type Props = {};
type: 'syncing' | 'ready' | 'error',
progress: number,
};
type State = { type State = {
withError: boolean, type: string,
icon: string,
progress: number,
isSyncing: boolean,
}; };
export class StatusPill extends React.PureComponent<Props, State> { export class StatusPill extends Component<Props, State> {
timer: ?IntervalID = null;
state = { state = {
withError: false, type: 'syncing',
icon: syncIcon,
progress: 0,
isSyncing: true,
}; };
componentDidMount() { componentDidMount() {
const { type } = this.props; this.timer = setInterval(() => {
if (type === 'error') { this.getBlockchainStatus();
this.setState(() => ({ withError: false })); }, 2000);
}
componentWillUnmount() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
} }
} }
getBlockchainStatus = async () => {
const [blockchainErr, blockchaininfo] = await eres(rpc.getblockchaininfo());
if (blockchainErr || !blockchaininfo) return;
const newProgress = blockchaininfo.verificationprogress * 100;
this.setState({
progress: newProgress,
...(newProgress > 99.99
? {
type: 'ready',
icon: readyIcon,
isSyncing: false,
}
: {}),
});
if (blockchainErr) {
this.setState(() => ({ type: 'error', icon: errorIcon }));
}
};
render() { render() {
const { type, progress } = this.props; const {
const { withError } = this.state; type, icon, progress, isSyncing,
} = this.state;
const isSyncing = type === 'syncing';
const icon = isSyncing ? syncIcon : readyIcon;
const showPercent = isSyncing ? `(${progress.toFixed(2)}%)` : ''; const showPercent = isSyncing ? `(${progress.toFixed(2)}%)` : '';
return ( return (
<Wrapper data-testid='StatusPill'> <Wrapper id='status-pill'>
<Icon src={withError ? errorIcon : icon} animated={isSyncing} /> <Icon src={icon} animated={isSyncing} />
<StatusPillLabel value={`${type} ${showPercent}`} /> <StatusPillLabel value={`${type} ${showPercent}`} />
</Wrapper> </Wrapper>
); );

View File

@ -1,9 +1,8 @@
// @flow // @flow
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { Transition, animated } from 'react-spring';
import { ColumnComponent } from './column';
import { Button } from './button'; import { Button } from './button';
import { QRCode } from './qrcode'; import { QRCode } from './qrcode';
@ -18,7 +17,6 @@ const AddressWrapper = styled.div`
border-radius: 6px; border-radius: 6px;
padding: 7px 13px; padding: 7px 13px;
width: 100%; width: 100%;
margin-bottom: 5px;
`; `;
const Input = styled.input` const Input = styled.input`
@ -36,123 +34,70 @@ const Input = styled.input`
} }
`; `;
/* eslint-disable max-len */
const Btn = styled(Button)`
border-width: 1px;
font-weight: ${props => props.theme.fontWeight.regular};
border-color: ${props => (props.isVisible ? props.theme.colors.primary : props.theme.colors.buttonBorderColor)};
padding: 8px 10px;
min-width: 260px;
img {
height: 12px;
width: 20px;
}
`;
/* eslint-enable max-len */
const QRCodeWrapper = styled.div` const QRCodeWrapper = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
background-color: #000; background-color: #000;
border-radius: 6px; border-radius: 6px;
padding: 20px; padding: 20px;
margin-top: 10px;
width: 100%; width: 100%;
`; `;
const RevealsMain = styled.div`
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: flex-start;
justify-content: flex-start;
height: ${props => (props.isVisible ? '178px' : 0)}
transition: all 0.25s ease-in-out;
& > div {
top: 0;
right: 0;
left: 0;
}
`;
type Props = { type Props = {
address: string, address: string,
isVisible?: boolean,
}; };
type State = { type State = {
isVisible: boolean, isVisible: boolean,
}; };
export class WalletAddress extends PureComponent<Props, State> { export class WalletAddress extends Component<Props, State> {
static defaultProps = { state = {
isVisible: false, isVisible: false,
}; };
constructor(props: Props) { show = () => {
super(props); this.setState(
() => ({ isVisible: true }),
);
};
this.state = { isVisible: Boolean(props.isVisible) }; hide = () => {
} this.setState(
() => ({ isVisible: false }),
);
};
show = () => this.setState(() => ({ isVisible: true }));
hide = () => this.setState(() => ({ isVisible: false }));
render() { render() {
const { address } = this.props; const { address } = this.props;
const { isVisible } = this.state; const { isVisible } = this.state;
const toggleVisibility = isVisible ? this.hide : this.show; const toggleVisibility = isVisible ? this.hide : this.show;
const buttonLabel = `${isVisible ? 'Hide' : 'Show'} details and QR Code`;
return ( return (
<ColumnComponent width='100%'> <ColumnComponent width='100%'>
<AddressWrapper data-testid='Address'> <AddressWrapper>
<Input <Input
value={isVisible ? address : truncateAddress(address)} value={isVisible ? address : truncateAddress(address)}
onChange={() => { }} onChange={() => { }}
onFocus={event => event.currentTarget.select()} onFocus={event => event.currentTarget.select()}
/> />
<Btn <Button
icon={eyeIcon} icon={eyeIcon}
label={buttonLabel} label={`${isVisible ? 'Hide' : 'Show'} full address and QR Code`}
onClick={toggleVisibility} onClick={toggleVisibility}
variant='secondary' variant='secondary'
isVisible={isVisible}
/> />
</AddressWrapper> </AddressWrapper>
<RevealsMain isVisible={isVisible}> {isVisible
<Transition ? (
native
items={isVisible}
enter={[
{
height: 'auto',
opacity: 1,
},
]}
leave={{ height: 0, opacity: 0 }}
from={{
position: 'absolute',
overflow: 'hidden',
opacity: 0,
height: 0,
}}
>
{show => show
&& (props => (
<animated.div style={props}>
<QRCodeWrapper> <QRCodeWrapper>
<QRCode value={address} /> <QRCode value={address} />
</QRCodeWrapper> </QRCodeWrapper>
</animated.div> )
)) : null}
} </ColumnComponent>
</Transition>
</RevealsMain>
</div>
); );
} }
} }

View File

@ -25,8 +25,7 @@ export const MENU_OPTIONS = [
{ {
label: 'Dashboard', label: 'Dashboard',
route: DASHBOARD_ROUTE, route: DASHBOARD_ROUTE,
// eslint-disable-next-line icon: (isActive: boolean) => (isActive ? DashboardIconActive : DashboardIcon),
icon: (isActive: boolean) => isActive ? DashboardIconActive : DashboardIcon,
}, },
{ {
label: 'Send', label: 'Send',
@ -41,8 +40,7 @@ export const MENU_OPTIONS = [
{ {
label: 'Transactions', label: 'Transactions',
route: TRANSACTIONS_ROUTE, route: TRANSACTIONS_ROUTE,
// eslint-disable-next-line icon: (isActive: boolean) => (isActive ? TransactionsIconActive : TransactionsIcon),
icon: (isActive: boolean) => isActive ? TransactionsIconActive : TransactionsIcon,
}, },
{ {
label: 'Settings', label: 'Settings',

23
app/containers/app.js Normal file
View File

@ -0,0 +1,23 @@
// @flow
import { connect } from 'react-redux';
import { closeErrorModal } from '../redux/modules/app';
import { LayoutComponent } from '../components/layout';
import type { Dispatch } from '../types/redux';
import type { AppState } from '../types/app-state';
const mapStateToProps = ({ app }: AppState) => ({
isErrorModalVisible: app.isErrorModalVisible,
error: app.error,
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
closeErrorModal: () => dispatch(closeErrorModal()),
});
// $FlowFixMe
export const AppContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(LayoutComponent);

View File

@ -7,6 +7,7 @@ import thunk from 'redux-thunk';
import type { RouterHistory } from 'react-router-dom'; import type { RouterHistory } from 'react-router-dom';
import { createRootReducer } from './modules/reducer'; import { createRootReducer } from './modules/reducer';
import { errorHandler } from './errorHandler';
export const history: RouterHistory = createHashHistory(); export const history: RouterHistory = createHashHistory();
@ -14,7 +15,8 @@ const shouldEnableDevTools = (process.env.NODE_ENV !== 'production' || process.e
&& window.devToolsExtension; && window.devToolsExtension;
export const configureStore = (initialState: Object) => { export const configureStore = (initialState: Object) => {
const middleware = applyMiddleware(thunk, routerMiddleware(history)); // $FlowFixMe
const middleware = applyMiddleware(thunk, routerMiddleware(history), errorHandler);
const enhancer = compose( const enhancer = compose(
middleware, middleware,

16
app/redux/errorHandler.js Normal file
View File

@ -0,0 +1,16 @@
// @flow
import { LOAD_ADDRESSES_ERROR } from './modules/receive';
import { LOAD_TRANSACTIONS_ERROR } from './modules/transactions';
import { LOAD_WALLET_SUMMARY_ERROR } from './modules/wallet';
import { showErrorModal } from './modules/app';
import type { Middleware } from '../types/redux';
const ERRORS = [LOAD_ADDRESSES_ERROR, LOAD_TRANSACTIONS_ERROR, LOAD_WALLET_SUMMARY_ERROR];
export const errorHandler: Middleware = ({ dispatch }) => next => (action) => {
// eslint-disable-next-line max-len
if (ERRORS.includes(action.type)) return dispatch(showErrorModal({ error: action.payload.error || 'Something went wrong!' }));
return next(action);
};

40
app/redux/modules/app.js Normal file
View File

@ -0,0 +1,40 @@
// @flow
import type { Action } from '../../types/redux';
// Actions
export const SHOW_ERROR_MODAL = 'SHOW_ERROR_MODAL';
export const HIDE_ERROR_MODAL = 'HIDE_ERROR_MODAL';
export const showErrorModal = ({ error }: { error: string }) => ({
type: SHOW_ERROR_MODAL,
payload: {
error,
},
});
export const closeErrorModal = () => ({
type: HIDE_ERROR_MODAL,
payload: {},
});
export type State = {
isErrorModalVisible: boolean,
error: string | null,
};
const initialState: State = {
isErrorModalVisible: false,
error: null,
};
// eslint-disable-next-line
export default (state: State = initialState, action: Action) => {
switch (action.type) {
case SHOW_ERROR_MODAL:
return { isErrorModalVisible: true, error: action.payload.error };
case HIDE_ERROR_MODAL:
return { isErrorModalVisible: false, error: null };
default:
return state;
}
};

View File

@ -4,6 +4,7 @@ import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router'; import { connectRouter } from 'connected-react-router';
import type { RouterHistory } from 'react-router-dom'; import type { RouterHistory } from 'react-router-dom';
import app from './app';
import wallet from './wallet'; import wallet from './wallet';
import transactions from './transactions'; import transactions from './transactions';
import send from './send'; import send from './send';
@ -11,6 +12,7 @@ import receive from './receive';
// $FlowFixMe // $FlowFixMe
export const createRootReducer = (history: RouterHistory) => combineReducers({ export const createRootReducer = (history: RouterHistory) => combineReducers({
app,
walletSummary: wallet, walletSummary: wallet,
transactions, transactions,
sendStatus: send, sendStatus: send,

View File

@ -15,7 +15,7 @@ import { ReceiveContainer } from '../containers/receive';
import { SettingsContainer } from '../containers/settings'; import { SettingsContainer } from '../containers/settings';
import { NotFoundView } from '../views/not-found'; import { NotFoundView } from '../views/not-found';
import { ConsoleView } from '../views/console'; import { ConsoleView } from '../views/console';
import { LayoutComponent } from '../components/layout'; import { AppContainer as LayoutComponent } from '../containers/app';
import { HeaderComponent } from '../components/header'; import { HeaderComponent } from '../components/header';
import { import {

View File

@ -4,10 +4,12 @@ import type { State as WalletSummaryState } from '../redux/modules/wallet';
import type { State as TransactionsState } from '../redux/modules/transactions'; import type { State as TransactionsState } from '../redux/modules/transactions';
import type { State as SendState } from '../redux/modules/send'; import type { State as SendState } from '../redux/modules/send';
import type { State as ReceiveState } from '../redux/modules/receive'; import type { State as ReceiveState } from '../redux/modules/receive';
import type { State as App } from '../redux/modules/app';
export type AppState = { export type AppState = {
walletSummary: WalletSummaryState, walletSummary: WalletSummaryState,
transactions: TransactionsState, transactions: TransactionsState,
sendStatus: SendState, sendStatus: SendState,
receive: ReceiveState, receive: ReceiveState,
app: App,
}; };

View File

@ -1,7 +1,10 @@
// @flow // @flow
type State = {||}; import type { AppState } from './app-state';
export type Action = { type: $Subtype<string>, payload: Object }; export type Action = { type: $Subtype<string>, payload: Object };
export type GetState = () => State; export type GetState = () => AppState;
export type Dispatch = (action: Action) => void; export type Dispatch = (action: Action) => void;
export type Middleware = ({ dispatch: Dispatch, getState: GetState }) => (
(Action) => void,
) => Action => void;

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="125" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="125" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h91v20H0z"/><path fill="#4C1" d="M91 0h34v20H91z"/><path fill="url(#b)" d="M0 0h125v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,DejaVu Sans,Geneva,sans-serif" font-size="11"><text x="45.5" y="15" fill="#010101" fill-opacity=".3">flow-coverage</text><text x="45.5" y="14">flow-coverage</text><text x="107" y="15" fill="#010101" fill-opacity=".3">82%</text><text x="107" y="14">82%</text></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="125" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="125" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h91v20H0z"/><path fill="#4C1" d="M91 0h34v20H91z"/><path fill="url(#b)" d="M0 0h125v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,DejaVu Sans,Geneva,sans-serif" font-size="11"><text x="45.5" y="15" fill="#010101" fill-opacity=".3">flow-coverage</text><text x="45.5" y="14">flow-coverage</text><text x="107" y="15" fill="#010101" fill-opacity=".3">83%</text><text x="107" y="14">83%</text></g></svg>

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 745 B