Skip to content

Commit

Permalink
Merge pull request #2263 from mozilla/feature.signupCodes2, r=@philbooth
Browse files Browse the repository at this point in the history


feat(codes): Add the ux for signup codes
  • Loading branch information
vbudhram authored Aug 28, 2019
2 parents d53611f + 37929e1 commit 953231c
Show file tree
Hide file tree
Showing 29 changed files with 974 additions and 13 deletions.
5 changes: 5 additions & 0 deletions packages/fxa-auth-server/lib/routes/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ module.exports = (
.regex(HEX_STRING)
.optional(),
authAt: isA.number().integer(),
verificationMethod: validators.verificationMethod.optional(),
},
},
},
Expand Down Expand Up @@ -446,6 +447,10 @@ module.exports = (
response.keyFetchToken = keyFetchToken.data;
}

if (verificationMethod) {
response.verificationMethod = verificationMethod;
}

return response;
}
},
Expand Down
6 changes: 5 additions & 1 deletion packages/fxa-auth-server/test/local/routes/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,7 @@ describe('/account/create', () => {

mockRequest.payload.verificationMethod = 'email-otp';

await runTest(route, mockRequest, () => {
await runTest(route, mockRequest, res => {
assert.calledOnce(mockMailer.sendVerifyShortCode);

const authenticator = new otplib.authenticator.Authenticator();
Expand All @@ -972,6 +972,10 @@ describe('/account/create', () => {
mockRequest.app.geo.location,
'location set'
);
assert.equal(
res.verificationMethod,
mockRequest.payload.verificationMethod
);
});
});
});
Expand Down
5 changes: 4 additions & 1 deletion packages/fxa-auth-server/test/mail_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ module.exports = printLogs => {
const rul = mail.headers['x-report-signin-link'];
const uc = mail.headers['x-unblock-code'];
const vc = mail.headers['x-verify-code'];
const vsc = mail.headers['x-verify-short-code'];
const sc = mail.headers['x-signin-verify-code'];
const template = mail.headers['x-template-name'];

Expand All @@ -56,7 +57,9 @@ module.exports = printLogs => {
// See: https://github.com/mozilla/fxa-content-server/pull/6470#issuecomment-415224438
const name = emailName(mail.headers.to.replace(/\<(.*?)\>/g, '$1'));

if (vc) {
if (vsc) {
console.log('\x1B[34mSignin code', vsc, '\x1B[39m');
} else if (vc) {
console.log('\x1B[32m', link, '\x1B[39m');
} else if (sc) {
console.log('\x1B[32mToken code: ', sc, '\x1B[39m');
Expand Down
3 changes: 2 additions & 1 deletion packages/fxa-auth-server/test/mailbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ module.exports = function(host, port, printLogs) {
return waitForEmail(email).then(emailData => {
const code =
emailData.headers['x-verify-code'] ||
emailData.headers['x-recovery-code'];
emailData.headers['x-recovery-code'] ||
emailData.headers['x-verify-short-code'];
if (!code) {
throw new Error('email did not contain a verification code');
}
Expand Down
4 changes: 4 additions & 0 deletions packages/fxa-content-server/app/scripts/lib/auth-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ var ERRORS = {
errno: 181,
message: t('Update was rejected, please try again'),
},
INVALID_EXPIRED_SIGNUP_CODE: {
errno: 182,
message: t('Expired or invalid signup code'),
},
SERVER_BUSY: {
errno: 201,
message: t('Server busy, try again soon'),
Expand Down
2 changes: 2 additions & 0 deletions packages/fxa-content-server/app/scripts/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,7 @@ module.exports = {

// https://stripe.com/docs/error-codes#expired-card
CC_EXPIRED: 'expired_card',

SIGNUP_CODE_LENGTH: 6,
};
/*eslint-enable sorting/sort-object-props*/
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const experimentGroupingRules = [
require('./send-sms-install-link'),
require('./sentry'),
require('./token-code'),
require('./signup-code'),
].map(ExperimentGroupingRule => new ExperimentGroupingRule());

class ExperimentChoiceIndex {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* 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';

const BaseGroupingRule = require('./base');
const Constants = require('../../../lib/constants');
const GROUPS_DEFAULT = ['control, treatment'];

const ROLLOUT_CLIENTS = {
'37fdfa37698f251a': {
enableTestEmails: false,
groups: GROUPS_DEFAULT,
name: 'Lockbox Extension',
rolloutRate: 0.0,
},
'3c49430b43dfba77': {
enableTestEmails: false,
groups: GROUPS_DEFAULT,
name: 'Android Components Reference Browser',
rolloutRate: 0.0,
},
'98adfa37698f255b': {
enableTestEmails: true,
groups: GROUPS_DEFAULT,
name: 'Lockbox Extension iOS',
rolloutRate: 0.0,
},
ecdb5ae7add825d4: {
enableTestEmails: false,
groups: GROUPS_DEFAULT,
name: 'TestClient',
rolloutRate: 0.0,
},
};

module.exports = class SignupCodeGroupingRule extends BaseGroupingRule {
constructor() {
super();
this.name = 'signupCode';
this.SYNC_ROLLOUT_RATE = 0.0;
this.ROLLOUT_CLIENTS = ROLLOUT_CLIENTS;
}

choose(subject) {
if (
!subject ||
!subject.uniqueUserId ||
!subject.experimentGroupingRules ||
!subject.isSignupCodeSupported ||
!subject.account
) {
return false;
}

const { featureFlags } = subject;

if (subject.clientId) {
let client = this.ROLLOUT_CLIENTS[subject.clientId];
if (featureFlags && featureFlags.signupCodeClients) {
client = featureFlags.signupCodeClients[subject.clientId];
}

if (client) {
const groups = client.groups || GROUPS_DEFAULT;

// Check if this client supports test emails
if (
client.enableTestEmails &&
this.isTestEmail(subject.account.get('email'))
) {
return this.uniformChoice(groups, subject.uniqueUserId);
}

if (this.bernoulliTrial(client.rolloutRate, subject.uniqueUserId)) {
return this.uniformChoice(groups, subject.uniqueUserId);
}
}

// If a clientId was specified but not defined in the rollout configuration, the default
// is to disable the experiment for them.
return false;
}

if (subject.service && subject.service === Constants.SYNC_SERVICE) {
let syncRolloutRate = this.SYNC_ROLLOUT_RATE;
if (featureFlags && featureFlags.signupCodeClients) {
syncRolloutRate = featureFlags.signupCodeClients.sync.rolloutRate;
}

if (this.bernoulliTrial(syncRolloutRate, subject.uniqueUserId)) {
return this.uniformChoice(GROUPS_DEFAULT, subject.uniqueUserId);
}
}

return false;
}
};
21 changes: 21 additions & 0 deletions packages/fxa-content-server/app/scripts/lib/fxa-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,10 @@ FxaClientWrapper.prototype = {
signUpOptions.resume = options.resume;
}

if (options.verificationMethod) {
signUpOptions.verificationMethod = options.verificationMethod;
}

if (relier.has('style')) {
signUpOptions.style = relier.get('style');
}
Expand Down Expand Up @@ -712,6 +716,23 @@ FxaClientWrapper.prototype = {
*/
sessionStatus: createClientDelegate('sessionStatus'),

/**
* Verify an account and a session using a otp based code.
*
* @param {String} sessionToken User session token
* @param {String} code Code to verify account and session
* @return {Promise} resolves when complete
*/
sessionVerifyCode: createClientDelegate('sessionVerifyCode'),

/**
* Resend the verify code based on otp.
*
* @param {String} sessionToken User session token
* @return {Promise} resolves when complete
*/
sessionResendVerifyCode: createClientDelegate('sessionResendVerifyCode'),

/**
* Check if `sessionToken` is valid
*
Expand Down
2 changes: 2 additions & 0 deletions packages/fxa-content-server/app/scripts/lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import CompleteResetPasswordView from '../views/complete_reset_password';
import CompleteSignUpView from '../views/complete_sign_up';
import ConfirmResetPasswordView from '../views/confirm_reset_password';
import ConfirmView from '../views/confirm';
import ConfirmSignupCodeView from '../views/confirm_signup_code';
import ConnectAnotherDeviceView from '../views/connect_another_device';
import CookiesDisabledView from '../views/cookies_disabled';
import DeleteAccountView from '../views/settings/delete_account';
Expand Down Expand Up @@ -119,6 +120,7 @@ const Router = Backbone.Router.extend({
'confirm_signin(/)': createViewHandler(ConfirmView, {
type: VerificationReasons.SIGN_IN,
}),
'confirm_signup_code(/)': createViewHandler(ConfirmSignupCodeView),
'connect_another_device(/)': createViewHandler(ConnectAnotherDeviceView),
'connect_another_device/why(/)': createChildViewHandler(
WhyConnectAnotherDeviceView,
Expand Down
32 changes: 32 additions & 0 deletions packages/fxa-content-server/app/scripts/models/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ const Account = Backbone.Model.extend(
.signUp(this.get('email'), password, relier, {
metricsContext: this._metrics.getFlowEventMetadata(),
resume: options.resume,
verificationMethod: options.verificationMethod,
})
.then(updatedSessionData => {
this.set(updatedSessionData);
Expand Down Expand Up @@ -771,6 +772,37 @@ const Account = Backbone.Model.extend(
});
},

/**
* Verify the session and account using the verification code.
*
* @param {String} code - the verification code
* @param {Object} [options]
* @param {Object} [options.service] - the service issuing signup request
* @returns {Promise} - resolves when complete
*/
verifySessionCode(code, options = {}) {
const newsletters = this.get('newsletters');
if (newsletters && newsletters.length) {
this.unset('newsletters');
options.newsletters = newsletters;
}

return this._fxaClient.sessionVerifyCode(
this.get('sessionToken'),
code,
options
);
},

/**
* Resend the session and account verification code.
*
* @returns {Promise} - resolves when complete
*/
verifySessionResendCode() {
return this._fxaClient.sessionResendVerifyCode(this.get('sessionToken'));
},

/**
* Verify the account using the token code
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,10 @@ const BaseAuthenticationBroker = Backbone.Model.extend({
* security events will be shown with `&security_events=true` in the url
*/
showSecurityEvents: false,
/*
* Is using signup codes supported?
*/
signupCode: true,
/**
* Does this environment support pairing?
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div id="main-content" class="card confirm-signup">
<header>
{{#isTrailhead}}
<div class="step step-3"></div>
{{/isTrailhead}}
<h1 id="fxa-confirm-signup-code-header">
{{#serviceName}}
<!-- L10N: For languages structured like English, the second phrase can read "to continue to %(serviceName)s" -->
{{#t}}Enter verification code{{/t}} <span class="service">{{#t}}Continue to %(serviceName)s{{/t}}</span>
{{/serviceName}}
{{^serviceName}}
{{#t}}Enter verification code{{/t}}
{{/serviceName}}
</h1>
</header>

<section>
<div class="error"></div>
<div class="success"></div>

<div class="graphic graphic-mail"></div>

<p class="verification-email-message">{{#unsafeTranslate}}Please enter the verification code that was sent to %(escapedEmail)s within 20 minutes.{{/unsafeTranslate}}</p>
<form novalidate>
<div class="input-row">
<input type="number" pattern="\d*" class="tooltip-below token-code" placeholder="{{#t}}Enter 6-digit code{{/t}}" required autofocus />
</div>
<div class="button-row">
<button id="submit-btn" type="submit">{{#t}}Verify{{/t}}</button>
</div>
<div class="links">
<a id="resend" class="left delayed-fadein" href="#">{{#t}}Not in inbox or spam folder? Resend{{/t}}</a>
</div>
</form>
</section>
</div>
Loading

0 comments on commit 953231c

Please sign in to comment.