From cb1a4b7d55ab09d91ea769358214fad103cded0a Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Tue, 2 Aug 2016 21:59:29 +0300 Subject: [PATCH] #104: add AcceptRules auth panel --- src/components/auth/PanelTransition.jsx | 1 + src/components/auth/README.md | 15 ++++ .../auth/acceptRules/AcceptRules.intl.json | 6 ++ .../auth/acceptRules/AcceptRules.jsx | 17 ++++ .../auth/acceptRules/AcceptRulesBody.jsx | 38 ++++++++ .../auth/acceptRules/acceptRules.scss | 16 ++++ src/components/auth/actions.js | 9 +- src/components/user/User.js | 3 +- src/components/user/actions.js | 13 +++ src/routes.js | 2 + src/services/api/accounts.js | 4 + src/services/authFlow/AcceptRulesState.js | 23 +++++ src/services/authFlow/AuthFlow.js | 1 + src/services/authFlow/CompleteState.js | 3 + .../authFlow/AcceptRulesState.test.js | 88 +++++++++++++++++++ tests/services/authFlow/AuthFlow.test.js | 2 + tests/services/authFlow/CompleteState.test.js | 30 +++++++ 17 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 src/components/auth/README.md create mode 100644 src/components/auth/acceptRules/AcceptRules.intl.json create mode 100644 src/components/auth/acceptRules/AcceptRules.jsx create mode 100644 src/components/auth/acceptRules/AcceptRulesBody.jsx create mode 100644 src/components/auth/acceptRules/acceptRules.scss create mode 100644 src/services/authFlow/AcceptRulesState.js create mode 100644 tests/services/authFlow/AcceptRulesState.test.js diff --git a/src/components/auth/PanelTransition.jsx b/src/components/auth/PanelTransition.jsx index bf4699a..dae99df 100644 --- a/src/components/auth/PanelTransition.jsx +++ b/src/components/auth/PanelTransition.jsx @@ -33,6 +33,7 @@ const contexts = [ ['login', 'password', 'forgotPassword', 'recoverPassword'], ['register', 'activation', 'resendActivation'], ['changePassword'], + ['acceptRules'], ['permissions'] ]; diff --git a/src/components/auth/README.md b/src/components/auth/README.md new file mode 100644 index 0000000..8dd2b60 --- /dev/null +++ b/src/components/auth/README.md @@ -0,0 +1,15 @@ +# How to add new auth panel + +To add new panel you need to: + +* add new state to `services/authFlow` and coresponding test to `tests/services/authFlow` +* connect state to `authFlow`. Update `services/authFlow/AuthFlow.test` and `services/authFlow/AuthFlow.functional.test` (the last one for some complex flow) +* add new actions to `components/auth/actions` and api endpoints to `services/api` +* create panel component at `components/auth/[panelId]` +* add new context in `components/auth/PanelTransition` +* connect component to `routes` +* whatever else you need + +# TODO + +This flow must be simplified diff --git a/src/components/auth/acceptRules/AcceptRules.intl.json b/src/components/auth/acceptRules/AcceptRules.intl.json new file mode 100644 index 0000000..ec23675 --- /dev/null +++ b/src/components/auth/acceptRules/AcceptRules.intl.json @@ -0,0 +1,6 @@ +{ + "title": "Accept new rules", + "accept": "Accept", + "decline": "Decline", + "description": "We have updated our {link}. In order to get access to accounts.ely.by service, you need to accept them." +} diff --git a/src/components/auth/acceptRules/AcceptRules.jsx b/src/components/auth/acceptRules/AcceptRules.jsx new file mode 100644 index 0000000..67504b5 --- /dev/null +++ b/src/components/auth/acceptRules/AcceptRules.jsx @@ -0,0 +1,17 @@ +import factory from 'components/auth/factory'; + +import Body from './AcceptRulesBody'; +import messages from './AcceptRules.intl.json'; + +export default factory({ + title: messages.title, + body: Body, + footer: { + color: 'darkBlue', + autoFocus: true, + label: messages.accept + }, + links: { + label: messages.decline + } +}); diff --git a/src/components/auth/acceptRules/AcceptRulesBody.jsx b/src/components/auth/acceptRules/AcceptRulesBody.jsx new file mode 100644 index 0000000..2515ed4 --- /dev/null +++ b/src/components/auth/acceptRules/AcceptRulesBody.jsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { FormattedMessage as Message } from 'react-intl'; +import { Link } from 'react-router'; + +import icons from 'components/ui/icons.scss'; +import BaseAuthBody from 'components/auth/BaseAuthBody'; +import registerMessages from 'components/auth/register/Register.intl.json'; + +import styles from './acceptRules.scss'; +import messages from './AcceptRules.intl.json'; + +export default class AcceptRulesBody extends BaseAuthBody { + static displayName = 'AcceptRulesBody'; + static panelId = 'acceptRules'; + + render() { + return ( +
+ {this.renderErrors()} + +
+ +
+ +

