diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..a152ac51b7deb7e0122be3d367317dce0048d930
--- /dev/null
+++ b/resources/static/dialog/controllers/actions.js
@@ -0,0 +1,166 @@
+/*jshint brgwser:true, jQuery: true, forin: true, laxbreak:true */
+/*global _: true, BrowserID: true, PageController: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+BrowserID.Modules.Actions = (function() {
+  "use strict";
+
+  var bid = BrowserID,
+      sc,
+      serviceManager = bid.module,
+      user = bid.User,
+      errors = bid.Errors,
+      runningService,
+      onsuccess,
+      onerror;
+
+  function startService(name, options) {
+    // Only one service outside of the main dialog allowed.
+    if(runningService) {
+      serviceManager.stop(runningService);
+    }
+    runningService = name;
+    return serviceManager.start(name, options);
+  }
+
+  function startRegCheckService(email, verifier, message) {
+    this.confirmEmail = email;
+
+    var controller = startService("check_registration", {
+      email: email,
+      verifier: verifier,
+      verificationMessage: message
+    });
+    controller.startCheck();
+  }
+
+  var Module = bid.Modules.PageModule.extend({
+    start: function(data) {
+      var self=this;
+
+      data = data || {};
+
+      onsuccess = data.onsuccess;
+      onerror = data.onerror;
+
+      sc.start.call(self, data);
+
+      if(data.ready) {
+        data.ready();
+      }
+    },
+
+    doOffline: function() {
+      this.renderError("offline", {});
+    },
+
+    doCancel: function() {
+      onsuccess && onsuccess(null);
+    },
+
+    doConfirmUser: function(email) {
+      startRegCheckService.call(this, email, "waitForUserValidation", "user_confirmed");
+    },
+
+    doPickEmail: function(info) {
+      startService("pick_email", info);
+    },
+
+    doAddEmail: function() {
+      startService("add_email", {});
+    },
+
+    doAuthenticate: function(info) {
+      startService("authenticate", info);
+    },
+
+    doAuthenticateWithRequiredEmail: function(info) {
+      startService("required_email", info);
+    },
+
+    doForgotPassword: function(email) {
+      startService("forgot_password", {
+        email: email
+      });
+    },
+
+    doConfirmEmail: function(email) {
+      startRegCheckService.call(this, email, "waitForEmailValidation", "email_confirmed");
+    },
+
+    doEmailConfirmed: function() {
+      var self=this;
+      // yay!  now we need to produce an assertion.
+      user.getAssertion(self.confirmEmail, function(assertion) {
+        self.publish("assertion_generated", {
+          assertion: assertion
+        });
+      }, self.getErrorDialog(errors.getAssertion));
+    },
+
+    doAssertionGenerated: function(assertion) {
+      // Clear onerror before the call to onsuccess - the code to onsuccess
+      // calls window.close, which would trigger the onerror callback if we
+      // tried this afterwards.
+      onerror = null;
+      onsuccess && onsuccess(assertion);
+    },
+
+    doNotMe: function() {
+      var self=this;
+      user.logoutUser(self.publish.bind(self, "logged_out"), self.getErrorDialog(errors.logoutUser));
+    },
+
+    doSyncThenPickEmail: function() {
+      var self = this;
+      user.syncEmails(self.doPickEmail.bind(self),
+        self.getErrorDialog(errors.syncEmails));
+    },
+
+    doCheckAuth: function() {
+      var self=this;
+      user.checkAuthenticationAndSync(function onSuccess() {}, function onComplete(authenticated) {
+        self.publish("authentication_checked", {
+          authenticated: authenticated
+        });
+      }, self.getErrorDialog(errors.checkAuthentication));
+    }
+
+  });
+
+  sc = Module.sc;
+
+  return Module;
+}());
diff --git a/resources/static/dialog/controllers/addemail.js b/resources/static/dialog/controllers/addemail.js
index 13564465bafec60324376ae704509600ce56ce35..c3656c7a980457ff376596db1ebe4f810a7c6734 100644
--- a/resources/static/dialog/controllers/addemail.js
+++ b/resources/static/dialog/controllers/addemail.js
@@ -77,7 +77,7 @@ BrowserID.Modules.AddEmail = (function() {
       AddEmail.sc.start.call(self, options);
     },
     submit: addEmail
-    // START TESTING API
+    // BEGIN TESTING API
     ,
     addEmail: addEmail,
     cancelAddEmail: cancelAddEmail
diff --git a/resources/static/dialog/controllers/checkregistration.js b/resources/static/dialog/controllers/checkregistration.js
index d497b18eee914a63502f890ee2174707c8002a87..cb6ca63c24ed7f749270fd9c8c723ee39e69ce65 100644
--- a/resources/static/dialog/controllers/checkregistration.js
+++ b/resources/static/dialog/controllers/checkregistration.js
@@ -57,23 +57,25 @@ BrowserID.Modules.CheckRegistration = (function() {
       CheckRegistration.sc.start.call(self, options);
     },
 
-    startCheck: function() {
+    startCheck: function(oncomplete) {
       var self=this;
       user[self.verifier](self.email, function(status) {
         if (status === "complete") {
           user.syncEmails(function() {
             self.close(self.verificationMessage);
+            oncomplete && oncomplete();
           });
         }
         else if (status === "mustAuth") {
           self.close("auth", { email: self.email });
+          oncomplete && oncomplete();
         }
-      }, self.getErrorDialog(errors.registration));
+      }, self.getErrorDialog(errors.registration, oncomplete));
     },
 
     cancel: function() {
       var self=this;
-      // XXX this should change to cancelEmailValidation for email, but this 
+      // XXX this should change to cancelEmailValidation for email, but this
       // will work.
       user.cancelUserValidation();
       self.close("cancel_state");
diff --git a/resources/static/dialog/controllers/code_check.js b/resources/static/dialog/controllers/code_check.js
new file mode 100644
index 0000000000000000000000000000000000000000..692b3d61cae3b91982ad72980eb50ba7b6a3ee1d
--- /dev/null
+++ b/resources/static/dialog/controllers/code_check.js
@@ -0,0 +1,109 @@
+/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*global BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+
+BrowserID.Modules.CodeCheck = (function() {
+  "use strict";
+
+  var bid = BrowserID,
+      dom = bid.DOM,
+      sc,
+      expectedCodeVer;
+
+  function getMostRecentCodeVersion(oncomplete) {
+    bid.Network.codeVersion(oncomplete, this.getErrorDialog(bid.Errors.checkScriptVersion, oncomplete));
+  }
+
+  function updateCodeIfNeeded(oncomplete, version) {
+    var mostRecent = version === expectedCodeVer;
+
+    function ready() {
+      oncomplete && oncomplete(mostRecent);
+    }
+
+    if(mostRecent) {
+      ready();
+    }
+    else {
+      loadScript(version, ready);
+    }
+  }
+
+  function loadScript(version, oncomplete) {
+    var script = document.createElement("script");
+    script.type = "text/javascript";
+    script.src = "https://browserid.org/dialog/production_v" + version + ".js";
+    document.head.appendChild(script);
+
+    oncomplete();
+  }
+
+  var Module = bid.Modules.PageModule.extend({
+      start: function(data) {
+        var self=this;
+
+        function complete(val) {
+          data.ready && data.ready(val);
+        }
+
+        data = data || {};
+
+        if (data.code_ver) {
+          expectedCodeVer = data.code_ver;
+        }
+        else {
+          throw "init: code_ver must be defined";
+        }
+
+        getMostRecentCodeVersion.call(self, function(version) {
+          if(version) {
+            updateCodeIfNeeded.call(self, complete, version);
+          }
+          else {
+            complete();
+          }
+        });
+        sc.start.call(self, data);
+      }
+  });
+
+  sc = Module.sc;
+
+  return Module;
+
+}());
+
diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js
index 380932446cd7e7f1ac3ca8394663f9cd54bfc1d0..92a97b5293f1cecec9f7a786ab558f6e17511067 100644
--- a/resources/static/dialog/controllers/dialog.js
+++ b/resources/static/dialog/controllers/dialog.js
@@ -1,5 +1,5 @@
 /*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
-/*global setupChannel:true, BrowserID: true */
+/*global BrowserID: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -42,56 +42,41 @@ BrowserID.Modules.Dialog = (function() {
   var bid = BrowserID,
       user = bid.User,
       errors = bid.Errors,
+      channel = bid.Channel,
       dom = bid.DOM,
-      offline = false,
-      win = window,
-      serviceManager = bid.module,
-      runningService;
-
-  function startService(name, options) {
-    // Only one service outside of the main dialog allowed.
-    if(runningService) {
-      serviceManager.stop(runningService);
-    }
-    runningService = name;
-    return serviceManager.start(name, options);
-  }
-
-  function startRegCheckService(email, verifier, message) {
-    this.confirmEmail = email;
-
-    var controller = startService("check_registration", {
-      email: email,
-      verifier: verifier,
-      verificationMessage: message
-    });
-    controller.startCheck();
-  }
+      win = window;
 
   function checkOnline() {
     if ('onLine' in navigator && !navigator.onLine) {
-      this.doOffline();
+      this.publish("offline");
       return false;
     }
 
     return true;
   }
 
-  function onWinUnload() {
-    // do this only if something else hasn't declared success
-    var self=this;
-    if (!self.success) {
-      bid.Storage.setStagedOnBehalfOf("");
-      self.doCancel();
-    }
-    window.teardownChannel();
+  function startActions(onsuccess, onerror) {
+    var actions = BrowserID.Modules.Actions.create();
+    actions.start({
+      onsuccess: onsuccess,
+      onerror: onerror
+    });
+    return actions;
   }
 
-  function setupChannel() {
+  function startStateMachine(controller) {
+    // start this directly because it should always be running.
+    var machine = BrowserID.StateMachine.create();
+    machine.start({
+      controller: controller
+    });
+  }
+
+  function startChannel() {
     var self = this;
 
     try {
-      win.setupChannel(self);
+      channel.open(self);
     } catch (e) {
       self.renderError("error", {
         action: errors.relaySetup
@@ -104,161 +89,53 @@ BrowserID.Modules.Dialog = (function() {
     dom.setInner("#sitename", user.getHostname());
   }
 
-  var Dialog = bid.Modules.PageModule.extend({
-      init: function(options) {
-        offline = false;
-
-        options = options || {};
-
-        if (options.window) {
-          win = options.window;
-        }
-
-        var self=this;
-
-        Dialog.sc.init.call(self, options);
-
-        // keep track of where we are and what we do on success and error
-        self.onsuccess = null;
-        self.onerror = null;
-
-        // start this directly because it should always be running.
-        var machine = BrowserID.StateMachine.create();
-        machine.start({
-          controller: this
-        });
-
-        setupChannel.call(self);
-      },
-
-      getVerifiedEmail: function(origin_url, onsuccess, onerror) {
-        return this.get(origin_url, {}, onsuccess, onerror);
-      },
-
-      get: function(origin_url, params, onsuccess, onerror) {
-        var self=this;
-
-        if(checkOnline.call(self)) {
-          self.onsuccess = onsuccess;
-          self.onerror = onerror;
-
-          params = params || {};
-
-          self.allowPersistent = !!params.allowPersistent;
-          self.requiredEmail = params.requiredEmail;
-
-          setOrigin(origin_url);
-
-          self.bind(win, "unload", onWinUnload);
-
-          self.doCheckAuth();
-        }
-      },
-
-      doOffline: function() {
-        this.renderError("offline", {});
-        offline = true;
-      },
+  function onWindowUnload() {
+    this.publish("window_unload");
+    channel.close();
+  }
 
-      doXHRError: function(info) {
-        if (!offline) {
-          this.renderError("error", $.extend({
-            action: errors.xhrError
-          }, info));
-        }
-      },
+  var Dialog = bid.Modules.PageModule.extend({
+    init: function(options) {
+      var self=this;
 
-      doConfirmUser: function(email) {
-        startRegCheckService.call(this, email, "waitForUserValidation", "user_confirmed");
-      },
+      options = options || {};
 
-      doCancel: function() {
-        var self=this;
-        if (self.onsuccess) {
-          self.onsuccess(null);
-        }
-      },
+      win = options.window || window;
 
-      doPickEmail: function() {
-        var self=this;
-        startService("pick_email", {
-          // XXX ideal is to get rid of this and have a User function
-          // that takes care of getting email addresses AND the last used email
-          // for this site.
-          origin: user.getHostname(),
-          allow_persistent: self.allowPersistent
-        });
-      },
+      Dialog.sc.init.call(self, options);
 
-      doAddEmail: function() {
-        startService("add_email", {});
-      },
+      startChannel.call(self);
 
-      doAuthenticate: function(info) {
-        startService("authenticate", info);
-      },
+      options.ready && _.defer(options.ready);
+    },
 
-      doAuthenticateWithRequiredEmail: function(info) {
-        startService("required_email", info);
-      },
+    getVerifiedEmail: function(origin_url, success, error) {
+      return this.get(origin_url, {}, success, error);
+    },
 
-      doForgotPassword: function(email) {
-        startService("forgot_password", {
-          email: email
-        });
-      },
+    get: function(origin_url, params, success, error) {
+      var self=this;
 
-      doConfirmEmail: function(email) {
-        startRegCheckService.call(this, email, "waitForEmailValidation", "email_confirmed");
-      },
+      setOrigin(origin_url);
 
-      doEmailConfirmed: function() {
-        var self=this;
-        // yay!  now we need to produce an assertion.
-        user.getAssertion(self.confirmEmail, self.doAssertionGenerated.bind(self),
-          self.getErrorDialog(errors.getAssertion));
-      },
+      var actions = startActions.call(self, success, error);
+      startStateMachine.call(self, actions);
 
-      doAssertionGenerated: function(assertion) {
-        var self=this;
-        // Clear onerror before the call to onsuccess - the code to onsuccess
-        // calls window.close, which would trigger the onerror callback if we
-        // tried this afterwards.
-        self.onerror = null;
-        self.success = true;
-        self.onsuccess(assertion);
-      },
+      if(checkOnline.call(self)) {
+        params = params || {};
 
-      doNotMe: function() {
-        var self=this;
-        user.logoutUser(self.publish.bind(self, "auth"), self.getErrorDialog(errors.logoutUser));
-      },
+        params.hostname = user.getHostname();
 
-      doSyncThenPickEmail: function() {
-        var self = this;
-        user.syncEmails(self.doPickEmail.bind(self),
-          self.getErrorDialog(errors.signIn));
-      },
+        self.bind(win, "unload", onWindowUnload);
 
-      doCheckAuth: function() {
-        var self=this;
-        user.checkAuthenticationAndSync(function onSuccess() {},
-          function onComplete(authenticated) {
-            if (self.requiredEmail) {
-              self.publish("authenticate_with_required_email", {
-                email: self.requiredEmail,
-                authenticated: authenticated
-              });
-            }
-            else if (authenticated) {
-              self.publish("pick_email");
-            } else {
-              self.publish("auth");
-            }
-          }, self.getErrorDialog(errors.checkAuthentication));
-    },
+        self.publish("start", params);
+      }
+    }
 
-    doWinUnload: onWinUnload
+    // BEGIN TESTING API
+    ,
+    onWindowUnload: onWindowUnload
+    // END TESTING API
 
   });
 
diff --git a/resources/static/dialog/controllers/page.js b/resources/static/dialog/controllers/page.js
index b26e4372750edca51dfad49df298b7f9d8b412cc..149c1a349756ab5647080456a339e1c086412121 100644
--- a/resources/static/dialog/controllers/page.js
+++ b/resources/static/dialog/controllers/page.js
@@ -124,18 +124,12 @@ BrowserID.Modules.PageModule = (function() {
       screens.wait.show(body, body_vars);
     },
 
-    renderError: function(body, body_vars) {
+    renderError: function(body, body_vars, oncomplete) {
       screens.error.show(body, body_vars);
 
-      /**
-       * TODO XXX - Use the error-display for this.
-       */
-      this.bind("#openMoreInfo", "click", function(event) {
-        event.preventDefault();
+      bid.ErrorDisplay.start();
 
-        $("#moreInfo").slideDown();
-        $("#openMoreInfo").css({visibility: "hidden"});
-      });
+      $("#error").stop().css('opacity', 1).hide().fadeIn(ANIMATION_TIME, oncomplete);
     },
 
     validate: function() {
@@ -160,14 +154,16 @@ BrowserID.Modules.PageModule = (function() {
      * Get a curried function to an error dialog.
      * @method getErrorDialog
      * @method {object} action - info to use for the error dialog.  Should have
+     * @method {function} [onerror] - callback to call after the
+     * error has been displayed.
      * two fields, message, description.
      */
-    getErrorDialog: function(action) {
+    getErrorDialog: function(action, onerror) {
       var self=this;
       return function(lowLevelInfo) {
         self.renderError("error", $.extend({
           action: action
-        }, lowLevelInfo));
+        }, lowLevelInfo), onerror);
       }
     }
   });
