feat(emails): UX for additional emails r=vladikoff,vbudhram,shane-tomlinson
Fixes #4756
This commit is contained in:
Родитель
574ecb4e01
Коммит
314e593ea2
|
@ -145,6 +145,36 @@ define(function (require, exports, module) {
|
||||||
errno: 135,
|
errno: 135,
|
||||||
message: t('Unable to deliver email')
|
message: t('Unable to deliver email')
|
||||||
},
|
},
|
||||||
|
// Secondary Email errors
|
||||||
|
EMAIL_EXISTS: {
|
||||||
|
errno: 136,
|
||||||
|
message: t('This email was already verified by another user')
|
||||||
|
},
|
||||||
|
EMAIL_PRIMARY_EXISTS: {
|
||||||
|
errno: 139,
|
||||||
|
message: t('Secondary email must be different than your account email')
|
||||||
|
},
|
||||||
|
EMAIL_VERIFIED_PRIMARY_EXISTS: {
|
||||||
|
errno: 140,
|
||||||
|
message: t('Account already exists')
|
||||||
|
},
|
||||||
|
UNVERIFIED_PRIMARY_EMAIL_NEWLY_CREATED: {
|
||||||
|
errno: 141,
|
||||||
|
message: t('Account already exists')
|
||||||
|
},
|
||||||
|
LOGIN_WITH_SECONDARY_EMAIL: {
|
||||||
|
errno: 142,
|
||||||
|
message: t('Primary account email required for sign-in')
|
||||||
|
},
|
||||||
|
VERIFIED_SECONDARY_EMAIL_EXISTS: {
|
||||||
|
errno: 144,
|
||||||
|
message: t('Address in use by another account')
|
||||||
|
},
|
||||||
|
RESET_PASSWORD_WITH_SECONDARY_EMAIL: {
|
||||||
|
errno: 145,
|
||||||
|
message: t('Primary account email required for reset')
|
||||||
|
},
|
||||||
|
// Secondary Email errors end
|
||||||
SERVER_BUSY: {
|
SERVER_BUSY: {
|
||||||
errno: 201,
|
errno: 201,
|
||||||
message: t('Server busy, try again soon')
|
message: t('Server busy, try again soon')
|
||||||
|
|
|
@ -565,6 +565,21 @@ define(function (require, exports, module) {
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function gets the status of the user's sessionToken.
|
||||||
|
* It differs from `recoveryEmailStatus` because it also returns
|
||||||
|
* `sessionVerified`, which gives the true state of the sessionToken.
|
||||||
|
*
|
||||||
|
* Note that a session is considered verified if it has gone through
|
||||||
|
* an email verification loop.
|
||||||
|
*
|
||||||
|
* @param {String} sessionToken
|
||||||
|
* @returns {Promise} resolves with response when complete.
|
||||||
|
*/
|
||||||
|
sessionVerificationStatus: withClient(function (client, sessionToken) {
|
||||||
|
return client.recoveryEmailStatus(sessionToken);
|
||||||
|
}),
|
||||||
|
|
||||||
accountKeys: withClient((client, keyFetchToken, unwrapBKey) => {
|
accountKeys: withClient((client, keyFetchToken, unwrapBKey) => {
|
||||||
return client.accountKeys(keyFetchToken, unwrapBKey);
|
return client.accountKeys(keyFetchToken, unwrapBKey);
|
||||||
}),
|
}),
|
||||||
|
@ -651,6 +666,22 @@ define(function (require, exports, module) {
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
recoveryEmails: withClient((client, sessionToken) => {
|
||||||
|
return client.recoveryEmails(sessionToken);
|
||||||
|
}),
|
||||||
|
|
||||||
|
recoveryEmailCreate: withClient((client, sessionToken, email) => {
|
||||||
|
return client.recoveryEmailCreate(sessionToken, email);
|
||||||
|
}),
|
||||||
|
|
||||||
|
recoveryEmailDestroy: withClient((client, sessionToken, email) => {
|
||||||
|
return client.recoveryEmailDestroy(sessionToken, email);
|
||||||
|
}),
|
||||||
|
|
||||||
|
resendEmailCode: withClient((client, sessionToken, email) => {
|
||||||
|
return client.recoveryEmailResendCode(sessionToken, {email: email});
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether SMS is enabled for the user
|
* Check whether SMS is enabled for the user
|
||||||
*
|
*
|
||||||
|
@ -663,6 +694,10 @@ define(function (require, exports, module) {
|
||||||
*/
|
*/
|
||||||
smsStatus: withClient((client, sessionToken, options) => {
|
smsStatus: withClient((client, sessionToken, options) => {
|
||||||
return client.smsStatus(sessionToken, options);
|
return client.smsStatus(sessionToken, options);
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteEmail: withClient((client, sessionToken, email) => {
|
||||||
|
return client.deleteEmail(sessionToken, email);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ define(function (require, exports, module) {
|
||||||
const CookiesDisabledView = require('../views/cookies_disabled');
|
const CookiesDisabledView = require('../views/cookies_disabled');
|
||||||
const DeleteAccountView = require('../views/settings/delete_account');
|
const DeleteAccountView = require('../views/settings/delete_account');
|
||||||
const DisplayNameView = require('../views/settings/display_name');
|
const DisplayNameView = require('../views/settings/display_name');
|
||||||
|
const EmailsView = require('../views/settings/emails');
|
||||||
const ForceAuthView = require('../views/force_auth');
|
const ForceAuthView = require('../views/force_auth');
|
||||||
const IndexView = require('../views/index');
|
const IndexView = require('../views/index');
|
||||||
const LegalView = require('../views/legal');
|
const LegalView = require('../views/legal');
|
||||||
|
@ -89,6 +90,7 @@ define(function (require, exports, module) {
|
||||||
'reset_password(/)': createViewHandler(ResetPasswordView),
|
'reset_password(/)': createViewHandler(ResetPasswordView),
|
||||||
'reset_password_confirmed(/)': createViewHandler(ReadyView, { type: VerificationReasons.PASSWORD_RESET }),
|
'reset_password_confirmed(/)': createViewHandler(ReadyView, { type: VerificationReasons.PASSWORD_RESET }),
|
||||||
'reset_password_verified(/)': createViewHandler(ReadyView, { type: VerificationReasons.PASSWORD_RESET }),
|
'reset_password_verified(/)': createViewHandler(ReadyView, { type: VerificationReasons.PASSWORD_RESET }),
|
||||||
|
'secondary_email_verified(/)': createViewHandler(ReadyView, { type: VerificationReasons.SECONDARY_EMAIL_VERIFIED }),
|
||||||
'settings(/)': createViewHandler(SettingsView),
|
'settings(/)': createViewHandler(SettingsView),
|
||||||
'settings/avatar/camera(/)': createChildViewHandler(AvatarCameraView, SettingsView),
|
'settings/avatar/camera(/)': createChildViewHandler(AvatarCameraView, SettingsView),
|
||||||
'settings/avatar/change(/)': createChildViewHandler(AvatarChangeView, SettingsView),
|
'settings/avatar/change(/)': createChildViewHandler(AvatarChangeView, SettingsView),
|
||||||
|
@ -99,6 +101,7 @@ define(function (require, exports, module) {
|
||||||
'settings/communication_preferences(/)': createChildViewHandler(CommunicationPreferencesView, SettingsView),
|
'settings/communication_preferences(/)': createChildViewHandler(CommunicationPreferencesView, SettingsView),
|
||||||
'settings/delete_account(/)': createChildViewHandler(DeleteAccountView, SettingsView),
|
'settings/delete_account(/)': createChildViewHandler(DeleteAccountView, SettingsView),
|
||||||
'settings/display_name(/)': createChildViewHandler(DisplayNameView, SettingsView),
|
'settings/display_name(/)': createChildViewHandler(DisplayNameView, SettingsView),
|
||||||
|
'settings/emails(/)': createChildViewHandler(EmailsView, SettingsView),
|
||||||
'signin(/)': createViewHandler(SignInView),
|
'signin(/)': createViewHandler(SignInView),
|
||||||
'signin_confirmed(/)': createViewHandler(ReadyView, { type: VerificationReasons.SIGN_IN }),
|
'signin_confirmed(/)': createViewHandler(ReadyView, { type: VerificationReasons.SIGN_IN }),
|
||||||
'signin_permissions(/)': createViewHandler(PermissionsView, { type: VerificationReasons.SIGN_IN }),
|
'signin_permissions(/)': createViewHandler(PermissionsView, { type: VerificationReasons.SIGN_IN }),
|
||||||
|
@ -112,7 +115,8 @@ define(function (require, exports, module) {
|
||||||
'sms(/)': createViewHandler(SmsSendView),
|
'sms(/)': createViewHandler(SmsSendView),
|
||||||
'sms/sent(/)': createViewHandler(SmsSentView),
|
'sms/sent(/)': createViewHandler(SmsSentView),
|
||||||
'sms/why(/)': createChildViewHandler(WhyConnectAnotherDeviceView, SmsSendView),
|
'sms/why(/)': createChildViewHandler(WhyConnectAnotherDeviceView, SmsSendView),
|
||||||
'verify_email(/)': createViewHandler(CompleteSignUpView, { type: VerificationReasons.SIGN_UP })
|
'verify_email(/)': createViewHandler(CompleteSignUpView, { type: VerificationReasons.SIGN_UP }),
|
||||||
|
'verify_secondary_email(/)': createViewHandler(CompleteSignUpView, { type: VerificationReasons.SECONDARY_EMAIL_VERIFIED })
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize (options = {}) {
|
initialize (options = {}) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ define(function (require, exports, module) {
|
||||||
return {
|
return {
|
||||||
FORCE_AUTH: 'force_auth',
|
FORCE_AUTH: 'force_auth',
|
||||||
PASSWORD_RESET: 'reset_password',
|
PASSWORD_RESET: 'reset_password',
|
||||||
|
SECONDARY_EMAIL_VERIFIED: 'secondary_email_verified',
|
||||||
SIGN_IN: 'login',
|
SIGN_IN: 'login',
|
||||||
SIGN_UP: 'signup'
|
SIGN_UP: 'signup'
|
||||||
};
|
};
|
||||||
|
|
|
@ -262,6 +262,43 @@ define(function (require, exports, module) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function simply returns the session status of the user. It differs
|
||||||
|
* from `sessionStatus` function above because it is not used to determine
|
||||||
|
* which view to take a user after the login. This function also does not
|
||||||
|
* have the restriction to be backwards compatible to legacy clients.
|
||||||
|
*
|
||||||
|
* @returns {Promise} resolves with the account's current session
|
||||||
|
* information if session is valid. Rejects with an INVALID_TOKEN error
|
||||||
|
* if session is invalid.
|
||||||
|
*
|
||||||
|
* Session information:
|
||||||
|
* {
|
||||||
|
* email: <canonicalized email>,
|
||||||
|
* verified: <boolean>
|
||||||
|
* emailVerified: <boolean>
|
||||||
|
* sessionVerified: <boolean>
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
sessionVerificationStatus () {
|
||||||
|
const sessionToken = this.get('sessionToken');
|
||||||
|
if (! sessionToken) {
|
||||||
|
return p.reject(AuthErrors.toError('INVALID_TOKEN'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._fxaClient.sessionVerificationStatus(sessionToken)
|
||||||
|
.then((resp) => {
|
||||||
|
return resp;
|
||||||
|
}, (err) => {
|
||||||
|
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
|
||||||
|
// sessionToken is no longer valid, kill it.
|
||||||
|
this.unset('sessionToken');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for the session to become verified.
|
* Wait for the session to become verified.
|
||||||
*
|
*
|
||||||
|
@ -1035,6 +1072,98 @@ define(function (require, exports, module) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._fxaClient.smsStatus(sessionToken, options);
|
return this._fxaClient.smsStatus(sessionToken, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get emails associated with user.
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
recoveryEmails () {
|
||||||
|
return this._fxaClient.recoveryEmails(
|
||||||
|
this.get('sessionToken')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates a new email to a user's account.
|
||||||
|
*
|
||||||
|
* @param {String} email
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
recoveryEmailCreate (email) {
|
||||||
|
return this._fxaClient.recoveryEmailCreate(
|
||||||
|
this.get('sessionToken'),
|
||||||
|
email
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes email from user's account.
|
||||||
|
*
|
||||||
|
* @param {String} email
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
recoveryEmailDestroy (email) {
|
||||||
|
return this._fxaClient.recoveryEmailDestroy(
|
||||||
|
this.get('sessionToken'),
|
||||||
|
email
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend the verification code associated with the passed email address
|
||||||
|
*
|
||||||
|
* @param {String} email
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
resendEmailCode (email) {
|
||||||
|
return this._fxaClient.resendEmailCode(
|
||||||
|
this.get('sessionToken'),
|
||||||
|
email
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get emails associated with user.
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
getEmails () {
|
||||||
|
return this._fxaClient.getEmails(
|
||||||
|
this.get('sessionToken')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates a new email to a users account.
|
||||||
|
*
|
||||||
|
* @param {String} email
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
createEmail (email) {
|
||||||
|
return this._fxaClient.createEmail(
|
||||||
|
this.get('sessionToken'),
|
||||||
|
email
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates a new email to a users account.
|
||||||
|
*
|
||||||
|
* @param {String} email
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
deleteEmail (email) {
|
||||||
|
return this._fxaClient.deleteEmail(
|
||||||
|
this.get('sessionToken'),
|
||||||
|
email
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
ALLOWED_KEYS: ALLOWED_KEYS,
|
ALLOWED_KEYS: ALLOWED_KEYS,
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email information
|
||||||
|
*/
|
||||||
|
|
||||||
|
define(function (require, exports, module) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Backbone = require('backbone');
|
||||||
|
|
||||||
|
var Email = Backbone.Model.extend({
|
||||||
|
defaults: {
|
||||||
|
email: null,
|
||||||
|
isPrimary: false,
|
||||||
|
verified: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Email;
|
||||||
|
});
|
||||||
|
|
|
@ -16,6 +16,7 @@ define(function (require, exports, module) {
|
||||||
defaults: {
|
defaults: {
|
||||||
code: null,
|
code: null,
|
||||||
reminder: null,
|
reminder: null,
|
||||||
|
type: null,
|
||||||
uid: null
|
uid: null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,12 @@
|
||||||
<p class="account-ready-service">{{#t}}You are now ready to use %(serviceName)s{{/t}}</p>
|
<p class="account-ready-service">{{#t}}You are now ready to use %(serviceName)s{{/t}}</p>
|
||||||
{{/serviceName}}
|
{{/serviceName}}
|
||||||
{{^serviceName}}
|
{{^serviceName}}
|
||||||
<p class="account-ready-generic">{{#t}}Your account is ready!{{/t}}</p>
|
{{#secondaryEmailVerified}}
|
||||||
|
<p class="account-ready-service">{{#t}}Account notifications will now also be sent to %(secondaryEmailVerified)s{{/t}}</p>
|
||||||
|
{{/secondaryEmailVerified}}
|
||||||
|
{{^secondaryEmailVerified}}
|
||||||
|
<p class="account-ready-generic">{{#t}}Your account is ready!{{/t}}</p>
|
||||||
|
{{/secondaryEmailVerified}}
|
||||||
{{/serviceName}}
|
{{/serviceName}}
|
||||||
{{/isSync}}
|
{{/isSync}}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
<div id="emails" class="settings-unit {{#isPanelOpen}}open{{/isPanelOpen}}">
|
||||||
|
<div class="settings-unit-stub">
|
||||||
|
<header class="settings-unit-summary">
|
||||||
|
<h2 class="settings-unit-title">{{#t}}Secondary email{{/t}}</h2>
|
||||||
|
</header>
|
||||||
|
<button class="settings-button settings-unit-toggle hidden {{buttonClass}}" data-href="/settings/emails">
|
||||||
|
{{#hasSecondaryEmail}}{{#t}}Change…{{/t}}{{/hasSecondaryEmail}}
|
||||||
|
{{^hasSecondaryEmail}}{{#t}}Add…{{/t}}{{/hasSecondaryEmail}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-unit-details">
|
||||||
|
<form novalidate>
|
||||||
|
<p>
|
||||||
|
{{#t}}A secondary email is an additional address for receiving security notices and confirming new Sync devices.{{/t}}
|
||||||
|
</p>
|
||||||
|
{{#hasSecondaryEmail}}
|
||||||
|
<ul class="email-list button-row">
|
||||||
|
{{#emails}}
|
||||||
|
{{^isPrimary}}
|
||||||
|
<li class="email-address">
|
||||||
|
<div class="address">{{email}}</div>
|
||||||
|
<div class="details">
|
||||||
|
{{#verified}}
|
||||||
|
<div class="verified">{{#t}}verified{{/t}}</div>
|
||||||
|
{{/verified}}
|
||||||
|
{{^verified}}
|
||||||
|
<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>
|
||||||
|
</li>
|
||||||
|
{{^verified}}
|
||||||
|
<a class="resend" data-id="{{email}}">{{#t}}Didn't arrive and not in spam folder? Resend?{{/t}}</a>
|
||||||
|
{{/verified}}
|
||||||
|
{{/isPrimary}}
|
||||||
|
{{/emails}}
|
||||||
|
</ul>
|
||||||
|
{{/hasSecondaryEmail}}
|
||||||
|
|
||||||
|
{{^hasSecondaryEmail}}
|
||||||
|
<div class="input-row">
|
||||||
|
<label class="label-helper"></label>
|
||||||
|
<input type="email" class="new-email tooltip-below" placeholder="{{#t}}Secondary email{{/t}}"
|
||||||
|
value="{{newEmail}}" autofocus autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
{{/hasSecondaryEmail}}
|
||||||
|
|
||||||
|
<div class="button-row">
|
||||||
|
{{#hasSecondaryEmail}}
|
||||||
|
<button class="settings-button email-refresh primary enabled">{{#t}}Refresh{{/t}}</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="settings-button {{#hasSecondaryVerifiedEmail}}cancel secondary enabled{{/hasSecondaryVerifiedEmail}}
|
||||||
|
{{^hasSecondaryVerifiedEmail}} secondary-disabled disabled {{/hasSecondaryVerifiedEmail}}">
|
||||||
|
{{#t}}Done{{/t}}
|
||||||
|
</button>
|
||||||
|
{{/hasSecondaryEmail}}
|
||||||
|
|
||||||
|
{{^hasSecondaryEmail}}
|
||||||
|
<button type="submit" class="settings-button email-add primary disabled">{{#t}}Add{{/t}}</button>
|
||||||
|
<button class="settings-button cancel secondary enabled">{{#t}}Cancel{{/t}}</button>
|
||||||
|
{{/hasSecondaryEmail}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -10,6 +10,7 @@
|
||||||
* 2. Existing users that have signed in with an unverified account.
|
* 2. Existing users that have signed in with an unverified account.
|
||||||
* 3. Existing users that are signing into Sync and
|
* 3. Existing users that are signing into Sync and
|
||||||
* must re-confirm their account.
|
* must re-confirm their account.
|
||||||
|
* 4. Existing users that confirmed a secondary email.
|
||||||
*
|
*
|
||||||
* The auth server endpoints that are called are the same in all cases.
|
* The auth server endpoints that are called are the same in all cases.
|
||||||
*/
|
*/
|
||||||
|
@ -76,8 +77,10 @@ define(function (require, exports, module) {
|
||||||
const code = verificationInfo.get('code');
|
const code = verificationInfo.get('code');
|
||||||
const options = {
|
const options = {
|
||||||
reminder: verificationInfo.get('reminder'),
|
reminder: verificationInfo.get('reminder'),
|
||||||
|
secondaryEmailVerified: this.getSearchParam('secondary_email_verified') || null,
|
||||||
serverVerificationStatus: this.getSearchParam('server_verification') || null,
|
serverVerificationStatus: this.getSearchParam('server_verification') || null,
|
||||||
service: this.relier.get('service') || null
|
service: this.relier.get('service') || null,
|
||||||
|
type: verificationInfo.get('type')
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.user.completeAccountSignUp(account, code, options)
|
return this.user.completeAccountSignUp(account, code, options)
|
||||||
|
@ -187,7 +190,9 @@ define(function (require, exports, module) {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_navigateToVerifiedScreen () {
|
_navigateToVerifiedScreen () {
|
||||||
if (this.isSignUp()) {
|
if (this.getSearchParam('secondary_email_verified')) {
|
||||||
|
this.navigate('secondary_email_verified');
|
||||||
|
} else if (this.isSignUp()) {
|
||||||
this.navigate('signup_verified');
|
this.navigate('signup_verified');
|
||||||
} else {
|
} else {
|
||||||
this.navigate('signin_verified');
|
this.navigate('signin_verified');
|
||||||
|
|
|
@ -18,8 +18,16 @@ define(function (require, exports, module) {
|
||||||
const p = require('lib/promise');
|
const p = require('lib/promise');
|
||||||
const ProgressIndicator = require('views/progress_indicator');
|
const ProgressIndicator = require('views/progress_indicator');
|
||||||
|
|
||||||
function showProgressIndicator(handler, _el) {
|
// Return a promise delayed by ms
|
||||||
|
function delay(progressIndicator, ms) {
|
||||||
|
var deferred = p.defer();
|
||||||
|
progressIndicator.setTimeout(deferred.resolve, ms);
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showProgressIndicator(handler, _el, delayMills) {
|
||||||
var el = _el || 'button[type=submit]';
|
var el = _el || 'button[type=submit]';
|
||||||
|
const delayHandlerByMills = delayMills || 0;
|
||||||
|
|
||||||
return function () {
|
return function () {
|
||||||
var args = arguments;
|
var args = arguments;
|
||||||
|
@ -28,7 +36,8 @@ define(function (require, exports, module) {
|
||||||
var progressIndicator = getProgressIndicator(this, target);
|
var progressIndicator = getProgressIndicator(this, target);
|
||||||
progressIndicator.start(target);
|
progressIndicator.start(target);
|
||||||
|
|
||||||
return p().then(() => this.invokeHandler(handler, args))
|
return delay(progressIndicator, delayHandlerByMills)
|
||||||
|
.then(() => this.invokeHandler(handler, args))
|
||||||
.then(function (value) {
|
.then(function (value) {
|
||||||
// Stop the progress indicator unless the flow halts.
|
// Stop the progress indicator unless the flow halts.
|
||||||
if (! (value && value.halt)) {
|
if (! (value && value.halt)) {
|
||||||
|
|
|
@ -89,12 +89,15 @@ define(function (require, exports, module) {
|
||||||
$('.settings-unit').removeClass('open');
|
$('.settings-unit').removeClass('open');
|
||||||
},
|
},
|
||||||
|
|
||||||
displaySuccess (msg) {
|
displaySuccess (msg, options = {closePanel: true}) {
|
||||||
if (! this.parentView) {
|
if (! this.parentView) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.parentView.displaySuccess(msg);
|
this.parentView.displaySuccess(msg);
|
||||||
this.closePanel();
|
|
||||||
}
|
if (options.closePanel) {
|
||||||
|
this.closePanel();
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,6 +39,10 @@ define(function (require, exports, module) {
|
||||||
headerTitle: t('Password reset'),
|
headerTitle: t('Password reset'),
|
||||||
readyToSyncText: t('Complete set-up by entering the new password on your other Firefox devices.')
|
readyToSyncText: t('Complete set-up by entering the new password on your other Firefox devices.')
|
||||||
},
|
},
|
||||||
|
SECONDARY_EMAIL_VERIFIED: {
|
||||||
|
headerId: 'fxa-sign-up-complete-header',
|
||||||
|
headerTitle: t('Email verified')
|
||||||
|
},
|
||||||
// signin_confirmed and signin_verified are only shown to Sync for now.
|
// signin_confirmed and signin_verified are only shown to Sync for now.
|
||||||
SIGN_IN: {
|
SIGN_IN: {
|
||||||
headerId: 'fxa-sign-in-complete-header',
|
headerId: 'fxa-sign-in-complete-header',
|
||||||
|
@ -69,6 +73,7 @@ define(function (require, exports, module) {
|
||||||
isSync: this.relier.isSync(),
|
isSync: this.relier.isSync(),
|
||||||
readyToSyncText: this._getReadyToSyncText(),
|
readyToSyncText: this._getReadyToSyncText(),
|
||||||
redirectUri: this.relier.get('redirectUri'),
|
redirectUri: this.relier.get('redirectUri'),
|
||||||
|
secondaryEmailVerified: this.getSearchParam('secondary_email_verified') || null,
|
||||||
service: this.relier.get('service'),
|
service: this.relier.get('service'),
|
||||||
serviceName: this.relier.get('serviceName')
|
serviceName: this.relier.get('serviceName')
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,7 @@ define(function (require, exports, module) {
|
||||||
const ClientsView = require('views/settings/clients');
|
const ClientsView = require('views/settings/clients');
|
||||||
const ClientDisconnectView = require('views/settings/client_disconnect');
|
const ClientDisconnectView = require('views/settings/client_disconnect');
|
||||||
const DisplayNameView = require('views/settings/display_name');
|
const DisplayNameView = require('views/settings/display_name');
|
||||||
|
const EmailsView = require('views/settings/emails');
|
||||||
const Duration = require('duration');
|
const Duration = require('duration');
|
||||||
const LoadingMixin = require('views/mixins/loading-mixin');
|
const LoadingMixin = require('views/mixins/loading-mixin');
|
||||||
const modal = require('modal'); //eslint-disable-line no-unused-vars
|
const modal = require('modal'); //eslint-disable-line no-unused-vars
|
||||||
|
@ -30,6 +31,7 @@ define(function (require, exports, module) {
|
||||||
const Template = require('stache!templates/settings');
|
const Template = require('stache!templates/settings');
|
||||||
|
|
||||||
var PANEL_VIEWS = [
|
var PANEL_VIEWS = [
|
||||||
|
EmailsView,
|
||||||
AvatarView,
|
AvatarView,
|
||||||
ClientsView,
|
ClientsView,
|
||||||
ClientDisconnectView,
|
ClientDisconnectView,
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
define(function (require, exports, module) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const $ = require('jquery');
|
||||||
|
const BaseView = require('views/base');
|
||||||
|
const Cocktail = require('cocktail');
|
||||||
|
const Email = require('models/email');
|
||||||
|
const FloatingPlaceholderMixin = require('views/mixins/floating-placeholder-mixin');
|
||||||
|
const FormView = require('views/form');
|
||||||
|
const preventDefaultThen = require('views/base').preventDefaultThen;
|
||||||
|
const SettingsPanelMixin = require('views/mixins/settings-panel-mixin');
|
||||||
|
const showProgressIndicator = require('views/decorators/progress_indicator');
|
||||||
|
const Strings = require('lib/strings');
|
||||||
|
const Template = require('stache!templates/settings/emails');
|
||||||
|
|
||||||
|
var t = BaseView.t;
|
||||||
|
|
||||||
|
const EMAIL_INPUT_SELECTOR = 'input.new-email';
|
||||||
|
const EMAIL_REFRESH_SELECTOR = 'button.settings-button.email-refresh';
|
||||||
|
const EMAIL_REFRESH_DELAYMS = 350;
|
||||||
|
|
||||||
|
var View = FormView.extend({
|
||||||
|
template: Template,
|
||||||
|
className: 'emails',
|
||||||
|
viewName: 'settings.emails',
|
||||||
|
|
||||||
|
events: {
|
||||||
|
'click .email-disconnect': preventDefaultThen('_onDisconnectEmail'),
|
||||||
|
'click .email-refresh.enabled': preventDefaultThen('refresh'),
|
||||||
|
'click .resend': preventDefaultThen('resend')
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize (options) {
|
||||||
|
if (options.emails) {
|
||||||
|
this._emails = options.emails;
|
||||||
|
} else {
|
||||||
|
this._emails = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
context () {
|
||||||
|
return {
|
||||||
|
buttonClass: this._hasSecondaryEmail() ? 'secondary' : 'primary',
|
||||||
|
emails: this._emails,
|
||||||
|
hasSecondaryEmail: this._hasSecondaryEmail(),
|
||||||
|
hasSecondaryVerifiedEmail: this._hasSecondaryVerifiedEmail(),
|
||||||
|
isPanelOpen: this.isPanelOpen(),
|
||||||
|
newEmail: this.newEmail
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeRender () {
|
||||||
|
// Only show this view on verified session
|
||||||
|
return this._isSecondaryEmailEnabled();
|
||||||
|
},
|
||||||
|
|
||||||
|
afterRender () {
|
||||||
|
// Panel should remain open if there are any unverified secondary emails
|
||||||
|
if (this._hasSecondaryEmail() && ! this._hasSecondaryVerifiedEmail()) {
|
||||||
|
this.openPanel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_isSecondaryEmailEnabled () {
|
||||||
|
// Only show secondary email panel if the user is in a verified session and feature is enabled.
|
||||||
|
const account = this.getSignedInAccount();
|
||||||
|
return account.sessionVerificationStatus()
|
||||||
|
.then((res) => {
|
||||||
|
if (! res.sessionVerified) {
|
||||||
|
return this.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we fail to fetch emails, then this user does not have this feature enabled
|
||||||
|
// and we should not display this panel.
|
||||||
|
return this._fetchEmails()
|
||||||
|
.fail(() => {
|
||||||
|
return this.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_hasSecondaryEmail () {
|
||||||
|
return this._emails.length > 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
_hasSecondaryVerifiedEmail () {
|
||||||
|
return this._hasSecondaryEmail() ? this._emails[1].verified : false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_onDisconnectEmail (event) {
|
||||||
|
const email = $(event.currentTarget).data('id');
|
||||||
|
const account = this.getSignedInAccount();
|
||||||
|
return account.recoveryEmailDestroy(email)
|
||||||
|
.then(()=> {
|
||||||
|
return this.render()
|
||||||
|
.then(()=> {
|
||||||
|
this.navigate('/settings/emails');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_fetchEmails () {
|
||||||
|
const account = this.getSignedInAccount();
|
||||||
|
return account.recoveryEmails()
|
||||||
|
.then((emails) => {
|
||||||
|
this._emails = emails.map((email) => {
|
||||||
|
return new Email(email).toJSON();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: showProgressIndicator(function() {
|
||||||
|
return this.render();
|
||||||
|
}, EMAIL_REFRESH_SELECTOR, EMAIL_REFRESH_DELAYMS),
|
||||||
|
|
||||||
|
resend (event) {
|
||||||
|
const email = $(event.currentTarget).data('id');
|
||||||
|
const account = this.getSignedInAccount();
|
||||||
|
return account.resendEmailCode(email)
|
||||||
|
.then(() => {
|
||||||
|
this.displaySuccess(Strings.interpolate(t('A verification link has been sent to %(email)s'), { email: email }), {
|
||||||
|
closePanel: false
|
||||||
|
});
|
||||||
|
this.navigate('/settings/emails');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
submit () {
|
||||||
|
const newEmail = this.getElementValue('input.new-email');
|
||||||
|
if (this.isPanelOpen() && newEmail) {
|
||||||
|
const account = this.getSignedInAccount();
|
||||||
|
return account.recoveryEmailCreate(newEmail)
|
||||||
|
.then(() => {
|
||||||
|
this.displaySuccess(Strings.interpolate(t('Verification emailed to %(email)s'), { email: newEmail }), {
|
||||||
|
closePanel: false
|
||||||
|
});
|
||||||
|
this.render();
|
||||||
|
})
|
||||||
|
.fail((err) => this.showValidationError(this.$(EMAIL_INPUT_SELECTOR), err));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Cocktail.mixin(
|
||||||
|
View,
|
||||||
|
SettingsPanelMixin,
|
||||||
|
FloatingPlaceholderMixin
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = View;
|
||||||
|
});
|
|
@ -245,6 +245,12 @@ body.settings #main-content.card {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.secondary-disabled {
|
||||||
|
background: $secondary-button-background-color;
|
||||||
|
border: 1px solid $secondary-button-border-color;
|
||||||
|
color: $text-color;
|
||||||
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
background: $error-background-color;
|
background: $error-background-color;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -641,3 +647,69 @@ section.modal-panel {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.email-list {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-address {
|
||||||
|
height: 40px;
|
||||||
|
list-style: none;
|
||||||
|
margin: 10px 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
html[dir='ltr'] & {
|
||||||
|
background-position: left 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir='rtl'] & {
|
||||||
|
background-position: right 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address {
|
||||||
|
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);
|
||||||
|
|
||||||
|
& .not-verified {
|
||||||
|
color: $color-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .verified {
|
||||||
|
color: $color-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button {
|
||||||
|
height: 35px;
|
||||||
|
/*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;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir='rtl'] & {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.settings-button {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
define(function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('chai').assert;
|
||||||
|
const Email = require('models/email');
|
||||||
|
|
||||||
|
describe('models/email', function () {
|
||||||
|
let email;
|
||||||
|
const emailOpts = {
|
||||||
|
email: 'some@email.com',
|
||||||
|
isPrimary: false,
|
||||||
|
verified: false
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
email = new Email(emailOpts);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', function () {
|
||||||
|
it('correctly sets model properties', function () {
|
||||||
|
assert.deepEqual(email.attributes, emailOpts);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -200,7 +200,7 @@ define(function (require, exports, module) {
|
||||||
var args = account.verifySignUp.getCall(0).args;
|
var args = account.verifySignUp.getCall(0).args;
|
||||||
assert.isTrue(account.verifySignUp.called);
|
assert.isTrue(account.verifySignUp.called);
|
||||||
assert.ok(args[0]);
|
assert.ok(args[0]);
|
||||||
assert.deepEqual(args[1], {reminder: null, serverVerificationStatus: null, service: validService});
|
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: null, serverVerificationStatus: null, service: validService, type: null});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -219,7 +219,7 @@ define(function (require, exports, module) {
|
||||||
var args = account.verifySignUp.getCall(0).args;
|
var args = account.verifySignUp.getCall(0).args;
|
||||||
assert.isTrue(account.verifySignUp.called);
|
assert.isTrue(account.verifySignUp.called);
|
||||||
assert.ok(args[0]);
|
assert.ok(args[0]);
|
||||||
assert.deepEqual(args[1], {reminder: validReminder, serverVerificationStatus: null, service: null});
|
assert.deepEqual(args[1], {reminder: validReminder, secondaryEmailVerified: null, serverVerificationStatus: null, service: null, type: null});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -239,7 +239,7 @@ define(function (require, exports, module) {
|
||||||
var args = account.verifySignUp.getCall(0).args;
|
var args = account.verifySignUp.getCall(0).args;
|
||||||
assert.isTrue(account.verifySignUp.called);
|
assert.isTrue(account.verifySignUp.called);
|
||||||
assert.ok(args[0]);
|
assert.ok(args[0]);
|
||||||
assert.deepEqual(args[1], {reminder: validReminder, serverVerificationStatus: null, service: validService});
|
assert.deepEqual(args[1], {reminder: validReminder, secondaryEmailVerified: null, serverVerificationStatus: null, service: validService, type: null});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -259,7 +259,48 @@ define(function (require, exports, module) {
|
||||||
var args = account.verifySignUp.getCall(0).args;
|
var args = account.verifySignUp.getCall(0).args;
|
||||||
assert.isTrue(account.verifySignUp.called);
|
assert.isTrue(account.verifySignUp.called);
|
||||||
assert.ok(args[0]);
|
assert.ok(args[0]);
|
||||||
assert.deepEqual(args[1], {reminder: null, serverVerificationStatus: 'verified', service: null});
|
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: null, serverVerificationStatus: 'verified', service: null, type: null});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if type is in the url', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
windowMock.location.search = '?code=' + validCode + '&uid=' + validUid +
|
||||||
|
'&type=secondary';
|
||||||
|
relier = new Relier({}, {
|
||||||
|
window: windowMock
|
||||||
|
});
|
||||||
|
relier.fetch();
|
||||||
|
initView(account);
|
||||||
|
return view.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('attempt to pass type to verifySignUp', function () {
|
||||||
|
var args = account.verifySignUp.getCall(0).args;
|
||||||
|
assert.isTrue(account.verifySignUp.called);
|
||||||
|
assert.ok(args[0]);
|
||||||
|
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: null, serverVerificationStatus: null, service: null, type: 'secondary'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('if secondary_email_verified is in the url', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
windowMock.location.search = '?code=' + validCode + '&uid=' + validUid +
|
||||||
|
'&secondary_email_verified=some@email.com';
|
||||||
|
relier = new Relier({}, {
|
||||||
|
window: windowMock
|
||||||
|
});
|
||||||
|
relier.fetch();
|
||||||
|
initView(account);
|
||||||
|
return view.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('attempt to pass secondary_email_verified to verifySignUp', function () {
|
||||||
|
var args = account.verifySignUp.getCall(0).args;
|
||||||
|
assert.isTrue(account.verifySignUp.called);
|
||||||
|
assert.ok(args[0]);
|
||||||
|
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: 'some@email.com', serverVerificationStatus: null, service: null, type: null});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,323 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
define(function (require, exports, module) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const $ = require('jquery');
|
||||||
|
const assert = require('chai').assert;
|
||||||
|
const BaseBroker = require('models/auth_brokers/base');
|
||||||
|
const BaseView = require('views/base');
|
||||||
|
const Metrics = require('lib/metrics');
|
||||||
|
const Notifier = require('lib/channels/notifier');
|
||||||
|
const p = require('lib/promise');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
const TestHelpers = require('../../../lib/helpers');
|
||||||
|
const Translator = require('lib/translator');
|
||||||
|
const User = require('models/user');
|
||||||
|
const View = require('views/settings/emails');
|
||||||
|
const WindowMock = require('../../../mocks/window');
|
||||||
|
|
||||||
|
describe('views/settings/emails', function () {
|
||||||
|
let account;
|
||||||
|
let emails;
|
||||||
|
let broker;
|
||||||
|
let email;
|
||||||
|
let metrics;
|
||||||
|
let notifier;
|
||||||
|
let parentView;
|
||||||
|
let translator;
|
||||||
|
const UID = '123';
|
||||||
|
let user;
|
||||||
|
let view;
|
||||||
|
let windowMock;
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
view = new View({
|
||||||
|
broker: broker,
|
||||||
|
emails: emails,
|
||||||
|
metrics: metrics,
|
||||||
|
notifier: notifier,
|
||||||
|
parentView: parentView,
|
||||||
|
translator: translator,
|
||||||
|
user: user,
|
||||||
|
window: windowMock
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
broker = new BaseBroker();
|
||||||
|
email = TestHelpers.createEmail();
|
||||||
|
notifier = new Notifier();
|
||||||
|
metrics = new Metrics({notifier});
|
||||||
|
parentView = new BaseView();
|
||||||
|
translator = new Translator({forceEnglish: true});
|
||||||
|
user = new User();
|
||||||
|
windowMock = new WindowMock();
|
||||||
|
|
||||||
|
account = user.initAccount({
|
||||||
|
email: email,
|
||||||
|
sessionToken: 'abc123',
|
||||||
|
uid: UID,
|
||||||
|
verified: true
|
||||||
|
});
|
||||||
|
|
||||||
|
emails = [];
|
||||||
|
|
||||||
|
sinon.stub(user, 'getSignedInAccount', () => {
|
||||||
|
return account;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if ($.prototype.trigger.restore) {
|
||||||
|
$.prototype.trigger.restore();
|
||||||
|
}
|
||||||
|
view.remove();
|
||||||
|
view.destroy();
|
||||||
|
|
||||||
|
view = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
view = new View({
|
||||||
|
notifier: notifier,
|
||||||
|
parentView: parentView,
|
||||||
|
user: user
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates `Email` instances if passed in', () => {
|
||||||
|
assert.ok(view._emails);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('feature disabled', () => {
|
||||||
|
describe('for user', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(account, 'recoveryEmails', () => {
|
||||||
|
return p.reject();
|
||||||
|
});
|
||||||
|
|
||||||
|
sinon.stub(account, 'sessionVerificationStatus', () => {
|
||||||
|
return p({sessionVerified: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
view = new View({
|
||||||
|
broker: broker,
|
||||||
|
emails: emails,
|
||||||
|
metrics: metrics,
|
||||||
|
notifier: notifier,
|
||||||
|
parentView: parentView,
|
||||||
|
translator: translator,
|
||||||
|
user: user,
|
||||||
|
window: windowMock
|
||||||
|
});
|
||||||
|
|
||||||
|
sinon.stub(view, 'remove', () => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be disabled when feature is disabled for user', () => {
|
||||||
|
assert.equal(view.remove.callCount, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for unverified session', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(account, 'recoveryEmails', () => {
|
||||||
|
return p();
|
||||||
|
});
|
||||||
|
|
||||||
|
sinon.stub(account, 'sessionVerificationStatus', () => {
|
||||||
|
return p({sessionVerified: false});
|
||||||
|
});
|
||||||
|
|
||||||
|
view = new View({
|
||||||
|
broker: broker,
|
||||||
|
emails: emails,
|
||||||
|
metrics: metrics,
|
||||||
|
notifier: notifier,
|
||||||
|
parentView: parentView,
|
||||||
|
translator: translator,
|
||||||
|
user: user,
|
||||||
|
window: windowMock
|
||||||
|
});
|
||||||
|
|
||||||
|
sinon.stub(view, 'remove', () => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be disabled when in unverified session', () => {
|
||||||
|
assert.equal(view.remove.callCount, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('feature enabled', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(account, 'recoveryEmails', () => {
|
||||||
|
return p(emails);
|
||||||
|
});
|
||||||
|
|
||||||
|
sinon.stub(account, 'sessionVerificationStatus', () => {
|
||||||
|
return p({sessionVerified: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
sinon.stub(account, 'recoveryEmailDestroy', () => {
|
||||||
|
return p();
|
||||||
|
});
|
||||||
|
|
||||||
|
sinon.stub(account, 'resendEmailCode', () => {
|
||||||
|
return p();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with no secondary email', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
emails = [{
|
||||||
|
email: 'primary@email.com',
|
||||||
|
isPrimary: true,
|
||||||
|
verified: true
|
||||||
|
}];
|
||||||
|
return initView();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has email input field', function () {
|
||||||
|
assert.ok(view.$('input.new-email').length, 1);
|
||||||
|
assert.ok(view.$('.email-add.primary.disabled').length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('add button enabled when email entered', function () {
|
||||||
|
view.$('input.new-email').val('asdf@email.com');
|
||||||
|
view.$('input.new-email').trigger({
|
||||||
|
type: 'keyup',
|
||||||
|
which: 9
|
||||||
|
});
|
||||||
|
assert.ok(view.$('.email-add.primary:not(.disabled)').length, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with unverified secondary email', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
emails = [{
|
||||||
|
email: 'primary@email.com',
|
||||||
|
isPrimary: true,
|
||||||
|
verified: true
|
||||||
|
}, {
|
||||||
|
email: 'another@one.com',
|
||||||
|
isPrimary: false,
|
||||||
|
verified: false
|
||||||
|
}];
|
||||||
|
|
||||||
|
return initView()
|
||||||
|
.then(function () {
|
||||||
|
// click events require the view to be in the DOM
|
||||||
|
$('#container').html(view.el);
|
||||||
|
sinon.spy(view, 'navigate');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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.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');
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls `render` when refreshed', (done) => {
|
||||||
|
$('.email-refresh').click();
|
||||||
|
sinon.spy(view, 'render');
|
||||||
|
setTimeout(function () {
|
||||||
|
assert.isTrue(view.render.calledOnce);
|
||||||
|
done();
|
||||||
|
}, 450); // Delay is higher here because refresh has a min delay of 350
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls `render` when resend and navigate to /emails', (done) => {
|
||||||
|
$('.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();
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('panel always open when unverified secondary email', () => {
|
||||||
|
assert.equal(view.isPanelOpen(), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with verified secondary email', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
emails = [{
|
||||||
|
email: 'primary@email.com',
|
||||||
|
isPrimary: true,
|
||||||
|
verified: true
|
||||||
|
}, {
|
||||||
|
email: 'another@one.com',
|
||||||
|
isPrimary: false,
|
||||||
|
verified: true
|
||||||
|
}];
|
||||||
|
|
||||||
|
return initView()
|
||||||
|
.then(function () {
|
||||||
|
// click events require the view to be in the DOM
|
||||||
|
$('#container').html(view.el);
|
||||||
|
sinon.spy(view, 'navigate');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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.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');
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('panel closed when verified secondary email', () => {
|
||||||
|
assert.equal(view.isPanelOpen(), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -81,6 +81,7 @@ function (Translator, Session) {
|
||||||
'../tests/spec/models/auth_brokers/oauth-redirect',
|
'../tests/spec/models/auth_brokers/oauth-redirect',
|
||||||
'../tests/spec/models/auth_brokers/web',
|
'../tests/spec/models/auth_brokers/web',
|
||||||
'../tests/spec/models/device',
|
'../tests/spec/models/device',
|
||||||
|
'../tests/spec/models/email',
|
||||||
'../tests/spec/models/email-resend',
|
'../tests/spec/models/email-resend',
|
||||||
'../tests/spec/models/flow',
|
'../tests/spec/models/flow',
|
||||||
'../tests/spec/models/form-prefill',
|
'../tests/spec/models/form-prefill',
|
||||||
|
@ -176,6 +177,7 @@ function (Translator, Session) {
|
||||||
'../tests/spec/views/settings/communication_preferences',
|
'../tests/spec/views/settings/communication_preferences',
|
||||||
'../tests/spec/views/settings/delete_account',
|
'../tests/spec/views/settings/delete_account',
|
||||||
'../tests/spec/views/settings/display_name',
|
'../tests/spec/views/settings/display_name',
|
||||||
|
'../tests/spec/views/settings/emails',
|
||||||
'../tests/spec/views/sign_in',
|
'../tests/spec/views/sign_in',
|
||||||
'../tests/spec/views/sign_in_reported',
|
'../tests/spec/views/sign_in_reported',
|
||||||
'../tests/spec/views/sign_in_unblock',
|
'../tests/spec/views/sign_in_unblock',
|
||||||
|
|
|
@ -37,6 +37,7 @@ module.exports = function () {
|
||||||
'settings/communication_preferences',
|
'settings/communication_preferences',
|
||||||
'settings/delete_account',
|
'settings/delete_account',
|
||||||
'settings/display_name',
|
'settings/display_name',
|
||||||
|
'settings/emails',
|
||||||
'signin',
|
'signin',
|
||||||
'signin_confirmed',
|
'signin_confirmed',
|
||||||
'signin_permissions',
|
'signin_permissions',
|
||||||
|
@ -47,10 +48,12 @@ module.exports = function () {
|
||||||
'signup_confirmed',
|
'signup_confirmed',
|
||||||
'signup_permissions',
|
'signup_permissions',
|
||||||
'signup_verified',
|
'signup_verified',
|
||||||
|
'secondary_email_verified',
|
||||||
'sms',
|
'sms',
|
||||||
'sms/sent',
|
'sms/sent',
|
||||||
'sms/why',
|
'sms/why',
|
||||||
'verify_email'
|
'verify_email',
|
||||||
|
'verify_secondary_email'
|
||||||
].join('|'); // prepare for use in a RegExp
|
].join('|'); // prepare for use in a RegExp
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -47,6 +47,7 @@ define([
|
||||||
'./functional/settings',
|
'./functional/settings',
|
||||||
'./functional/settings_clients',
|
'./functional/settings_clients',
|
||||||
'./functional/settings_common',
|
'./functional/settings_common',
|
||||||
|
'./functional/settings_secondary_emails.js',
|
||||||
'./functional/sync_settings',
|
'./functional/sync_settings',
|
||||||
'./functional/change_password',
|
'./functional/change_password',
|
||||||
'./functional/force_auth',
|
'./functional/force_auth',
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
/* 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'
|
||||||
|
], function (intern, registerSuite, TestHelpers, FunctionalHelpers) {
|
||||||
|
|
||||||
|
const config = intern.config;
|
||||||
|
const SIGNUP_URL = config.fxaContentRoot + 'signup';
|
||||||
|
const PASSWORD = 'password';
|
||||||
|
|
||||||
|
let email;
|
||||||
|
let secondaryEmail;
|
||||||
|
|
||||||
|
const clearBrowserState = FunctionalHelpers.clearBrowserState;
|
||||||
|
const click = FunctionalHelpers.click;
|
||||||
|
const createUser = FunctionalHelpers.createUser;
|
||||||
|
const fillOutResetPassword = FunctionalHelpers.fillOutResetPassword;
|
||||||
|
const fillOutSignIn = FunctionalHelpers.fillOutSignIn;
|
||||||
|
const fillOutSignUp = FunctionalHelpers.fillOutSignUp;
|
||||||
|
const openPage = FunctionalHelpers.openPage;
|
||||||
|
const openVerificationLinkInSameTab = FunctionalHelpers.openVerificationLinkInSameTab;
|
||||||
|
const testElementExists = FunctionalHelpers.testElementExists;
|
||||||
|
const testElementTextEquals = FunctionalHelpers.testElementTextEquals;
|
||||||
|
const testErrorTextInclude = FunctionalHelpers.testErrorTextInclude;
|
||||||
|
const type = FunctionalHelpers.type;
|
||||||
|
const visibleByQSA = FunctionalHelpers.visibleByQSA;
|
||||||
|
|
||||||
|
registerSuite({
|
||||||
|
name: 'settings secondary emails',
|
||||||
|
|
||||||
|
beforeEach: function () {
|
||||||
|
email = TestHelpers.createEmail();
|
||||||
|
secondaryEmail = TestHelpers.createEmail();
|
||||||
|
|
||||||
|
return this.remote.then(clearBrowserState());
|
||||||
|
},
|
||||||
|
|
||||||
|
afterEach: function () {
|
||||||
|
return this.remote.then(clearBrowserState());
|
||||||
|
},
|
||||||
|
|
||||||
|
'add and verify secondary email': function () {
|
||||||
|
return this.remote
|
||||||
|
// sign up via the UI, we need a verified session to use secondary email
|
||||||
|
.then(openPage(SIGNUP_URL, '#fxa-signup-header'))
|
||||||
|
.then(fillOutSignUp(email, PASSWORD))
|
||||||
|
.then(testElementExists('#fxa-confirm-header'))
|
||||||
|
.then(openVerificationLinkInSameTab(email, 0))
|
||||||
|
.then(testElementExists('#fxa-settings-header'))
|
||||||
|
.then(click('#emails .settings-unit-stub button'))
|
||||||
|
|
||||||
|
// attempt to the same email as primary
|
||||||
|
.then(type('.new-email', email))
|
||||||
|
.then(click('.email-add:not(.disabled)'))
|
||||||
|
.then(visibleByQSA('.tooltip'))
|
||||||
|
|
||||||
|
// add secondary email, resend and remove
|
||||||
|
.then(type('.new-email', TestHelpers.createEmail()))
|
||||||
|
.then(click('.email-add:not(.disabled)'))
|
||||||
|
.then(testElementExists('.not-verified'))
|
||||||
|
.then(click('.email-disconnect'))
|
||||||
|
|
||||||
|
// add secondary email, verify
|
||||||
|
.then(type('.new-email', secondaryEmail))
|
||||||
|
.then(click('.email-add:not(.disabled)'))
|
||||||
|
.then(testElementExists('.not-verified'))
|
||||||
|
.then(openVerificationLinkInSameTab(secondaryEmail, 0))
|
||||||
|
|
||||||
|
.then(click('#emails .settings-unit-stub button'))
|
||||||
|
|
||||||
|
.then(testElementTextEquals('#emails .address', secondaryEmail))
|
||||||
|
.then(testElementExists('.verified'))
|
||||||
|
|
||||||
|
// sign out, try to sign in with secondary
|
||||||
|
.then(click('#signout'))
|
||||||
|
.then(testElementExists('#fxa-signin-header'))
|
||||||
|
.then(fillOutSignIn(secondaryEmail, PASSWORD))
|
||||||
|
.then(testErrorTextInclude('primary account email required'))
|
||||||
|
|
||||||
|
// try to reset with secondary email
|
||||||
|
.then(fillOutResetPassword(secondaryEmail, PASSWORD))
|
||||||
|
.then(testErrorTextInclude('primary account email required'))
|
||||||
|
|
||||||
|
// make sure sign in still works
|
||||||
|
.then(fillOutSignIn(email, PASSWORD));
|
||||||
|
},
|
||||||
|
|
||||||
|
'add secondary email that is primary to another account': function () {
|
||||||
|
const existingUnverified = TestHelpers.createEmail();
|
||||||
|
const existingVerified = TestHelpers.createEmail();
|
||||||
|
const unverifiedAccountEmail = TestHelpers.createEmail();
|
||||||
|
|
||||||
|
return this.remote
|
||||||
|
// create an unverified and verified accounts
|
||||||
|
// these are going to be tried as a secondary emails for another account
|
||||||
|
.then(createUser(existingUnverified, PASSWORD, { preVerified: false }))
|
||||||
|
.then(createUser(existingVerified, PASSWORD, { preVerified: true }))
|
||||||
|
|
||||||
|
.then(openPage(SIGNUP_URL, '#fxa-signup-header'))
|
||||||
|
.then(fillOutSignUp(unverifiedAccountEmail, PASSWORD))
|
||||||
|
.then(testElementExists('#fxa-confirm-header'))
|
||||||
|
|
||||||
|
// sign up and verify
|
||||||
|
.then(openPage(SIGNUP_URL, '#fxa-signup-header'))
|
||||||
|
.then(fillOutSignUp(email, PASSWORD))
|
||||||
|
.then(testElementExists('#fxa-confirm-header'))
|
||||||
|
.then(openVerificationLinkInSameTab(email, 0))
|
||||||
|
.then(click('#emails .settings-unit-stub button'))
|
||||||
|
.then(type('.new-email', unverifiedAccountEmail))
|
||||||
|
.then(click('.email-add:not(.disabled)'))
|
||||||
|
.then(visibleByQSA('.tooltip'));
|
||||||
|
},
|
||||||
|
|
||||||
|
'signin and signup with existing secondary email': function () {
|
||||||
|
return this.remote
|
||||||
|
// sign up via the UI, we need a verified session to use secondary email
|
||||||
|
.then(openPage(SIGNUP_URL, '#fxa-signup-header'))
|
||||||
|
.then(fillOutSignUp(email, PASSWORD))
|
||||||
|
.then(testElementExists('#fxa-confirm-header'))
|
||||||
|
.then(openVerificationLinkInSameTab(email, 0))
|
||||||
|
.then(testElementExists('#fxa-settings-header'))
|
||||||
|
.then(click('#emails .settings-unit-stub button'))
|
||||||
|
|
||||||
|
.then(type('.new-email', secondaryEmail))
|
||||||
|
.then(click('.email-add:not(.disabled)'))
|
||||||
|
.then(testElementExists('.not-verified'))
|
||||||
|
.then(openVerificationLinkInSameTab(secondaryEmail, 0))
|
||||||
|
|
||||||
|
.then(click('#emails .settings-unit-stub button'))
|
||||||
|
.then(testElementExists('.verified'))
|
||||||
|
.then(click('#signout'))
|
||||||
|
.then(testElementExists('#fxa-signin-header'))
|
||||||
|
// try to signin with the secondary email
|
||||||
|
.then(fillOutSignIn(secondaryEmail, PASSWORD))
|
||||||
|
.then(testErrorTextInclude('Primary account email required'))
|
||||||
|
// try to signup with the secondary email
|
||||||
|
.then(openPage(SIGNUP_URL, '#fxa-signup-header'))
|
||||||
|
.then(fillOutSignUp(email, PASSWORD))
|
||||||
|
.then(testElementExists('#fxa-settings-content'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -8,6 +8,7 @@ define([
|
||||||
], function (intern, selectCircleTests) {
|
], function (intern, selectCircleTests) {
|
||||||
|
|
||||||
intern.functionalSuites = selectCircleTests([
|
intern.functionalSuites = selectCircleTests([
|
||||||
|
'tests/functional/settings_secondary_emails.js',
|
||||||
// flaky tests go above here.
|
// flaky tests go above here.
|
||||||
'tests/functional/avatar',
|
'tests/functional/avatar',
|
||||||
'tests/functional/back_button_after_start',
|
'tests/functional/back_button_after_start',
|
||||||
|
|
Загрузка…
Ссылка в новой задаче