#41: integrate change email with backend

This commit is contained in:
SleepWalker
2016-05-22 10:53:40 +03:00
parent fa5ba292cb
commit dffc73cc6d
21 changed files with 338 additions and 284 deletions

View File

@@ -0,0 +1,17 @@
{
"changeEmailTitle": "Change E-mail",
"changeEmailDescription": "To change current account E-mail you must first verify that you own the current address and then confirm the new one.",
"currentAccountEmail": "Current account E-mail address:",
"pressButtonToStart": "Press the button below to send a message with the code for E-mail change initialization.",
"enterInitializationCode": "The E-mail with an initialization code for E-mail change procedure was sent to {email}. Please enter the code into the field below:",
"enterNewEmail": "Then provide your new E-mail address, that you want to use with this account. You will be mailed with confirmation code.",
"finalizationCodeWasSentToEmail": "The E-mail change confirmation code was sent to {email}.",
"enterFinalizationCode": "In order to confirm your new E-mail, please enter the code received into the field below:",
"newEmailPlaceholder": "Enter new E-mail",
"codePlaceholder": "Paste the code here",
"sendEmailButton": "Send E-mail",
"changeEmailButton": "Change E-mail",
"alreadyReceivedCode": "Already received code"
}

View File

@@ -12,19 +12,28 @@ import helpLinks from 'components/auth/helpLinks.scss';
import MeasureHeight from 'components/MeasureHeight';
import changeEmail from './changeEmail.scss';
import messages from './ChangeEmail.messages';
import messages from './ChangeEmail.intl.json';
const STEPS_TOTAL = 3;
// TODO: disable code field, if the code was passed through url
export default class ChangeEmail extends Component {
static displayName = 'ChangeEmail';
static propTypes = {
onChangeStep: PropTypes.func,
email: PropTypes.string.isRequired,
form: PropTypes.instanceOf(FormModel),
stepForms: PropTypes.arrayOf((propValue, key, componentName, location, propFullName) => {
if (propValue.length !== 3) {
return new Error(`\`${propFullName}\` must be an array of 3 FormModel instances. Validation failed.`);
}
if (!(propValue[key] instanceof FormModel)) {
return new Error(
`Invalid prop \`${propFullName}\` supplied to \
\`${componentName}\`. Validation failed.`
);
}
}),
onSubmit: PropTypes.func.isRequired,
step: PropTypes.oneOf([0, 1, 2]),
code: PropTypes.string
@@ -32,7 +41,11 @@ export default class ChangeEmail extends Component {
static get defaultProps() {
return {
form: new FormModel(),
stepForms: [
new FormModel(),
new FormModel(),
new FormModel()
],
onChangeStep() {},
step: 0
};
@@ -45,18 +58,19 @@ export default class ChangeEmail extends Component {
componentWillReceiveProps(nextProps) {
this.setState({
activeStep: nextProps.step || this.state.activeStep,
activeStep: typeof nextProps.step === 'number' ? nextProps.step : this.state.activeStep,
code: nextProps.code || ''
});
}
render() {
const {form} = this.props;
const {activeStep} = this.state;
const form = this.props.stepForms[activeStep];
return (
<Form onSubmit={this.onFormSubmit}
form={form}
<Form form={form}
onSubmit={this.onFormSubmit}
onInvalid={() => this.forceUpdate()}
>
<div className={styles.contentWithBackButton}>
<Link className={styles.backButton} to="/" />
@@ -95,7 +109,7 @@ export default class ChangeEmail extends Component {
color="violet"
block
label={this.isLastStep() ? messages.changeEmailButton : messages.sendEmailButton}
onClick={this.onSwitchStep}
onClick={this.onSubmit}
/>
</div>
@@ -112,9 +126,9 @@ export default class ChangeEmail extends Component {
}
renderStepForms() {
const {form, email} = this.props;
const {email} = this.props;
const {activeStep, code} = this.state;
const isCodeEntered = !!this.props.code;
const isCodeSpecified = !!this.props.code;
const activeStepHeight = this.state[`step${activeStep}Height`] || 0;
@@ -138,90 +152,25 @@ export default class ChangeEmail extends Component {
WebkitTransform: `translateX(-${interpolatingStyle.transform}%)`,
transform: `translateX(-${interpolatingStyle.transform}%)`
}}>
<MeasureHeight className={changeEmail.stepForm} onMeasure={this.onStepMeasure(0)}>
<div className={styles.formBody}>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.currentAccountEmail} />
</p>
</div>
{(new Array(STEPS_TOTAL)).fill(0).map((_, step) => {
const form = this.props.stepForms[step];
<div className={styles.formRow}>
<h2 className={changeEmail.currentAccountEmail}>
{email}
</h2>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.pressButtonToStart} />
</p>
</div>
</div>
</MeasureHeight>
<MeasureHeight className={changeEmail.stepForm} onMeasure={this.onStepMeasure(1)}>
<div className={styles.formBody}>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.enterInitializationCode} values={{
email: (<b>{email}</b>)
}} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('initializationCode')}
required
disabled={isCodeEntered}
value={code}
onChange={this.onCodeInput}
skin="light"
color="violet"
placeholder={messages.codePlaceholder}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.enterNewEmail} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('newEmail')}
required
skin="light"
color="violet"
placeholder={messages.newEmailPlaceholder}
/>
</div>
</div>
</MeasureHeight>
<MeasureHeight className={changeEmail.stepForm} onMeasure={this.onStepMeasure(2)}>
<div className={styles.formBody}>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.enterFinalizationCode} values={{
email: (<b>{form.value('newEmail')}</b>)
}} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('finalizationCode')}
required
disabled={isCodeEntered}
value={code}
onChange={this.onCodeInput}
skin="light"
color="violet"
placeholder={messages.codePlaceholder}
/>
</div>
</div>
</MeasureHeight>
return (
<MeasureHeight
className={changeEmail.stepForm}
onMeasure={this.onStepMeasure(step)}
state={`${step}.${form.hasErrors()}`}
key={step}
>
{this[`renderStep${step}`]({
email,
code,
form,
isActiveStep: step === activeStep
})}
</MeasureHeight>
);
})}
</div>
</div>
)}
@@ -229,15 +178,114 @@ export default class ChangeEmail extends Component {
);
}
renderStep0({email}) {
return (
<div className={styles.formBody}>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.currentAccountEmail} />
</p>
</div>
<div className={styles.formRow}>
<h2 className={changeEmail.currentAccountEmail}>
{email}
</h2>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.pressButtonToStart} />
</p>
</div>
</div>
);
}
renderStep1({email, form, code, isCodeSpecified, isActiveStep}) {
return (
<div className={styles.formBody}>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.enterInitializationCode} values={{
email: (<b>{email}</b>)
}} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('key')}
required={isActiveStep}
disabled={isCodeSpecified}
value={code}
onChange={this.onCodeInput}
autoComplete="off"
skin="light"
color="violet"
placeholder={messages.codePlaceholder}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.enterNewEmail} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('email')}
required={isActiveStep}
skin="light"
color="violet"
placeholder={messages.newEmailPlaceholder}
/>
</div>
</div>
);
}
renderStep2({form, code, isCodeSpecified, isActiveStep}) {
const newEmail = this.props.stepForms[1].value('email');
return (
<div className={styles.formBody}>
<div className={styles.formRow}>
<p className={styles.description}>
{newEmail ? (
<span>
<Message {...messages.finalizationCodeWasSentToEmail} values={{
email: (<b>{newEmail}</b>)
}} />
{' '}
</span>
) : null}
<Message {...messages.enterFinalizationCode} />
</p>
</div>
<div className={styles.formRow}>
<Input {...form.bindField('key')}
required={isActiveStep}
disabled={isCodeSpecified}
value={code}
onChange={this.onCodeInput}
autoComplete="off"
skin="light"
color="violet"
placeholder={messages.codePlaceholder}
/>
</div>
</div>
);
}
onStepMeasure(step) {
return (height) => this.setState({
[`step${step}Height`]: height
});
}
onSwitchStep = (event) => {
event.preventDefault();
nextStep() {
const {activeStep} = this.state;
const nextStep = activeStep + 1;
@@ -248,6 +296,16 @@ export default class ChangeEmail extends Component {
this.props.onChangeStep(nextStep);
}
}
isLastStep() {
return this.state.activeStep + 1 === STEPS_TOTAL;
}
onSwitchStep = (event) => {
event.preventDefault();
this.nextStep();
};
onCodeInput = (event) => {
@@ -258,11 +316,14 @@ export default class ChangeEmail extends Component {
});
};
isLastStep() {
return this.state.activeStep + 1 === STEPS_TOTAL;
}
onFormSubmit = () => {
this.props.onSubmit(this.props.form);
const {activeStep} = this.state;
const promise = this.props.onSubmit(activeStep, this.props.stepForms[activeStep]);
if (!promise || !promise.then) {
throw new Error('Expecting promise from onSubmit');
}
promise.then(() => this.nextStep(), () => this.forceUpdate());
};
}

