diff --git a/packages/fxa-content-server/app/scripts/lib/fxa-client.js b/packages/fxa-content-server/app/scripts/lib/fxa-client.js index 43857d7f9f4..65596563704 100644 --- a/packages/fxa-content-server/app/scripts/lib/fxa-client.js +++ b/packages/fxa-content-server/app/scripts/lib/fxa-client.js @@ -1390,6 +1390,13 @@ FxaClientWrapper.prototype = { }); } ), + + createPassword: withClient( + (client, token, email, password) => { + return client + .createPassword(token, email, password) + } + ), }; export default FxaClientWrapper; diff --git a/packages/fxa-content-server/app/scripts/lib/router.js b/packages/fxa-content-server/app/scripts/lib/router.js index 28ac72e948b..0bbfe7d6625 100644 --- a/packages/fxa-content-server/app/scripts/lib/router.js +++ b/packages/fxa-content-server/app/scripts/lib/router.js @@ -36,6 +36,7 @@ import SignInTotpCodeView from '../views/sign_in_totp_code'; import SignInUnblockView from '../views/sign_in_unblock'; import SignUpPasswordView from '../views/sign_up_password'; import ThirdPartyAuthCallbackView from '../views/post_verify/third_party_auth/callback'; +import ThirdPartyAuthSetPasswordView from '../views/post_verify/third_party_auth/set_password'; import Storage from './storage'; import SubscriptionsProductRedirectView from '../views/subscriptions_product_redirect'; import SubscriptionsManagementRedirectView from '../views/subscriptions_management_redirect'; @@ -302,7 +303,13 @@ Router = Router.extend({ ThirdPartyAuthCallbackView ); }, - + 'post_verify/third_party_auth/set_password(/)': function () { + this.createReactOrBackboneViewHandler( + 'post_verify/third_party_auth/set_password', + ThirdPartyAuthSetPasswordView + ); + }, + 'push/confirm_login(/)': createViewHandler('push/confirm_login'), 'push/send_login(/)': createViewHandler('push/send_login'), 'push/completed(/)': createViewHandler('push/completed'), diff --git a/packages/fxa-content-server/app/scripts/models/account.js b/packages/fxa-content-server/app/scripts/models/account.js index 6c1a023143f..66df61ce4bd 100644 --- a/packages/fxa-content-server/app/scripts/models/account.js +++ b/packages/fxa-content-server/app/scripts/models/account.js @@ -1112,6 +1112,10 @@ const Account = Backbone.Model.extend( .then(this.set.bind(this)); }, + createPassword(email, password) { + return this._fxaClient.createPassword(this.get('sessionToken'), email, password); + }, + /** * Fetch the account's list of attached clients. * diff --git a/packages/fxa-content-server/app/scripts/templates/post_verify/third_party_auth/set_password.mustache b/packages/fxa-content-server/app/scripts/templates/post_verify/third_party_auth/set_password.mustache new file mode 100644 index 00000000000..eb521601eee --- /dev/null +++ b/packages/fxa-content-server/app/scripts/templates/post_verify/third_party_auth/set_password.mustache @@ -0,0 +1,54 @@ +
+
+

+ {{#t}}Set your password{{/t}} +

+
+ +
+
+
+ +
+

{{ email }}

+ +

+ {{#t}}Please create a password to continue to Firefox Sync. Your data is encrypted with your password to protect your privacy.{{/t}} +

+ +
+ +
+
+ +
+ + + +
+ +
+

+ {{#t}}Choose what to sync{{/t}} +

+
+
+ {{#engines}} +
+ +
+ {{/engines}} +
+ +
+ +
+
+
+
diff --git a/packages/fxa-content-server/app/scripts/views/mixins/third-party-auth-mixin.js b/packages/fxa-content-server/app/scripts/views/mixins/third-party-auth-mixin.js index e0b0bfc4810..229e393d2cc 100644 --- a/packages/fxa-content-server/app/scripts/views/mixins/third-party-auth-mixin.js +++ b/packages/fxa-content-server/app/scripts/views/mixins/third-party-auth-mixin.js @@ -206,6 +206,13 @@ export default { this.logFlowEvent(`${provider}.signin-complete`); this.metrics.flush(); + + // Sync service requires a password to be set before it can be used. + // Note that once a password is set, the user will not have an option to use + // third party login for Sync since it always requires a password. + if (this.relier.isSync()) { + return this.navigate('/post_verify/third_party_auth/set_password', { provider }); + } return this.signIn(updatedAccount); }) diff --git a/packages/fxa-content-server/app/scripts/views/post_verify/third_party_auth/set_password.js b/packages/fxa-content-server/app/scripts/views/post_verify/third_party_auth/set_password.js new file mode 100644 index 00000000000..69d248e59c2 --- /dev/null +++ b/packages/fxa-content-server/app/scripts/views/post_verify/third_party_auth/set_password.js @@ -0,0 +1,87 @@ +/* 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/. */ + +import AuthErrors from '../../../lib/auth-errors'; +import Cocktail from '../../../lib/cocktail'; +import Template from '../../../templates/post_verify/third_party_auth/set_password.mustache'; +import FormView from '../../form'; +import FlowEventsMixin from '../../mixins/flow-events-mixin'; +import PasswordMixin from '../../mixins/password-mixin'; +import PasswordStrengthMixin from '../../mixins/password-strength-mixin'; +import CWTSOnSignupPasswordMixin from '../../mixins/cwts-on-signup-password'; +import ServiceMixin from '../../mixins/service-mixin'; +import AccountSuggestionMixin from '../../mixins/account-suggestion-mixin'; +import SigninMixin from '../../mixins/signin-mixin'; + +const PASSWORD_INPUT_SELECTOR = '#password'; +const VPASSWORD_INPUT_SELECTOR = '#vpassword'; + +class SetPassword extends FormView { + template = Template; + + _getPassword() { + return this.$(PASSWORD_INPUT_SELECTOR).val(); + } + + _getVPassword() { + return this.$(VPASSWORD_INPUT_SELECTOR).val(); + } + + isValidEnd() { + return this._getPassword() === this._getVPassword(); + } + + showValidationErrorsEnd() { + if (this._getPassword() !== this._getVPassword()) { + const err = AuthErrors.toError('PASSWORDS_DO_NOT_MATCH'); + this.showValidationError(this.$(PASSWORD_INPUT_SELECTOR), err, true); + } + } + + getAccount() { + return this.getSignedInAccount(); + } + + beforeRender() { + const account = this.getSignedInAccount(); + if (account.isDefault()) { + return this.replaceCurrentPage('/'); + } + } + + setInitialContext(context) { + const email = this.getAccount().get('email'); + context.set({ + email, + }); + } + + submit() { + const account = this.getAccount(); + const password = this._getPassword(); + + return account.createPassword(account.get('email'), password) + .then(() => { + // After the user has set a password, initiated the standard Sync + // login flow with the password they set. + return this.signIn(account, password); + }); + } +} + +Cocktail.mixin( + SetPassword, + PasswordMixin, + CWTSOnSignupPasswordMixin, + PasswordStrengthMixin({ + balloonEl: '#password-strength-balloon-container', + passwordEl: PASSWORD_INPUT_SELECTOR, + }), + ServiceMixin, + AccountSuggestionMixin, + FlowEventsMixin, + SigninMixin +); + +export default SetPassword; diff --git a/packages/fxa-content-server/app/tests/spec/views/post_verify/third_party_auth/set_password.js b/packages/fxa-content-server/app/tests/spec/views/post_verify/third_party_auth/set_password.js new file mode 100644 index 00000000000..65995b3c75e --- /dev/null +++ b/packages/fxa-content-server/app/tests/spec/views/post_verify/third_party_auth/set_password.js @@ -0,0 +1,147 @@ +/* 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/. */ + +import _ from 'underscore'; +import { assert } from 'chai'; +import Account from 'models/account'; +import Backbone from 'backbone'; +import BaseBroker from 'models/auth_brokers/base'; +import Metrics from 'lib/metrics'; +import Relier from 'models/reliers/relier'; +import SentryMetrics from 'lib/sentry'; +import sinon from 'sinon'; +import User from 'models/user'; +import View from 'views/post_verify/third_party_auth/set_password'; +import WindowMock from '../../../../mocks/window'; +import $ from 'jquery'; + +const PASSWORD_INPUT_SELECTOR = '#password'; +const VPASSWORD_INPUT_SELECTOR = '#vpassword'; +const PASSWORD = 'passwordzxcv'; + +describe('views/post_verify/third_party_auth/set_password', () => { + let account; + let broker; + let metrics; + let model; + let notifier; + let relier; + let sentryMetrics; + let user; + let view; + let windowMock; + + beforeEach(() => { + windowMock = new WindowMock(); + relier = new Relier({ + window: windowMock, + }); + broker = new BaseBroker({ + relier, + window: windowMock, + }); + account = new Account({ + email: 'a@a.com', + uid: 'uid', + }); + model = new Backbone.Model({ + account, + }); + notifier = _.extend({}, Backbone.Events); + sentryMetrics = new SentryMetrics(); + metrics = new Metrics({ notifier, sentryMetrics }); + user = new User(); + view = new View({ + broker, + metrics, + model, + notifier, + relier, + user, + }); + + sinon.stub(view, 'getSignedInAccount').callsFake(() => account); + sinon + .stub(account, 'createPassword') + .callsFake(() => Promise.resolve()); + sinon + .stub(view, 'signIn') + .callsFake(() => Promise.resolve()); + + return view.render().then(() => $('#container').html(view.$el)); + }); + + afterEach(function () { + view.remove(); + view.destroy(); + }); + + describe('render', () => { + it('renders the view', () => { + assert.lengthOf(view.$('#fxa-signup-password-header'), 1); + assert.include(view.$('#prefillEmail').text(), 'a@a.com'); + assert.lengthOf(view.$('#fxa-choose-what-to-sync-header'), 1); + assert.lengthOf(view.$('#submit-btn'), 1); + }); + + describe('without an account', () => { + beforeEach(() => { + account = new Account({}); + sinon.spy(view, 'navigate'); + return view.render(); + }); + + it('redirects to the email first page', () => { + assert.isTrue(view.navigate.calledWith('/')); + }); + }); + }); + + describe('validateAndSubmit', () => { + beforeEach(() => { + sinon.stub(view, 'submit').callsFake(() => Promise.resolve()); + sinon.spy(view, 'showValidationError'); + }); + + describe('with invalid password', () => { + beforeEach(() => { + view.$(PASSWORD_INPUT_SELECTOR).val(''); + return view.validateAndSubmit().then(assert.fail, () => {}); + }); + + it('displays a tooltip, does not call submit', () => { + assert.isTrue(view.showValidationError.called); + assert.isFalse(view.submit.called); + }); + }); + + describe('with valid password', () => { + beforeEach(() => { + view.$(PASSWORD_INPUT_SELECTOR).val(PASSWORD); + view.$(VPASSWORD_INPUT_SELECTOR).val(PASSWORD); + return view.validateAndSubmit(); + }); + + it('calls submit', () => { + assert.equal(view.submit.callCount, 1); + }); + }); + }); + + describe('submit', () => { + describe('success', () => { + beforeEach(() => { + sinon.spy(view, 'navigate'); + view.$(PASSWORD_INPUT_SELECTOR).val(PASSWORD); + view.$(VPASSWORD_INPUT_SELECTOR).val(PASSWORD); + return view.submit(); + }); + + it('calls correct methods', () => { + assert.isTrue(account.createPassword.calledWith('a@a.com', PASSWORD),); + assert.isTrue(view.signIn.calledWith(account, PASSWORD,)); + }); + }); + }); +}); diff --git a/packages/fxa-content-server/app/tests/test_start.js b/packages/fxa-content-server/app/tests/test_start.js index de39954ecd7..bf70d439d7a 100644 --- a/packages/fxa-content-server/app/tests/test_start.js +++ b/packages/fxa-content-server/app/tests/test_start.js @@ -232,6 +232,7 @@ require('./spec/views/post_verify/newsletters/add_newsletters'); require('./spec/views/post_verify/password/force_password_change'); require('./spec/views/post_verify/secondary_email/add_secondary_email'); require('./spec/views/post_verify/secondary_email/confirm_secondary_email'); +require('./spec/views/post_verify/third_party_auth/set_password'); require('./spec/views/progress_indicator'); require('./spec/views/push/confirm_login'); require('./spec/views/push/send_login'); diff --git a/packages/fxa-content-server/server/lib/routes/react-app/content-server-routes.js b/packages/fxa-content-server/server/lib/routes/react-app/content-server-routes.js index 8c43fa61ad4..8ae5d1a36fc 100644 --- a/packages/fxa-content-server/server/lib/routes/react-app/content-server-routes.js +++ b/packages/fxa-content-server/server/lib/routes/react-app/content-server-routes.js @@ -53,6 +53,7 @@ const FRONTEND_ROUTES = [ 'post_verify/secondary_email/confirm_secondary_email', 'post_verify/secondary_email/verified_secondary_email', 'post_verify/third_party_auth/callback', + 'post_verify/third_party_auth/set_password', 'primary_email_verified', 'push/completed', 'push/confirm_login',