diff --git a/resources/static/css/style.css b/resources/static/css/style.css index 817012bcab516e04c2772cf28210079b5225cb45..951553961de674231e3dee2f3b05e00b00674d2f 100644 --- a/resources/static/css/style.css +++ b/resources/static/css/style.css @@ -649,14 +649,18 @@ h1 { border-radius: 3px; } -#signUpForm > .password_entry { - display: none; +#signUpForm > .siteinfo { + margin-bottom: 10px; } -.siteinfo, #congrats { +.siteinfo, #congrats, #signUpForm > .password_entry, .enter_password .hint { display: none; } +.enter_password #signUpForm > .password_entry { + display: block; +} + #congrats p { color: #62615F; text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5); diff --git a/resources/static/dialog/controllers/page.js b/resources/static/dialog/controllers/page.js index 8f29623cdfbb3b22ba7ea26fe16f0956bd3d49a2..08a3711b5fdde34cbf242c50d4707a8a47f881a3 100644 --- a/resources/static/dialog/controllers/page.js +++ b/resources/static/dialog/controllers/page.js @@ -75,6 +75,15 @@ BrowserID.Modules.PageModule = (function() { } }, + checkRequired: function(options) { + var list = [].slice.call(arguments, 1); + for(var item, index = 0; item = list[index]; ++index) { + if(!options.hasOwnProperty(item)) { + throw "missing config option: " + item; + } + } + }, + start: function(options) { var self=this; self.bind("form", "submit", onSubmit); diff --git a/resources/static/pages/add_email_address.js b/resources/static/pages/add_email_address.js index d14aa7befac874cf5d338d39d38f2587bd877ab6..c9b5238f5c7e8e950f003ed8847d493ad93030c7 100644 --- a/resources/static/pages/add_email_address.js +++ b/resources/static/pages/add_email_address.js @@ -34,47 +34,124 @@ * * ***** END LICENSE BLOCK ***** */ -(function() { +BrowserID.addEmailAddress = (function() { "use strict"; var ANIMATION_TIME=250, bid = BrowserID, - dom = bid.DOM; + user = bid.User, + storage = bid.Storage, + errors = bid.Errors, + pageHelpers = bid.PageHelpers, + dom = bid.DOM, + token, + sc; - function emailRegistrationSuccess(info) { + function showError(el) { + $(".hint,#signUpForm").hide(); + $(el).fadeIn(ANIMATION_TIME); + } + + function emailRegistrationComplete(oncomplete, info) { + var valid = info.valid; + if (valid) { + emailRegistrationSuccess(info); + } + else { + showError("#cannotconfirm"); + } + oncomplete && oncomplete(valid); + } - dom.setInner("#email", info.email); + function showRegistrationInfo(info) { + dom.setInner(".email", info.email); if (info.origin) { dom.setInner(".website", info.origin); $(".siteinfo").show(); } + } + + function emailRegistrationSuccess(info) { + dom.addClass("body", "complete"); + + showRegistrationInfo(info); - $("#signUpForm").delay(2000).fadeOut(ANIMATION_TIME, function() { - $("#congrats").fadeIn(ANIMATION_TIME); - }); + setTimeout(function() { + pageHelpers.replaceFormWithNotice("#congrats"); + }, 2000); } - function showError(el) { - $(".hint").hide(); - $(el).fadeIn(ANIMATION_TIME); + function userMustEnterPassword() { + var emails = storage.getEmails(), + length = 0, + anySecondaries = _.find(emails, function(item) { length++; return item.type === "secondary"; }); + + return length && !anySecondaries; + } + + function verifyWithoutPassword(oncomplete) { + user.verifyEmailNoPassword(token, + emailRegistrationComplete.bind(null, oncomplete), + pageHelpers.getFailure(errors.verifyEmail, oncomplete) + ); + } + + function verifyWithPassword(oncomplete) { + var pass = dom.getInner("#password"), + vpass = dom.getInner("#vpassword"), + valid = bid.Validation.passwordAndValidationPassword(pass, vpass); + + if(valid) { + user.verifyEmailWithPassword(token, pass, + emailRegistrationComplete.bind(null, oncomplete), + pageHelpers.getFailure(errors.verifyEmail, oncomplete) + ); + } + else { + oncomplete && oncomplete(false); + } } - BrowserID.addEmailAddress = function(token, oncomplete) { - var user = BrowserID.User; + function startVerification(oncomplete) { + user.tokenInfo(token, function(info) { + if(info) { + showRegistrationInfo(info); - user.verifyEmail(token, function onSuccess(info) { - if (info.valid) { - emailRegistrationSuccess(info); + if(userMustEnterPassword()) { + dom.addClass("body", "enter_password"); + oncomplete(true); + } + else { + verifyWithoutPassword(oncomplete); + } } else { showError("#cannotconfirm"); + oncomplete(false); + } + }, pageHelpers.getFailure(errors.getTokenInfo, oncomplete)); + } + + var Module = bid.Modules.PageModule.extend({ + start: function(options) { + function oncomplete(status) { + options.ready && options.ready(status); } - oncomplete && oncomplete(); - }, function onFailure() { - // XXX This should use a real error page. - showError("#cannotcommunicate"); - oncomplete && oncomplete(); - }); - }; + + this.checkRequired(options, "token"); + + token = options.token; + + startVerification(oncomplete); + + sc.start.call(this, options); + }, + + submit: verifyWithPassword + }); + + sc = Module.sc; + + return Module; }()); diff --git a/resources/static/pages/browserid.js b/resources/static/pages/start.js similarity index 95% rename from resources/static/pages/browserid.js rename to resources/static/pages/start.js index f872832b3ffed6d259ea5796f3ad8cada5293e06..060d16e6e0b16320d6b6aafe0871189ad4ea3c66 100644 --- a/resources/static/pages/browserid.js +++ b/resources/static/pages/start.js @@ -59,8 +59,11 @@ $(function() { else if (path === "/forgot") { bid.forgot(); } - else if (token && path === "/add_email_address") { - bid.addEmailAddress(token); + else if (path === "/add_email_address") { + var module = bid.addEmailAddress.create(); + module.start({ + token: token + }); } else if(token && path === "/verify_email_address") { bid.verifyEmailAddress(token); diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js index 9f96220519ce985403b2f6fd745c5684d984f9b5..789025acd7c72f919e2f1b81176ec3de6b27f04b 100644 --- a/resources/static/shared/error-messages.js +++ b/resources/static/shared/error-messages.js @@ -150,10 +150,18 @@ BrowserID.Errors = (function(){ title: "Sync Keys for Address" }, + tokenInfo: { + title: "Getting Token Info" + }, + updatePassword: { title: "Updating password" }, + verifyEmail: { + title: "Verifying email address" + }, + xhrError: { title: "Communication Error" } diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js index 56fadfd9980b0c749bd9a7ecf08e7c41507a8796..fcbcc9885cf34d5e3596e2e4d44fe8d3b295a3e7 100644 --- a/resources/static/shared/user.js +++ b/resources/static/shared/user.js @@ -417,31 +417,44 @@ BrowserID.User = (function() { cancelRegistrationPoll(); }, + /** + * Get site and email info for a token + * @method tokenInfo + * @param {string} token + * @param {function} [onComplete] + * @param {function} [onFailure] + */ + tokenInfo: function(token, onComplete, onFailure) { + network.emailForVerificationToken(token, function (email) { + var info = email ? { + email: email, + origin: storage.getStagedOnBehalfOf() + } : null; + + onComplete(info); + }, onFailure); + + }, + /** * Verify a user * @method verifyUser * @param {string} token - token to verify. * @param {string} password - password to set for account. - * @param {function} [onSuccess] - Called to give status updates. + * @param {function} [onComplete] - Called to give status updates. * @param {function} [onFailure] - Called on error. */ - verifyUser: function(token, password, onSuccess, onFailure) { - network.emailForVerificationToken(token, function (email) { + verifyUser: function(token, password, onComplete, onFailure) { + User.tokenInfo(token, function(info) { var invalidInfo = { valid: false }; - if (email) { + if (info) { network.completeUserRegistration(token, password, function (valid) { - var info = valid ? { - valid: valid, - email: email, - origin: storage.getStagedOnBehalfOf() - } : invalidInfo; - + info.valid = valid; storage.setStagedOnBehalfOf(""); - - if (onSuccess) onSuccess(info); + if (onComplete) onComplete(info); }, onFailure); - } else if (onSuccess) { - onSuccess(invalidInfo); + } else if (onComplete) { + onComplete(invalidInfo); } }, onFailure); }, @@ -718,12 +731,12 @@ BrowserID.User = (function() { * Verify a users email address given by the token * @method verifyEmail * @param {string} token - * @param {function} [onSuccess] - Called on success. + * @param {function} [onComplete] - Called on completion. * Called with an object with valid, email, and origin if valid, called * with only valid otw. * @param {function} [onFailure] - Called on error. */ - verifyEmail: function(token, onSuccess, onFailure) { + verifyEmailNoPassword: function(token, onComplete, onFailure) { network.emailForVerificationToken(token, function (email) { var invalidInfo = { valid: false }; if (email) { @@ -736,10 +749,24 @@ BrowserID.User = (function() { storage.setStagedOnBehalfOf(""); - if (onSuccess) onSuccess(info); + if (onComplete) onComplete(info); + }, onFailure); + } else if (onComplete) { + onComplete(invalidInfo); + } + }, onFailure); + }, + + verifyEmailWithPassword: function(token, pass, onComplete, onFailure) { + User.verifyEmailNoPassword(token, function(userInfo) { + var invalidInfo = { valid: false }; + if(userInfo.status !== false) { + User.setPassword(pass, function(status) { + onComplete(status ? userInfo : invalidInfo); }, onFailure); - } else if (onSuccess) { - onSuccess(invalidInfo); + } + else { + onComplete(invalidInfo); } }, onFailure); }, diff --git a/resources/static/test/qunit/controllers/page_unit_test.js b/resources/static/test/qunit/controllers/page_unit_test.js index 012c55d93d3fdd5201f5d7b2310bb96638b2fe1b..3c044c1c4074480e6d4ac8bec7c050599af834a3 100644 --- a/resources/static/test/qunit/controllers/page_unit_test.js +++ b/resources/static/test/qunit/controllers/page_unit_test.js @@ -238,5 +238,9 @@ }); }); + test("checkRequired", function() { + ok(false, "write a test for this"); + }); + }()); diff --git a/resources/static/test/qunit/pages/add_email_address_test.js b/resources/static/test/qunit/pages/add_email_address_test.js index 7f6916c101f223b145b714de4432caf87a41fa8c..656e1ac01ed85adff0fe08a796a4193d73dd1a75 100644 --- a/resources/static/test/qunit/pages/add_email_address_test.js +++ b/resources/static/test/qunit/pages/add_email_address_test.js @@ -38,17 +38,21 @@ "use strict"; var bid = BrowserID, - network = bid.Network, storage = bid.Storage, xhr = bid.Mocks.xhr, + dom = bid.DOM, testHelpers = bid.TestHelpers, - validToken = true; + validToken = true, + controller, + config = { + token: "token" + }; module("pages/add_email_address", { setup: function() { testHelpers.setup(); bid.Renderer.render("#page_head", "site/add_email_address", {}); - $(".siteinfo").hide(); + $(".siteinfo,.password_entry").hide(); }, teardown: function() { testHelpers.teardown(); @@ -56,45 +60,148 @@ } }); - asyncTest("addEmailAddress with good token and site", function() { + function createPrimaryUser() { + storage.addEmail("testuser@testuser.com", { + created: new Date(), + type: "primary" + }); + } + + function createController(options, callback) { + controller = BrowserID.addEmailAddress.create(); + options = options || {}; + options.ready = callback; + controller.start(options); + } + + function expectTooltipVisible() { + createPrimaryUser(); + createController(config, function() { + controller.submit(function() { + testHelpers.testTooltipVisible(); + start(); + }); + }); + } + + function testEmail() { + equal(dom.getInner(".email"), "testuser@testuser.com", "correct email shown"); + } + + function testCannotConfirm() { + ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible"); + } + + test("start with missing token", function() { + var error; + try { + createController({}); + } catch(e) { + error = e; + } + + equal(error, "missing config option: token", "correct error thrown"); + }); + + asyncTest("no password: start with good token and site", function() { storage.setStagedOnBehalfOf("browserid.org"); - bid.addEmailAddress("token", function() { - equal($("#email").val(), "testuser@testuser.com", "email set"); + createController(config, function() { + testEmail(); ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is"); equal($(".website:nth(0)").text(), "browserid.org", "origin is updated"); + equal($("body").hasClass("complete"), true, "body has complete class"); start(); }); }); - asyncTest("addEmailAddress with good token and nosite", function() { - bid.addEmailAddress("token", function() { - equal($("#email").val(), "testuser@testuser.com", "email set"); + asyncTest("no password: start with good token and nosite", function() { + createController(config, function() { + testEmail(); equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it"); equal($(".siteinfo .website").text(), "", "origin is not updated"); start(); }); }); - asyncTest("addEmailAddress with bad token", function() { + asyncTest("no password: start with bad token", function() { xhr.useResult("invalid"); - bid.addEmailAddress("token", function() { - ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible"); + createController(config, function() { + testCannotConfirm(); start(); }); }); - asyncTest("addEmailAddress with emailForVerficationToken XHR failure", function() { + asyncTest("no password: start with emailForVerficationToken XHR failure", function() { xhr.useResult("ajaxError"); - bid.addEmailAddress("token", function() { - ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible"); + createController(config, function() { + testHelpers.testErrorVisible(); start(); }); }); - asyncTest("addEmailAddress - first secondary address added to account with only primaries - must enter password", function() { - start(); + asyncTest("password: first secondary address added", function() { + createPrimaryUser(); + createController(config, function() { + equal($("body").hasClass("enter_password"), true, "enter_password added to body"); + testEmail(); + start(); + }); + }); + + asyncTest("password: missing password", function() { + $("#password").val(); + $("#vpassword").val("password"); + + expectTooltipVisible(); + }); + + asyncTest("password: missing verify password", function() { + $("#password").val("password"); + $("#vpassword").val(); + + expectTooltipVisible(); + }); + + asyncTest("password: too short of a password", function() { + $("#password").val("pass"); + $("#vpassword").val("pass"); + + expectTooltipVisible(); + }); + + asyncTest("password: mismatched passwords", function() { + $("#password").val("passwords"); + $("#vpassword").val("password"); + + expectTooltipVisible(); + }); + + asyncTest("password: good password", function() { + $("#password").val("password"); + $("#vpassword").val("password"); + + createPrimaryUser(); + createController(config, function() { + controller.submit(function(status) { + equal(status, true, "correct status"); + equal($("body").hasClass("complete"), true, "body has complete class"); + start(); + }); + }); + }); + + asyncTest("password: good password bad token", function() { + $("#password").val("password"); + $("#vpassword").val("password"); + + xhr.useResult("invalid"); + createPrimaryUser(); + createController(config, function() { + testCannotConfirm(); + start(); + }); }); }()); diff --git a/resources/static/test/qunit/shared/user_unit_test.js b/resources/static/test/qunit/shared/user_unit_test.js index 54a2e2d0fe258fc1f59b1c325dd422c06f7a0bff..7a8db43257bf88ad7937a5563bd0b96cfdd2a711 100644 --- a/resources/static/test/qunit/shared/user_unit_test.js +++ b/resources/static/test/qunit/shared/user_unit_test.js @@ -317,6 +317,33 @@ var vep = require("./vep"); }, 500); }); + asyncTest("tokenInfo with a good token and origin info, expect origin in results", function() { + storage.setStagedOnBehalfOf(testOrigin); + + lib.tokenInfo("token", function(info) { + equal(info.email, "testuser@testuser.com", "correct email"); + equal(info.origin, testOrigin, "correct origin"); + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("tokenInfo with a bad token without site info, no site in results", function() { + lib.tokenInfo("token", function(info) { + equal(info.email, "testuser@testuser.com", "correct email"); + equal(typeof info.origin, "undefined", "origin is undefined"); + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("tokenInfo with XHR error", function() { + xhr.useResult("ajaxError"); + lib.tokenInfo( + "token", + testHelpers.unexpectedSuccess, + testHelpers.expectedXHRFailure + ); + }); + asyncTest("verifyUser with a good token", function() { storage.setStagedOnBehalfOf(testOrigin); @@ -650,9 +677,9 @@ var vep = require("./vep"); }, 500); }); - asyncTest("verifyEmail with a good token", function() { + asyncTest("verifyEmailNoPassword with a good token", function() { storage.setStagedOnBehalfOf(testOrigin); - lib.verifyEmail("token", function onSuccess(info) { + lib.verifyEmailNoPassword("token", function onSuccess(info) { ok(info.valid, "token was valid"); equal(info.email, "testuser@testuser.com", "email part of info"); @@ -663,27 +690,58 @@ var vep = require("./vep"); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("verifyEmail with a bad token", function() { + asyncTest("verifyEmailNoPassword with a bad token", function() { xhr.useResult("invalid"); - lib.verifyEmail("token", function onSuccess(info) { - + lib.verifyEmailNoPassword("token", function onSuccess(info) { equal(info.valid, false, "bad token calls onSuccess with a false validity"); start(); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("verifyEmail with an XHR failure", function() { + asyncTest("verifyEmailNoPassword with an XHR failure", function() { xhr.useResult("ajaxError"); - lib.verifyEmail("token", function onSuccess(info) { - ok(false, "xhr failure should never succeed"); + lib.verifyEmailNoPassword( + "token", + testHelpers.unexpectedSuccess, + testHelpers.expectedXHRFailure + ); + }); + + asyncTest("verifyEmailWithPassword with a good token", function() { + storage.setStagedOnBehalfOf(testOrigin); + lib.verifyEmailWithPassword("token", "password", function onSuccess(info) { + + ok(info.valid, "token was valid"); + equal(info.email, "testuser@testuser.com", "email part of info"); + equal(info.origin, testOrigin, "origin in info"); + equal(storage.getStagedOnBehalfOf(), "", "initiating origin was removed"); + start(); - }, function() { - ok(true, "xhr failure should always be a failure"); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("verifyEmailWithPassword with a bad token", function() { + xhr.useResult("invalid"); + + lib.verifyEmailWithPassword("token", "password", function onSuccess(info) { + equal(info.valid, false, "bad token calls onSuccess with a false validity"); + start(); - }); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("verifyEmailWithPassword with an XHR failure", function() { + xhr.useResult("ajaxError"); + + lib.verifyEmailWithPassword( + "token", + "password", + testHelpers.unexpectedSuccess, + testHelpers.expectedXHRFailure + ); }); asyncTest("syncEmailKeypair with successful sync", function() { diff --git a/resources/views/add_email_address.ejs b/resources/views/add_email_address.ejs index 36c22156c4b10475295fd76c79515cabe6b85028..92b1c0e47da582e1105d96db1f090d00980ef855 100644 --- a/resources/views/add_email_address.ejs +++ b/resources/views/add_email_address.ejs @@ -2,18 +2,18 @@ <div id="signUpFormWrap"> <ul class="notifications"> <li class="notification error" id="cannotconfirm">Error encountered while attempting to confirm your address. Have you previously verified this address?</li> - <li class="notification error" id="cannotcommunicate">Error comunicating with server.</li> <li class="notification error" id="cannotcomplete">Error encountered trying to complete registration.</li> </ul> <form id="signUpForm" class="cf"> <p class="hint siteinfo">Finish signing into: <strong><span class="website"></span></strong></p> + <h1 class="serif">Email Verification</h1> <ul class="inputs password_entry"> <li> <label class="serif" for="email">Email Address</label> - <input class="youraddress sans" id="email" placeholder="Your Email" type="email" value="" disabled="disabled" maxlength="254" /> + <input class="youraddress sans email" id="email" placeholder="Your Email" type="email" value="" disabled="disabled" maxlength="254" /> </li> <li> <label class="serif" for="password">New Password</label> @@ -45,13 +45,12 @@ <button>Finish</button> </div> - <p class="hint">One moment while we attempt to confirm your email address...</p> </form> <div id="congrats"> <p class="serif"> - <strong id="email">Your address</strong> has been verified! + <strong class="email">Your address</strong> has been verified! <p class="siteinfo"> Your new address is set up and you should now be signed in. diff --git a/resources/views/layout.ejs b/resources/views/layout.ejs index 72a2c3fc955b04c4430d97abd813e572bdef7b31..5e0b43f2697a83f5c048aa136d7caba8ee09c6ba 100644 --- a/resources/views/layout.ejs +++ b/resources/views/layout.ejs @@ -38,10 +38,13 @@ <script src="/shared/tooltip.js" type="text/javascript"></script> <script src="/shared/validation.js" type="text/javascript"></script> <script src="/shared/helpers.js" type="text/javascript"></script> + <script src="/shared/class.js" type="text/javascript"></script> + + <script src="/dialog/controllers/page.js" type="text/javascript"></script> <script src="/pages/page_helpers.js" type="text/javascript"></script> - <script src="/pages/browserid.js" type="text/javascript"></script> <script src="/pages/index.js" type="text/javascript"></script> + <script src="/pages/start.js" type="text/javascript"></script> <script src="/pages/add_email_address.js" type="text/javascript"></script> <script src="/pages/verify_email_address.js" type="text/javascript"></script> <script src="/pages/forgot.js" type="text/javascript"></script> diff --git a/scripts/compress.sh b/scripts/compress.sh index 6c1260f74422590639072f561d67f74285188ebc..d9d6ffd569e37b38820459a1393a8d5a83a81945 100755 --- a/scripts/compress.sh +++ b/scripts/compress.sh @@ -67,7 +67,7 @@ echo '****Building BrowserID.org HTML, CSS, and JS****' echo '' #produce the main site js -cat lib/jquery-1.6.2.min.js lib/json2.js lib/underscore-min.js lib/ejs.js shared/javascript-extensions.js shared/browserid.js lib/dom-jquery.js $BUILD_PATH/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/error-messages.js shared/storage.js shared/network.js shared/provisioning.js shared/user.js shared/tooltip.js shared/validation.js shared/helpers.js pages/page_helpers.js pages/browserid.js pages/index.js pages/add_email_address.js pages/verify_email_address.js pages/forgot.js pages/manage_account.js pages/signin.js pages/signup.js > $BUILD_PATH/browserid.uncompressed.js +cat lib/jquery-1.6.2.min.js lib/json2.js lib/underscore-min.js lib/ejs.js shared/javascript-extensions.js shared/browserid.js lib/dom-jquery.js $BUILD_PATH/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/error-messages.js shared/storage.js shared/network.js shared/provisioning.js shared/user.js shared/tooltip.js shared/validation.js shared/helpers.js shared/class.js dialog/controllers/page.js pages/page_helpers.js pages/index.js pages/start.js pages/add_email_address.js pages/verify_email_address.js pages/forgot.js pages/manage_account.js pages/signin.js pages/signup.js > $BUILD_PATH/browserid.uncompressed.js # produce the main site css cat css/common.css css/style.css css/m.css > $BUILD_PATH/browserid.uncompressed.css