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 @@
+
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',