Bug 1146904: Add the FxAccountsWebChannel to drive Sync
authorShane Tomlinson <stomlinson@mozilla.com>
Sat, 11 Apr 2015 13:23:08 +0100
changeset 274024 eac6ac60b5e648bd0bb52a8a085f73491bf66faa
parent 274023 feefdfa526a80bc51010582ca8eae5263735a657
child 274025 cd19fda2d5f84b223342d96af5c311ed23a9e25d
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1146904
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1146904: Add the FxAccountsWebChannel to drive Sync * * * fix tests From 6bb486068a8b002f222f4658989968d86df9eff1 Mon Sep 17 00:00:00 2001 --- .../test/general/browser_fxa_web_channel.html | 2 +- .../test/general/browser_fxa_web_channel.js | 131 +++++++++++---------- services/fxaccounts/tests/xpcshell/test_profile.js | 26 ---- 3 files changed, 70 insertions(+), 89 deletions(-)
browser/app/profile/firefox.js
browser/base/content/browser-fxaccounts.js
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_fxa_profile_channel.html
browser/base/content/test/general/browser_fxa_profile_channel.js
browser/base/content/test/general/browser_fxa_web_channel.html
browser/base/content/test/general/browser_fxa_web_channel.js
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsCommon.js
services/fxaccounts/FxAccountsProfile.jsm
services/fxaccounts/FxAccountsProfileChannel.jsm
services/fxaccounts/FxAccountsWebChannel.jsm
services/fxaccounts/moz.build
services/fxaccounts/tests/xpcshell/test_profile.js
services/fxaccounts/tests/xpcshell/test_profile_channel.js
services/fxaccounts/tests/xpcshell/test_web_channel.js
services/fxaccounts/tests/xpcshell/xpcshell.ini
testing/profiles/prefs_general.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1785,16 +1785,19 @@ pref("identity.fxaccounts.remote.signup.
 
 // The URL where remote content that forces re-authentication for Firefox Accounts
 // should be fetched.  Must use HTTPS.
 pref("identity.fxaccounts.remote.force_auth.uri", "https://accounts.firefox.com/force_auth?service=sync&context=fx_desktop_v1");
 
 // The remote content URL shown for signin in. Must use HTTPS.
 pref("identity.fxaccounts.remote.signin.uri", "https://accounts.firefox.com/signin?service=sync&context=fx_desktop_v1");
 
+// The remote content URL where FxAccountsWebChannel messages originate.
+pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com/");
+
 // The URL we take the user to when they opt to "manage" their Firefox Account.
 // Note that this will always need to be in the same TLD as the
 // "identity.fxaccounts.remote.signup.uri" pref.
 pref("identity.fxaccounts.settings.uri", "https://accounts.firefox.com/settings");
 
 // The remote URL of the FxA Profile Server
 pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
 
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -83,16 +83,24 @@ let gFxAccounts = {
     addEventListener("activate", this);
     gNavToolbox.addEventListener("customizationstarting", this);
     gNavToolbox.addEventListener("customizationending", this);
 
     // Request the current Legacy-Sync-to-FxA migration status.  We'll be
     // notified of fxa-migration:state-changed in response if necessary.
     Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
 
+    let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
+    // The FxAccountsWebChannel listens for events and updates
+    // the state machine accordingly.
+    let fxAccountsWebChannel = new FxAccountsWebChannel({
+      content_uri: contentUri,
+      channel_id: this.FxAccountsCommon.WEBCHANNEL_ID
+    });
+
     this._initialized = true;
 
     this.updateUI();
   },
 
   uninit: function () {
     if (!this._initialized) {
       return;
@@ -398,8 +406,11 @@ let gFxAccounts = {
 };
 
 XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () {
   return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
 });
 
 XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator",
   "resource://services-sync/FxaMigrator.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsWebChannel",
+  "resource://gre/modules/FxAccountsWebChannel.jsm");
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -8,17 +8,17 @@ support-files =
   authenticate.sjs
   aboutHome_content_script.js
   browser_bug479408_sample.html
   browser_bug678392-1.html
   browser_bug678392-2.html
   browser_bug970746.xhtml
   browser_fxa_oauth.html
   browser_fxa_oauth_with_keys.html
-  browser_fxa_profile_channel.html
+  browser_fxa_web_channel.html
   browser_registerProtocolHandler_notification.html
   browser_ssl_error_reports_content.js
   browser_star_hsts.sjs
   browser_tab_dragdrop2_frame1.xul
   browser_web_channel.html
   bug592338.html
   bug792517-2.html
   bug792517.html
@@ -297,17 +297,17 @@ skip-if = true # browser_drag.js is disa
 [browser_favicon_change.js]
 [browser_favicon_change_not_in_document.js]
 skip-if = e10s
 [browser_findbarClose.js]
 [browser_fullscreen-window-open.js]
 skip-if = buildapp == 'mulet' || e10s || os == "linux" # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly. Linux: Intermittent failures - bug 941575.
 [browser_fxa_migrate.js]
 [browser_fxa_oauth.js]
-[browser_fxa_profile_channel.js]
+[browser_fxa_web_channel.js]
 [browser_gestureSupport.js]
 skip-if = e10s # Bug 863514 - no gesture support.
 [browser_getshortcutoruri.js]
 [browser_hide_removing.js]
 [browser_homeDrop.js]
 skip-if = buildapp == 'mulet'
 [browser_identity_UI.js]
 skip-if = e10s && debug # Seeing lots of timeouts (bug 1095517)
rename from browser/base/content/test/general/browser_fxa_profile_channel.html
rename to browser/base/content/test/general/browser_fxa_web_channel.html
--- a/browser/base/content/test/general/browser_fxa_profile_channel.html
+++ b/browser/base/content/test/general/browser_fxa_web_channel.html
@@ -1,26 +1,98 @@
 <!DOCTYPE html>
 <html>
 <head>
   <meta charset="utf-8">
-  <title>fxa_profile_channel_test</title>
+  <title>fxa_web_channel_test</title>
 </head>
 <body>
 <script>
+  var webChannelId = "account_updates_test";
+
   window.onload = function(){
+    var testName = window.location.search.replace(/^\?/, "");
+
+    switch(testName) {
+      case "profile_change":
+        test_profile_change();
+        break;
+      case "login":
+        test_login();
+        break;
+      case "can_link_account":
+        test_can_link_account();
+        break;
+    }
+  };
+
+  function test_profile_change() {
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
       detail: {
-        id: "account_updates",
+        id: webChannelId,
         message: {
           command: "profile:change",
           data: {
             uid: "abc123",
           },
         },
       },
     });
 
     window.dispatchEvent(event);