diff --git a/resources/static/dialog/controllers/required_email.js b/resources/static/dialog/controllers/required_email.js
index 48d695f329e9534357b30cd50a7bfe6376105cdc..765627699fea0b77b4881b22be116159c70b74fd 100644
--- a/resources/static/dialog/controllers/required_email.js
+++ b/resources/static/dialog/controllers/required_email.js
@@ -139,7 +139,7 @@ BrowserID.Modules.RequiredEmail = (function() {
           // verify ownership of the address.
           showTemplate(registered, registered);
           ready();
-        }, self.getErrorDialog(errors.isEmailRegistered));
+        }, self.getErrorDialog(errors.isEmailRegistered, ready));
       }
 
       function showTemplate(requireSignin, showPassword) {
diff --git a/resources/static/dialog/resources/channel.js b/resources/static/dialog/resources/channel.js
index a2dad83d7475d8e0d7cd0a1f97ac01afb4e38ba1..0df7dc8c224402ff73abdef5096a7320de8bbc36 100644
--- a/resources/static/dialog/resources/channel.js
+++ b/resources/static/dialog/resources/channel.js
@@ -143,29 +143,27 @@
   }
 
 
-  if(window.BrowserID) {
-    BrowserID.Channel = {
-      /**
-       * Used to intialize the channel, mostly for unit testing to override
-       * window and navigator.
-       * @method init
-       */
-      init: init,
-
-      /**
-       * Open the channel.
-       * @method open
-       * @param {object} options - contains:
-       * *   options.getVerifiedEmail {function} - function to /get
-       */
-      open: open,
-
-      /**
-       * Close the channel
-       */
-      close: close
-    };
-  }
+  BrowserID.Channel = {
+    /**
+     * Used to intialize the channel, mostly for unit testing to override
+     * window and navigator.
+     * @method init
+     */
+    init: init,
+
+    /**
+     * Open the channel.
+     * @method open
+     * @param {object} options - contains:
+     * *   options.getVerifiedEmail {function} - function to /get
+     */
+    open: open,
+
+    /**
+     * Close the channel
+     */
+    close: close
+  };
 
   /**
    * This is here as a legacy API for addons/etc that are depending on
diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js
index 3bd9962b618c4bc0221eea80b123796cd4e3b515..5c5d516672d8ad92e56d7f176732b8877466448c 100644
--- a/resources/static/dialog/resources/helpers.js
+++ b/resources/static/dialog/resources/helpers.js
@@ -102,7 +102,7 @@
         tooltip.showTooltip("#could_not_add");
       }
       if (callback) callback(staged);
-    }, self.getErrorDialog(errors.createUser));
+    }, self.getErrorDialog(errors.createUser, callback));
   }
 
   function resetPassword(email, callback) {
diff --git a/resources/static/dialog/resources/state_machine.js b/resources/static/dialog/resources/state_machine.js
index 93704afaa4b29de38ec268911e9eba483211ed6f..948a9e5bf0952eb6f2502c48af8361b8c5ecafa7 100644
--- a/resources/static/dialog/resources/state_machine.js
+++ b/resources/static/dialog/resources/state_machine.js
@@ -36,10 +36,10 @@
  * ***** END LICENSE BLOCK ***** */
 (function() {
   var bid = BrowserID,
-      user = bid.User,
       mediator = bid.Mediator,
       subscriptions = [],
-      stateStack = [];
+      stateStack = [],
+      controller;
 
   function subscribe(message, cb) {
     subscriptions.push(mediator.subscribe(message, cb));
@@ -52,8 +52,7 @@
   }
 
   function pushState(funcName) {
-    var args = [].slice.call(arguments, 1),
-        controller = this.controller;
+    var args = [].slice.call(arguments, 1);
 
     // Remember the state and the information for the state in case we have to
     // go back to it.
@@ -75,14 +74,12 @@
     var gotoState = stateStack[stateStack.length - 1];
 
     if(gotoState) {
-      var controller = this.controller;
       controller[gotoState.funcName].apply(controller, gotoState.args);
     }
   }
 
   function startStateMachine() {
     var self = this,
-        controller = self.controller,
         gotoState = pushState.bind(self),
         cancelState = popState.bind(self);
 
@@ -90,8 +87,50 @@
       gotoState("doOffline");
     });
 
-    subscribe("cancel_state", function(msg, info) {
-      cancelState();
+    subscribe("start", function(msg, info) {
+      info = info || {};
+
+      self.hostname = info.hostname;
+      self.allowPersistent = !!info.allowPersistent;
+      self.requiredEmail = info.requiredEmail;
+
+      gotoState("doCheckAuth");
+    });
+
+    subscribe("cancel", function() {
+      gotoState("doCancel");
+    });
+
+    subscribe("window_unload", function() {
+      if(!self.success) {
+        bid.Storage.setStagedOnBehalfOf("");
+        gotoState("doCancel");
+      }
+    });
+
+    subscribe("authentication_checked", function(msg, info) {
+      var authenticated = info.authenticated;
+
+      if (self.requiredEmail) {
+        // XXX get this out of here and into the state machine!
+        gotoState("doAuthenticateWithRequiredEmail", {
+          email: self.requiredEmail,
+          authenticated: authenticated
+        });
+      }
+      else if (authenticated) {
+        mediator.publish("pick_email");
+      } else {
+        mediator.publish("authenticate");
+      }
+    });
+
+    subscribe("authenticate", function(msg, info) {
+      info = info || {};
+
+      gotoState("doAuthenticate", {
+        email: info.email
+      });
     });
 
     subscribe("user_staged", function(msg, info) {
@@ -107,7 +146,18 @@
     });
 
     subscribe("pick_email", function() {
-      gotoState("doPickEmail");
+      gotoState("doPickEmail", {
+        origin: self.hostname,
+        allow_persistent: self.allowPersistent
+      });
+    });
+
+    subscribe("notme", function() {
+      gotoState("doNotMe");
+    });
+
+    subscribe("logged_out", function() {
+      mediator.publish("authenticate");
     });
 
     subscribe("authenticated", function(msg, info) {
@@ -123,6 +173,7 @@
     });
 
     subscribe("assertion_generated", function(msg, info) {
+      self.success = true;
       if (info.assertion !== null) {
         gotoState("doAssertionGenerated", info.assertion);
       }
@@ -143,25 +194,10 @@
       gotoState("doEmailConfirmed");
     });
 
-    subscribe("notme", function() {
-      gotoState("doNotMe");
-    });
-
-    subscribe("auth", function(msg, info) {
-      info = info || {};
-
-      gotoState("doAuthenticate", {
-        email: info.email
-      });
-    });
-
-    subscribe("start", function() {
-      gotoState("doCheckAuth");
+    subscribe("cancel_state", function(msg, info) {
+      cancelState();
     });
 
-    subscribe("cancel", function() {
-      gotoState("doCancel");
-    });
   }
 
   var StateMachine = BrowserID.Class({
@@ -171,7 +207,12 @@
 
     start: function(options) {
       options = options || {};
-      this.controller = options.controller;
+
+      controller = options.controller;
+      if(!controller) {
+        throw "start: controller must be specified";
+      }
+
       startStateMachine.call(this);
     },
 
diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js
index e5b21f761c531869e9a363d17154b1cf91d824b2..a6787584ab511e2a3559278560f33ea12aae4733 100644
--- a/resources/static/dialog/start.js
+++ b/resources/static/dialog/start.js
@@ -2,16 +2,26 @@
   var bid = BrowserID,
       moduleManager = bid.module,
       modules = bid.Modules;
-  
-  moduleManager.register("dialog", modules.Dialog);
-  moduleManager.register("add_email", modules.AddEmail);
-  moduleManager.register("authenticate", modules.Authenticate);
-  moduleManager.register("check_registration", modules.CheckRegistration);
-  moduleManager.register("forgot_password", modules.ForgotPassword);
-  moduleManager.register("pick_email", modules.PickEmail);
-  moduleManager.register("required_email", modules.RequiredEmail);
 
-  moduleManager.start("dialog");
+  moduleManager.register("code_check", modules.CodeCheck);
 
+  moduleManager.start("code_check", {
+    code_ver: "ABC123",
+    ready: function(status) {
+      // if status is false, that means the javascript is out of date and we
+      // have to reload.
+      if(status) {
+        moduleManager.register("dialog", modules.Dialog);
+        moduleManager.register("add_email", modules.AddEmail);
+        moduleManager.register("authenticate", modules.Authenticate);
+        moduleManager.register("check_registration", modules.CheckRegistration);
+        moduleManager.register("forgot_password", modules.ForgotPassword);
+        moduleManager.register("pick_email", modules.PickEmail);
+        moduleManager.register("required_email", modules.RequiredEmail);
+
+        moduleManager.start("dialog");
+      }
+    }
+  });
 }());
 
diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js
index 38c321996f8aec42f1a911f57e0110bc88453907..f1e6cbb681922c7fce6ce5f9978b0a3094b34fa8 100644
--- a/resources/static/shared/error-messages.js
+++ b/resources/static/shared/error-messages.js
@@ -53,10 +53,14 @@ BrowserID.Errors = (function(){
       title: "Checking Authentication"
     },
 
+    checkScriptVersion: {
+      title: "Checking Script Version"
+    },
+
     completeUserRegistration: {
       title: "Completing User Registration"
     },
-    
+
     createUser: {
       title: "Creating Account"
     },
diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js
index d44f8fd0cbcf2ec6c2657ba3a2e01e2b040f238e..d15d8d345f41f2834949b6f78bbe1bd859fe35f8 100644
--- a/resources/static/shared/network.js
+++ b/resources/static/shared/network.js
@@ -42,6 +42,7 @@ BrowserID.Network = (function() {
       server_time,
       domain_key_creation_time,
       auth_status,
+      code_version,
       mediator = BrowserID.Mediator;
 
   function deferResponse(cb) {
@@ -73,8 +74,8 @@ BrowserID.Network = (function() {
     xhr.ajax({
       type: "GET",
       url: options.url,
-      // We defer the responses because otherwise jQuery eats any exceptions 
-      // that are thrown in the response handlers and it becomes very difficult 
+      // We defer the responses because otherwise jQuery eats any exceptions
+      // that are thrown in the response handlers and it becomes very difficult
       // to debug.
       success: deferResponse(options.success),
       error: deferResponse(xhrError(options.error, {
@@ -91,7 +92,7 @@ BrowserID.Network = (function() {
     withContext(function() {
       var data = options.data || {};
 
-      if(!data.csrf) {
+      if (!data.csrf) {
         data.csrf = csrf_token;
       }
 
@@ -99,8 +100,8 @@ BrowserID.Network = (function() {
         type: "POST",
         url: options.url,
         data: data,
-        // We defer the responses because otherwise jQuery eats any exceptions 
-        // that are thrown in the response handlers and it becomes very difficult 
+        // We defer the responses because otherwise jQuery eats any exceptions
+        // that are thrown in the response handlers and it becomes very difficult
         // to debug.
         success: deferResponse(options.success),
         error: deferResponse(xhrError(options.error, {
@@ -128,7 +129,10 @@ BrowserID.Network = (function() {
           };
           domain_key_creation_time = result.domain_key_creation_time;
           auth_status = result.authenticated;
-          cb();
+          // XXX remove the ABC123
+          code_version = result.code_version || "ABC123";
+
+          _.defer(cb);
         },
         error: deferResponse(xhrError(onFailure, {
           network: {
@@ -187,7 +191,7 @@ BrowserID.Network = (function() {
               // session, let's set it to perhaps save a network request
               // (to fetch session context).
               auth_status = authenticated;
-              if(onSuccess) onSuccess(authenticated);
+              if (onSuccess) onSuccess(authenticated);
             } catch (e) {
               onFailure("unexpected server response: " + e);
             }
@@ -200,7 +204,7 @@ BrowserID.Network = (function() {
     /**
      * Check whether a user is currently logged in.
      * @method checkAuth
-     * @param {function} [onSuccess] - Success callback, called with one 
+     * @param {function} [onSuccess] - Success callback, called with one
      * boolean parameter, whether the user is authenticated.
      * @param {function} [onFailure] - called on XHR failure.
      */
@@ -257,8 +261,8 @@ BrowserID.Network = (function() {
         },
         error: function(info) {
           // 403 is throttling.
-          if(info.network.status === 403) {
-            if (onSuccess) onSuccess(false); 
+          if (info.network.status === 403) {
+            if (onSuccess) onSuccess(false);
           }
           else if (onFailure) onFailure(info);
         }
@@ -325,7 +329,7 @@ BrowserID.Network = (function() {
      * Call with a token to prove an email address ownership.
      * @method completeEmailRegistration
      * @param {string} token - token proving email ownership.
-     * @param {function} [onSuccess] - Callback to call when complete.  Called 
+     * @param {function} [onSuccess] - Callback to call when complete.  Called
      * with one boolean parameter that specifies the validity of the token.
      * @param {function} [onFailure] - Called on XHR failure.
      */
@@ -364,7 +368,7 @@ BrowserID.Network = (function() {
      * @param {string} password - new password.
      * @param {function} [onSuccess] - Callback to call when complete.
      * @param {function} [onFailure] - Called on XHR failure.
-     */ 
+     */
     resetPassword: function(password, onSuccess, onFailure) {
       // XXX fill this in.
       if (onSuccess) onSuccess();
@@ -375,10 +379,10 @@ BrowserID.Network = (function() {
      * @method changePassword
      * @param {string} oldpassword - old password.
      * @param {string} newpassword - new password.
-     * @param {function} [onSuccess] - Callback to call when complete. Will be 
+     * @param {function} [onSuccess] - Callback to call when complete. Will be
      * called with true if successful, false otw.
      * @param {function} [onFailure] - Called on XHR failure.
-     */ 
+     */
     changePassword: function(oldPassword, newPassword, onSuccess, onFailure) {
       // XXX fill this in
       if (onSuccess) {
@@ -421,8 +425,8 @@ BrowserID.Network = (function() {
         },
         error: function(info) {
           // 403 is throttling.
-          if(info.network.status === 403) {
-            if (onSuccess) onSuccess(false); 
+          if (info.network.status === 403) {
+            if (onSuccess) onSuccess(false);
           }
           else if (onFailure) onFailure(info);
         }
@@ -450,8 +454,8 @@ BrowserID.Network = (function() {
      * Check whether the email is already registered.
      * @method emailRegistered
      * @param {string} email - Email address to check.
-     * @param {function} [onSuccess] - Called with one boolean parameter when 
-     * complete.  Parameter is true if `email` is already registered, false 
+     * @param {function} [onSuccess] - Called with one boolean parameter when
+     * complete.  Parameter is true if `email` is already registered, false
      * otw.
      * @param {function} [onFailure] - Called on XHR failure.
      */
@@ -459,7 +463,7 @@ BrowserID.Network = (function() {
       get({
         url: "/wsapi/have_email?email=" + encodeURIComponent(email),
         success: function(data, textStatus, xhr) {
-          if(onSuccess) onSuccess(data.email_known);
+          if (onSuccess) onSuccess(data.email_known);
         },
         error: onFailure
       });
@@ -528,9 +532,9 @@ BrowserID.Network = (function() {
         try {
           if (!server_time) throw "can't get server time!";
           var offset = (new Date()).getTime() - server_time.local;
-          onSuccess(new Date(offset + server_time.remote));
+          if (onSuccess) onSuccess(new Date(offset + server_time.remote));
         } catch(e) {
-          onFailure(e.toString());
+          if (onFailure) onFailure(e.toString());
         }
       }, onFailure);
     },
@@ -548,9 +552,29 @@ BrowserID.Network = (function() {
       withContext(function() {
         try {
           if (!domain_key_creation_time) throw "can't get domain key creation time!";
-          onSuccess(new Date(domain_key_creation_time));
+          if (onSuccess) onSuccess(new Date(domain_key_creation_time));
+        } catch(e) {
+          if (onFailure) onFailure(e.toString());
+        }
+      }, onFailure);
+    },
+
+    /**
+     * Get the most recent code version
+     *
+     * Note: this function will perform a network request if
+     * during this session /wsapi/session_context has not
+     * been called.
+     *
+     * @method codeVersion
+     */
+    codeVersion: function(onComplete, onFailure) {
+      withContext(function() {
+        try {
+          if (!code_version) throw "can't get code version!";
+          if (onComplete) onComplete(code_version);
         } catch(e) {
-          onFailure(e.toString());
+          if (onFailure) onFailure(e.toString());
         }
       }, onFailure);
     }
diff --git a/resources/static/test/index.html b/resources/static/test/index.html
index 9be2191ec3936350a3ec0f7936b5478119b01e2e..30ea349272d58cc956a828dd7eeb91fb8cef2f3f 100644
--- a/resources/static/test/index.html
+++ b/resources/static/test/index.html
@@ -109,6 +109,8 @@
     <script type="text/javascript" src="/dialog/resources/state_machine.js"></script>
 
     <script type="text/javascript" src="/dialog/controllers/page.js"></script>
+    <script type="text/javascript" src="/dialog/controllers/code_check.js"></script>
+    <script type="text/javascript" src="/dialog/controllers/actions.js"></script>
     <script type="text/javascript" src="/dialog/controllers/pickemail.js"></script>
     <script type="text/javascript" src="/dialog/controllers/addemail.js"></script>
     <script type="text/javascript" src="/dialog/controllers/dialog.js"></script>
@@ -158,6 +160,8 @@
     <script type="text/javascript" src="qunit/resources/channel_unit_test.js"></script>
 
     <script type="text/javascript" src="qunit/controllers/page_unit_test.js"></script>
+    <script type="text/javascript" src="qunit/controllers/code_check_unit_test.js"></script>
+    <script type="text/javascript" src="qunit/controllers/actions_unit_test.js"></script>
     <script type="text/javascript" src="qunit/controllers/pickemail_unit_test.js"></script>
     <script type="text/javascript" src="qunit/controllers/addemail_unit_test.js"></script>
     <script type="text/javascript" src="qunit/controllers/checkregistration_unit_test.js"></script>
diff --git a/resources/static/test/qunit/controllers/actions_unit_test.js b/resources/static/test/qunit/controllers/actions_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..54bcb64e31a9f3aae6e8cddde1cddf4035766ac4
--- /dev/null
+++ b/resources/static/test/qunit/controllers/actions_unit_test.js
@@ -0,0 +1,88 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+(function() {
+  "use strict";
+
+  var bid = BrowserID,
+      controller,
+      el;
+
+  function reset() {
+    el = $("#controller_head");
+    el.find("#formWrap .contents").html("");
+    el.find("#wait .contents").html("");
+    el.find("#error .contents").html("");
+  }
+
+  function createController(config) {
+    controller = BrowserID.Modules.Actions.create();
+    controller.start(config);
+  }
+
+  // XXX Make a test helper class for this.
+  function checkNetworkError() {
+    ok($("#error .contents").text().length, "contents have been written");
+    ok($("#error #action").text().length, "action contents have been written");
+    ok($("#error #network").text().length, "network contents have been written");
+  }
+
+  module("controllers/actions", {
+    setup: function() {
+      reset();
+    },
+
+    teardown: function() {
+      if(controller) {
+        controller.destroy();
+      }
+      reset();
+    }
+  });
+
+  asyncTest("doOffline", function() {
+    createController({
+      ready: function() {
+        controller.doOffline();
+        ok($("#error .contents").text().length, "contents have been written");
+        ok($("#error #offline").text().length, "offline error message has been written");
+        start();
+      }
+    });
+  });
+
+}());
+
diff --git a/resources/static/test/qunit/controllers/authenticate_unit_test.js b/resources/static/test/qunit/controllers/authenticate_unit_test.js
index 497145e20ccbb61953810869cc0b14e48483da2e..7a0d73ed193d26e88687d874f29e307b5a7326c0 100644
--- a/resources/static/test/qunit/controllers/authenticate_unit_test.js
+++ b/resources/static/test/qunit/controllers/authenticate_unit_test.js
@@ -202,12 +202,10 @@
     });
 
     xhr.useResult("ajaxError");
-    controller.createUser()
-
-    setTimeout(function() {
+    controller.createUser(function() {
       equal(handlerCalled, false, "bad jiji, user_staged should not have been called with XHR error");
       start();
-    }, 50);
+    });
   });
 
 }());
diff --git a/resources/static/test/qunit/controllers/checkregistration_unit_test.js b/resources/static/test/qunit/controllers/checkregistration_unit_test.js
index 4ee5076b6db9974db90a3fc3c37536552112b83b..28db5714a279988204f97763b079f54ee44abe15 100644
--- a/resources/static/test/qunit/controllers/checkregistration_unit_test.js
+++ b/resources/static/test/qunit/controllers/checkregistration_unit_test.js
@@ -88,36 +88,34 @@
     xhr.useResult("pending");
 
     testVerifiedUserEvent("user_verified", "User verified");
+    // use setTimeout to simulate a delay in the user opening the email.
     setTimeout(function() {
       xhr.useResult("complete");
-    }, 1000);
+    }, 500);
   });
 
   asyncTest("user validation with XHR error", function() {
     xhr.useResult("ajaxError");
 
     createController("waitForUserValidation", "user_verified");
-    register("user_verified", function() {
-      ok(false, "on XHR error, should not complete");
-    });
-    controller.startCheck();
-
-    setTimeout(function() {
+    controller.startCheck(function() {
+      register("user_verified", function() {
+        ok(false, "on XHR error, should not complete");
+      });
       ok(testHelpers.errorVisible(), "Error message is visible");
       start();
-    }, 500);
+    });
   });
 
   asyncTest("cancel raises cancel_state", function() {
     createController("waitForUserValidation", "user_verified");
-    register("cancel_state", function() {
-      ok(true, "on cancel, cancel_state is triggered");
-      start();
+    controller.startCheck(function() {
+      register("cancel_state", function() {
+        ok(true, "on cancel, cancel_state is triggered");
+        start();
+      });
+      controller.cancel();
     });
-    controller.startCheck();
-    controller.cancel();
-
-
   });
 
 }());
diff --git a/resources/static/test/qunit/controllers/code_check_unit_test.js b/resources/static/test/qunit/controllers/code_check_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..aab8a19daa197ac192864c62f653c7e357a4fd15
--- /dev/null
+++ b/resources/static/test/qunit/controllers/code_check_unit_test.js
@@ -0,0 +1,123 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+(function() {
+  "use strict";
+
+  var bid = BrowserID,
+      network = bid.Network,
+      xhr = bid.Mocks.xhr,
+      helpers = bid.TestHelpers,
+      controller;
+
+  function createController(config) {
+    var config = $.extend({
+      code_ver: "ABC123"
+    }, config);
+
+    controller = BrowserID.Modules.CodeCheck.create();
+    controller.start(config);
+  }
+
+  module("controllers/code_check", {
+    setup: function() {
+      helpers.setup();
+    },
+
+    teardown: function() {
+      helpers.teardown();
+
+      controller.destroy();
+    }
+  });
+
+  asyncTest("create controller with most recent scripts", function() {
+    createController({
+      ready: function(mostRecent) {
+        equal(mostRecent, true, "scripts are the most recent");
+        start();
+      }
+    });
+  });
+
+  test("create controller without code_ver specified", function() {
+    raises(function() {
+      createController({
+        code_ver: null
+      });
+    }, "init: code_ver must be defined", "code version not specified throws exception");
+  });
+
+  asyncTest("create controller with out of date scripts", function() {
+    var scriptCount = $("head > script").length;
+
+    createController({
+      code_ver: "ABC122",
+      ready: function(mostRecent) {
+        equal(mostRecent, false, "scripts are not the most recent");
+        var scripts = $("head > script");
+        var scriptAdded = scripts.length !== scriptCount;
+
+        equal(scriptAdded, true, "a script was added to the dom to force reload");
+
+        if(scriptAdded) {
+          // Only remove the last script if the script was actually added.
+          scripts.last().remove();
+        }
+
+        start();
+      }
+    });
+  });
+
+  asyncTest("create controller with XHR error during script check", function() {
+    xhr.useResult("contextAjaxError");
+    var scriptCount = $("head > script").length;
+
+    createController({
+      ready: function() {
+        helpers.checkNetworkError();
+        var scripts = $("head > script");
+        var scriptAdded = scripts.length !== scriptCount;
+
+        equal(scriptAdded, false, "a script was not added on XHR error");
+        start();
+      }
+    });
+  });
+
+}());
+
diff --git a/resources/static/test/qunit/controllers/dialog_unit_test.js b/resources/static/test/qunit/controllers/dialog_unit_test.js
index e6c2ea1a6b280bce3106765788e92a8fa50dc7e9..cc2738de22306b7263d3d1d16d30b28d226bef28 100644
--- a/resources/static/test/qunit/controllers/dialog_unit_test.js
+++ b/resources/static/test/qunit/controllers/dialog_unit_test.js
@@ -39,14 +39,14 @@
 
   var bid = BrowserID,
       channel = bid.Channel,
+      network = bid.Network,
+      xhr = bid.Mocks.xhr,
       controller,
       el,
-      channelError = false,
       winMock,
       navMock;
 
   function reset() {
-    channelError = false;
   }
 
   function WinMock() {
@@ -54,10 +54,6 @@
   }
 
   WinMock.prototype = {
-    setupChannel: function() {
-      if (channelError) throw "Channel error";
-    },
-
     // Oh so beautiful.
     opener: {
       frames: {
@@ -82,14 +78,14 @@
   }
 
   function createController(config) {
-    var config = $.extend(config, {
+    var config = $.extend({
       window: winMock
-    });
+    }, config);
 
     controller = BrowserID.Modules.Dialog.create(config);
   }
 
-  module("controllers/dialog_controller", {
+  module("controllers/dialog", {
     setup: function() {
       winMock = new WinMock();
       channel.init({
@@ -111,20 +107,24 @@
     }
   });
 
-  test("initialization with channel error", function() {
-    channelError = true;
-    createController();
-
+  function checkNetworkError() {
     ok($("#error .contents").text().length, "contents have been written");
-  });
+    ok($("#error #action").text().length, "action contents have been written");
+    ok($("#error #network").text().length, "network contents have been written");
+  }
 
-  test("doOffline", function() {
-    createController();
-    controller.doOffline();
-    ok($("#error .contents").text().length, "contents have been written");
-    ok($("#error #offline").text().length, "offline error message has been written");
+  asyncTest("initialization with channel error", function() {
+    // Set the hash so that the channel cannot be found.
+    winMock.location.hash = "#1235";
+    createController({
+      ready: function() {
+        ok($("#error .contents").text().length, "contents have been written");
+        start();
+      }
+    });
   });
 
+  /*
   test("doXHRError while online, no network info given", function() {
     createController();
     controller.doXHRError();
@@ -141,9 +141,7 @@
         url: "browserid.org/verify"
       }
     });
-    ok($("#error .contents").text().length, "contents have been written");
-    ok($("#error #action").text().length, "action contents have been written");
-    ok($("#error #network").text().length, "network contents have been written");
+    checkNetworkError();
   });
 
   test("doXHRError while offline does not update contents", function() {
@@ -154,7 +152,7 @@
     controller.doXHRError();
     ok(!$("#error #action").text().length, "XHR error is not reported if the user is offline.");
   });
-
+*/
 
   /*
   test("doCheckAuth with registered requiredEmail, authenticated", function() {
@@ -190,21 +188,23 @@
   });
 */
 
-  test("doWinUnload", function() {
+  asyncTest("onWindowUnload", function() {
     createController({
-      requiredEmail: "registered@testuser.com"
-    });
+      requiredEmail: "registered@testuser.com",
+      ready: function() {
+        var error;
 
-    var error;
-
-    try {
-      controller.doWinUnload();
-    }
-    catch(e) {
-      error = e;
-    }
+        try {
+          controller.onWindowUnload();
+        }
+        catch(e) {
+          error = e;
+        }
 
-    equal(typeof error, "undefined", "unexpected error thrown when unloading window (" + error + ")");
+        equal(typeof error, "undefined", "unexpected error thrown when unloading window (" + error + ")");
+        start();
+      }
+    });
   });
 
 }());
diff --git a/resources/static/test/qunit/controllers/page_unit_test.js b/resources/static/test/qunit/controllers/page_unit_test.js
index ea295702e90757d4e0640f899a39e3e3876d0fc4..012c55d93d3fdd5201f5d7b2310bb96638b2fe1b 100644
--- a/resources/static/test/qunit/controllers/page_unit_test.js
+++ b/resources/static/test/qunit/controllers/page_unit_test.js
@@ -123,7 +123,7 @@
     ok(html.length, "with error template specified, error text is loaded");
   });
 
-  test("renderError renders an error message", function() {
+  asyncTest("renderError renders an error message", function() {
     createController({
       waitTemplate: waitTemplate,
       waitVars: {
@@ -135,14 +135,15 @@
     controller.renderError("wait", {
       title: "error title",
       message: "error message"
+    }, function() {
+      var html = el.find("#error .contents").html();
+      // XXX underpowered test, we don't actually check the contents.
+      ok(html.length, "with error template specified, error text is loaded");
+      start();
     });
-
-    var html = el.find("#error .contents").html();
-    // XXX underpowered test, we don't actually check the contents.
-    ok(html.length, "with error template specified, error text is loaded");
   });
 
-  test("renderError allows us to open expanded error info", function() {
+  asyncTest("renderError allows us to open expanded error info", function() {
     createController();
 
     controller.renderError("error", {
@@ -150,26 +151,22 @@
         title: "expanded action info",
         message: "expanded message"
       }
-    });
-
-    var html = el.find("#error .contents").html();
+    }, function() {
+      var html = el.find("#error .contents").html();
 
-    $("#moreInfo").hide();
+      $("#moreInfo").hide();
 
-    var evt = $.Event("click");
-    $("#openMoreInfo").trigger( evt );
+      $("#openMoreInfo").click();
 
-    /*
-    setTimeout(function() {
-      equal($("#showMoreInfo").is(":visible"), false, "button is not visible after clicking expanded info");
-      equal($("#moreInfo").is(":visible"), true, "expanded error info is visible after clicking expanded info");
-      start();
-    }, 500);
-    stop();
-*/
+      setTimeout(function() {
+        equal($("#showMoreInfo").is(":visible"), false, "button is not visible after clicking expanded info");
+        equal($("#moreInfo").is(":visible"), true, "expanded error info is visible after clicking expanded info");
+        start();
+      }, 1);
+    });
   });
 
-  test("getErrorDialog gets a function that can be used to render an error message", function() {
+  asyncTest("getErrorDialog gets a function that can be used to render an error message", function() {
     createController({
       waitTemplate: waitTemplate,
       waitVars: {
@@ -182,15 +179,16 @@
     var func = controller.getErrorDialog({
       title: "medium level info error title",
       message: "medium level info error message"
+    }, function() {
+      ok(true, "onerror callback called when returned function is called");
+      var html = el.find("#error .contents").html();
+      // XXX underpowered test, we don't actually check the contents.
+      ok(html.length, "when function is run, error text is loaded");
+      start();
     });
 
     equal(typeof func, "function", "a function was returned from getErrorDialog");
     func();
-
-    var html = el.find("#error .contents").html();
-    // XXX underpowered test, we don't actually check the contents.
-    ok(html.length, "when function is run, error text is loaded");
-
   });
 
   asyncTest("bind DOM Events", function() {
diff --git a/resources/static/test/qunit/controllers/required_email_unit_test.js b/resources/static/test/qunit/controllers/required_email_unit_test.js
index 43fa76ffbc96a70b7be482eeb6d0b55d04215f1b..4bd12a94b99ba8436c6d5d8df202a16836c4ce52 100644
--- a/resources/static/test/qunit/controllers/required_email_unit_test.js
+++ b/resources/static/test/qunit/controllers/required_email_unit_test.js
@@ -131,13 +131,10 @@
       email: email,
       authenticated: false,
       ready: function() {
+        ok(testHelpers.errorVisible(), "Error message is visible");
+        start();
       }
     });
-
-    setTimeout(function() {
-      ok(testHelpers.errorVisible(), "Error message is visible");
-      start();
-    }, 500);
   });
 
   asyncTest("user who is authenticated, email belongs to user", function() {
diff --git a/resources/static/test/qunit/mocks/xhr.js b/resources/static/test/qunit/mocks/xhr.js
index 7a5a5d865a3ff54b4a1bc23cddac665edf5a123a..a65e6e5bfd0a89c83b719b86e4dc151e70421679 100644
--- a/resources/static/test/qunit/mocks/xhr.js
+++ b/resources/static/test/qunit/mocks/xhr.js
@@ -40,7 +40,8 @@ BrowserID.Mocks.xhr = (function() {
       server_time: new Date().getTime(),
       domain_key_creation_time: (new Date().getTime() - (30 * 24 * 60 * 60 * 1000)),
       csrf_token: "csrf",
-      authenticated: false
+      authenticated: false,
+      code_version: "ABC123"
     };
 
   // this cert is meaningless, but it has the right format
diff --git a/resources/static/test/qunit/resources/state_machine_unit_test.js b/resources/static/test/qunit/resources/state_machine_unit_test.js
index 1d796e8dc5aef1a5a376d4fe1f65ea06156c3770..65601ee80ca77124758e7071a77116c3be0c184e 100644
--- a/resources/static/test/qunit/resources/state_machine_unit_test.js
+++ b/resources/static/test/qunit/resources/state_machine_unit_test.js
@@ -36,7 +36,7 @@
  * ***** END LICENSE BLOCK ***** */
 (function() {
   "use strict";
-  
+
   var bid = BrowserID,
       mediator = bid.Mediator,
       machine,
@@ -122,12 +122,19 @@
     ok(machine, "Machine has been created");
   });
 
+  test("attempt to create a state machine without a controller", function() {
+    raises(function() {
+      var badmachine = bid.StateMachine.create();
+      badmachine.start();
+    }, "start: controller must be specified", "creating a state machine without a controller fails");
+  });
+
   test("offline does offline", function() {
     mediator.publish("offline");
 
     equal(controllerMock.offline, true, "controller is offline");
   });
-  
+
   test("user_staged", function() {
     // XXX rename user_staged to confirm_user or something to that effect.
     mediator.publish("user_staged", {
@@ -206,7 +213,7 @@
   test("cancel_state", function() {
     mediator.publish("add_email");
     mediator.publish("email_staged", {
-      email: "testuser@testuser.com" 
+      email: "testuser@testuser.com"
     });
 
     controllerMock.requestAddEmail = false;
@@ -221,9 +228,9 @@
     ok(controllerMock.notMe, "notMe has been called");
   });
 
-  test("auth", function() {
-    mediator.publish("auth", {
-      email: "testuser@testuser.com" 
+  test("authenticate", function() {
+    mediator.publish("authenticate", {
+      email: "testuser@testuser.com"
     });
 
     equal(controllerMock.email, "testuser@testuser.com", "authenticate with testuser@testuser.com");
diff --git a/resources/static/test/qunit/shared/network_unit_test.js b/resources/static/test/qunit/shared/network_unit_test.js
index 84028ca9b06735e4674b6c25686bba34e8506e2b..d8d3fa46745c3111a52e938a9058458ebe17e288 100644
--- a/resources/static/test/qunit/shared/network_unit_test.js
+++ b/resources/static/test/qunit/shared/network_unit_test.js
@@ -640,15 +640,26 @@
 
   });
 
-  /*
-  wrappedAsyncTest("body offline message triggers offline message", function() {
-    mediator.subscribe("offline", function() {
-      ok(true, "offline event caught and application notified");
-      start();
+  wrappedAsyncTest("codeVersion", function() {
+    network.codeVersion(function onComplete(version) {
+      equal(version, "ABC123", "version returned properly");
+      wrappedStart();
+    }, function onFailure() {
+      ok(false, "unexpected failure");
+      wrappedStart();
+    });
+  });
+
+  wrappedAsyncTest("codeVersion with XHR error", function() {
+    xhr.useResult("contextAjaxError");
+
+    network.codeVersion(function onComplete(version) {
+      ok(false, "XHR failure should never call complete");
+      wrappedStart();
+    }, function onFailure() {
+      ok(true, "XHR fialure should always return failure");
+      wrappedStart();
     });
 
-    var evt = $.Event("offline");
-    $("body").trigger(evt);
   });
-  */
 }());
diff --git a/resources/static/test/qunit/testHelpers/helpers.js b/resources/static/test/qunit/testHelpers/helpers.js
index a55d948047a8ce038d30367496123d694c9a6cca..d7ca0c676660680ba8737649b1fa02ba827b4cfc 100644
--- a/resources/static/test/qunit/testHelpers/helpers.js
+++ b/resources/static/test/qunit/testHelpers/helpers.js
@@ -28,6 +28,12 @@
     calls = {};
   }
 
+  function checkNetworkError() {
+    ok($("#error .contents").text().length, "contents have been written");
+    ok($("#error #action").text().length, "action contents have been written");
+    ok($("#error #network").text().length, "network contents have been written");
+  }
+
   BrowserID.TestHelpers = {
     setup: function() {
       network.setXHR(xhr);
@@ -58,6 +64,7 @@
     register: register,
     errorVisible: function() {
       return screens.error.visible;
-    }
+    },
+    checkNetworkError: checkNetworkError
   };
 }());
diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs
index 06753109d0c09b477df753f0f28713dd4682a8cd..4cacf37b83fa947c7350072755b18d457584ca5e 100644
--- a/resources/views/dialog_layout.ejs
+++ b/resources/views/dialog_layout.ejs
@@ -50,7 +50,6 @@
           <script type="text/javascript" src="/dialog/production.js"></script>
 
         <% } else { %>
-          <script type="text/javascript" src="/dialog/resources/channel.js"></script>
           <script type="text/javascript" src="/lib/jquery-1.6.2.min.js"></script>
           <script type="text/javascript" src="/lib/jschannel.js"></script>
           <script type="text/javascript" src="/lib/underscore-min.js"></script>
@@ -76,10 +75,15 @@
           <script type="text/javascript" src="/shared/browser-support.js"></script>
           <script type="text/javascript" src="/shared/wait-messages.js"></script>
           <script type="text/javascript" src="/shared/helpers.js"></script>
+
           <script type="text/javascript" src="/dialog/resources/internal_api.js"></script>
+          <script type="text/javascript" src="/dialog/resources/channel.js"></script>
           <script type="text/javascript" src="/dialog/resources/helpers.js"></script>
           <script type="text/javascript" src="/dialog/resources/state_machine.js"></script>
+
           <script type="text/javascript" src="/dialog/controllers/page.js"></script>
+          <script type="text/javascript" src="/dialog/controllers/code_check.js"></script>
+          <script type="text/javascript" src="/dialog/controllers/actions.js"></script>
           <script type="text/javascript" src="/dialog/controllers/dialog.js"></script>
           <script type="text/javascript" src="/dialog/controllers/authenticate.js"></script>
           <script type="text/javascript" src="/dialog/controllers/forgotpassword.js"></script>
diff --git a/scripts/compress.sh b/scripts/compress.sh
index 65176a3332a7dff2264c6d66f956fa5b0d0db3ff..9f0eefc285b70a58dfa44f2cb90a59fc7f9755f1 100755
--- a/scripts/compress.sh
+++ b/scripts/compress.sh
@@ -42,7 +42,7 @@ cp shared/templates.js shared/templates.js.orig
 cp dialog/views/templates.js shared/templates.js
 
 # produce the dialog js
-cat dialog/resources/channel.js lib/jquery-1.6.2.min.js lib/jschannel.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js shared/javascript-extensions.js shared/mediator.js shared/class.js shared/storage.js shared/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/tooltip.js shared/validation.js shared/network.js shared/user.js shared/error-messages.js shared/browser-support.js shared/wait-messages.js shared/helpers.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state_machine.js dialog/controllers/page.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgotpassword.js dialog/controllers/checkregistration.js dialog/controllers/pickemail.js dialog/controllers/addemail.js dialog/controllers/required_email.js dialog/start.js > dialog/production.js
+cat lib/jquery-1.6.2.min.js lib/jschannel.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js shared/javascript-extensions.js shared/mediator.js shared/class.js shared/storage.js shared/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/tooltip.js shared/validation.js shared/network.js shared/user.js shared/error-messages.js shared/browser-support.js shared/wait-messages.js shared/helpers.js dialog/resources/channel.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state_machine.js dialog/controllers/page.js dialog/controllers/code_check.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgotpassword.js dialog/controllers/checkregistration.js dialog/controllers/pickemail.js dialog/controllers/addemail.js dialog/controllers/required_email.js dialog/start.js > dialog/production.js
 
 # produce the non interactive frame js
 cat lib/jquery-1.6.2.min.js lib/jschannel.js lib/underscore-min.js lib/vepbundle.js shared/javascript-extensions.js shared/browserid.js shared/storage.js shared/network.js shared/user.js communication_iframe/start.js > communication_iframe/production.js