+ + + + ) + }} /> +

+
+ ); + } +} diff --git a/src/components/auth/acceptRules/acceptRules.scss b/src/components/auth/acceptRules/acceptRules.scss new file mode 100644 index 0000000..1daf163 --- /dev/null +++ b/src/components/auth/acceptRules/acceptRules.scss @@ -0,0 +1,16 @@ +@import '~components/ui/colors.scss'; + +.descriptionText { + font-size: 15px; + line-height: 1.4; + padding-bottom: 8px; + color: #aaa; +} + +// TODO: вынести иконки такого типа в какую-то внешнюю структуру? +.security { + color: #fff; + font-size: 90px; + line-height: 1; + margin-bottom: 15px; +} diff --git a/src/components/auth/actions.js b/src/components/auth/actions.js index a1ad175..fb122e6 100644 --- a/src/components/auth/actions.js +++ b/src/components/auth/actions.js @@ -1,6 +1,6 @@ import { routeActions } from 'react-router-redux'; -import { updateUser, logout as logoutUser, changePassword as changeUserPassword, authenticate } from 'components/user/actions'; +import { updateUser, logout as logoutUser, changePassword as changeUserPassword, acceptRules as userAcceptRules, authenticate } from 'components/user/actions'; import authentication from 'services/api/authentication'; import oauth from 'services/api/oauth'; import signup from 'services/api/signup'; @@ -46,6 +46,13 @@ export function changePassword({ ); } +export function acceptRules() { + return wrapInLoader((dispatch) => + dispatch(userAcceptRules()) + .catch(validationErrorsHandler(dispatch)) + ); +} + export function forgotPassword({ login = '' }) { diff --git a/src/components/user/User.js b/src/components/user/User.js index da86249..726fbb9 100644 --- a/src/components/user/User.js +++ b/src/components/user/User.js @@ -4,7 +4,7 @@ const KEY_USER = 'user'; export default class User { /** - * @param {Object|string|undefined} data plain object or jwt token or empty to load from storage + * @param {object|string|undefined} data plain object or jwt token or empty to load from storage * * @return {User} */ @@ -30,6 +30,7 @@ export default class User { goal: null, // the goal with wich user entered site isGuest: true, isActive: false, + shouldAcceptRules: false, // whether user need to review updated rules shouldChangePassword: false, // TODO: нужно ещё пробросить причину необходимости смены passwordChangedAt: null, hasMojangUsernameCollision: false, diff --git a/src/components/user/actions.js b/src/components/user/actions.js index e02fb72..65dbea8 100644 --- a/src/components/user/actions.js +++ b/src/components/user/actions.js @@ -89,6 +89,19 @@ export function changePassword({ ; } +export function acceptRules() { + return (dispatch) => + accounts.acceptRules() + .then((resp) => { + dispatch(updateUser({ + shouldAcceptRules: false + })); + + return resp; + }) + ; +} + let middlewareAdded = false; export function authenticate(token, refreshToken) { // TODO: this action, probably, belongs to components/auth return (dispatch, getState) => { diff --git a/src/routes.js b/src/routes.js index 972fd9e..962650f 100644 --- a/src/routes.js +++ b/src/routes.js @@ -21,6 +21,7 @@ import Activation from 'components/auth/activation/Activation'; import ResendActivation from 'components/auth/resendActivation/ResendActivation'; import Password from 'components/auth/password/Password'; import ChangePassword from 'components/auth/changePassword/ChangePassword'; +import AcceptRules from 'components/auth/acceptRules/AcceptRules'; import ForgotPassword from 'components/auth/forgotPassword/ForgotPassword'; import RecoverPassword from 'components/auth/recoverPassword/RecoverPassword'; import Finish from 'components/auth/finish/Finish'; @@ -60,6 +61,7 @@ export default function routesFactory(store) { + diff --git a/src/services/api/accounts.js b/src/services/api/accounts.js index 03d7c58..34dac16 100644 --- a/src/services/api/accounts.js +++ b/src/services/api/accounts.js @@ -17,6 +17,10 @@ export default { ); }, + acceptRules() { + return request.post('/api/accounts/accept-rules'); + }, + changeUsername({ username = '', password = '' diff --git a/src/services/authFlow/AcceptRulesState.js b/src/services/authFlow/AcceptRulesState.js new file mode 100644 index 0000000..755302d --- /dev/null +++ b/src/services/authFlow/AcceptRulesState.js @@ -0,0 +1,23 @@ +import AbstractState from './AbstractState'; +import CompleteState from './CompleteState'; + +export default class AcceptRulesState extends AbstractState { + enter(context) { + const {user} = context.getState(); + + if (user.shouldAcceptRules) { + context.navigate('/accept-rules'); + } else { + context.setState(new CompleteState()); + } + } + + resolve(context) { + context.run('acceptRules') + .then(() => context.setState(new CompleteState())); + } + + reject(context) { + context.run('logout'); + } +} diff --git a/src/services/authFlow/AuthFlow.js b/src/services/authFlow/AuthFlow.js index c46c4db..c7f4e5b 100644 --- a/src/services/authFlow/AuthFlow.js +++ b/src/services/authFlow/AuthFlow.js @@ -143,6 +143,7 @@ export default class AuthFlow { case '/': case '/login': case '/password': + case '/accept-rules': case '/change-password': case '/oauth/permissions': case '/oauth/finish': diff --git a/src/services/authFlow/CompleteState.js b/src/services/authFlow/CompleteState.js index d7ab333..c5d68b9 100644 --- a/src/services/authFlow/CompleteState.js +++ b/src/services/authFlow/CompleteState.js @@ -3,6 +3,7 @@ import LoginState from './LoginState'; import PermissionsState from './PermissionsState'; import ActivationState from './ActivationState'; import ChangePasswordState from './ChangePasswordState'; +import AcceptRulesState from './AcceptRulesState'; import FinishState from './FinishState'; export default class CompleteState extends AbstractState { @@ -19,6 +20,8 @@ export default class CompleteState extends AbstractState { context.setState(new LoginState()); } else if (!user.isActive) { context.setState(new ActivationState()); + } else if (user.shouldAcceptRules) { + context.setState(new AcceptRulesState()); } else if (user.shouldChangePassword) { context.setState(new ChangePasswordState()); } else if (auth.oauth && auth.oauth.clientId) { diff --git a/tests/services/authFlow/AcceptRulesState.test.js b/tests/services/authFlow/AcceptRulesState.test.js new file mode 100644 index 0000000..f0f05f1 --- /dev/null +++ b/tests/services/authFlow/AcceptRulesState.test.js @@ -0,0 +1,88 @@ +import AcceptRulesState from 'services/authFlow/AcceptRulesState'; +import CompleteState from 'services/authFlow/CompleteState'; + +import { bootstrap, expectState, expectNavigate, expectRun } from './helpers'; + +describe('AcceptRulesState', () => { + let state; + let context; + let mock; + + beforeEach(() => { + state = new AcceptRulesState(); + + const data = bootstrap(); + context = data.context; + mock = data.mock; + }); + + afterEach(() => { + mock.verify(); + }); + + describe('#enter', () => { + it('should navigate to /accept-rules', () => { + context.getState.returns({ + user: { + shouldAcceptRules: true, + isGuest: false + } + }); + + expectNavigate(mock, '/accept-rules'); + + state.enter(context); + }); + + it('should transition to complete state if rules accepted', () => { + context.getState.returns({ + user: { + shouldAcceptRules: false, + isGuest: false + } + }); + + expectState(mock, CompleteState); + + state.enter(context); + }); + }); + + describe('#resolve', () => { + it('should call acceptRules', () => { + expectRun(mock, 'acceptRules').returns({then() {}}); + + state.resolve(context); + }); + + it('should transition to complete state on success', () => { + const promise = Promise.resolve(); + + mock.expects('run').returns(promise); + expectState(mock, CompleteState); + + state.resolve(context); + + return promise; + }); + + it('should NOT transition to complete state on fail', () => { + const promise = Promise.reject(); + + mock.expects('run').returns(promise); + mock.expects('setState').never(); + + state.resolve(context); + + return promise.catch(mock.verify.bind(mock)); + }); + }); + + describe('#reject', () => { + it('should logout', () => { + expectRun(mock, 'logout'); + + state.reject(context); + }); + }); +}); diff --git a/tests/services/authFlow/AuthFlow.test.js b/tests/services/authFlow/AuthFlow.test.js index 21e9f07..2b2c6d2 100644 --- a/tests/services/authFlow/AuthFlow.test.js +++ b/tests/services/authFlow/AuthFlow.test.js @@ -5,6 +5,7 @@ import AbstractState from 'services/authFlow/AbstractState'; import OAuthState from 'services/authFlow/OAuthState'; import RegisterState from 'services/authFlow/RegisterState'; +import AcceptRulesState from 'services/authFlow/AcceptRulesState'; import RecoverPasswordState from 'services/authFlow/RecoverPasswordState'; import ForgotPasswordState from 'services/authFlow/ForgotPasswordState'; import ActivationState from 'services/authFlow/ActivationState'; @@ -178,6 +179,7 @@ describe('AuthFlow', () => { '/login': LoginState, '/password': LoginState, '/change-password': LoginState, + '/accept-rules': LoginState, '/oauth/permissions': LoginState, '/oauth/finish': LoginState, '/oauth2/v1': OAuthState, diff --git a/tests/services/authFlow/CompleteState.test.js b/tests/services/authFlow/CompleteState.test.js index e77de5d..4d0977f 100644 --- a/tests/services/authFlow/CompleteState.test.js +++ b/tests/services/authFlow/CompleteState.test.js @@ -4,6 +4,7 @@ import CompleteState from 'services/authFlow/CompleteState'; import LoginState from 'services/authFlow/LoginState'; import ActivationState from 'services/authFlow/ActivationState'; import ChangePasswordState from 'services/authFlow/ChangePasswordState'; +import AcceptRulesState from 'services/authFlow/AcceptRulesState'; import FinishState from 'services/authFlow/FinishState'; import PermissionsState from 'services/authFlow/PermissionsState'; @@ -96,6 +97,35 @@ describe('CompleteState', () => { state.enter(context); }); + it('should transition to accept-rules if shouldAcceptRules', () => { + context.getState.returns({ + user: { + shouldAcceptRules: true, + isActive: true, + isGuest: false + }, + auth: {} + }); + + expectState(mock, AcceptRulesState); + + state.enter(context); + }); + + it('should transition to activation with higher priority than shouldAcceptRules', () => { + context.getState.returns({ + user: { + shouldAcceptRules: true, + isGuest: false + }, + auth: {} + }); + + expectState(mock, ActivationState); + + state.enter(context); + }); + it('should transition to finish state if code is present', () => { context.getState.returns({ user: {