-  };
+  }
+
+  function test_login() {
+    var event = new window.CustomEvent("WebChannelMessageToChrome", {
+      detail: {
+        id: webChannelId,
+        message: {
+          command: "fxaccounts:login",
+          data: {
+            authAt: Date.now(),
+            email: "testuser@testuser.com",
+            keyFetchToken: 'key_fetch_token',
+            sessionToken: 'session_token',
+            uid: 'uid',
+            unwrapBKey: 'unwrap_b_key',
+            verified: true,
+          },
+          messageId: 1,
+        },
+      },
+    });
+
+    window.dispatchEvent(event);
+  }
+
+  function test_can_link_account() {
+    window.addEventListener("WebChannelMessageToContent", function (e) {
+      // echo any responses from the browser back to the tests on the
+      // fxaccounts_webchannel_response_echo WebChannel. The tests are
+      // listening for events and do the appropriate checks.
+      var event = new window.CustomEvent("WebChannelMessageToChrome", {
+        detail: {
+          id: 'fxaccounts_webchannel_response_echo',
+          message: e.detail.message,
+        }
+      });
+
+      window.dispatchEvent(event);
+    }, true);
+
+    var event = new window.CustomEvent("WebChannelMessageToChrome", {
+      detail: {
+        id: webChannelId,
+        message: {
+          command: "fxaccounts:can_link_account",
+          data: {
+            email: "testuser@testuser.com",
+          },
+          messageId: 2,
+        },
+      },
+    });
+
+    window.dispatchEvent(event);
+  }
 </script>
 </body>
 </html>
rename from browser/base/content/test/general/browser_fxa_profile_channel.js
rename to browser/base/content/test/general/browser_fxa_web_channel.js
--- a/browser/base/content/test/general/browser_fxa_profile_channel.js
+++ b/browser/base/content/test/general/browser_fxa_web_channel.js
@@ -4,49 +4,124 @@
 
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
   return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
 });
 
-XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileChannel",
-  "resource://gre/modules/FxAccountsProfileChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
+                                  "resource://gre/modules/WebChannel.jsm");
 
-const HTTP_PATH = "http://example.com";
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsWebChannel",
+  "resource://gre/modules/FxAccountsWebChannel.jsm");
+
+const TEST_HTTP_PATH = "http://example.com";
+const TEST_BASE_URL = TEST_HTTP_PATH + "/browser/browser/base/content/test/general/browser_fxa_web_channel.html";
+const TEST_CHANNEL_ID = "account_updates_test";
 
 let gTests = [
   {
-    desc: "FxA Profile Channel - should receive message about account updates",
+    desc: "FxA Web Channel - should receive message about profile changes",
     run: function* () {
-      return new Promise(function(resolve, reject) {
-        let tabOpened = false;
-        let properUrl = "http://example.com/browser/browser/base/content/test/general/browser_fxa_profile_channel.html";
+      let client = new FxAccountsWebChannel({
+        content_uri: TEST_HTTP_PATH,
+        channel_id: TEST_CHANNEL_ID,
+      });
+      let promiseObserver = new Promise((resolve, reject) => {
+        makeObserver(FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
+          Assert.equal(data, "abc123");
+          client.tearDown();
+          resolve();
+        });
+      });
+
+      yield BrowserTestUtils.withNewTab({
+        gBrowser: gBrowser,
+        url: TEST_BASE_URL + "?profile_change"
+      }, function* () {
+        yield promiseObserver;
+      });
+    }
+  },
+  {
+    desc: "fxa web channel - login messages should notify the fxAccounts object",
+    run: function* () {
+
+      let promiseLogin = new Promise((resolve, reject) => {
+        let login = (accountData) => {
+          Assert.equal(typeof accountData.authAt, 'number');
+          Assert.equal(accountData.email, 'testuser@testuser.com');
+          Assert.equal(accountData.keyFetchToken, 'key_fetch_token');
+          Assert.equal(accountData.sessionToken, 'session_token');
+          Assert.equal(accountData.uid, 'uid');
+          Assert.equal(accountData.unwrapBKey, 'unwrap_b_key');
+          Assert.equal(accountData.verified, true);
 
-        waitForTab(function (tab) {
-          Assert.ok("Tab successfully opened");
-          let match = gBrowser.currentURI.spec == properUrl;
-          Assert.ok(match);
+          client.tearDown();
+          resolve();
+        };
+
+        let client = new FxAccountsWebChannel({
+          content_uri: TEST_HTTP_PATH,
+          channel_id: TEST_CHANNEL_ID,
+          helpers: {
+            login: login
+          }
+        });
+      });
 
-          tabOpened = true;
+      yield BrowserTestUtils.withNewTab({
+        gBrowser: gBrowser,
+        url: TEST_BASE_URL + "?login"
+      }, function* () {
+        yield promiseLogin;
+      });
+    }
+  },
+  {
+    desc: "fxa web channel - can_link_account messages should respond",
+    run: function* () {
+      let properUrl = TEST_BASE_URL + "?can_link_account";
+
+      let promiseEcho = new Promise((resolve, reject) => {
+
+        let webChannelOrigin = Services.io.newURI(properUrl, null, null);
+        // responses sent to content are echoed back over the
+        // `fxaccounts_webchannel_response_echo` channel. Ensure the
+        // fxaccounts:can_link_account message is responded to.
+        let echoWebChannel = new WebChannel('fxaccounts_webchannel_response_echo', webChannelOrigin);
+        echoWebChannel.listen((webChannelId, message, target) => {
+          Assert.equal(message.command, 'fxaccounts:can_link_account');
+          Assert.equal(message.messageId, 2);
+          Assert.equal(message.data.ok, true);
+
+          client.tearDown();
+          echoWebChannel.stopListening();
+
+          resolve();
         });
 
-        let client = new FxAccountsProfileChannel({
-          content_uri: HTTP_PATH,
+        let client = new FxAccountsWebChannel({
+          content_uri: TEST_HTTP_PATH,
+          channel_id: TEST_CHANNEL_ID,
+          helpers: {
+            shouldAllowRelink(acctName) {
+              return acctName === 'testuser@testuser.com';
+            }
+          }
         });
+      });
 
-        makeObserver(FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
-          Assert.ok(tabOpened);
-          Assert.equal(data, "abc123");
-          resolve();
-          gBrowser.removeCurrentTab();
-        });
-
-        gBrowser.selectedTab = gBrowser.addTab(properUrl);
+      yield BrowserTestUtils.withNewTab({
+        gBrowser: gBrowser,
+        url: properUrl
+      }, function* () {
+        yield promiseEcho;
       });
     }
   }
 ]; // gTests
 
 function makeObserver(aObserveTopic, aObserveFunc) {
   let callback = function (aSubject, aTopic, aData) {
     if (aTopic == aObserveTopic) {
@@ -58,28 +133,16 @@ function makeObserver(aObserveTopic, aOb
   function removeMe() {
     Services.obs.removeObserver(callback, aObserveTopic);
   }
 
   Services.obs.addObserver(callback, aObserveTopic, false);
   return removeMe;
 }
 
-function waitForTab(aCallback) {
-  let container = gBrowser.tabContainer;
-  container.addEventListener("TabOpen", function tabOpener(event) {
-    container.removeEventListener("TabOpen", tabOpener, false);
-    gBrowser.addEventListener("load", function listener() {
-      gBrowser.removeEventListener("load", listener, true);
-      let tab = event.target;
-      aCallback(tab);
-    }, true);
-  }, false);
-}
-
 function test() {
   waitForExplicitFinish();
 
   Task.spawn(function () {
     for (let test of gTests) {
       info("Running: " + test.desc);
       yield test.run();
     }
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -1401,19 +1401,19 @@ FxAccountsInternal.prototype = {
     return reason;
   },
 
   /**
    * Get the user's account and profile data
    *
    * @param options
    *        {
-   *          contentUrl: (string) Used by the FxAccountsProfileChannel.
+   *          contentUrl: (string) Used by the FxAccountsWebChannel.
    *            Defaults to pref identity.fxaccounts.settings.uri
-   *          profileServerUrl: (string) Used by the FxAccountsProfileChannel.
+   *          profileServerUrl: (string) Used by the FxAccountsWebChannel.
    *            Defaults to pref identity.fxaccounts.remote.profile.uri
    *        }
    *
    * @return Promise.<object | Error>
    *        The promise resolves to an accountData object with extra profile
    *        information such as profileImageUrl, or rejects with
    *        an error object ({error: ERROR, details: {}}) of the following:
    *          INVALID_PARAMETER
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -95,18 +95,18 @@ exports.ON_PROFILE_CHANGE_NOTIFICATION =
 
 // UI Requests.
 exports.UI_REQUEST_SIGN_IN_FLOW = "signInFlow";
 exports.UI_REQUEST_REFRESH_AUTH = "refreshAuthentication";
 
 // The OAuth client ID for Firefox Desktop
 exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776";
 
-// Profile WebChannel ID
-exports.PROFILE_WEBCHANNEL_ID = "account_updates";
+// Firefox Accounts WebChannel ID
+exports.WEBCHANNEL_ID = "account_updates";
 
 // Server errno.
 // From https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format
 exports.ERRNO_ACCOUNT_ALREADY_EXISTS         = 101;
 exports.ERRNO_ACCOUNT_DOES_NOT_EXIST         = 102;
 exports.ERRNO_INCORRECT_PASSWORD             = 103;
 exports.ERRNO_UNVERIFIED_ACCOUNT             = 104;
 exports.ERRNO_INVALID_VERIFICATION_CODE      = 105;
--- a/services/fxaccounts/FxAccountsProfile.jsm
+++ b/services/fxaccounts/FxAccountsProfile.jsm
@@ -19,21 +19,16 @@ const {classes: Cc, interfaces: Ci, util
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient",
   "resource://gre/modules/FxAccountsProfileClient.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileChannel",
-  "resource://gre/modules/FxAccountsProfileChannel.jsm");
-
-let fxAccountProfileChannel = null;
-
 // Based off of deepEqual from Assert.jsm
 function deepEqual(actual, expected) {
   if (actual === expected) {
     return true;
   } else if (typeof actual != "object" && typeof expected != "object") {
     return actual == expected;
   } else {
     return objEquiv(actual, expected);
@@ -126,35 +121,20 @@ this.FxAccountsProfile.prototype = {
 
   _fetchAndCacheProfile: function () {
     return this.client.fetchProfile()
       .then(profile => {
         return this._cacheProfile(profile).then(() => profile);
       });
   },
 
-  // Initialize a profile channel to listen for account changes.
-  _listenForProfileChanges: function () {
-    if (! fxAccountProfileChannel) {
-      let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri");
-
-      fxAccountProfileChannel = new FxAccountsProfileChannel({
-        content_uri: contentUri
-      });
-    }
-
-    return fxAccountProfileChannel;
-  },
-
   // Returns cached data right away if available, then fetches the latest profile
   // data in the background. After data is fetched a notification will be sent
   // out if the profile has changed.
   getProfile: function () {
-    this._listenForProfileChanges();
-
     return this._getCachedProfile()
       .then(cachedProfile => {
         if (cachedProfile) {
           this._fetchAndCacheProfile();
           return cachedProfile;
         }
         return this._fetchAndCacheProfile();
       })
rename from services/fxaccounts/FxAccountsProfileChannel.jsm
rename to services/fxaccounts/FxAccountsWebChannel.jsm
--- a/services/fxaccounts/FxAccountsProfileChannel.jsm
+++ b/services/fxaccounts/FxAccountsWebChannel.jsm
@@ -1,118 +1,313 @@
 /* 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/. */
 
 /**
- * Firefox Accounts Profile update helper.
- * Uses the WebChannel component to receive messages about account changes.
+ * Firefox Accounts Web Channel.
+ *
+ * Uses the WebChannel component to receive messages
+ * about account state changes.
  */
 
-this.EXPORTED_SYMBOLS = ["FxAccountsProfileChannel"];
+this.EXPORTED_SYMBOLS = ["FxAccountsWebChannel"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
                                   "resource://gre/modules/WebChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+                                  "resource://gre/modules/FxAccounts.jsm");
 
-const PROFILE_CHANGE_COMMAND = "profile:change";
+const COMMAND_PROFILE_CHANGE       = "profile:change";
+const COMMAND_CAN_LINK_ACCOUNT     = "fxaccounts:can_link_account";
+const COMMAND_LOGIN                = "fxaccounts:login";
+
+const PREF_LAST_FXA_USER           = "identity.fxaccounts.lastSignedInUserHash";
+const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog";
 
 /**
- * Create a new FxAccountsProfileChannel to listen to profile updates
+ * Create a new FxAccountsWebChannel to listen for account updates
  *
  * @param {Object} options Options
- *   @param {Object} options.parameters
- *     @param {String} options.parameters.content_uri
+ *   @param {Object} options
+ *     @param {String} options.content_uri
  *     The FxA Content server uri
+ *     @param {String} options.channel_id
+ *     The ID of the WebChannel
+ *     @param {String} options.helpers
+ *     Helpers functions. Should only be passed in for testing.
  * @constructor
  */
-this.FxAccountsProfileChannel = function(options) {
+this.FxAccountsWebChannel = function(options) {
   if (!options) {
     throw new Error("Missing configuration options");
   }
   if (!options["content_uri"]) {
     throw new Error("Missing 'content_uri' option");
   }
-  this.parameters = options;
+  this._contentUri = options.content_uri;
+
+  if (!options["channel_id"]) {
+    throw new Error("Missing 'channel_id' option");
+  }
+  this._webChannelId = options.channel_id;
+
+  // options.helpers is only specified by tests.
+  this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
 
   this._setupChannel();
 };
 
-this.FxAccountsProfileChannel.prototype = {
-  /**
-   * Configuration object
-   */
-  parameters: null,
+this.FxAccountsWebChannel.prototype = {
   /**
    * WebChannel that is used to communicate with content page
    */
   _channel: null,
+
+  /**
+   * Helpers interface that does the heavy lifting.
+   */
+  _helpers: null,
+
+  /**
+   * WebChannel ID.
+   */
+  _webChannelId: null,
   /**
    * WebChannel origin, used to validate origin of messages
    */
   _webChannelOrigin: null,
 
   /**
    * Release all resources that are in use.
    */
-  tearDown: function() {
+  tearDown() {
     this._channel.stopListening();
     this._channel = null;
     this._channelCallback = null;
   },
 
   /**
    * Configures and registers a new WebChannel
    *
    * @private
    */
-  _setupChannel: function() {
-    // if this.parameters.content_uri is present but not a valid URI, then this will throw an error.
+  _setupChannel() {
+    // if this.contentUri is present but not a valid URI, then this will throw an error.
     try {
-      this._webChannelOrigin = Services.io.newURI(this.parameters.content_uri, null, null);
+      this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null);
       this._registerChannel();
     } catch (e) {
       log.error(e);
       throw e;
     }
   },
 
   /**
    * Create a new channel with the WebChannelBroker, setup a callback listener
    * @private
    */
-  _registerChannel: function() {
+  _registerChannel() {
     /**
      * Processes messages that are called back from the FxAccountsChannel
      *
      * @param webChannelId {String}
      *        Command webChannelId
      * @param message {Object}
      *        Command message
-     * @param target {EventTarget}
-     *        Channel message event target
+     * @param sendingContext {Object}
+     *        Message sending context.
+     *        @param sendingContext.browser {browser}
+     *               The <browser> object that captured the
+     *               WebChannelMessageToChrome.
+     *        @param sendingContext.eventTarget {EventTarget}
+     *               The <EventTarget> where the message was sent.
+     *        @param sendingContext.principal {Principal}
+     *               The <Principal> of the EventTarget where the message was sent.
      * @private
+     *
      */
-    let listener = (webChannelId, message, target) => {
+    let listener = (webChannelId, message, sendingContext) => {
       if (message) {
+        log.debug("FxAccountsWebChannel message received", message);
         let command = message.command;
         let data = message.data;
+
         switch (command) {
-          case PROFILE_CHANGE_COMMAND:
+          case COMMAND_PROFILE_CHANGE:
             Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, data.uid);
-          break;
+            break;
+          case COMMAND_LOGIN:
+            this._helpers.login(data);
+            break;
+          case COMMAND_CAN_LINK_ACCOUNT:
+            let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
+
+            let response = {
+              command: command,
+              messageId: message.messageId,
+              data: { ok: canLinkAccount }
+            };
+
+            log.debug("FxAccountsWebChannel response", response);
+            this._channel.send(response, sendingContext);
+            break;
+          default:
+            log.warn("Unrecognized FxAccountsWebChannel command", command);
+            break;
         }
       }
     };
 
     this._channelCallback = listener;
-    this._channel = new WebChannel(PROFILE_WEBCHANNEL_ID, this._webChannelOrigin);
-    this._channel.listen(this._channelCallback);
-    log.debug("Channel registered: " + PROFILE_WEBCHANNEL_ID + " with origin " + this._webChannelOrigin.prePath);
+    this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
+    this._channel.listen(listener);
+    log.debug("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
   }
+};
 
+this.FxAccountsWebChannelHelpers = function(options) {
+  options = options || {};
+
+  this._fxAccounts = options.fxAccounts || fxAccounts;
 };
+
+this.FxAccountsWebChannelHelpers.prototype = {
+  // If the last fxa account used for sync isn't this account, we display
+  // a modal dialog checking they really really want to do this...
+  // (This is sync-specific, so ideally would be in sync's identity module,
+  // but it's a little more seamless to do here, and sync is currently the
+  // only fxa consumer, so...
+  shouldAllowRelink(acctName) {
+    return !this._needRelinkWarning(acctName) ||
+            this._promptForRelink(acctName);
+  },
+
+  /**
+   * New users are asked in the content server whether they want to
+   * customize which data should be synced. The user is only shown
+   * the dialog listing the possible data types upon verification.
+   *
+   * Save a bit into prefs that is read on verification to see whether
+   * to show the list of data types that can be saved.
+   */
+  setShowCustomizeSyncPref(showCustomizeSyncPref) {
+    Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, showCustomizeSyncPref);
+  },
+
+  getShowCustomizeSyncPref(showCustomizeSyncPref) {
+    return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
+  },
+
+  /**
+   * stores sync login info it in the fxaccounts service
+   *
+   * @param accountData the user's account data and credentials
+   */
+  login(accountData) {
+    if (accountData.customizeSync) {
+      this.setShowCustomizeSyncPref(true);
+      delete accountData.customizeSync;
+    }
+
+    // the user has already been shown the "can link account"
+    // screen. No need to keep this data around.
+    delete accountData.verifiedCanLinkAccount;
+
+    // Remember who it was so we can log out next time.
+    this.setPreviousAccountNameHashPref(accountData.email);
+
+    // A sync-specific hack - we want to ensure sync has been initialized
+    // before we set the signed-in user.
+    let xps = Cc["@mozilla.org/weave/service;1"]
+              .getService(Ci.nsISupports)
+              .wrappedJSObject;
+    return xps.whenLoaded().then(() => {
+      return this._fxAccounts.setSignedInUser(accountData);
+    });
+  },
+
+  /**
+   * Get the hash of account name of the previously signed in account
+   */
+  getPreviousAccountNameHashPref() {
+    try {
+      return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
+    } catch (_) {
+      return "";
+    }
+  },
+
+  /**
+   * Given an account name, set the hash of the previously signed in account
+   *
+   * @param acctName the account name of the user's account.
+   */
+  setPreviousAccountNameHashPref(acctName) {
+    let string = Cc["@mozilla.org/supports-string;1"]
+                 .createInstance(Ci.nsISupportsString);
+    string.data = this.sha256(acctName);
+    Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string);
+  },
+
+  /**
+   * Given a string, returns the SHA265 hash in base64
+   */
+  sha256(str) {
+    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                      .createInstance(Ci.nsIScriptableUnicodeConverter);
+    converter.charset = "UTF-8";
+    // Data is an array of bytes.
+    let data = converter.convertToByteArray(str, {});
+    let hasher = Cc["@mozilla.org/security/hash;1"]
+                   .createInstance(Ci.nsICryptoHash);
+    hasher.init(hasher.SHA256);
+    hasher.update(data, data.length);
+
+    return hasher.finish(true);
+  },
+
+  /**
+   * If a user signs in using a different account, the data from the
+   * previous account and the new account will be merged. Ask the user
+   * if they want to continue.
+   *
+   * @private
+   */
+  _needRelinkWarning(acctName) {
+    let prevAcctHash = this.getPreviousAccountNameHashPref();
+    return prevAcctHash && prevAcctHash != this.sha256(acctName);
+  },
+
+  /**
+   * Show the user a warning dialog that the data from the previous account
+   * and the new account will be merged.
+   *
+   * @private
+   */
+  _promptForRelink(acctName) {
+    let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
+    let continueLabel = sb.GetStringFromName("continue.label");
+    let title = sb.GetStringFromName("relinkVerify.title");
+    let description = sb.formatStringFromName("relinkVerify.description",
+                                              [acctName], 1);
+    let body = sb.GetStringFromName("relinkVerify.heading") +
+               "\n\n" + description;
+    let ps = Services.prompt;
+    let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) +
+                      (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) +
+                      ps.BUTTON_POS_1_DEFAULT;
+
+    // If running in context of the browser chrome, window does not exist.
+    var targetWindow = typeof window === 'undefined' ? null : window;
+    let pressed = Services.prompt.confirmEx(targetWindow, title, body, buttonFlags,
+                                       continueLabel, null, null, null,
+                                       {});
+    return pressed === 0; // 0 is the "continue" button
+  }
+};
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -12,18 +12,18 @@ XPCSHELL_TESTS_MANIFESTS += ['tests/xpcs
 
 EXTRA_JS_MODULES += [
   'Credentials.jsm',
   'FxAccountsClient.jsm',
   'FxAccountsCommon.js',
   'FxAccountsOAuthClient.jsm',
   'FxAccountsOAuthGrantClient.jsm',
   'FxAccountsProfile.jsm',
-  'FxAccountsProfileChannel.jsm',
   'FxAccountsProfileClient.jsm',
+  'FxAccountsWebChannel.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
   'FxAccounts.jsm',
 ]
 
 # For now, we will only be using the FxA manager in B2G.
 if CONFIG['MOZ_B2G']:
--- a/services/fxaccounts/tests/xpcshell/test_profile.js
+++ b/services/fxaccounts/tests/xpcshell/test_profile.js
@@ -149,30 +149,16 @@ add_test(function fetchAndCacheProfile_o
 
   return profile._fetchAndCacheProfile()
     .then(result => {
       do_check_eq(result.avatar, "myimg");
       run_next_test();
     });
 });
 
-
-add_test(function profile_channel() {
-  let profile = new FxAccountsProfile(mockAccountData(), PROFILE_CLIENT_OPTIONS);
-
-  let channel = profile._listenForProfileChanges();
-  do_check_true(!!channel);
-
-  let channel2 = profile._listenForProfileChanges();
-
-  do_check_eq(channel, channel2);
-
-  run_next_test();
-});
-
 add_test(function tearDown_ok() {
   let profile = new FxAccountsProfile(mockAccountData(), PROFILE_CLIENT_OPTIONS);
 
   do_check_true(!!profile.client);
   do_check_true(!!profile.currentAccountState);
 
   profile.tearDown();
   do_check_null(profile.currentAccountState);
@@ -180,68 +166,56 @@ add_test(function tearDown_ok() {
 
   run_next_test();
 });
 
 add_test(function getProfile_ok() {
   let cachedUrl = "myurl";
   let accountData = mockAccountData();
   let didFetch = false;
-  let didListen = false;
 
   let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
   profile._getCachedProfile = function () {
     return Promise.resolve({ avatar: cachedUrl });
   };
 
   profile._fetchAndCacheProfile = function () {
     didFetch = true;
   };
-  profile._listenForProfileChanges = function () {
-    didListen = true;
-  };
 
   return profile.getProfile()
     .then(result => {
       do_check_eq(result.avatar, cachedUrl);
       do_check_true(didFetch);
-      do_check_true(didListen);
       run_next_test();
     });
 });
 
 add_test(function getProfile_no_cache() {
   let fetchedUrl = "newUrl";
   let accountData = mockAccountData();
-  let didListen = false;
 
   let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
   profile._getCachedProfile = function () {
     return Promise.resolve();
   };
 
   profile._fetchAndCacheProfile = function () {
     return Promise.resolve({ avatar: fetchedUrl });
   };
-  profile._listenForProfileChanges = function () {
-    didListen = true;
-  };
 
   return profile.getProfile()
     .then(result => {
       do_check_eq(result.avatar, fetchedUrl);
-      do_check_true(didListen);
       run_next_test();
     });
 });
 
 add_test(function getProfile_has_cached_fetch_deleted() {
   let cachedUrl = "myurl";
-  let didFetch = false;
-  let didListen = false;
 
   let client = mockClient();
   client.fetchProfile = function () {
     return Promise.resolve({ avatar: null });
   };
 
   let accountData = mockAccountData();
   accountData.getUserAccountData = function () {
rename from services/fxaccounts/tests/xpcshell/test_profile_channel.js
rename to services/fxaccounts/tests/xpcshell/test_web_channel.js
--- a/services/fxaccounts/tests/xpcshell/test_profile_channel.js
+++ b/services/fxaccounts/tests/xpcshell/test_web_channel.js
@@ -1,47 +1,208 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
-Cu.import("resource://gre/modules/FxAccountsProfileChannel.jsm");
+const { FxAccountsWebChannel, FxAccountsWebChannelHelpers } =
+    Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
 
 const URL_STRING = "https://example.com";
 
+const mockSendingContext = {
+  browser: {},
+  principal: {},
+  eventTarget: {}
+};
+
 add_test(function () {
   validationHelper(undefined,
   "Error: Missing configuration options");
 
-  validationHelper({},
+  validationHelper({
+    channel_id: WEBCHANNEL_ID
+  },
   "Error: Missing 'content_uri' option");
 
-  validationHelper({ content_uri: 'bad uri' },
+  validationHelper({
+    content_uri: 'bad uri',
+    channel_id: WEBCHANNEL_ID
+  },
   /NS_ERROR_MALFORMED_URI/);
 
+  validationHelper({
+    content_uri: URL_STRING
+  },
+  'Error: Missing \'channel_id\' option');
+
   run_next_test();
 });
 
-add_test(function () {
+add_test(function test_profile_image_change_message() {
   var mockMessage = {
     command: "profile:change",
     data: { uid: "foo" }
   };
 
   makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
     do_check_eq(data, "foo");
     run_next_test();
   });
 
-  var channel = new FxAccountsProfileChannel({
+  var channel = new FxAccountsWebChannel({
+    channel_id: WEBCHANNEL_ID,
+    content_uri: URL_STRING
+  });
+
+  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
+});
+
+add_test(function test_login_message() {
+  let mockMessage = {
+    command: 'fxaccounts:login',
+    data: { email: 'testuser@testuser.com' }
+  };
+
+  let channel = new FxAccountsWebChannel({
+    channel_id: WEBCHANNEL_ID,
+    content_uri: URL_STRING,
+    helpers: {
+      login: function (accountData) {
+        do_check_eq(accountData.email, 'testuser@testuser.com');
+        run_next_test();
+      }
+    }
+  });
+
+  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
+});
+
+add_test(function test_can_link_account_message() {
+  let mockMessage = {
+    command: 'fxaccounts:can_link_account',
+    data: { email: 'testuser@testuser.com' }
+  };
+
+  let channel = new FxAccountsWebChannel({
+    channel_id: WEBCHANNEL_ID,
+    content_uri: URL_STRING,
+    helpers: {
+      shouldAllowRelink: function (email) {
+        do_check_eq(email, 'testuser@testuser.com');
+        run_next_test();
+      }
+    }
+  });
+
+  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
+});
+
+add_test(function test_unrecognized_message() {
+  let mockMessage = {
+    command: 'fxaccounts:unrecognized',
+    data: {}
+  };
+
+  let channel = new FxAccountsWebChannel({
+    channel_id: WEBCHANNEL_ID,
     content_uri: URL_STRING
   });
 
-  channel._channelCallback(PROFILE_WEBCHANNEL_ID, mockMessage);
+  // no error is expected.
+  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
+  run_next_test();
+});
+
+
+add_test(function test_helpers_should_allow_relink_same_email() {
+  let helpers = new FxAccountsWebChannelHelpers();
+
+  helpers.setPreviousAccountNameHashPref('testuser@testuser.com');
+  do_check_true(helpers.shouldAllowRelink('testuser@testuser.com'));
+
+  run_next_test();
+});
+
+add_test(function test_helpers_should_allow_relink_different_email() {
+  let helpers = new FxAccountsWebChannelHelpers();
+
+  helpers.setPreviousAccountNameHashPref('testuser@testuser.com');
+
+  helpers._promptForRelink = (acctName) => {
+    return acctName === 'allowed_to_relink@testuser.com';
+  };
+
+  do_check_true(helpers.shouldAllowRelink('allowed_to_relink@testuser.com'));
+  do_check_false(helpers.shouldAllowRelink('not_allowed_to_relink@testuser.com'));
+
+  run_next_test();
+});
+
+add_test(function test_helpers_login_without_customize_sync() {
+  let helpers = new FxAccountsWebChannelHelpers({
+    fxAccounts: {
+      setSignedInUser: function(accountData) {
+        // ensure fxAccounts is informed of the new user being signed in.
+        do_check_eq(accountData.email, 'testuser@testuser.com');
+
+        // verifiedCanLinkAccount should be stripped in the data.
+        do_check_false('verifiedCanLinkAccount' in accountData);
+
+        // the customizeSync pref should not update
+        do_check_false(helpers.getShowCustomizeSyncPref());
+
+        // previously signed in user preference is updated.
+        do_check_eq(helpers.getPreviousAccountNameHashPref(), helpers.sha256('testuser@testuser.com'));
+
+        run_next_test();
+      }
+    }
+  });
+
+  // the show customize sync pref should stay the same
+  helpers.setShowCustomizeSyncPref(false);
+
+  // ensure the previous account pref is overwritten.
+  helpers.setPreviousAccountNameHashPref('lastuser@testuser.com');
+
+  helpers.login({
+    email: 'testuser@testuser.com',
+    verifiedCanLinkAccount: true,
+    customizeSync: false
+  });
+});
+
+add_test(function test_helpers_login_with_customize_sync() {
+  let helpers = new FxAccountsWebChannelHelpers({
+    fxAccounts: {
+      setSignedInUser: function(accountData) {
+        // ensure fxAccounts is informed of the new user being signed in.
+        do_check_eq(accountData.email, 'testuser@testuser.com');
+
+        // customizeSync should be stripped in the data.
+        do_check_false('customizeSync' in accountData);
+
+        // the customizeSync pref should not update
+        do_check_true(helpers.getShowCustomizeSyncPref());
+
+        run_next_test();
+      }
+    }
+  });
+
+  // the customize sync pref should be overwritten
+  helpers.setShowCustomizeSyncPref(false);
+
+  helpers.login({
+    email: 'testuser@testuser.com',
+    verifiedCanLinkAccount: true,
+    customizeSync: true
+  });
 });
 
 function run_test() {
   run_next_test();
 }
 
 function makeObserver(aObserveTopic, aObserveFunc) {
   let callback = function (aSubject, aTopic, aData) {
@@ -58,17 +219,17 @@ function makeObserver(aObserveTopic, aOb
   }
 
   Services.obs.addObserver(callback, aObserveTopic, false);
   return removeMe;
 }
 
 function validationHelper(params, expected) {
   try {
-    new FxAccountsProfileChannel(params);
+    new FxAccountsWebChannel(params);
   } catch (e) {
     if (typeof expected === 'string') {
       return do_check_eq(e.toString(), expected);
     } else {
       return do_check_true(e.toString().match(expected));
     }
   }
   throw new Error("Validation helper error");
--- a/services/fxaccounts/tests/xpcshell/xpcshell.ini
+++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini
@@ -13,10 +13,11 @@ skip-if = appname == 'b2g' # login manag
 skip-if = appname != 'b2g'
 reason = FxAccountsManager is only available for B2G for now
 [test_oauth_client.js]
 [test_oauth_grant_client.js]
 [test_oauth_grant_client_server.js]
 [test_oauth_tokens.js]
 [test_oauth_token_storage.js]
 [test_profile_client.js]
-[test_profile_channel.js]
+[test_web_channel.js]
+skip-if = appname == 'b2g' # fxa web channels only used on desktop
 [test_profile.js]
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -238,16 +238,17 @@ user_pref('toolkit.telemetry.test.pref2'
 // resolves and accepts requests, even if they all fail.
 user_pref('identity.fxaccounts.auth.uri', 'https://%(server)s/fxa-dummy/');
 
 // Ditto for all the other Firefox accounts URIs used for about:accounts et al.:
 user_pref("identity.fxaccounts.remote.signup.uri", "https://%(server)s/fxa-signup");
 user_pref("identity.fxaccounts.remote.force_auth.uri", "https://%(server)s/fxa-force-auth");
 user_pref("identity.fxaccounts.remote.signin.uri", "https://%(server)s/fxa-signin");
 user_pref("identity.fxaccounts.settings.uri", "https://%(server)s/fxa-settings");
+user_pref('identity.fxaccounts.remote.webchannel.uri', 'https://%(server)s/');
 
 // Enable logging of APZ test data (see bug 961289).
 user_pref('apz.test.logging_enabled', true);
 
 // Make sure SSL Error reports don't hit the network
 user_pref("security.ssl.errorReporting.url", "https://example.com/browser/browser/base/content/test/general/pinning_reports.sjs?succeed");
 
 // Make sure Translation won't hit the network.