feat(emails): Add support for change email (#5242) r=shane-tomlinson,vladikoff
This commit is contained in:
Родитель
413d65c9a8
Коммит
39bb7715c0
|
@ -268,6 +268,10 @@ define(function (require, exports, module) {
|
|||
signInOptions.skipCaseError = options.skipCaseError;
|
||||
}
|
||||
|
||||
if (options.originalLoginEmail) {
|
||||
signInOptions.originalLoginEmail = options.originalLoginEmail;
|
||||
}
|
||||
|
||||
setMetricsContext(signInOptions, options);
|
||||
|
||||
return client.signIn(email, password, signInOptions)
|
||||
|
@ -472,6 +476,9 @@ define(function (require, exports, module) {
|
|||
* @param {String} code
|
||||
* @param {Object} relier
|
||||
* @param {Object} [options={}]
|
||||
* @param {String} [options.emailToHashWith]
|
||||
* If specified, the password is hashed with this email address, otherwise
|
||||
* the user's password is hashed with their current email.
|
||||
* @return {Promise} resolves when complete
|
||||
*/
|
||||
completePasswordReset: withClient((client, originalEmail, newPassword, token, code, relier, options = {}) => {
|
||||
|
@ -488,7 +495,18 @@ define(function (require, exports, module) {
|
|||
|
||||
return client.passwordForgotVerifyCode(code, token, passwordVerifyCodeOptions)
|
||||
.then(result => {
|
||||
return client.accountReset(email,
|
||||
let emailToHashWith = email;
|
||||
|
||||
// The `emailToHashWith` option is returned by the auth-server to let the content-server
|
||||
// know what to hash the new password with. This is important in the scenario where a user
|
||||
// has changed their primary email address. In this case, they must still hash with the
|
||||
// account's original email because this will maintain backwards compatibility with
|
||||
// how account password hashing works previously.
|
||||
if (options.emailToHashWith) {
|
||||
emailToHashWith = trim(options.emailToHashWith);
|
||||
}
|
||||
|
||||
return client.accountReset(emailToHashWith,
|
||||
newPassword,
|
||||
result.accountResetToken,
|
||||
accountResetOptions
|
||||
|
@ -844,7 +862,16 @@ define(function (require, exports, module) {
|
|||
* @param {String} sessionToken User session token
|
||||
* @return {Promise} resolves when complete
|
||||
*/
|
||||
recoveryEmailSecondaryEmailEnabled: createClientDelegate('recoveryEmailSecondaryEmailEnabled')
|
||||
recoveryEmailSecondaryEmailEnabled: createClientDelegate('recoveryEmailSecondaryEmailEnabled'),
|
||||
|
||||
/**
|
||||
* Set the new primary email address for a user.
|
||||
*
|
||||
* @param {String} sessionToken User session token
|
||||
* @param {String} email The new primary email address
|
||||
* @return {Promise} resolves when complete
|
||||
*/
|
||||
recoveryEmailSetPrimaryEmail: createClientDelegate('recoveryEmailSetPrimaryEmail')
|
||||
};
|
||||
|
||||
module.exports = FxaClientWrapper;
|
||||
|
|
|
@ -462,15 +462,15 @@ define(function (require, exports, module) {
|
|||
* @param {String} [options.resume] - Resume token to send
|
||||
* in verification email if user is unverified.
|
||||
* @param {String} [options.unblockCode] - Unblock code.
|
||||
* @param {String} [options.originalLoginEmail] - Login used to login with originally.
|
||||
* @returns {Promise} - resolves when complete
|
||||
*/
|
||||
signIn (password, relier, options = {}) {
|
||||
var email = this.get('email');
|
||||
return p().then(() => {
|
||||
var email = this.get('email');
|
||||
var sessionToken = this.get('sessionToken');
|
||||
|
||||
if (password) {
|
||||
return this._fxaClient.signIn(email, password, relier, {
|
||||
const signinOptions = {
|
||||
metricsContext: this._metrics.getFlowEventMetadata(),
|
||||
reason: options.reason || SignInReasons.SIGN_IN,
|
||||
resume: options.resume,
|
||||
|
@ -478,7 +478,16 @@ define(function (require, exports, module) {
|
|||
// can be updated with the correct case.
|
||||
skipCaseError: true,
|
||||
unblockCode: options.unblockCode
|
||||
});
|
||||
};
|
||||
|
||||
// `originalLoginEmail` is specified when the account's primary email has changed.
|
||||
// This param lets the auth-server known that it should check that this email
|
||||
// is the current primary for the account.
|
||||
if (options.originalLoginEmail) {
|
||||
signinOptions.originalLoginEmail = options.originalLoginEmail;
|
||||
}
|
||||
|
||||
return this._fxaClient.signIn(email, password, relier, signinOptions);
|
||||
} else if (sessionToken) {
|
||||
// We have a cached Sync session so just check that it hasn't expired.
|
||||
// The result includes the latest verified state
|
||||
|
@ -488,11 +497,27 @@ define(function (require, exports, module) {
|
|||
}
|
||||
})
|
||||
.then((updatedSessionData) => {
|
||||
// If a different email case or primary email was used to login,
|
||||
// the session won't have correct email. Update the session to use the one
|
||||
// originally used for login.
|
||||
if (options.originalLoginEmail && email.toLowerCase() !== options.originalLoginEmail.toLowerCase()) {
|
||||
updatedSessionData.email = options.originalLoginEmail;
|
||||
}
|
||||
|
||||
this.set(updatedSessionData);
|
||||
return updatedSessionData;
|
||||
})
|
||||
.fail((err) => {
|
||||
// The `INCORRECT_EMAIL_CASE` can be returned if a user is attempting to login with a different
|
||||
// email case than what the account was created with or if they changed their primary email address.
|
||||
// In both scenarios, the content-server needs to know the original account email to hash
|
||||
// the user's password with.
|
||||
if (AuthErrors.is(err, 'INCORRECT_EMAIL_CASE')) {
|
||||
|
||||
// Save the original email that was used for login so that the auth-server
|
||||
// can verify that this is the accounts primary email address.
|
||||
options.originalLoginEmail = email;
|
||||
|
||||
// The server will respond with the canonical email
|
||||
// for this account. Use it hereafter.
|
||||
this.set('email', err.email);
|
||||
|
@ -1202,7 +1227,7 @@ define(function (require, exports, module) {
|
|||
},
|
||||
|
||||
/**
|
||||
* Associates a new email to a users account.
|
||||
* Deletes an email from a users account.
|
||||
*
|
||||
* @param {String} email
|
||||
*
|
||||
|
@ -1213,6 +1238,20 @@ define(function (require, exports, module) {
|
|||
this.get('sessionToken'),
|
||||
email
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the primary email address of the user.
|
||||
*
|
||||
* @param {String} email
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
setPrimaryEmail (email) {
|
||||
return this._fxaClient.recoveryEmailSetPrimaryEmail(
|
||||
this.get('sessionToken'),
|
||||
email
|
||||
);
|
||||
}
|
||||
}, {
|
||||
ALLOWED_KEYS: ALLOWED_KEYS,
|
||||
|
|
|
@ -22,6 +22,7 @@ define(function (require, exports, module) {
|
|||
validation: {
|
||||
code: Vat.verificationCode().required(),
|
||||
email: Vat.email().required(),
|
||||
emailToHashWith: Vat.email().optional(),
|
||||
token: Vat.token().required()
|
||||
}
|
||||
});
|
||||
|
|
|
@ -28,13 +28,38 @@
|
|||
<div class="not-verified">{{#t}}verification required{{/t}}</div>
|
||||
{{/verified}}
|
||||
</div>
|
||||
<button class="settings-button warning email-disconnect" data-id="{{email}}">
|
||||
{{#t}}Remove{{/t}}
|
||||
</button>
|
||||
<div class="settings-button-group">
|
||||
{{#verified}}
|
||||
{{#canChangePrimaryEmail }}
|
||||
<button class="settings-button secondary set-primary" data-id="{{email}}">
|
||||
{{#t}}Make primary{{/t}}
|
||||
</button>
|
||||
{{/canChangePrimaryEmail }}
|
||||
{{/verified}}
|
||||
<button class="settings-button warning email-disconnect" data-id="{{email}}">
|
||||
{{#t}}Remove{{/t}}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="email-options">
|
||||
<div class="settings-button-group">
|
||||
{{#verified}}
|
||||
{{#canChangePrimaryEmail }}
|
||||
<button class="settings-button secondary set-primary" data-id="{{email}}">
|
||||
{{#t}}Make primary{{/t}}
|
||||
</button>
|
||||
{{/canChangePrimaryEmail }}
|
||||
{{/verified}}
|
||||
<button class="settings-button warning email-disconnect unpaired" data-id="{{email}}">
|
||||
{{#t}}Remove{{/t}}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{^verified}}
|
||||
<a class="resend" data-id="{{email}}">{{#t}}Not in inbox or spam folder? Resend?{{/t}}</a>
|
||||
{{/verified}}
|
||||
|
||||
{{/isPrimary}}
|
||||
{{/emails}}
|
||||
</ul>
|
||||
|
|
|
@ -183,7 +183,14 @@ define(function (require, exports, module) {
|
|||
var account = this.getSignedInAccount();
|
||||
account.set('displayName', displayName);
|
||||
return this.user.setAccount(account)
|
||||
.then(_.bind(this._notifyProfileUpdate, this, account.get('uid')));
|
||||
.then(() => this._notifyProfileUpdate(account.get('uid')));
|
||||
},
|
||||
|
||||
updateDisplayEmail (email) {
|
||||
var account = this.getSignedInAccount();
|
||||
account.set('email', email);
|
||||
return this.user.setAccount(account)
|
||||
.then(() => this._notifyProfileUpdate(account.get('uid')));
|
||||
},
|
||||
|
||||
_notifyProfileUpdate (uid) {
|
||||
|
|
|
@ -6,6 +6,7 @@ define(function (require, exports, module) {
|
|||
'use strict';
|
||||
|
||||
const $ = require('jquery');
|
||||
const AvatarMixin = require('views/mixins/avatar-mixin');
|
||||
const BaseView = require('views/base');
|
||||
const Cocktail = require('cocktail');
|
||||
const Email = require('models/email');
|
||||
|
@ -13,6 +14,8 @@ define(function (require, exports, module) {
|
|||
const FormView = require('views/form');
|
||||
const preventDefaultThen = require('views/base').preventDefaultThen;
|
||||
const SettingsPanelMixin = require('views/mixins/settings-panel-mixin');
|
||||
const SearchParamMixin = require('lib/search-param-mixin');
|
||||
const Strings = require('lib/strings');
|
||||
const showProgressIndicator = require('views/decorators/progress_indicator');
|
||||
const Template = require('stache!templates/settings/emails');
|
||||
|
||||
|
@ -30,7 +33,8 @@ define(function (require, exports, module) {
|
|||
events: {
|
||||
'click .email-disconnect': preventDefaultThen('_onDisconnectEmail'),
|
||||
'click .email-refresh.enabled': preventDefaultThen('refresh'),
|
||||
'click .resend': preventDefaultThen('resend')
|
||||
'click .resend': preventDefaultThen('resend'),
|
||||
'click .set-primary': preventDefaultThen('setPrimary')
|
||||
},
|
||||
|
||||
initialize (options) {
|
||||
|
@ -44,6 +48,7 @@ define(function (require, exports, module) {
|
|||
setInitialContext (context) {
|
||||
context.set({
|
||||
buttonClass: this._hasSecondaryEmail() ? 'secondary' : 'primary',
|
||||
canChangePrimaryEmail: this._canChangePrimaryEmail(),
|
||||
emails: this._emails,
|
||||
hasSecondaryEmail: this._hasSecondaryEmail(),
|
||||
hasSecondaryVerifiedEmail: this._hasSecondaryVerifiedEmail(),
|
||||
|
@ -64,6 +69,14 @@ define(function (require, exports, module) {
|
|||
}
|
||||
},
|
||||
|
||||
_canChangePrimaryEmail () {
|
||||
if (this.getSearchParam('canChangeEmail')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_isSecondaryEmailEnabled () {
|
||||
// Only show secondary email panel if the user is in a verified session and feature is enabled.
|
||||
const account = this.getSignedInAccount();
|
||||
|
@ -120,7 +133,7 @@ define(function (require, exports, module) {
|
|||
resend (event) {
|
||||
const email = $(event.currentTarget).data('id');
|
||||
const account = this.getSignedInAccount();
|
||||
return account.resendEmailCode(email)
|
||||
return account.resendEmailCode({ email })
|
||||
.then(() => {
|
||||
this.displaySuccess(t('Verification email sent'), {
|
||||
closePanel: false
|
||||
|
@ -143,12 +156,28 @@ define(function (require, exports, module) {
|
|||
.fail((err) => this.showValidationError(this.$(EMAIL_INPUT_SELECTOR), err));
|
||||
}
|
||||
},
|
||||
|
||||
setPrimary (event) {
|
||||
const email = $(event.currentTarget).data('id');
|
||||
const account = this.getSignedInAccount();
|
||||
return account.setPrimaryEmail(email)
|
||||
.then(() => {
|
||||
this.updateDisplayEmail(email);
|
||||
this.displaySuccess(Strings.interpolate(t('Primary email set to %(email)s'), { email }), {
|
||||
closePanel: false
|
||||
});
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Cocktail.mixin(
|
||||
View,
|
||||
AvatarMixin,
|
||||
SettingsPanelMixin,
|
||||
FloatingPlaceholderMixin
|
||||
FloatingPlaceholderMixin,
|
||||
SearchParamMixin
|
||||
);
|
||||
|
||||
module.exports = View;
|
||||
|
|
|
@ -655,67 +655,121 @@ section.modal-panel {
|
|||
}
|
||||
|
||||
.email-list {
|
||||
padding: 0;
|
||||
}
|
||||
padding: 0;
|
||||
|
||||
.email-address {
|
||||
.email-address {
|
||||
height: 40px;
|
||||
list-style: none;
|
||||
margin: 10px 0;
|
||||
position: relative;
|
||||
|
||||
html[dir='ltr'] & {
|
||||
background-position: left 2px;
|
||||
background-position: left 2px;
|
||||
}
|
||||
|
||||
html[dir='rtl'] & {
|
||||
background-position: right 2px;
|
||||
background-position: right 2px;
|
||||
}
|
||||
|
||||
.address {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: calc(95% - 95px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: calc(95% - 95px);
|
||||
}
|
||||
|
||||
.details {
|
||||
color: $color-grey;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: calc(95% - 95px);
|
||||
color: $color-grey;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: calc(95% - 95px);
|
||||
|
||||
& .not-verified {
|
||||
color: $color-red;
|
||||
}
|
||||
& .not-verified {
|
||||
color: $color-red;
|
||||
}
|
||||
|
||||
& .verified {
|
||||
color: $color-green;
|
||||
}
|
||||
& .verified {
|
||||
color: $color-green;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
@media screen and (max-width: 400px) {
|
||||
.settings-button-group {
|
||||
/*!important is used here because on small screens these options are displayed*/
|
||||
/*in another area of the panel. They are to the right of the email.*/
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-button-group {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
html[dir='ltr'] & {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
html[dir='rtl'] & {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
height: 35px;
|
||||
margin-left: 10px;
|
||||
/*minimum width required for the button to look good without occupying too much space*/
|
||||
/*is also the default computed width on desktop screen*/
|
||||
min-width: 100px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[dir='ltr'] & {
|
||||
right: 0;
|
||||
}
|
||||
@media screen and (min-width: 400px) {
|
||||
.email-options {
|
||||
/*!important is used here because on large screens these options are not displayed.*/
|
||||
/*They sit below the email address.*/
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
html[dir='rtl'] & {
|
||||
left: 0;
|
||||
}
|
||||
.email-options {
|
||||
height: 40px;
|
||||
list-style: none;
|
||||
margin: 10px 0;
|
||||
position: relative;
|
||||
|
||||
html[dir='ltr'] & {
|
||||
background-position: left 2px;
|
||||
}
|
||||
|
||||
a.settings-button {
|
||||
padding-top: 8px;
|
||||
html[dir='rtl'] & {
|
||||
background-position: right 2px;
|
||||
}
|
||||
|
||||
.settings-button-group {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
html[dir='ltr'] & {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
html[dir='rtl'] & {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
height: 35px;
|
||||
margin-left: 10px;
|
||||
/*minimum width required for the button to look good without occupying too much space*/
|
||||
/*is also the default computed width on desktop screen*/
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -488,7 +488,7 @@ define(function (require, exports, module) {
|
|||
});
|
||||
|
||||
it('re-tries with the normalized email, updates model with normalized email', function () {
|
||||
const expectedOptions = {
|
||||
const firstExpectedOptions = {
|
||||
metricsContext: {
|
||||
baz: 'qux',
|
||||
foo: 'bar'
|
||||
|
@ -499,11 +499,23 @@ define(function (require, exports, module) {
|
|||
unblockCode: 'unblock code'
|
||||
};
|
||||
|
||||
const secondExpectedOptions = {
|
||||
metricsContext: {
|
||||
baz: 'qux',
|
||||
foo: 'bar'
|
||||
},
|
||||
originalLoginEmail: upperCaseEmail,
|
||||
reason: SignInReasons.SIGN_IN,
|
||||
resume: 'resume token',
|
||||
skipCaseError: true,
|
||||
unblockCode: 'unblock code'
|
||||
};
|
||||
|
||||
assert.equal(fxaClient.signIn.callCount, 2);
|
||||
assert.isTrue(
|
||||
fxaClient.signIn.calledWith(upperCaseEmail, PASSWORD, relier, expectedOptions));
|
||||
fxaClient.signIn.calledWith(upperCaseEmail, PASSWORD, relier, firstExpectedOptions));
|
||||
assert.isTrue(
|
||||
fxaClient.signIn.calledWith(EMAIL, PASSWORD, relier, expectedOptions));
|
||||
fxaClient.signIn.calledWith(EMAIL, PASSWORD, relier, secondExpectedOptions));
|
||||
|
||||
assert.equal(account.get('email'), EMAIL);
|
||||
});
|
||||
|
|
|
@ -78,6 +78,17 @@ define(function (require, exports, module) {
|
|||
assert.isFalse(model.isValid());
|
||||
});
|
||||
|
||||
it('returns false if emailToHasWith is invalid', function () {
|
||||
var model = new Model({
|
||||
code: validCode,
|
||||
email: validEmail,
|
||||
emailToHashWith: invalidEmail,
|
||||
token: validToken
|
||||
});
|
||||
|
||||
assert.isFalse(model.isValid());
|
||||
});
|
||||
|
||||
it('returns true otherwise', function () {
|
||||
var model = new Model({
|
||||
code: validCode,
|
||||
|
|
|
@ -57,6 +57,7 @@ define(function (require, exports, module) {
|
|||
translator = new Translator({forceEnglish: true});
|
||||
user = new User();
|
||||
windowMock = new WindowMock();
|
||||
windowMock.location.search = '?canChangeEmail=true';
|
||||
|
||||
account = user.initAccount({
|
||||
email: email,
|
||||
|
@ -148,6 +149,11 @@ define(function (require, exports, module) {
|
|||
sinon.stub(account, 'resendEmailCode', () => {
|
||||
return p();
|
||||
});
|
||||
|
||||
sinon.stub(account, 'setPrimaryEmail', (newEmail) => {
|
||||
email = newEmail;
|
||||
return p();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no secondary email', () => {
|
||||
|
@ -197,21 +203,24 @@ define(function (require, exports, module) {
|
|||
|
||||
it('can render', () => {
|
||||
assert.equal(view.$('.email-address').length, 1);
|
||||
assert.equal(view.$('.email-address .address').length, 1);
|
||||
assert.equal(view.$('.email-address .address')[0].innerHTML, 'another@one.com');
|
||||
assert.lengthOf(view.$('.email-address .address'), 1);
|
||||
assert.equal(view.$('.email-address .address').html(), 'another@one.com');
|
||||
assert.equal(view.$('.email-address .details .not-verified').length, 1);
|
||||
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').length, 1);
|
||||
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').attr('data-id'), 'another@one.com');
|
||||
assert.equal(view.$('.email-address .settings-button.secondary.set-primary').length, 0);
|
||||
|
||||
});
|
||||
|
||||
it('can disconnect email and navigate to /emails', (done) => {
|
||||
$('.email-address .settings-button.warning.email-disconnect').click();
|
||||
setTimeout(function () {
|
||||
assert.isTrue(view.navigate.calledOnce);
|
||||
const args = view.navigate.args[0];
|
||||
assert.equal(args.length, 1);
|
||||
assert.equal(args[0], '/settings/emails');
|
||||
done();
|
||||
TestHelpers.wrapAssertion(() => {
|
||||
assert.isTrue(view.navigate.calledOnce);
|
||||
const args = view.navigate.args[0];
|
||||
assert.equal(args.length, 1);
|
||||
assert.equal(args[0], '/settings/emails');
|
||||
}, done);
|
||||
}, 150);
|
||||
});
|
||||
|
||||
|
@ -219,8 +228,9 @@ define(function (require, exports, module) {
|
|||
$('.email-refresh').click();
|
||||
sinon.spy(view, 'render');
|
||||
setTimeout(function () {
|
||||
assert.isTrue(view.render.calledOnce);
|
||||
done();
|
||||
TestHelpers.wrapAssertion(() => {
|
||||
assert.isTrue(view.render.calledOnce);
|
||||
}, done);
|
||||
}, 450); // Delay is higher here because refresh has a min delay of 350
|
||||
});
|
||||
|
||||
|
@ -228,11 +238,12 @@ define(function (require, exports, module) {
|
|||
$('.resend').click();
|
||||
sinon.spy(view, 'render');
|
||||
setTimeout(function () {
|
||||
assert.isTrue(view.navigate.calledOnce);
|
||||
const args = view.navigate.args[0];
|
||||
assert.equal(args.length, 1);
|
||||
assert.equal(args[0], '/settings/emails');
|
||||
done();
|
||||
TestHelpers.wrapAssertion(() => {
|
||||
assert.isTrue(view.navigate.calledOnce);
|
||||
const args = view.navigate.args[0];
|
||||
assert.equal(args.length, 1);
|
||||
assert.equal(args[0], '/settings/emails');
|
||||
}, done);
|
||||
}, 150);
|
||||
});
|
||||
|
||||
|
@ -263,8 +274,8 @@ define(function (require, exports, module) {
|
|||
|
||||
it('can render', () => {
|
||||
assert.equal(view.$('.email-address').length, 1);
|
||||
assert.equal(view.$('.email-address .address').length, 1);
|
||||
assert.equal(view.$('.email-address .address')[0].innerHTML, 'another@one.com');
|
||||
assert.lengthOf(view.$('.email-address .address'), 1);
|
||||
assert.equal(view.$('.email-address .address').html(), 'another@one.com');
|
||||
assert.equal(view.$('.email-address .details .verified').length, 1);
|
||||
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').length, 1);
|
||||
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').attr('data-id'), 'another@one.com');
|
||||
|
@ -273,11 +284,12 @@ define(function (require, exports, module) {
|
|||
it('can disconnect email and navigate to /emails', (done) => {
|
||||
$('.email-address .settings-button.warning.email-disconnect').click();
|
||||
setTimeout(() => {
|
||||
assert.isTrue(view.navigate.calledOnce);
|
||||
const args = view.navigate.args[0];
|
||||
assert.equal(args.length, 1);
|
||||
assert.equal(args[0], '/settings/emails');
|
||||
done();
|
||||
TestHelpers.wrapAssertion(() => {
|
||||
assert.isTrue(view.navigate.calledOnce);
|
||||
const args = view.navigate.args[0];
|
||||
assert.equal(args.length, 1);
|
||||
assert.equal(args[0], '/settings/emails');
|
||||
}, done);
|
||||
}, 150);
|
||||
});
|
||||
|
||||
|
@ -285,6 +297,82 @@ define(function (require, exports, module) {
|
|||
assert.equal(view.isPanelOpen(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('does not show change email when `canChangeEmail` not set', () => {
|
||||
const newEmail = 'secondary@email.com';
|
||||
beforeEach(() => {
|
||||
emails = [{
|
||||
email: 'primary@email.com',
|
||||
isPrimary: true,
|
||||
verified: true
|
||||
}, {
|
||||
email: newEmail,
|
||||
isPrimary: false,
|
||||
verified: true
|
||||
}];
|
||||
|
||||
windowMock.location.search = '';
|
||||
|
||||
return initView()
|
||||
.then(function () {
|
||||
// click events require the view to be in the DOM
|
||||
$('#container').html(view.el);
|
||||
});
|
||||
});
|
||||
|
||||
it('can render', () => {
|
||||
assert.equal(view.$('.email-address').length, 1);
|
||||
assert.lengthOf(view.$('.email-address .address'), 1);
|
||||
assert.equal(view.$('.email-address .address').html(), 'secondary@email.com');
|
||||
assert.equal(view.$('.email-address .details .verified').length, 1);
|
||||
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').length, 1);
|
||||
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').attr('data-id'), 'secondary@email.com');
|
||||
assert.equal(view.$('.email-address .settings-button.secondary.set-primary').length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('can change email', () => {
|
||||
const newEmail = 'secondary@email.com';
|
||||
beforeEach(() => {
|
||||
emails = [{
|
||||
email: 'primary@email.com',
|
||||
isPrimary: true,
|
||||
verified: true
|
||||
}, {
|
||||
email: newEmail,
|
||||
isPrimary: false,
|
||||
verified: true
|
||||
}];
|
||||
|
||||
return initView()
|
||||
.then(function () {
|
||||
// click events require the view to be in the DOM
|
||||
$('#container').html(view.el);
|
||||
});
|
||||
});
|
||||
|
||||
it('can render', () => {
|
||||
assert.equal(view.$('.email-address').length, 1);
|
||||
assert.lengthOf(view.$('.email-address .address'), 1);
|
||||
assert.equal(view.$('.email-address .address').html(), 'secondary@email.com');
|
||||
assert.equal(view.$('.email-address .details .verified').length, 1);
|
||||
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').length, 1);
|
||||
assert.equal(view.$('.email-address .settings-button.warning.email-disconnect').attr('data-id'), 'secondary@email.com');
|
||||
assert.equal(view.$('.email-address .settings-button.secondary.set-primary').length, 1);
|
||||
assert.equal(view.$('.email-address .settings-button.secondary.set-primary').attr('data-id'), 'secondary@email.com');
|
||||
});
|
||||
|
||||
it('can change email', (done) => {
|
||||
$('.email-address .settings-button.secondary.set-primary').click();
|
||||
setTimeout(() => {
|
||||
TestHelpers.wrapAssertion(() => {
|
||||
assert.equal(account.get('email'), newEmail, 'account email updated');
|
||||
}, done);
|
||||
}, 150);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"cocktail": "0.5.9",
|
||||
"easteregg": "https://github.com/mozilla/fxa-easter-egg.git#ab20cd517cf8ae9feee115e48745189d28e13bc3",
|
||||
"fxa-checkbox": "mozilla/fxa-checkbox#7f856afffd394a144f718e28e6fb79092d6ccddd",
|
||||
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.60",
|
||||
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.61",
|
||||
"html5shiv": "3.7.2",
|
||||
"jquery": "3.1.0",
|
||||
"jquery-modal": "https://github.com/shane-tomlinson/jquery-modal.git#0576775d1b4590314b114386019f4c7421c77503",
|
||||
|
|
|
@ -340,3 +340,25 @@ Used to skip the confirmation form to reset a password
|
|||
#### When to use
|
||||
Should not be used by reliers.
|
||||
Should only be used for accounts that must be reset.
|
||||
|
||||
### `emailToHashWith`
|
||||
Allows you to override the default email that a reset password is hashed with.
|
||||
|
||||
#### Options
|
||||
* user's current primary email (default)
|
||||
|
||||
#### When to use
|
||||
After a user has changed their primary email you need to hash with the original account email
|
||||
if they perform a reset password.
|
||||
|
||||
## Secondary email parameters
|
||||
|
||||
### `canChangeEmail`
|
||||
Shows the option to change a user's primary email address.
|
||||
|
||||
#### Options
|
||||
* `true`
|
||||
* `false` (default)
|
||||
|
||||
#### When to specify
|
||||
* /settings/emails
|
||||
|
|
|
@ -50,6 +50,7 @@ define([
|
|||
'./functional/settings',
|
||||
'./functional/settings_clients',
|
||||
'./functional/settings_common',
|
||||
'./functional/settings_change_email.js',
|
||||
'./functional/settings_secondary_emails.js',
|
||||
'./functional/sync_settings',
|
||||
'./functional/change_password',
|
||||
|
|
|
@ -14,6 +14,9 @@ define([], function () {
|
|||
ERROR: '.error',
|
||||
HEADER: '#fxa-400-header'
|
||||
},
|
||||
CHANGE_PASSWORD: {
|
||||
MENU_BUTTON: '#change-password .settings-unit-toggle'
|
||||
},
|
||||
CHOOSE_WHAT_TO_SYNC: {
|
||||
ENGINE_ADDRESSES: '#sync-engine-addresses',
|
||||
ENGINE_CREDIT_CARDS: '#sync-engine-creditcards',
|
||||
|
@ -22,6 +25,12 @@ define([], function () {
|
|||
HEADER: '#fxa-choose-what-to-sync-header',
|
||||
SUBMIT: 'button[type=submit]'
|
||||
},
|
||||
COMPLETE_RESET_PASSWORD: {
|
||||
HEADER: '#fxa-complete-reset-password-header'
|
||||
},
|
||||
CONFIRM_RESET_PASSWORD: {
|
||||
HEADER: '#fxa-confirm-reset-password-header'
|
||||
},
|
||||
CONFIRM_SIGNIN: {
|
||||
HEADER: '#fxa-confirm-signin-header'
|
||||
},
|
||||
|
@ -31,6 +40,15 @@ define([], function () {
|
|||
CONNECT_ANOTHER_DEVICE: {
|
||||
HEADER: '#fxa-connect-another-device-header'
|
||||
},
|
||||
EMAIL: {
|
||||
ADDRESS_LABEL: '#emails .address',
|
||||
ADD_BUTTON: '.email-add:not(.disabled)',
|
||||
INPUT: '.new-email',
|
||||
MENU_BUTTON: '#emails .settings-unit-stub button',
|
||||
NOT_VERIFIED_LABEL: '.not-verified',
|
||||
SET_PRIMARY_EMAIL_BUTTON: '.email-address .set-primary',
|
||||
VERIFIED_LABEL: '.verified'
|
||||
},
|
||||
FORCE_AUTH: {
|
||||
EMAIL: 'input[type=email]',
|
||||
HEADER: '#fxa-force-auth-header'
|
||||
|
@ -49,7 +67,8 @@ define([], function () {
|
|||
EMAIL_NOT_EDITABLE: '.prefillEmail',
|
||||
HEADER: '#fxa-signin-header',
|
||||
PASSWORD: 'input[type=password]',
|
||||
SUBMIT: 'button[type=submit]'
|
||||
SUBMIT: 'button[type=submit]',
|
||||
TOOLTIP: '.tooltip',
|
||||
},
|
||||
SIGNIN_COMPLETE: {
|
||||
HEADER: '#fxa-sign-in-complete-header'
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'intern',
|
||||
'intern!object',
|
||||
'tests/lib/helpers',
|
||||
'tests/functional/lib/helpers',
|
||||
'tests/functional/lib/selectors',
|
||||
], function (intern, registerSuite, TestHelpers, FunctionalHelpers, selectors) {
|
||||
|
||||
const config = intern.config;
|
||||
|
||||
const SIGNUP_URL = config.fxaContentRoot + 'signup?canChangeEmail=true';
|
||||
const SIGNIN_URL = config.fxaContentRoot + 'signin?canChangeEmail=true';
|
||||
const SIGNIN_URL_NO_CHANGE_EMAIL = config.fxaContentRoot + 'signin';
|
||||
const PASSWORD = 'password';
|
||||
const NEW_PASSWORD = 'password1';
|
||||
|
||||
let email;
|
||||
let secondaryEmail;
|
||||
|
||||
const clearBrowserState = FunctionalHelpers.clearBrowserState;
|
||||
const click = FunctionalHelpers.click;
|
||||
const fillOutChangePassword = FunctionalHelpers.fillOutChangePassword;
|
||||
const fillOutResetPassword = FunctionalHelpers.fillOutResetPassword;
|
||||
const fillOutCompleteResetPassword = FunctionalHelpers.fillOutCompleteResetPassword;
|
||||
const fillOutSignUp = FunctionalHelpers.fillOutSignUp;
|
||||
const fillOutSignIn = FunctionalHelpers.fillOutSignIn;
|
||||
const openPage = FunctionalHelpers.openPage;
|
||||
const openVerificationLinkInNewTab = FunctionalHelpers.openVerificationLinkInNewTab;
|
||||
const openVerificationLinkInSameTab = FunctionalHelpers.openVerificationLinkInSameTab;
|
||||
const noSuchElement = FunctionalHelpers.noSuchElement;
|
||||
const testIsBrowserNotified = FunctionalHelpers.testIsBrowserNotified;
|
||||
const testElementExists = FunctionalHelpers.testElementExists;
|
||||
const testElementTextEquals = FunctionalHelpers.testElementTextEquals;
|
||||
const testErrorTextInclude = FunctionalHelpers.testErrorTextInclude;
|
||||
const testSuccessWasShown = FunctionalHelpers.testSuccessWasShown;
|
||||
const type = FunctionalHelpers.type;
|
||||
const visibleByQSA = FunctionalHelpers.visibleByQSA;
|
||||
|
||||
registerSuite({
|
||||
name: 'settings change email',
|
||||
|
||||
beforeEach: function () {
|
||||
email = TestHelpers.createEmail();
|
||||
secondaryEmail = TestHelpers.createEmail();
|
||||
return this.remote.then(clearBrowserState())
|
||||
.then(openPage(SIGNUP_URL, selectors.SIGNUP.HEADER))
|
||||
.then(fillOutSignUp(email, PASSWORD))
|
||||
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
|
||||
.then(openVerificationLinkInSameTab(email, 0))
|
||||
.then(testElementExists(selectors.SETTINGS.HEADER))
|
||||
.then(click(selectors.EMAIL.MENU_BUTTON))
|
||||
|
||||
// add secondary email, verify
|
||||
.then(type(selectors.EMAIL.INPUT, secondaryEmail))
|
||||
.then(click(selectors.EMAIL.ADD_BUTTON))
|
||||
.then(testElementExists(selectors.EMAIL.NOT_VERIFIED_LABEL))
|
||||
.then(openVerificationLinkInSameTab(secondaryEmail, 0))
|
||||
|
||||
.then(click(selectors.SETTINGS.SIGNOUT))
|
||||
.then(openPage(SIGNIN_URL, selectors.SIGNIN.HEADER))
|
||||
.then(fillOutSignIn(email, PASSWORD))
|
||||
|
||||
// set new primary email
|
||||
.then(click(selectors.EMAIL.MENU_BUTTON ))
|
||||
.then(testElementTextEquals(selectors.EMAIL.ADDRESS_LABEL, secondaryEmail))
|
||||
.then(testElementExists(selectors.EMAIL.VERIFIED_LABEL))
|
||||
.then(click(selectors.EMAIL.SET_PRIMARY_EMAIL_BUTTON));
|
||||
},
|
||||
|
||||
afterEach: function () {
|
||||
return this.remote.then(clearBrowserState());
|
||||
},
|
||||
|
||||
'does no show change email option if query `canChangeEmail` not set': function () {
|
||||
return this.remote
|
||||
// sign out
|
||||
.then(click(selectors.SETTINGS.SIGNOUT))
|
||||
|
||||
// sign in and does not show change primary email button
|
||||
.then(openPage(SIGNIN_URL_NO_CHANGE_EMAIL, selectors.SIGNIN.HEADER))
|
||||
.then(testElementExists(selectors.SIGNIN.HEADER))
|
||||
.then(fillOutSignIn(secondaryEmail, PASSWORD))
|
||||
.then(click(selectors.EMAIL.MENU_BUTTON ))
|
||||
.then(noSuchElement(selectors.EMAIL.SET_PRIMARY_EMAIL_BUTTON));
|
||||
},
|
||||
|
||||
'can change primary email and login': function () {
|
||||
return this.remote
|
||||
// sign out
|
||||
.then(click(selectors.SETTINGS.SIGNOUT))
|
||||
|
||||
// sign in with old primary email fails
|
||||
.then(openPage(SIGNIN_URL, selectors.SIGNIN.HEADER))
|
||||
.then(testElementExists(selectors.SIGNIN.HEADER))
|
||||
.then(fillOutSignIn(email, PASSWORD))
|
||||
.then(testErrorTextInclude('Primary account email required'))
|
||||
|
||||
// sign in with new primary email
|
||||
.then(testElementExists(selectors.SIGNIN.HEADER))
|
||||
.then(fillOutSignIn(secondaryEmail, PASSWORD))
|
||||
|
||||
// shows new primary email
|
||||
.then(testElementExists(selectors.SETTINGS.HEADER))
|
||||
.then(testElementTextEquals(selectors.SETTINGS.PROFILE_HEADER, secondaryEmail));
|
||||
},
|
||||
|
||||
'can change primary email, change password and login': function () {
|
||||
return this.remote
|
||||
// change password
|
||||
.then(click(selectors.CHANGE_PASSWORD.MENU_BUTTON))
|
||||
.then(fillOutChangePassword(PASSWORD, NEW_PASSWORD))
|
||||
.then(testIsBrowserNotified('fxaccounts:change_password'))
|
||||
.then(testElementExists(selectors.SETTINGS.HEADER))
|
||||
.then(testSuccessWasShown())
|
||||
|
||||
// sign out and fails login with old password
|
||||
.then(click(selectors.SETTINGS.SIGNOUT))
|
||||
.then(testElementExists(selectors.SIGNIN.HEADER))
|
||||
.then(fillOutSignIn(secondaryEmail, PASSWORD))
|
||||
.then(visibleByQSA(selectors.SIGNIN.TOOLTIP))
|
||||
|
||||
// sign in with new password
|
||||
.then(fillOutSignIn(secondaryEmail, NEW_PASSWORD))
|
||||
.then(testElementTextEquals(selectors.SETTINGS.PROFILE_HEADER, secondaryEmail));
|
||||
},
|
||||
|
||||
'can change primary email, reset password and login': function () {
|
||||
return this.remote
|
||||
.then(click(selectors.SETTINGS.SIGNOUT))
|
||||
|
||||
// reset password
|
||||
.then(fillOutResetPassword(secondaryEmail))
|
||||
.then(testElementExists(selectors.CONFIRM_RESET_PASSWORD.HEADER))
|
||||
.then(openVerificationLinkInNewTab(secondaryEmail, 1))
|
||||
|
||||
// complete the reset password in the new tab
|
||||
.switchToWindow('newwindow')
|
||||
.then(testElementExists(selectors.COMPLETE_RESET_PASSWORD.HEADER))
|
||||
.then(fillOutCompleteResetPassword(NEW_PASSWORD, NEW_PASSWORD))
|
||||
|
||||
.then(testElementTextEquals(selectors.SETTINGS.PROFILE_HEADER, secondaryEmail))
|
||||
|
||||
// sign out and fails login with old password
|
||||
.then(click(selectors.SETTINGS.SIGNOUT))
|
||||
.then(testElementExists(selectors.SIGNIN.HEADER))
|
||||
.then(fillOutSignIn(secondaryEmail, PASSWORD))
|
||||
.then(visibleByQSA(selectors.SIGNIN.TOOLTIP))
|
||||
|
||||
// sign in with new password succeeds
|
||||
.then(fillOutSignIn(secondaryEmail, NEW_PASSWORD))
|
||||
.then(testElementTextEquals(selectors.SETTINGS.PROFILE_HEADER, secondaryEmail));
|
||||
}
|
||||
});
|
||||
});
|
|
@ -76,7 +76,7 @@ define([
|
|||
.then(type('.new-email', TestHelpers.createEmail()))
|
||||
.then(click('.email-add:not(.disabled)'))
|
||||
.then(testElementExists('.not-verified'))
|
||||
.then(click('.email-disconnect'))
|
||||
.then(click('.email-address .settings-button.warning.email-disconnect'))
|
||||
|
||||
// add secondary email, verify
|
||||
.then(type('.new-email', secondaryEmail))
|
||||
|
|
Загрузка…
Ссылка в новой задаче