#97: frontend for repeat activation functionality

This commit is contained in:
SleepWalker 2016-05-22 21:58:43 +03:00
parent 6d44e43f5d
commit 57f3134cd8
15 changed files with 238 additions and 2 deletions

View File

@ -31,7 +31,7 @@ const changeContextSpringConfig = {stiffness: 500, damping: 20, precision: 0.5};
*/
const contexts = [
['login', 'password', 'forgotPassword', 'recoverPassword'],
['register', 'activation'],
['register', 'activation', 'resendActivation'],
['changePassword'],
['permissions']
];

View File

@ -126,7 +126,17 @@ export function activate({key = ''}) {
return dispatch(authenticate(resp.jwt));
})
.catch(validationErrorsHandler(dispatch, '/reactivate'))
.catch(validationErrorsHandler(dispatch, '/resend-activation'))
);
}
export function resendActivation({email = ''}) {
return wrapInLoader((dispatch) =>
request.post(
'/api/signup/repeat-message',
{email}
)
.catch(validationErrorsHandler(dispatch))
);
}

View File

@ -0,0 +1,5 @@
{
"title": "Did not received an E-mail",
"specifyYourEmail": "Please, enter an E-mail you've registered with and we will send you new activation code.",
"sendNewEmail": "Send new E-mail"
}

View File

@ -0,0 +1,16 @@
import React from 'react';
import { Button } from 'components/ui/form';
import AuthTitle from 'components/auth/AuthTitle';
import messages from './ResendActivation.intl.json';
import Body from './ResendActivationBody';
export default function ResendActivation() {
return {
Title: () => <AuthTitle title={messages.title} />,
Body,
Footer: () => <Button color="blue" label={messages.sendNewEmail} type="submit" />,
Links: () => null
};
}

View File

