mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-27 19:42:11 +10:00
Merge pull request #58 from sharetribe/login-error-message
Authentication error messages
This commit is contained in:
commit
aa0cf4330f
9 changed files with 107 additions and 42 deletions
|
|
@ -7,13 +7,10 @@ const HeroSection = props => (
|
|||
<div className={css.content}>
|
||||
<div className={css.titleWrapper}>
|
||||
<div className={css.title}>
|
||||
<FormattedMessage id={'HeroSection.title'} defaultMessage={'Book Studiotime anywhere'} />
|
||||
<FormattedMessage id="HeroSection.title" />
|
||||
</div>
|
||||
<div className={css.subTitle}>
|
||||
<FormattedMessage
|
||||
id={'HeroSection.subTitle'}
|
||||
defaultMessage={'The largest online community to rent music studios'}
|
||||
/>
|
||||
<FormattedMessage id="HeroSection.subTitle" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.ctaWrapper}>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { Component, PropTypes } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import Helmet from 'react-helmet';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Topbar } from '../../containers';
|
||||
|
||||
const scrollToTop = () => {
|
||||
|
|
@ -21,12 +22,32 @@ class PageLayout extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { className, title, children, authError } = this.props;
|
||||
// TODO: use FlashMessages for authError
|
||||
const { className, title, children, authInfoError, logoutError } = this.props;
|
||||
|
||||
// TODO: use FlashMessages for auth errors
|
||||
|
||||
/* eslint-disable no-console */
|
||||
if (authInfoError && console && console.error) {
|
||||
console.error(authInfoError);
|
||||
}
|
||||
if (logoutError && console && console.error) {
|
||||
console.error(logoutError);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Helmet title={title} />
|
||||
{authError ? <p style={{ color: 'red' }}>Error in auth: {authError.message}</p> : null}
|
||||
{authInfoError
|
||||
? <div style={{ color: 'red' }}>
|
||||
<FormattedMessage id="PageLayout.authInfoFailed" />
|
||||
</div>
|
||||
: null}
|
||||
{logoutError
|
||||
? <div style={{ color: 'red' }}>
|
||||
<FormattedMessage id="PageLayout.logoutFailed" />
|
||||
</div>
|
||||
: null}
|
||||
<Topbar />
|
||||
<h1>{title}</h1>
|
||||
{children}
|
||||
|
|
@ -37,17 +58,21 @@ class PageLayout extends Component {
|
|||
|
||||
const { any, string, instanceOf, func } = PropTypes;
|
||||
|
||||
PageLayout.defaultProps = { className: '', children: null, authError: null };
|
||||
PageLayout.defaultProps = { className: '', children: null, authInfoError: null, logoutError: null };
|
||||
|
||||
PageLayout.propTypes = {
|
||||
className: string,
|
||||
title: string.isRequired,
|
||||
children: any,
|
||||
authError: instanceOf(Error),
|
||||
authInfoError: instanceOf(Error),
|
||||
logoutError: instanceOf(Error),
|
||||
// history.listen function from withRouter
|
||||
listen: func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({ authError: state.Auth.error });
|
||||
const mapStateToProps = state => ({
|
||||
authInfoError: state.Auth.authInfoError,
|
||||
logoutError: state.Auth.logoutError,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withRouter(PageLayout));
|
||||
|
|
|
|||
|
|
@ -1,23 +1,44 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { PageLayout, NamedLink, NamedRedirect } from '../../components';
|
||||
import { LoginForm, SignUpForm } from '../../containers';
|
||||
import { login } from '../../ducks/Auth.duck';
|
||||
|
||||
export const AuthenticationPageComponent = props => {
|
||||
const { location, tab, isAuthenticated, onLoginSubmit, onSignUpSubmit } = props;
|
||||
const {
|
||||
location,
|
||||
tab,
|
||||
isAuthenticated,
|
||||
loginError,
|
||||
onLoginSubmit,
|
||||
onSignUpSubmit,
|
||||
} = props;
|
||||
const isLogin = tab === 'login';
|
||||
const from = location.state && location.state.from ? location.state.from : null;
|
||||
|
||||
const authRedirect = from ? <Redirect to={from} /> : <NamedRedirect name="LandingPage" />;
|
||||
|
||||
// TODO: use FlashMessages for auth errors
|
||||
|
||||
/* eslint-disable no-console */
|
||||
if (loginError && console && console.error) {
|
||||
console.error(loginError);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
|
||||
return (
|
||||
<PageLayout title={`Authentication page: ${tab} tab`}>
|
||||
{isAuthenticated ? authRedirect : null}
|
||||
{loginError
|
||||
? <div style={{ color: 'red' }}>
|
||||
<FormattedMessage id="AuthenticationPage.loginFailed" />
|
||||
</div>
|
||||
: null}
|
||||
{from
|
||||
? <p>
|
||||
You must log in to view the page at
|
||||
<FormattedMessage id="AuthenticationPage.loginRequiredFor" />
|
||||
<code>{from}</code>
|
||||
</p>
|
||||
: null}
|
||||
|
|
@ -29,19 +50,28 @@ export const AuthenticationPageComponent = props => {
|
|||
);
|
||||
};
|
||||
|
||||
AuthenticationPageComponent.defaultProps = { tab: 'signup' };
|
||||
AuthenticationPageComponent.defaultProps = {
|
||||
tab: 'signup',
|
||||
authInfoError: null,
|
||||
loginError: null,
|
||||
logoutError: null,
|
||||
};
|
||||
|
||||
const { object, oneOf, shape, bool, func } = PropTypes;
|
||||
const { object, oneOf, shape, bool, func, instanceOf } = PropTypes;
|
||||
|
||||
AuthenticationPageComponent.propTypes = {
|
||||
location: shape({ state: object }).isRequired,
|
||||
tab: oneOf(['login', 'signup']),
|
||||
isAuthenticated: bool.isRequired,
|
||||
loginError: instanceOf(Error),
|
||||
onLoginSubmit: func.isRequired,
|
||||
onSignUpSubmit: func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({ isAuthenticated: state.Auth.isAuthenticated });
|
||||
const mapStateToProps = state => ({
|
||||
isAuthenticated: state.Auth.isAuthenticated,
|
||||
loginError: state.Auth.loginError,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onLoginSubmit: ({ email, password }) => dispatch(login(email, password)),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ exports[`AuthenticationPageComponent matches snapshot 1`] = `
|
|||
<Connect(withRouter(PageLayout))
|
||||
title="Authentication page: signup tab">
|
||||
<p>
|
||||
You must log in to view the page at
|
||||
<FormattedMessage
|
||||
id="AuthenticationPage.loginRequiredFor"
|
||||
values={Object {}} />
|
||||
<code>
|
||||
/protected
|
||||
</code>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ import css from './HeroSearchForm.css';
|
|||
const HeroSearchForm = props => {
|
||||
const { className, intl, handleSubmit, pristine, submitting } = props;
|
||||
const addClassName = className ? { className } : {};
|
||||
const placeholderMsg = {
|
||||
id: 'HeroSearchForm.placeholder',
|
||||
defaultMessage: 'Location search (soon)',
|
||||
};
|
||||
const placeholderMsg = { id: 'HeroSearchForm.placeholder' };
|
||||
|
||||
return (
|
||||
<form {...addClassName} onSubmit={handleSubmit}>
|
||||
|
|
@ -21,7 +18,7 @@ const HeroSearchForm = props => {
|
|||
placeholder={intl.formatMessage(placeholderMsg)}
|
||||
/>
|
||||
<button className={css.locationButton} type="submit" disabled={pristine || submitting}>
|
||||
<FormattedMessage id="HeroSearchForm.search" defaultMessage="Search" />
|
||||
<FormattedMessage id="HeroSearchForm.search" />
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,31 +22,37 @@ export const LOGOUT_ERROR = 'app/Auth/LOGOUT_ERROR';
|
|||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = { authInfoLoaded: false, isAuthenticated: false, error: null };
|
||||
const initialState = {
|
||||
authInfoLoaded: false,
|
||||
isAuthenticated: false,
|
||||
authInfoError: null,
|
||||
loginError: null,
|
||||
logoutError: null,
|
||||
};
|
||||
|
||||
export default function reducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case AUTH_INFO_REQUEST:
|
||||
return { ...state, error: null };
|
||||
return { ...state, authInfoError: null };
|
||||
case AUTH_INFO_SUCCESS:
|
||||
return { ...state, authInfoLoaded: true, isAuthenticated: authenticated(payload) };
|
||||
case AUTH_INFO_ERROR:
|
||||
return { ...state, error: payload };
|
||||
return { ...state, authInfoError: payload };
|
||||
|
||||
case LOGIN_REQUEST:
|
||||
return { ...state, error: null };
|
||||
return { ...state, loginError: null, logoutError: null };
|
||||
case LOGIN_SUCCESS:
|
||||
return { ...state, isAuthenticated: true };
|
||||
case LOGIN_ERROR:
|
||||
return { ...state, error: payload };
|
||||
return { ...state, loginError: payload };
|
||||
|
||||
case LOGOUT_REQUEST:
|
||||
return { ...state, error: null };
|
||||
return { ...state, loginError: null, logoutError: null };
|
||||
case LOGOUT_SUCCESS:
|
||||
return { ...state, isAuthenticated: false };
|
||||
case LOGOUT_ERROR:
|
||||
return { ...state, error: payload };
|
||||
return { ...state, logoutError: payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ describe('Auth duck', () => {
|
|||
const state = reducer();
|
||||
expect(state.authInfoLoaded).toEqual(false);
|
||||
expect(state.isAuthenticated).toEqual(false);
|
||||
expect(state.error).toBeNull();
|
||||
expect(state.authInfoError).toBeNull();
|
||||
expect(state.loginError).toBeNull();
|
||||
expect(state.logoutError).toBeNull();
|
||||
});
|
||||
|
||||
it('should login successfully', () => {
|
||||
|
|
@ -36,23 +38,23 @@ describe('Auth duck', () => {
|
|||
let state = reducer();
|
||||
state = reducer(state, login(username, password));
|
||||
expect(state.isAuthenticated).toEqual(false);
|
||||
expect(state.error).toBeNull();
|
||||
expect(state.loginError).toBeNull();
|
||||
|
||||
state = reducer(state, loginSuccess());
|
||||
expect(state.isAuthenticated).toEqual(true);
|
||||
expect(state.error).toBeNull();
|
||||
expect(state.loginError).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle failed login', () => {
|
||||
let state = reducer();
|
||||
state = reducer(state, login('username', 'pass'));
|
||||
expect(state.isAuthenticated).toEqual(false);
|
||||
expect(state.error).toBeNull();
|
||||
expect(state.loginError).toBeNull();
|
||||
|
||||
const error = new Error('test error');
|
||||
state = reducer(state, loginError(error));
|
||||
expect(state.isAuthenticated).toEqual(false);
|
||||
expect(state.error).toEqual(error);
|
||||
expect(state.loginError).toEqual(error);
|
||||
});
|
||||
|
||||
it('should set initial state for unauthenticated users', () => {
|
||||
|
|
@ -62,7 +64,7 @@ describe('Auth duck', () => {
|
|||
const state = reducer(initialState, authInfoSuccess(authInfoLoggedOut));
|
||||
expect(state.authInfoLoaded).toEqual(true);
|
||||
expect(state.isAuthenticated).toEqual(false);
|
||||
expect(state.error).toBeNull();
|
||||
expect(state.authInfoError).toBeNull();
|
||||
});
|
||||
|
||||
it('should set initial state for anonymous users', () => {
|
||||
|
|
@ -72,7 +74,7 @@ describe('Auth duck', () => {
|
|||
const state = reducer(initialState, authInfoSuccess(authInfoAnonymous));
|
||||
expect(state.authInfoLoaded).toEqual(true);
|
||||
expect(state.isAuthenticated).toEqual(false);
|
||||
expect(state.error).toBeNull();
|
||||
expect(state.authInfoError).toBeNull();
|
||||
});
|
||||
|
||||
it('should set initial state for unauthenticated users', () => {
|
||||
|
|
@ -82,7 +84,7 @@ describe('Auth duck', () => {
|
|||
const state = reducer(initialState, authInfoSuccess(authInfoLoggedIn));
|
||||
expect(state.authInfoLoaded).toEqual(true);
|
||||
expect(state.isAuthenticated).toEqual(true);
|
||||
expect(state.error).toBeNull();
|
||||
expect(state.authInfoError).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
{
|
||||
"landingpage.examplelink": "Show nice studios! (from json)",
|
||||
"AuthenticationPage.loginFailed": "The email and password you entered did not match our records. Please double-check and try again.",
|
||||
"AuthenticationPage.loginRequiredFor": "You must log in to view the page at",
|
||||
"HeroSearchForm.placeholder": "Location search (soon)",
|
||||
"HeroSearchForm.search": "Search",
|
||||
"HeroSection.title": "Book Studiotime anywhere",
|
||||
"HeroSection.subTitle": "The largest online community to rent music studios",
|
||||
"ListingPage.loadingListingData": "Loading listing data"
|
||||
"HeroSection.title": "Book Studiotime anywhere",
|
||||
"ListingPage.loadingListingData": "Loading listing data",
|
||||
"PageLayout.authInfoFailed": "Could not get authentication information.",
|
||||
"PageLayout.logoutFailed": "Logout failed. Please try again."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,20 @@ import React from 'react';
|
|||
import renderer from 'react-test-renderer';
|
||||
import { shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import en from 'react-intl/locale-data/en';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from '../store';
|
||||
import localeData from '../translations/en.json';
|
||||
|
||||
// Provide all the context for components that connect to the Redux
|
||||
// store, i18n, router, etc.
|
||||
export const TestProvider = props => {
|
||||
const store = configureStore();
|
||||
addLocaleData([...en]);
|
||||
return (
|
||||
<IntlProvider locale="en">
|
||||
<IntlProvider locale="en" messages={localeData}>
|
||||
<BrowserRouter>
|
||||
<Provider store={store}>
|
||||
{props.children}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue