diff --git a/resources/static/pages/js/page_helpers.js b/resources/static/pages/js/page_helpers.js
index 22df8dbbcc5ba89174aa34936d374531f38eafe0..9ec528c6b0103bdf4ebbfbb7bde50f75eb7cb139 100644
--- a/resources/static/pages/js/page_helpers.js
+++ b/resources/static/pages/js/page_helpers.js
@@ -73,26 +73,19 @@ BrowserID.PageHelpers = (function() {
 
   function replaceFormWithNotice(selector, onComplete) {
     $("form").hide();
-    $(selector).fadeIn(ANIMATION_SPEED);
-    // If there is more than one .forminputs, the onComplete callback is called
-    // multiple times, we only want once.
-    onComplete && setTimeout(onComplete, ANIMATION_SPEED);
+    $(selector).fadeIn(ANIMATION_SPEED).promise().done(onComplete);
   }
 
   function replaceInputsWithNotice(selector, onComplete) {
     $('.forminputs').hide();
-    $(selector).stop().hide().css({opacity:1}).fadeIn(ANIMATION_SPEED);
-    // If there is more than one .forminputs, the onComplete callback is called
-    // multiple times, we only want once.
-    onComplete && setTimeout(onComplete, ANIMATION_SPEED);
+    $(selector).stop().hide().css({opacity:1}).fadeIn(ANIMATION_SPEED)
+      .promise().done(onComplete);
   }
 
   function showInputs(onComplete) {
     $('.notification').hide();
-    $('.forminputs').stop().hide().css({opacity:1}).fadeIn(ANIMATION_SPEED);
-    // If there is more than one .forminputs, the onComplete callback is called
-    // multiple times, we only want once.
-    onComplete && setTimeout(onComplete, ANIMATION_SPEED);
+    $('.forminputs').stop().hide().css({opacity:1}).fadeIn(ANIMATION_SPEED)
+      .promise().done(onComplete);
   }
 
   function emailSent(onComplete) {
diff --git a/resources/static/pages/js/verify_secondary_address.js b/resources/static/pages/js/verify_secondary_address.js
index 5b2f7f20051e8bd9c3ce71c181ac75c2cc6c4c69..ee9e2227c2bb35be670dd0962599f849208360b2 100644
--- a/resources/static/pages/js/verify_secondary_address.js
+++ b/resources/static/pages/js/verify_secondary_address.js
@@ -1,4 +1,4 @@
-/*globals BrowserID: true, $:true */
+/*globals BrowserID: true, $:true, URLParse: true */
 /* 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/. */
@@ -26,25 +26,36 @@ BrowserID.verifySecondaryAddress = (function() {
       secondsRemaining = REDIRECT_SECONDS,
       email,
       redirectTo,
-      redirectTimeout;  // set in config if available, use REDIRECT_SECONDS otw.
+      redirectTimeout,  // set in config if available, use REDIRECT_SECONDS otw.
+      uiTimeoutID;
 
   function showRegistrationInfo(info) {
     dom.setInner("#email", info.email);
 
     if (info.returnTo) {
       dom.setInner(".website", info.returnTo);
+      if (uiTimeoutID) uiTimeoutID = clearTimeout(uiTimeoutID);
       updateRedirectTimeout();
       dom.show(".siteinfo");
     }
   }
 
   function updateRedirectTimeout() {
-    if (secondsRemaining > 0) {
-      dom.setInner("#redirectTimeout", secondsRemaining);
+    dom.setInner("#redirectTimeout", secondsRemaining);
+  }
 
-      secondsRemaining--;
-      setTimeout(updateRedirectTimeout, 1000);
+  function countdownTimeout(onComplete) {
+    function checkTime() {
+      if (secondsRemaining > 0) {
+        updateRedirectTimeout();
+        secondsRemaining--;
+        uiTimeoutID = setTimeout(checkTime, 1000);
+      } else {
+        complete(onComplete);
+      }
     }
+
+    checkTime();
   }
 
   function submit(oncomplete) {
@@ -67,10 +78,10 @@ BrowserID.verifySecondaryAddress = (function() {
               // has had a chance to finish its business.
               storage.setLoggedIn(URLParse(redirectTo).originOnly(), email);
 
-              setTimeout(function() {
+              countdownTimeout(function() {
                 doc.location.href = redirectTo;
                 complete(oncomplete, verified);
-              }, redirectTimeout);
+              });
             }
             else {
               complete(oncomplete, verified);
@@ -96,13 +107,13 @@ BrowserID.verifySecondaryAddress = (function() {
   }
 
   function startVerification(oncomplete) {
+    /*jshint validthis: true*/
     var self=this;
     user.tokenInfo(token, function(info) {
       if (info) {
         redirectTo = info.returnTo;
         email = info.email;
         showRegistrationInfo(info);
-
         mustAuth = info.must_auth;
         if (mustAuth) {
           // These are users who are authenticating in a different browser or
@@ -138,6 +149,8 @@ BrowserID.verifySecondaryAddress = (function() {
       if (typeof redirectTimeout === "undefined") {
         redirectTimeout = REDIRECT_SECONDS * 1000;
       }
+      secondsRemaining = redirectTimeout / 1000;
+
 
       startVerification.call(self, options.ready);
 
diff --git a/resources/static/test/cases/pages/js/verify_secondary_address.js b/resources/static/test/cases/pages/js/verify_secondary_address.js
index fcbbe88392b2d1f792bc76d9e1f1a3ac5d7b74d1..8afe1e7f1030a16f0a3db3e181b2e3713e32038e 100644
--- a/resources/static/test/cases/pages/js/verify_secondary_address.js
+++ b/resources/static/test/cases/pages/js/verify_secondary_address.js
@@ -11,6 +11,7 @@
       xhr = bid.Mocks.xhr,
       WindowMock = bid.Mocks.WindowMock,
       dom = bid.DOM,
+      pageHelpers = bid.PageHelpers,
       testHelpers = bid.TestHelpers,
       testHasClass = testHelpers.testHasClass,
       testVisible = testHelpers.testVisible,
@@ -26,19 +27,24 @@
     setup: function() {
       testHelpers.setup();
       bid.Renderer.render("#page_head", "site/confirm", {});
+      $(document.body).append($('<div id=redirectTimeout>'));
       $(".siteinfo,.password_entry").hide();
     },
     teardown: function() {
+      $('#redirectTimeout').remove();
       testHelpers.teardown();
     }
   });
 
   function createController(options, callback) {
     controller = BrowserID.verifySecondaryAddress.create();
-    options = options || {};
-    options.document = doc = new WindowMock().document;
-    options.redirectTimeout = 0;
-    options.ready = callback;
+    // defaults, but options can override
+    options = _.extend({
+      document: new WindowMock().document,
+      redirectTimeout: 0,
+      ready: callback
+    }, options || {});
+    doc = options.document;
     controller.start(options);
   }
 
@@ -156,4 +162,32 @@
     });
   });
 
+  asyncTest("redirect: message shows with correct timeout", function() {
+    var returnTo = 'http://test.domain/path';
+    storage.setReturnTo(returnTo);
+    var timeout = 2;
+
+    //mock out helper so we can check progress of redirectTimeout el
+    var replaceFormWithNotice = pageHelpers.replaceFormWithNotice;
+    pageHelpers.replaceFormWithNotice = function(selector, cb) {
+      // mock out 2s network response
+      setTimeout(function mockedNetwork() {
+        replaceFormWithNotice.call(this, selector, function intercepted() {
+          equal(parseInt($('#redirectTimeout').html(), 10), timeout,
+            'timeout should not have started countdown yet');
+
+          //at the end, finish with cb
+          cb && cb();
+        });
+      }, (timeout - 1) * 1000);
+    };
+
+    var options = _.extend({ redirectTimeout: timeout * 1000 }, config);
+    createController(options, function() {
+      // teardown
+      pageHelpers.replaceFormWithNotice = replaceFormWithNotice;
+      start();
+    });
+  });
+
 }());