Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change email dialog #22309

Merged
merged 29 commits into from
May 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d9d55da
Nonfunctional dialog layout with story
islemaster May 9, 2018
5728937
Attach ChangeEmailModal to account page
islemaster May 9, 2018
64c8ee9
Interactable fields and client-side validation
islemaster May 9, 2018
7598a61
Able to successfully update email address
islemaster May 10, 2018
b6cb184
Better documentation of the Rails UJS scheme
islemaster May 10, 2018
c25c79f
Remove copypasta'd comment
islemaster May 10, 2018
907957b
Server sends back validation errors, client handles them gracefully
islemaster May 10, 2018
249f179
Proper JSON response and handling of unknown error
islemaster May 11, 2018
0b80087
Disable when saving
islemaster May 11, 2018
a7da285
Submit on enter key
islemaster May 11, 2018
4aeb229
Not changing email is a validation error
islemaster May 11, 2018
a73dfe9
Break out header and footer components
islemaster May 11, 2018
f5dbf38
Update email on the account page after successful submission
islemaster May 11, 2018
2a9db7f
Focus email input when dialog opens
islemaster May 11, 2018
8c28d54
Fix id collision
islemaster May 11, 2018
a16420b
Use 'namespace' feature
islemaster May 11, 2018
e31b996
Remove old email update code
islemaster May 11, 2018
865465e
Formatting cleanup
islemaster May 11, 2018
a97ab50
Extract ChangeEmailForm
islemaster May 11, 2018
ed93558
Change state format
islemaster May 11, 2018
f434008
Tests for ChangeEmailForm
islemaster May 11, 2018
2f7b454
ChangeEmailModalTest
islemaster May 12, 2018
fb28643
Test success and failure handlers
islemaster May 12, 2018
4d146da
Deduplicate hashing logic
islemaster May 12, 2018
3fbadeb
Fix test name
islemaster May 12, 2018
29ed7e6
Fix ChangeEmailModal story
islemaster May 14, 2018
1e1b16c
Extract and test can_change_own_user_type?
islemaster May 14, 2018
df13710
Temporary constraint on changing user type
islemaster May 14, 2018
dd54d6f
Remove UI test no longer relevant under new spec
islemaster May 14, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/i18n/common/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@
"challengeLevelSkip": "Skip for now",
"challengeLevelStart": "I'm ready!",
"challengeLevelTitle": "Challenge Puzzle!",
"changeEmailModal_currentPassword_isRequired": "Current password is required.",
"changeEmailModal_currentPassword_label": "Current password",
"changeEmailModal_emailOptIn_description": "Would you like to be added to our mailing list so that we can tell you about updates to our courses, local learning opportunities, or other computer science news? This will not impact your subscription preferences for your old email address.",
"changeEmailModal_emailOptIn_isRequired": "This field is required.",
"changeEmailModal_newEmail_invalid": "The email address you provided is not valid.",
"changeEmailModal_newEmail_isRequired": "A new email address is required.",
"changeEmailmodal_newEmail_mustBeDifferent": "New email address must not match old email address.",
"changeEmailModal_newEmail_label": "New email address",
"changeEmailModal_save": "Update email address",
"changeEmailModal_title": "Update email address",
"changeEmailModal_unexpectedError": "An unexpected error has occurred. Please wait a moment and try again.",
"changeLoginType": "Change login type",
"changeLoginTypeQuestion": "Change student login type?",
"changeLoginTypeToPicture_button": "Change to picture login",
Expand Down
18 changes: 13 additions & 5 deletions apps/src/code-studio/hashEmail.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import $ from 'jquery';
import MD5 from 'crypto-js/md5';

var EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
const EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;

module.exports = function hashEmail(options) {
export default function (options) {
// Hash the email, if it is an email.
var email = $(options.email_selector).val().toLowerCase().trim();
const email = normalizeEmail($(options.email_selector).val());
if (email !== '' && EMAIL_REGEX.test(email)) {
var hashed_email = MD5(email);
const hashed_email = hashEmail(email);
$(options.hashed_email_selector).val(hashed_email);

// Unless we want to deliberately skip the step of clearing the email.
Expand All @@ -18,4 +18,12 @@ module.exports = function hashEmail(options) {
}
}
}
};
}

export function hashEmail(cleartextEmail) {
return MD5(normalizeEmail(cleartextEmail)).toString();
}

function normalizeEmail(rawEmail) {
return rawEmail.toLowerCase().trim();
}
174 changes: 174 additions & 0 deletions apps/src/lib/ui/ChangeEmailForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React, {PropTypes} from 'react';
import color from '@cdo/apps/util/color';
import i18n from '@cdo/locale';