@ -0,0 +1,40 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Input } from 'components/ui/form';
import registerMessages from 'components/auth/register/Register.intl.json';
import BaseAuthBody from 'components/auth/BaseAuthBody';
import styles from './resendActivation.scss';
import messages from './ResendActivation.intl.json';
export default class ResendActivation extends BaseAuthBody {
static displayName = 'ResendActivation';
static panelId = 'resendActivation';
static hasGoBack = true;
autoFocusField = 'email';
render() {
return (
<div>
{this.renderErrors()}
<div className={styles.description}>
<Message {...messages.specifyYourEmail} />
</div>
<div className={styles.formRow}>
<Input {...this.bindField('email')}
icon="envelope"
color="blue"
type="email"
required
placeholder={registerMessages.yourEmail}
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,8 @@
@import '~components/ui/fonts.scss';
.description {
font-family: $font-family-title;
margin: 5px 0 19px;
line-height: 1.4;
font-size: 16px;
}

View File

@ -57,6 +57,9 @@
"components.auth.register.termsOfService": "terms of service",
"components.auth.register.yourEmail": "Your E-mail",
"components.auth.register.yourNickname": "Your nickname",
"components.auth.resendActivation.sendNewEmail": "Send new E-mail",
"components.auth.resendActivation.specifyYourEmail": "Please, enter an E-mail you've registered with and we will send you new activation code.",
"components.auth.resendActivation.title": "Did not received an E-mail",
"components.contact.email": "E-mail",
"components.contact.message": "Message",
"components.contact.send": "Send",

View File

@ -57,6 +57,9 @@
"components.auth.register.termsOfService": "правилами сервиса",
"components.auth.register.yourEmail": "Ваш E-mail",
"components.auth.register.yourNickname": "Желаемый ник",
"components.auth.resendActivation.sendNewEmail": "Отправить новое письмо",
"components.auth.resendActivation.specifyYourEmail": "Укажите здесь ваш регистрационный E-mail адрес и мы вышлем на него новое письмо с кодом активации аккаунта",
"components.auth.resendActivation.title": "Не получил письмо",
"components.contact.email": "E-mail",
"components.contact.message": "Сообщение",
"components.contact.send": "Отправить",

View File

@ -17,6 +17,7 @@ import Register from 'components/auth/register/Register';
import Login from 'components/auth/login/Login';
import Permissions from 'components/auth/permissions/Permissions';
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 ForgotPassword from 'components/auth/forgotPassword/ForgotPassword';
@ -55,6 +56,7 @@ export default function routesFactory(store) {
<Route path="/password" components={new Password()} {...startAuthFlow} />
<Route path="/register" components={new Register()} {...startAuthFlow} />
<Route path="/activation" components={new Activation()} {...startAuthFlow} />
<Route path="/resend-activation" components={new ResendActivation()} {...startAuthFlow} />
<Route path="/oauth/permissions" components={new Permissions()} {...startAuthFlow} />
<Route path="/oauth/finish" component={Finish} {...startAuthFlow} />
<Route path="/change-password" components={new ChangePassword()} {...startAuthFlow} />

View File

@ -1,5 +1,6 @@
import AbstractState from './AbstractState';
import CompleteState from './CompleteState';
import ResendActivationState from './ResendActivationState';
export default class ActivationState extends AbstractState {
enter(context) {
@ -16,4 +17,8 @@ export default class ActivationState extends AbstractState {
context.run('activate', payload)
.then(() => context.setState(new CompleteState()));
}
reject(context) {
context.setState(new ResendActivationState());
}
}

View File

@ -5,6 +5,7 @@ import LoginState from './LoginState';
import OAuthState from './OAuthState';
import ForgotPasswordState from './ForgotPasswordState';
import RecoverPasswordState from './RecoverPasswordState';
import ResendActivationState from './ResendActivationState';
// TODO: a way to unload service (when we are on account page)
@ -99,6 +100,10 @@ export default class AuthFlow {
this.setState(new ForgotPasswordState());
break;
case '/resend-activation':
this.setState(new ResendActivationState());
break;
case '/':
case '/login':
case '/password':

View File

@ -0,0 +1,24 @@
import AbstractState from './AbstractState';
import CompleteState from './CompleteState';
import ActivationState from './ActivationState';
export default class ResendActivationState extends AbstractState {
enter(context) {
const {user} = context.getState();
if (user.isActive) {
context.setState(new CompleteState());
} else {
context.navigate('/repeat-message');
}
}
resolve(context, payload) {
context.run('resendActivation', payload)
.then(() => context.setState(new CompleteState()));
}
goBack(context) {
context.setState(new ActivationState());
}
}

View File

@ -1,5 +1,6 @@
import ActivationState from 'services/authFlow/ActivationState';
import CompleteState from 'services/authFlow/CompleteState';
import ResendActivationState from 'services/authFlow/ResendActivationState';
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
@ -83,4 +84,12 @@ describe('ActivationState', () => {
return promise.catch(mock.verify.bind(mock));
});
});
describe('#reject', () => {
it('should transition to resend-activation', () => {
expectState(mock, ResendActivationState);
state.reject(context);
});
});
});

View File

@ -84,6 +84,17 @@ describe('RecoverPasswordState', () => {
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', () => {

View File

@ -0,0 +1,95 @@
import ResendActivationState from 'services/authFlow/ResendActivationState';
import CompleteState from 'services/authFlow/CompleteState';
import ActivationState from 'services/authFlow/ActivationState';
import { bootstrap, expectState, expectNavigate, expectRun } from './helpers';
describe('ResendActivationState', () => {
let state;
let context;
let mock;
beforeEach(() => {
state = new ResendActivationState();
const data = bootstrap();
context = data.context;
mock = data.mock;
});
afterEach(() => {
mock.verify();
});
describe('#enter', () => {
it('should navigate to /resend-activation', () => {
context.getState.returns({
user: {
isGuest: false,
isActive: false
}
});
expectNavigate(mock, '/resend-activation');
state.enter(context);
});
it('should transition to complete state if account activated', () => {
context.getState.returns({
user: {
isGuest: false,
isActive: true
}
});
expectState(mock, CompleteState);
state.enter(context);
});
});
describe('#resolve', () => {
it('should call resendActivation with payload', () => {
const payload = {email: 'foo@bar.com'};
expectRun(
mock,
'resendActivation',
sinon.match.same(payload)
).returns({then() {}});
state.resolve(context, payload);
});
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('#goBack', () => {
it('should transition to resend-activation', () => {
expectState(mock, ActivationState);
state.goBack(context);
});
});
});