View File

@@ -1,66 +0,0 @@
import { defineMessages } from 'react-intl';
export default defineMessages({
changeEmailTitle: {
id: 'changeEmailTitle',
defaultMessage: 'Change E-mail'
// defaultMessage: 'Смена E-mail'
},
changeEmailDescription: {
id: 'changeEmailDescription',
defaultMessage: 'To change current account E-mail you must first verify that you own the current address and then confirm the new one.'
// defaultMessage: 'Для смены E-mail адреса аккаунта сперва необходимо подтвердить владение текущим адресом, а за тем привязать новый.'
},
currentAccountEmail: {
id: 'currentAccountEmail',
defaultMessage: 'Current account E-mail address:'
// defaultMessage: 'Текущий E-mail адрес, привязанный к аккаунту:'
},
pressButtonToStart: {
id: 'pressButtonToStart',
defaultMessage: 'Press the button below to send a message with the code for E-mail change initialization.'
// defaultMessage: 'Нажмите кнопку ниже, что бы отправить письмо с кодом для инциализации процесса смены E-mail адреса.'
},
enterInitializationCode: {
id: 'enterInitializationCode',
defaultMessage: 'The E-mail with an initialization code for E-mail change procedure was sent to {email}. Please enter the code into the field below:'
// defaultMessage: 'На E-mail {email} было отправлено письмо с кодом для инициализации смены E-mail адреса. Введите его в поле ниже:'
},
//
enterNewEmail: {
id: 'enterNewEmail',
defaultMessage: 'Then provide your new E-mail address, that you want to use with this account. You will be mailed with confirmation code.'
// defaultMessage: 'За тем укажите новый E-mail адрес, к котором хотите привязать свой аккаунт. На него будет выслан код с подтверждением.'
},
enterFinalizationCode: {
id: 'enterFinalizationCode',
defaultMessage: 'The E-mail change confirmation code was sent to {email}. Please enter the code received into the field below:'
// defaultMessage: 'На указанный E-mail {email} было выслано письмо с кодом для завершщения смены E-mail адреса. Введите полученный код в поле ниже:'
},
//
newEmailPlaceholder: {
id: 'newEmailPlaceholder',
defaultMessage: 'Enter new E-mail'
// defaultMessage: 'Введите новый E-mail'
},
codePlaceholder: {
id: 'codePlaceholder',
defaultMessage: 'Paste the code here'
// defaultMessage: 'Вставьте код сюда'
},
sendEmailButton: {
id: 'sendEmailButton',
defaultMessage: 'Send E-mail'
// defaultMessage: 'Отправить E-mail'
},
changeEmailButton: {
id: 'changeEmailButton',
defaultMessage: 'Change E-mail'
// defaultMessage: 'Сменить E-mail'
},
alreadyReceivedCode: {
id: 'alreadyReceivedCode',
defaultMessage: 'Already received code'
// defaultMessage: 'Я получил код'
}
});