export default class ChangeEmailForm extends React.Component {
static propTypes = {
values: PropTypes.shape({
newEmail: PropTypes.string,
currentPassword: PropTypes.string,
emailOptIn: PropTypes.string,
}).isRequired,
validationErrors: PropTypes.shape({
newEmail: PropTypes.string,
currentPassword: PropTypes.string,
emailOptIn: PropTypes.string,
}).isRequired,
disabled: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
};

componentDidMount() {
this.newEmailInput.focus();
}

focusOnAnError() {
const {validationErrors} = this.props;
if (validationErrors.newEmail) {
this.newEmailInput.focus();
} else if (validationErrors.currentPassword) {
this.currentPasswordInput.focus();
}
}

onNewEmailChange = (event) => this.props.onChange({
...this.props.values,
newEmail: event.target.value,
});

onCurrentPasswordChange = (event) => this.props.onChange({
...this.props.values,
currentPassword: event.target.value,
});

onEmailOptInChange = (event) => this.props.onChange({
...this.props.values,
emailOptIn: event.target.value,
});

onKeyDown = (event) => {
if (event.key === 'Enter' && !this.props.disabled) {
this.props.onSubmit();
}
};

render() {
const {values, validationErrors, disabled} = this.props;
return (
<div>
<Field>
<label
htmlFor="user_email"
style={styles.label}
>
{i18n.changeEmailModal_newEmail_label()}
</label>
<input
id="user_email"
type="email"
value={values.newEmail}
disabled={disabled}
onKeyDown={this.onKeyDown}
onChange={this.onNewEmailChange}
autoComplete="off"
maxLength="255"
size="255"
style={styles.input}
ref={el => this.newEmailInput = el}
/>
<FieldError>
{validationErrors.newEmail}
</FieldError>
</Field>
<Field>
<label
htmlFor="user_current_password"
style={styles.label}
>
{i18n.changeEmailModal_currentPassword_label()}
</label>
<input
id="user_current_password"
type="password"
value={values.currentPassword}
disabled={disabled}
onKeyDown={this.onKeyDown}
onChange={this.onCurrentPasswordChange}
maxLength="255"
size="255"
style={styles.input}
ref={el => this.currentPasswordInput = el}
/>
<FieldError>
{validationErrors.currentPassword}
</FieldError>
</Field>
<Field style={{display: 'none'}}>
<p>
{i18n.changeEmailModal_emailOptIn_description()}
</p>
<select
value={values.emailOptIn}
onKeyDown={this.onKeyDown}
onChange={this.onEmailOptInChange}
disabled={disabled}
style={{
...styles.input,
width: 100,
}}
>
<option value=""/>
<option value="yes">
{i18n.yes()}
</option>
<option value="no">
{i18n.no()}
</option>
</select>
<FieldError>
{validationErrors.emailOptIn}
</FieldError>
</Field>
</div>
);
}
}

const Field = ({children, style}) => (
<div
style={{
marginBottom: 15,
...style,
}}
>
{children}
</div>
);
Field.propTypes = {
children: PropTypes.any,
style: PropTypes.object,
};

const FieldError = ({children}) => (
<div
style={{
color: color.red,
fontStyle: 'italic',
}}
>
{children}
</div>
);
FieldError.propTypes = {children: PropTypes.string};

const styles = {
label: {
display: 'block',
fontWeight: 'bold',
color: color.charcoal,
},
input: {
marginBottom: 4,
},
};
66 changes: 66 additions & 0 deletions apps/src/lib/ui/ChangeEmailForm.story.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import ChangeEmailForm from './ChangeEmailForm';
import {action} from '@storybook/addon-actions';

const DEFAULT_PROPS = {
values: {
newEmail: '',
currentPassword: '',
emailOptIn: '',
},
validationErrors: {
newEmail: undefined,
currentPassword: undefined,
emailOptIn: undefined,
},
disabled: false,
onChange: action('onChange'),
onSubmit: action('onSubmit')
};

export default storybook => storybook
.storiesOf('ChangeEmailForm', module)
.addStoryTable([
{
name: 'with valid content',
story: () => (
<ChangeEmailForm
{...DEFAULT_PROPS}
values={{
newEmail: 'batman@bat.cave',
currentPassword: 'imsorich',
}}
/>
)
},
{
name: 'with validation errors',
story: () => (
<ChangeEmailForm
{...DEFAULT_PROPS}
values={{
newEmail: 'robin@bat.cave',
currentPassword: 'no1fan',
}}
validationErrors={{
newEmail: "Robin, get out of here!",
currentPassword: "That's totally the wrong password."
}}
/>
)
},
{
name: 'disabled',
story: () => (
<ChangeEmailForm
{...DEFAULT_PROPS}
values={{
newEmail: 'currently-saving@bat.cave',
currentPassword: 'currently-saving',
}}
disabled={true}
/>
)
}
]);