View File

@@ -0,0 +1,6 @@
{
"changeUsernameTitle": "Change nickname",
"changeUsernameDescription": "You can change your nickname to any arbitrary value. Remember that it is not recommended to take a nickname of already existing Mojang account.",
"changeUsernameWarning": "Be careful: if you playing on the server with nickname binding, then after changing nickname you may lose all your progress.",
"changeUsernameButton": "Change nickname"
}

View File

@@ -7,7 +7,7 @@ import Helmet from 'react-helmet';
import { Input, Button, Form, FormModel } from 'components/ui/form';
import styles from 'components/profile/profileForm.scss';
import messages from './ChangeUsername.messages';
import messages from './ChangeUsername.intl.json';
export default class ChangeUsername extends Component {
static displayName = 'ChangeUsername';

View File

@@ -1,24 +0,0 @@
import { defineMessages } from 'react-intl';
export default defineMessages({
changeUsernameTitle: {
id: 'changeUsernameTitle',
defaultMessage: 'Change nickname'
// defaultMessage: 'Смена никнейма'
},
changeUsernameDescription: {
id: 'changeUsernameDescription',
defaultMessage: 'You can change your nickname to any arbitrary value. Remember that it is not recommended to take a nickname of already existing Mojang account.'
// defaultMessage: 'Вы можете сменить свой никнейм на любое допустимое значение. Помните о том, что не рекомендуется занимать никнеймы пользователей Mojang.'
},
changeUsernameWarning: {
id: 'changeUsernameWarning',
defaultMessage: 'Be careful: if you playing on the server with nickname binding, then after changing nickname you may lose all your progress.'
// defaultMessage: 'Будьте внимательны: если вы играли на сервере с привязкой по нику, то после смены ника вы можете утратить весь свой прогресс.'
},
changeUsernameButton: {
id: 'changeUsernameButton',
defaultMessage: 'Change nickname'
// defaultMessage: 'Сменить никнейм'
}
});

View File

@@ -0,0 +1,4 @@
{
"pleaseEnterPassword": "Please, enter your current password",
"title": "Confirm your action"
}

View File

@@ -4,7 +4,7 @@ import { FormattedMessage as Message } from 'react-intl';
import { Form, Button, Input, FormModel } from 'components/ui/form';
import messages from './PasswordRequestForm.messages';
import messages from './PasswordRequestForm.intl.json';
export default class PasswordRequestForm extends Component {
static displayName = 'PasswordRequestForm';

View File

@@ -1,12 +0,0 @@
import { defineMessages } from 'react-intl';
export default defineMessages({
pleaseEnterPassword: {
id: 'pleaseEnterPassword',
defaultMessage: 'Please, enter your current password'
},
title: {
id: 'title',
defaultMessage: 'Confirm your action'
}
});

View File

@@ -72,6 +72,10 @@ export default class FormModel {
return this.errors[fieldId] || null;
}
hasErrors() {
return Object.keys(this.errors).length > 0;
}
serialize() {
return Object.keys(this.fields).reduce((acc, fieldId) => {
acc[fieldId] = this.fields[fieldId].getValue();

View File

@@ -140,6 +140,17 @@
color: $red;
font-size: 12px;
margin: 3px 0;
a {
border-bottom: 1px dotted rgba($red, 0.75);
text-decoration: none;
transition: .25s;
color: $red;
&:hover {
border-bottom-color: transparent;
}
}
}
@include input-theme('green', $green);